diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-11 15:58:58 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-11 16:26:20 -0300 |
| commit | 4ca28745d774c232cd31a29c17878f19bbe24cf5 (patch) | |
| tree | face852acae5bc347e6bab2bb5cede501e0ce1d3 /src/pardes.zig | |
| parent | dedfdea43f0d6c7151c541284c81027969d89032 (diff) | |
| parent | 89d93d5e7348304bc7d8a148f9ad9c1beb200459 (diff) | |
| download | pardes-4ca28745d774c232cd31a29c17878f19bbe24cf5.tar.gz pardes-4ca28745d774c232cd31a29c17878f19bbe24cf5.zip | |
merge the macOS app branch: the AppKit shell, pixel attachments, live theming, and mupdf -Djpx
Three commits off 38e9919 (macos-app@upstream) merged into main's ghostty bump.
No textual conflicts, and two things the merge needed:
- nested.zig asked libc for fstatat. Darwin has it; on linux std.c declares it
`void` (glibc hides it behind a versioned symbol std cannot name), so the tty
build stopped at 'type void not a function'. statNoFollow keeps fstatat on
darwin and asks statx on linux for the same three fields, which is what this
file did before the branch generalized it to both platforms.
- .DS_Store rode along with a797a1a. Deleted, and .gitignore now says so.
linux: snap 86/86, unit-test, image-harness and mupdf-check green. nested.zig
also type-checks for aarch64-macos.
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index e2ad426f..e9cc746d 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -45,6 +45,12 @@ pub const lsp = @import("lsp/lsp.zig"); pub const Platform = enum { tty, gui, web, macos }; pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform)); +/// Frontends that draw their own text, and can therefore be told which face to +/// wear. On the tty the font belongs to the terminal emulator and in the +/// browser it belongs to the page, so there the Font builtins are not +/// disabled so much as meaningless — see builtins.zig. +pub const font_picker = platform == .gui or platform == .macos; + /// Native PDF quality is a shell property, but the core owns MuPDF and the /// RGBA cache. Kitty favors wire bandwidth; SDL favors physical-pixel text /// quality and asks the renderer to cover either fit axis without upscaling. @@ -1742,6 +1748,43 @@ test "startup config runs builtin lines in order and isolates bad lines" { }; } +test "Config prints the resolved startup config path and refreshes one buffer" { + const path = "/home/pardes-test/.config/pardes"; + // `startup_config` stays null: the file is MISSING and the path still + // resolves, which is the case this builtin exists to answer. + const p = try Pardes.init(std.testing.allocator, .{ .startup_config_path = path }); + defer p.deinit(); + + try std.testing.expect(p.executeBuiltinLine(0, "Config")); + const opened = p.active; + const out = p.panes[opened].?.file.?; + try std.testing.expectEqualStrings(path ++ "\n", out.content); + try std.testing.expectEqualStrings(config.config_buffer, std.fs.path.basename(out.path)); + try std.testing.expectEqual(output_pane.Origin{ .cmd = .Config }, out.output.?.from); + + // Asking again refreshes the buffer already open rather than stacking a + // byte-identical twin beside it — Help's rule, and for the same reason. + try std.testing.expect(p.executeBuiltinLine(0, "Config")); + try std.testing.expectEqual(opened, p.active); + var buffers: usize = 0; + for (p.panes) |slot| { + const sp = slot orelse continue; + const f = sp.file orelse continue; + const o = f.output orelse continue; + if (std.meta.eql(o.from, output_pane.Origin{ .cmd = .Config })) buffers += 1; + } + try std.testing.expectEqual(@as(usize, 1), buffers); +} + +test "Config says so when there is no per-user config path" { + const p = try Pardes.init(std.testing.allocator, .{}); + defer p.deinit(); + + try std.testing.expect(p.executeBuiltinLine(0, "Config")); + const out = p.panes[p.active].?.file.?; + try std.testing.expect(std.mem.indexOf(u8, out.content, "no per-user config path") != null); +} + test "runtime theme changes animate chrome and retarget without a jump" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); @@ -3230,6 +3273,22 @@ pub const File = struct { /// goes through file_pane.setContent, which bumps this; a pipe completion /// accepted against another revision would overwrite intervening work. revision: u32 = 0, + /// The `revision` this buffer was last WRITTEN at. Equal means what is on + /// screen is what is on disk; anything else is unsaved work. + /// + /// Bookkeeping only — nothing in the core renders it, and no shell has to + /// read it. It exists because a windowed host has somewhere to PUT the + /// answer (macOS puts a dot in the close button, and the proxy icon it sits + /// beside is the same pane's path), and a shell cannot derive it: revision + /// counts edits, and only the save knows which edit was the last one + /// committed. Zero for a fresh buffer, which is why every construction site + /// gets clean-on-open from the default and none of them mention it. + /// + /// Marked at the moment Save is ASKED, not when the write lands: the + /// save_file effect carries no completion back, so this is as honest as the + /// rest of that path. A failed write reads as saved, exactly as the tagline + /// already does. + saved_revision: u32 = 0, /// set = this is an OUTPUT buffer (acme's +Errors): a file pane with no /// file behind it, showing text the core produced itself. It records the /// COMMAND that opened it, and output_pane.zig's one table turns that into @@ -4191,6 +4250,11 @@ pub const Options = struct { /// deterministic; when present, each line is dispatched as a builtin /// before init returns and therefore before any frontend can render. startup_config: ?[]const u8 = null, + /// ...and WHERE that came from, which is a separate fact: the path + /// resolves even when the file does not exist, and that is precisely the + /// case the Config builtin is asked about. Null on the web and in every + /// core test, where there is no per-user config to name. + startup_config_path: ?[]const u8 = null, image_allocator: ?std.mem.Allocator = null, pdf_allocator: ?std.mem.Allocator = null, tree_sitter_allocator: ?std.mem.Allocator = null, @@ -4234,6 +4298,18 @@ const PendingPipe = struct { } }; +/// The acme verb the core just performed, for a shell that can answer with +/// something physical. macOS taps the trackpad under the finger that asked +/// (NSHapticFeedbackManager); the SDL shell already does the same thing with a +/// gamepad — `rumble` in src/gui/deck.zig, "a brief gentle ack for +/// execute/look, not a buzz". Two verbs rather than one flag because they +/// deserve to feel different: Exec did something, Look went somewhere. +pub const Haptic = enum { none, exec, look }; + +/// Zero-sized off macOS, the way PdfSlot is off -Dmupdf: no other shell reads +/// the field, so no other shell carries it. +const HapticSlot = if (platform == .macos) Haptic else void; + pub const Pardes = struct { gpa: std.mem.Allocator, image_gpa: std.mem.Allocator, @@ -4308,6 +4384,10 @@ pub const Pardes = struct { /// the PETSCII matcher without it. native_images: bool = false, quit: bool = false, + /// The Look or Exec that has happened and not yet been felt, taken by the + /// shell once per pump (takeHaptic). A pulse, not a queue: five Execs + /// inside one keystroke are still one thing the hand did. + haptic: HapticSlot = if (platform == .macos) .none else {}, drag: Drag = .none, hover_col: u16 = 0, hover_row: u16 = 0, @@ -4466,6 +4546,9 @@ pub const Pardes = struct { p.applyStartupConfig(); p.finishThemeInitialization(); p.sync(); + // A config file that opens a file with `Look …` armed the pulse before + // anyone touched anything. Nobody asked for that, so boot is silent. + _ = p.takeHaptic(); return p; } @@ -11641,6 +11724,7 @@ pub const Pardes = struct { /// search of the pane it came from, which is acme's button-3. pub fn lookAt(p: *Pardes, id: usize, txt: []const u8) void { const pane = p.panes[id] orelse return; + p.noteHaptic(.look); const trimmed = std.mem.trim(u8, txt, " \t\r\n"); // `` @`ls -la` `` names a COMMAND, not a path: run it, and land in the // pane that answers — looking at a thing means being SHOWN it, and a @@ -11808,6 +11892,10 @@ pub const Pardes = struct { const pane = p.panes[id] orelse return null; const cmd = commandText(txt); if (cmd.len == 0) return null; + // Before the builtin dispatch, and only at depth zero: `Exec ls` comes + // back through here as `ls` (executeBuiltinLine holds the depth), and + // one Tab is one thing the hand did, however many words it unwraps to. + if (p.exec_depth == 0) p.noteHaptic(.exec); if (p.executeBuiltinLine(id, cmd)) return null; if (p.exec_depth >= max_exec_depth) return null; p.exec_depth += 1; @@ -12282,6 +12370,7 @@ pub const Pardes = struct { p.applyStartupConfig(); p.finishThemeInitialization(); p.sync(); + _ = p.takeHaptic(); // see init: a restored session is not a gesture return p; } @@ -12549,6 +12638,23 @@ pub const Pardes = struct { return p.chrome_animation.isActive(); } + /// Arm the pulse. Look wins a tie because a Look that runs a command + /// (`` @`ls` ``, which is one gesture spelled as both) is felt as the + /// thing the user asked for, not as the shell it happened to need. + fn noteHaptic(p: *Pardes, pulse: Haptic) void { + if (comptime platform != .macos) return; + if (p.haptic == .look) return; + p.haptic = pulse; + } + + /// Take the armed pulse and disarm. The shell calls this once per pump, + /// after the tick that may have set it. + pub fn takeHaptic(p: *Pardes) Haptic { + if (comptime platform != .macos) return .none; + defer p.haptic = .none; + return p.haptic; + } + fn finishThemeInitialization(p: *Pardes) void { p.chrome_animation.snap(ChromeTheme.fromTheme(p.theme())); p.animate_theme_changes = true; |
