From a08da0217d8e47459e68ee4717eb659f194587f9 Mon Sep 17 00:00:00 2001 From: Jason <17367223+jsoltys@users.noreply.github.com> Date: Fri, 29 May 2026 10:43:48 -0500 Subject: [PATCH] feat: Add Print Service Tray application with configuration and queue management - Implemented PrintQueueService with job counting and snapshot retrieval. - Created Form1 as the main entry point for the tray application. - Developed ConfigurationForm for managing printer configurations and document types. - Added models for printer configuration, queue status requests, and responses. - Established ConfigurationService for loading and saving printer configurations. - Introduced ServiceClient for IPC communication with the print service. - Built TrayApplicationContext for managing the system tray icon and status updates. - Added TODO.md for tracking remaining work and future enhancements. --- .gitignore | 3 + .vscode/tasks.json | 118 +++- PrintService.sln | 6 + PrintService/Services/IpcService.cs | 99 ++++ PrintService/Services/PrintQueueService.cs | 8 + PrintServiceTray/Form1.Designer.cs | 38 ++ PrintServiceTray/Form1.cs | 9 + PrintServiceTray/Forms/ConfigurationForm.cs | 515 ++++++++++++++++++ PrintServiceTray/Models/PrinterConfig.cs | 29 + PrintServiceTray/Models/QueueStatusRequest.cs | 6 + .../Models/QueueStatusResponse.cs | 19 + PrintServiceTray/PrintServiceTray.csproj | 16 + PrintServiceTray/Program.cs | 35 ++ .../Services/ConfigurationService.cs | 136 +++++ PrintServiceTray/Services/ServiceClient.cs | 62 +++ PrintServiceTray/TrayApplicationContext.cs | 206 +++++++ TODO.md | 240 ++++++++ 17 files changed, 1541 insertions(+), 4 deletions(-) create mode 100644 PrintServiceTray/Form1.Designer.cs create mode 100644 PrintServiceTray/Form1.cs create mode 100644 PrintServiceTray/Forms/ConfigurationForm.cs create mode 100644 PrintServiceTray/Models/PrinterConfig.cs create mode 100644 PrintServiceTray/Models/QueueStatusRequest.cs create mode 100644 PrintServiceTray/Models/QueueStatusResponse.cs create mode 100644 PrintServiceTray/PrintServiceTray.csproj create mode 100644 PrintServiceTray/Program.cs create mode 100644 PrintServiceTray/Services/ConfigurationService.cs create mode 100644 PrintServiceTray/Services/ServiceClient.cs create mode 100644 PrintServiceTray/TrayApplicationContext.cs create mode 100644 TODO.md diff --git a/.gitignore b/.gitignore index b794f3b..c720dee 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ Archive/ Error/ Output/ +# Rust Project (reference implementation) +RUST/ + # User-specific files *.suo *.user diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 643eacc..5cb12e4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -150,7 +150,8 @@ "label": "build-all-x86", "dependsOn": [ "build-service-release-x86", - "build-cli-release-x86" + "build-cli-release-x86", + "build-tray-release-x86" ], "dependsOrder": "parallel", "group": { @@ -163,7 +164,8 @@ "label": "build-all-x64", "dependsOn": [ "build-service-release-x64", - "build-cli-release-x64" + "build-cli-release-x64", + "build-tray-release-x64" ], "dependsOrder": "parallel", "group": "build", @@ -270,7 +272,8 @@ "label": "publish-all-x86", "dependsOn": [ "publish-service-x86-self-contained", - "publish-cli-x86-self-contained" + "publish-cli-x86-self-contained", + "publish-tray-x86-self-contained" ], "dependsOrder": "parallel", "group": "build", @@ -280,7 +283,8 @@ "label": "publish-all-x64", "dependsOn": [ "publish-service-x64-self-contained", - "publish-cli-x64-self-contained" + "publish-cli-x64-self-contained", + "publish-tray-x64-self-contained" ], "dependsOrder": "parallel", "group": "build", @@ -354,6 +358,112 @@ "group": "build" }, // ============================================ + // TRAY APP TASKS + // ============================================ + { + "label": "build-tray-debug", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Debug" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "build-tray-release-x86", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release", + "-r", + "win-x86" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "build-tray-release-x64", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "-c", + "Release", + "-r", + "win-x64" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "run-tray", + "command": "dotnet", + "type": "process", + "args": [ + "run", + "--project", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj" + ], + "problemMatcher": "$msCompile", + "group": "test" + }, + { + "label": "publish-tray-x86-self-contained", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj", + "-c", + "Release", + "-r", + "win-x86", + "--self-contained", + "true", + "/p:PublishSingleFile=true", + "/p:PublishTrimmed=false", + "-o", + "${workspaceFolder}/publish/x86" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "publish-tray-x64-self-contained", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/PrintServiceTray/PrintServiceTray.csproj", + "-c", + "Release", + "-r", + "win-x64", + "--self-contained", + "true", + "/p:PublishSingleFile=true", + "/p:PublishTrimmed=false", + "-o", + "${workspaceFolder}/publish/x64" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + // ============================================ // CLEAN TASKS // ============================================ { diff --git a/PrintService.sln b/PrintService.sln index 385961e..9ba5ffd 100644 --- a/PrintService.sln +++ b/PrintService.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintService", "PrintServic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceCLI", "PrintServiceCLI\PrintServiceCLI.csproj", "{799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintServiceTray", "PrintServiceTray\PrintServiceTray.csproj", "{42CF28FF-90E4-4912-B427-EA5F675F641E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Debug|Any CPU.Build.0 = Debug|Any CPU {799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.ActiveCfg = Release|Any CPU {799E32EC-0A62-4CA1-92A6-F7E53A2A7F73}.Release|Any CPU.Build.0 = Release|Any CPU + {42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42CF28FF-90E4-4912-B427-EA5F675F641E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42CF28FF-90E4-4912-B427-EA5F675F641E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/PrintService/Services/IpcService.cs b/PrintService/Services/IpcService.cs index 465c2fb..a6b5f7c 100644 --- a/PrintService/Services/IpcService.cs +++ b/PrintService/Services/IpcService.cs @@ -122,6 +122,17 @@ public class IpcService : IDisposable _logger.LogDebug("Received IPC message: {Message}", message); } + // Try to determine command type + var commandType = DetermineCommandType(message); + + if (commandType == "GetQueueStatus") + { + var status = GetQueueStatus(); + await SendQueueStatusResponse(pipeServer, status); + return; + } + + // Handle print command var command = JsonSerializer.Deserialize(message); if (command == null) { @@ -163,6 +174,70 @@ public class IpcService : IDisposable } } + /// + /// Determine the type of command from JSON message + /// + private string? DetermineCommandType(string message) + { + try + { + using var doc = JsonDocument.Parse(message); + if (doc.RootElement.TryGetProperty("Command", out var cmdProp)) + { + return cmdProp.GetString(); + } + } + catch { } + return null; + } + + /// + /// Get current queue status + /// + private QueueStatusResponse GetQueueStatus() + { + try + { + var jobs = _queueService.GetAllJobs(); + var jobStatuses = jobs.Select(j => new JobStatus + { + Id = j.Id.ToString(), + FileName = j.OriginalFilename, + DocumentType = j.DocumentType, + Status = j.Status.ToString(), + PrinterName = null, // Will be set when processing starts + CurrentPage = null, + TotalPages = null + }).ToList(); + + return new QueueStatusResponse + { + Success = true, + Jobs = jobStatuses + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get queue status"); + return new QueueStatusResponse + { + Success = false, + Message = $"Error: {ex.Message}" + }; + } + } + + /// + /// Send queue status response + /// + private async Task SendQueueStatusResponse(NamedPipeServerStream pipeServer, QueueStatusResponse response) + { + var json = JsonSerializer.Serialize(response); + var bytes = Encoding.UTF8.GetBytes(json); + await pipeServer.WriteAsync(bytes, 0, bytes.Length); + await pipeServer.FlushAsync(); + } + /// /// Process a print command /// @@ -239,3 +314,27 @@ public class PrintResponse public bool Success { get; set; } public string Message { get; set; } = string.Empty; } + +/// +/// Queue status response for tray app +/// +public class QueueStatusResponse +{ + public bool Success { get; set; } + public string? Message { get; set; } + public List Jobs { get; set; } = new(); +} + +/// +/// Status of a single job +/// +public class JobStatus +{ + public string Id { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string DocumentType { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string? PrinterName { get; set; } + public int? CurrentPage { get; set; } + public int? TotalPages { get; set; } +} diff --git a/PrintService/Services/PrintQueueService.cs b/PrintService/Services/PrintQueueService.cs index 3a7746e..231afa3 100644 --- a/PrintService/Services/PrintQueueService.cs +++ b/PrintService/Services/PrintQueueService.cs @@ -59,6 +59,14 @@ public class PrintQueueService /// public int Count => _queue.Count; + /// + /// Get all jobs in the queue (snapshot) + /// + public PrintJob[] GetAllJobs() + { + return _queue.ToArray(); + } + /// /// Save queue state to disk /// diff --git a/PrintServiceTray/Form1.Designer.cs b/PrintServiceTray/Form1.Designer.cs new file mode 100644 index 0000000..6e92c02 --- /dev/null +++ b/PrintServiceTray/Form1.Designer.cs @@ -0,0 +1,38 @@ +namespace PrintServiceTray; + +partial class Form1 +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Text = "Form1"; + } + + #endregion +} diff --git a/PrintServiceTray/Form1.cs b/PrintServiceTray/Form1.cs new file mode 100644 index 0000000..1081e77 --- /dev/null +++ b/PrintServiceTray/Form1.cs @@ -0,0 +1,9 @@ +namespace PrintServiceTray; + +public partial class Form1 : Form +{ + public Form1() + { + InitializeComponent(); + } +} diff --git a/PrintServiceTray/Forms/ConfigurationForm.cs b/PrintServiceTray/Forms/ConfigurationForm.cs new file mode 100644 index 0000000..f8372ed --- /dev/null +++ b/PrintServiceTray/Forms/ConfigurationForm.cs @@ -0,0 +1,515 @@ +using PrintServiceTray.Models; +using PrintServiceTray.Services; + +namespace PrintServiceTray.Forms; + +public partial class ConfigurationForm : Form +{ + private readonly ConfigurationService _configService; + private PrinterConfiguration _config; + private string? _selectedDocType; + private List _installedPrinters; + + // UI Controls + private ListBox _docTypeListBox = null!; + private Button _newDocTypeButton = null!; + private Button _deleteDocTypeButton = null!; + private Panel _pageConfigPanel = null!; + private RadioButton _saveGlobalRadio = null!; + private RadioButton _saveLocalRadio = null!; + private Button _saveButton = null!; + private Button _cancelButton = null!; + private Button _addPageButton = null!; + private Button _removePageButton = null!; + private Label _configLabel = null!; + + private List _pageControls = new(); + + public ConfigurationForm() + { + _configService = new ConfigurationService(); + _config = _configService.Load(); + _installedPrinters = _configService.GetInstalledPrinters(); + + InitializeComponent(); + LoadDocumentTypes(); + } + + private void InitializeComponent() + { + Text = "LAAPC Printer Configuration"; + Size = new Size(900, 600); + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + + // Left panel - Document Types + var leftPanel = new Panel + { + Location = new Point(10, 10), + Size = new Size(250, 540), + BorderStyle = BorderStyle.FixedSingle + }; + + var docTypeLabel = new Label + { + Text = "Document Types:", + Location = new Point(10, 10), + Size = new Size(230, 20), + Font = new Font(Font, FontStyle.Bold) + }; + + _docTypeListBox = new ListBox + { + Location = new Point(10, 35), + Size = new Size(230, 430) + }; + _docTypeListBox.SelectedIndexChanged += DocTypeListBox_SelectedIndexChanged; + + _newDocTypeButton = new Button + { + Text = "New", + Location = new Point(10, 475), + Size = new Size(110, 30) + }; + _newDocTypeButton.Click += NewDocType_Click; + + _deleteDocTypeButton = new Button + { + Text = "Delete", + Location = new Point(130, 475), + Size = new Size(110, 30) + }; + _deleteDocTypeButton.Click += DeleteDocType_Click; + + leftPanel.Controls.AddRange(new Control[] { + docTypeLabel, _docTypeListBox, _newDocTypeButton, _deleteDocTypeButton + }); + + // Right panel - Page Configuration + var rightPanel = new Panel + { + Location = new Point(270, 10), + Size = new Size(610, 540), + BorderStyle = BorderStyle.FixedSingle + }; + + _configLabel = new Label + { + Text = "Configuration for: (Select a document type)", + Location = new Point(10, 10), + Size = new Size(590, 20), + Font = new Font(Font, FontStyle.Bold) + }; + + var pageConfigLabel = new Label + { + Text = "Page Configuration:", + Location = new Point(10, 40), + Size = new Size(590, 20) + }; + + _pageConfigPanel = new Panel + { + Location = new Point(10, 65), + Size = new Size(590, 350), + AutoScroll = true, + BorderStyle = BorderStyle.FixedSingle + }; + + _addPageButton = new Button + { + Text = "+ Add Page", + Location = new Point(10, 425), + Size = new Size(120, 30) + }; + _addPageButton.Click += AddPage_Click; + + _removePageButton = new Button + { + Text = "- Remove Last", + Location = new Point(140, 425), + Size = new Size(120, 30) + }; + _removePageButton.Click += RemovePage_Click; + + // Save options + var saveToLabel = new Label + { + Text = "Save to:", + Location = new Point(10, 465), + Size = new Size(60, 20) + }; + + _saveGlobalRadio = new RadioButton + { + Text = "Global (All Users)", + Location = new Point(80, 465), + Size = new Size(150, 20) + }; + + _saveLocalRadio = new RadioButton + { + Text = "Local (My Computer)", + Location = new Point(240, 465), + Size = new Size(170, 20), + Checked = true + }; + + _saveButton = new Button + { + Text = "Save", + Location = new Point(430, 495), + Size = new Size(80, 35) + }; + _saveButton.Click += Save_Click; + + _cancelButton = new Button + { + Text = "Cancel", + Location = new Point(520, 495), + Size = new Size(80, 35) + }; + _cancelButton.Click += (s, e) => Close(); + + rightPanel.Controls.AddRange(new Control[] { + _configLabel, pageConfigLabel, _pageConfigPanel, + _addPageButton, _removePageButton, + saveToLabel, _saveGlobalRadio, _saveLocalRadio, + _saveButton, _cancelButton + }); + + Controls.AddRange(new Control[] { leftPanel, rightPanel }); + } + + private void LoadDocumentTypes() + { + _docTypeListBox.Items.Clear(); + foreach (var docType in _config.DocumentTypes.Keys.OrderBy(k => k)) + { + _docTypeListBox.Items.Add(docType); + } + } + + private void DocTypeListBox_SelectedIndexChanged(object? sender, EventArgs e) + { + if (_docTypeListBox.SelectedItem is string docType) + { + _selectedDocType = docType; + LoadPageConfiguration(docType); + } + } + + private void LoadPageConfiguration(string docType) + { + _configLabel.Text = $"Configuration for: {docType}"; + _pageConfigPanel.Controls.Clear(); + _pageControls.Clear(); + + if (!_config.DocumentTypes.TryGetValue(docType, out var config)) + { + config = new DocumentTypeConfig { Name = docType }; + _config.DocumentTypes[docType] = config; + } + + int yPos = 10; + foreach (var page in config.Pages.OrderBy(p => p.PageNumber)) + { + var pageControl = new PageConfigControl(page.PageNumber, _installedPrinters, _configService); + pageControl.Location = new Point(10, yPos); + pageControl.LoadPage(page); + _pageConfigPanel.Controls.Add(pageControl); + _pageControls.Add(pageControl); + yPos += pageControl.Height + 10; + } + + if (config.Pages.Count == 0) + { + // Add first page by default + AddPage_Click(null, EventArgs.Empty); + } + } + + private void AddPage_Click(object? sender, EventArgs e) + { + if (_selectedDocType == null) return; + + var config = _config.DocumentTypes[_selectedDocType]; + int nextPageNum = config.Pages.Count > 0 ? config.Pages.Max(p => p.PageNumber) + 1 : 1; + + var newPage = new PageConfig + { + PageNumber = nextPageNum, + PrinterName = _installedPrinters.FirstOrDefault() ?? "", + TrayNumber = 1 + }; + config.Pages.Add(newPage); + + LoadPageConfiguration(_selectedDocType); + } + + private void RemovePage_Click(object? sender, EventArgs e) + { + if (_selectedDocType == null) return; + + var config = _config.DocumentTypes[_selectedDocType]; + if (config.Pages.Count > 0) + { + var lastPage = config.Pages.OrderByDescending(p => p.PageNumber).First(); + config.Pages.Remove(lastPage); + LoadPageConfiguration(_selectedDocType); + } + } + + private void NewDocType_Click(object? sender, EventArgs e) + { + var dialog = new TextInputDialog("New Document Type", "Enter document type name:"); + if (dialog.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.InputText)) + { + var docType = dialog.InputText.Trim(); + if (!_config.DocumentTypes.ContainsKey(docType)) + { + _config.DocumentTypes[docType] = new DocumentTypeConfig { Name = docType }; + LoadDocumentTypes(); + _docTypeListBox.SelectedItem = docType; + } + else + { + MessageBox.Show($"Document type '{docType}' already exists.", "Duplicate", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + } + + private void DeleteDocType_Click(object? sender, EventArgs e) + { + if (_selectedDocType == null) return; + + var result = MessageBox.Show( + $"Are you sure you want to delete '{_selectedDocType}'?", + "Confirm Delete", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + _config.DocumentTypes.Remove(_selectedDocType); + _selectedDocType = null; + LoadDocumentTypes(); + _pageConfigPanel.Controls.Clear(); + _configLabel.Text = "Configuration for: (Select a document type)"; + } + } + + private void Save_Click(object? sender, EventArgs e) + { + try + { + // Save current page configurations + if (_selectedDocType != null && _config.DocumentTypes.TryGetValue(_selectedDocType, out var config)) + { + config.Pages.Clear(); + foreach (var pageControl in _pageControls) + { + config.Pages.Add(pageControl.GetPageConfig()); + } + } + + bool saveGlobal = _saveGlobalRadio.Checked; + _configService.Save(_config, saveGlobal); + + MessageBox.Show( + $"Configuration saved to {(saveGlobal ? "global" : "local")} file successfully!", + "Success", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + DialogResult = DialogResult.OK; + Close(); + } + catch (Exception ex) + { + MessageBox.Show( + $"Failed to save configuration: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } +} + +// Custom control for each page configuration +public class PageConfigControl : UserControl +{ + private readonly int _pageNumber; + private readonly List _printers; + private readonly ConfigurationService _configService; + + private ComboBox _printerCombo = null!; + private ComboBox _trayCombo = null!; + + public PageConfigControl(int pageNumber, List printers, ConfigurationService configService) + { + _pageNumber = pageNumber; + _printers = printers; + _configService = configService; + + InitializeComponent(); + } + + private void InitializeComponent() + { + Size = new Size(560, 60); + BorderStyle = BorderStyle.FixedSingle; + + var pageLabel = new Label + { + Text = $"Page {_pageNumber}:", + Location = new Point(10, 10), + Size = new Size(70, 20), + Font = new Font(Font, FontStyle.Bold) + }; + + var printerLabel = new Label + { + Text = "Printer:", + Location = new Point(10, 32), + Size = new Size(60, 20) + }; + + _printerCombo = new ComboBox + { + Location = new Point(75, 30), + Size = new Size(250, 25), + DropDownStyle = ComboBoxStyle.DropDownList + }; + _printerCombo.Items.AddRange(_printers.ToArray()); + _printerCombo.SelectedIndexChanged += PrinterCombo_SelectedIndexChanged; + + var trayLabel = new Label + { + Text = "Tray:", + Location = new Point(340, 32), + Size = new Size(40, 20) + }; + + _trayCombo = new ComboBox + { + Location = new Point(385, 30), + Size = new Size(165, 25), + DropDownStyle = ComboBoxStyle.DropDownList + }; + + Controls.AddRange(new Control[] { + pageLabel, printerLabel, _printerCombo, trayLabel, _trayCombo + }); + } + + public void LoadPage(PageConfig page) + { + _printerCombo.SelectedItem = page.PrinterName; + LoadTrays(page.PrinterName); + + // Try to find and select the tray + for (int i = 0; i < _trayCombo.Items.Count; i++) + { + if (_trayCombo.Items[i] is TrayItem item && item.TrayNumber == page.TrayNumber) + { + _trayCombo.SelectedIndex = i; + break; + } + } + } + + private void PrinterCombo_SelectedIndexChanged(object? sender, EventArgs e) + { + if (_printerCombo.SelectedItem is string printer) + { + LoadTrays(printer); + } + } + + private void LoadTrays(string printerName) + { + _trayCombo.Items.Clear(); + var trays = _configService.GetPrinterTrays(printerName); + foreach (var (number, name) in trays) + { + _trayCombo.Items.Add(new TrayItem { TrayNumber = number, TrayName = name }); + } + if (_trayCombo.Items.Count > 0) + { + _trayCombo.SelectedIndex = 0; + } + } + + public PageConfig GetPageConfig() + { + var selectedTray = _trayCombo.SelectedItem as TrayItem; + return new PageConfig + { + PageNumber = _pageNumber, + PrinterName = _printerCombo.SelectedItem?.ToString() ?? "", + TrayNumber = selectedTray?.TrayNumber ?? 1, + TrayLabel = selectedTray?.TrayName + }; + } + + private class TrayItem + { + public int TrayNumber { get; set; } + public string TrayName { get; set; } = ""; + + public override string ToString() => $"Tray {TrayNumber} - {TrayName}"; + } +} + +// Simple text input dialog +public class TextInputDialog : Form +{ + private TextBox _textBox = null!; + public string InputText => _textBox.Text; + + public TextInputDialog(string title, string prompt) + { + Text = title; + Size = new Size(400, 150); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + + var promptLabel = new Label + { + Text = prompt, + Location = new Point(20, 20), + Size = new Size(360, 20) + }; + + _textBox = new TextBox + { + Location = new Point(20, 50), + Size = new Size(360, 25) + }; + + var okButton = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + Location = new Point(220, 85), + Size = new Size(75, 30) + }; + + var cancelButton = new Button + { + Text = "Cancel", + DialogResult = DialogResult.Cancel, + Location = new Point(305, 85), + Size = new Size(75, 30) + }; + + AcceptButton = okButton; + CancelButton = cancelButton; + + Controls.AddRange(new Control[] { promptLabel, _textBox, okButton, cancelButton }); + } +} diff --git a/PrintServiceTray/Models/PrinterConfig.cs b/PrintServiceTray/Models/PrinterConfig.cs new file mode 100644 index 0000000..01839e5 --- /dev/null +++ b/PrintServiceTray/Models/PrinterConfig.cs @@ -0,0 +1,29 @@ +namespace PrintServiceTray.Models; + +/// +/// Configuration for all document types +/// +public class PrinterConfiguration +{ + public Dictionary DocumentTypes { get; set; } = new(); +} + +/// +/// Configuration for a single document type +/// +public class DocumentTypeConfig +{ + public string Name { get; set; } = string.Empty; + public List Pages { get; set; } = new(); +} + +/// +/// Configuration for a single page +/// +public class PageConfig +{ + public int PageNumber { get; set; } + public string PrinterName { get; set; } = string.Empty; + public int TrayNumber { get; set; } + public string? TrayLabel { get; set; } // Optional: "Pink Paper", "Green Paper", etc. +} diff --git a/PrintServiceTray/Models/QueueStatusRequest.cs b/PrintServiceTray/Models/QueueStatusRequest.cs new file mode 100644 index 0000000..4782883 --- /dev/null +++ b/PrintServiceTray/Models/QueueStatusRequest.cs @@ -0,0 +1,6 @@ +namespace PrintServiceTray.Models; + +public class QueueStatusRequest +{ + public string Command { get; set; } = "GetQueueStatus"; +} diff --git a/PrintServiceTray/Models/QueueStatusResponse.cs b/PrintServiceTray/Models/QueueStatusResponse.cs new file mode 100644 index 0000000..754256a --- /dev/null +++ b/PrintServiceTray/Models/QueueStatusResponse.cs @@ -0,0 +1,19 @@ +namespace PrintServiceTray.Models; + +public class QueueStatusResponse +{ + public bool Success { get; set; } + public string? Message { get; set; } + public List Jobs { get; set; } = new(); +} + +public class JobStatus +{ + public string Id { get; set; } = string.Empty; + public string FileName { get; set; } = string.Empty; + public string DocumentType { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string? PrinterName { get; set; } + public int? CurrentPage { get; set; } + public int? TotalPages { get; set; } +} diff --git a/PrintServiceTray/PrintServiceTray.csproj b/PrintServiceTray/PrintServiceTray.csproj new file mode 100644 index 0000000..f94356e --- /dev/null +++ b/PrintServiceTray/PrintServiceTray.csproj @@ -0,0 +1,16 @@ + + + + WinExe + net7.0-windows + enable + true + enable + true + + + + + + + \ No newline at end of file diff --git a/PrintServiceTray/Program.cs b/PrintServiceTray/Program.cs new file mode 100644 index 0000000..13ca86a --- /dev/null +++ b/PrintServiceTray/Program.cs @@ -0,0 +1,35 @@ +namespace PrintServiceTray; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + try + { + // Prevent multiple instances + bool createdNew; + using var mutex = new Mutex(true, "LAAPC_PrintServiceTray_Mutex", out createdNew); + + if (!createdNew) + { + MessageBox.Show("LAAPC Print Service Tray is already running.", "Already Running", + MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + ApplicationConfiguration.Initialize(); + Application.Run(new TrayApplicationContext()); + + GC.KeepAlive(mutex); + } + catch (Exception ex) + { + MessageBox.Show($"Fatal error: {ex.Message}\n\n{ex.StackTrace}", "Error", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } +} \ No newline at end of file diff --git a/PrintServiceTray/Services/ConfigurationService.cs b/PrintServiceTray/Services/ConfigurationService.cs new file mode 100644 index 0000000..c7b51fc --- /dev/null +++ b/PrintServiceTray/Services/ConfigurationService.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using PrintServiceTray.Models; + +namespace PrintServiceTray.Services; + +public class ConfigurationService +{ + private readonly string _globalConfigPath; + private readonly string _localConfigPath; + + public ConfigurationService() + { + // Global config: next to CLI executable (C:\LAAPC or wherever installed) + var cliPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "LAAPC" + ); + _globalConfigPath = Path.Combine(cliPath, "printer-config.json"); + + // Local config: user's AppData + var appDataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LAAPC" + ); + Directory.CreateDirectory(appDataPath); + _localConfigPath = Path.Combine(appDataPath, "printer-config.json"); + } + + /// + /// Load configuration (local overrides global) + /// + public PrinterConfiguration Load() + { + var config = new PrinterConfiguration(); + + // Load global first + if (File.Exists(_globalConfigPath)) + { + try + { + var json = File.ReadAllText(_globalConfigPath); + var globalConfig = JsonSerializer.Deserialize(json); + if (globalConfig != null) + { + config = globalConfig; + } + } + catch { } + } + + // Load local and merge/override + if (File.Exists(_localConfigPath)) + { + try + { + var json = File.ReadAllText(_localConfigPath); + var localConfig = JsonSerializer.Deserialize(json); + if (localConfig != null) + { + // Merge: local overrides global for matching document types + foreach (var kvp in localConfig.DocumentTypes) + { + config.DocumentTypes[kvp.Key] = kvp.Value; + } + } + } + catch { } + } + + return config; + } + + /// + /// Save configuration to global or local + /// + public void Save(PrinterConfiguration config, bool saveGlobal) + { + var path = saveGlobal ? _globalConfigPath : _localConfigPath; + + // Ensure directory exists + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + var options = new JsonSerializerOptions + { + WriteIndented = true + }; + var json = JsonSerializer.Serialize(config, options); + File.WriteAllText(path, json); + } + + /// + /// Get all installed printers + /// + public List GetInstalledPrinters() + { + var printers = new List(); + try + { + foreach (string printerName in System.Drawing.Printing.PrinterSettings.InstalledPrinters) + { + printers.Add(printerName); + } + } + catch { } + return printers; + } + + /// + /// Get available trays for a printer + /// + public List<(int Number, string Name)> GetPrinterTrays(string printerName) + { + var trays = new List<(int, string)>(); + try + { + var printerSettings = new System.Drawing.Printing.PrinterSettings + { + PrinterName = printerName + }; + + foreach (System.Drawing.Printing.PaperSource source in printerSettings.PaperSources) + { + trays.Add((source.RawKind, source.SourceName)); + } + } + catch { } + return trays; + } + + public string GlobalConfigPath => _globalConfigPath; + public string LocalConfigPath => _localConfigPath; +} diff --git a/PrintServiceTray/Services/ServiceClient.cs b/PrintServiceTray/Services/ServiceClient.cs new file mode 100644 index 0000000..3caf95a --- /dev/null +++ b/PrintServiceTray/Services/ServiceClient.cs @@ -0,0 +1,62 @@ +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using PrintServiceTray.Models; + +namespace PrintServiceTray.Services; + +public class ServiceClient +{ + private readonly string _pipeName; + private readonly int _timeout; + + public ServiceClient(string pipeName = "PrintServicePipe", int timeoutMs = 5000) + { + _pipeName = pipeName; + _timeout = timeoutMs; + } + + public async Task GetQueueStatusAsync() + { + try + { + using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + await pipe.ConnectAsync(_timeout); + + var request = new QueueStatusRequest(); + var requestJson = JsonSerializer.Serialize(request); + var requestBytes = Encoding.UTF8.GetBytes(requestJson); + + await pipe.WriteAsync(requestBytes); + await pipe.FlushAsync(); + + var buffer = new byte[4096]; + var bytesRead = await pipe.ReadAsync(buffer); + var responseJson = Encoding.UTF8.GetString(buffer, 0, bytesRead); + + var response = JsonSerializer.Deserialize(responseJson); + return response ?? new QueueStatusResponse { Success = false, Message = "Invalid response" }; + } + catch (TimeoutException) + { + return new QueueStatusResponse { Success = false, Message = "Service not responding" }; + } + catch (Exception ex) + { + return new QueueStatusResponse { Success = false, Message = $"Error: {ex.Message}" }; + } + } + + public async Task IsServiceRunningAsync() + { + try + { + var response = await GetQueueStatusAsync(); + return response.Success; + } + catch + { + return false; + } + } +} diff --git a/PrintServiceTray/TrayApplicationContext.cs b/PrintServiceTray/TrayApplicationContext.cs new file mode 100644 index 0000000..075144e --- /dev/null +++ b/PrintServiceTray/TrayApplicationContext.cs @@ -0,0 +1,206 @@ +using System.Diagnostics; +using PrintServiceTray.Services; +using PrintServiceTray.Models; + +namespace PrintServiceTray; + +public class TrayApplicationContext : ApplicationContext +{ + private readonly NotifyIcon _trayIcon; + private readonly ServiceClient _serviceClient; + private readonly System.Windows.Forms.Timer _refreshTimer; + private readonly ToolStripMenuItem _queueStatusItem; + private readonly ToolStripMenuItem _serviceStatusItem; + + public TrayApplicationContext() + { + try + { + _serviceClient = new ServiceClient(); + + // Create context menu + var contextMenu = new ContextMenuStrip(); + + // Service status (at top) + _serviceStatusItem = new ToolStripMenuItem("● Service Running") + { + Enabled = false, + Font = new Font(contextMenu.Font, FontStyle.Bold) + }; + contextMenu.Items.Add(_serviceStatusItem); + contextMenu.Items.Add(new ToolStripSeparator()); + + // Queue status section + _queueStatusItem = new ToolStripMenuItem("Queue Status") + { + Enabled = false + }; + contextMenu.Items.Add(_queueStatusItem); + + var viewQueueItem = new ToolStripMenuItem("View Full Queue..."); + viewQueueItem.Click += ViewQueue_Click; + contextMenu.Items.Add(viewQueueItem); + + contextMenu.Items.Add(new ToolStripSeparator()); + + // Configuration + var configureItem = new ToolStripMenuItem("Configure Printers..."); + configureItem.Click += Configure_Click; + contextMenu.Items.Add(configureItem); + + contextMenu.Items.Add(new ToolStripSeparator()); + + // Exit + var exitItem = new ToolStripMenuItem("Exit"); + exitItem.Click += Exit_Click; + contextMenu.Items.Add(exitItem); + + // Create tray icon + _trayIcon = new NotifyIcon + { + Icon = SystemIcons.Information, + ContextMenuStrip = contextMenu, + Visible = true, + Text = "LAAPC Print Service" + }; + + _trayIcon.DoubleClick += TrayIcon_DoubleClick; + + // Show startup notification + _trayIcon.ShowBalloonTip(2000, "LAAPC Print Service", "Tray app started", ToolTipIcon.Info); + + // Set up refresh timer (every 2 seconds) + _refreshTimer = new System.Windows.Forms.Timer + { + Interval = 2000 + }; + _refreshTimer.Tick += RefreshTimer_Tick; + _refreshTimer.Start(); + + // Initial refresh + _ = RefreshStatusAsync(); + } + catch (Exception ex) + { + MessageBox.Show($"Failed to initialize tray app: {ex.Message}\n\n{ex.StackTrace}", + "Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + throw; + } + } + + private async void RefreshTimer_Tick(object? sender, EventArgs e) + { + await RefreshStatusAsync(); + } + + private async Task RefreshStatusAsync() + { + try + { + var status = await _serviceClient.GetQueueStatusAsync(); + + if (status.Success) + { + _serviceStatusItem.Text = "● Service Running"; + _serviceStatusItem.ForeColor = Color.Green; + + // Update queue status + if (status.Jobs.Count == 0) + { + _queueStatusItem.Text = "Queue Status: Empty"; + _queueStatusItem.DropDownItems.Clear(); + } + else + { + _queueStatusItem.Text = $"Queue Status ({status.Jobs.Count} {(status.Jobs.Count == 1 ? "job" : "jobs")})"; + _queueStatusItem.Enabled = true; + + // Update dropdown items with job list + _queueStatusItem.DropDownItems.Clear(); + + foreach (var job in status.Jobs.Take(10)) // Show max 10 in menu + { + var statusIcon = job.Status == "Processing" ? "●" : "○"; + var jobText = $"{statusIcon} {job.Status}: {job.FileName}"; + + if (job.PrinterName != null) + { + jobText += $" ({job.PrinterName})"; + } + + if (job.CurrentPage.HasValue && job.TotalPages.HasValue) + { + jobText += $" - Page {job.CurrentPage}/{job.TotalPages}"; + } + + var jobItem = new ToolStripMenuItem(jobText) { Enabled = false }; + _queueStatusItem.DropDownItems.Add(jobItem); + } + + if (status.Jobs.Count > 10) + { + _queueStatusItem.DropDownItems.Add(new ToolStripSeparator()); + var moreItem = new ToolStripMenuItem($"... and {status.Jobs.Count - 10} more") + { + Enabled = false + }; + _queueStatusItem.DropDownItems.Add(moreItem); + } + } + + // Update tooltip + _trayIcon.Text = status.Jobs.Count == 0 + ? "LAAPC Print Service - Queue empty" + : $"LAAPC Print Service - {status.Jobs.Count} job(s) in queue"; + } + else + { + _serviceStatusItem.Text = "○ Service Not Running"; + _serviceStatusItem.ForeColor = Color.Red; + _queueStatusItem.Text = "Queue Status: Unavailable"; + _queueStatusItem.DropDownItems.Clear(); + _trayIcon.Text = "LAAPC Print Service - Not running"; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error refreshing status: {ex.Message}"); + } + } + + private void TrayIcon_DoubleClick(object? sender, EventArgs e) + { + // Double-click opens full queue view + ViewQueue_Click(sender, e); + } + + private void ViewQueue_Click(object? sender, EventArgs e) + { + // TODO: Open queue view window + MessageBox.Show("Queue view window coming soon!", "LAAPC Print Service", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void Configure_Click(object? sender, EventArgs e) + { + var configForm = new Forms.ConfigurationForm(); + configForm.ShowDialog(); + } + + private void Exit_Click(object? sender, EventArgs e) + { + _refreshTimer.Stop(); + _trayIcon.Visible = false; + Application.Exit(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _refreshTimer?.Stop(); + _refreshTimer?.Dispose(); + _trayIcon?.Dispose(); + } + base.Dispose(disposing); + } +} diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7549993 --- /dev/null +++ b/TODO.md @@ -0,0 +1,240 @@ +# LAAPC Print Service - Remaining Work + +## ✅ Completed +- [x] Core service architecture (queue, IPC, file monitoring) +- [x] CLI with self-installation capability +- [x] Basic printing with tray control +- [x] PDF output for testing +- [x] System tray application with status display +- [x] Configuration dialog for printer/tray assignments +- [x] Fixed System.Drawing.Common platform compatibility + +## 🅿️ Parking Lot - Configuration Dialog Issues + +### Known Issues (Non-Critical) +- [ ] **Printer resets on add page:** When adding a new page to a document type, the printer dropdown resets to the first printer in the list if the configuration hasn't been saved yet + - Expected: Should remember previous page's printer selection or default intelligently + - Workaround: Save after configuring each page + +- [ ] **Save closes dialog:** Clicking Save closes the configuration dialog, forcing users to reopen it to continue editing + - Expected: Save button should persist changes but keep dialog open + - Suggested: Add "Save & Close" and "Save" as separate buttons, or just make Save not close the dialog + +## 🔲 Immediate Testing + +### Test Configuration Dialog +- [ ] Right-click tray icon → "Configure Printers..." +- [ ] Create new document type +- [ ] Add pages with printer/tray assignments +- [ ] Save configuration (test both local and global) +- [ ] Verify printer-config.json file created correctly +- [ ] Test loading existing configuration + +## 🔨 High Priority - Service Integration + +### 1. Integrate printer-config.json into PrintService +**Goal:** Service should read printer-config.json instead of appsettings.json + +**Files to modify:** +- `PrintService/Services/ConfigurationService.cs` (CREATE NEW) + - Load printer-config.json from C:\ProgramData\LAAPC\ (global) and %LOCALAPPDATA%\LAAPC\ (local) + - Merge local over global (same logic as tray app) + - Return `PrinterConfiguration` object + +- `PrintService/Models/PrinterConfig.cs` (COPY from PrintServiceTray) + - Copy the new config models into PrintService project + +- `PrintService/Worker.cs` (MODIFY) + - Replace DocumentConfig loading with PrinterConfiguration loading + - Pass PageConfig[] to DocumentProcessor + +- `PrintService/Services/DocumentProcessor.cs` (MODIFY - or DELETE if not needed) + - Update to work with new PageConfig format + - May need to refactor logic + +- `PrintService/Services/PrinterService.cs` (MODIFY) + - Update Print() method signature to accept PageConfig[] + - Print each page to correct printer/tray based on PageConfig + +**Notes:** +- Keep appsettings.json for service-level settings (queue path, retry counts, etc.) +- printer-config.json ONLY for document type → printer/tray mappings +- Support missing config gracefully (log warning, skip job) + +## 🔨 High Priority - Page Duplication Pattern + +### 2. Implement Multi-Tray Page Duplication +**User Requirement:** Print EACH original page to ALL trays in sequence + +**Current Behavior:** +``` +TraySequence: [3,4,1,2] +Original document: 4 pages +Output: 4 pages (page 1→tray3, page 2→tray4, page 3→tray1, page 4→tray2) +``` + +**New Behavior:** +``` +TraySequence: [3,4,1,2] +Original document: 4 pages +Output: 16 pages + Page 1 → Tray 3 + Page 1 → Tray 4 + Page 1 → Tray 1 + Page 1 → Tray 2 + Page 2 → Tray 3 + Page 2 → Tray 4 + Page 2 → Tray 1 + Page 2 → Tray 2 + ... (and so on) +``` + +**Files to modify:** +- `PrintService/Services/PrinterService.cs` + - Modify PrintPage event handler + - Add page duplication logic + - Track: originalPageIndex, trayIndex, currentOutputPage + - For each original page, cycle through all trays before moving to next page + +**Algorithm:** +```csharp +int originalPageIndex = 0; +int trayIndex = 0; +int totalOriginalPages = CalculateTotalPages(lines); +bool hasMorePages = true; + +PrintPage event: + 1. Render lines for originalPageIndex + 2. Set tray to pageConfigs[originalPageIndex].TrayNumber + 3. Increment trayIndex + 4. If trayIndex >= trays.Length: + trayIndex = 0 + originalPageIndex++ + 5. hasMorePages = (originalPageIndex < totalOriginalPages) +``` + +## 🔨 Medium Priority - Content Modifications + +### 3. Port Rust Content Transformation Logic +**Reference:** `RUST/cgwprint/src/background.rs` and `RUST/cgwprint/data/printers.json` + +**DO NOT START UNTIL:** Tray config and page duplication are complete and tested + +**New config fields needed in PageConfig:** +```json +{ + "PageNumber": 1, + "PrinterName": "Brother HL-L6415DW", + "TrayNumber": 3, + "TrayLabel": "Pink", + + // NEW FIELDS: + "RemoveOrderNumber": false, + "RemoveOrderNumberLabel": false, + "IndentOrderNumber": 0, + "RowShift": { + "5": -2, // Move line 5 up by 2 positions + "12": 1 // Move line 12 down by 1 position + }, + "RowTrim": { + "3": { "Start": 0, "End": 40 }, // Keep only first 40 chars of line 3 + "7": { "Start": 10, "End": 50 } // Keep chars 10-50 of line 7 + }, + "BoldFont": "Courier New Bold", + "NormalFont": "Courier New" +} +``` + +**Processing steps:** +1. Parse hex escapes (\xHH → bytes) +2. Split on form feed (\x0C) into pages +3. Font replacement (ESC+w+1 → bold, ESC+w+0 → normal) +4. Order number manipulation (remove/indent based on config) +5. Apply row_shift (vertical position adjustments) +6. Apply row_trim (substring extraction) +7. Add PCL/PJL header/footer wrapping + +**Files to modify:** +- `PrintService/Models/PageConfig.cs` - Add new properties +- `PrintService/Services/DocumentProcessor.cs` - Add content transformation +- `PrintService/Services/ContentTransformer.cs` (CREATE NEW) + - ParseHexEscapes() + - SplitPages() + - ProcessFontCodes() + - ManipulateOrderNumber() + - ApplyRowShift() + - ApplyRowTrim() + - WrapWithPclHeaders() + +**Location-specific variations:** +- "there are 3 locations where this app is used and each has a slight different layout" +- Use document type naming to differentiate: "Invoice_Location1", "Invoice_Location2", etc. +- Or add "Location" field to config and filter by it + +## 📋 Testing Checklist + +### Before Production Deployment +- [ ] Test service installation on clean 32-bit Windows 10 machine +- [ ] Test CLI from Harbor/Clipper integration +- [ ] Test rapid concurrent print requests (race condition prevention) +- [ ] Test multi-tray printing on Brother HL-L6415DW +- [ ] Test configuration changes without service restart +- [ ] Test queue persistence (stop service mid-job, restart, verify resume) +- [ ] Test error handling (printer offline, invalid tray, etc.) +- [ ] Test tray app startup on Windows boot (add to Startup folder?) +- [ ] Verify file cleanup after successful print +- [ ] Verify error folder gets populated on failures + +## 🎯 Future Enhancements (Low Priority) +- [ ] Add logging to file for tray app (currently only service logs) +- [ ] Add "Start/Stop Service" option in tray menu +- [ ] Add "View Logs" option in tray menu +- [ ] Add notification sound for completed jobs +- [ ] Add job history view (last 50 jobs) +- [ ] Support per-page margins configuration +- [ ] Support custom font sizes per document type +- [ ] Add web UI for remote monitoring + +## 📝 Important Notes + +### Platform Compatibility +- **System.Drawing.Common:** Requires RuntimeHostConfigurationOption in .csproj +- **Remove explicit package reference** to avoid startup errors +- WinForms apps automatically include System.Drawing on Windows + +### Configuration Locations +- **Global:** `C:\ProgramData\LAAPC\printer-config.json` (shared, requires admin) +- **Local:** `%LOCALAPPDATA%\LAAPC\printer-config.json` (per-user, no admin) +- **Merge strategy:** Local settings override global settings by document type + +### Print Service Config +- Service currently uses: `appsettings.json` (old format with DocumentConfig) +- Service needs to migrate to: `printer-config.json` (new format with PageConfig[]) +- Keep `appsettings.json` for non-printer settings (QueuePath, MaxRetries, etc.) + +### Page Duplication Logic +- Current: One page per tray (simple mapping) +- Required: All pages to all trays (page duplication with tray cycling) +- Example: 4-page invoice × 5 trays = 20 physical pages printed +- Reason: Simulates carbon copy paper (each page needs copy in each color) + +### Content Modifications +- Do LAST (most complex, least critical for initial deployment) +- Three locations have slightly different requirements +- May need per-location configuration or document type naming convention +- PCL/PJL wrapping may only apply to specific printers + +### Git Status +- Last commit: cce9248 +- `RUST/` folder excluded from version control +- Remember to commit after each major milestone + +## 🚀 Next Steps (In Order) +1. **RIGHT NOW:** Test configuration dialog in tray app +2. Integrate printer-config.json into service +3. Implement page duplication pattern +4. Test on production hardware (Brother HL-L6415DW) +5. Deploy to first location +6. Gather feedback +7. Port content modifications from Rust +8. Deploy to remaining two locations