mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
feat(tui): switch sessions with the web UI's shortcuts
Alt+1..9 switches to that session, and `[` / `]` / Tab step through them, so the
muscle memory from the web UI carries over.
Alt+N SELECTS rather than attaches, which is what the web UI's Alt+N does:
switching which tab you look at is cheap and reversible, and the terminal
equivalent is moving the selection and its preview, not handing the whole
terminal to a pane. Bare 1-9 keeps its documented jump-and-attach meaning.
Two of the web UI's chords cannot cross into a terminal, so the nearest
transmittable keys carry them instead:
Alt+[ / Alt+] ESC+[ IS the CSI introducer every arrow key arrives on, and
ESC+] is OSC, so neither chord is distinguishable from a
sequence. Bare `[` and `]` do the job.
Ctrl+Tab a terminal cannot report the Ctrl, so plain Tab carries it.
⚠️ The parser now decodes ESC + a printable character in ONE read as an Alt
chord, and the app replays every chord it does not claim as `escape` then that
character. That fallback is load-bearing, not tidiness: a real Esc landing in
the same read as the next keystroke is byte-identical to a chord, and without
the replay "Esc then q" typed quickly decoded as Alt+Q, matched nothing and was
swallowed. The e2e suite caught exactly that as the dashboard refusing to quit.
A lone Esc is still held and flushed on the caller's timer, which is what keeps
the two separable at all.
This commit is contained in:
@@ -121,8 +121,11 @@ describe('escape sequences', () => {
|
||||
expect(decode('\x1b[M !!x')).toEqual([{ type: 'char', value: 'x' }]);
|
||||
});
|
||||
|
||||
it('reads ESC followed by a letter as Escape then that letter', () => {
|
||||
expect(decode('\x1bx')).toEqual([{ type: 'escape' }, { type: 'char', value: 'x' }]);
|
||||
it('reads ESC followed by a letter as the Alt chord it is', () => {
|
||||
// Changed deliberately: this used to decode as Escape + `x`, which made
|
||||
// Alt+N unreachable. A lone Esc is still separable because it is HELD until
|
||||
// the caller's timer flushes it (see the 'lone escape' suite).
|
||||
expect(decode('\x1bx')).toEqual([{ type: 'alt', value: 'x' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,3 +211,35 @@ describe('torn reads', () => {
|
||||
expect(parser.feed('\x1b[A')).toEqual([{ type: 'key', name: 'up' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alt chords', () => {
|
||||
it('reads ESC + a printable character in one read as Alt+that key', () => {
|
||||
expect(decode('\x1b1')).toEqual([{ type: 'alt', value: '1' }]);
|
||||
expect(decode('\x1bk')).toEqual([{ type: 'alt', value: 'k' }]);
|
||||
});
|
||||
|
||||
it('never steals the sequence introducers, or every arrow key would break', () => {
|
||||
// ESC [ is CSI and ESC O is SS3: both are Up, not Alt+[ / Alt+O.
|
||||
expect(decode('\x1b[A')).toEqual([{ type: 'key', name: 'up' }]);
|
||||
expect(decode('\x1bOA')).toEqual([{ type: 'key', name: 'up' }]);
|
||||
});
|
||||
|
||||
it('leaves ESC ] alone, so a terminal colour reply is never read as a chord', () => {
|
||||
// OSC introducer: decoded as Escape then `]`, exactly as before.
|
||||
expect(decode('\x1b]')).toEqual([{ type: 'escape' }, { type: 'char', value: ']' }]);
|
||||
});
|
||||
|
||||
it('keeps a lone ESC held, which is what separates it from a chord', () => {
|
||||
const parser = createKeyParser();
|
||||
expect(parser.feed('\x1b')).toEqual([]);
|
||||
expect(parser.flush()).toEqual([{ type: 'escape' }]);
|
||||
});
|
||||
|
||||
it('decodes a chord torn across two reads as Escape then the character', () => {
|
||||
// The unavoidable ambiguity, resolved the standard way: same read = chord.
|
||||
const parser = createKeyParser();
|
||||
expect(parser.feed('\x1b')).toEqual([]);
|
||||
expect(parser.flush()).toEqual([{ type: 'escape' }]);
|
||||
expect(parser.feed('1')).toEqual([{ type: 'char', value: '1' }]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user