From a373fdda0102a99cde988f87f822fc451e0f4391 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 6 Jul 2026 21:18:35 -0300 Subject: insert vs normal cursor shape (all pane types) + the tag is always insert mode. Surface.cursor grew a bar flag: the core sets it from the active pane mode (insert -> bar; normal/tty -> block) for both the modal/file cursor and the vt-tracking terminal cursor, and unconditionally for the tag cursor. tty shell: DECSCUSR via vaxis setCursorShape (beam for insert, the terminal default otherwise). gui shell: the insert cursor draws the left-eighth-block glyph in cell colors instead of the reverse-video block (ponytail: it hides the char under it; a dedicated quad if that ever matters). Tag editing: enterTagEdit goes straight to insert, Esc (or anything that exits the tag) restores the body normal mode; Enter looks / Tab executes the word at the cursor from inside the tag (tagChordText kept); the whole normal-mode tag machinery — handleTagKey motions, v/x selection, d/c/y/p, tagEnterInsert/tagDelete/tagYank/tagPaste — deleted as unreachable (~120 lines; multi-word tag execution stays the mouse middle-drag). tag.snap rewritten: INS on click, type/backspace straight in, Esc exit with the text persisting, Enter-look opens the file. Tutor documents it. Cursor SHAPE is not capturable by the harness (position only) — verify by eye: bar in insert/tags, block elsewhere, both shells. 30/30. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pardes.zig | 131 ++++++++------------------------------------------------- 1 file changed, 17 insertions(+), 114 deletions(-) (limited to 'src/pardes.zig') diff --git a/src/pardes.zig b/src/pardes.zig index 32a58f89..51965bd4 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -217,7 +217,8 @@ pub const Surface = struct { cols: u16 = 0, rows: u16 = 0, cells: []Cell = &.{}, - cursor: ?struct { x: u16, y: u16 } = null, + /// bar: draw an insert-style thin cursor instead of the block + cursor: ?struct { x: u16, y: u16, bar: bool = false } = null, images: [MAX_PANES]?ImagePlace = @splat(null), nimages: usize = 0, @@ -958,7 +959,8 @@ pub const Pardes = struct { } pane.tag_edit = true; pane.tag_sel = false; - pane.mode = .normal; + // a one-line tag has no use for normal mode: always insert + pane.mode = .insert; pane.pending = 0; const cc = std.math.clamp(click_col - @as(i32, @intCast(pfx.len)), 0, @as(i32, @intCast(pane.tag_tail.items.len))); pane.tag_col = @intCast(cc); @@ -968,6 +970,7 @@ pub const Pardes = struct { pane.tag_edit = false; pane.tag_sel = false; pane.search_edit = false; // an abandoned `/` input stays as tag text + pane.mode = .normal; // tags are always insert; leaving one never is } fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } { @@ -998,66 +1001,17 @@ pub const Pardes = struct { /// tag-edit key dispatch: a modal one-line editor over the tail, sharing /// the pane's mode. Newlines are always dropped. - fn handleTagKey(p: *Pardes, pane: *Pane, key: Key) void { - if (pane.mode == .insert) return p.tagInsertKey(pane, key); - const lines = [1][]const u8{pane.tag_tail.items}; - const cur: modal.Cursor = .{ .row = 0, .col = pane.tag_col }; - - if (pane.pending == 'g') { - pane.pending = 0; - const m: modal.Cursor = if (is(key, 'g')) modal.gotoFirst() else if (is(key, 'e')) modal.gotoLast(&lines) else if (is(key, 'h')) modal.lineStart(cur) else if (is(key, 'l')) modal.lineEnd(&lines, cur) else cur; - pane.tag_col = @intCast(m.col); - return; - } - if (is(key, 'g')) { - pane.pending = 'g'; - return; - } - const moved: ?modal.Cursor = - if (is(key, 'h') or is(key, Key.left)) modal.charLeft(cur) else if (is(key, 'l') or is(key, Key.right)) modal.charRight(&lines, cur) else if (is(key, 'w')) modal.nextWordStart(&lines, cur, false) else if (is(key, 'W')) modal.nextWordStart(&lines, cur, true) else if (is(key, 'b')) modal.prevWordStart(&lines, cur, false) else if (is(key, 'B')) modal.prevWordStart(&lines, cur, true) else if (is(key, 'e')) modal.nextWordEnd(&lines, cur, false) else if (is(key, 'E')) modal.nextWordEnd(&lines, cur, true) else if (is(key, '0') or is(key, Key.home)) modal.lineStart(cur) else if (is(key, '$') or is(key, Key.end)) modal.lineEnd(&lines, cur) else if (is(key, '^')) modal.firstNonWsOf(&lines, cur) else null; - if (moved) |m| { - pane.tag_col = @intCast(m.col); - return; - } - if (is(key, 'i')) return tagEnterInsert(pane, pane.tag_col); - if (is(key, 'a')) return tagEnterInsert(pane, @intCast(modal.charRight(&lines, cur).col)); - if (is(key, 'I')) return tagEnterInsert(pane, @intCast(modal.firstNonWsOf(&lines, cur).col)); - if (is(key, 'A')) return tagEnterInsert(pane, @intCast(pane.tag_tail.items.len)); - if (is(key, 'v')) { - pane.tag_sel = !pane.tag_sel; - if (pane.tag_sel) pane.tag_anchor = pane.tag_col; - return; - } - if (is(key, 'x')) { // select the whole one-line tail - const len = pane.tag_tail.items.len; - if (len > 0) { - pane.tag_sel = true; - pane.tag_anchor = 0; - pane.tag_col = @intCast(len - 1); - } - return; - } - if (is(key, 'd')) return p.tagDelete(pane, false); - if (is(key, 'c')) return p.tagDelete(pane, true); - if (is(key, 'y')) return p.tagYank(pane); - if (is(key, 'p')) return p.tagPaste(pane); - // Esc escalates: clear the selection, else hand focus back to the body + fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void { if (is(key, Key.escape)) { - if (pane.tag_sel) pane.tag_sel = false else exitTagEdit(pane); + exitTagEdit(pane); // the tag is ALWAYS insert; Esc leaves it return; } - } - - fn tagEnterInsert(pane: *Pane, col: u16) void { - pane.tag_col = @min(col, @as(u16, @intCast(pane.tag_tail.items.len))); - pane.tag_sel = false; - pane.mode = .insert; - pane.pending = 0; - } - - fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void { - if (is(key, Key.escape)) { - pane.mode = .normal; + // Enter looks / Tab executes the selection or the word at the cursor + if (is(key, Key.enter) or is(key, Key.tab)) { + if (p.tagChordText(pane)) |txt| { + pane.tag_sel = false; + p.actOnSelection(if (is(key, Key.tab)) .middle else .right, p.active, txt, null); + } return; } if (key.text.len > 0) { @@ -1087,50 +1041,6 @@ pub const Pardes = struct { /// d/c: delete the selection (or the char under the cursor), yanking it; /// c then enters insert - fn tagDelete(p: *Pardes, pane: *Pane, change: bool) void { - const tail = pane.tag_tail.items; - var lo: usize = undefined; - var count: usize = undefined; - if (pane.tag_sel) { - const b = tagSelBounds(pane); - lo = b.lo; - count = @min(b.hi + 1, tail.len) - b.lo; - } else { - lo = @min(@as(usize, pane.tag_col), tail.len); - count = if (lo < tail.len) 1 else 0; - } - if (count > 0) { - p.setYank(tail[lo .. lo + count]); - pane.tag_tail.replaceRange(p.gpa, lo, count, "") catch return; - } - pane.tag_col = @intCast(lo); - pane.tag_sel = false; - if (change) pane.mode = .insert; - } - - fn tagYank(p: *Pardes, pane: *Pane) void { - const tail = pane.tag_tail.items; - if (pane.tag_sel) { - const b = tagSelBounds(pane); - const hi = @min(b.hi + 1, tail.len); - p.setYank(tail[b.lo..hi]); - pane.tag_col = @intCast(b.lo); - pane.tag_sel = false; - } else p.setYank(tail); // whole tail, like a line yank - } - - fn tagPaste(p: *Pardes, pane: *Pane) void { - const y = p.yank orelse return; - const col: usize = @min(@as(usize, pane.tag_col), pane.tag_tail.items.len); - var at = col; - for (y) |ch| { - if (ch == '\n' or ch == '\r') continue; - pane.tag_tail.insert(p.gpa, at, ch) catch break; - at += 1; - } - pane.tag_col = @intCast(at); - } - /// tag + '\n' + body: the full selectable pane text (row 0 = the tag) fn paneText(p: *Pardes, pane: *Pane) ![]u8 { const arena = p.scratch.allocator(); @@ -1272,13 +1182,6 @@ pub const Pardes = struct { // (on the tag tail when it's focused for editing, else the body). if (pane.mode == .normal and (is(key, Key.enter) or is(key, Key.tab))) { const btn: Mouse.Button = if (is(key, Key.tab)) .middle else .right; - if (pane.tag_edit) { - if (p.tagChordText(pane)) |txt| { - pane.tag_sel = false; - p.actOnSelection(btn, p.active, txt, null); - } - return; - } pane.pinCursor(); if (p.currentSelText(pane)) |txt| { pane.vsel.active = false; @@ -1295,7 +1198,7 @@ pub const Pardes = struct { // tag editing intercepts every other key: a modal one-line editor // over the tail, sharing the pane's mode. if (pane.tag_edit) { - p.handleTagKey(pane, key); + p.tagInsertKey(pane, key); return; } // normal-mode `p` pastes the SYSTEM clipboard: an async OSC 52 round @@ -3564,7 +3467,7 @@ pub const Pardes = struct { // cursor while editing the tail: prefix length + column within it if (active and pane.tag_edit) { const cx: usize = pfx.len + pane.tag_col; - if (cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y }; + if (cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y, .bar = true }; // tags are always insert } // image pane: the picture fills the body — petscii glyph art into the @@ -3760,9 +3663,9 @@ pub const Pardes = struct { else ccol; if (prow >= 0 and cx >= 0 and prow < r.h and cx < tw) - s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y + @as(u16, @intCast(prow)) }; + s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = r.y + @as(u16, @intCast(prow)), .bar = pane.mode == .insert }; } else if (cur.y + BOX_H < r.h and cur.x < tw) { - s.cursor = .{ .x = tx + cur.x, .y = r.y + cur.y + BOX_H }; + s.cursor = .{ .x = tx + cur.x, .y = r.y + cur.y + BOX_H, .bar = pane.mode == .insert }; } } -- cgit v1.3