summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-01 11:15:30 -0300
committerGabriel Schneider <[email protected]>2026-07-01 11:15:56 -0300
commitc301ae3567204666c914ba4b36336d3a91af0cd1 (patch)
tree35313e47977ac4e0e6ff8ffaaf1caa0aa3ed9de7
parent6570dc04d6343cbbf31c0eb2363f9bec817aa4db (diff)
downloadpardes-c301ae3567204666c914ba4b36336d3a91af0cd1.tar.gz
pardes-c301ae3567204666c914ba4b36336d3a91af0cd1.zip
changed some draggable styling
-rw-r--r--main.zig116
-rw-r--r--tests.zig25
-rw-r--r--tutor.txt9
3 files changed, 99 insertions, 51 deletions
diff --git a/main.zig b/main.zig
index 3bb0cfda..67904722 100644
--- a/main.zig
+++ b/main.zig
@@ -158,6 +158,10 @@ const msel_fg = [3]u8{ 0xd8, 0xc8, 0xa8 };
// are skipped so there's no per-frame cost. Toggled by the "Colors" tagbar command.
var colors_on: bool = true;
+// The stats overlay, toggled by the "Debug" builtin. Global (like colors_on) so the
+// central command dispatcher can flip it; the render reads it.
+var show_debug: bool = false;
+
// Color themes. Chrome (scrollbar/box/selection/edit colors above) stays fixed;
// only the page background, text, tag bar and syntax colors swap. `bg`/`fg` are
// null in the dark theme so it keeps the terminal's own default colors (i.e. looks
@@ -244,24 +248,27 @@ fn synStyle(s: Syn) ?struct { fg: [3]u8, bold: bool } {
};
}
-// Builtins. Clicking the name runs the action. Window-specific ones (Del, Delcol)
-// live in each pane's tag; window-agnostic ones (Kill, Newcol, Tutor) live in the
-// single global tagbar at the top. onSelect matches against the whole list, so a
-// name works wherever it's shown.
-const SelectAction = enum { none, send, newcol, delcol, del, tutor, kill };
+// Builtins. Executing the name (middle-click / Tab) runs the action through the ONE
+// dispatcher (onSelect + actOnSelection), no matter where the name is shown — a pane
+// body, a file, the tagbar. onSelect matches against the whole list, so a name works
+// wherever it appears. Window-specific ones (Del, Delcol) live in each pane's tag;
+// window-agnostic ones (Kill, Newcol, Tutor, Debug, Colors, NextColor) in the tagbar.
+const SelectAction = enum { none, send, newcol, delcol, del, tutor, kill, debug, colors, nextcolor };
const Builtin = struct { name: []const u8, action: SelectAction };
const builtins = [_]Builtin{
.{ .name = "Kill", .action = .kill },
.{ .name = "Newcol", .action = .newcol },
.{ .name = "Tutor", .action = .tutor },
+ .{ .name = "Debug", .action = .debug },
+ .{ .name = "Colors", .action = .colors },
+ .{ .name = "NextColor", .action = .nextcolor },
.{ .name = "Del", .action = .del },
.{ .name = "Delcol", .action = .delcol },
};
// per-pane tag commands (window-specific).
const pane_builtins_str = "Del Delcol";
-// the global tagbar: window-agnostic commands. Kill closes everything; Debug
-// toggles the stats overlay, Colors the ansi/syntax recolor, NextColor cycles the
-// theme (all handled in the tagbar click, not a SelectAction).
+// the global tagbar: window-agnostic commands. Kill closes everything; Debug toggles
+// the stats overlay, Colors the ansi/syntax recolor, NextColor cycles the theme.
const topbar_str = "Kill Newcol Tutor Debug Colors NextColor";
// Path of the generated bash rc that adds OSC 133 semantic prompts (shell
@@ -835,6 +842,16 @@ fn ttyForDir(
return nt;
}
+// Overlay a resize-handle dash on a cell WITHOUT touching its colors: read the cell,
+// swap only the grapheme, write it back. So the boundary hint reads as a dashed line
+// over whatever content is already there — the only UI change is the character.
+fn overlayDash(win: vaxis.Window, col: u16, row: u16, glyph: []const u8) void {
+ var cell = win.readCell(col, row) orelse return;
+ cell.char.grapheme = glyph; // static literal — safe under vaxis's retained render
+ cell.default = false; // make it draw with the kept style, not as a blank cell
+ win.writeCell(col, row, cell);
+}
+
// Tile the screen: columns by col_weight, then each column's windows by vweight.
// Last column/window absorbs rounding so the tiling always fills the screen.
// `out_*` written for every live term and live column.
@@ -851,13 +868,15 @@ fn computeGeom(
var wsum: f32 = 0;
for (0..l.ncol) |c| wsum += l.col_weight[c];
if (wsum <= 0) wsum = 1;
- const sep_cols: u16 = @intCast(l.ncol - 1);
- const avail_w: u16 = sw -| sep_cols;
+ // Panes tile edge to edge — NO reserved inter-pane gap. The cells that used to be
+ // an empty drag strip are now usable pane content; the resize handle is the shared
+ // boundary itself (a pane's right/bottom edge), surfaced as a dashed line on hover.
+ // Only the top row is held back, for the global tagbar.
var x: u16 = 0;
for (0..l.ncol) |c| {
const last = (c + 1 == l.ncol);
- const cw: u16 = if (last) (sw -| x) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(avail_w)) * l.col_weight[c] / wsum))));
+ const cw: u16 = if (last) (sw -| x) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(sw)) * l.col_weight[c] / wsum))));
col_x[c] = x;
col_w[c] = cw;
@@ -867,9 +886,7 @@ fn computeGeom(
if (terms[l.col_terms[c][k]]) |t| vsum += t.vweight;
}
if (vsum <= 0) vsum = 1;
- const sep_rows: u16 = if (n > 0) @intCast(n - 1) else 0;
- // reserve the top row for the global tagbar: panes tile [TOPBAR_H, sh).
- const avail_h: u16 = (sh -| TOPBAR_H) -| sep_rows;
+ const avail_h: u16 = sh -| TOPBAR_H;
var y: u16 = TOPBAR_H;
for (0..n) |k| {
@@ -878,9 +895,9 @@ fn computeGeom(
const lastk = (k + 1 == n);
const ch: u16 = if (lastk) (sh -| y) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(avail_h)) * t.vweight / vsum))));
rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch };
- y +|= ch + 1;
+ y +|= ch;
}
- x +|= cw + 1;
+ x +|= cw;
}
}
@@ -2315,6 +2332,9 @@ fn actOnSelection(
}
switch (onSelect(button, txt)) {
.none => {},
+ .debug => show_debug = !show_debug,
+ .colors => colors_on = !colors_on,
+ .nextcolor => theme_idx = (theme_idx + 1) % themes.len,
.kill => return true, // close everything: signal the main loop to quit
.send => {
// terminal: run in itself. file pane: run in a shell sitting in the
@@ -2527,7 +2547,6 @@ pub fn main(init: std.process.Init) !void {
var hover_col: u16 = 0; // last mouse position; used to tint the resize border under it
var hover_row: u16 = 0;
var ctrl_w_pending: bool = false; // Ctrl-w prefix for helix-style pane focus
- var show_debug: bool = false; // the stats overlay; toggled by the Debug tagbar command
while (true) {
const event = try loop.nextEvent();
@@ -2556,29 +2575,26 @@ pub fn main(init: std.process.Init) !void {
// action context; Kill/Newcol/Tutor ignore it.
if (mouse.button == .middle) {
const word = wordAtCol(topbar_str, mcol);
- if (std.mem.eql(u8, word, "Debug")) {
- show_debug = !show_debug;
- } else if (std.mem.eql(u8, word, "Colors")) {
- colors_on = !colors_on;
- } else if (std.mem.eql(u8, word, "NextColor")) {
- theme_idx = (theme_idx + 1) % themes.len;
- } else if (word.len > 0) {
+ if (word.len > 0)
if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, vx.caps.kitty_graphics, .middle, active, word)) break;
- }
}
} else if (mouse.type == .press and mouse.button == .left) {
var handled = false;
- // vertical separator (between columns)?
+ // vertical resize handle = the LEFT pane's last column (its own
+ // content, not the right pane's gutter). The border check runs before
+ // the pane hit test, so a press there resizes instead of clicking.
var c: usize = 0;
while (c + 1 < layout.ncol and !handled) : (c += 1) {
- if (mcol == col_x[c] + col_w[c]) {
+ if (mcol == col_x[c] + col_w[c] -| 1) {
drag = .{ .border_v = .{ .left_col = c, .cur_x = mcol } };
handled = true;
}
}
- // horizontal separator (between stacked windows)?
+ // horizontal resize handle = the UPPER pane's last row (a's own last
+ // line, sitting right above b's tag) — not b's tag row, so the tag +
+ // its box-drag stay clean.
if (!handled) {
var cc: usize = 0;
outer: while (cc < layout.ncol) : (cc += 1) {
@@ -2587,7 +2603,7 @@ pub fn main(init: std.process.Init) !void {
while (k + 1 < layout.col_n[cc]) : (k += 1) {
const id = layout.col_terms[cc][k];
const r = rects[id];
- if (mrow == r.y + r.h) {
+ if (mrow == r.y + r.h -| 1) {
drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } };
handled = true;
break :outer;
@@ -2684,7 +2700,10 @@ pub fn main(init: std.process.Init) !void {
const c = d.left_col;
if (c + 1 < layout.ncol) {
const combined: f32 = @floatFromInt(col_w[c] + col_w[c + 1]);
- var nl: f32 = @floatFromInt(d.cur_x -| col_x[c]);
+ // handle sits on the left pane's LAST column (inclusive),
+ // so the new left width includes it: cur_x - col_x + 1.
+ // A click with no drag then leaves the width unchanged.
+ var nl: f32 = @floatFromInt((d.cur_x + 1) -| col_x[c]);
const minw: f32 = MINW;
nl = std.math.clamp(nl, minw, @max(minw, combined - minw));
const pair = layout.col_weight[c] + layout.col_weight[c + 1];
@@ -2700,7 +2719,9 @@ pub fn main(init: std.process.Init) !void {
const b = layout.col_terms[c][k + 1];
if (terms[a]) |ta| if (terms[b]) |tb| {
const combined: f32 = @floatFromInt(rects[a].h + rects[b].h);
- var nt: f32 = @floatFromInt(d.cur_y -| rects[a].y);
+ // handle is the upper pane's LAST row (inclusive) ->
+ // new upper height includes it: cur_y - y + 1.
+ var nt: f32 = @floatFromInt((d.cur_y + 1) -| rects[a].y);
const minh: f32 = MINH;
nt = std.math.clamp(nt, minh, @max(minh, combined - minh));
const pair = ta.vweight + tb.vweight;
@@ -2979,22 +3000,23 @@ pub fn main(init: std.process.Init) !void {
bar.fill(.{ .char = .{ .grapheme = " " }, .style = .{ .bg = rgbColor(T().tag_bg) } });
_ = bar.printSegment(.{ .text = topbar_str, .style = .{ .fg = rgbColor(T().tag_fg), .bg = rgbColor(T().tag_bg) } }, .{ .wrap = .none });
- // No static separators: each pane's tagline (top) and scrollbar gutter
- // (left) already read as borders. The 1-cell gap between panes stays as
- // an (invisible) drag zone; the dashed preview below shows it on drag.
+ // No static separators and no reserved gap: each pane's tagline (top) and
+ // scrollbar gutter (left) read as the borders, and adjacent panes abut. The
+ // resize handle is a pane's own trailing edge (left pane's last col / upper
+ // pane's last row) — hinted by a dash overlay that keeps the cell's colors.
- // dashed preview while dragging; commit happens on release.
+ // resize-handle hint: overlay a dash on the boundary cells, keeping their
+ // colors (only the glyph changes). Shown while dragging a border and, in the
+ // else branch, on hover.
switch (drag) {
.border_v => |d| {
var ry: u16 = TOPBAR_H;
- while (ry < screen_h) : (ry += 1)
- win.writeCell(d.cur_x, ry, .{ .char = .{ .grapheme = "╎" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
+ while (ry < screen_h) : (ry += 1) overlayDash(win, d.cur_x, ry, "╎");
},
.border_h => |d| {
const c = d.col;
var rx: u16 = col_x[c];
- while (rx < col_x[c] + col_w[c]) : (rx += 1)
- win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
+ while (rx < col_x[c] + col_w[c]) : (rx += 1) overlayDash(win, rx, d.cur_y, "╌");
},
.move => |d| {
// insertion indicator: dashed line at cur_y across the target column
@@ -3011,15 +3033,14 @@ pub fn main(init: std.process.Init) !void {
win.writeCell(d.cur_x, d.cur_y, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = gray.mid } } });
},
else => {
- // not dragging: preview the resize border under the cursor with the
- // same dashed line the drag shows, drawn on top (no background tint),
- // so the invisible drag zones are discoverable on hover.
+ // not dragging: hint the resize handle under the cursor with the same
+ // dash overlay. Vertical = the left pane's last column; horizontal =
+ // the upper pane's last row (a's own last line, just above b's tag).
var c: usize = 0;
while (c + 1 < layout.ncol) : (c += 1) {
- if (hover_col != col_x[c] + col_w[c]) continue;
+ if (hover_col != col_x[c] + col_w[c] -| 1) continue;
var ry: u16 = TOPBAR_H;
- while (ry < screen_h) : (ry += 1)
- win.writeCell(hover_col, ry, .{ .char = .{ .grapheme = "╎" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
+ while (ry < screen_h) : (ry += 1) overlayDash(win, hover_col, ry, "╎");
}
var cc: usize = 0;
while (cc < layout.ncol) : (cc += 1) {
@@ -3027,10 +3048,9 @@ pub fn main(init: std.process.Init) !void {
var k: usize = 0;
while (k + 1 < layout.col_n[cc]) : (k += 1) {
const r = rects[layout.col_terms[cc][k]];
- if (hover_row != r.y + r.h) continue;
+ if (hover_row != r.y + r.h -| 1) continue;
var rx: u16 = col_x[cc];
- while (rx < col_x[cc] + col_w[cc]) : (rx += 1)
- win.writeCell(rx, hover_row, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = gray.lit } } });
+ while (rx < col_x[cc] + col_w[cc]) : (rx += 1) overlayDash(win, rx, hover_row, "╌");
}
}
},
diff --git a/tests.zig b/tests.zig
index 1b4d6c24..9bb97f22 100644
--- a/tests.zig
+++ b/tests.zig
@@ -56,6 +56,14 @@ pub fn main(init: std.process.Init) !void {
try hs.resize(30, 100);
try hs.expectWaitFor("resizes=2", 8000, "SIGWINCH was not turned into a resize (counter stuck at 1)");
+ // 2b. RECLAIMED GAP + HOVER HANDLE: no dead gap column between panes now — the
+ // resize handle is the LEFT pane's last column, and hovering it overlays a
+ // dash (keeping the cell's colors). 2 equal columns at 100 wide -> left width
+ // 50, last col = 49 (SGR 50). A no-button motion there paints the "╎".
+ try hs.send("\x1b[<35;50;10M"); // motion (no button) over the left pane's last col
+ try hs.pump(400);
+ try hs.expectContains("╎", "hovering the column boundary did not overlay the dashed resize handle");
+
// 3. Default is NORMAL mode: the tag shows "nm" (helix-style block cursor).
try hs.expectContains("nm /", "default tag is not 'nm' (normal mode not default)");
@@ -80,6 +88,23 @@ pub fn main(init: std.process.Init) !void {
try hs.expectWaitFor("ZZTABRAN", 8000, "normal-mode Tab did not execute the word under the cursor");
try hs.pump(400);
+ // 3c-bis. CENTRAL DISPATCH: a builtin executes from a PANE BODY, not only the
+ // tagbar. Echo "Debug" as output (visible in normal mode), click it, TAB it:
+ // the same .debug action runs through the one dispatcher and toggles the
+ // overlay OFF (step 0 turned it on) -> "resizes=" disappears. Proves onSelect
+ // matches builtins wherever the word appears (tty here, not the tagbar).
+ try hs.send("\x02"); // normal -> tty
+ try hs.pump(400);
+ try hs.send("clear; echo Debug\r");
+ try hs.pump(900);
+ try hs.send("\x02"); // tty -> normal
+ try hs.pump(400);
+ try hs.send("\x1b[<0;5;3M\x1b[<0;5;3m"); // left-click "Debug" on body row 0 (col 3=body 0; GUTTER 2)
+ try hs.pump(400);
+ try hs.send("\t"); // TAB: execute the builtin "Debug"
+ try hs.pump(600);
+ try hs.expectNotContains("resizes=", "executing 'Debug' from a pane body did not toggle the overlay (central dispatch broken)");
+
// 3d. OUTPUT PADDING: a command composed (typed as an overlay) far below the
// prompt runs BELOW where it was typed, not back up at the prompt. The app
// pads the shell down with newlines to max(last-edit row, prompt row); the
diff --git a/tutor.txt b/tutor.txt
index c2982fb7..3ce27edb 100644
--- a/tutor.txt
+++ b/tutor.txt
@@ -80,9 +80,12 @@
syntax/ansi recolor, and the theme.
Right-click a directory in any body to open a terminal there.
- LAYOUT: columns split the screen; windows stack within a column.
- - drag the VERTICAL gap between columns to resize
- - drag the HORIZONTAL gap between stacked windows to resize
+ LAYOUT: columns split the screen; windows stack within a column. Panes
+ abut with no wasted gap — a pane's own trailing edge (its last column, or
+ last row above the next tag) IS the resize handle: hover it and a dashed
+ line overlays that edge (keeping the underlying colors) to mark the drag.
+ - drag a column's right edge to resize it against its neighbour
+ - drag a window's bottom edge to resize it against the one below
- drag the accent box (top-left of a pane; indigo-purple, brighter
when the pane is focused) to MOVE a window between columns / reorder
- the scrollbar is the gutter below the box: left-click scrolls UP