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/gui.zig | 17 +++++--- src/pardes.zig | 131 ++++++++------------------------------------------------- src/tty.zig | 6 ++- src/tutor.txt | 5 ++- 4 files changed, 38 insertions(+), 121 deletions(-) (limited to 'src') diff --git a/src/gui.zig b/src/gui.zig index 60d9501f..cc949cd1 100644 --- a/src/gui.zig +++ b/src/gui.zig @@ -1650,12 +1650,13 @@ fn renderFrame(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !void const instances: [*]CellInstance = @ptrCast(@alignCast(vptr)); const layout = fixedCellLayout(g); const cursor_idx: u32 = if (surface.cursor) |cu| @as(u32, cu.y) * surface.cols + cu.x else cells; + const cursor_bar = if (surface.cursor) |cu| cu.bar else false; var row: u16 = 0; while (row < surface.rows) : (row += 1) { var col: u16 = 0; while (col < surface.cols) : (col += 1) { const idx: u32 = @as(u32, row) * surface.cols + col; - emitInstance(g, instances, idx, col, row, layout, win_w, win_h, surface.at(col, row), idx == cursor_idx); + emitInstance(g, instances, idx, col, row, layout, win_w, win_h, surface.at(col, row), idx == cursor_idx, cursor_bar); } } c.SDL_UnmapGPUTransferBuffer(g.device, g.vxfer.?); @@ -1701,13 +1702,15 @@ fn emitInstance( win_h: f32, cell: *const pardes.Cell, is_cursor: bool, + cursor_bar: bool, ) void { // resolve the cell style to concrete fg/bg, matching the prototype's // cellKey: reverse swaps, invisible hides, dim darkens. The cursor cell - // renders as reverse (cancelling an already-reversed cell). + // renders as reverse (cancelling an already-reversed cell); an insert + // cursor keeps the cell colors and draws a bar glyph instead. var fg = fg_default; var bg = bg_default; - var reverse = is_cursor; + var reverse = is_cursor and !cursor_bar; if (!cell.default) { const st = cell.style; fg = switch (st.fg) { @@ -1737,7 +1740,10 @@ fn emitInstance( const y1 = 1.0 - ((py0 + layout.h) / win_h) * 2.0; const grapheme = cell.grapheme(); - const cp: u32 = if (cell.default or grapheme.len == 0) ' ' else firstCp(grapheme); + var cp: u32 = if (cell.default or grapheme.len == 0) ' ' else firstCp(grapheme); + // ponytail: the insert bar is the left-eighth block glyph over the cell + // (hides the char under it); a dedicated quad if that ever matters + if (is_cursor and cursor_bar) cp = 0x258F; const slot = if (cp == ' ') g.space_slot else ensureGlyph(g, cp); const aw_f: f32 = @floatFromInt(atlas_w); const ah_f: f32 = @floatFromInt(atlas_h); @@ -1944,12 +1950,13 @@ fn renderFrameGl(g: *Gui, gpa: std.mem.Allocator, surface: *pardes.Surface) !voi const win_h: f32 = @floatFromInt(sh); const layout = cellLayout(g, surface.cols, surface.rows, sw, sh); const cursor_idx: u32 = if (surface.cursor) |cu| @as(u32, cu.y) * surface.cols + cu.x else cells; + const cursor_bar = if (surface.cursor) |cu| cu.bar else false; var row: u16 = 0; while (row < surface.rows) : (row += 1) { var col: u16 = 0; while (col < surface.cols) : (col += 1) { const idx: u32 = @as(u32, row) * surface.cols + col; - emitInstance(g, g.gl_instances.ptr, idx, col, row, layout, win_w, win_h, surface.at(col, row), idx == cursor_idx); + emitInstance(g, g.gl_instances.ptr, idx, col, row, layout, win_w, win_h, surface.at(col, row), idx == cursor_idx, cursor_bar); } } if (g.atlas_dirty) uploadAtlasGl(g); 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 }; } } diff --git a/src/tty.zig b/src/tty.zig index f503303a..ed97fd2a 100644 --- a/src/tty.zig +++ b/src/tty.zig @@ -260,7 +260,11 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { h.draw(child, .{ .scale = .contain }) catch {}; } } - if (surface.cursor) |cur| win.showCursor(cur.x, cur.y); + if (surface.cursor) |cur| { + win.showCursor(cur.x, cur.y); + // insert = beam, everything else = the terminal's default shape + win.setCursorShape(if (cur.bar) .beam else .default); + } const tz_render = tracy.zone(@src(), "vx.render"); try vx.render(tty.writer()); tz_render.end(); diff --git a/src/tutor.txt b/src/tutor.txt index 94a2a20e..4c162b97 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -73,7 +73,10 @@ THE TAG: each pane has a one-line tag: its MODE (NOR/INS/TTY) + directory or file path + builtins. File panes show "Save Del" by default; Save - writes the current file to disk, Del closes the window. Terminal and + writes the current file to disk, Del closes the window. Clicking a tag + edits it, ALWAYS in insert mode (a one-line tag has no use for normal + mode): type straight in, Enter looks / Tab executes the word at the + cursor, Esc hands focus back to the body. Terminal and image panes show "Del". "Delcol" still exists as a command: type it in a tag or body and execute it to close the whole column. A SEPARATE bar across the top of the screen holds the window-agnostic builtins: -- cgit v1.3