From 98ef880f9a1ce60ccbd6f123b7be9f90ce270b21 Mon Sep 17 00:00:00 2001 From: jchas2 Date: Wed, 19 Aug 2026 18:18:26 +1000 Subject: [PATCH] Chart config, Closes #55 --- README.md | 38 +++++++++- docs/RELEASING.md | 2 +- .../FileSystem.cs | 12 +++ .../IFileSystem.cs | 1 + .../Controls/Chart/Chart.cs | 31 +++++--- src/taskmon/Assets/Themes/Alien Blood.theme | 17 ++++- src/taskmon/Assets/Themes/Ayu.theme | 17 ++++- src/taskmon/Assets/Themes/Catppuccin.theme | 17 ++++- src/taskmon/Assets/Themes/Cobalt2.theme | 17 ++++- .../Assets/Themes/Cyberpunk 2077.theme | 17 ++++- .../Assets/Themes/Dracula Official.theme | 17 ++++- src/taskmon/Assets/Themes/Github Dark.theme | 17 ++++- .../Assets/Themes/Green Beautiful 2.theme | 17 ++++- src/taskmon/Assets/Themes/MSDOS.theme | 17 ++++- src/taskmon/Assets/Themes/Monochrome.theme | 17 ++++- src/taskmon/Assets/Themes/Night Owl.theme | 17 ++++- src/taskmon/Assets/Themes/Nord.theme | 17 ++++- src/taskmon/Assets/Themes/One Dark Pro.theme | 17 ++++- .../Assets/Themes/Shades of Purple.theme | 17 ++++- src/taskmon/Assets/Themes/SynthWave 84.theme | 17 ++++- .../Assets/Themes/Taskmon Default.theme | 17 ++++- src/taskmon/Assets/Themes/Tokyo Night.theme | 17 ++++- src/taskmon/Assets/Themes/macOS Classic.theme | 18 ++++- src/taskmon/Configuration/AppConfig.cs | 67 ++++++++++------- src/taskmon/Configuration/Constants.cs | 3 + src/taskmon/Configuration/Theme.cs | 13 ++++ src/taskmon/Gui/Controls/HeaderControl.cs | 2 + .../Configuration/AppConfigTests.cs | 73 +++++++++---------- .../Configuration/ThemeTests.cs | 13 +++- 29 files changed, 459 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 8527b97..35c6c21 100644 --- a/README.md +++ b/README.md @@ -160,15 +160,34 @@ Task Monitor stores configuration and log files in the following system director ## Theme Files Theme files control the color palette behavior for color desaturation, background opacity, and the colors applied to the TUI screen elements. -There a number of default themes that are pre-loaded when Task Monitor starts. These themes are kept in .ini files and can be customized as required. Additionally, new theme -files can be authored and copied to the theme files directory and will automatically be available next time Task Monitor starts. +Theme files use the ini file format (see example below). Any modifications to default/shjpped theme files will be reset automatically when +taskmon updates. To create new, custom themes, simply copy an existing theme and modify the settings as required. +These files will automatically be available next time Task Monitor starts. Below is an example theme file: ``` +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= + [Dracula Official] colour-mode=indexed + background=transparent background-highlight=#3b5070 + +chart-border=#a9b1d6 +chart-y-axis=#9ece6a + col-cmd-normal-user-space=#50fa7b col-cmd-low-priority=#0000FF col-cmd-high-cpu=#ff79c6 @@ -178,20 +197,27 @@ col-user-current-non-root=#f8f8f2 col-user-other-non-root=#f1fa8c col-user-system=#ff79c6 col-user-root=#50fa7b + command-foreground=#bd93f9 command-background=#282a36 + delta-highlight-colour=#ffb86c + error=#db61a2 + foreground=#f8f8f2 foreground-highlight=#ffb86c + menubar-foreground=#f1fa8c menubar-background=#282a36 + range-high-background=#bd93f9 range-low-background=#f1fa8c range-mid-background=#ffb86c range-high-foreground=#000000 range-low-foreground=#000000 range-mid-foreground=#000000 + header-background=#282a36 header-foreground=#f1fa8c ``` @@ -202,6 +228,10 @@ header-foreground=#f1fa8c | `colour-mode=indexed` | `Indexed` means use the color palette that supports color desaturation on modern terminals. `Truecolour` prevents desaturation (by emitting ARGB) escape codes. `Auto` detects terminal support. | | `background=transparent` | `transparent` honors background color opacity settings in the terminal. Otherwise specify a custom background color to override the terminal default. | | `background-highlight=#3b5070` | Row selection background color in a list. | + +| `chart-border=#f8f8f2` | Chart border color. | +| `chart-y-axis=#50fa7b` | Text color for chart Y-axis scale. | + | `col-cmd-normal-user-space=#50fa7b` | Text color for user-mode applications. | | `col-cmd-low-priority=#0000FF` | Text color for applications running at a low priority. | | `col-cmd-high-cpu=#ff79c6` | Text color for a process running at high CPU%. | @@ -232,7 +262,7 @@ Note: ```colour-mode``` can be temporarily overridden for ALL theme files by set ## Layout Files -Layout files control the number, sequence, rows and columns of the chart layouts on the main process screen. There a number of default layouts that are pre-loaded when Task Monitor starts. +Layout files control the number, sequence, rows and columns of the chart layouts on the main process screen. There are a number of default layouts that are pre-loaded when Task Monitor starts. These layouts are kept in .ini files and can be customized as required. Additionally, new layout files can be authored and copied to the layout files directory and will automatically be available next time Task Monitor starts. @@ -341,7 +371,7 @@ or behaviors can't be reproduced in IDE consoles, or are specific to the termina ### Writing Code / Project Structure * **Interop Layer**: All native OS calls are isolated in `Task.Monitor.Interop.Mach` (macOS) and `Task.Monitor.Interop.Win32` (Windows). If you need to add a new OS metric, define the P/Invoke signatures here. -* **System Abstraction**: The `Task.Monitor.System` project consumes the Interop layer to provide cross-platform models (e.g., `SystemStatistics`, `ProcessInfo`). +* **System Abstraction**: The `Task.Monitor.System` project consumes the Interop layer to provide cross-platform models (e.g., `SystemStatistics`, `ProcessInfo`). The TUI rendering framework also lives in this library. * **UI/CLI**: The `taskmon` project contains the main application logic, rendering, and terminal UI layouts. * **Themes/Layouts**: Custom terminal themes and layouts are stored as embedded resources in `src/taskmon/Assets/`. * **Test Projects**: There is a matching test project for every application project. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ab9b658..ff94281 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -173,4 +173,4 @@ brew install --build-from-source homebrew-tap/Formula/taskmon.rb Releases are tracked in: - [GitHub Releases](https://github.com/jchas2/taskmon/releases) -- [Homebrew Formula](https://github.com/jchas2/homebrew-taskmon/commits/main/Formula/taskmgr.rb) +- [Homebrew Formula](https://github.com/jchas2/homebrew-taskmon/commits/main/Formula/taskmon.rb) diff --git a/src/Task.Monitor.Internal.Abstractions/FileSystem.cs b/src/Task.Monitor.Internal.Abstractions/FileSystem.cs index 4398a3c..30e7ac9 100644 --- a/src/Task.Monitor.Internal.Abstractions/FileSystem.cs +++ b/src/Task.Monitor.Internal.Abstractions/FileSystem.cs @@ -8,6 +8,18 @@ public sealed partial class FileSystem : IFileSystem public bool FileExists(string? path) => File.Exists(path); + public long GetFileLength(string path) + { + try { + FileInfo finfo = new(path); + return finfo.Length; + } + catch (Exception ex) { + ExceptionHelper.LogException(ex, $"Error on GetFileLength '{path}'"); + return -1; + } + } + public string[] GetFiles(string path) { try { diff --git a/src/Task.Monitor.Internal.Abstractions/IFileSystem.cs b/src/Task.Monitor.Internal.Abstractions/IFileSystem.cs index 44dfffb..dc99952 100644 --- a/src/Task.Monitor.Internal.Abstractions/IFileSystem.cs +++ b/src/Task.Monitor.Internal.Abstractions/IFileSystem.cs @@ -5,6 +5,7 @@ public interface IFileSystem public bool TryCreateDirectory(string path); public bool DirectoryExists(string? path); public bool FileExists(string? path); + public long GetFileLength(string path); string[] GetFiles(string path); public string ReadAllText(string path); public void WriteAllText(string path, string? contents); diff --git a/src/Task.Monitor.System/Controls/Chart/Chart.cs b/src/Task.Monitor.System/Controls/Chart/Chart.cs index 3bb2bff..5169e4f 100644 --- a/src/Task.Monitor.System/Controls/Chart/Chart.cs +++ b/src/Task.Monitor.System/Controls/Chart/Chart.cs @@ -1,3 +1,4 @@ +using System.Buffers.Text; using System.Drawing; using Task.Monitor.Cli.Utils; @@ -67,10 +68,8 @@ private void AddInternal(double value) } public bool AutoScale { get; set; } = true; - - public Func? CustomYAxisScaleFormatter { get; set; } - - private int DataCapacity => Math.Max(0, Width - 2 - ScaleWidth); + + public Color BorderColour { get; set; } = ConsolePalette.White; public Color ColourHigh { get; set; } = ConsolePalette.Red; @@ -78,8 +77,12 @@ private void AddInternal(double value) public Color ColourMid { get; set; } = ConsolePalette.Yellow; + public Func? CustomYAxisScaleFormatter { get; set; } + private double DataAt(int i) => data[(dataHead + i) % data.Length]; + private int DataCapacity => Math.Max(0, Width - 2 - ScaleWidth); + public static string FormatYScalePercentage(double value) { int pct = (int)Math.Round(value * 100.0); @@ -153,14 +156,14 @@ protected override void OnDraw() frame.Clear(); frame.MoveTo(X, Y); - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u256D'); frame.Append('\u2500', totalInnerWidth); frame.Append('\u256E'); for (int row = 0; row < chartHeight; row++) { frame.MoveTo(X, Y + 1 + row); - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2502'); int rowFromBottom = chartHeight - 1 - row; @@ -170,7 +173,7 @@ protected override void OnDraw() int sampleIndex = sampleCount - chartWidth + col; if (sampleIndex < 0) { - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2800'); continue; } @@ -215,7 +218,7 @@ protected override void OnDraw() SetCellColour(chartColour); } else { - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); ch = '\u2800'; } @@ -224,7 +227,7 @@ protected override void OnDraw() if (showScale) { bool isIndexRow = (row % 2 == 0) || (row == chartHeight - 1); - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); if (isIndexRow) { double ratio = chartHeight > 1 ? (double)rowFromBottom / (chartHeight - 1) : 0.0; @@ -235,7 +238,9 @@ protected override void OnDraw() formatted = formatted[..scaleWidth]; } + frame.SetColour(YAxisColour, BackgroundColour); frame.Append(formatted.PadLeft(scaleWidth)); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2524'); } else { @@ -244,13 +249,12 @@ protected override void OnDraw() } } else { - frame.SetColour(ForegroundColour, BackgroundColour); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2502'); } } frame.MoveTo(X, Y + Height - 1); - frame.SetColour(ForegroundColour, BackgroundColour); string label = string.IsNullOrEmpty(LabelSeries) ? Text @@ -261,9 +265,12 @@ protected override void OnDraw() int leftDashes = (totalInnerWidth - labelLen) / 2; int rightDashes = totalInnerWidth - labelLen - leftDashes; + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2570'); frame.Append('\u2500', leftDashes); + frame.SetColour(ForegroundColour, BackgroundColour); frame.Append(labelLen < labelPadded.Length ? labelPadded[..labelLen] : labelPadded); + frame.SetColour(BorderColour, BackgroundColour); frame.Append('\u2500', rightDashes); frame.Append('\u256F'); @@ -317,4 +324,6 @@ public bool ShowYAxisScale } public string Text { get; set; } = string.Empty; + + public Color YAxisColour { get; set; } = ConsolePalette.White; } diff --git a/src/taskmon/Assets/Themes/Alien Blood.theme b/src/taskmon/Assets/Themes/Alien Blood.theme index 7a07772..1e565df 100644 --- a/src/taskmon/Assets/Themes/Alien Blood.theme +++ b/src/taskmon/Assets/Themes/Alien Blood.theme @@ -1,9 +1,24 @@ -[Alien Blood] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Alien Blood] colour-mode=truecolour background=#0f1610 background-highlight=#1d4125 +chart-border=#717f24 +chart-y-axis=#327f77 + col-cmd-normal-user-space=#327f77 col-cmd-low-priority=#10b981 col-cmd-high-cpu=#b082d1 diff --git a/src/taskmon/Assets/Themes/Ayu.theme b/src/taskmon/Assets/Themes/Ayu.theme index deec2a9..51bb7fb 100644 --- a/src/taskmon/Assets/Themes/Ayu.theme +++ b/src/taskmon/Assets/Themes/Ayu.theme @@ -1,9 +1,24 @@ -[Ayu] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Ayu] colour-mode=indexed background=transparent background-highlight=#3b5070 +chart-border=#bae67e +chart-y-axis=#59a6e0 + col-cmd-normal-user-space=#59a6e0 col-cmd-low-priority=#0000FF col-cmd-high-cpu=#ffa759 diff --git a/src/taskmon/Assets/Themes/Catppuccin.theme b/src/taskmon/Assets/Themes/Catppuccin.theme index ee3e1ef..45d01ef 100644 --- a/src/taskmon/Assets/Themes/Catppuccin.theme +++ b/src/taskmon/Assets/Themes/Catppuccin.theme @@ -1,9 +1,24 @@ -[Catppuccin] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Catppuccin] colour-mode=indexed background=transparent background-highlight=#3b5070 +chart-border=#a6e3a1 +chart-y-axis=#89b4fa + col-cmd-normal-user-space=#89b4fa col-cmd-low-priority=#0000FF col-cmd-high-cpu=#cba6f7 diff --git a/src/taskmon/Assets/Themes/Cobalt2.theme b/src/taskmon/Assets/Themes/Cobalt2.theme index 97938ec..d624a5f 100644 --- a/src/taskmon/Assets/Themes/Cobalt2.theme +++ b/src/taskmon/Assets/Themes/Cobalt2.theme @@ -1,9 +1,24 @@ -[Cobalt2] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Cobalt2] colour-mode=indexed background=transparent background-highlight=#0050A4 +chart-border=#ffc600 +chart-y-axis=#a5ff90 + col-cmd-normal-user-space=#a5ff90 col-cmd-low-priority=#ff628c col-cmd-high-cpu=#ba55d3 diff --git a/src/taskmon/Assets/Themes/Cyberpunk 2077.theme b/src/taskmon/Assets/Themes/Cyberpunk 2077.theme index 944a723..1e781d0 100644 --- a/src/taskmon/Assets/Themes/Cyberpunk 2077.theme +++ b/src/taskmon/Assets/Themes/Cyberpunk 2077.theme @@ -1,9 +1,24 @@ -[Cyberpunk 2077] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Cyberpunk 2077] colour-mode=indexed background=transparent background-highlight=#6272A4 +chart-border=#E455AE +chart-y-axis=#FDF500 + col-cmd-normal-user-space=#FDF500 col-cmd-low-priority=#37EBF3 col-cmd-high-cpu=#37EBF3 diff --git a/src/taskmon/Assets/Themes/Dracula Official.theme b/src/taskmon/Assets/Themes/Dracula Official.theme index 10e040d..f952307 100644 --- a/src/taskmon/Assets/Themes/Dracula Official.theme +++ b/src/taskmon/Assets/Themes/Dracula Official.theme @@ -1,9 +1,24 @@ -[Dracula Official] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Dracula Official] colour-mode=indexed background=transparent background-highlight=#3b5070 +chart-border=#f8f8f2 +chart-y-axis=#50fa7b + col-cmd-normal-user-space=#50fa7b col-cmd-low-priority=#0000FF col-cmd-high-cpu=#bd93f9 diff --git a/src/taskmon/Assets/Themes/Github Dark.theme b/src/taskmon/Assets/Themes/Github Dark.theme index 5df66eb..34c2f21 100644 --- a/src/taskmon/Assets/Themes/Github Dark.theme +++ b/src/taskmon/Assets/Themes/Github Dark.theme @@ -1,9 +1,24 @@ -[Github Dark] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Github Dark] colour-mode=indexed background=transparent background-highlight=#3b5070 +chart-border=#a5d6ff +chart-y-axis=#d2a8ff + col-cmd-normal-user-space=#d2a8ff col-cmd-low-priority=#0000FF col-cmd-high-cpu=#ff7b72 diff --git a/src/taskmon/Assets/Themes/Green Beautiful 2.theme b/src/taskmon/Assets/Themes/Green Beautiful 2.theme index 7f3bf8b..ae1d3ef 100644 --- a/src/taskmon/Assets/Themes/Green Beautiful 2.theme +++ b/src/taskmon/Assets/Themes/Green Beautiful 2.theme @@ -1,9 +1,24 @@ -[Green Beautiful 2] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Green Beautiful 2] colour-mode=indexed background=transparent background-highlight=#228b22 +chart-border=#228b22 +chart-y-axis=#FDFD96 + col-cmd-normal-user-space=#FDFD96 col-cmd-low-priority=#115d33 col-cmd-high-cpu=#4cbb17 diff --git a/src/taskmon/Assets/Themes/MSDOS.theme b/src/taskmon/Assets/Themes/MSDOS.theme index 07fb51b..32ecfb3 100644 --- a/src/taskmon/Assets/Themes/MSDOS.theme +++ b/src/taskmon/Assets/Themes/MSDOS.theme @@ -1,9 +1,24 @@ -[MS-DOS] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[MS-DOS] colour-mode=truecolour background=#000080 background-highlight=#00FFFF +chart-border=#808080 +chart-y-axis=#FFFF00 + col-cmd-normal-user-space=#FFFF00 col-cmd-low-priority=#C0C0C0 col-cmd-high-cpu=#FF0000 diff --git a/src/taskmon/Assets/Themes/Monochrome.theme b/src/taskmon/Assets/Themes/Monochrome.theme index 3332228..8418119 100644 --- a/src/taskmon/Assets/Themes/Monochrome.theme +++ b/src/taskmon/Assets/Themes/Monochrome.theme @@ -1,9 +1,24 @@ -[Monochrome] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Monochrome] colour-mode=indexed background=transparent background-highlight=#808080 +chart-border=#ffffff +chart-y-axis=#ffffff + col-cmd-normal-user-space=#ffffff col-cmd-low-priority=#ffffff col-cmd-high-cpu=#ffffff diff --git a/src/taskmon/Assets/Themes/Night Owl.theme b/src/taskmon/Assets/Themes/Night Owl.theme index da8a865..a3c5db3 100644 --- a/src/taskmon/Assets/Themes/Night Owl.theme +++ b/src/taskmon/Assets/Themes/Night Owl.theme @@ -1,9 +1,24 @@ -[Night Owl] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Night Owl] colour-mode=indexed background=transparent background-highlight=#1d3b53 ; editor.selectionBackground +chart-border=#c792ea +chart-y-axis=#ecc48d + col-cmd-normal-user-space=#ecc48d ; tokenColors: string col-cmd-low-priority=#82AAFF ; tokenColors: constant col-cmd-high-cpu=#F78C6C ; tokenColors: variable diff --git a/src/taskmon/Assets/Themes/Nord.theme b/src/taskmon/Assets/Themes/Nord.theme index 759918e..0d1fbe4 100644 --- a/src/taskmon/Assets/Themes/Nord.theme +++ b/src/taskmon/Assets/Themes/Nord.theme @@ -1,9 +1,24 @@ -[Nord] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Nord] colour-mode=indexed background=transparent background-highlight=#434c5ecc +chart-border=#8FBCBB +chart-y-axis=#A3BE8C + col-cmd-normal-user-space=#A3BE8C col-cmd-low-priority=#81A1C1 col-cmd-high-cpu=#8FBCBB diff --git a/src/taskmon/Assets/Themes/One Dark Pro.theme b/src/taskmon/Assets/Themes/One Dark Pro.theme index 666a23c..66a7455 100644 --- a/src/taskmon/Assets/Themes/One Dark Pro.theme +++ b/src/taskmon/Assets/Themes/One Dark Pro.theme @@ -1,9 +1,24 @@ -[One Dark Pro] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[One Dark Pro] colour-mode=indexed background=transparent background-highlight=##3b5070 +chart-border=#98c379 +chart-y-axis=#61afef + col-cmd-normal-user-space=#61afef col-cmd-low-priority=#0000FF col-cmd-high-cpu=#c678dd diff --git a/src/taskmon/Assets/Themes/Shades of Purple.theme b/src/taskmon/Assets/Themes/Shades of Purple.theme index 4ef20f2..8d9f8c8 100644 --- a/src/taskmon/Assets/Themes/Shades of Purple.theme +++ b/src/taskmon/Assets/Themes/Shades of Purple.theme @@ -1,9 +1,24 @@ -[Shades of Purple] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Shades of Purple] colour-mode=indexed background=transparent background-highlight=#B362FF +chart-border=#B362FF +chart-y-axis=#A5FF90 + col-cmd-normal-user-space=#A5FF90 col-cmd-low-priority=#FF628C col-cmd-high-cpu=#B362FF diff --git a/src/taskmon/Assets/Themes/SynthWave 84.theme b/src/taskmon/Assets/Themes/SynthWave 84.theme index 542227e..9837311 100644 --- a/src/taskmon/Assets/Themes/SynthWave 84.theme +++ b/src/taskmon/Assets/Themes/SynthWave 84.theme @@ -1,9 +1,24 @@ -[SynthWave 84] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[SynthWave 84] colour-mode=indexed background=transparent background-highlight=#fede5d ; editor.selectionBackground +chart-border=#ff7edb +chart-y-axis=#ff8b39 + col-cmd-normal-user-space=#ff8b39 ; tokenColors: string col-cmd-low-priority=#f97e72 ; tokenColors: constant col-cmd-high-cpu=#36f9f6 ; tokenColors: variable diff --git a/src/taskmon/Assets/Themes/Taskmon Default.theme b/src/taskmon/Assets/Themes/Taskmon Default.theme index 35bf99c..0c62037 100644 --- a/src/taskmon/Assets/Themes/Taskmon Default.theme +++ b/src/taskmon/Assets/Themes/Taskmon Default.theme @@ -1,9 +1,24 @@ -[Taskmon Default] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Taskmon Default] colour-mode=indexed background=transparent background-highlight=#00FFFF +chart-border=#FFFFFF +chart-y-axis=#00FF00 + col-cmd-normal-user-space=#00FF00 col-cmd-low-priority=#0000FF col-cmd-high-cpu=#FF0000 diff --git a/src/taskmon/Assets/Themes/Tokyo Night.theme b/src/taskmon/Assets/Themes/Tokyo Night.theme index 5b23df6..712bfad 100644 --- a/src/taskmon/Assets/Themes/Tokyo Night.theme +++ b/src/taskmon/Assets/Themes/Tokyo Night.theme @@ -1,9 +1,24 @@ -[Tokyo Night] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[Tokyo Night] colour-mode=indexed background=transparent background-highlight=##3b5070 +chart-border=#a9b1d6 +chart-y-axis=#9ece6a + col-cmd-normal-user-space=#9ece6a col-cmd-low-priority=#0000FF col-cmd-high-cpu=#bb9af7 diff --git a/src/taskmon/Assets/Themes/macOS Classic.theme b/src/taskmon/Assets/Themes/macOS Classic.theme index ba82b4f..b25fb84 100644 --- a/src/taskmon/Assets/Themes/macOS Classic.theme +++ b/src/taskmon/Assets/Themes/macOS Classic.theme @@ -1,9 +1,24 @@ -[macOS Classic] +;============================================================================= +; Task Monitor (taskmon) - Built-in Theme +;============================================================================= +; IMPORTANT: +; Any modifications to default/shipped theme files will be reset automatically +; when taskmon runs or updates. +; +; How to create a custom theme: +; Step 1: Copy this file to a new name (e.g. "Tokyo Night Custom.theme"). +; Step 2: Rename the header "[Tokyo Night]" to "[Tokyo Night Custom]". +; Step 3: Customize color hex values (#RRGGBB). +;============================================================================= +[macOS Classic] colour-mode=indexed background=transparent background-highlight=#004a9e +chart-border=#E19773 +chart-y-axis=#ffffff + col-cmd-normal-user-space=#ffffff col-cmd-low-priority=#b5cea8 col-cmd-high-cpu=#AC98AD @@ -23,7 +38,6 @@ delta-highlight-colour=#E1D797 error=#ff453a foreground=#E19773 -;foreground=AC98AD foreground-highlight=#ffffff menubar-foreground=#858585 diff --git a/src/taskmon/Configuration/AppConfig.cs b/src/taskmon/Configuration/AppConfig.cs index 3678f45..dd68130 100644 --- a/src/taskmon/Configuration/AppConfig.cs +++ b/src/taskmon/Configuration/AppConfig.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; @@ -469,13 +469,11 @@ private void LoadSections() private void LoadThemes() { - bool validThemePath = true; - string themePath = !string.IsNullOrEmpty(DefaultConfigPath) ? Path.Combine(DefaultConfigPath, Constants.ThemeDirectory) : string.Empty; - validThemePath = !string.IsNullOrEmpty(themePath); + bool validThemePath = !string.IsNullOrEmpty(themePath); if (validThemePath && !fileSystem.DirectoryExists(themePath)) { if (fileSystem.TryCreateDirectory(themePath)) { @@ -486,28 +484,9 @@ private void LoadThemes() } } - if (validThemePath) { - string[] themeFiles = fileSystem.GetFiles(themePath); - - foreach (string themeFile in themeFiles) { - string themeText = fileSystem.ReadAllText(themeFile); - Trace.WriteLine($"Parsing {themeFile}"); - - if (!TryParseIni(themeText, out Theme? theme)) { - Trace.WriteLine($"Failed to parse: \n{themeText}\n"); - continue; - } - - theme!.Normalize(); - - if (!allThemes.Any(t => t.Name.Equals(theme!.Name))) { - allThemes.Add(theme!); - } - } - } - Assembly asm = Assembly.GetExecutingAssembly(); + // Load default/shipped themes. foreach (string name in asm.GetManifestResourceNames()) { if (!name.EndsWith(Constants.ThemeExtension)) { continue; @@ -515,6 +494,7 @@ private void LoadThemes() using StreamReader reader = new(asm.GetManifestResourceStream(name)!); string themeText = reader.ReadToEnd(); + Trace.WriteLine($"Parsing asset {name}"); if (!TryParseIni(themeText, out Theme? theme)) { Debug.Fail($"Failed to parse manifest asset {name}"); @@ -523,11 +503,42 @@ private void LoadThemes() theme!.Normalize(); - if (!allThemes.Any(t => t.Name.Equals(theme!.Name))) { - allThemes.Add(theme!); + if (!allThemes.Any(t => t.Name.Equals(theme.Name))) { + allThemes.Add(theme); + } + + if (validThemePath) { string themeFilePath = Path.Combine(themePath, $"{theme!.Name}{Constants.ThemeExtension}"); - fileSystem.WriteAllText(themeFilePath, themeText); - PathPermissions.EnsureUserOwnership(themeFilePath); + + if (!fileSystem.FileExists(themeFilePath) || fileSystem.GetFileLength(themeFilePath) != themeText.Length) { + Trace.WriteLine($"Creating/Overwriting {themeFilePath}"); + fileSystem.WriteAllText(themeFilePath, themeText); + PathPermissions.EnsureUserOwnership(themeFilePath); + } + } + } + + if (validThemePath) { + // Load custom theme files from disk into allThemes array. + string[] themeFiles = fileSystem.GetFiles(themePath); + + foreach (string themeFile in themeFiles) { + string themeName = Path.GetFileNameWithoutExtension(themeFile); + + if (allThemes.Any(t => t.Name.Equals(themeName))) { + continue; + } + + string themeText = fileSystem.ReadAllText(themeFile); + Trace.WriteLine($"Parsing file {themeFile}"); + + if (!TryParseIni(themeText, out Theme? theme)) { + Trace.WriteLine($"Failed to parse: \n{themeText}\n"); + continue; + } + + theme!.Normalize(); + allThemes.Add(theme!); } } diff --git a/src/taskmon/Configuration/Constants.cs b/src/taskmon/Configuration/Constants.cs index cab2121..6f13ba9 100644 --- a/src/taskmon/Configuration/Constants.cs +++ b/src/taskmon/Configuration/Constants.cs @@ -67,6 +67,9 @@ public sealed class Keys public const string Background = "background"; public const string BackgroundHighlight = "background-highlight"; + public const string ChartBorder = "chart-border"; + public const string ChartYAxis = "chart-y-axis"; + public const string ColCmdNormalUserSpace = "col-cmd-normal-user-space"; public const string ColCmdLowPriority = "col-cmd-low-priority"; public const string ColCmdHighCpu = "col-cmd-high-cpu"; diff --git a/src/taskmon/Configuration/Theme.cs b/src/taskmon/Configuration/Theme.cs index 16fb39c..b91351b 100644 --- a/src/taskmon/Configuration/Theme.cs +++ b/src/taskmon/Configuration/Theme.cs @@ -20,6 +20,8 @@ public Theme() { } [ Constants.Keys.Background, Constants.Keys.BackgroundHighlight, + Constants.Keys.ChartBorder, + Constants.Keys.ChartYAxis, Constants.Keys.ColCmdNormalUserSpace, Constants.Keys.ColCmdLowPriority, Constants.Keys.ColCmdHighCpu, @@ -83,6 +85,17 @@ public Color BackgroundHighlight set => SetColour(Constants.Keys.BackgroundHighlight, value); } + public Color ChartBprder + { + get => GetColour(Constants.Keys.ChartBorder, ConsolePalette.White); + set => SetColour(Constants.Keys.ChartBorder, value); + } + public Color ChartYAxis + { + get => GetColour(Constants.Keys.ChartYAxis, ConsolePalette.White); + set => SetColour(Constants.Keys.ChartYAxis, value); + } + public Color ColumnCommandNormalUserSpace { get => GetColour(Constants.Keys.ColCmdNormalUserSpace, ConsolePalette.Green); diff --git a/src/taskmon/Gui/Controls/HeaderControl.cs b/src/taskmon/Gui/Controls/HeaderControl.cs index e0ea19c..71e5c72 100644 --- a/src/taskmon/Gui/Controls/HeaderControl.cs +++ b/src/taskmon/Gui/Controls/HeaderControl.cs @@ -257,11 +257,13 @@ protected override void OnLoad() } foreach (Chart chart in charts) { + chart.BorderColour = appConfig.DefaultTheme.ChartBprder; chart.ColourHigh = appConfig.DefaultTheme.RangeHighBackground; chart.ColourLow = appConfig.DefaultTheme.RangeLowBackground; chart.ColourMid = appConfig.DefaultTheme.RangeMidBackground; chart.MetreStyle = appConfig.MetreStyle; chart.ShowYAxisScale = appConfig.ShowYAxisScale; + chart.YAxisColour = appConfig.DefaultTheme.ChartYAxis; } processor.ProcessorUpdated += OnProcessorUpdated; diff --git a/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs b/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs index af3c164..2755f5e 100644 --- a/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs +++ b/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Drawing; using System.Runtime.InteropServices; using Moq; using Task.Monitor.Configuration; @@ -423,43 +424,37 @@ public void DefaultConfigFilePath_ReturnsNonNullPath() Assert.Contains("taskmon.ini", result); } - // [Fact] - // public void DefaultLogPath_ReturnsNonNullPath() - // { - // AppConfig appConfig = new(fileSystem.Object); - // - // fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny())).Returns(true); - // - // string? result = appConfig.DefaultLogPath; - // - // Assert.NotNull(result); - // Assert.Contains("taskmon", result); - // } - // - // [Fact] - // public void DefaultLogPath_Under_Sudo_Resolves_Invoking_User_Home_On_MacOS() - // { - // // GetUserHomeDirectory only takes the SUDO_USER branch on macOS. - // if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - // return; - // } - // - // string? originalSudoUser = Environment.GetEnvironmentVariable("SUDO_USER"); - // - // try { - // Environment.SetEnvironmentVariable("SUDO_USER", "alice"); - // fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny())).Returns(true); - // - // AppConfig appConfig = new(fileSystem.Object); - // - // string? result = appConfig.DefaultLogPath; - // - // // Logs go to the invoking user's home, not root's (/var/root). - // Assert.NotNull(result); - // Assert.StartsWith("/Users/alice/.local/state", result); - // } - // finally { - // Environment.SetEnvironmentVariable("SUDO_USER", originalSudoUser); - // } - // } + [Fact] + public void LoadThemes_ManifestThemes_Write_To_Disk_On_Load() + { + fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny())).Returns(true); + + AppConfig appConfig = new(fileSystem.Object); + + Assert.NotEmpty(appConfig.Themes); + Assert.Contains(appConfig.Themes, t => t.Name == "Taskmon Default"); + fileSystem.Verify(fs => fs.WriteAllText( + It.Is(p => p.EndsWith($"Taskmon Default{Constants.ThemeExtension}")), + It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void LoadThemes_Custom_Themes_On_Disk_Are_Loaded() + { + string customThemeIni = @" +[My Custom Theme] +colour-mode=truecolour +background=#123456 +foreground=#abcdef +"; + fileSystem.Setup(fs => fs.DirectoryExists(It.IsAny())).Returns(true); + fileSystem.Setup(fs => fs.GetFiles(It.IsAny())).Returns(["/fake/path/themes/My Custom Theme.theme"]); + fileSystem.Setup(fs => fs.ReadAllText("/fake/path/themes/My Custom Theme.theme")).Returns(customThemeIni); + + AppConfig appConfig = new(fileSystem.Object); + + Assert.Contains(appConfig.Themes, t => t.Name == "My Custom Theme"); + Theme customTheme = appConfig.Themes.First(t => t.Name == "My Custom Theme"); + Assert.Equal(ColorTranslator.FromHtml("#123456"), customTheme.Background); + } } diff --git a/tests/Task.Monitor.Tests/Configuration/ThemeTests.cs b/tests/Task.Monitor.Tests/Configuration/ThemeTests.cs index 86e5b5c..14ef0bc 100644 --- a/tests/Task.Monitor.Tests/Configuration/ThemeTests.cs +++ b/tests/Task.Monitor.Tests/Configuration/ThemeTests.cs @@ -15,6 +15,9 @@ [Test Theme] background=#0f1610 background-highlight=#1d4125 +chart-border=#334455 +chart-y-axis=#667788 + col-cmd-normal-user-space=#327f77 col-cmd-low-priority=#10b981 col-cmd-high-cpu=#b082d1 @@ -55,7 +58,7 @@ [Test Theme] [Fact] public void Theme_Canary_Test() => - Assert.Equal(29, CanaryTestHelper.GetPropertyCount()); + Assert.Equal(31, CanaryTestHelper.GetPropertyCount()); [Fact] public void Constructor_Initialises_Successfully() @@ -75,6 +78,8 @@ public void Setters_Initialise_Successfully() Theme theme = new(section) { Background = ColorTranslator.FromHtml("#0f1610"), BackgroundHighlight = ColorTranslator.FromHtml("#1d4125"), + ChartBprder = ColorTranslator.FromHtml("#334455"), + ChartYAxis = ColorTranslator.FromHtml("#667788"), ColumnCommandNormalUserSpace = ColorTranslator.FromHtml("#327f77"), ColumnCommandLowPriority = ColorTranslator.FromHtml("#10b981"), ColumnCommandHighCpu = ColorTranslator.FromHtml("#b082d1"), @@ -116,6 +121,8 @@ public void Setters_ReadBack_All_Colour_Properties_Successfully() // Act – write every colour property through its setter. theme.Background = ColorTranslator.FromHtml("#010101"); theme.BackgroundHighlight = ColorTranslator.FromHtml("#020202"); + theme.ChartBprder = ColorTranslator.FromHtml("#1c1c1c"); + theme.ChartYAxis = ColorTranslator.FromHtml("#1d1d1d"); theme.ColumnCommandNormalUserSpace = ColorTranslator.FromHtml("#030303"); theme.ColumnCommandLowPriority = ColorTranslator.FromHtml("#040404"); theme.ColumnCommandHighCpu = ColorTranslator.FromHtml("#050505"); @@ -145,6 +152,8 @@ public void Setters_ReadBack_All_Colour_Properties_Successfully() // Assert – read every colour property back through its getter. Assert.Equal(ColorTranslator.FromHtml("#010101"), theme.Background); Assert.Equal(ColorTranslator.FromHtml("#020202"), theme.BackgroundHighlight); + Assert.Equal(ColorTranslator.FromHtml("#1c1c1c"), theme.ChartBprder); + Assert.Equal(ColorTranslator.FromHtml("#1d1d1d"), theme.ChartYAxis); Assert.Equal(ColorTranslator.FromHtml("#030303"), theme.ColumnCommandNormalUserSpace); Assert.Equal(ColorTranslator.FromHtml("#040404"), theme.ColumnCommandLowPriority); Assert.Equal(ColorTranslator.FromHtml("#050505"), theme.ColumnCommandHighCpu); @@ -176,6 +185,8 @@ private void AssertThemeColours(Theme theme) { Assert.Equal(ColorTranslator.FromHtml("#0f1610"), theme.Background); Assert.Equal(ColorTranslator.FromHtml("#1d4125"), theme.BackgroundHighlight); + Assert.Equal(ColorTranslator.FromHtml("#334455"), theme.ChartBprder); + Assert.Equal(ColorTranslator.FromHtml("#667788"), theme.ChartYAxis); Assert.Equal(ColorTranslator.FromHtml("#327f77"), theme.ColumnCommandNormalUserSpace); Assert.Equal(ColorTranslator.FromHtml("#10b981"), theme.ColumnCommandLowPriority); Assert.Equal(ColorTranslator.FromHtml("#b082d1"), theme.ColumnCommandHighCpu);