Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ What changed in each release. The section for a version is lifted into that vers

Newest first. Add a section before tagging.

## 0.8.0 - 2026-08-21

- **`tui --driver <name>`**, and `tui --driver list` to see what your platform offers. Terminal.Gui ships three console drivers (`windows`, `ansi`, `dotnet`) and picks one for you. Since a repaint costs whatever the driver and console between them make it cost, and that varies enormously, this makes it something you can change rather than something you are stuck with.
- **`tui --bench`** times what one screen repaint actually costs on your console, because a repaint is exactly what one typed character costs. Run it per driver and use the quickest:

```
tait-codeplug tui --bench
tait-codeplug tui --bench --driver ansi
tait-codeplug tui --bench --driver dotnet
```

It prints the screen size, the driver, and a median over 30 repaints. For scale, on Linux in tmux at 100x30 this machine gives 17.7 ms on `ansi` and 8.5 ms on `dotnet`; anything under about 30 ms feels instant, and a few hundred milliseconds is the editor feeling sluggish.

This is aimed at the report of typing being slow in the editor **on Windows, with the tool running locally**. That rules out the link, which leaves how the driver hands a repaint to the console, and on Windows that cost is far higher per call than on a Unix pty. Which of the three drivers is quickest there is not something that can be settled from a Linux box, so the tool now measures it where it matters.

## 0.7.0 - 2026-08-21

- **"Power-cycle the radio now" is a prompt, not a line in the log.** A read or a write puts it on the screen where it cannot be missed, and it takes itself back down the moment the radio answers - the normal case needs no keystroke at all. Cancel, or Esc, abandons the operation, which is the way out when the radio is not going to answer rather than sitting through the full 90-second wait.
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,21 @@ Left alone, it goes quiet: the main loop steps down after ten seconds untouched

It is, and it is worth knowing why before you go looking for a fault at your end. Terminal.Gui repaints the whole screen for every character typed into a text box - about 7-8 bytes per cell on screen, so 22 KB on a 100x30 terminal and 82 KB at 200x50, per keystroke. A minimal Terminal.Gui app does the same, so it is the library rather than this tool, and there is nothing to configure around it.

Locally you will not notice. Across an SSH link to a maximised terminal it is a second or two per character. What helps:
How much that costs you depends on the console and on which of Terminal.Gui's three drivers is in front of it, and the difference between them is large. Measure it on your own machine rather than trusting a number from someone else's:

```sh
tait-codeplug tui --driver list # what this platform offers
tait-codeplug tui --bench # time one repaint on the default driver
tait-codeplug tui --bench --driver ansi
```

Under about 30ms per repaint feels instant; a few hundred milliseconds is the editor feeling sluggish. If another driver is quicker, use it: `tait-codeplug tui --driver ansi radio.m8p`.

Beyond that:

- Make the terminal window smaller while you are editing: 80x24 costs a sixth of what 200x50 does.
- Skip the editor for a single value: `tait-codeplug patch /dev/ttyUSB0 ch0.rxfreq 144.812500` does a read-modify-write with no typing in a UI at all.
- Run the tool on the machine the radio is plugged into, rather than across the link.
- Over SSH, run the tool on the machine the radio is plugged into rather than across the link.

Colours are true-colour: a dark slate palette, green for read, amber for write (it is the one that changes your radio), red for errors. Terminal.Gui maps them down on a 16- or 256-colour terminal, so it stays legible on a plain console.

Expand Down
51 changes: 45 additions & 6 deletions src/M0LTE.Tait.Codeplug.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
case "--upgrade":
return SelfUpgrade.RunAsync().GetAwaiter().GetResult();
case "tui":
return CmdTui(args.Length > 1 ? args[1] : null);
return CmdTui(args);
case "help":
case "--help":
case "-h":
Expand Down Expand Up @@ -106,17 +106,54 @@ static int CmdChannel(string[] args)
}
}

// tui [file.m8p] - the interactive editor, optionally opened on a saved codeplug rather than
// starting empty and reading the radio. Same screen either way.
static int CmdTui(string? path)
// tui [--driver <name>] [file.m8p] - the interactive editor, optionally opened on a saved codeplug
// rather than starting empty and reading the radio. Same screen either way.
static int CmdTui(string[] args)
{
string? driver = null;
string? path = null;
bool bench = false;

for (int i = 1; i < args.Length; i++)
{
if (args[i] == "--bench")
{
bench = true;
continue;
}

if (args[i] == "--driver")
{
driver = i + 1 < args.Length
? args[i + 1]
: throw new FormatException("--driver needs a name, or 'list' to see what is available");
i++;
continue;
}

path ??= args[i];
}

if (string.Equals(driver, "list", StringComparison.OrdinalIgnoreCase))
{
TuiDriverChoice.PrintAvailable(Console.Out);
return 0;
}

string? resolved = TuiDriverChoice.Resolve(driver);

if (bench)
{
return TuiBench.Run(resolved, Console.Out);
}

if (path is null)
{
return Tui.Run();
return Tui.Run(driver: resolved);
}

CodeplugImage image = CodeplugImage.LoadM8p(File.ReadAllText(path));
return Tui.Run(image, path);
return Tui.Run(image, path, resolved);
}

static int CmdParse(string source)
Expand Down Expand Up @@ -327,6 +364,8 @@ static void PrintUsage()
Console.WriteLine("usage:");
Console.WriteLine(" (no arguments) interactive mode: pick a port, read, edit, write");
Console.WriteLine(" tui [file.m8p] interactive mode, optionally opened on a saved codeplug");
Console.WriteLine(" tui --driver <name|list> [file.m8p] force a console driver (try this if typing is slow)");
Console.WriteLine(" tui --bench [--driver <name>] time what one screen repaint costs on this console");
Console.WriteLine(" --upgrade replace this binary with the latest GitHub release");
Console.WriteLine(" parse <file.m8p | port> verify checksums + section map (file or live radio)");
Console.WriteLine(" dump <file.m8p | port> decode every mapped field (file or live radio)");
Expand Down
11 changes: 10 additions & 1 deletion src/M0LTE.Tait.Codeplug.Cli/Tui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ internal static class Tui

private static DateTime _lastInputUtc = DateTime.UtcNow;

internal static int Run(CodeplugImage? initial = null, string? source = null)
/// <param name="initial">A codeplug to open on, or null to start empty.</param>
/// <param name="source">Where <paramref name="initial"/> came from, for the log line.</param>
/// <param name="driver">A Terminal.Gui driver name to force, or null to let it choose. See
/// <see cref="TuiDriverChoice"/> for why this is worth being able to change.</param>
internal static int Run(CodeplugImage? initial = null, string? source = null, string? driver = null)
{
_app = Application.Create();
try
{
if (driver is not null)
{
_app.ForceDriver = driver;
}

_app.Init();
GoQuietWhenLeftAlone();
TuiTheme.Apply();
Expand Down
133 changes: 133 additions & 0 deletions src/M0LTE.Tait.Codeplug.Cli/TuiBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Diagnostics;
using Terminal.Gui.App;

namespace M0LTE.Tait.Codeplug.Cli;

/// <summary>
/// Times what one screen repaint costs on the console this is actually running on.
///
/// Terminal.Gui repaints the whole screen for every character typed into a text box, so a repaint IS
/// a keystroke as far as the editor is concerned. How long that takes is entirely down to the console
/// and the driver in front of it, and it is not something that can be measured from anywhere else -
/// hence a benchmark that ships in the tool rather than a number quoted from someone else's machine.
///
/// Run it per driver to find the quickest one for your console:
/// tait-codeplug tui --bench
/// tait-codeplug tui --bench --driver ansi
/// tait-codeplug tui --bench --driver windows
/// </summary>
internal static class TuiBench
{
private const int WarmUp = 5;
private const int Iterations = 30;

internal static int Run(string? driver, TextWriter output)
{
ArgumentNullException.ThrowIfNull(output);

var timings = new List<double>(Iterations);
int rows;
int columns;
string driverInUse;

IApplication app = Application.Create();
try
{
if (driver is not null)
{
app.ForceDriver = driver;
}

app.Init();

using var window = BenchWindow(out Action mutate);
int screenRows = 0;
int screenColumns = 0;
string seenDriver = "(unknown)";

// The timing has to happen inside the running loop: outside it the driver is not live, the
// terminal has not answered the size query yet, and a draw costs nothing because nothing
// reaches the console. So run the app, do the work on the first tick, and stop.
app.AddTimeout(TimeSpan.FromMilliseconds(250), () =>
{
seenDriver = driver ?? Terminal.Gui.Drivers.DriverRegistry.GetDefaultDriver().Name;
screenRows = app.Screen.Height;
screenColumns = app.Screen.Width;

for (int i = 0; i < WarmUp + Iterations; i++)
{
mutate();
long start = Stopwatch.GetTimestamp();
app.LayoutAndDraw(true);
double ms = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
if (i >= WarmUp)
{
timings.Add(ms);
}
}

app.RequestStop(window);
return false;
});

app.Run(window);
rows = screenRows;
columns = screenColumns;
driverInUse = seenDriver;
}
finally
{
app.Dispose();
}

if (timings.Count == 0)
{
output.WriteLine("the benchmark did not get a chance to draw anything - is this a real terminal?");
return 1;
}

timings.Sort();
double median = timings[timings.Count / 2];
double worst = timings[^1];
double best = timings[0];
int cells = rows * columns;

output.WriteLine();
output.WriteLine(FormattableString.Invariant($"screen : {columns}x{rows} ({cells} cells)"));
output.WriteLine(FormattableString.Invariant($"driver : {driverInUse.ToLowerInvariant()}{(driver is null ? " (the default on this platform)" : " (forced)")}"));
output.WriteLine(FormattableString.Invariant($"repaints : {Iterations} timed, {WarmUp} discarded as warm-up"));
output.WriteLine(FormattableString.Invariant($"per repaint : {median:F1} ms median ({best:F1} best, {worst:F1} worst)"));
output.WriteLine();
output.WriteLine("A repaint is what one character typed into a text box costs, because Terminal.Gui");
output.WriteLine("redraws the whole screen for it. Under about 30ms feels instant; a few hundred");
output.WriteLine("milliseconds is the editor feeling sluggish. Try --driver <name> to compare, and");
output.WriteLine("a smaller window: the cost scales with the number of cells on screen.");
return 0;
}

/// <summary>
/// A window the size of the real one, with something on it that changes every repaint, so the
/// driver has actual work to do rather than an unchanged screen.
/// </summary>
private static Terminal.Gui.Views.Window BenchWindow(out Action mutate)
{
var window = new Terminal.Gui.Views.Window { Title = "tait-codeplug repaint benchmark" };
var label = new Terminal.Gui.Views.Label
{
X = 2,
Y = 2,
Text = "measuring what one screen repaint costs on this console...",
};
window.Add(label);

int n = 0;
mutate = () =>
{
n++;
label.Text = $"measuring what one screen repaint costs on this console... {n}";
window.SetNeedsDraw();
};

return window;
}
}
61 changes: 61 additions & 0 deletions src/M0LTE.Tait.Codeplug.Cli/TuiDriverChoice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Terminal.Gui.Drivers;

namespace M0LTE.Tait.Codeplug.Cli;

/// <summary>
/// Which Terminal.Gui console driver the interactive mode should use.
///
/// Terminal.Gui picks one per platform, and on Windows that choice matters a lot more than it does
/// elsewhere. Every character typed into a text box repaints the whole screen (~7-8 bytes per cell,
/// so 22 KB at 100x30 and 82 KB at 200x50), and how expensive that is depends entirely on how the
/// driver hands it to the console: one buffered write is cheap, thousands of small console calls is
/// not, and on Windows the per-call cost is far higher than on a Unix pty.
///
/// So rather than guess which driver is quickest on someone else's console, this makes it a switch:
/// `tait-codeplug tui --driver ansi`, and `--driver list` to see what this platform offers.
/// </summary>
internal static class TuiDriverChoice
{
/// <summary>
/// Resolve a user-supplied driver name to the name Terminal.Gui knows, case-insensitively.
/// Returns null for "default", meaning let the library choose as it always has.
/// </summary>
/// <exception cref="FormatException">The name is not one this platform supports.</exception>
internal static string? Resolve(string? requested)
{
if (string.IsNullOrWhiteSpace(requested)
|| requested.Equals("default", StringComparison.OrdinalIgnoreCase))
{
return null;
}

string[] supported = SupportedNames();
string? match = Array.Find(supported, n => n.Equals(requested.Trim(), StringComparison.OrdinalIgnoreCase));

return match ?? throw new FormatException(
$"unknown driver '{requested}'. This platform supports: {string.Join(", ", supported.Select(n => n.ToLowerInvariant()))}, "
+ "or 'default' to let Terminal.Gui choose.");
}

/// <summary>The driver names Terminal.Gui reports as usable on the machine this is running on.</summary>
internal static string[] SupportedNames()
=> DriverRegistry.GetSupportedDrivers().Select(d => d.Name).ToArray();

/// <summary>What <c>--driver list</c> prints.</summary>
internal static void PrintAvailable(TextWriter output)
{
ArgumentNullException.ThrowIfNull(output);

output.WriteLine("console drivers available on this machine:");
foreach (DriverRegistry.DriverDescriptor d in DriverRegistry.GetSupportedDrivers())
{
string isDefault = d.Name == DriverRegistry.GetDefaultDriver().Name ? " (default here)" : string.Empty;
output.WriteLine($" {d.Name.ToLowerInvariant(),-10} {d.DisplayName}{isDefault}");
}

output.WriteLine();
output.WriteLine("Use with: tait-codeplug tui --driver <name> [file.m8p]");
output.WriteLine("Worth trying if typing into the editor feels slow: the drivers differ a lot in");
output.WriteLine("how much work a screen repaint costs, and which is quickest depends on your console.");
}
}
Loading