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
39 changes: 30 additions & 9 deletions graphcode-windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,31 @@ actions use the Windows `IFileOpenDialog` folder picker. The no-project state
also presents accessible native buttons for opening a folder or the global
overview; recent projects remain selectable in the sidebar.

Pressing and releasing unmodified `F10` enters the native menu bar, including
while an embedded terminal has focus. The shell passes the original key pair
Pressing and releasing unmodified `F10` enters the native menu bar. While an
embedded terminal has focus, `F10` and `F6` belong to the program in it and are
sent through the key encoder (`ESC[21~` and `ESC[17~`, with modifier
parameters such as `Shift+F6` -> `ESC[17;2~`); use `Ctrl+Shift+F10` to enter
the menu bar and `Ctrl+Shift+F6` to enter the window toolbar from a terminal.
`Alt+Space` still opens the window menu and `Alt+F4` closes the window. Alt
alone and Alt+letter chords are terminal input (an ESC prefix), so the menu
mnemonics (Alt+F, Alt+E, ...) are unavailable while a terminal has focus; use
`Ctrl+Shift+F10`, or click the menu bar, instead.
Outside a terminal, the shell passes the original key pair
to Windows default menu processing only when the release still targets the
same eligible, app-owned window. Observed focus/activation changes, modal
disablement, and intervening input cancel the pending pair; modified `F10`
(including `Shift+F10`) retains its existing input route.
(including `Shift+F10`) retains its existing input route (in a terminal,
`Shift+F10` opens the terminal context menu). A terminal-owned `F10` is never
buffered, so the menu bar cannot be left active by a key that went to the
terminal.
Both ordinary and system-key plain `F10` messages use this route; an Alt-context
message is excluded even if the modifier snapshot no longer shows Alt pressed.

`F6` (or View > Focus Window Toolbar) enters the window toolbar; `Shift+F6`
enters at its last visible control. Within the toolbar, `Tab`/`Shift+Tab` and
enters at its last visible control; `Ctrl+Shift+F6` enters it at the first
control and works while a terminal has focus. Within the toolbar, `Tab`/`Shift+Tab` and
Left/Right move between controls, Home/End select the first/last control, and
Enter/Space activate it. `F6`, `Shift+F6`, or Escape leave the toolbar and restore
Enter/Space activate it. `F6`, `Shift+F6`, `Ctrl+Shift+F6`, or Escape leave the toolbar and restore
the still-visible app-owned focus target. Outside the toolbar, Tab/Shift+Tab keep
their loop-navigation behavior and Ctrl+Tab still advances attention selection.
The header's loop-panel button is available only in a loop workspace with
Expand Down Expand Up @@ -237,17 +249,26 @@ chat deletion.

These root-window bindings are not universal terminal or dialog shortcuts.
While a terminal has focus it keeps `Ctrl+D` (EOF), `Ctrl+W` (delete word),
`Ctrl+S`, `Ctrl+T`, `Ctrl+N`, `Ctrl+[` and `Ctrl+]` for the program running in
`Ctrl+S`, `Ctrl+T`, `Ctrl+N`, `Ctrl+[`, `Ctrl+]`, `F6` and `F10` for the program running in
it, so those menu accelerators apply only elsewhere. The terminal-safe
alternatives work everywhere: `Ctrl+Shift+T` (new tab), `Ctrl+Shift+N` (new
loop), `Alt+Shift+D` (split right), `Ctrl+Shift+D` (split down), and
`Ctrl+Shift+[` / `Ctrl+Shift+]` (pane focus); `Ctrl+Shift+W` closes the tab
loop), `Alt+Shift+D` (split right), `Ctrl+Shift+D` (split down),
`Ctrl+Shift+[` / `Ctrl+Shift+]` (pane focus), `Ctrl+Shift+F6` (window toolbar),
and `Ctrl+Shift+F10` (menu bar); `Ctrl+Shift+W` closes the tab
while a terminal has focus (it opens Worktrees elsewhere). `Ctrl+J` and
`Ctrl+O` remain application keys in a terminal and `Ctrl+P` is not forwarded.
The terminal sends editing, cursor, Home/End, Insert/Delete, Page, and function
keys through the pinned Ghostty key encoder, so cursor-key application mode,
modifier parameters, and Alt's ESC prefix match the macOS terminal; Backspace
sends DEL and Ctrl+Backspace sends BS. In the jump palette,
sends DEL and Ctrl+Backspace sends BS. Plain `Ctrl+S` is sent as the raw 0x13
byte, like `Ctrl+T`, `Ctrl+D`, `Ctrl+W` and `Ctrl+N` (0x14, 0x04, 0x17, 0x0E).
Known limitation: in a program that reads keys with line input enabled (for
example PowerShell's `[Console]::ReadKey`), the Windows console's pause-output
handling swallows `Ctrl+S` and then the next key; `Ctrl+T` on its own works. A
workaround that sent `Ctrl+S` as a win32-input-mode key pair was rejected because
it broke Git MSYS vim (Enter no longer honored) and made cmd.exe insert a literal
`^S` instead of pausing output. This comes from an isolated zmx matrix, not a Dev
Box walkthrough, and `Ctrl+S` is not claimed to work in such programs. In the jump palette,
Up/Down moves through results and Enter accepts the selection, returning to
the selected loop in the graph rather than opening its terminal. Native forms
keep their own text editing, Tab navigation, and acceptance/cancellation;
Expand Down
18 changes: 15 additions & 3 deletions graphcode-windows/src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6898,7 +6898,7 @@ pub const App = struct {
switch (command) {
.none => return false,
.enter => {
const action = layout.step(null, shift) orelse return false;
const action = layout.step(null, shift and !ctrl) orelse return false;
return self.focusHeader(action);
},
.exit => self.leaveHeader(true),
Expand Down Expand Up @@ -12526,7 +12526,7 @@ const LiveKeyboard = struct {
index: usize,
original_state: [256]u8,

const Chord = struct { vk: u32, ctrl: bool = false, shift: bool = false, alt: bool = false, extended: bool = false, repeat: bool = false };
const Chord = struct { vk: u32, ctrl: bool = false, shift: bool = false, alt: bool = false, extended: bool = false, repeat: bool = false, system: bool = false };

fn begin(fixture: *LiveTerminalFixture, session: []const u8) !LiveKeyboard {
const class = std.unicode.utf8ToUtf16LeStringLiteral("WinghosttyEmbeddableSurface");
Expand All @@ -12541,6 +12541,7 @@ const LiveKeyboard = struct {
// The production accelerator table and terminal key routing, as the shell installs them.
fixture.app.window.accelerators = MainWindow.createAccelerators();
fixture.app.window.terminal_route = &App.onTerminalKeyRoute;
fixture.app.window.key_callback = &App.onShellKey;
fixture.app.window.context = &fixture.app;
return .{ .fixture = fixture, .surface = surface, .index = index, .original_state = original };
}
Expand All @@ -12551,6 +12552,7 @@ const LiveKeyboard = struct {
if (window.accelerators != null) _ = c.DestroyAcceleratorTable(window.accelerators);
window.accelerators = null;
window.terminal_route = null;
window.key_callback = null;
window.context = null;
_ = c.ShowWindow(window.hwnd, c.SW_HIDE);
}
Expand Down Expand Up @@ -12585,7 +12587,7 @@ const LiveKeyboard = struct {
try setModifiers(chord);
var message = std.mem.zeroes(c.MSG);
message.hwnd = self.surface;
message.message = if (chord.alt) c.WM_SYSKEYDOWN else c.WM_KEYDOWN;
message.message = if (chord.alt or chord.system) c.WM_SYSKEYDOWN else c.WM_KEYDOWN;
message.wParam = chord.vk;
const scan = c.MapVirtualKeyW(chord.vk, c.MAPVK_VK_TO_VSC);
message.lParam = @intCast(1 | (scan << 16) | (@as(u32, @intFromBool(chord.extended)) << 24) |
Expand Down Expand Up @@ -13050,6 +13052,13 @@ test "live terminal keyboard: key table of editing, cursor, function, and Ctrl/A
.{ .name = "F1", .chord = .{ .vk = f1 }, .expected = "\x1bOP" },
.{ .name = "F4", .chord = .{ .vk = f1 + 3 }, .expected = "\x1bOS" },
.{ .name = "F5", .chord = .{ .vk = f1 + 4 }, .expected = "\x1b[15~" },
// F6 and F10 used to be taken by the shell (window toolbar, native menu bar).
.{ .name = "F6", .chord = .{ .vk = f1 + 5 }, .expected = "\x1b[17~" },
.{ .name = "Shift+F6", .chord = .{ .vk = f1 + 5, .shift = true }, .expected = "\x1b[17;2~" },
.{ .name = "F10", .chord = .{ .vk = f1 + 9 }, .expected = "\x1b[21~" },
.{ .name = "F10 as a system key", .chord = .{ .vk = f1 + 9, .system = true }, .expected = "\x1b[21~" },
.{ .name = "Ctrl+F10", .chord = .{ .vk = f1 + 9, .ctrl = true }, .expected = "\x1b[21;5~" },
.{ .name = "F11", .chord = .{ .vk = f1 + 10 }, .expected = "\x1b[23~" },
.{ .name = "F12", .chord = .{ .vk = f1 + 11 }, .expected = "\x1b[24~" },
.{ .name = "a", .chord = .{ .vk = 'A' }, .expected = "a" },
.{ .name = "Shift+A", .chord = .{ .vk = 'A', .shift = true }, .expected = "A" },
Expand Down Expand Up @@ -13164,6 +13173,9 @@ test "live terminal keyboard: Ctrl+Shift+W and the Windows system keys become sh
try std.testing.expectEqual(@as(?c.WPARAM, c.SC_CLOSE), keyboard.takePosted(c.WM_SYSCOMMAND));
try keyboard.press(.{ .vk = c.VK_SPACE, .alt = true });
try std.testing.expectEqual(@as(?c.WPARAM, c.SC_KEYMENU), keyboard.takePosted(c.WM_SYSCOMMAND));
// Ctrl+Shift+F10 enters the menu bar from a terminal.
try keyboard.press(.{ .vk = c.VK_F10, .ctrl = true, .shift = true });
try std.testing.expectEqual(@as(?c.WPARAM, c.SC_KEYMENU), keyboard.takePosted(c.WM_SYSCOMMAND));
try keyboard.drainInput(&sent);
try std.testing.expectEqualStrings("", sent.items);
}
Expand Down
8 changes: 8 additions & 0 deletions graphcode-windows/src/InputRouter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const std = @import("std");
pub const HeaderKey = enum { none, enter, exit, next, previous, first, last, activate };

pub fn headerKey(key: usize, ctrl: bool, shift: bool, alt: bool, focused: bool) HeaderKey {
// Ctrl+Shift+F6 is the chord that reaches the toolbar from a terminal, which owns plain F6.
if (key == 0x75 and ctrl and shift and !alt) return if (focused) .exit else .enter;
if (ctrl or alt) return .none;
if (key == 0x75) return if (focused) .exit else .enter;
if (!focused) return .none;
Expand Down Expand Up @@ -217,6 +219,12 @@ test "header keys are scoped and preserve modified and global navigation" {
try std.testing.expectEqual(HeaderKey.none, headerKey(key, true, false, false, true));
try std.testing.expectEqual(HeaderKey.none, headerKey(key, false, false, true, true));
}
// Ctrl+Shift+F6 enters and leaves the toolbar from a terminal that owns plain F6.
try std.testing.expectEqual(HeaderKey.enter, headerKey(0x75, true, true, false, false));
try std.testing.expectEqual(HeaderKey.exit, headerKey(0x75, true, true, false, true));
try std.testing.expectEqual(HeaderKey.none, headerKey(0x75, true, true, true, false));
try std.testing.expectEqual(HeaderKey.none, headerKey(0x75, true, true, true, true));
try std.testing.expectEqual(HeaderKey.none, headerKey(0x09, true, true, false, true));
}

test "attention and worktree shortcuts are distinct from ordinary selection" {
Expand Down
Loading
Loading