mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
Merge PR #129: macOS Option/physical-key session shortcuts + terminal-ui ESC-leak fix
Make Option/Alt session shortcuts work on macOS (physical key codes)
This commit is contained in:
@@ -483,7 +483,8 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De
|
|||||||
|----------|--------|
|
|----------|--------|
|
||||||
| `Ctrl/Cmd+W` | Kill active session |
|
| `Ctrl/Cmd+W` | Kill active session |
|
||||||
| `Ctrl/Cmd+Tab` | Next session |
|
| `Ctrl/Cmd+Tab` | Next session |
|
||||||
| `Alt+1`–`Alt+9` | Switch to tab N |
|
| `Alt/Option+[` / `Alt/Option+]` | Previous / next session |
|
||||||
|
| `Alt/Option+1`-`Alt/Option+9` | Switch to tab N (physical keys, so macOS Option layouts work) |
|
||||||
| `Ctrl+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right |
|
| `Ctrl+Shift+{` / `Ctrl+Shift+}` | Move active tab left / right |
|
||||||
| `Ctrl/Cmd+L` | Clear terminal |
|
| `Ctrl/Cmd+L` | Clear terminal |
|
||||||
| `Ctrl+Shift+R` | Restore terminal size |
|
| `Ctrl+Shift+R` | Restore terminal size |
|
||||||
|
|||||||
+20
-3
@@ -770,15 +770,32 @@ class CodemanApp {
|
|||||||
if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory();
|
if (this.attachmentHistoryDrawerOpen) this.closeAttachmentHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alt+1-9: switch to Codeman session by index
|
// Option/Alt session navigation uses physical key CODES, not e.key, so macOS
|
||||||
if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key >= '1' && e.key <= '9') {
|
// keyboard layouts that emit special characters under Option (Option+1 -> ¡,
|
||||||
const idx = parseInt(e.key) - 1;
|
// Option+[ -> "“") still switch sessions. e.code is the physical key regardless
|
||||||
|
// of layout. Option+1-9 = switch by index; Option+[ / Option+] = prev / next.
|
||||||
|
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
|
||||||
|
const code = e.code || '';
|
||||||
|
const digitMatch = code.match(/^Digit([1-9])$/);
|
||||||
|
if (digitMatch) {
|
||||||
|
const idx = parseInt(digitMatch[1], 10) - 1;
|
||||||
if (idx < this.sessionOrder.length) {
|
if (idx < this.sessionOrder.length) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.selectSession(this.sessionOrder[idx]);
|
this.selectSession(this.sessionOrder[idx]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (e.code === 'BracketLeft') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.prevSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.code === 'BracketRight') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.nextSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match against shortcut table
|
// Match against shortcut table
|
||||||
for (const s of SHORTCUTS) {
|
for (const s of SHORTCUTS) {
|
||||||
|
|||||||
@@ -508,7 +508,8 @@
|
|||||||
<div class="shortcuts-grid">
|
<div class="shortcuts-grid">
|
||||||
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div>
|
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div>
|
||||||
<div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
|
<div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
|
||||||
<div><kbd>Alt</kbd>+<kbd>1-9</kbd></div><div>Switch to Tab N</div>
|
<div><kbd>Alt/Option</kbd>+<kbd>[</kbd> / <kbd>Alt/Option</kbd>+<kbd>]</kbd></div><div>Previous / Next Session</div>
|
||||||
|
<div><kbd>Alt/Option</kbd>+<kbd>1-9</kbd></div><div>Switch to Tab N</div>
|
||||||
<div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div>
|
<div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div>
|
||||||
<div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div>
|
<div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div>
|
||||||
<div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div>
|
<div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div>
|
||||||
|
|||||||
@@ -114,8 +114,14 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
this.terminal.attachCustomKeyEventHandler((ev) => {
|
this.terminal.attachCustomKeyEventHandler((ev) => {
|
||||||
if (ev.isComposing || ev.keyCode === 229) return false;
|
if (ev.isComposing || ev.keyCode === 229) return false;
|
||||||
|
|
||||||
// Let Alt+digit pass through to browser (tab switching)
|
// Let the app's Alt/Option session-nav shortcuts reach the document keydown handler
|
||||||
if (ev.altKey && ev.key >= '0' && ev.key <= '9') return false;
|
// (app.js switches tabs by PHYSICAL e.code) instead of xterm injecting ESC<char> into
|
||||||
|
// the PTY. Mirror app.js's gate exactly — same physical codes + modifier guard — so
|
||||||
|
// macOS Option layouts (Option+1 -> "¡", Option+[ -> "“") are suppressed here too and
|
||||||
|
// don't leak an escape sequence into the focused terminal on every tab switch.
|
||||||
|
if (ev.altKey && !ev.ctrlKey && !ev.shiftKey && /^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code || '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Ctrl+V / Cmd+V: intercept before xterm sends ^V to PTY.
|
// Ctrl+V / Cmd+V: intercept before xterm sends ^V to PTY.
|
||||||
// Route through our paste trap which handles both images and text.
|
// Route through our paste trap which handles both images and text.
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const appSource = readFileSync('src/web/public/app.js', 'utf8');
|
||||||
|
const terminalUiSource = readFileSync('src/web/public/terminal-ui.js', 'utf8');
|
||||||
|
const helpHtml = readFileSync('src/web/public/index.html', 'utf8');
|
||||||
|
const readme = readFileSync('README.md', 'utf8');
|
||||||
|
|
||||||
|
describe('keyboard shortcuts', () => {
|
||||||
|
it('uses physical Option+number keys so macOS special characters do not break tab switching', () => {
|
||||||
|
expect(appSource).toContain('e.code ||');
|
||||||
|
expect(appSource).toContain('Digit([1-9])');
|
||||||
|
expect(appSource).toContain('parseInt(digitMatch[1], 10) - 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides Option+bracket shortcuts for previous and next session', () => {
|
||||||
|
expect(appSource).toContain("e.code === 'BracketLeft'");
|
||||||
|
expect(appSource).toContain("e.code === 'BracketRight'");
|
||||||
|
expect(appSource).toContain('this.prevSession()');
|
||||||
|
expect(appSource).toContain('this.nextSession()');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('suppresses xterm PTY injection for the same physical Alt nav codes (no ESC leak)', () => {
|
||||||
|
// terminal-ui.js must gate its xterm pass-through on the SAME physical e.code set the
|
||||||
|
// app.js handler consumes; otherwise Alt+[ / Alt+] (and Option+digit on remapped macOS
|
||||||
|
// layouts) switch tabs AND inject ESC<char> into the focused terminal. Keep in sync.
|
||||||
|
expect(terminalUiSource).toContain('/^(Digit[1-9]|BracketLeft|BracketRight)$/.test(ev.code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('documents the Alt/Option shortcuts in help and README', () => {
|
||||||
|
expect(helpHtml).toContain('<kbd>Alt/Option</kbd>+<kbd>[</kbd>');
|
||||||
|
expect(helpHtml).toContain('<kbd>Alt/Option</kbd>+<kbd>]</kbd>');
|
||||||
|
expect(helpHtml).toContain('<kbd>Alt/Option</kbd>+<kbd>1-9</kbd>');
|
||||||
|
expect(readme).toContain('`Alt/Option+[` / `Alt/Option+]`');
|
||||||
|
expect(readme).toContain('`Alt/Option+1`-`Alt/Option+9`');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user