Skip to content

Draft: Add KeySequences add-on prototype#5458

Draft
YourRobotOverlord wants to merge 3 commits into
tui-cs:mainfrom
YourRobotOverlord:codex-key-sequences-addon
Draft

Draft: Add KeySequences add-on prototype#5458
YourRobotOverlord wants to merge 3 commits into
tui-cs:mainfrom
YourRobotOverlord:codex-key-sequences-addon

Conversation

@YourRobotOverlord

@YourRobotOverlord YourRobotOverlord commented May 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a prototype Terminal.Gui.KeySequences add-on for Vim-style multi-key command sequences.

This PR is intended as a demonstration and proof of concept for discussion. The final version could live in a separate repository, similar to the Editor add-on, rather than adding to the Terminal.Gui core.

Closes #5457

What This Demonstrates

  • Leader-key sequence bindings, such as ; m 4 k
  • Operator/count/motion-style commands, such as ; d 2 d
  • Persistent command mode entered with Esc
  • Command-mode exit with i
  • Vim-style :q quit command in the UICatalog demo
  • Pattern parsing with <count>, <char>, and <key> tokens
  • View-scoped and application-scoped registration extension methods
  • Integration through public Terminal.Gui keyboard events only
  • State change events for command-mode/status UI
  • A docfx deep dive and add-on specification

Notes

This intentionally does not add APIs to the core Terminal.Gui assembly. The add-on composes with existing keyboard routing through public events.

The UICatalog scenario (Key Sequences) is meant to make the behavior easy to inspect, not to define a complete Vim editor mode.

Verification

  • dotnet test --project Terminal.Gui.KeySequences\Tests\Terminal.Gui.KeySequences.Tests.csproj --filter-class "*KeySequence*"
  • dotnet build Terminal.Gui.KeySequences\Terminal.Gui.KeySequences.csproj --no-restore

@YourRobotOverlord YourRobotOverlord force-pushed the codex-key-sequences-addon branch from ea037e1 to 9f0135c Compare May 31, 2026 23:32
@YourRobotOverlord

Copy link
Copy Markdown
Collaborator Author

Key Sequences Deep Dive

See Also

Terminal.Gui.KeySequences is an add-on package for Vim-style keyboard sequences in Terminal.Gui apps. It is separate from the core Terminal.Gui assembly and uses public keyboard events instead of adding sequence APIs to core views.

Use key sequences to build command grammars that need more than a single shortcut, such as leader-key commands, operator-plus-motion commands, repeat counts, and persistent command modes.

Concepts

A key sequence has three parts:

  • Capture trigger - a leader key such as ;, or persistent command mode entered by a key such as Esc.
  • Pattern - a compact sequence string such as "; m <count> k" or "d <count> d".
  • Handler - a KeySequenceHandler that receives a KeySequenceContext after the pattern matches.

Leader mode captures a sequence after a configured leader key. Persistent mode captures sequence keys while command mode is active.

Leader Mode

To define leader-key commands, attach KeySequenceBindings to the view that should receive the commands.

using Terminal.Gui.Input;
using Terminal.Gui.KeySequences;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;

TextView editor = new ();

IDisposable registration = editor.UseKeySequences (bindings =>
{
    bindings.Add ("; m <count> k", context =>
    {
        for (int i = 0; i < context.Count; i++)
        {
            context.Target.InvokeCommand (Command.Up);
        }

        return true;
    });
});

The pattern "; m <count> k" reads as:

  • ; starts sequence capture.
  • m is a literal command key.
  • <count> accepts one or more digits and defaults to 1 when omitted.
  • k is a literal motion key.

The sequence ; m 4 k moves up four times. The sequence ; m k moves up once.

Operator And Motion Commands

To model Vim-style operator commands, use literal keys for the operator and motion and a count token for the repeat count.

editor.UseKeySequences (bindings =>
{
    bindings.Add ("; d <count> d", context =>
    {
        DeleteLines (context.Count);
        return true;
    });
});

The sequence ; d 2 d deletes two lines. Handlers can inspect context.OperatorKey, context.MotionKey, context.Count, context.Keys, and context.Values to share implementation across related commands.

Persistent Command Mode

Persistent command mode works like a small Vim command mode. The user presses one key to enter command mode, and subsequent keys are interpreted as sequence commands until the exit key is pressed.

using Terminal.Gui.App;
using Terminal.Gui.Input;
using Terminal.Gui.KeySequences;
using Terminal.Gui.Views;

IApplication app = Application.Create ().Init ();
TextView editor = new ();

app.Keyboard.KeyBindings.Remove (Key.Esc);

IDisposable registration = editor.UseKeySequences (
    bindings =>
    {
        bindings.Mode = KeySequenceMode.Persistent;
        bindings.EnterModeKey = Key.Esc;
        bindings.ExitModeKey = 'i';

        bindings.AddMode ("<count> k", context =>
        {
            MoveUp (context.Count);
            return true;
        });

        bindings.AddMode ("d <count> d", context =>
        {
            DeleteLines (context.Count);
            return true;
        });

        bindings.AddMode (": q", _ =>
        {
            app.RequestStop ();
            return true;
        });
    },
    KeySequenceInterceptionMode.Preemptive);

To use Esc as the command-mode enter key, remove or replace other Esc bindings that should no longer run first. The UICatalog Key Sequences scenario demonstrates Esc to enter command mode, i to exit command mode, and : q to quit.

Use AddMode for persistent-mode patterns. AddMode parses the pattern without a leader key, so "d <count> d" matches after command mode is already active.

Pattern Syntax

Pattern strings are split on spaces.

Token Meaning
; A literal key. In leader patterns, the first token is the leader key.
k A printable literal key.
<Esc> A named key parsed by Key.TryParse.
<count> A numeric repeat count. At most one count token is allowed.
<char> Any printable, non-control character.
<key> Any valid key.

To build a pattern in code, use KeySequencePattern.

KeySequencePattern pattern = KeySequencePattern
                             .Leader (';')
                             .Then ('m')
                             .Count ()
                             .Then ('k');

bindings.Add (pattern, context =>
{
    MoveUp (context.Count);
    return true;
});

To create a persistent-mode pattern in code, use KeySequencePattern.CommandMode ().

KeySequencePattern pattern = KeySequencePattern
                             .CommandMode ()
                             .Then ('d')
                             .Count ()
                             .Then ('d');

Counts

<count> accepts digit keys and exposes the parsed value through KeySequenceContext.Count.

If the user omits the count, Count is 1. If a pattern matches a count of 0, the match is rejected unless KeySequencePattern.AllowZeroCount is true.

KeySequencePattern pattern = KeySequenceParser.Parse ("; g <count> g");
pattern.AllowZeroCount = true;

bindings.Add (pattern, context =>
{
    GoToLine (context.Count);
    return true;
});

Capturing And Routing

View registrations can run in two interception modes:

  • AfterUnhandled starts capture only after normal view handling leaves a key unhandled.
  • Preemptive starts capture from the view's KeyDown event and continues consuming keys while capture is active.
IDisposable registration = editor.UseKeySequences (
    ConfigureBindings,
    KeySequenceInterceptionMode.Preemptive);

Application registrations attach to IApplication.Keyboard.KeyDown and target the current top runnable view.

IDisposable registration = app.UseKeySequences (bindings =>
{
    bindings.Add ("; q", _ =>
    {
        app.RequestStop ();
        return true;
    });
});

Some views handle character input before public KeyDown subscribers can consume it. To make a sequence key available in that case, attach the sequence handling at a level that receives the key first, use Preemptive where appropriate, or remove the view or application binding that conflicts with the sequence.

State And Feedback

Subscribe to KeySequenceBindings.StateChanged to update status bars, command palettes, or mode indicators.

bindings.StateChanged += (_, args) =>
{
    modeLabel.Text = args.IsCommandMode ? "COMMAND" : string.Empty;
    countLabel.Text = args.CountText;
};

KeySequenceStateChangedEventArgs includes:

  • State - idle, capturing, or command mode.
  • LeaderKey - the key that started the current leader sequence.
  • Keys - captured keys after the leader.
  • CountText - digits captured so far.
  • CandidateCount - matching patterns that remain possible.
  • Result - the result that caused the state change.
  • IsCommandMode - whether persistent command mode is active.

Timeouts And Cancellation

KeySequenceBindings.Timeout controls how long capture may pause between keys. The default is one second. Set it to TimeSpan.Zero or a negative value to disable timeout behavior.

bindings.Timeout = TimeSpan.FromSeconds (2);
bindings.CancelKey = Key.Esc;

Leader mode resets after a match, rejection, cancellation, or timeout. Persistent mode resets the current sequence after a match, rejection, cancellation, or timeout, but stays in command mode until the exit key is pressed.

Disposal

UseKeySequences returns an IDisposable registration. Dispose it when the binding lifetime is shorter than the view or application lifetime.

IDisposable registration = editor.UseKeySequences (ConfigureBindings);

// Later:
registration.Dispose ();

The registration removes event handlers. The view can then use normal keyboard handling again.

@tig

tig commented Jun 1, 2026

Copy link
Copy Markdown
Member

This is really neat.

I've lost count of the number of times I've mastered vi/vim and subsequent completely forgotten how to use it. lol.

I think a great proof point of this would be a version of ted that enabled it, or a config setting. If it's a separate version it could be named tim.

@YourRobotOverlord

Copy link
Copy Markdown
Collaborator Author

I've lost count of the number of times I've mastered vi/vim and subsequent completely forgotten how to use it. lol.

Haha me as well! Except I've never mastered it lol.

I think a great proof point of this would be a version of ted that enabled it, or a config setting. If it's a separate version it could be named tim.

I'll do that over the next few days if I have time after work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants