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

+
+
## 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
[](#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()
{