diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-01 11:15:30 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-07-01 11:15:56 -0300 |
| commit | c301ae3567204666c914ba4b36336d3a91af0cd1 (patch) | |
| tree | 35313e47977ac4e0e6ff8ffaaf1caa0aa3ed9de7 | |
| parent | 6570dc04d6343cbbf31c0eb2363f9bec817aa4db (diff) | |
| download | pardes-c301ae3567204666c914ba4b36336d3a91af0cd1.tar.gz pardes-c301ae3567204666c914ba4b36336d3a91af0cd1.zip | |
changed some draggable styling
| -rw-r--r-- | main.zig | 116 | ||||
| -rw-r--r-- | tests.zig | 25 | ||||
| -rw-r--r-- | tutor.txt | 9 |
3 files changed, 99 insertions, 51 deletions
@@ -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, "╌"); } } }, @@ -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 @@ -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 |
