diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-22 17:43:57 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 9695cb098c784792af79c22d46a17f7bc7247d33 (patch) | |
| tree | fa446f3cb588b3184b7914b3f50c36cd3e5ece7b /src/config.zig | |
| parent | f962de52df4b3d33438150920ae95b8708b0dde0 (diff) | |
| download | pardes-9695cb098c784792af79c22d46a17f7bc7247d33.tar.gz pardes-9695cb098c784792af79c22d46a17f7bc7247d33.zip | |
Repaint PDF highlights by row, send rasters by shared memory, and animate messages
PDF highlights (hover preview, search, selection) are baked into page rasters,
and any change re-rendered the whole page with MuPDF; the TTY then re-sent it
as base64 (4.7 MB a page), the GUI as a new texture. Worse, a pointer motion
over a PDF invalidated the page even when no preview was shown, so every
motion paid that. Now:
- A raster whose baked highlight set equals the wanted one is left alone.
- A highlighted page keeps its clean rows (before highlights and tint); a
change repaints only the rows of quads that differ, running MuPDF's
highlight pass (pardes_pdf_paint_highlights) over those clean rows and
tinting them: the operations a full render performs, so the pixels are
identical. MuPDF band renders are NOT bit-identical to a whole page (edge
rows, resampled images), so they are never used to patch; the comment
claiming otherwise is corrected.
- ImagePlace.patch hands shells the changed rows; the GUI uploads just those
rows into the texture it holds.
- The TTY probes kitty shared memory (t=s) with an id vaxis never reaches and
sends rasters as a /dev/shm name when the terminal reads it; direct base64
otherwise (ssh).
- Shells that take row patches (GUI, TTY with shm) repaint a selection while
it is dragged instead of only on release.
Latency elsewhere:
- TTY: an animating frame no longer sleeps 16 ms blind; a tick thread posts
into the input queue, so input inside the frame is handled at once.
- TTY and GUI: queued pointer motions coalesce to the last.
- GUI: a skipped swapchain image re-arms the frame (3 retries); animations
still tick while nothing presents.
- Editing: the line index is carried across an edit instead of rebuilt from
a scan of the whole file per keystroke.
Messages fall into their row (ease-in; the GUI slides the band out from under
the tagline, a terminal fades it), stay until the next input as before, linger
MessageLinger ms (default 800), and dissolve (ease-out). MessageAnimation
toggles it; both are settings, in Config and startup files. The snapshot
harness pins the old behaviour. The detached server now ticks animations.
A restored terminal comes back live: the old screen and scrollback (dumped
as clean VT by ghostty's formatter, replayed at the new size; older dumps
fall back to their rendered text), a dim
"restored history" marker, then a new shell in the directory it was in.
Right-click on a line number in a file pane looks at that line (a sticky
context header's number included).
Measured with an external pty driver (TTY), an in-process fence trace
(GUI, PARDES_TEST_LATENCY), and test/pdf_pointer_bench.zig (pixel identity
against the baseline and a whole-page oracle); balanced A/A/B rounds, paired
per-round statistics.
Messages stack: each event gets its own row and its own fall, linger and
dissolve; a line keeps its row until it leaves and a new one fills the first
free row. Announcements and statuses are replaced in place, not stacked.
MessageFall, MessageDissolve and DumpDir are settings Config reports.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src/config.zig')
| -rw-r--r-- | src/config.zig | 66 |
1 files changed, 63 insertions, 3 deletions
diff --git a/src/config.zig b/src/config.zig index 98bbf3f4..527c3752 100644 --- a/src/config.zig +++ b/src/config.zig @@ -102,6 +102,11 @@ pub const leader_path = paths: { table.set(.TaglineSize, null); } if (pardes.platform == .gui) table.set(.Pet, null); + table.set(.MessageAnimation, null); + table.set(.MessageLinger, null); + table.set(.MessageFall, null); + table.set(.MessageDissolve, null); + table.set(.DumpDir, null); if (builtins.capabilities.window_opacity) table.set(.WindowOpacity, null); if (builtins.capabilities.window_blur) table.set(.WindowBlur, null); if (builtins.capabilities.panel_transitions) { @@ -269,6 +274,10 @@ pub const wheel_cols: i32 = 4; pub const look_preview_delay_frames: ?u16 = 2; +/// The longest any message timing (MessageLinger, MessageFall, +/// MessageDissolve) may be set to, in milliseconds. +pub const message_ms_max: u16 = 60_000; + // Tag fonts keep body-cell geometry; valid sizes are 1...100 percent. pub const gui_tagline_font_percent: u8 = 82; comptime { @@ -558,6 +567,17 @@ pub const Runtime = struct { syntax_bold: bool = false, /// Builtins announce themselves on the message row. verbose: bool = true, + /// A message falls into its row and dissolves out of it. + message_animation: bool = true, + /// How long a message stays after the key or click that dismisses it. + message_linger_ms: u16 = 800, + /// How long its ease-in fall into the row takes. + message_fall_ms: u16 = 180, + /// How long its ease-out dissolve takes. + message_dissolve_ms: u16 = 300, + /// Where Dump writes and a relative Restore looks; empty is the default + /// ($XDG_DATA_HOME/pardes, else ~/.local/share/pardes). + dump_dir: Text(limits.host_path_cap) = .{}, window_opacity: u8 = 100, window_blur: u8 = 0, window_opacity_pending: bool = true, @@ -655,7 +675,10 @@ pub const Runtime = struct { pub const Capability = std.meta.FieldEnum(Capabilities); - pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose }; + /// The message timings a setting word sets, each in milliseconds. + pub const MessageTime = enum { linger, fall, dissolve }; + + pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation }; pub const Scene = std.meta.FieldEnum(layout.SceneEffect); pub const Action = union(enum) { @@ -666,6 +689,8 @@ pub const Runtime = struct { tagline_size, window_opacity, window_blur, + message_ms: MessageTime, + dump_dir, transition: layout.Transition, scene: Scene, }; @@ -679,7 +704,7 @@ pub const Runtime = struct { return switch (setting.action) { // A toggle flips when it is a bare tagline word and SETS when // it is given `on` or `off`, so the report is configuration. - .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .toggle => true, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .dump_dir, .toggle => true, else => false, }; } @@ -703,6 +728,11 @@ pub const Runtime = struct { .{ .word = "ColumnTags", .action = .{ .toggle = .column_tags } }, .{ .word = "SyntaxBold", .action = .{ .toggle = .syntax_bold } }, .{ .word = "Verbose", .action = .{ .toggle = .verbose } }, + .{ .word = "MessageAnimation", .action = .{ .toggle = .message_animation } }, + .{ .word = "MessageLinger", .action = .{ .message_ms = .linger } }, + .{ .word = "MessageFall", .action = .{ .message_ms = .fall } }, + .{ .word = "MessageDissolve", .action = .{ .message_ms = .dissolve } }, + .{ .word = "DumpDir", .action = .dump_dir }, .{ .word = "Theme", .action = .theme }, .{ .word = "Shell", .action = .shell }, .{ .word = "Font", .action = .font, .availability = .font_picker }, @@ -827,6 +857,17 @@ pub const Runtime = struct { if (percent > 100) return false; state.window_blur = percent; }, + // Bare, it returns to the default directory. + .dump_dir => if (!state.dump_dir.set(std.mem.trim(u8, argument orelse "", " \t\r\n"))) return false, + .message_ms => |which| { + const text = std.mem.trim(u8, argument orelse return false, " \t\r\n"); + for (text) |byte| if (!std.ascii.isDigit(byte)) return false; + const ms = std.fmt.parseInt(u16, text, 10) catch return false; + if (ms > message_ms_max) return false; + switch (which) { + inline else => |w| @field(state, "message_" ++ @tagName(w) ++ "_ms") = ms, + } + }, .transition => |effect| state.toggleTransition(effect), .scene => |effect| switch (effect) { inline else => |field| { @@ -847,6 +888,9 @@ pub const Runtime = struct { compiled_default_shell: []const u8, gui_shader_source_mode: ?[]const u8 = null, hover_delay_frames: ?u16, + /// Where dumps go when `DumpDir` is unset; empty without a host + /// filesystem. + default_dump_dir: []const u8 = "", native_images: bool, capabilities: Capabilities, state: *const Runtime, @@ -895,6 +939,13 @@ pub const Runtime = struct { else try out.print("{s} unsupported\n", .{setting.word}); }, + .dump_dir => { + const dir = if (state.dump_dir.len > 0) state.dump_dir.get() else context.default_dump_dir; + try out.print("{s} {s}\n", .{ setting.word, if (dir.len > 0) dir else "(none)" }); + }, + .message_ms => |which| try out.print("{s} {d}\n", .{ setting.word, switch (which) { + inline else => |w| @field(state, "message_" ++ @tagName(w) ++ "_ms"), + } }), .shell => { const chosen = state.shell.requested.get(); try out.print( @@ -993,7 +1044,7 @@ pub const Runtime = struct { for (settings[i + 1 ..]) |later| try std.testing.expect(!std.mem.eql(u8, setting.word, later.word)); try std.testing.expectEqual(switch (setting.action) { - .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .toggle => true, + .shell, .theme, .font, .tagline_size, .window_opacity, .window_blur, .message_ms, .dump_dir, .toggle => true, else => false, }, setting.takesArg()); } @@ -1152,6 +1203,8 @@ pub const Runtime = struct { .tagline_size => "73", .window_opacity => "85", .window_blur => "40", + .message_ms => "1500", + .dump_dir => "~/dumps", else => null, }; try std.testing.expect(apply(&state, setting, argument)); @@ -1170,11 +1223,18 @@ pub const Runtime = struct { .column_tags => "ColumnTags off\n", .syntax_bold => "SyntaxBold on\n", .verbose => "Verbose off\n", + .message_animation => "MessageAnimation off\n", }, .shell => "Shell requested (new panes) fish\n", .tagline_size => "TaglineSize 73\n", .window_opacity => "WindowOpacity 85 (pending)\n", .window_blur => "WindowBlur 40\n", + .dump_dir => "DumpDir ~/dumps\n", + .message_ms => |which| switch (which) { + .linger => "MessageLinger 1500\n", + .fall => "MessageFall 1500\n", + .dissolve => "MessageDissolve 1500\n", + }, .transition => |transition| switch (transition) { .off => unreachable, .slide => "Panel transition PanelSlide\n", |
