diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 09:16:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | a9de2b51ab76a43250a4d6c41a0ddd54e6970049 (patch) | |
| tree | a2138ead27f434026c224fd8b926f66b7fd7fe0c /src/tagline.zig | |
| parent | 0d04bc9214cccce42d74862b31376d49bdda3956 (diff) | |
| download | pardes-a9de2b51ab76a43250a4d6c41a0ddd54e6970049.tar.gz pardes-a9de2b51ab76a43250a4d6c41a0ddd54e6970049.zip | |
Let the keyboard reach a tag's path, read-only
The tag's computed path, dirty marker and page were out of the keyboard's
reach: its cursor lived in the text after them. It now lives in the tag as
shown, as in acme where the name is ordinary text at the start of the tag, so
`0` goes to the line's start and motions select and yank the path. The path
stays read-only: an edit that would change it is refused and leaves the
cursor where it was, and typing into a file's path drafts a new name as a
click on it does. Sync moves the tag's positions when the prefix changes
length, so a rename or the dirty marker still never moves the cursor off the
text after it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/tagline.zig')
| -rw-r--r-- | src/tagline.zig | 90 |
1 files changed, 63 insertions, 27 deletions
diff --git a/src/tagline.zig b/src/tagline.zig index 8bd8000a..92c18510 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -102,20 +102,29 @@ pub fn revealHeader(p: *Pardes) void { } } +/// The computed start of a pane's tag -- its path and dirty marker, a +/// PDF's page, a terminal's directory -- without the space that parts it +/// from the tag's own text. It is never stored, and never edited. +pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { + if (comptime pdf_enabled) if (pane.pdf) |pv| + return std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count }); + if (pane.image) |*state| return panes.Image.tagPrefix(arena, state); + if (pane.file) |f| { + const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; + if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision) + return std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker }); + return name; + } + return pane.cwdSlice(); +} + pub fn tagPrefix(p: *Pardes, pane: *Pane) ![]u8 { - const arena = p.scratch.allocator(); - const prefix = prefix: { - if (comptime pdf_enabled) if (pane.pdf) |pv| - break :prefix try std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count }); - if (pane.image) |*state| break :prefix try panes.Image.tagPrefix(arena, state); - if (pane.file) |f| { - const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; - if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision) - break :prefix try std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker }); - break :prefix name; - } - break :prefix pane.cwdSlice(); - }; + return prefixIn(p.scratch.allocator(), pane); +} + +/// The prefix as the tag shows it, built in `arena`. +pub fn prefixIn(arena: std.mem.Allocator, pane: *Pane) ![]u8 { + const prefix = try pathPrefix(arena, pane); // The space between the prefix and the tag's own text is the text's // (every default starts with one), but the prefix is computed, so it // cannot trust the text to keep it: with it deleted, Look and Exec @@ -159,21 +168,23 @@ pub fn tagText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]u8 { /// it starts on Save, so that `:` and the execute key write the file. pub fn enterTag(p: *Pardes, pane: *Pane) void { const t = &pane.tag; - const tail = curTail(pane); + const whole = tagText(p, p.scratch.allocator(), pane) catch return; if (!t.cur_pinned) { + // Save is looked for in the tag's own text, after the prefix. + const lead = whole.len - curTail(pane).len; + const tail = whole[lead..]; var words = std.mem.tokenizeAny(u8, tail, " \t\n"); - const at = while (words.next()) |word| { + const at = lead + while (words.next()) |word| { if (std.mem.eql(u8, word, "Save")) break @intFromPtr(word.ptr) - @intFromPtr(tail.ptr); } else tail.len - std.mem.trimStart(u8, tail, " ").len; - const cursor = modal.positionAt(tail, @min(at, tail.len)); + const cursor = modal.positionAt(whole, @min(at, whole.len)); t.cur_row = @intCast(cursor.row); t.cur_col = @intCast(cursor.col); t.cur_pinned = true; // Reveal the suffix together with its commands, not merely the // first command letter at the viewport's far right edge. if (p.paneIdOf(pane)) |id| { - const tag = tagText(p, p.scratch.allocator(), pane) catch return; - const line = tag[0 .. std.mem.indexOfScalar(u8, tag, '\n') orelse tag.len]; + const line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len]; const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET); pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width); } @@ -181,9 +192,9 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { // The text may have changed since (a 9P write, a rename), so the // remembered cursor is pulled back inside it. It is the cursor that // is remembered, not a selection. - const rows = modal.cursorLineCount(tail); + const rows = modal.cursorLineCount(whole); t.cur_row = @intCast(@min(@as(usize, @intCast(@max(0, t.cur_row))), rows - 1)); - const line = modal.lineSlice(tail, @intCast(t.cur_row)); + const line = modal.lineSlice(whole, @intCast(t.cur_row)); t.cur_col = @intCast(modal.graphemeStart(line, @min(line.len, @as(usize, @intCast(@max(0, t.cur_col)))))); t.vsel.active = false; t.msel.active = false; @@ -196,15 +207,40 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { pane.focus = .tag; } -/// Where display column `col` of tag line `row` (tag scroll included) -/// lands in the tag's own text, or null when it is on the prefix, which -/// only the first line has. +/// Where display column `col` of the tag's shown row `row` (tag scroll +/// included) puts its cursor: the tag as shown, prefix and all. pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor { const at = row + pane.tag_top; - const width = if (at == 0) panes.File.displayWidth(tagPrefix(p, pane) catch return null) else 0; - if (col < width) return null; - const line = modal.lineSlice(curTail(pane), at); - return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width)) }; + const line = modal.lineSlice(tagText(p, p.scratch.allocator(), pane) catch return null, at); + return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)) }; +} + +/// Whether display column `col` of the tag's shown row `row` is on a name +/// a click drafts: a saving file's path. +pub fn onName(pane: *Pane, row: usize, col: usize) bool { + if (row + pane.tag_top != 0) return false; + const f = pane.file orelse return false; + if (!panes.Output.fileTraits(f.output).saves) return false; + const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path; + return col < panes.File.displayWidth(name); +} + +/// A key typed into a saving file's path, with the tag's cursor there, +/// drafts a new name as a click on it does: the path is computed, so it is +/// never the tag's own text to edit, but a buffer's name can be drafted. +/// True when it did. +pub fn typedIntoName(p: *Pardes, pane: *Pane, key: pardes.Key) bool { + const f = pane.file orelse return false; + if (!panes.Output.fileTraits(f.output).saves or pane.tag.cur_row != 0) return false; + const at: usize = @intCast(@max(0, pane.tag.cur_col)); + const back = key.cp == pardes.Key.backspace or pardes.hit(key, config.insert_backspace_alias); + const forward = key.cp == pardes.Key.delete or pardes.hit(key, config.insert_delete_alias); + const typed = key.text.len > 0 and !key.ctrl and !key.alt; + const inside = if (back) at > 0 and at <= f.path.len else if (forward or typed) at < f.path.len else false; + if (!inside) return false; + draftName(p, pane, panes.File.displayWidth(f.path[0..at])); + edit.handleInsert(p, &pane.input, key); + return true; } /// Start a name draft for a file that can take one, its caret at display |
