diff --git a/README.md b/README.md index 2d4b23d..58bab38 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ Originally inspired by tools like top, htop and the Windows Tas ![Task Monitor](./docs/images/taskmon_main.png) +![Task Monitor Gpu](./docs/images/taskmon_gpu.png) + ## Features - **Perfect for HomeLab and Local LLM monitoring** - **No sudo or administrator privilege required** -- **Cross-Platform native performance**: Written in C# using platform native interop for maximum performance and minimum memory overhead (zero third-party dependencies) - **Real-Time System Monitoring**: Live updates of CPU, GPU, memory, swap, disk I/O and Network I/O. - High resolution charts in 24-bit colour. - Switch layouts (`F8`) to focus charts on specific system metrics. @@ -28,18 +29,18 @@ Originally inspired by tools like top, htop and the Windows Tas - Process CPU% defaults to Irix mode (individual core saturation) for *nix systems (`i` to toggle on/off). - Process deep dive (`F5`) to view live thread times and loaded libraries (system and user). - **Module Analysis**: Full dynamic and static library enumeration for selected process. -- Multiple process selection (`x` toggle show checkboxex, `spacebar` for selection, `u` to uncheck all selections) - Customise update interval (default 1500ms). - Customise top number of processes to display (default unlimited). - Customise number of process iteration loops. - Freeze process list updates (`f` or `z`). - **Windows Services**: Show service names and startup parameters, not just generic `svchost.exe` entries. +- Multiple process selection (`x` toggle show checkboxes, `spacebar` for selection, `u` to uncheck all selections) - Select and terminate multiple processes simultaneously (`F6`). - Filter by process name, username, command path or PID (`F4`). - Sort the process list by any visible column (`F3`). - Quick sort by CPU%, GPU% or Memory (`p`, `g` or `m` respectively). - Toggle process sorting between ascending (using the `a` key) and descending (using the `d` key). -- Navigate scrollable lists using the `↓`, `↑` arrow keys and `Pg Up` and `Pg Down` keys. +- Navigate scrollable lists using the ↓, ↑ arrow keys and `Pg Up` and `Pg Down` keys. - **Multiple Themes**: Ships with dozens of popular, modern 24-bit colour themes. - Modern terminal detection to support colour desaturation. - **Customise Configuration**: Manage configuration settings using the (`F2`) Setup function. Interactive UI for modifying and saving settings. @@ -77,7 +78,7 @@ Task Monitor does not require sudo permission on MacOS for system monitoring, ho ### Windows on x64 and ARM64 [![Chocolatey](https://img.shields.io/badge/chocolatey-blue?style=flat-square&logo=chocolatey&logoColor=white)](#windows-on-x64-and-arm64) -The easiest way to install on Windows isusing chocolately (pending): +The easiest way to install on Windows is using chocolately (pending): ```powershell choco install taskmon @@ -114,19 +115,19 @@ Task Monitor supports the following commands on startup. Note these commands can | `taskmon --gpu-only` | Automatically monitor GPU resources on startup. | | `taskmon --cpu-only` | Automatically monitor CPU resources on startup. | | `taskmon --pid ` | Automatically monitor the pid nnn on startup. | -| `taskmon --username ` | Automatically monitor all processes running under user on startup. | -| `taskmon --process ` | Automatically monitor all processes with matching on startup. | +| `taskmon --username ` | Automatically monitor all processes running under user username on startup. | +| `taskmon --process ` | Automatically monitor all processes with matching processname on startup. | | `taskmon --sort ` | Automatically sort by on startup. | +| `taskmon --sort-help` | Show the list of columns available for the `--sort` option. | | `taskmon --delay ` | Use in milliseconds between chart and process updates. | | `taskmon --limit ` | Limit the number of iteration loops and then stop. | -| `taskmon --nprocs ` | Only display the top number of processes . | +| `taskmon --nprocs ` | Only display the top number of processes. Eg: `taskmon --nprocs 10` | | `taskmon --theme ` | Load a theme from the available themes. Eg: `taskmon --theme "Dracula Official"` | +| `taskmon --theme-help` | Show the list of theme names available for the `--theme` option. | | `taskmon --debug` | Signal a debug break on startup and wait for a debugger to attach. | | `taskmon --version` | Print version information and exit. | | `taskmon --help` | Show help and usage information. | -Valid sort columns are: AvgCpu, AvgDisk, AvgGpu, AvgMem, Cpu, Disk, Gpu, MaxCpu, MaxDisk, MaxGpu, MaxMem, Mem, Path, Pid, Pri, Process, Thrd,User - ## Configuration Task Monitor stores configuration and log files in the following system directories: @@ -199,7 +200,7 @@ header-foreground=#f1fa8c | --- | --- | | `[Dracula Official]` | Theme name, used to identify the theme. | | `colour-mode=indexed` | `Indexed` means use the colour palette that supports colour desaturation on modern terminals. `Truecolour` prevents desaturation (by emitting ARGB) escape codes. `Auto` detects terminal support. | -| `background=transparent` | `transaparent` honours background opacity settings in the terminal. Otherwise use a hex colour. | +| `background=transparent` | `transaparent` honours background colour opacity settings in the terminal. Otherwise specify a custom background colour to override the terminal default. | | `background-highlight=#3b5070` | Row selection background colour in a list. | | `col-cmd-normal-user-space=#50fa7b` | Text colour for user-mode applications. | | `col-cmd-low-priority=#0000FF` | Text colour for applications running at a low priority. | @@ -227,7 +228,7 @@ header-foreground=#f1fa8c | `header-background=#282a36` | Column header background colour in a list. | | `header-foreground=#f1fa8c` | Column header foreground colour in a list. | -Note: ```colour-mode``` can be temporarily overriden for ALL theme files by setting the environment variable ```TASKMON_COLOUR_MODE``` to one of ```Indexed```, ```Truecolour``` or ```Auto```. +Note: ```colour-mode``` can be temporarily overriden for ALL theme files by setting the environment variable ```TASKMON_COLOUR_MODE``` to one of ```Indexed```, ```Truecolour``` or ```Auto```. ```Auto``` will support the theme setting if the terminal supports it. ## Layout Files @@ -255,4 +256,103 @@ charts=0,1,2,3,4,5,6,7 | `num-cols=4` | Number of columns to show each chart. | | `; Chart index` | | | `; 0=Cpu, 1=Gpu, 2=Disk, 3=NetworkSent, 4=Memory, 5=GpUMemory, 6=VirtualMemory, 7=NetworkRec` | Chart identifiers. | -| `charts=0,1,2,3,4,5,6,7` | Array of chart identifiers to render across the rows and columns. | \ No newline at end of file +| `charts=0,1,2,3,4,5,6,7` | Array of chart identifiers to render across the rows and columns. | + +## Development Guide + +This section covers everything you need to know to build, test, debug, and contribute to `taskmon`. + +### Prerequisites + +* [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or newer. +* A compatible OS and Architecture: + * **macOS**: Apple Silicon (`osx-arm64`). *Intel Macs (`osx-x64`) are not supported.* + * **Windows**: x64 (`win-x64`). + +### Building the Project + +The repository includes cross-platform build scripts in the root directory that wrap the underlying .NET CLI commands and engineering scripts (`eng/`). + +**macOS (Bash):** +```bash +./build.sh +``` + +**Windows (PowerShell):** +```powershell +.\build.ps1 +``` + +By default, these scripts perform a clean build using the `Debug` configuration. Under the hood, they invoke `eng/build.sh` or `eng/build.ps1`. +You can pass custom arguments to the underlying scripts: +* `--clean`: Clean the solution. +* `--restore`: Restore NuGet packages. +* `--build`: Build the solution. +* `-c `: Specify configuration (e.g., `Release`). + +### Running Tests + +The project uses **xUnit** and **Moq** for testing. Test projects are located in the `tests/` directory. + +To run the full test suite: + +**macOS:** +```bash +./test.sh +``` + +**Windows:** +```powershell +.\test.ps1 +``` +*(This is equivalent to running `./eng/build.sh --test`)* + +### Publishing + +To build self-contained, Native AOT-compiled binaries for release: + +**macOS:** +```bash +./publish.sh +``` + +**Windows:** +```powershell +.\publish.ps1 +``` +*(This cleans, builds, and publishes using the `Release` configuration).* + +The output will be located in `src/taskmon/bin/Release/net10.0//publish/`. + +### Debugging + +To debug the CLI application, you can instruct it to pause on startup and wait for a debugger to attach. This is useful when issues +or behaviours can't be reproduced in IDE consoles, or are specific to the terminal host (i.e. Powershell, conhost, Terminal, Ghostty, etc). + +1. Run the application with the `--debug` flag: + ```bash + taskmon --debug + ``` +2. The application will print its Process ID (PID) and wait: + `Waiting for debugger attach to Pid 12345` +3. Attach your IDE's debugger to the specified PID. +4. Once attached, the application will trigger a `Debugger.Break()` and you can step through the code. + +### 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`). +* **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. + +### Dependencies +This project contains no dependencies on third party .net libraries. The early iterations of the system layer functionality used +a number of libaries and the memory footprint for the tool quickly climbed to several hundred MB. Keeping the CPU and memory footprint as low as possible for a .net native application is a key design goal. + +## Acknowledgments + +- The original `htop` https://github.com/htop-dev/htop +- MacOS `top` https://github.com/apple-open-source/macos/tree/master/top +- The .net framework authors https://github.com/dotnet +- The WinForms authors: the TUI framework design in this project was modelled on the WinForms object and eventing models, to provide a clean, simple API for working with the underlying terminal. diff --git a/docs/images/taskmon_gpu.png b/docs/images/taskmon_gpu.png new file mode 100644 index 0000000..06dfba1 Binary files /dev/null and b/docs/images/taskmon_gpu.png differ diff --git a/docs/images/taskmon_main.png b/docs/images/taskmon_main.png index 3085f61..a8f5e4d 100644 Binary files a/docs/images/taskmon_main.png and b/docs/images/taskmon_main.png differ diff --git a/src/Task.Monitor.System/Controls/Chart/Chart.cs b/src/Task.Monitor.System/Controls/Chart/Chart.cs index 7772fd2..e77412b 100644 --- a/src/Task.Monitor.System/Controls/Chart/Chart.cs +++ b/src/Task.Monitor.System/Controls/Chart/Chart.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; using Task.Monitor.Cli.Utils; namespace Task.Monitor.System.Controls.Chart; @@ -15,18 +15,19 @@ public sealed class Chart : Control private static readonly char[] BrailleChars = ['\u2800', '\u28C0', '\u28E4', '\u28F6', '\u28FF']; private static readonly char BarChar = '|'; private static readonly char BlockChar = ' '; + + private const int DefaultScaleWidth = 5; private double[] data = []; private int dataHead = 0; private int dataCount = 0; private double dataMax = 0.0; + private bool showYAxisScale = true; private readonly object dataLock = new(); private readonly AnsiScreenBuffer frame = new(); public Chart(ISystemTerminal terminal) : base(terminal) { } - private int DataCapacity => Math.Max(0, Width - 2); - public void Add(double value) { lock (dataLock) { @@ -66,8 +67,10 @@ private void AddInternal(double value) } public bool AutoScale { get; set; } = true; - - public MetreControlStyle MetreStyle { get; set; } = MetreControlStyle.Dots; + + public Func? CustomYAxisScaleFormatter { get; set; } + + private int DataCapacity => Math.Max(0, Width - 2 - ScaleWidth); public Color ColourHigh { get; set; } = ConsolePalette.Red; @@ -77,6 +80,42 @@ private void AddInternal(double value) private double DataAt(int i) => data[(dataHead + i) % data.Length]; + private string FormatScaleValue(double value, double maxVal) + { + if (CustomYAxisScaleFormatter != null) { + return CustomYAxisScaleFormatter(value); + } + + if (maxVal <= 1.0 && maxVal > 0.0) { + int pct = (int)Math.Round(value * 100.0); + return $"{pct}%"; + } + + if (Math.Abs(value) < 1e-9) { + return "0"; + } + + if (value >= 1000000) { + return $"{value / 1000000.0:0.#}M"; + } + + if (value >= 1000) { + return $"{value / 1000.0:0.#}k"; + } + + if (value >= 100) { + return $"{value:0}"; + } + + if (value >= 10) { + return $"{value:0.#}"; + } + + return $"{value:0.##}"; + } + + private bool IsYAxisScaleVisible => ShowYAxisScale && (Height - 2) > 6; + public string LabelSeries { get; set; } = string.Empty; protected override void OnDraw() @@ -95,19 +134,22 @@ protected override void OnDraw() } int chartHeight = Math.Max(0, Height - 2); - int chartWidth = Math.Max(0, Width - 2); + bool showScale = ShowYAxisScale && chartHeight > 6; + int scaleWidth = showScale ? DefaultScaleWidth : 0; + int totalInnerWidth = Math.Max(0, Width - 2); + int chartWidth = Math.Max(0, totalInnerWidth - scaleWidth); int totalSubRows = chartHeight * 4; int sampleCount = samples.Length; double displayScale = AutoScale - ? 1.0 / snapshotMax + ? (snapshotMax > 0.0 ? 1.0 / snapshotMax : 1.0) : 1.0; frame.Clear(); frame.MoveTo(X, Y); frame.SetColour(ForegroundColour, BackgroundColour); frame.Append('\u256D'); - frame.Append('\u2500', chartWidth); + frame.Append('\u2500', totalInnerWidth); frame.Append('\u256E'); for (int row = 0; row < chartHeight; row++) { @@ -174,8 +216,31 @@ protected override void OnDraw() frame.Append(ch); } - frame.SetColour(ForegroundColour, BackgroundColour); - frame.Append('\u2502'); + if (showScale) { + bool isIndexRow = (row % 2 == 0) || (row == chartHeight - 1); + frame.SetColour(ForegroundColour, BackgroundColour); + + if (isIndexRow) { + double ratio = chartHeight > 1 ? (double)rowFromBottom / (chartHeight - 1) : 0.0; + double scaleValue = (AutoScale ? snapshotMax : 1.0) * ratio; + string formatted = FormatScaleValue(scaleValue, snapshotMax); + + if (formatted.Length > scaleWidth) { + formatted = formatted[..scaleWidth]; + } + + frame.Append(formatted.PadLeft(scaleWidth)); + frame.Append('\u2524'); + } + else { + frame.Append(' ', scaleWidth); + frame.Append('\u2502'); + } + } + else { + frame.SetColour(ForegroundColour, BackgroundColour); + frame.Append('\u2502'); + } } frame.MoveTo(X, Y + Height - 1); @@ -186,9 +251,9 @@ protected override void OnDraw() : $"{Text} {LabelSeries}"; string labelPadded = label.Length > 0 ? $" {label} " : string.Empty; - int labelLen = Math.Min(labelPadded.Length, chartWidth); - int leftDashes = (chartWidth - labelLen) / 2; - int rightDashes = chartWidth - labelLen - leftDashes; + int labelLen = Math.Min(labelPadded.Length, totalInnerWidth); + int leftDashes = (totalInnerWidth - labelLen) / 2; + int rightDashes = totalInnerWidth - labelLen - leftDashes; frame.Append('\u2570'); frame.Append('\u2500', leftDashes); @@ -199,11 +264,9 @@ protected override void OnDraw() frame.ResetColour(); Terminal.Write(frame.AsSpan()); } - - private void SetCellColour(Color chartColour) => frame.SetColour( - MetreStyle == MetreControlStyle.Blocks ? ForegroundColour : chartColour, - MetreStyle == MetreControlStyle.Blocks ? chartColour : BackgroundColour); - + + public MetreControlStyle MetreStyle { get; set; } = MetreControlStyle.Dots; + protected override void OnResize() { lock (dataLock) { @@ -229,5 +292,23 @@ protected override void OnResize() } } + private int ScaleWidth => IsYAxisScaleVisible ? DefaultScaleWidth : 0; + + private void SetCellColour(Color chartColour) => frame.SetColour( + MetreStyle == MetreControlStyle.Blocks ? ForegroundColour : chartColour, + MetreStyle == MetreControlStyle.Blocks ? chartColour : BackgroundColour); + + public bool ShowYAxisScale + { + get => showYAxisScale; + set + { + if (showYAxisScale != value) { + showYAxisScale = value; + OnResize(); + } + } + } + public string Text { get; set; } = string.Empty; } diff --git a/src/Task.Monitor.System/Controls/ListView/ListView.cs b/src/Task.Monitor.System/Controls/ListView/ListView.cs index 7b84fb0..752ce3b 100644 --- a/src/Task.Monitor.System/Controls/ListView/ListView.cs +++ b/src/Task.Monitor.System/Controls/ListView/ListView.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Drawing; using Task.Monitor.Cli.Utils; @@ -54,10 +55,10 @@ private void CalculateViewPortBounds() viewPort.Bounds = new Rectangle(X, y, Width, Height); if (viewPort.SelectedIndex >= items.Count) { - viewPort.SelectedIndex = items.Count - 1; + viewPort.SelectedIndex = Math.Max(0, items.Count - 1); } if (viewPort.PreviousSelectedIndex >= items.Count) { - viewPort.PreviousSelectedIndex = items.Count - 1; + viewPort.PreviousSelectedIndex = Math.Max(0, items.Count - 1); } if (viewPort.CurrentPageIndex > Math.Max(0, items.Count - viewPort.RowCount)) { viewPort.CurrentPageIndex = Math.Max(0, items.Count - viewPort.RowCount); @@ -528,7 +529,6 @@ protected override void OnKeyPressed(ConsoleKeyInfo keyInfo, ref bool handled) private void RedrawItem() { frame.Clear(); - ListViewItem selectedItem = items[viewPort.SelectedIndex]; DrawItem( diff --git a/src/taskmon/Configuration/AppConfig.cs b/src/taskmon/Configuration/AppConfig.cs index 4b274b2..3678f45 100644 --- a/src/taskmon/Configuration/AppConfig.cs +++ b/src/taskmon/Configuration/AppConfig.cs @@ -238,6 +238,12 @@ public Statistics VisibleColumns set => statsSection?.Add(Constants.Keys.Cols, value.ToString()); } + public bool ShowYAxisScale + { + get => uxSection?.GetBool(Constants.Keys.ShowYAxisScale, true) ?? true; + set => uxSection?.Add(Constants.Keys.ShowYAxisScale, value.ToString()); + } + public Statistics SortColumn { get => sortSection?.GetEnum(Constants.Keys.Col, Statistics.Cpu) ?? Statistics.Cpu; @@ -452,6 +458,7 @@ private void LoadSections() .AddIfMissing(Constants.Keys.ShowMetreGpuMemNumerically, true.ToString()) .AddIfMissing(Constants.Keys.ShowMetreNetworkNumerically, true.ToString()) .AddIfMissing(Constants.Keys.ShowMetreSwapNumerically, true.ToString()) + .AddIfMissing(Constants.Keys.ShowYAxisScale, true.ToString()) .AddIfMissing(Constants.Keys.UseLargeCharts, false.ToString()) .AddIfMissing(Constants.Keys.UseIrixCpuReporting, useIrixMode.ToString()); diff --git a/src/taskmon/Configuration/Constants.cs b/src/taskmon/Configuration/Constants.cs index 24a393c..cab2121 100644 --- a/src/taskmon/Configuration/Constants.cs +++ b/src/taskmon/Configuration/Constants.cs @@ -20,8 +20,8 @@ public sealed class Sections public const string ThemeMsDos = "MS-DOS"; public const string LayoutAllCharts = "All Charts"; - public const string LayoutGpuAndGpuMemoryLarge = "Gpu And Gpu Memory Large"; - public const string LayoutCpuAndMemoryLarge = "Cpu And Memory Large"; + public const string LayoutGpuAndGpuMemoryLarge = "Gpu and Gpu Memory Large"; + public const string LayoutCpuAndMemoryLarge = "Cpu and Memory Large"; } public sealed class Keys @@ -59,6 +59,7 @@ public sealed class Keys public const string ShowMetreGpuNumerically = "show-metre-gpu-numerically"; public const string ShowMetreGpuMemNumerically = "show-metre-gpu-mem-numerically"; public const string ShowMetreNetworkNumerically = "show-metre-network-numerically"; + public const string ShowYAxisScale = "show-y-axis-scale"; public const string UseLargeCharts = "use-large-charts"; public const string UseIrixCpuReporting = "use-irix-cpu-reporting"; diff --git a/src/taskmon/Gui/Controls/HeaderControl.cs b/src/taskmon/Gui/Controls/HeaderControl.cs index af50d88..892e436 100644 --- a/src/taskmon/Gui/Controls/HeaderControl.cs +++ b/src/taskmon/Gui/Controls/HeaderControl.cs @@ -255,6 +255,7 @@ protected override void OnLoad() chart.ColourLow = appConfig.DefaultTheme.RangeLowBackground; chart.ColourMid = appConfig.DefaultTheme.RangeMidBackground; chart.MetreStyle = appConfig.MetreStyle; + chart.ShowYAxisScale = appConfig.ShowYAxisScale; } processor.ProcessorUpdated += OnProcessorUpdated; diff --git a/src/taskmon/Gui/Controls/ProcessControl.cs b/src/taskmon/Gui/Controls/ProcessControl.cs index ccd927b..c42dfae 100644 --- a/src/taskmon/Gui/Controls/ProcessControl.cs +++ b/src/taskmon/Gui/Controls/ProcessControl.cs @@ -12,19 +12,10 @@ namespace Task.Monitor.Gui.Controls; public sealed partial class ProcessControl : Control { - private class CmdLineFilters - { - public int Pid { get; init; } - public string UserName { get; init; } = string.Empty; - public string Process { get; init; } = string.Empty; - public int NumProcs { get; init; } - } - private readonly IProcessor processor; private readonly AppConfig appConfig; private readonly ListView sortView; private readonly ListView processView; - private readonly CmdLineFilters cmdLineFilters; private List allProcesses = []; private SystemStatistics systemStatistics; @@ -48,14 +39,7 @@ public ProcessControl( { this.processor = processor; this.appConfig = appConfig; - - cmdLineFilters = new CmdLineFilters { - Pid = appConfig.FilterPid, - UserName = appConfig.FilterUserName, - Process = appConfig.FilterProcess, - NumProcs = appConfig.NumberOfProcesses - }; - + Statistics sortStatistic = appConfig.SortColumn; sortColumn = sortStatistic switch { @@ -511,17 +495,17 @@ private void UpdateListViewItems() lock (allProcessesLock) { IEnumerable filteredProcesses = allProcesses; - if (cmdLineFilters.Pid > -1) { + if (appConfig.FilterPid > -1) { filteredProcesses = filteredProcesses - .Where(p => p.Pid == cmdLineFilters.Pid); + .Where(p => p.Pid == appConfig.FilterPid); } - else if (!string.IsNullOrWhiteSpace(cmdLineFilters.UserName)) { + else if (!string.IsNullOrWhiteSpace(appConfig.FilterUserName)) { filteredProcesses = filteredProcesses - .Where(p => p.UserName.Contains(cmdLineFilters.UserName, StringComparison.OrdinalIgnoreCase)); + .Where(p => p.UserName.Contains(appConfig.FilterUserName, StringComparison.OrdinalIgnoreCase)); } - else if (!string.IsNullOrWhiteSpace(cmdLineFilters.Process)) { + else if (!string.IsNullOrWhiteSpace(appConfig.FilterProcess)) { filteredProcesses = filteredProcesses - .Where(p => p.ProcessName.Contains(cmdLineFilters.Process, StringComparison.OrdinalIgnoreCase)); + .Where(p => p.ProcessName.Contains(appConfig.FilterProcess, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrWhiteSpace(FilterText)) { @@ -559,9 +543,9 @@ IOrderedEnumerable Sort(Func key) => _ => filteredProcesses.OrderByDescending(p => p.CpuTimePercent) }).ToList(); - if (cmdLineFilters.NumProcs > -1) { + if (appConfig.NumberOfProcesses > -1) { sortedProcesses = sortedProcesses - .Take(cmdLineFilters.NumProcs) + .Take(appConfig.NumberOfProcesses) .ToList(); } diff --git a/src/taskmon/Gui/SetupScreen.cs b/src/taskmon/Gui/SetupScreen.cs index 7c0a01c..5971dc0 100644 --- a/src/taskmon/Gui/SetupScreen.cs +++ b/src/taskmon/Gui/SetupScreen.cs @@ -229,46 +229,51 @@ void AddGeneralItem(string text, string key, bool value) runContext.AppConfig.MultiSelectProcesses); AddGeneralItem( - "Show Cpu meter numerically", + "Show Cpu chart label numerically", Constants.Keys.ShowMetreCpuNumerically, runContext.AppConfig.ShowMetreCpuNumerically); AddGeneralItem( - "Show Gpu meter numerically", + "Show Gpu chart label numerically", Constants.Keys.ShowMetreGpuNumerically, runContext.AppConfig.ShowMetreGpuNumerically); AddGeneralItem( - "Show Memory metre numerically", + "Show Memory chart label numerically", Constants.Keys.ShowMetreMemNumerically, runContext.AppConfig.ShowMetreMemoryNumerically); AddGeneralItem( - "Show Gpu Memory metre numerically", + "Show Gpu Memory chart label numerically", Constants.Keys.ShowMetreGpuMemNumerically, runContext.AppConfig.ShowMetreGpuMemNumerically); #if __WIN32__ AddGeneralItem( - "Show Virtual memory numerically", + "Show Virtual memory chart label numerically", Constants.Keys.ShowMetreSwapNumerically, runContext.AppConfig.ShowMetreSwapNumerically); #endif #if __APPLE__ AddGeneralItem( - "Show Swap memory numerically", + "Show Swap memory chart label numerically", Constants.Keys.ShowMetreSwapNumerically, runContext.AppConfig.ShowMetreSwapNumerically); #endif AddGeneralItem( - "Show Disk metre numerically", + "Show Disk chart label numerically", Constants.Keys.ShowMetreDiskNumerically, runContext.AppConfig.ShowMetreDiskNumerically); AddGeneralItem( - "Show Network metre numerically", + "Show Network chart label numerically", Constants.Keys.ShowMetreNetworkNumerically, runContext.AppConfig.ShowMetreNetworkNumerically); - + + AddGeneralItem( + "Show chart Y axis scale", + Constants.Keys.ShowYAxisScale, + runContext.AppConfig.ShowYAxisScale); + AddGeneralItem( #if __WIN32__ "Use Irix mode for per-process CPU% (individual core saturation)", @@ -416,6 +421,7 @@ void UpdateConfigValue(ListViewItem? sourceItem, Action action) runContext.AppConfig.ShowMetreSwapNumerically = GetItemValueByKey(Constants.Keys.ShowMetreSwapNumerically).Checked; runContext.AppConfig.ShowMetreDiskNumerically = GetItemValueByKey(Constants.Keys.ShowMetreDiskNumerically).Checked; runContext.AppConfig.ShowMetreNetworkNumerically = GetItemValueByKey(Constants.Keys.ShowMetreNetworkNumerically).Checked; + runContext.AppConfig.ShowYAxisScale = GetItemValueByKey(Constants.Keys.ShowYAxisScale).Checked; runContext.AppConfig.UseIrixReporting = GetItemValueByKey(Constants.Keys.UseIrixCpuReporting).Checked; Statistics visibleColumns = Statistics.Process | Statistics.Pid; diff --git a/src/taskmon/Program.cs b/src/taskmon/Program.cs index 14e7760..effb702 100644 --- a/src/taskmon/Program.cs +++ b/src/taskmon/Program.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Reflection; -using System.Text; using Task.Monitor.System; using Task.Monitor.Cli.Utils; using Task.Monitor.Configuration; diff --git a/tests/Task.Monitor.System.Tests/Controls/Chart/ChartTests.cs b/tests/Task.Monitor.System.Tests/Controls/Chart/ChartTests.cs index 40cbcf3..d7ede4d 100644 --- a/tests/Task.Monitor.System.Tests/Controls/Chart/ChartTests.cs +++ b/tests/Task.Monitor.System.Tests/Controls/Chart/ChartTests.cs @@ -99,4 +99,63 @@ public void OnDraw_Emits_The_Low_Bar_Colour_For_Small_Values() ConsolePalette.ForegroundSgr(chart.ColourLow), terminal.Output); } + + [Fact] + public void OnDraw_Does_Not_Draw_YAxisScale_When_Chart_Height_Is_6_Or_Less() + { + RecordingTerminal terminal = new(); + ChartControl chart = CreateChart(terminal, width: 20, height: 8); // chartHeight = 6 (<= 6) + chart.Add(0.5); + + terminal.Reset(); + chart.Draw(); + + Assert.DoesNotContain('┤', terminal.Output); + } + + [Fact] + public void OnDraw_Draws_YAxisScale_When_Chart_Height_Is_Greater_Than_6() + { + RecordingTerminal terminal = new(); + ChartControl chart = CreateChart(terminal, width: 20, height: 9); // chartHeight = 7 (> 6) + chart.Add(1.0); + + terminal.Reset(); + chart.Draw(); + + string output = terminal.Output; + Assert.Contains('┤', output); + Assert.Contains("100%", output); + Assert.Contains("0%", output); + } + + [Fact] + public void OnDraw_Hides_YAxisScale_When_ShowYAxisScale_Is_False() + { + RecordingTerminal terminal = new(); + ChartControl chart = CreateChart(terminal, width: 20, height: 9); + chart.ShowYAxisScale = false; + chart.Add(1.0); + + terminal.Reset(); + chart.Draw(); + + Assert.DoesNotContain('┤', terminal.Output); + } + + [Fact] + public void OnDraw_Uses_CustomYAxisScaleFormatter_When_Provided() + { + RecordingTerminal terminal = new(); + ChartControl chart = CreateChart(terminal, width: 20, height: 9); + chart.CustomYAxisScaleFormatter = val => $"{val:F0}v"; + chart.Add(10.0); + + terminal.Reset(); + chart.Draw(); + + string output = terminal.Output; + Assert.Contains("10v", output); + Assert.Contains("0v", output); + } } diff --git a/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs b/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs index 10b0d80..22034be 100644 --- a/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs +++ b/tests/Task.Monitor.Tests/Configuration/AppConfigTests.cs @@ -66,7 +66,7 @@ public void Constructor_With_FileSystem_And_Config_Initialises_Successfully() show-metre-disk-numerically=True show-metre-mem-numerically=True show-metre-swap-numerically=True -use-large-charts=False +show-y-axis-scale=True use-irix-cpu-reporting=True "; @@ -141,6 +141,7 @@ public void Should_Load_AndOr_Parse_DefaultIniFile(string iniFileData) Assert.True(appConfig.ShowMetreGpuMemNumerically); Assert.True(appConfig.ShowMetreSwapNumerically); Assert.True(appConfig.ShowMetreNetworkNumerically); + Assert.True(appConfig.ShowYAxisScale); if (!string.IsNullOrEmpty(iniFileData)) { Assert.True(appConfig.UseIrixReporting); @@ -179,7 +180,7 @@ public void Should_Load_AndOr_Parse_DefaultIniFile(string iniFileData) show-metre-gpu-numerically=False show-metre-gpu-mem-numerically=False show-metre-network-numerically=False -use-large-charts=True +show-y-axis-scale=False use-irix-cpu-reporting=False "; @@ -246,6 +247,7 @@ public void Should_Load_And_Parse_CustomIniFile() Assert.False(appConfig.ShowMetreGpuNumerically); Assert.False(appConfig.ShowMetreGpuMemNumerically); Assert.False(appConfig.ShowMetreNetworkNumerically); + Assert.False(appConfig.ShowYAxisScale); Assert.False(appConfig.UseIrixReporting); } diff --git a/tests/Task.Monitor.Tests/Gui/SetupScreenTests.cs b/tests/Task.Monitor.Tests/Gui/SetupScreenTests.cs index 76639a8..8da0750 100644 --- a/tests/Task.Monitor.Tests/Gui/SetupScreenTests.cs +++ b/tests/Task.Monitor.Tests/Gui/SetupScreenTests.cs @@ -133,15 +133,19 @@ public static TheoryData ControlSettingData() #endif { "Highlight changed values", "generalView" }, { "Enable multiple process selection", "generalView" }, - { "Show Cpu meter numerically", "generalView" }, - { "Show Disk metre numerically", "generalView" }, - { "Show Memory metre numerically", "generalView" }, + { "Show Cpu chart label numerically", "generalView" }, + { "Show Gpu chart label numerically", "generalView" }, + { "Show Memory chart label numerically", "generalView" }, + { "Show Gpu Memory chart label numerically", "generalView" }, #if __WIN32__ - { "Show Virtual memory numerically", "generalView" }, + { "Show Virtual memory chart label numerically", "generalView" }, #endif #if __APPLE__ - { "Show Swap memory numerically", "generalView" }, + { "Show Swap memory chart label numerically", "generalView" }, #endif + { "Show Disk chart label numerically", "generalView" }, + { "Show Network chart label numerically", "generalView" }, + { "Show chart Y axis scale", "generalView" }, #if __WIN32__ { "Use Irix mode for per-process CPU% (individual core saturation)", "generalView" }, #endif diff --git a/tests/Task.Monitor.Tests/TaskMonAppTests.cs b/tests/Task.Monitor.Tests/TaskMonAppTests.cs index 5fab7cf..c1819ae 100644 --- a/tests/Task.Monitor.Tests/TaskMonAppTests.cs +++ b/tests/Task.Monitor.Tests/TaskMonAppTests.cs @@ -131,6 +131,25 @@ public void Should_Load_Actions_For_Args(string arg, Type actionType) Assert.Equal(actionType, actions[0].GetType()); } + public static TheoryData TargetedArgData() + => new() { + { "--cpu-only", Constants.Sections.LayoutCpuAndMemoryLarge, Statistics.Cpu }, + { "--gpu-only", Constants.Sections.LayoutGpuAndGpuMemoryLarge, Statistics.Gpu } + }; + [Theory] + [MemberData(nameof(TargetedArgData))] + public void Should_Setup_Config_For_Targeted_Monitoring(string arg, string layoutName, Statistics sortColumn) + { + RunContext runContext = new RunContextHelper().GetRunContext(); + TaskMonApp app = new(runContext); + bool result = app.ProcessArgs(new[] { arg }, out List actions); + + Assert.True(result); + Assert.Equal(layoutName, runContext.AppConfig.DefaultLayout.Name); + Assert.Equal(sortColumn, runContext.AppConfig.SortColumn); + Assert.Equal(typeof(RunAppAction), actions[0].GetType()); + } + [Fact] public void Should_Load_RunApp_Action_For_No_Args() {