From f4ac5ee26851ced516b826953fa340008fa7069c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 22:52:45 -0300 Subject: the mode is a character in the layout box, not a word in the tagline --- src/config.zig | 30 +++++++++++++++++++--- src/pardes.zig | 80 ++++++++++++++++++++++++++++++++++++++++++---------------- src/tutor.txt | 19 +++++++------- 3 files changed, 94 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/config.zig b/src/config.zig index 0dc12f98..46bd261e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -290,10 +290,32 @@ pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep Help"; pub const pane_builtins_str = "Del"; pub const file_pane_builtins_str = "Save Del"; -/// the tag prefix's mode word — live chrome, not text you own -pub const tag_normal = "NOR"; -pub const tag_insert = "INS"; -pub const tag_tty = "TTY"; +/// the pane's mode, as ONE character in the layout box at its top-left — live +/// chrome, not text you own. It used to be a three-letter word leading every +/// tagline; the box was already there carrying no information at all, so the +/// mode moved into it and the taglines got their four columns back. +/// +/// These are NOT the initials. A badge you read at a glance every time your +/// eye crosses a pane should LOOK like what it means, and each of these is a +/// mark that already means its mode somewhere else: `^` is the proofreader's +/// caret, the mark that says text goes in HERE; `$` is the shell prompt, and a +/// pane wearing it has the keyboard wired straight to the program on the other +/// end; `•` is the pane at rest, a full stop, nothing waiting to eat what you +/// type. (The caret's true form is `‸` U+2038 and the ASCII `^` is only its +/// stand-in — but `^` is in every font ever made and `‸` is in about four, and +/// a mode badge that renders blank on someone's terminal is worse than one +/// spelled with the near-miss.) +/// +/// ONE CODEPOINT each. The box prints a single cell, so a two-character string +/// here would be pushed into one cell as a single grapheme and come out wrong. +/// A font missing the glyph draws a blank box, which is exactly what the box +/// drew before there was anything in it. +pub const tag_normal = "•"; +pub const tag_insert = "^"; +pub const tag_tty = "$"; +/// ...and `img` stays a WORD at the head of an image pane's tagline, because +/// it is not a mode: it says what the pane IS, which no amount of watching the +/// box will tell you. The box on an image pane still shows its mode. pub const tag_image = "img"; /// on a focused tag: yank what the chord would run (the selection, else the diff --git a/src/pardes.zig b/src/pardes.zig index 4dd35758..b303acbb 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1456,21 +1456,21 @@ pub const Pardes = struct { // ---- tag + selection text (chord sources) ---- - /// the live tag prefix: mode + cwd/path (an image has only its path — the - /// renderer toggles it used to spell out are builtins now, under SPC t) + /// the live tag prefix: the pane's cwd/path, and nothing else (an image + /// still names its kind — the renderer toggles it used to spell out are + /// builtins now, under SPC t). The mode used to lead this as a word; it is + /// the one character in the layout box now (renderPane), so every tagline + /// starts four columns further left and spends them on the path instead. + /// + /// Arena-allocated like everything the renderer is handed — the tag is + /// retained through the frame and a cwd can be rewritten under us by the + /// next shell report, so this owns its bytes rather than lending the + /// pane's. fn tagPrefix(p: *Pardes, pane: *Pane) ![]u8 { const arena = p.scratch.allocator(); if (pane.image) |iv| return std.fmt.allocPrint(arena, config.tag_image ++ " {s}", .{iv.path}); - if (pane.file) |f| { - const fmode = if (pane.mode == .insert) config.tag_insert else config.tag_normal; - return std.fmt.allocPrint(arena, "{s} {s}", .{ fmode, f.path }); - } - const mode = switch (pane.mode) { - .normal => config.tag_normal, - .insert => config.tag_insert, - .tty => config.tag_tty, - }; - return std.fmt.allocPrint(arena, "{s} {s}", .{ mode, pane.cwdSlice() }); + if (pane.file) |f| return arena.dupe(u8, f.path); + return arena.dupe(u8, pane.cwdSlice()); } /// the editable tail: the user's edited buffer once touched, else defaults @@ -7016,7 +7016,44 @@ pub const Pardes = struct { s.clearRect(tx, r.y, tw, r.h); if (th.bg) |bg| s.fill(tx, r.y, tw, r.h, .{ .bg = .{ .rgb = bg } }); - // tag: live prefix (mode + cwd/path) + editable tail, on a dim bar + // the layout box: the pane's MODE, one character, in the gutter cells + // of the tag row. Same box you drag a pane by — the whole GUTTER is + // still painted and the `.move` press hit-test in handleMouse is + // untouched — it just carries the one piece of state that used to cost + // four columns of every tagline. + // The glyph goes in column 0, directly above the scrollbar's ink + // column below it, so a pane's chrome reads as one line down its left + // edge; column 1 stays plain colour, and that blank half is what keeps + // the thing reading as a BOX rather than as a letter someone dropped + // in the gutter. + // + // The mode shown is the BODY's. A tag edit hijacks pane.mode to insert + // (tags are always insert; the real one is parked in tag_mode), and a + // badge that flipped every time you clicked a tagline would be + // reporting the tag's mode on the pane's box. Reading tag_mode also + // makes the parked value VISIBLE: a terminal being tag-edited still + // shows `$`, which is exactly the invariant ttyclick.snap exists for. + // + // The ink is picked off the box's own brightness instead of being + // named in the theme, because box/box_dim come from a generated + // theme's cursor and selection colours — light on a light theme — and + // a fixed white would vanish there. Rec.601-ish integer weights. + // ponytail: two colours, black or white, chosen at a fixed threshold. + // Upgrade to the theme's own fg/bg pair when a theme turns up whose + // box wants a tint rather than a contrast. + const box_bg = if (active) th.box else th.box_dim; + const box_lum = (@as(u16, box_bg[0]) * 3 + @as(u16, box_bg[1]) * 6 + @as(u16, box_bg[2])) / 10; + const box_ink: [3]u8 = if (box_lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff }; + const box_style: CellStyle = .{ .fg = .{ .rgb = box_ink }, .bg = .{ .rgb = box_bg } }; + s.fill(r.x, r.y, config.GUTTER, BOX_H, box_style); + const box_mode = if (pane.tag_edit) pane.tag_mode else pane.mode; + s.set(r.x, r.y, switch (box_mode) { + .normal => config.tag_normal, + .insert => config.tag_insert, + .tty => config.tag_tty, + }, box_style); + + // tag: live prefix (cwd/path) + editable tail, on a dim bar const tag_style: CellStyle = .{ .fg = .{ .rgb = th.tag_fg }, .bg = .{ .rgb = th.tag_bg } }; s.fill(tx, r.y, tw, BOX_H, .{ .bg = .{ .rgb = th.tag_bg } }); const tag = try p.tagText(arena, pane); @@ -7062,8 +7099,6 @@ pub const Pardes = struct { // thumbless gutter so it reads like any other pane. if (pane.image != null) { p.drawImage(pane, r, tx, tw); - const box_bg2 = if (active) th.box else th.box_dim; - s.fill(r.x, r.y, config.GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg2 } }); // thumbless, but the same one column as the real scrollbar below — // that is the whole point of drawing it s.fill(r.x, r.y + BOX_H, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = th.scroll_track } }); @@ -7215,7 +7250,9 @@ pub const Pardes = struct { } } - // gutter: move box on top, scrollbar track + thumb below. + // gutter below the tag row: scrollbar track + thumb. (The move box on + // the tag row itself is drawn up with the tag, since it now carries + // the mode and belongs with the rest of that row.) // // The bar is ONE column: column 1 is the track/thumb, column 2 is the // pane's own background. That second fill is not optional — render() @@ -7223,16 +7260,15 @@ pub const Pardes = struct { // panes read as chrome, so a column left unpainted here keeps the // track colour and the bar looks two wide again. // - // The move box above stays the full GUTTER. It is a target you drag, - // not a gauge you read, and its whole width is the affordance — the - // step where the box ends and the narrower bar begins is the one place - // on screen that says those are two different pieces of chrome. + // The move box above stays the full GUTTER even though only half of it + // has ink in it. It is a target you drag, not a gauge you read, and its + // whole width is the affordance — the step where the box ends and the + // narrower bar begins is the one place on screen that says those are + // two different pieces of chrome. // // Nothing here touches layout or hit-testing: the gutter is still // config.GUTTER columns and the click handlers still scroll on any of // them, so the blank column is still live. Only the ink narrowed. - const box_bg = if (active) th.box else th.box_dim; - s.fill(r.x, r.y, config.GUTTER, BOX_H, .{ .bg = .{ .rgb = box_bg } }); if (r.h > BOX_H) { s.fill(r.x, r.y + BOX_H, 1, r.h - BOX_H, .{ .bg = .{ .rgb = th.scroll_track } }); s.fill(r.x + 1, r.y + BOX_H, 1, r.h - BOX_H, .{ .bg = pane_bg }); diff --git a/src/tutor.txt b/src/tutor.txt index ee6f9f79..71a8fabb 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -17,10 +17,10 @@ be live terminals (tty mode); most are text you can move a cursor over. Modal editing (helix-style) is layered on top of that. - Three modes (the tag shows which): - NOR NORMAL block cursor, keys move/edit. DEFAULT on startup. - INS INSERT keys type text at the cursor. - TTY TTY keys go straight to the shell. (terminals only) + Three modes (the BOX at the pane's top-left corner shows which): + • NORMAL block cursor, keys move/edit. DEFAULT on startup. + ^ INSERT keys type text at the cursor. + $ TTY keys go straight to the shell. (terminals only) This tutor is in THREE parts, ordered by what's most different from editors you may know: @@ -108,8 +108,9 @@ The selection a chord leaves behind is dismissed by the next left click (it doesn't stretch to the click). - 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 + THE TAG: each pane has a one-line tag: its directory or file path + + builtins. (The mode is the box at the tag's left end, not a word in the + tag.) File panes show "Save Del" by default; Save writes the current file to disk, Del closes the window. Clicking a tag edits it in insert mode: type straight in, Enter looks / Tab executes the word at the cursor, Esc hands focus back to the body. `:` from the @@ -217,11 +218,11 @@ prompt model rather than layering an editor cursor on top. On a terminal pane: - NORMAL (NOR) prompt rows are HIDDEN — a clean acme-style page. You + NORMAL (•) prompt rows are HIDDEN — a clean acme-style page. You navigate it with the same h/j/k/l/w/b/e as a file. - INSERT (INS) same clean page; keys type an insertion overlay (the + INSERT (^) same clean page; keys type an insertion overlay (the command you're composing). Prompts still hidden. - TTY (TTY) the REAL shell — prompts + typed input shown, and keys + TTY ($) the REAL shell — prompts + typed input shown, and keys go straight to the pty as terminal input. ESC: insert -> normal. (A plain Esc never reaches tty.) -- cgit v1.3