From 3955af8fd4c4aea7a98e7027cad9c18a343e559e Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Tue, 17 Feb 2026 21:47:23 +0000 Subject: [PATCH 1/7] setup network command parsing and icon logic --- stmx/Commands/NetworkCommand.cs | 72 ++++++++++++++++++++++ stmx/Services/IIconService.cs | 1 + stmx/Services/IconService.cs | 9 +++ stmx/Services/IconServiceOptions.cs | 2 + stmx/Services/SystemStatsServiceOptions.cs | 4 +- stmx/Utils/Enums.cs | 21 +++++++ 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 stmx/Commands/NetworkCommand.cs diff --git a/stmx/Commands/NetworkCommand.cs b/stmx/Commands/NetworkCommand.cs new file mode 100644 index 0000000..088b5bb --- /dev/null +++ b/stmx/Commands/NetworkCommand.cs @@ -0,0 +1,72 @@ +using System.CommandLine; + +using stmx.Services; +using stmx.Utils; + +namespace stmx.Commands; + +class NetworkCommand : Command +{ + private readonly ISystemStatsService _systemStats; + private readonly IIconService _icons; + + public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : base("network", "get current network usage") + { + _systemStats = systemStats ?? throw new ArgumentNullException(nameof(systemStats)); + _icons = icons ?? throw new ArgumentNullException(nameof(icons)); + + var showIconOption = new Option("--show-icon", ["-i"]); + showIconOption.Description = "show icon (optionally provide custom icon)"; + // this allows users to pass one or more values + showIconOption.Arity = ArgumentArity.ZeroOrOne; + Add(showIconOption); + + var directionOption = new Option("--direction", "-d"); + directionOption.Description = "Show upload or download"; + directionOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkDirection; + Add(directionOption); + + var unitOption = new Option("--unit", "-u"); + unitOption.Description = "Select the network unit for display"; + unitOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkUnit; + Add(unitOption); + + SetAction(async (parseResult, cancellationToken) => + { + var iconValue = parseResult.GetValue(showIconOption); + var direction = parseResult.GetValue(directionOption); + var unit = parseResult.GetValue(unitOption); + await ExecuteAsync( + parseResult.GetResult(showIconOption) is not null, + iconValue!, + direction!, + unit! + ); + }); + } + + public async Task ExecuteAsync(bool showIcon, string iconValue, NetworkDirection direction, + NetworkUnits unit) + { + string directionIcon = getDirectionIcon(showIcon, iconValue, direction); + + if (direction == NetworkDirection.Download) + { + + } + else + { + + } + } + + private string getDirectionIcon(bool showIcon, string iconValue, NetworkDirection direction) { + if (showIcon) + { + return string.IsNullOrEmpty(iconValue) + ? $"{_icons.GetDirectionIcon(direction == NetworkDirection.Download)} " + : $"{iconValue} "; + } + return ""; + } +} diff --git a/stmx/Services/IIconService.cs b/stmx/Services/IIconService.cs index bbcef2b..110209f 100644 --- a/stmx/Services/IIconService.cs +++ b/stmx/Services/IIconService.cs @@ -4,5 +4,6 @@ public interface IIconService { Task GetBatteryCapacityIcon(int batteryCapacity); Task GetBatteryStatusIcon(int batteryStatus); + Task GetDirectionIcon(bool download); IconServiceOptions Options { get; } } diff --git a/stmx/Services/IconService.cs b/stmx/Services/IconService.cs index 9f29989..3288d7a 100644 --- a/stmx/Services/IconService.cs +++ b/stmx/Services/IconService.cs @@ -1,3 +1,5 @@ +using stmx.Utils; + namespace stmx.Services; class IconService : IIconService @@ -13,4 +15,11 @@ public Task GetBatteryCapacityIcon(int batteryCapacity) public Task GetBatteryStatusIcon(int batteryStatus) { return Task.FromResult(Options.BatteryStatusIcons[batteryStatus]); } + + public Task GetDirectionIcon(bool download) + { + return Task.FromResult( + (download) ? Options.NetworkDownload : Options.NetworkUpload + ); + } } diff --git a/stmx/Services/IconServiceOptions.cs b/stmx/Services/IconServiceOptions.cs index d893f61..a94576d 100644 --- a/stmx/Services/IconServiceOptions.cs +++ b/stmx/Services/IconServiceOptions.cs @@ -13,4 +13,6 @@ public class IconServiceOptions public string MemoryIcon { get; set; } = ""; public string CpuIcon { get; set; } = ""; public string PercentIcon { get; set; } = ""; + public string NetworkDownload { get; set; } = "󰜮"; + public string NetworkUpload { get; set; } = "󰜷"; } diff --git a/stmx/Services/SystemStatsServiceOptions.cs b/stmx/Services/SystemStatsServiceOptions.cs index 03750cb..b7b2261 100644 --- a/stmx/Services/SystemStatsServiceOptions.cs +++ b/stmx/Services/SystemStatsServiceOptions.cs @@ -8,5 +8,7 @@ public class SystemStatsServiceOptions public bool DefaultShowBatteryChargingIcon { get; set; } = false; public bool DefaultShowBatteryPercent { get; set; } = false; - public MemoryUnits DefaultMemoryUnit {get; set; } = MemoryUnits.Percent; + public MemoryUnits DefaultMemoryUnit { get; set; } = MemoryUnits.Percent; + public NetworkUnits DefaultNetworkUnit { get; set; } = NetworkUnits.KiloBytesPerSecond; + public NetworkDirection DefaultNetworkDirection { get; set; } = NetworkDirection.Download; } diff --git a/stmx/Utils/Enums.cs b/stmx/Utils/Enums.cs index 724c40a..c1f3488 100644 --- a/stmx/Utils/Enums.cs +++ b/stmx/Utils/Enums.cs @@ -13,3 +13,24 @@ public enum MemoryUnits TeraBytes = 8, TibiBytes = 9 } + +public enum NetworkDirection +{ + Upload = 1, + Download = 2 +} + +public enum NetworkUnits +{ + Auto = 0, + BytesPerSecond = 1, + BitsPerSecond = 2, + KiloBytesPerSecond = 3, + KiloBitsPerSecond = 4, + MegaBytesPerSecond = 5, + MegaBitsPerSecond = 6, + GigaBytesPerSecond = 7, + GigaBitsPerSecond = 8, + TeraBytesPerSecond = 9, + TeraBitsPerSecond = 10 +} From 6c86015d3da9cdee0441fbec9a3665322c2a18fd Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sat, 21 Feb 2026 18:10:06 +0000 Subject: [PATCH 2/7] get measurements from sys files --- stmx/Commands/NetworkCommand.cs | 13 ++-- stmx/Program.cs | 1 + stmx/Services/ISystemStatsService.cs | 1 + stmx/Services/LinuxSystemStatsService.cs | 89 ++++++++++++++++++++++ stmx/Services/SystemStatsServiceOptions.cs | 2 +- stmx/Utils/Enums.cs | 15 ++-- 6 files changed, 105 insertions(+), 16 deletions(-) diff --git a/stmx/Commands/NetworkCommand.cs b/stmx/Commands/NetworkCommand.cs index 088b5bb..b6c8654 100644 --- a/stmx/Commands/NetworkCommand.cs +++ b/stmx/Commands/NetworkCommand.cs @@ -26,6 +26,8 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas directionOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkDirection; Add(directionOption); + // option to specify network interface + var unitOption = new Option("--unit", "-u"); unitOption.Description = "Select the network unit for display"; unitOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkUnit; @@ -48,23 +50,24 @@ await ExecuteAsync( public async Task ExecuteAsync(bool showIcon, string iconValue, NetworkDirection direction, NetworkUnits unit) { - string directionIcon = getDirectionIcon(showIcon, iconValue, direction); + string directionIcon = await getDirectionIcon(showIcon, iconValue, direction); + var dataSpeed = await _systemStats.GetNetworkSpeed(unit, 3); if (direction == NetworkDirection.Download) { - + System.Console.Write($"{directionIcon}{dataSpeed.Download:F2}"); } else { - + System.Console.Write($"{directionIcon}{dataSpeed.Upload:F2}"); } } - private string getDirectionIcon(bool showIcon, string iconValue, NetworkDirection direction) { + private async Task getDirectionIcon(bool showIcon, string iconValue, NetworkDirection direction) { if (showIcon) { return string.IsNullOrEmpty(iconValue) - ? $"{_icons.GetDirectionIcon(direction == NetworkDirection.Download)} " + ? $"{await _icons.GetDirectionIcon(direction == NetworkDirection.Download)} " : $"{iconValue} "; } return ""; diff --git a/stmx/Program.cs b/stmx/Program.cs index ea8ea52..a3788de 100644 --- a/stmx/Program.cs +++ b/stmx/Program.cs @@ -15,6 +15,7 @@ static async Task Main(string[] args) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // TODO: introduce a switch to load based on OS services.AddTransient(); diff --git a/stmx/Services/ISystemStatsService.cs b/stmx/Services/ISystemStatsService.cs index a6f39be..33b8341 100644 --- a/stmx/Services/ISystemStatsService.cs +++ b/stmx/Services/ISystemStatsService.cs @@ -8,6 +8,7 @@ public interface ISystemStatsService Task GetBatteryStatus(); Task GetMemoryUsageNumber(MemoryUnits unit); Task GetMemoryUsagePercent(); + Task GetNetworkSpeed(NetworkUnits unit, int delaySecs); Task GetCpuUsagePercent(); SystemStatsServiceOptions Options { get; } } diff --git a/stmx/Services/LinuxSystemStatsService.cs b/stmx/Services/LinuxSystemStatsService.cs index 38ef0ce..a2a7905 100644 --- a/stmx/Services/LinuxSystemStatsService.cs +++ b/stmx/Services/LinuxSystemStatsService.cs @@ -119,6 +119,32 @@ public MemoryUsageData ReadMemoryUsageFromProcFile() return new MemoryUsageData(totalMemory, availableMemory, MemoryUnits.KiloBytes); } + public async Task GetNetworkSpeed(NetworkUnits unit, int delaySecs) + { + // take two samples + var dataOld = ReadNetworkSpeedFromSysFile(); + await Task.Delay(delaySecs * 1000); + var dataNew = ReadNetworkSpeedFromSysFile(); + + // compute speed in bits per second + var uploadSpeed = (dataNew.Upload - dataOld.Upload) / delaySecs; + var downloadSpeed = (dataNew.Download - dataOld.Download) / delaySecs; + var dataSpeed = new NetworkSpeed(uploadSpeed, downloadSpeed, NetworkUnits.Bits); + + // auto or explicit conversion + return (unit == NetworkUnits.Auto) + ? dataSpeed.ConvertAuto() + : dataSpeed.ConvertTo(unit); + } + + public NetworkSpeed ReadNetworkSpeedFromSysFile() + { + var upload = long.Parse(_fileReader.ReadAllText($"/sys/class/net/wlo1/statistics/tx_bytes")) * 8; + var download = long.Parse(_fileReader.ReadAllText($"/sys/class/net/wlo1/statistics/rx_bytes")) * 8; + + return new NetworkSpeed(upload, download, NetworkUnits.Bits); + } + public async Task GetCpuUsagePercent() { int delayMs = 100; @@ -249,3 +275,66 @@ private static double UnitToBytesFactor(MemoryUnits unit) }; } } + +public class NetworkSpeed +{ + public double Upload { set; get; } + public double Download { set; get; } + public NetworkUnits Unit { set; get; } + + public NetworkSpeed(double upload, double download, NetworkUnits unit) + { + Upload = upload; + Download = download; + Unit = unit; + } + + public NetworkSpeed ConvertTo(NetworkUnits targetUnit) + { + return new NetworkSpeed( + ConvertValue(Upload, Unit, targetUnit), + ConvertValue(Download, Unit, targetUnit), + targetUnit + ); + } + + // Assumes that upload and download are in bits + public NetworkSpeed ConvertAuto() + { + // use whichever is highest as reference value + double referenceValue = Math.Max(Upload, Download); + + NetworkUnits targetUnit = referenceValue switch + { + >= 1_099_511_627_776 => NetworkUnits.TeraBits, + >= 1_073_741_824 => NetworkUnits.GigaBits, + >= 1_048_576 => NetworkUnits.MegaBits, + >= 1_024 => NetworkUnits.KiloBits, + _ => NetworkUnits.Bits + }; + + return ConvertTo(targetUnit); + } + + private static double ConvertValue(double value, NetworkUnits from, NetworkUnits to) + { + double bits = value * UnitToBitsFactor(from); + double result = bits / UnitToBitsFactor(to); + + return result; + } + + private static double UnitToBitsFactor(NetworkUnits unit) + { + return unit switch + { + NetworkUnits.Bits => 1, + NetworkUnits.KiloBits => 1_024, + NetworkUnits.MegaBits => 1_048_576, + NetworkUnits.GigaBits => 1_073_741_824, + NetworkUnits.TeraBits => 1_099_511_627_776, + + _ => throw new ArgumentOutOfRangeException(nameof(unit)) + }; + } +} diff --git a/stmx/Services/SystemStatsServiceOptions.cs b/stmx/Services/SystemStatsServiceOptions.cs index b7b2261..0616c90 100644 --- a/stmx/Services/SystemStatsServiceOptions.cs +++ b/stmx/Services/SystemStatsServiceOptions.cs @@ -9,6 +9,6 @@ public class SystemStatsServiceOptions public bool DefaultShowBatteryPercent { get; set; } = false; public MemoryUnits DefaultMemoryUnit { get; set; } = MemoryUnits.Percent; - public NetworkUnits DefaultNetworkUnit { get; set; } = NetworkUnits.KiloBytesPerSecond; + public NetworkUnits DefaultNetworkUnit { get; set; } = NetworkUnits.Auto; public NetworkDirection DefaultNetworkDirection { get; set; } = NetworkDirection.Download; } diff --git a/stmx/Utils/Enums.cs b/stmx/Utils/Enums.cs index c1f3488..f14e1b3 100644 --- a/stmx/Utils/Enums.cs +++ b/stmx/Utils/Enums.cs @@ -23,14 +23,9 @@ public enum NetworkDirection public enum NetworkUnits { Auto = 0, - BytesPerSecond = 1, - BitsPerSecond = 2, - KiloBytesPerSecond = 3, - KiloBitsPerSecond = 4, - MegaBytesPerSecond = 5, - MegaBitsPerSecond = 6, - GigaBytesPerSecond = 7, - GigaBitsPerSecond = 8, - TeraBytesPerSecond = 9, - TeraBitsPerSecond = 10 + Bits = 1, + KiloBits = 2, + MegaBits = 3, + GigaBits = 4, + TeraBits = 5 } From 6b718493d11a05a646f15db193f0c6a69bd0deb6 Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sun, 22 Feb 2026 18:43:45 +0000 Subject: [PATCH 3/7] allow Both directions in network command --- stmx/Commands/NetworkCommand.cs | 74 +++++++++++++++++----- stmx/Services/ISystemStatsService.cs | 3 +- stmx/Services/LinuxSystemStatsService.cs | 72 +++++++++++++-------- stmx/Services/SystemStatsServiceOptions.cs | 1 + stmx/Utils/Enums.cs | 1 + 5 files changed, 108 insertions(+), 43 deletions(-) diff --git a/stmx/Commands/NetworkCommand.cs b/stmx/Commands/NetworkCommand.cs index b6c8654..f8f42b7 100644 --- a/stmx/Commands/NetworkCommand.cs +++ b/stmx/Commands/NetworkCommand.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using System.Text; using stmx.Services; using stmx.Utils; @@ -15,11 +16,16 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas _systemStats = systemStats ?? throw new ArgumentNullException(nameof(systemStats)); _icons = icons ?? throw new ArgumentNullException(nameof(icons)); - var showIconOption = new Option("--show-icon", ["-i"]); - showIconOption.Description = "show icon (optionally provide custom icon)"; + var showDownloadIconOption = new Option("--show-download-icon", ["-di"]); + showDownloadIconOption.Description = "show download icon (optionally provide custom icon)"; + showDownloadIconOption.Arity = ArgumentArity.ZeroOrOne; + Add(showDownloadIconOption); + + var showUploadIconOption = new Option("--show-upload-icon", ["-ui"]); + showUploadIconOption.Description = "show upload icon (optionally provide custom icon)"; // this allows users to pass one or more values - showIconOption.Arity = ArgumentArity.ZeroOrOne; - Add(showIconOption); + showUploadIconOption.Arity = ArgumentArity.ZeroOrOne; + Add(showUploadIconOption); var directionOption = new Option("--direction", "-d"); directionOption.Description = "Show upload or download"; @@ -27,6 +33,16 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas Add(directionOption); // option to specify network interface + var networkOption = new Option("--network", "-n"); + networkOption.Description = "Select the network interface"; + networkOption.Required = true; + Add(networkOption); + + // option to specify sample delay + var delayOption = new Option("--time-delay", "-t"); + delayOption.Description = "Set the delay for sampling network speeds in seconds"; + delayOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkDelay; + Add(delayOption); var unitOption = new Option("--unit", "-u"); unitOption.Description = "Select the network unit for display"; @@ -35,39 +51,65 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas SetAction(async (parseResult, cancellationToken) => { - var iconValue = parseResult.GetValue(showIconOption); + var uploadIconValue = parseResult.GetValue(showUploadIconOption); + var downloadIconValue = parseResult.GetValue(showDownloadIconOption); var direction = parseResult.GetValue(directionOption); + var network = parseResult.GetValue(networkOption); + var delay = parseResult.GetValue(delayOption); var unit = parseResult.GetValue(unitOption); await ExecuteAsync( - parseResult.GetResult(showIconOption) is not null, - iconValue!, + parseResult.GetResult(showUploadIconOption) is not null, + uploadIconValue!, + parseResult.GetResult(showDownloadIconOption) is not null, + downloadIconValue!, direction!, + network!, + delay!, unit! ); }); } - public async Task ExecuteAsync(bool showIcon, string iconValue, NetworkDirection direction, + public async Task ExecuteAsync( + bool showUploadIcon, + string uploadIconValue, + bool showDownloadIcon, + string downloadIconValue, + NetworkDirection direction, + string network, + int delay, NetworkUnits unit) { - string directionIcon = await getDirectionIcon(showIcon, iconValue, direction); - var dataSpeed = await _systemStats.GetNetworkSpeed(unit, 3); - if (direction == NetworkDirection.Download) + + var dataSpeed = await _systemStats.GetNetworkSpeed(unit, network, delay); + var commandOutputSb = new StringBuilder(); + + if (direction is NetworkDirection.Download or NetworkDirection.Both) { - System.Console.Write($"{directionIcon}{dataSpeed.Download:F2}"); + string downloadIcon = GetDirectionIcon(showDownloadIcon, downloadIconValue, NetworkDirection.Download); + commandOutputSb.Append($"{downloadIcon}{dataSpeed.Download.Value:F2}{dataSpeed.Download.UnitToString()}"); } - else + if (direction is NetworkDirection.Both) + commandOutputSb.Append(" "); + if (direction is NetworkDirection.Upload or NetworkDirection.Both) { - System.Console.Write($"{directionIcon}{dataSpeed.Upload:F2}"); + string uploadIcon = GetDirectionIcon(showUploadIcon, uploadIconValue, NetworkDirection.Upload); + commandOutputSb.Append($"{uploadIcon}{dataSpeed.Upload.Value:F2}{dataSpeed.Upload.UnitToString()}"); } + + System.Console.Write(commandOutputSb.ToString()); } - private async Task getDirectionIcon(bool showIcon, string iconValue, NetworkDirection direction) { + private string GetDirectionIcon(bool showIcon, string iconValue, NetworkDirection direction) { if (showIcon) { + var defaultIcon = direction == NetworkDirection.Upload + ? _icons.Options.NetworkUpload + : _icons.Options.NetworkDownload; + return string.IsNullOrEmpty(iconValue) - ? $"{await _icons.GetDirectionIcon(direction == NetworkDirection.Download)} " + ? $"{defaultIcon} " : $"{iconValue} "; } return ""; diff --git a/stmx/Services/ISystemStatsService.cs b/stmx/Services/ISystemStatsService.cs index 33b8341..f55923c 100644 --- a/stmx/Services/ISystemStatsService.cs +++ b/stmx/Services/ISystemStatsService.cs @@ -8,7 +8,8 @@ public interface ISystemStatsService Task GetBatteryStatus(); Task GetMemoryUsageNumber(MemoryUnits unit); Task GetMemoryUsagePercent(); - Task GetNetworkSpeed(NetworkUnits unit, int delaySecs); + Task<(NetworkSpeed Upload, NetworkSpeed Download)> GetNetworkSpeed( + NetworkUnits unit, string network, int delaySecs); Task GetCpuUsagePercent(); SystemStatsServiceOptions Options { get; } } diff --git a/stmx/Services/LinuxSystemStatsService.cs b/stmx/Services/LinuxSystemStatsService.cs index a2a7905..0a6f389 100644 --- a/stmx/Services/LinuxSystemStatsService.cs +++ b/stmx/Services/LinuxSystemStatsService.cs @@ -119,30 +119,42 @@ public MemoryUsageData ReadMemoryUsageFromProcFile() return new MemoryUsageData(totalMemory, availableMemory, MemoryUnits.KiloBytes); } - public async Task GetNetworkSpeed(NetworkUnits unit, int delaySecs) + public async Task<(NetworkSpeed Upload, NetworkSpeed Download)> GetNetworkSpeed( + NetworkUnits unit, string network, int delaySecs) { // take two samples - var dataOld = ReadNetworkSpeedFromSysFile(); + var dataOld = ReadNetworkSpeedFromSysFile(network); await Task.Delay(delaySecs * 1000); - var dataNew = ReadNetworkSpeedFromSysFile(); + var dataNew = ReadNetworkSpeedFromSysFile(network); // compute speed in bits per second - var uploadSpeed = (dataNew.Upload - dataOld.Upload) / delaySecs; - var downloadSpeed = (dataNew.Download - dataOld.Download) / delaySecs; - var dataSpeed = new NetworkSpeed(uploadSpeed, downloadSpeed, NetworkUnits.Bits); + var uploadSpeed = new NetworkSpeed((dataNew.Upload - dataOld.Upload) / delaySecs, NetworkUnits.Bits); + var downloadSpeed = new NetworkSpeed((dataNew.Download - dataOld.Download) / delaySecs, NetworkUnits.Bits); // auto or explicit conversion - return (unit == NetworkUnits.Auto) - ? dataSpeed.ConvertAuto() - : dataSpeed.ConvertTo(unit); + if (unit == NetworkUnits.Auto) + { + return (uploadSpeed.ConvertAuto(), downloadSpeed.ConvertAuto()); + } + else + { + return (uploadSpeed.ConvertTo(unit), downloadSpeed.ConvertTo(unit)); + } } - public NetworkSpeed ReadNetworkSpeedFromSysFile() + // Returns current values of tx and rx counters + public (long Upload, long Download) ReadNetworkSpeedFromSysFile(string network_interface = "wlo1") { - var upload = long.Parse(_fileReader.ReadAllText($"/sys/class/net/wlo1/statistics/tx_bytes")) * 8; - var download = long.Parse(_fileReader.ReadAllText($"/sys/class/net/wlo1/statistics/rx_bytes")) * 8; + string basePath = $"/sys/class/net/{network_interface}"; + if (!_fileSystem.DirectoryExists(basePath)) + throw new IOException($"No entry found for network {network_interface}"); + + var upload = long.Parse(_fileReader.ReadAllText( + $"/sys/class/net/{network_interface}/statistics/tx_bytes")) * 8; + var download = long.Parse(_fileReader.ReadAllText( + $"/sys/class/net/{network_interface}/statistics/rx_bytes")) * 8; - return new NetworkSpeed(upload, download, NetworkUnits.Bits); + return (upload, download); } public async Task GetCpuUsagePercent() @@ -278,22 +290,19 @@ private static double UnitToBytesFactor(MemoryUnits unit) public class NetworkSpeed { - public double Upload { set; get; } - public double Download { set; get; } + public double Value { set; get; } public NetworkUnits Unit { set; get; } - public NetworkSpeed(double upload, double download, NetworkUnits unit) + public NetworkSpeed(double value, NetworkUnits unit) { - Upload = upload; - Download = download; + Value = value; Unit = unit; } public NetworkSpeed ConvertTo(NetworkUnits targetUnit) { return new NetworkSpeed( - ConvertValue(Upload, Unit, targetUnit), - ConvertValue(Download, Unit, targetUnit), + ConvertValue(Value, Unit, targetUnit), targetUnit ); } @@ -301,16 +310,13 @@ public NetworkSpeed ConvertTo(NetworkUnits targetUnit) // Assumes that upload and download are in bits public NetworkSpeed ConvertAuto() { - // use whichever is highest as reference value - double referenceValue = Math.Max(Upload, Download); - - NetworkUnits targetUnit = referenceValue switch + NetworkUnits targetUnit = Value switch { >= 1_099_511_627_776 => NetworkUnits.TeraBits, >= 1_073_741_824 => NetworkUnits.GigaBits, >= 1_048_576 => NetworkUnits.MegaBits, - >= 1_024 => NetworkUnits.KiloBits, - _ => NetworkUnits.Bits + _ => NetworkUnits.KiloBits + // not including bits per second with Auto option }; return ConvertTo(targetUnit); @@ -337,4 +343,18 @@ private static double UnitToBitsFactor(NetworkUnits unit) _ => throw new ArgumentOutOfRangeException(nameof(unit)) }; } + + public string UnitToString() + { + return Unit switch + { + NetworkUnits.Bits => "b", + NetworkUnits.KiloBits => "k", + NetworkUnits.MegaBits => "m", + NetworkUnits.GigaBits => "g", + NetworkUnits.TeraBits => "t", + + _ => throw new ArgumentOutOfRangeException(nameof(Unit)) + }; + } } diff --git a/stmx/Services/SystemStatsServiceOptions.cs b/stmx/Services/SystemStatsServiceOptions.cs index 0616c90..fc5235d 100644 --- a/stmx/Services/SystemStatsServiceOptions.cs +++ b/stmx/Services/SystemStatsServiceOptions.cs @@ -10,5 +10,6 @@ public class SystemStatsServiceOptions public MemoryUnits DefaultMemoryUnit { get; set; } = MemoryUnits.Percent; public NetworkUnits DefaultNetworkUnit { get; set; } = NetworkUnits.Auto; + public int DefaultNetworkDelay { get; set; } = 3; public NetworkDirection DefaultNetworkDirection { get; set; } = NetworkDirection.Download; } diff --git a/stmx/Utils/Enums.cs b/stmx/Utils/Enums.cs index f14e1b3..0b518f3 100644 --- a/stmx/Utils/Enums.cs +++ b/stmx/Utils/Enums.cs @@ -16,6 +16,7 @@ public enum MemoryUnits public enum NetworkDirection { + Both = 0, Upload = 1, Download = 2 } From 65977e0a0e2102dedf23d668cf0b87c55a33def0 Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sun, 22 Feb 2026 19:20:56 +0000 Subject: [PATCH 4/7] add tests --- stmx.tests/Commands/NetworkCommandTest.cs | 213 ++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 stmx.tests/Commands/NetworkCommandTest.cs diff --git a/stmx.tests/Commands/NetworkCommandTest.cs b/stmx.tests/Commands/NetworkCommandTest.cs new file mode 100644 index 0000000..5dc3a15 --- /dev/null +++ b/stmx.tests/Commands/NetworkCommandTest.cs @@ -0,0 +1,213 @@ +using Moq; +using stmx.Commands; +using stmx.Services; +using stmx.Utils; + +namespace stmx.Tests; + +public class NetworkCommandTests +{ + private Mock _mockStats; + private Mock _mockIcons; + private NetworkCommand _cmd; + + [SetUp] + public void SetUp() + { + _mockStats = new Mock(); + _mockIcons = new Mock(); + + _mockStats.SetupGet(s => s.Options).Returns(new SystemStatsServiceOptions + { + DefaultNetworkDirection = NetworkDirection.Both, + DefaultNetworkUnit = NetworkUnits.Auto, + DefaultNetworkDelay = 1 + }); + _mockIcons.SetupGet(i => i.Options).Returns(new IconServiceOptions + { + NetworkDownload = "DOWN_ICON", + NetworkUpload = "UP_ICON" + }); + + _cmd = new NetworkCommand(_mockStats.Object, _mockIcons.Object); + } + + private void SetupNetworkSpeed(double upload, double download, NetworkUnits unit = NetworkUnits.KiloBits) + { + _mockStats.Setup(s => s.GetNetworkSpeed(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(( + new NetworkSpeed(upload, unit), + new NetworkSpeed(download, unit) + )); + } + + [Test] + public async Task TestNetworkCommand_PrintsBothDirections() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Both, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("20.30k 10.50k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsDownloadOnly() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Download, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("20.30k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsUploadOnly() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Upload, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("10.50k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsDefaultDownloadIcon() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: true, downloadIconValue: "", + direction: NetworkDirection.Download, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("DOWN_ICON 20.30k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsDefaultUploadIcon() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: true, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Upload, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("UP_ICON 10.50k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsCustomDownloadIcon() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: true, downloadIconValue: "↓", + direction: NetworkDirection.Download, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("↓ 20.30k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsCustomUploadIcon() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: true, uploadIconValue: "↑", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Upload, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("↑ 10.50k")); + } + + [Test] + public async Task TestNetworkCommand_PrintsBothWithIcons() + { + SetupNetworkSpeed(10.5, 20.3); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: true, uploadIconValue: "↑", + showDownloadIcon: true, downloadIconValue: "↓", + direction: NetworkDirection.Both, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("↓ 20.30k ↑ 10.50k")); + } + + [Test] + public async Task TestNetworkCommand_FormatsToTwoDecimalPlaces() + { + SetupNetworkSpeed(10.1234, 20.5678); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Both, + network: "wlo1", delay: 1, unit: NetworkUnits.KiloBits + ); + + Assert.That(consoleOut.ToString(), Is.EqualTo("20.57k 10.12k")); + } + + [Test] + public async Task TestNetworkCommand_UsesCorrectNetworkInterface() + { + SetupNetworkSpeed(0, 0); + var consoleOut = new StringWriter(); + Console.SetOut(consoleOut); + + await _cmd.ExecuteAsync( + showUploadIcon: false, uploadIconValue: "", + showDownloadIcon: false, downloadIconValue: "", + direction: NetworkDirection.Both, + network: "eth0", delay: 1, unit: NetworkUnits.KiloBits + ); + + _mockStats.Verify(s => s.GetNetworkSpeed(It.IsAny(), "eth0", It.IsAny()), Times.Once); + } +} From 92c4088c8aefb16c279a2d7e0152f39bfea5ce6f Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sun, 22 Feb 2026 19:35:58 +0000 Subject: [PATCH 5/7] Handle some edge cases --- stmx/Commands/NetworkCommand.cs | 6 ++++++ stmx/Services/LinuxSystemStatsService.cs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/stmx/Commands/NetworkCommand.cs b/stmx/Commands/NetworkCommand.cs index f8f42b7..c279cb5 100644 --- a/stmx/Commands/NetworkCommand.cs +++ b/stmx/Commands/NetworkCommand.cs @@ -42,6 +42,12 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas var delayOption = new Option("--time-delay", "-t"); delayOption.Description = "Set the delay for sampling network speeds in seconds"; delayOption.DefaultValueFactory = _ => _systemStats.Options.DefaultNetworkDelay; + delayOption.Validators.Add(result => + { + var value = result.GetValueOrDefault(); + if (value <= 0) + result.AddError("Delay must be greater than 0"); + }); Add(delayOption); var unitOption = new Option("--unit", "-u"); diff --git a/stmx/Services/LinuxSystemStatsService.cs b/stmx/Services/LinuxSystemStatsService.cs index 0a6f389..bfae3d2 100644 --- a/stmx/Services/LinuxSystemStatsService.cs +++ b/stmx/Services/LinuxSystemStatsService.cs @@ -128,8 +128,16 @@ public MemoryUsageData ReadMemoryUsageFromProcFile() var dataNew = ReadNetworkSpeedFromSysFile(network); // compute speed in bits per second - var uploadSpeed = new NetworkSpeed((dataNew.Upload - dataOld.Upload) / delaySecs, NetworkUnits.Bits); - var downloadSpeed = new NetworkSpeed((dataNew.Download - dataOld.Download) / delaySecs, NetworkUnits.Bits); + // If the counter was reset to 0 for second sample + // set the diff to 0 using Math.Max() to avoid negative value + var uploadSpeed = new NetworkSpeed( + Math.Max(0, dataNew.Upload - dataOld.Upload) / delaySecs, + NetworkUnits.Bits + ); + var downloadSpeed = new NetworkSpeed( + Math.Max(0, dataNew.Download - dataOld.Download) / delaySecs, + NetworkUnits.Bits + ); // auto or explicit conversion if (unit == NetworkUnits.Auto) From b53d18d745496ce2fbaf19bf4dddf80fb361ee01 Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sun, 22 Feb 2026 19:36:25 +0000 Subject: [PATCH 6/7] add tests for new network rx tx file ops and parsing --- .../Services/LinuxSystemStatsServiceTest.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/stmx.tests/Services/LinuxSystemStatsServiceTest.cs b/stmx.tests/Services/LinuxSystemStatsServiceTest.cs index c48069b..90f2004 100644 --- a/stmx.tests/Services/LinuxSystemStatsServiceTest.cs +++ b/stmx.tests/Services/LinuxSystemStatsServiceTest.cs @@ -265,5 +265,125 @@ public async Task GetCpuUsagePercent_ReturnsNull_OnIOException() Assert.That(result, Is.Null); } + + [Test] + public void ReadNetworkSpeedFromSysFile_ParsesCorrectly() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("1000") // tx_bytes + .Returns("2000"); // rx_bytes + + var (upload, download) = _service.ReadNetworkSpeedFromSysFile("wlo1"); + + Assert.That(upload, Is.EqualTo(1000 * 8)); + Assert.That(download, Is.EqualTo(2000 * 8)); + } + + [Test] + public void ReadNetworkSpeedFromSysFile_ThrowsIOException_WhenInterfaceNotFound() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/eth99")).Returns(false); + + Assert.Throws(() => _service.ReadNetworkSpeedFromSysFile("eth99")); + } + + [Test] + public async Task GetNetworkSpeed_ComputesCorrectBitsPerSecond() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + + // First sample: 1000 tx, 2000 rx bytes + // Second sample: 4000 tx, 7000 rx bytes + // Over 1 second delay: upload = (4000-1000)*8 = 24000 bps, download = (7000-2000)*8 = 40000 bps + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("1000") + .Returns("2000") + .Returns("4000") + .Returns("7000"); + + var (upload, download) = await _service.GetNetworkSpeed(NetworkUnits.Bits, "wlo1", 1); + + Assert.That(upload.Value, Is.EqualTo(24000)); + Assert.That(download.Value, Is.EqualTo(40000)); + Assert.That(upload.Unit, Is.EqualTo(NetworkUnits.Bits)); + Assert.That(download.Unit, Is.EqualTo(NetworkUnits.Bits)); + } + + [Test] + public async Task GetNetworkSpeed_ConvertsToKiloBits() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + + // upload = (4000-1000)*8 = 24000 bps = ~23.4375 kbps + // download = (7000-2000)*8 = 40000 bps = ~39.0625 kbps + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("1000") + .Returns("2000") + .Returns("4000") + .Returns("7000"); + + var (upload, download) = await _service.GetNetworkSpeed(NetworkUnits.KiloBits, "wlo1", 1); + + Assert.That(upload.Unit, Is.EqualTo(NetworkUnits.KiloBits)); + Assert.That(download.Unit, Is.EqualTo(NetworkUnits.KiloBits)); + Assert.That(upload.Value, Is.EqualTo(24000.0 / 1024).Within(0.001)); + Assert.That(download.Value, Is.EqualTo(40000.0 / 1024).Within(0.001)); + } + + [Test] + public async Task GetNetworkSpeed_Auto_ReturnsKiloBitsAtMinimum() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + + // Very low speed — should still return KiloBits not Bits + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("1000") + .Returns("2000") + .Returns("1001") + .Returns("2001"); + + var (upload, download) = await _service.GetNetworkSpeed(NetworkUnits.Auto, "wlo1", 1); + + Assert.That(upload.Unit, Is.EqualTo(NetworkUnits.KiloBits)); + Assert.That(download.Unit, Is.EqualTo(NetworkUnits.KiloBits)); + } + + [Test] + public async Task GetNetworkSpeed_Auto_SelectsMegaBits_WhenSpeedIsHigh() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + + // delta = 200_000 bytes = 1_600_000 bits > 1_048_576 → MegaBits + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("0") + .Returns("0") + .Returns("200000") + .Returns("200000"); + + var (upload, download) = await _service.GetNetworkSpeed(NetworkUnits.Auto, "wlo1", 1); + + Assert.That(upload.Unit, Is.EqualTo(NetworkUnits.MegaBits)); + Assert.That(download.Unit, Is.EqualTo(NetworkUnits.MegaBits)); + } + + [Test] + public async Task GetNetworkSpeed_HandlesCounterReset_Gracefully() + { + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/wlo1")).Returns(true); + + // Counter went backwards (interface reset) + _fileReaderMock.SetupSequence(f => f.ReadAllText(It.IsAny())) + .Returns("5000") + .Returns("5000") + .Returns("100") + .Returns("100"); + + var (upload, download) = await _service.GetNetworkSpeed(NetworkUnits.Bits, "wlo1", 1); + + // Should not be negative + Assert.That(upload.Value, Is.GreaterThanOrEqualTo(0)); + Assert.That(download.Value, Is.GreaterThanOrEqualTo(0)); + } } From cad89ac3c8cfdb8ee09b69bdaba16a090c258ee3 Mon Sep 17 00:00:00 2001 From: Anupal Mishra Date: Sun, 22 Feb 2026 19:51:58 +0000 Subject: [PATCH 7/7] change default for direction to both, figure out default interface --- .../Services/LinuxSystemStatsServiceTest.cs | 17 +++++++++++++++++ stmx/Commands/NetworkCommand.cs | 1 + stmx/Services/LinuxSystemStatsService.cs | 16 ++++++++++++++++ stmx/Services/SystemStatsServiceOptions.cs | 2 +- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/stmx.tests/Services/LinuxSystemStatsServiceTest.cs b/stmx.tests/Services/LinuxSystemStatsServiceTest.cs index 90f2004..29b80f9 100644 --- a/stmx.tests/Services/LinuxSystemStatsServiceTest.cs +++ b/stmx.tests/Services/LinuxSystemStatsServiceTest.cs @@ -385,5 +385,22 @@ public async Task GetNetworkSpeed_HandlesCounterReset_Gracefully() Assert.That(upload.Value, Is.GreaterThanOrEqualTo(0)); Assert.That(download.Value, Is.GreaterThanOrEqualTo(0)); } + + [Test] + public void ReadNetworkSpeedFromSysFile_UsesDefaultInterface_WhenPassedDefault() + { + string mockRoute = "Iface\tDestination\tGateway\n" + + "eth0\t00000000\t0101A8C0\n"; + + _fileSystemMock.Setup(fs => fs.DirectoryExists("/sys/class/net/eth0")).Returns(true); + _fileReaderMock.Setup(f => f.ReadAllText("/proc/net/route")).Returns(mockRoute); + _fileReaderMock.Setup(f => f.ReadAllText("/sys/class/net/eth0/statistics/tx_bytes")).Returns("1000"); + _fileReaderMock.Setup(f => f.ReadAllText("/sys/class/net/eth0/statistics/rx_bytes")).Returns("2000"); + + var (upload, download) = _service.ReadNetworkSpeedFromSysFile("default"); + + Assert.That(upload, Is.EqualTo(1000 * 8)); + Assert.That(download, Is.EqualTo(2000 * 8)); + } } diff --git a/stmx/Commands/NetworkCommand.cs b/stmx/Commands/NetworkCommand.cs index c279cb5..112669b 100644 --- a/stmx/Commands/NetworkCommand.cs +++ b/stmx/Commands/NetworkCommand.cs @@ -36,6 +36,7 @@ public NetworkCommand(ISystemStatsService systemStats, IIconService icons) : bas var networkOption = new Option("--network", "-n"); networkOption.Description = "Select the network interface"; networkOption.Required = true; + networkOption.DefaultValueFactory = _ => "default"; Add(networkOption); // option to specify sample delay diff --git a/stmx/Services/LinuxSystemStatsService.cs b/stmx/Services/LinuxSystemStatsService.cs index bfae3d2..023f190 100644 --- a/stmx/Services/LinuxSystemStatsService.cs +++ b/stmx/Services/LinuxSystemStatsService.cs @@ -153,6 +153,9 @@ public MemoryUsageData ReadMemoryUsageFromProcFile() // Returns current values of tx and rx counters public (long Upload, long Download) ReadNetworkSpeedFromSysFile(string network_interface = "wlo1") { + if (network_interface == "default") + network_interface = GetDefaultNetworkInterface(); + string basePath = $"/sys/class/net/{network_interface}"; if (!_fileSystem.DirectoryExists(basePath)) throw new IOException($"No entry found for network {network_interface}"); @@ -165,6 +168,19 @@ public MemoryUsageData ReadMemoryUsageFromProcFile() return (upload, download); } + public string GetDefaultNetworkInterface() + { + var lines = _fileReader.ReadAllText("/proc/net/route").Split('\n', StringSplitOptions.RemoveEmptyEntries); + // skip header line, find route with destination 00000000 (default) + foreach (var line in lines.Skip(1)) + { + var fields = line.Split('\t'); + if (fields.Length > 1 && fields[1] == "00000000") + return fields[0]; + } + throw new IOException("No default network interface found"); + } + public async Task GetCpuUsagePercent() { int delayMs = 100; diff --git a/stmx/Services/SystemStatsServiceOptions.cs b/stmx/Services/SystemStatsServiceOptions.cs index fc5235d..4f8d205 100644 --- a/stmx/Services/SystemStatsServiceOptions.cs +++ b/stmx/Services/SystemStatsServiceOptions.cs @@ -11,5 +11,5 @@ public class SystemStatsServiceOptions public MemoryUnits DefaultMemoryUnit { get; set; } = MemoryUnits.Percent; public NetworkUnits DefaultNetworkUnit { get; set; } = NetworkUnits.Auto; public int DefaultNetworkDelay { get; set; } = 3; - public NetworkDirection DefaultNetworkDirection { get; set; } = NetworkDirection.Download; + public NetworkDirection DefaultNetworkDirection { get; set; } = NetworkDirection.Both; }