summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 09:16:36 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commita9de2b51ab76a43250a4d6c41a0ddd54e6970049 (patch)
treea2138ead27f434026c224fd8b926f66b7fd7fe0c /src
parent0d04bc9214cccce42d74862b31376d49bdda3956 (diff)
downloadpardes-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')
-rw-r--r--src/Text.zig4
-rw-r--r--src/body_layer.zig16
-rw-r--r--src/edit.zig30
-rw-r--r--src/mouse.zig21
-rw-r--r--src/ninep/pane.zig7
-rw-r--r--src/panes.zig5
-rw-r--r--src/pardes.zig35
-rw-r--r--src/tagline.zig90
8 files changed, 153 insertions, 55 deletions
diff --git a/src/Text.zig b/src/Text.zig
index 61f238ae..43dd8a95 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -74,6 +74,10 @@ ed_undo: [Terminal.history_max]Terminal.Snapshot = undefined,
ed_undo_len: usize = 0,
ed_redo: [Terminal.history_max]Terminal.Snapshot = undefined,
ed_redo_len: usize = 0,
+/// Set when an edit would have changed characters this text does not own
+/// (a tag's computed prefix) and was refused; the key that tried it puts
+/// the cursor back.
+refused: bool = false,
/// The window this text belongs to (acme's t->w). A column's or the
/// workspace's tag belongs to none, and the optional makes every caller
diff --git a/src/body_layer.zig b/src/body_layer.zig
index ff1f903c..f9e26b10 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -925,8 +925,7 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
.{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) }
else if (pane.focus == .tag and pane.prompt == .none and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: {
const row: usize = @intCast(pane.tag.cur_row - top);
- const lead: usize = if (row + top == 0) prefix_len else 0;
- break :caret .{ .row = row, .at = @min(lines[row].len, lead + @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
+ break :caret .{ .row = row, .at = @min(lines[row].len, @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
} else null;
if (caret) |at| {
const line = lines[at.row];
@@ -968,19 +967,18 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
// time. A range of one grapheme is the block cursor, not a selection.
if (pane.focus == .tag) {
var ranges: [Text.max_selections]modal.Selection = undefined;
- const got = pane.tag.ranges(tail, 0, &ranges);
+ const got = pane.tag.ranges(whole, 0, &ranges);
for (ranges[0..got.n]) |range| {
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
- if (modal.nextGrapheme(tail, lo) >= hi) continue;
- const first = modal.positionAt(tail, lo);
- const last = modal.positionAt(tail, hi);
+ if (modal.nextGrapheme(whole, lo) >= hi) continue;
+ const first = modal.positionAt(whole, lo);
+ const last = modal.positionAt(whole, hi);
var row = @max(first.row, top);
while (row <= last.row and row < top + rows) : (row += 1) {
const line = lines[row - top];
- const lead: usize = if (row == 0) prefix_len else 0;
- const from = lead + if (row == first.row) first.col else 0;
- const to = @min(line.len, lead + if (row == last.row) last.col else modal.lineSlice(tail, row).len);
+ const from = if (row == first.row) first.col else 0;
+ const to = @min(line.len, if (row == last.row) last.col else line.len);
var col = panes.File.rawDisplayCol(line, from) -| pane.tag_scroll;
const end = panes.File.rawDisplayCol(line, to);
const y = tag_y + @as(u16, @intCast(row - top));
diff --git a/src/edit.zig b/src/edit.zig
index e21352e1..b2d71ed2 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -112,8 +112,12 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
/// into, after the cursor's cell in normal mode, as a body's paste is.
fn pasteInto(p: *Pardes, t: *Text, bytes: []const u8) void {
if (bytes.len == 0) return;
- if (t.mode == .insert) return insertKey(p, t, .{ .cp = bytes[0], .text = bytes });
- pasteText(p, t, bytes, false, 1);
+ const typed: pardes.Key = .{ .cp = bytes[0], .text = bytes };
+ if (t.what == .tag and t.mode == .insert) if (t.pane()) |pane| if (tagline.typedIntoName(p, pane, typed)) return;
+ const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel };
+ t.refused = false;
+ if (t.mode == .insert) insertKey(p, t, typed) else pasteText(p, t, bytes, false, 1);
+ if (t.refused) t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel = kept;
}
pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void {
@@ -358,7 +362,8 @@ pub fn pinPaneCursor(p: *Pardes, t: *Text) void {
pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 {
switch (t.what) {
.body => {},
- .tag => return if (t.pane()) |pane| tagline.curTail(pane) else "",
+ // the tag as it is shown: the keyboard reaches its prefix too
+ .tag => return if (t.pane()) |pane| try tagline.tagText(p, p.scratch.allocator(), pane) else "",
.input => return t.own orelse "",
.column, .workspace => return t.own orelse tagline.headerDefault(p, t.what),
}
@@ -384,6 +389,10 @@ pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize {
fn editText(p: *Pardes, t: *Text, lo: i32, hi: i32, col: i32) ?panes.EditText {
// A tag or header nobody has changed shows its default; the first edit
// makes that default its own text.
+ if (t.what == .tag) {
+ const pane = t.pane() orelse return null;
+ return .{ .text = tagline.tagText(p, p.scratch.allocator(), pane) catch return null, .row0 = 0 };
+ }
if (t.what != .body and t.what != .input and t.own == null)
t.own = p.gpa.dupe(u8, flatSurface(p, t) catch return null) catch return null;
if (t.what != .body) return .{ .text = t.own orelse &.{}, .row0 = 0 };
@@ -403,6 +412,21 @@ fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText {
/// install a rewritten editable text (frees the old one)
pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
+ if (t.what == .tag) {
+ // A tag is edited as it is shown, but only what follows its
+ // prefix is its own: an edit that changed the prefix is refused.
+ defer p.gpa.free(new);
+ const pane = t.pane() orelse return;
+ const prefix = tagline.pathPrefix(p.scratch.allocator(), pane) catch return;
+ if (!std.mem.startsWith(u8, new, prefix)) {
+ t.refused = true;
+ return;
+ }
+ const tail = p.gpa.dupe(u8, new[prefix.len..]) catch return;
+ if (t.own) |old| p.gpa.free(old);
+ t.own = tail;
+ return;
+ }
if (t.what != .body) {
if (t.own) |old| p.gpa.free(old);
t.own = new;
diff --git a/src/mouse.zig b/src/mouse.zig
index 69feea29..beb2e624 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -827,11 +827,14 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
if (sl.r0 < pane.tag_rows) {
// On the tag the caret goes where the button came up. A
// click on the path, if it can be renamed, drafts a name.
- const at = tagline.tagOffsetAt(p, pane, @intCast(@max(0, sl.r1)), @intCast(@max(0, sl.c1)));
- if (at) |cursor| {
+ const row: usize = @intCast(@max(0, sl.r1));
+ const col: usize = @intCast(@max(0, sl.c1));
+ if (!dragged and tagline.onName(pane, row, col)) {
+ tagline.draftName(p, pane, col);
+ } else if (tagline.tagOffsetAt(p, pane, row, col)) |cursor| {
pane.tag.cur_row = @intCast(cursor.row);
pane.tag.cur_col = @intCast(cursor.col);
- } else if (!dragged) tagline.draftName(p, pane, @intCast(@max(0, sl.c1)));
+ }
return;
}
if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {};
@@ -932,7 +935,8 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
};
if (@min(pane.sel[sel_slot].r0, pane.sel[sel_slot].r1) < pane.tag_rows) {
// A sweep over the tag cuts from and pastes into the tag's own text;
- // the part of it over the prefix is not the tag's to cut.
+ // the part of it over the prefix is not the tag's to cut, so the
+ // sweep starts after it.
const t = &pane.tag;
if (!s.chorded) {
s.chorded = true;
@@ -941,9 +945,12 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
const forward = sl.r0 < sl.r1 or (sl.r0 == sl.r1 and sl.c0 <= sl.c1);
const start = if (forward) .{ sl.r0, sl.c0 } else .{ sl.r1, sl.c1 };
const end = if (forward) .{ sl.r1, sl.c1 } else .{ sl.r0, sl.c0 };
- const origin: modal.Cursor = .{ .row = 0, .col = 0 };
- const from = tagline.tagOffsetAt(p, pane, @intCast(@max(0, start[0])), @intCast(@max(0, start[1]))) orelse origin;
- const to = tagline.tagOffsetAt(p, pane, @intCast(@max(0, end[0])), @intCast(@max(0, end[1]))) orelse origin;
+ const lead = pane.tag_lead;
+ const origin: modal.Cursor = .{ .row = 0, .col = lead };
+ var from = tagline.tagOffsetAt(p, pane, @intCast(@max(0, start[0])), @intCast(@max(0, start[1]))) orelse origin;
+ var to = tagline.tagOffsetAt(p, pane, @intCast(@max(0, end[0])), @intCast(@max(0, end[1]))) orelse origin;
+ if (from.row == 0) from.col = @max(from.col, lead);
+ if (to.row == 0) to.col = @max(to.col, lead);
t.cur_row = @intCast(to.row);
t.cur_col = @intCast(to.col);
t.cur_pinned = true;
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 80dd97cd..64355e0e 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -613,8 +613,9 @@ pub fn truncate(p: *Pardes, pane: *Pane, file: PaneFile) tree.Status {
pane.tag.remember(p.gpa, if (pane.tag.own) |own| .{ .text = own } else null);
if (pane.tag.own) |own| p.gpa.free(own);
pane.tag.own = empty;
+ // what is left is the prefix; the cursor goes after it
pane.tag.cur_row = 0;
- pane.tag.cur_col = 0;
+ pane.tag.cur_col = if (tagline.pathPrefix(p.scratch.allocator(), pane)) |prefix| @intCast(prefix.len) else |_| 0;
pane.tag.vsel.active = false;
pane.tag.nsel = 0;
},
@@ -910,9 +911,9 @@ test "truncating the tag clears its editable tail" {
try testing.expectEqualStrings("/test.txt Get", whole);
const word = config.wordBounds(whole, whole.len - "Get".len);
try testing.expectEqualStrings("Get", whole[word.lo..word.hi]);
- // and the keyboard's first column of the text is the G
+ // and a click on the G puts the tag's cursor on it
const at = tagline.tagOffsetAt(p, pane, 0, "/test.txt ".len).?;
- try testing.expectEqual(@as(usize, 0), at.col);
+ try testing.expectEqualStrings("Get", whole[at.col..]);
}
test "name reads the file name and writing it promotes a scratch without touching its body" {
diff --git a/src/panes.zig b/src/panes.zig
index a67aa8ac..ab4f5072 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -133,6 +133,11 @@ pub const Pane = struct {
/// scrolls to keep its caret in view while it has the keyboard; pointer
/// rows count from this line, not the tag's first.
tag_top: u16 = 0,
+ /// How long the tag's computed prefix was when its positions were last
+ /// placed. They count from the start of the prefix, so when it grows or
+ /// shrinks (a rename, the dirty marker, a PDF's page) what follows it
+ /// moves with it.
+ tag_lead: usize = 0,
cwd: Cwd = .none,
hscroll: i32 = 0,
// Visible rows map to source lines and raw/display column origins.
diff --git a/src/pardes.zig b/src/pardes.zig
index 65f3aebb..321a6b3c 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4976,16 +4976,14 @@ pub const Pardes = struct {
if (t.what != .body) {
// An explicit selection, else the word under the cursor in the
// text as it is shown: a tag's prefix is part of what a word is.
- const arena = p.scratch.allocator();
- const own = arena.dupe(u8, edit.flatSurface(p, t) catch return) catch return;
- const shown = if (t.what == .tag) tagline.tagText(p, arena, pane) catch return else own;
- const range = t.primaryRange(own, 0);
+ const shown = p.scratch.allocator().dupe(u8, edit.flatSurface(p, t) catch return) catch return;
+ const range = t.primaryRange(shown, 0);
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
const txt = if ((t.vsel.active and t.vsel.explicit) or t.msel.active)
- own[lo..hi]
+ shown[lo..hi]
else word: {
- const b = config.wordBounds(shown, shown.len - own.len + modal.selectionCursor(own, range));
+ const b = config.wordBounds(shown, modal.selectionCursor(shown, range));
break :word shown[b.lo..b.hi];
};
// Out of the tag or header FIRST: Del frees the pane, Kill
@@ -5013,6 +5011,14 @@ pub const Pardes = struct {
return;
}
if (t.mode == .normal and t.what == .body and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key);
+ if (t.what == .tag and t.mode == .insert and tagline.typedIntoName(p, pane, key)) return;
+ // A tag's prefix is read-only: a key whose edit reached into it
+ // changes nothing, not even where the cursor is.
+ const kept = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel };
+ t.refused = false;
+ defer if (t.refused) {
+ t.cur_row, t.cur_col, t.vsel, t.msel, t.sels, t.nsel = kept;
+ };
switch (t.mode) {
.normal => normal.handleNormal(p, t, key),
.insert => {
@@ -5597,6 +5603,23 @@ pub const Pardes = struct {
const r = p.rects[id];
// The tag takes a row per line, leaving the body one row if it
// had any; a folded pane shows its first line only.
+ // Measured on the stack: this runs on every event, and not
+ // every event may take from the scratch arena.
+ var room: [2 * limits.host_path_cap + 256]u8 = undefined;
+ var measure = std.heap.FixedBufferAllocator.init(&room);
+ const lead = if (tagline.prefixIn(measure.allocator(), pane)) |prefix| prefix.len else |_| pane.tag_lead;
+ if (lead != pane.tag_lead) {
+ const t = &pane.tag;
+ const old: i32 = @intCast(pane.tag_lead);
+ const new: i32 = @intCast(lead);
+ if (t.cur_row == 0 and t.cur_col >= old) t.cur_col += new - old;
+ if (t.vsel.row == 0 and t.vsel.col >= old) t.vsel.col += new - old;
+ for (t.sels[0..t.nsel]) |*sel| {
+ if (sel.row == 0 and sel.col >= old) sel.col += new - old;
+ if (sel.arow == 0 and sel.acol >= old) sel.acol += new - old;
+ }
+ pane.tag_lead = lead;
+ }
const lines = std.mem.count(u8, tagline.curTail(pane), "\n") + 1;
pane.tag_rows = @intCast(@min(lines, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1)));
if (pane.collapsed) pane.tag_rows = BOX_H;
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