mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Add keyboard navigation for Terminal IO window (#3909)
## Changes - Add keyboard navigation support in Terminal IO window - Use J/K keys for vim-style navigation - Support arrow keys for traditional navigation https://github.com/user-attachments/assets/e5d3bba1-1a47-49c9-ac59-c6195515605c Resolves https://github.com/ghostty-org/ghostty/issues/1096
This commit is contained in:
@ -53,6 +53,22 @@ key_events: inspector.key.EventRing,
|
||||
vt_events: inspector.termio.VTEventRing,
|
||||
vt_stream: inspector.termio.Stream,
|
||||
|
||||
/// The currently selected event sequence number for keyboard navigation
|
||||
selected_event_seq: ?u32 = null,
|
||||
|
||||
/// Flag indicating whether we need to scroll to the selected item
|
||||
need_scroll_to_selected: bool = false,
|
||||
|
||||
/// Flag indicating whether the selection was made by keyboard
|
||||
is_keyboard_selection: bool = false,
|
||||
|
||||
/// Enum representing keyboard navigation actions
|
||||
const KeyAction = enum {
|
||||
down,
|
||||
none,
|
||||
up,
|
||||
};
|
||||
|
||||
const CellInspect = union(enum) {
|
||||
/// Idle, no cell inspection is requested
|
||||
idle: void,
|
||||
@ -1014,6 +1030,24 @@ fn renderKeyboardWindow(self: *Inspector) void {
|
||||
} // table
|
||||
}
|
||||
|
||||
/// Helper function to check keyboard state and determine navigation action.
|
||||
fn getKeyAction(self: *Inspector) KeyAction {
|
||||
_ = self;
|
||||
const keys = .{
|
||||
.{ .key = cimgui.c.ImGuiKey_J, .action = KeyAction.down },
|
||||
.{ .key = cimgui.c.ImGuiKey_DownArrow, .action = KeyAction.down },
|
||||
.{ .key = cimgui.c.ImGuiKey_K, .action = KeyAction.up },
|
||||
.{ .key = cimgui.c.ImGuiKey_UpArrow, .action = KeyAction.up },
|
||||
};
|
||||
|
||||
inline for (keys) |k| {
|
||||
if (cimgui.c.igIsKeyPressed_Bool(k.key, false)) {
|
||||
return k.action;
|
||||
}
|
||||
}
|
||||
return .none;
|
||||
}
|
||||
|
||||
fn renderTermioWindow(self: *Inspector) void {
|
||||
// Start our window. If we're collapsed we do nothing.
|
||||
defer cimgui.c.igEnd();
|
||||
@ -1090,6 +1124,60 @@ fn renderTermioWindow(self: *Inspector) void {
|
||||
0,
|
||||
);
|
||||
|
||||
// Handle keyboard navigation when window is focused
|
||||
if (cimgui.c.igIsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
|
||||
const key_pressed = self.getKeyAction();
|
||||
|
||||
switch (key_pressed) {
|
||||
.none => {},
|
||||
.up, .down => {
|
||||
// If no event is selected, select the first/last event based on direction
|
||||
if (self.selected_event_seq == null) {
|
||||
if (!self.vt_events.empty()) {
|
||||
var it = self.vt_events.iterator(if (key_pressed == .up) .forward else .reverse);
|
||||
if (it.next()) |ev| {
|
||||
self.selected_event_seq = @as(u32, @intCast(ev.seq));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find next/previous event based on current selection
|
||||
var it = self.vt_events.iterator(.reverse);
|
||||
switch (key_pressed) {
|
||||
.down => {
|
||||
var found = false;
|
||||
while (it.next()) |ev| {
|
||||
if (found) {
|
||||
self.selected_event_seq = @as(u32, @intCast(ev.seq));
|
||||
break;
|
||||
}
|
||||
if (ev.seq == self.selected_event_seq.?) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.up => {
|
||||
var prev_ev: ?*const inspector.termio.VTEvent = null;
|
||||
while (it.next()) |ev| {
|
||||
if (ev.seq == self.selected_event_seq.?) {
|
||||
if (prev_ev) |prev| {
|
||||
self.selected_event_seq = @as(u32, @intCast(prev.seq));
|
||||
break;
|
||||
}
|
||||
}
|
||||
prev_ev = ev;
|
||||
}
|
||||
},
|
||||
.none => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// Mark that we need to scroll to the newly selected item
|
||||
self.need_scroll_to_selected = true;
|
||||
self.is_keyboard_selection = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var it = self.vt_events.iterator(.reverse);
|
||||
while (it.next()) |ev| {
|
||||
// Need to push an ID so that our selectable is unique.
|
||||
@ -1098,12 +1186,32 @@ fn renderTermioWindow(self: *Inspector) void {
|
||||
|
||||
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
|
||||
_ = cimgui.c.igTableNextColumn();
|
||||
_ = cimgui.c.igSelectable_BoolPtr(
|
||||
|
||||
// Store the previous selection state to detect changes
|
||||
const was_selected = ev.imgui_selected;
|
||||
|
||||
// Update selection state based on keyboard navigation
|
||||
if (self.selected_event_seq) |seq| {
|
||||
ev.imgui_selected = (@as(u32, @intCast(ev.seq)) == seq);
|
||||
}
|
||||
|
||||
// Handle selectable widget
|
||||
if (cimgui.c.igSelectable_BoolPtr(
|
||||
"##select",
|
||||
&ev.imgui_selected,
|
||||
cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
|
||||
.{ .x = 0, .y = 0 },
|
||||
);
|
||||
)) {
|
||||
// If selection state changed, update keyboard navigation state
|
||||
if (ev.imgui_selected != was_selected) {
|
||||
self.selected_event_seq = if (ev.imgui_selected)
|
||||
@as(u32, @intCast(ev.seq))
|
||||
else
|
||||
null;
|
||||
self.is_keyboard_selection = false;
|
||||
}
|
||||
}
|
||||
|
||||
cimgui.c.igSameLine(0, 0);
|
||||
cimgui.c.igText("%d", ev.seq);
|
||||
_ = cimgui.c.igTableNextColumn();
|
||||
@ -1159,6 +1267,12 @@ fn renderTermioWindow(self: *Inspector) void {
|
||||
cimgui.c.igText("%s", entry.value_ptr.ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the selected event and scrolling is needed, scroll to it
|
||||
if (self.need_scroll_to_selected and self.is_keyboard_selection) {
|
||||
cimgui.c.igSetScrollHereY(0.5);
|
||||
self.need_scroll_to_selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // table
|
||||
|
Reference in New Issue
Block a user