summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 22:59:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit6d148bb9c433814835935ead30020e43781b7f03 (patch)
tree49e42dbbbff64119ead447598a896a69e638e061 /src
parentb1485c97b493f7eaf65c6afdfb82e3065306b8fe (diff)
parentd4960472375ea68f709c56a677d8a37571419ff7 (diff)
downloadpardes-6d148bb9c433814835935ead30020e43781b7f03.tar.gz
pardes-6d148bb9c433814835935ead30020e43781b7f03.zip
Release Pardes 0.6
Merge: audit top-five cuts + tag sweeps + Config/DumpConfig (on the audit bug fixes C9, C2). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/CHANGELOG.md20
-rw-r--r--src/File.zig4
-rw-r--r--src/Messages.zig2
-rw-r--r--src/Output.zig2
-rw-r--r--src/builtins.zig32
-rw-r--r--src/config.zig11
-rw-r--r--src/crash.zig2
-rw-r--r--src/draw.zig26
-rw-r--r--src/fs.zig19
-rw-r--r--src/mouse.zig161
-rw-r--r--src/ninep/ctl.zig6
-rw-r--r--src/pardes.zig12
-rw-r--r--src/tutor.txt4
13 files changed, 245 insertions, 56 deletions
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index a2f493df..42f2eb6f 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,12 +1,30 @@
# Changelog
-## Unreleased
+## 0.0.6
+- A middle or right sweep in a column tag or the workspace tag runs or looks
+ up the swept text on release, highlighted while it is held, as in a pane's
+ tag and as in acme; a press used to act on the word under it at once.
+- `Config` (`SPC f c`) opens the startup file, `init`, in a pane, or goes
+ to its pane when it is open. With no file yet, it opens an empty pane of
+ that name, and Save writes it, making the directory first. The settings
+ report `Config` used to open is `DumpConfig`, in a `+DumpConfig` pane.
+- The workspace tag reads `… Dump Themes Config Debug Exit`: `Themes` in
+ place of `NextColor`, which stays a builtin, and `Config` beside it. A
+ workspace tag you edited, and one a dump kept, stays as it is.
+- F1-F25, Insert and the keypad reach the GUI's terminal panes too: one key
+ map serves the tty, the GUI and the board.
+- A session restored from a dump is built as a fresh one is: its column and
+ workspace tags hold several selections again, and 9P mtimes are real.
+- Zig files use `zls` from `PATH` as a language server like any other; the
+ in-process ZLS backend is gone, and the binary is about 9 MB smaller.
- Dumps and init files from before 0.0.4 no longer load their old tag
formats: a restored pane's tag tail is read only from `tag_tail`, and an old
terminal's `TTY` tag or raw output tail is not replayed. `ThemeSel`,
`FontSel` and `ColumnTags` are no longer recognised in init files or dumps
(`Themes` and `Fonts` are the words).
+- The GUI always embeds its shader sources; the prebuilt GLSL copies are
+ gone. `lspprobe` is `zig build lspbench -- probe`.
## 0.0.5
diff --git a/src/File.zig b/src/File.zig
index 8babb27d..e8a9e2f8 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -58,6 +58,10 @@ pub const State = struct {
/// as an undo to the save point is in acme.
saved_hash: ?u64 = null,
watch_after_save: bool = false,
+ /// Save makes the file's directory, and any above it, first: `Config`'s
+ /// pane for an init file that is not there yet, whose directory may not
+ /// be either.
+ make_dirs: bool = false,
/// The file changed on disk under unsaved edits, which were kept (see
/// `changed`): its hash, so the same change is said once, until a Save
/// or get settles it. A Save warns once before overwriting it, as acme's
diff --git a/src/Messages.zig b/src/Messages.zig
index 7dcb22ed..d1478ac7 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -782,7 +782,7 @@ test "a message falls in, lingers past the input that dismisses it, and dissolve
try std.testing.expect(p.executeBuiltinLine(p.active, "MessageDissolve 0"));
try std.testing.expectEqual(@as(u16, 3), messageFrames(p.settings.message_fall_ms));
try std.testing.expectEqual(@as(u16, 1), messageFrames(p.settings.message_dissolve_ms));
- // The Config report reads back as configuration.
+ // The DumpConfig report reads back as configuration.
try std.testing.expect(p.executeBuiltinLine(p.active, "MessageAnimation"));
try std.testing.expect(!p.settings.message_animation);
try std.testing.expect(p.executeBuiltinLine(p.active, "Msg plain"));
diff --git a/src/Output.zig b/src/Output.zig
index 5bc1f9fc..0581be41 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -827,7 +827,7 @@ pub fn openConfig(p: *Pardes, id: usize) !void {
.state = &p.settings,
});
const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Config }, "", content);
+ return openRead(p, id, .{ .cmd = .DumpConfig }, "", content);
}
/// The message-row log, oldest first — the lines that were said in passing and
diff --git a/src/builtins.zig b/src/builtins.zig
index c549b17f..c40b2e00 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1283,10 +1283,38 @@ pub const Help = struct {
}
};
-/// Open the settings in effect, each a word to click.
+/// Open the startup file, `init`, in a pane: its pane when it is open, else
+/// the file, else a pane of that name for Save to make it and its directory.
pub const Config = struct {
pub const scope: Scope = .session;
- pub const output: OutputTraits = .{ .name = config.config_buffer };
+ pub fn run(c: Ctx) void {
+ const path = c.p.opts.startup_config_path orelse
+ return c.p.reportFailure(c.id, "Config: no startup file here: no configuration directory");
+ var real_buf: [4096]u8 = undefined;
+ const real = if (pardes.filesystem.resolveOs(path, &real_buf)) |r| r.path else null;
+ for (c.p.panes, 0..) |slot, id| {
+ const pane = slot orelse continue;
+ const f = pane.file orelse continue;
+ if (f.output != null) continue;
+ if (!std.mem.eql(u8, f.path, path) and !(if (real) |r| std.mem.eql(u8, f.path, r) else false)) continue;
+ return look.focusPaneLine(c.p, id, .{}, .keep);
+ }
+ if (real != null) return look.lookAt(c.p, c.id, path);
+ // Not there yet: a pane named for it, which Save makes.
+ const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Config", error.NoPaneSlots);
+ c.p.newScratchFor(c.id, .scratch);
+ const pane = c.p.panes[free] orelse return;
+ @import("ninep/pane.zig").nameBuffer(c.p, free, path, false) catch |err| return c.p.reportError(c.id, "Config", err);
+ pane.file.?.make_dirs = true;
+ c.p.active = free;
+ }
+};
+
+/// Open the settings in effect, each a word to click, and the startup
+/// file's path.
+pub const DumpConfig = struct {
+ pub const scope: Scope = .session;
+ pub const output: OutputTraits = .{ .name = config.dump_config_buffer };
pub fn run(c: Ctx) void {
panes.Output.openConfig(c.p, c.id) catch |err| c.p.reportError(c.id, "config", err);
}
diff --git a/src/config.zig b/src/config.zig
index a8b4c24e..a70ae708 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -68,6 +68,7 @@ pub const leader_path = paths: {
.Recent = "fr",
.Grep = "fg",
.Config = "fc",
+ .DumpConfig = null,
.LocationsConfig = null,
.Tutor = "ht",
.Changelog = "hc",
@@ -266,7 +267,7 @@ pub const child_term = "xterm-256color";
pub const child_colorterm = "truecolor";
pub const child_term_program = "pardes";
-pub const topbar_str = "Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit";
+pub const topbar_str = "Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit";
pub const pane_builtins_str = "Tty Collapse Del";
pub const pdf_pane_builtins_str = "Tty PdfSections PdfTint Collapse Del";
@@ -452,7 +453,7 @@ pub const pipe_marker = " |";
pub const search_buffer = "+Search";
pub const help_buffer = "+Help";
-pub const config_buffer = "+Config";
+pub const dump_config_buffer = "+DumpConfig";
pub const effect_code_buffer = "+EffectCode";
pub const jumps_buffer = "+Jumps";
pub const recent_buffer = "+Recent";
@@ -968,7 +969,7 @@ pub const Runtime = struct {
}
};
- // The builtin registry and Config report share this command table.
+ // The builtin registry and DumpConfig report share this command table.
pub const settings = [_]Setting{
.{ .word = "Colors", .action = .{ .toggle = .colors }, .doc = "Syntax colours in text panes, on or off." },
.{ .word = "Wrap", .action = .{ .toggle = .wrap }, .doc = "Long lines wrap onto the next row, on or off." },
@@ -1291,7 +1292,7 @@ pub const Runtime = struct {
/// Each line reads as the command that would set it: the word, then the
/// value you would type after it. No colons, no units — `WindowOpacity 70`
- /// is both what Config prints and what you write back.
+ /// is both what DumpConfig prints and what you write back.
pub fn writeReport(out: *std.Io.Writer, context: ReportContext) !void {
const state = context.state;
var wrote_transition = false;
@@ -1650,7 +1651,7 @@ pub const Runtime = struct {
try std.testing.expect(FontSpec.parse(bad) == null);
}
- test "Config report observes every simple setting and all live context" {
+ test "DumpConfig report observes every simple setting and all live context" {
var state: Runtime = .{};
// Every setting, every context line: the report outgrows 4K.
var storage: [8192]u8 = undefined;
diff --git a/src/crash.zig b/src/crash.zig
index 10d38591..99822aad 100644
--- a/src/crash.zig
+++ b/src/crash.zig
@@ -23,7 +23,7 @@ const std = @import("std");
const builtin = @import("builtin");
const pardes = @import("pardes.zig");
-/// Beside `init`, so `Config` opens the directory that holds both.
+/// The crash log's name, beside `init` in the configuration directory.
pub const name = "crashes";
/// The config directory `config.User.load` resolved, COPIED rather than
diff --git a/src/draw.zig b/src/draw.zig
index ad24153f..0ea0b35e 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -1109,8 +1109,9 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer,
// The word under the pointer, a Look or Exec target: on a header even
// while it is edited, on a notice, and on the grid's copy of either
// (joined from this); not on a column a carried column's pointer is
- // aiming at a place over.
- const hovered = p.pointer_inside and !(kind == .column and p.drag == .column_move);
+ // aiming at a place over; nor under a held middle or right sweep,
+ // which paints what it sweeps instead.
+ const hovered = p.pointer_inside and !(kind == .column and p.drag == .column_move) and p.header_sweep == null;
if (hovered) {
const tag_hit = p.pointer_tag_hit;
const col: ?usize = if (tag_hit) |pointer|
@@ -1141,6 +1142,27 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer,
const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .x = @intCast(caret), .y = line, .bar = t.mode == .insert };
}
+ // A middle or right sweep held over this header, tinted by its
+ // button as a pane tag's is.
+ if (p.header_sweep) |sweep| if (sweep.column(p)) |column| {
+ const mine = switch (kind) {
+ .workspace => column == null,
+ .column => column == id,
+ .pane, .notice, .body => false,
+ };
+ const swept = sweep.span(text.len);
+ const start = modal.lineStartOffset(text, index + top);
+ const end = start + shown.len;
+ if (mine and swept.lo < swept.hi and swept.hi > start and swept.lo < end) {
+ const button = @intFromEnum(sweep.button);
+ var col = panes.File.rawDisplayCol(shown, @max(swept.lo, start) - start) -| scroll;
+ const stop = panes.File.rawDisplayCol(shown, @min(swept.hi, end) - start) -| scroll;
+ while (col < stop and col < columns) : (col += 1) {
+ surface.at(@intCast(col), 0).style.bg = .{ .rgb = body_layer.buttonTints(p.theme())[button] };
+ surface.at(@intCast(col), 0).style.fg = .{ .rgb = body_layer.buttonInks(p.theme())[button] };
+ }
+ }
+ } else |_| {};
}
}
diff --git a/src/fs.zig b/src/fs.zig
index fc3eb98f..1d0bbc22 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -637,6 +637,25 @@ test "a path with a part over 255 bytes is NameTooLong, never a panic" {
try std.testing.expectError(error.NameTooLong, statPath(std.testing.io, long, .{}));
}
+/// Make `dir` and each directory above it that is missing, as `mkdir -p`
+/// does: one already there is no failure. A failure shows at the open
+/// that follows.
+pub fn makeDirs(dir: []const u8) void {
+ if (comptime !platform_has_fs) return;
+ var z: [4096:0]u8 = undefined;
+ if (dir.len == 0 or dir.len >= z.len) return;
+ @memcpy(z[0..dir.len], dir);
+ z[dir.len] = 0;
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ for (z[1..dir.len], 1..) |c, i| if (c == '/') {
+ z[i] = 0;
+ _ = libc.mkdir(z[0..i :0], 0o755);
+ z[i] = '/';
+ };
+ _ = libc.mkdir(z[0..dir.len :0], 0o755);
+}
+
pub fn localPath(path: []const u8) ?[]const u8 {
if (std.mem.eql(u8, path, "/n/os")) return "/";
if (std.mem.startsWith(u8, path, "/n/os/")) return path[5..];
diff --git a/src/mouse.zig b/src/mouse.zig
index cb0378de..2280020d 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -70,6 +70,84 @@ pub const Drag = union(enum) {
}
};
+/// A middle or right sweep over a column's tag or the workspace's, held as
+/// a pane tag's is: painted in its button's tint while the button is down,
+/// and run (or looked for) in that tag's context when it comes up. `anchor`
+/// and `cur` are byte offsets into the header's text.
+pub const HeaderSweep = struct {
+ /// The column's serial; null for the workspace's tag.
+ serial: ?u32,
+ button: Mouse.Button,
+ anchor: usize,
+ cur: usize,
+
+ /// The column index the sweep is over, or null for the workspace's
+ /// tag; `error.Gone` once its column has been closed.
+ pub fn column(s: HeaderSweep, p: *const Pardes) error{Gone}!?usize {
+ const serial = s.serial orelse return null;
+ return layout.columnBySerial(p, serial) orelse error.Gone;
+ }
+
+ /// The swept bytes, empty for a click that has not moved.
+ pub fn span(s: HeaderSweep, len: usize) struct { lo: usize, hi: usize } {
+ return .{ .lo = @min(s.anchor, s.cur, len), .hi = @min(@max(s.anchor, s.cur), len) };
+ }
+};
+
+/// What a pointer at (mcol, mrow) is over in a column's tag, or the
+/// workspace's for null: the header's text, the line, and the byte offset
+/// in that text. `clamp` holds a held sweep to the header's rows and text,
+/// as a pane tag's sweep stays on its tag.
+fn headerPoint(p: *Pardes, column: ?usize, mcol: u16, mrow: u16, clamp: bool) ?struct { t: *panes.Text, text: []const u8, line: u16, at: usize } {
+ const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
+ const x = if (column) |c| p.col_x[c] + TAG_TEXT_INSET else 0;
+ const focused = p.header_focus and p.header_column == column;
+ const text = edit.flatSurface(p, t) catch return null;
+ const base = if (column != null) p.topBarHeight() else 0;
+ const rows = if (column != null) p.columnBarHeight() else p.topBarHeight();
+ const row: u16 = if (clamp) @min(mrow -| base, rows -| 1) else mrow -| base;
+ const top: usize = if (focused) p.header_top else 0;
+ const lines = modal.lineCount(text);
+ const line: u16 = if (clamp) @intCast(@min(row + top, lines -| 1)) else @intCast(row + top);
+ if (line > 0 and line >= lines) return null;
+ const bar = modal.lineSlice(text, line);
+ const scroll = if (focused) p.header_scroll else 0;
+ const kind: Layer.Kind = if (column != null) .column else .workspace;
+ const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, kind, column orelse 0, p.pointer_tag_hit, clamp, row) orelse (mcol -| x)) + scroll));
+ return .{ .t = t, .text = text, .line = line, .at = modal.lineStartOffset(text, line) + at };
+}
+
+/// A header sweep's button came up: a click runs the word under it, or the
+/// header's selection when it is on that; a sweep runs what it swept.
+fn headerSweepRun(p: *Pardes, sweep: HeaderSweep) void {
+ const column = sweep.column(p) catch return;
+ const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
+ const text = edit.flatSurface(p, t) catch return;
+ const swept = sweep.span(text.len);
+ const selected = if (swept.lo < swept.hi) text[swept.lo..swept.hi] else click: {
+ const line = std.mem.count(u8, text[0..swept.lo], "\n");
+ const start = modal.lineStartOffset(text, line);
+ const bar = modal.lineSlice(text, line);
+ const at = swept.lo - start;
+ const focused = p.header_focus and p.header_column == column;
+ const on = if (focused) tagline.headerSelection(t, text, line) else null;
+ break :click if (on) |sp| (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at)) else wordAtCol(bar, at);
+ };
+ const word = p.scratch.allocator().dupe(u8, selected) catch return;
+ tagline.exitHeader(p);
+ // A column tag's command acts on that column, empty or not.
+ p.exec_column = column;
+ defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
+ if (word.len == 0) return;
+ if (sweep.button == config.exec_button) {
+ const named = std.meta.stringToEnum(Builtin, word);
+ const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active);
+ _ = exec.execute(p, p.active, exec.withArg(p, word, held));
+ } else exec.runBuiltin(p, config.look_cmd, p.active, "", word);
+}
+
fn clampBorderCol(lx: u16, lw: u16, rw: u16, mcol: u16) u16 {
const lo = lx + config.MINW;
const hi = lx +| lw +| rw -| config.MINW;
@@ -255,7 +333,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const previous_tag_hit = p.pointer_tag_hit;
p.pointer_tag_hit = if (m.tag_hit) |point| p.reprojectTagHit(point) else null;
if (m.tag_hit != null and p.pointer_tag_hit == null and m.kind == .press) return;
- if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) {
+ if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag or p.header_sweep != null)) {
if (previous_tag_hit) |previous| {
var continued = previous;
continued.pixel_x = (@as(f32, @floatFromInt(raw_col)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_w));
@@ -263,7 +341,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.pointer_tag_hit = continued;
}
}
- if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) {
+ if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag or p.header_sweep != null)) {
if (previous_tag_hit) |previous| if (p.pointer_tag_hit) |*current| {
current.kind = previous.kind;
current.id = previous.id;
@@ -289,7 +367,13 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.raw_hover_intent = false;
const mapped = p.presentation.pointer(p.screen_w, p.screen_h, raw_col, raw_row) orelse {
look.cancelLookHover(p);
- if (m.kind == .release) dragRelease(p, m.button);
+ if (m.kind == .release) {
+ if (p.header_sweep) |sweep| if (sweep.button == m.button) {
+ p.header_sweep = null;
+ return headerSweepRun(p, sweep);
+ };
+ dragRelease(p, m.button);
+ }
return;
};
const mcol = mapped.col;
@@ -309,6 +393,32 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
p.chord_arg = null;
}
}
+ if (p.header_sweep) |*sweep| {
+ if (m.kind == .press and switch (m.button) {
+ .left, .middle, .right => true,
+ else => false,
+ }) {
+ // acme 2-3 / 3-2: the other of execute and look cancels the
+ // sweep. The select button is inert: a header's command already
+ // takes the held selection as its argument, which is what a 2-1
+ // chord would give it.
+ if (m.button != config.select_button) p.header_sweep = null;
+ return;
+ }
+ if (m.button == sweep.button and (m.kind == .drag or m.kind == .release)) {
+ const column = sweep.column(p) catch {
+ p.header_sweep = null;
+ return;
+ };
+ if (headerPoint(p, column, mcol, mrow, true)) |point| sweep.cur = point.at;
+ if (m.kind == .release) {
+ const done = sweep.*;
+ p.header_sweep = null;
+ headerSweepRun(p, done);
+ }
+ return;
+ }
+ }
if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) {
for (0..p.ncol) |column| {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
@@ -360,43 +470,26 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
};
if (column == null or mcol < x) return;
}
- const t = if (column) |c| &p.col_tags[c] else &p.global_tag;
- const focused = p.header_focus and p.header_column == column;
- const text = edit.flatSurface(p, t) catch return;
- const row: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0);
- const line: u16 = @intCast(row + if (focused) p.header_top else 0);
- if (line > 0 and line >= modal.lineCount(text)) return;
- const bar = modal.lineSlice(text, line);
- const scroll = if (focused) p.header_scroll else 0;
- const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, row) orelse (mcol -| x)) + scroll));
+ const point = headerPoint(p, column, mcol, mrow, false) orelse return;
if (m.button == config.select_button) {
+ const t = point.t;
+ const at = point.at - modal.lineStartOffset(point.text, point.line);
// A clicked header is typed straight into, as a clicked tag is.
- if (!focused) tagline.enterHeader(p, column);
+ if (!(p.header_focus and p.header_column == column)) tagline.enterHeader(p, column);
t.mode = .insert;
- t.cur_row = line;
+ t.cur_row = point.line;
t.cur_col = @intCast(at);
- t.vsel = .{ .active = false, .row = line, .col = @intCast(at) };
+ t.vsel = .{ .active = false, .row = point.line, .col = @intCast(at) };
p.header_drag = true;
} else {
- const span = if (focused) tagline.headerSelection(t, text, line) else null;
- const selected = if (span) |sp|
- (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at))
- else
- wordAtCol(bar, at);
- const word = p.scratch.allocator().dupe(u8, selected) catch return;
- tagline.exitHeader(p);
- // A column tag's command acts on that column, empty or not.
- p.exec_column = column;
- defer p.exec_column = null;
- p.exec_header = true;
- defer p.exec_header = false;
- if (word.len > 0) {
- if (m.button == config.exec_button) {
- const named = std.meta.stringToEnum(Builtin, word);
- const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active);
- _ = exec.execute(p, p.active, exec.withArg(p, word, held));
- } else exec.runBuiltin(p, config.look_cmd, p.active, "", word);
- }
+ // Held, as a pane tag's sweep is: it runs when the button
+ // comes up (headerSweepRun).
+ p.header_sweep = .{
+ .serial = if (column) |c| p.col_serial[c] else null,
+ .button = m.button,
+ .anchor = point.at,
+ .cur = point.at,
+ };
}
return;
}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 060e0d6c..5ed03c58 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -485,7 +485,7 @@ const e_prompt = "control message needs its argument";
/// Every setting, one a line, in the words a write of it takes, so that the
/// file read back is the configuration, as webfs's ctl reads (webfs(4)).
/// What is not a setting -- platform, themes, where the config came from --
-/// is /status's or the Config window's.
+/// is /status's or the DumpConfig window's.
fn writeSettings(p: *Pardes, w: *std.Io.Writer) !void {
try writeSettingsOf(p, &p.settings, w);
try w.writeAll("LocationsConfig ");
@@ -523,7 +523,7 @@ pub fn writeSettingsOf(p: *Pardes, s: *const config.Runtime, w: *std.Io.Writer)
inline else => |field| try w.print("{s} {s}\n", .{ word, @tagName(@field(s, @tagName(field))) }),
},
.message_ms => |which| try w.print("{s} {d}\n", .{ word, @field(s, "message_" ++ @tagName(which) ++ "_ms") }),
- // The directory in effect, as Config reports it; bare only when
+ // The directory in effect, as DumpConfig reports it; bare only when
// there is none (no home to put the default in).
.dump_dir => {
var buf: [900]u8 = undefined;
@@ -1165,7 +1165,7 @@ test "the root ctl reads the settings as a write takes them, and takes the sessi
try testing.expectEqual(@as(u64, settings.len), call(p, .{ .tag = 1, .op = .getattr, .node = root_ctl }).reply.attr.size);
try testing.expect(std.mem.indexOf(u8, settings, "Verbose on\n") != null);
try testing.expect(std.mem.indexOf(u8, settings, "LocationsConfig ") != null);
- // Settings only: nothing /status or the Config window says.
+ // Settings only: nothing /status or the DumpConfig window says.
try testing.expect(std.mem.indexOf(u8, settings, "Platform") == null);
try testing.expect(std.mem.indexOf(u8, settings, "requested") == null);
diff --git a/src/pardes.zig b/src/pardes.zig
index 151fe7b0..a63568db 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1199,7 +1199,7 @@ test "acme placement: New and 9P's new take the bottom half of the last pane" {
try std.testing.expect(p.rects[made].y > was.y + BOX_H);
}
-test "Placement pardes brings back the old rules, and says so in Config" {
+test "Placement pardes brings back the old rules, and says so in DumpConfig" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 });
defer p.deinit();
try std.testing.expect(p.executeBuiltinLine(p.active, "Placement pardes"));
@@ -3336,7 +3336,7 @@ test "s in a header selects in the header's own text" {
var ranges: [Text.max_selections]modal.Selection = undefined;
const text = try edit.flatSurface(p, &p.global_tag);
const got = p.global_tag.ranges(text, 0, &ranges);
- try std.testing.expectEqual(@as(usize, 3), got.n); // Newcol, Joincol, NextColor
+ try std.testing.expectEqual(@as(usize, 2), got.n); // Newcol, Joincol
try std.testing.expectEqual(@as(u16, 0), pane.body.nsel);
}
@@ -4317,6 +4317,8 @@ pub const Pardes = struct {
header_column: ?usize = null,
/// A mouse sweep is extending the focused header's selection.
header_drag: bool = false,
+ /// A middle or right sweep held over a column's or the workspace's tag.
+ header_sweep: ?mouse.HeaderSweep = null,
header_scroll: usize = 0,
/// The focused header's first line on screen, which follows its caret
/// when it has more lines than rows.
@@ -5102,7 +5104,7 @@ pub const Pardes = struct {
}
/// Resolve the request already handed to the GUI host. Requested and
- /// effective tuples deliberately remain separate so Config can show both.
+ /// effective tuples deliberately remain separate so DumpConfig can show both.
pub fn acknowledgeFont(
p: *Pardes,
effective_name: []const u8,
@@ -5437,6 +5439,8 @@ pub const Pardes = struct {
const was = f.saved_revision;
const failures = p.fs.failures;
f.saved_revision = f.revision;
+ if (f.make_dirs) if (filesystem.localPath(f.path)) |local|
+ filesystem.makeDirs(std.fs.path.dirname(local) orelse "/");
p.hostWriteFile(sf.pane, f.path, f.content);
const saved_pane = p.panes[sf.pane] orelse return;
if (saved_pane.serial != serial) return;
@@ -6562,7 +6566,7 @@ pub const Pardes = struct {
}
/// A native post pass proved unavailable: the chain is emptied, which
- /// Config then reports.
+ /// DumpConfig then reports.
pub fn disableSceneEffects(p: *Pardes) void {
p.settings.post.clear();
}
diff --git a/src/tutor.txt b/src/tutor.txt
index c59deeae..f8aa7aef 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -65,8 +65,8 @@
Every pane has Collapse: hide its body, then execute again to expand it.
The top bar is:
- Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor
- Debug Exit
+ Newcol Joincol Find Grep Help Changelog Tutor Dump Themes
+ Config Debug Exit
Reach it from the keyboard with `k` off the topmost tagline; `j` comes
back down.