summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig106
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;