diff --git a/src/Surface.zig b/src/Surface.zig index dfdc5b2bd..55fb89f7c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2050,6 +2050,45 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !void try self.io_thread.wakeup.notify(); }, + .scroll_to_top => { + _ = self.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .top = {} }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .scroll_to_bottom => { + _ = self.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .bottom = {} }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .scroll_page_up => { + const rows: isize = @intCast(self.grid_size.rows); + _ = self.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .delta = -1 * rows }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .scroll_page_down => { + const rows: isize = @intCast(self.grid_size.rows); + _ = self.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .delta = rows }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + + .scroll_page_fractional => |fraction| { + const rows: f32 = @floatFromInt(self.grid_size.rows); + const delta: isize = @intFromFloat(@floor(fraction * rows)); + _ = self.io_thread.mailbox.push(.{ + .scroll_viewport = .{ .delta = delta }, + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + .jump_to_prompt => |delta| { _ = self.io_thread.mailbox.push(.{ .jump_to_prompt = @intCast(delta), diff --git a/src/config.zig b/src/config.zig index c43884558..b5afc0684 100644 --- a/src/config.zig +++ b/src/config.zig @@ -435,15 +435,37 @@ pub const Config = struct { .{ .goto_split = .right }, ); - // Semantic prompts + // Viewport scrolling + try result.keybind.set.put( + alloc, + .{ .key = .home, .mods = .{ .shift = true } }, + .{ .scroll_to_top = {} }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .end, .mods = .{ .shift = true } }, + .{ .scroll_to_bottom = {} }, + ); try result.keybind.set.put( alloc, .{ .key = .page_up, .mods = .{ .shift = true } }, - .{ .jump_to_prompt = -1 }, + .{ .scroll_page_up = {} }, ); try result.keybind.set.put( alloc, .{ .key = .page_down, .mods = .{ .shift = true } }, + .{ .scroll_page_down = {} }, + ); + + // Semantic prompts + try result.keybind.set.put( + alloc, + .{ .key = .page_up, .mods = .{ .shift = true, .ctrl = true } }, + .{ .jump_to_prompt = -1 }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .page_down, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = 1 }, ); } @@ -489,6 +511,28 @@ pub const Config = struct { .{ .clear_screen = {} }, ); + // Viewport scrolling + try result.keybind.set.put( + alloc, + .{ .key = .home, .mods = .{ .super = true } }, + .{ .scroll_to_top = {} }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .end, .mods = .{ .super = true } }, + .{ .scroll_to_bottom = {} }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .page_up, .mods = .{ .super = true } }, + .{ .scroll_page_up = {} }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .page_down, .mods = .{ .super = true } }, + .{ .scroll_page_down = {} }, + ); + // Semantic prompts try result.keybind.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 4dec02d6f..46292ee90 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -132,6 +132,14 @@ pub fn parse(input: []const u8) !Binding { break :action @unionInit(Action, field.name, value); }, + .Float => { + const idx = colonIdx orelse return Error.InvalidFormat; + const param = actionRaw[idx + 1 ..]; + const value = std.fmt.parseFloat(field.type, param) catch + return Error.InvalidFormat; + break :action @unionInit(Action, field.name, value); + }, + else => unreachable, }, } @@ -177,6 +185,13 @@ pub const Action = union(enum) { /// Clear the screen. This also clears all scrollback. clear_screen: void, + /// Scroll the screen varying amounts. + scroll_to_top: void, + scroll_to_bottom: void, + scroll_page_up: void, + scroll_page_down: void, + scroll_page_fractional: f32, + /// Jump the viewport forward or back by prompt. Positive /// number is the number of prompts to jump forward, negative /// is backwards. @@ -442,3 +457,19 @@ test "parse: action with int" { try testing.expectEqual(@as(i16, 10), binding.action.jump_to_prompt); } } + +test "parse: action with float" { + const testing = std.testing; + + // parameter + { + const binding = try parse("a=scroll_page_fractional:-0.5"); + try testing.expect(binding.action == .scroll_page_fractional); + try testing.expectEqual(@as(f32, -0.5), binding.action.scroll_page_fractional); + } + { + const binding = try parse("a=scroll_page_fractional:+0.5"); + try testing.expect(binding.action == .scroll_page_fractional); + try testing.expectEqual(@as(f32, 0.5), binding.action.scroll_page_fractional); + } +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d3d140f89..f1bd0f517 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1460,6 +1460,7 @@ pub const ScrollViewport = union(enum) { /// Scroll to the bottom, i.e. the top of the active area bottom: void, + /// Scroll by some delta amount, up is negative. delta: isize, }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 3ed481ef2..f4986e3c6 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -300,6 +300,13 @@ pub fn clearScreen(self: *Exec, history: bool) !void { try self.queueWrite(&[_]u8{0x0C}); } +/// Scroll the viewport +pub fn scrollViewport(self: *Exec, scroll: terminal.Terminal.ScrollViewport) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + try self.terminal.scrollViewport(scroll); +} + /// Jump the viewport to the prompt. pub fn jumpToPrompt(self: *Exec, delta: isize) !void { const wakeup: bool = wakeup: { diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d64200792..2c32d459f 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -156,6 +156,7 @@ fn drainMailbox(self: *Thread) !void { }, .resize => |v| self.handleResize(v), .clear_screen => |v| try self.impl.clearScreen(v.history), + .scroll_viewport => |v| try self.impl.scrollViewport(v), .jump_to_prompt => |v| try self.impl.jumpToPrompt(v), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_stable => |v| try self.impl.queueWrite(v), diff --git a/src/termio/message.zig b/src/termio/message.zig index a75a06258..d0fc9f7c3 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -45,6 +45,9 @@ pub const Message = union(enum) { history: bool, }, + /// Scroll the viewport + scroll_viewport: terminal.Terminal.ScrollViewport, + /// Jump forward/backward n prompts. jump_to_prompt: isize,