summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 22:52:45 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commitf4ac5ee26851ced516b826953fa340008fa7069c (patch)
tree7e2c7cf5cff42f083afb4c30b1582816af60ec18 /src
parent6002422d04f1120ba7b0c5169d6a4ff01c1b5aff (diff)
downloadpardes-f4ac5ee26851ced516b826953fa340008fa7069c.tar.gz
pardes-f4ac5ee26851ced516b826953fa340008fa7069c.zip
the mode is a character in the layout box, not a word in the tagline
Diffstat (limited to 'src')
-rw-r--r--src/config.zig30
-rw-r--r--src/pardes.zig80
-rw-r--r--src/tutor.txt19
3 files changed, 94 insertions, 35 deletions
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.)