Add better and configurable shortcuts.
This commit is contained in:
+123
-115
@@ -26,6 +26,8 @@ const Terminal = @import("Terminal.zig");
|
||||
const View = @import("View.zig");
|
||||
const appearance = @import("appearance.zig");
|
||||
const emoji = @import("emoji.zig");
|
||||
const key = @import("key.zig");
|
||||
const shortcuts = @import("shortcuts.zig");
|
||||
|
||||
const Window = @This();
|
||||
|
||||
@@ -298,16 +300,16 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
|
||||
|
||||
// Window-level shortcuts run in the capture phase so they are handled
|
||||
// before the focused terminal turns the key into a VT sequence.
|
||||
const shortcuts = gtk.EventControllerKey.new();
|
||||
shortcuts.as(gtk.EventController).setPropagationPhase(.capture);
|
||||
const keys = gtk.EventControllerKey.new();
|
||||
keys.as(gtk.EventController).setPropagationPhase(.capture);
|
||||
_ = gtk.EventControllerKey.signals.key_pressed.connect(
|
||||
shortcuts,
|
||||
keys,
|
||||
*Window,
|
||||
&onShortcut,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
window.as(gtk.Widget).addController(shortcuts.as(gtk.EventController));
|
||||
window.as(gtk.Widget).addController(keys.as(gtk.EventController));
|
||||
|
||||
// Free our own state once GTK is done with the window. Doing this on
|
||||
// `destroy` rather than `close-request` means no further events can
|
||||
@@ -1603,6 +1605,17 @@ fn cycle(self: *Window, delta: isize) void {
|
||||
self.selectIndex(@intCast(next));
|
||||
}
|
||||
|
||||
/// Turn a key press into an action, and run it.
|
||||
///
|
||||
/// This used to be a switch over keyvals, and is now a table lookup, because
|
||||
/// the chords are configurable — see `shortcuts.zig`. What is left here is the
|
||||
/// translation into a chord and the doing of each action; which chord means
|
||||
/// which action is no longer this file's business.
|
||||
///
|
||||
/// The modifier match is exact, which the switch it replaced was not: it tested
|
||||
/// `ctrl and shift` and so also fired on Ctrl+Alt+Shift+T. Requiring the whole
|
||||
/// set to agree is what makes two chords over the same key — Alt+J and
|
||||
/// Alt+Shift+J — reliably different things.
|
||||
fn onShortcut(
|
||||
_: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
@@ -1610,121 +1623,116 @@ fn onShortcut(
|
||||
state: gdk.ModifierType,
|
||||
self: *Window,
|
||||
) callconv(.c) c_int {
|
||||
const ctrl = state.control_mask;
|
||||
const shift = state.shift_mask;
|
||||
const alt = state.alt_mask;
|
||||
const mods: shortcuts.Mods = .{
|
||||
.ctrl = state.control_mask,
|
||||
.alt = state.alt_mask,
|
||||
.shift = state.shift_mask,
|
||||
.super = state.super_mask,
|
||||
};
|
||||
|
||||
if (ctrl and shift) {
|
||||
switch (keyval) {
|
||||
gdk.KEY_T, gdk.KEY_t => {
|
||||
self.newTab() catch |err| {
|
||||
std.log.err("failed to open tab: {s}", .{@errorName(err)});
|
||||
};
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_W, gdk.KEY_w => {
|
||||
// Closes the focused pane. The view raises on_empty when its
|
||||
// last pane goes, which is what closes the tab.
|
||||
if (self.activeTab()) |tab| {
|
||||
if (tab.view.focusedPane()) |pane| tab.view.closePane(pane);
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_E, gdk.KEY_e => {
|
||||
self.addPane(.terminal);
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_B, gdk.KEY_b => {
|
||||
self.addPane(.web);
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_R, gdk.KEY_r => {
|
||||
if (self.activeTab()) |tab| self.beginRename(tab);
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_Z, gdk.KEY_z => {
|
||||
if (self.activeTab()) |tab| tab.view.toggleZoomFocused();
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_V, gdk.KEY_v => {
|
||||
// Only a terminal needs us to encode a paste for it. A web
|
||||
// pane has its own clipboard handling, so the key is left
|
||||
// alone rather than swallowed here.
|
||||
const terminal = self.focusedTerminal() orelse return 0;
|
||||
terminal.pasteFrom(.standard);
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_C, gdk.KEY_c => {
|
||||
// With nothing selected the key is declined rather than
|
||||
// swallowed, so a web pane's own copy still works and a
|
||||
// terminal still receives it.
|
||||
const terminal = self.focusedTerminal() orelse return 0;
|
||||
return if (terminal.copySelection(.standard)) 1 else 0;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
// Every shortcut needs one of these, so ordinary typing — which arrives
|
||||
// here first, on every key — is declined before anything is looked up.
|
||||
if (!mods.claiming()) return 0;
|
||||
|
||||
// Ctrl+Shift+arrows rearrange the focused terminal within its view. This
|
||||
// is the keyboard route to the same rearranging that dragging a pane's
|
||||
// header does.
|
||||
if (ctrl and shift) {
|
||||
const side: ?View.Side = switch (keyval) {
|
||||
gdk.KEY_Left => .left,
|
||||
gdk.KEY_Right => .right,
|
||||
gdk.KEY_Up => .top,
|
||||
gdk.KEY_Down => .bottom,
|
||||
else => null,
|
||||
};
|
||||
if (side) |s| {
|
||||
if (self.activeTab()) |tab| tab.view.moveFocused(s);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Shift turns the letter keys into their capitals, and a chord is written
|
||||
// as the key you press rather than the character it produces.
|
||||
const key_val = key.keyFromKeyval(gdk.keyvalToLower(keyval)) orelse return 0;
|
||||
|
||||
// Chords without Shift, which have to be the ones a terminal doesn't want
|
||||
// for itself. Ctrl+PageUp/PageDown cycles tabs, matching most tabbed
|
||||
// terminals; Ctrl+comma opens settings, which is the convention nearly
|
||||
// everywhere; Ctrl+F is claimed only over a web pane.
|
||||
if (ctrl and !shift) {
|
||||
switch (keyval) {
|
||||
gdk.KEY_F, gdk.KEY_f => {
|
||||
// Find-in-page, on the chord every browser uses. In a
|
||||
// terminal Ctrl+F is an ordinary control character that the
|
||||
// program running there is waiting for, so this only claims
|
||||
// the key when a web pane has focus.
|
||||
const browser = self.focusedBrowser() orelse return 0;
|
||||
browser.openFind();
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_comma => {
|
||||
self.openSettings();
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_Page_Up => {
|
||||
self.cycle(-1);
|
||||
return 1;
|
||||
},
|
||||
gdk.KEY_Page_Down => {
|
||||
self.cycle(1);
|
||||
return 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
const action = shortcuts.actionFor(
|
||||
.{ .mods = mods, .key = key_val },
|
||||
&Settings.get().keys,
|
||||
) orelse return 0;
|
||||
|
||||
// Alt+1..9 jumps straight to a tab; Alt+9 is "last tab" by convention.
|
||||
if (alt and !ctrl) {
|
||||
if (keyval >= gdk.KEY_1 and keyval <= gdk.KEY_9) {
|
||||
const n = keyval - gdk.KEY_1;
|
||||
if (n == 8) {
|
||||
self.selectIndex(self.tabs.items.len -| 1);
|
||||
} else {
|
||||
self.selectIndex(@intCast(n));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return if (self.perform(action)) 1 else 0;
|
||||
}
|
||||
|
||||
/// Run one action. Returns whether the key was used, which is not the same as
|
||||
/// whether anything happened: an action that has nothing to act on here — copy
|
||||
/// with no selection, find outside a web pane — declines the key so that
|
||||
/// whatever is focused gets it instead, while one that simply had nowhere to go
|
||||
/// still swallows it rather than sending a stray control code to a shell.
|
||||
fn perform(self: *Window, action: shortcuts.Action) bool {
|
||||
switch (action) {
|
||||
.new_tab => {
|
||||
self.newTab() catch |err| {
|
||||
std.log.err("failed to open tab: {s}", .{@errorName(err)});
|
||||
};
|
||||
},
|
||||
|
||||
// Closes the focused pane. The view raises on_empty when its last pane
|
||||
// goes, which is what closes the tab.
|
||||
.close_pane => if (self.activeTab()) |tab| {
|
||||
if (tab.view.focusedPane()) |pane| tab.view.closePane(pane);
|
||||
},
|
||||
|
||||
.new_terminal => self.addPane(.terminal),
|
||||
.new_web => self.addPane(.web),
|
||||
.rename_tab => if (self.activeTab()) |tab| self.beginRename(tab),
|
||||
.toggle_zoom => if (self.activeTab()) |tab| tab.view.toggleZoomFocused(),
|
||||
.open_settings => self.openSettings(),
|
||||
|
||||
// Only a terminal needs us to encode a paste for it. A web pane has its
|
||||
// own clipboard handling, so the key is left alone rather than
|
||||
// swallowed here.
|
||||
.paste => {
|
||||
const terminal = self.focusedTerminal() orelse return false;
|
||||
terminal.pasteFrom(.standard);
|
||||
},
|
||||
|
||||
// With nothing selected the key is declined rather than swallowed, so a
|
||||
// web pane's own copy still works and a terminal still receives it.
|
||||
.copy => {
|
||||
const terminal = self.focusedTerminal() orelse return false;
|
||||
return terminal.copySelection(.standard);
|
||||
},
|
||||
|
||||
// Find-in-page, on the chord every browser uses. In a terminal Ctrl+F
|
||||
// is an ordinary control character that the program running there is
|
||||
// waiting for, so this only claims the key over a web pane.
|
||||
.find => {
|
||||
const browser = self.focusedBrowser() orelse return false;
|
||||
browser.openFind();
|
||||
},
|
||||
|
||||
.prev_tab => self.cycle(-1),
|
||||
.next_tab => self.cycle(1),
|
||||
|
||||
// Moving focus between panes. A view edge with nothing beyond it stops
|
||||
// the move, but still takes the key: the chord was bound for navigating,
|
||||
// and sending it on to the shell at the edge of a split would be a
|
||||
// control code nobody asked for.
|
||||
.focus_pane_left => _ = self.focusNeighbor(.left),
|
||||
.focus_pane_right => _ = self.focusNeighbor(.right),
|
||||
.focus_pane_up => _ = self.focusNeighbor(.top),
|
||||
.focus_pane_down => _ = self.focusNeighbor(.bottom),
|
||||
|
||||
// Moving the pane itself, which is the keyboard route to the same
|
||||
// rearranging that dragging a pane's header does.
|
||||
.move_pane_left => self.movePane(.left),
|
||||
.move_pane_right => self.movePane(.right),
|
||||
.move_pane_up => self.movePane(.top),
|
||||
.move_pane_down => self.movePane(.bottom),
|
||||
|
||||
.select_tab_1 => self.selectIndex(0),
|
||||
.select_tab_2 => self.selectIndex(1),
|
||||
.select_tab_3 => self.selectIndex(2),
|
||||
.select_tab_4 => self.selectIndex(3),
|
||||
.select_tab_5 => self.selectIndex(4),
|
||||
.select_tab_6 => self.selectIndex(5),
|
||||
.select_tab_7 => self.selectIndex(6),
|
||||
.select_tab_8 => self.selectIndex(7),
|
||||
.select_last_tab => self.selectIndex(self.tabs.items.len -| 1),
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn focusNeighbor(self: *Window, side: View.Side) bool {
|
||||
const tab = self.activeTab() orelse return false;
|
||||
return tab.view.focusNeighbor(side);
|
||||
}
|
||||
|
||||
return 0;
|
||||
fn movePane(self: *Window, side: View.Side) void {
|
||||
const tab = self.activeTab() orelse return;
|
||||
tab.view.moveFocused(side);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user