summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-06-30 11:43:52 -0300
committerGabriel Schneider <[email protected]>2026-06-30 12:55:48 -0300
commit651af1db938bcbaa039d14b65ff6bc184a3d91b4 (patch)
treea9d2580bddd813c276b89875b5c15d85b2039b41 /main.zig
parent8c54adee7d942606ff7c3bd8b293b11507f5b0a9 (diff)
downloadpardes-651af1db938bcbaa039d14b65ff6bc184a3d91b4.tar.gz
pardes-651af1db938bcbaa039d14b65ff6bc184a3d91b4.zip
vibes cleaning slop
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig534
1 files changed, 337 insertions, 197 deletions
diff --git a/main.zig b/main.zig
index 49413eec..7221c5eb 100644
--- a/main.zig
+++ b/main.zig
@@ -213,6 +213,28 @@ fn paneScrollBy(t: *Term, delta: i32) void {
}
}
+// Before running a composed command in a terminal, push the shell down so its
+// output lands BELOW the last row the user typed overlay text on. The shell owns
+// its cursor, so we move it the only way we can: emit newlines to the pty until
+// the prompt sits at max(last-edit row, the shell's current prompt row). The empty
+// prompts this creates are hidden in text mode (promptFlags), so the gap reads as
+// blank padding and the output flows right under the composed text. No-op unless
+// the shell is at a prompt (don't inject newlines into a running program).
+// ponytail: pad caps at the grid height — you can't click/type past the pane.
+fn padOutputBelowEdits(t: *Term) void {
+ if (t.file != null or t.nedits == 0) return;
+ if (!t.term.cursorIsAtPrompt()) return;
+ var max_row: i32 = std.math.minInt(i32);
+ for (t.edits[0..t.nedits]) |e| {
+ if (e.len > 0 and e.row > max_row) max_row = e.row;
+ }
+ if (max_row == std.math.minInt(i32)) return;
+ const cursor_abs: i32 = paneScroll(t) + @as(i32, @intCast(t.term.screens.active.cursor.y));
+ const pad = std.math.clamp(max_row - cursor_abs, 0, @as(i32, t.rows));
+ var i: i32 = 0;
+ while (i < pad) : (i += 1) termWrite(t, "\r");
+}
+
// The shell's current working directory (via /proc/<pid>/cwd). `buf` holds it.
// A raw linux syscall returns -errno; check the sign directly. (std.posix.errno
// is the LIBC convention here — `rc == -1` — and we link libc, so it misreads a
@@ -242,8 +264,11 @@ fn isDir(path: [*:0]const u8) bool {
// The window tag: the shell's cwd followed by the builtin command names. Plain,
// selectable text (acme-style). Caller frees with `alloc`.
fn tagLine(alloc: std.mem.Allocator, t: *Term) ![]u8 {
- // a file pane's tag is just its path (+ builtins); no tty mode/cwd.
- if (t.file) |f| return std.fmt.allocPrint(alloc, "{s} {s}", .{ f.path, builtins_str });
+ // a file pane's tag: mode + path (+ builtins); no cwd. Files never reach tty.
+ if (t.file) |f| {
+ const fmode = if (t.mode == .insert) "in" else "nm";
+ return std.fmt.allocPrint(alloc, "{s} {s} {s}", .{ fmode, f.path, builtins_str });
+ }
var link: [1024]u8 = undefined;
const cwd = termCwd(t, &link);
// leading 2-char mode indicator: "nm" (normal), "in" (insert) or "sy" (sym/tty).
@@ -518,6 +543,40 @@ fn evictLonePristineTty(l: *Layout, terms: *[MAX_TERMS]?*Term, io: std.Io, gpa:
if (active.* == tty_id) active.* = keep_id;
}
+// Find a terminal whose shell cwd is `dir`, else fork a fresh shell there and
+// stack it at the bottom of the rightmost column. Returns the terminal to drive,
+// or null if no slot is free / the fork failed. Backs middle-click "send" from a
+// file pane: the selected line runs in a shell in the file's dir. Does NOT change
+// focus — middle/execute keeps you where you were editing (right/look focuses).
+// ponytail: rightmost column; with a single column the tty stacks under the file.
+fn ttyForDir(
+ io: std.Io,
+ gpa: std.mem.Allocator,
+ loop: *Loop,
+ terms: *[MAX_TERMS]?*Term,
+ layout: *Layout,
+ sw: u16,
+ sh: u16,
+ dir: []const u8,
+) ?*Term {
+ for (terms.*) |slot| if (slot) |tt| {
+ if (tt.file != null) continue;
+ var cb: [1024]u8 = undefined;
+ if (std.mem.eql(u8, termCwd(tt, &cb), dir)) return tt;
+ };
+ const free: usize = for (terms.*, 0..) |slot, i| {
+ if (slot == null) break i;
+ } else return null;
+ var dirbuf: [1024]u8 = undefined;
+ const dirz = std.fmt.bufPrintZ(&dirbuf, "{s}", .{dir}) catch return null;
+ const nt = forkTerm(gpa, sw, sh, dirz.ptr) catch return null;
+ terms[free] = nt;
+ startReader(io, gpa, loop, nt, free) catch {};
+ const rc = if (layout.ncol > 0) layout.ncol - 1 else 0;
+ layoutInsert(layout, rc, layout.col_n[rc], free);
+ return nt;
+}
+
// 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.
@@ -1087,13 +1146,14 @@ fn insertRunText(t: *Term, text: []const u8) void {
// viewport z t / z z / z b (top/center/bottom)
// insert i a I A o O (at/append/line-start/line-end/below/above)
// select line x (extend down) delete d change c yank y paste p
-// mode Esc -> insert exits to normal; Enter -> tty (terminals only)
+// chord Enter -> acme look (open file/dir) Tab -> acme execute (run word)
+// mode Esc -> insert exits to normal; Ctrl-b -> raw tty (terminals)
// window Ctrl-w h/j/k/l focus neighbor pane (Alt-n new term, Alt-c new col)
// File panes mutate their content (real edits); terminals yank rendered text and
// paste it as an insertion run (shell output can't be deleted, so d/c there only
// clear pasted runs in the selection). Select mode (`v`, char ranges) is not yet
// wired; use `x` for line-oriented selection.
-// Enter tty mode from normal (terminals only). tty-native: if the shell is at a
+// Enter raw tty mode (the Ctrl-b toggle; terminals only). tty-native: if the shell is at a
// prompt and the modal cursor sits on the current input line, move the shell's
// REAL cursor there via ghostty's promptClickMove (sends left/right arrows the
// shell understands) — so dropping into tty lands where you navigated: at the
@@ -1129,9 +1189,6 @@ fn enterTty(t: *Term) void {
fn handleNormal(gpa: std.mem.Allocator, talloc: std.mem.Allocator, t: *Term, key: vaxis.Key) void {
pinCursor(t);
- // terminals: Enter drops into tty, positioning the shell cursor at the modal
- // cursor's spot on the input line (start if empty, end if typed text).
- if (t.file == null and key.matches(vaxis.Key.enter, .{})) return enterTty(t);
const pl = paneCursorLines(talloc, t) catch return;
defer freePaneLines(talloc, pl);
const cur = toModalCursor(t, pl);
@@ -1371,6 +1428,213 @@ fn focusDir(active: *usize, terms: *const [MAX_TERMS]?*Term, rects: *const [MAX_
if (best) |b| active.* = b;
}
+// Perform an acme button-2/3 chord with `txt` as the text, from pane `id`:
+// .right = "look" (open the file/dir under the word, peeling a :line suffix),
+// .middle = "execute" (a builtin, else run the text in a shell). Shared by the
+// mouse release and the normal-mode Enter (look) / Tab (execute) keys. Returns
+// true if the app should quit (the last pane was closed).
+fn actOnSelection(
+ io: std.Io,
+ gpa: std.mem.Allocator,
+ loop: *Loop,
+ terms: *[MAX_TERMS]?*Term,
+ layout: *Layout,
+ rects: *const [MAX_TERMS]Rect,
+ active: *usize,
+ sw: u16,
+ sh: u16,
+ button: Button,
+ id: usize,
+ txt: []const u8,
+) bool {
+ const t = terms[id] orelse return false;
+ if (button == .right) {
+ // acme "look": resolve the selected word (peeling a :line suffix) relative
+ // to the window's dir. A directory opens/focuses a terminal there + ls; a
+ // file opens/focuses a file pane scrolled to :line.
+ const word_raw = std.mem.trim(u8, txt, " \t\r\n");
+ const pl = parsePathLine(word_raw);
+ const word = pl.path;
+ var cwdbuf: [1024]u8 = undefined;
+ var joinbuf: [2048]u8 = undefined;
+ var realbuf: [4096]u8 = undefined;
+ const cwd = if (t.file) |f| (std.fs.path.dirname(f.path) orelse "/") else termCwd(t, &cwdbuf);
+ const joined: ?[:0]u8 = if (word.len == 0) null else if (word[0] == '/')
+ (std.fmt.bufPrintZ(&joinbuf, "{s}", .{word}) catch null)
+ else
+ (std.fmt.bufPrintZ(&joinbuf, "{s}/{s}", .{ cwd, word }) catch null);
+ if (joined) |jz| if (realpath(jz.ptr, &realbuf)) |rp| if (isDir(rp)) {
+ const dir = std.mem.span(rp);
+ // focus an existing terminal on this dir
+ var found: ?usize = null;
+ for (terms, 0..) |*slot, i| if (slot.*) |tt| {
+ var cb: [1024]u8 = undefined;
+ if (std.mem.eql(u8, termCwd(tt, &cb), dir)) {
+ found = i;
+ break;
+ }
+ };
+ if (found) |fid| {
+ active.* = fid;
+ if (terms[fid]) |tt| termWrite(tt, "ls\r");
+ } else {
+ const slot: ?usize = for (terms.*, 0..) |sl, i| {
+ if (sl == null) break i;
+ } else null;
+ if (slot) |free| {
+ if (forkTerm(gpa, sw, sh, rp)) |nt| {
+ terms[free] = nt;
+ startReader(io, gpa, loop, nt, free) catch {};
+ const f = layoutFindTerm(layout, id).?;
+ layoutInsert(layout, f.col, f.idx + 1, free);
+ splitBelow(t, nt, rects[id].h);
+ active.* = free;
+ termWrite(nt, "ls\r");
+ } else |_| {}
+ }
+ }
+ } else {
+ const fp = std.mem.span(rp);
+ // focus an existing file pane for this path
+ var found: ?usize = null;
+ for (terms, 0..) |*slot, i| if (slot.*) |tt| {
+ if (tt.file) |ff| if (std.mem.eql(u8, ff.path, fp)) {
+ found = i;
+ break;
+ };
+ };
+ if (found) |fid| {
+ active.* = fid;
+ if (pl.line > 0) if (terms[fid]) |tt| if (tt.file) |*ff|
+ if (pl.line <= fileLineCount(ff.content)) {
+ ff.scroll = pl.line - 1;
+ };
+ } else {
+ const slot: ?usize = for (terms.*, 0..) |sl, i| {
+ if (sl == null) break i;
+ } else null;
+ if (slot) |free| {
+ if (openFileView(gpa, rp, pl.line, sw, sh)) |nt| {
+ terms[free] = nt;
+ // files live in the LEFT column (acme convention). Stack at its
+ // top with a fair share of the column height, then drop a
+ // leftover unused shell.
+ const lc = 0;
+ const ln = layout.col_n[lc];
+ if (ln > 0) {
+ var vsum: f32 = 0;
+ for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
+ vsum += p.vweight;
+ };
+ nt.vweight = vsum / @as(f32, @floatFromInt(ln));
+ }
+ layoutInsert(layout, lc, 0, free);
+ active.* = free;
+ evictLonePristineTty(layout, terms, io, gpa, lc, free, active);
+ } else |_| {}
+ }
+ }
+ };
+ return false;
+ }
+ switch (onSelect(button, txt)) {
+ .none => {},
+ .send => {
+ // terminal: run in itself. file pane: run in a shell sitting in the
+ // file's directory (found by cwd, else a fresh tty in the right column).
+ const cmd = std.mem.trim(u8, txt, " \t\r\n");
+ const dst: ?*Term = if (t.file) |f|
+ ttyForDir(io, gpa, loop, terms, layout, sw, sh, std.fs.path.dirname(f.path) orelse "/")
+ else
+ t;
+ if (dst) |dt| {
+ padOutputBelowEdits(dt);
+ termWrite(dt, cmd);
+ termWrite(dt, "\r");
+ }
+ },
+ .newcol => {
+ // new terminal in a fresh column; ls its directory.
+ const slot: ?usize = for (terms.*, 0..) |sl, i| {
+ if (sl == null) break i;
+ } else null;
+ if (slot) |free| if (layout.ncol < MAX_COLS) {
+ if (forkTerm(gpa, sw, sh, null)) |nt| {
+ terms[free] = nt;
+ startReader(io, gpa, loop, nt, free) catch {};
+ layoutAppendColumn(layout, free);
+ active.* = free;
+ termWrite(nt, "ls\r");
+ } else |_| {}
+ };
+ },
+ .del => {
+ // close just this window
+ absorbVWeight(layout, terms, id);
+ layoutRemove(layout, id);
+ t.deinit(io, gpa);
+ gpa.destroy(t);
+ terms[id] = null;
+ if (active.* == id) active.* = for (terms.*, 0..) |sl, i| {
+ if (sl != null) break i;
+ } else return true;
+ },
+ .delcol => {
+ // close every terminal in this one's column
+ if (layoutFindTerm(layout, id)) |f| {
+ var ids: [MAX_TERMS]usize = undefined;
+ const nids = layout.col_n[f.col];
+ for (0..nids) |k| ids[k] = layout.col_terms[f.col][k];
+ for (ids[0..nids]) |tid| {
+ if (terms[tid]) |tt| {
+ layoutRemove(layout, tid);
+ tt.deinit(io, gpa);
+ gpa.destroy(tt);
+ terms[tid] = null;
+ }
+ }
+ if (terms[active.*] == null) active.* = for (terms.*, 0..) |sl, i| {
+ if (sl != null) break i;
+ } else return true;
+ }
+ },
+ .tutor => {
+ // spawn the Tutor as a file pane in the LEFT column (acme convention for
+ // files), stacked at the top with a fair share of the column.
+ const slot: ?usize = for (terms.*, 0..) |sl, i| {
+ if (sl == null) break i;
+ } else null;
+ if (slot) |free| {
+ if (openTutorView(gpa, sw, sh)) |nt| {
+ terms[free] = nt;
+ const lc = 0;
+ const ln = layout.col_n[lc];
+ if (ln > 0) {
+ var vsum: f32 = 0;
+ for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
+ vsum += p.vweight;
+ };
+ nt.vweight = vsum / @as(f32, @floatFromInt(ln));
+ }
+ layoutInsert(layout, lc, 0, free);
+ active.* = free;
+ evictLonePristineTty(layout, terms, io, gpa, lc, free, active);
+ } else |_| {}
+ }
+ },
+ }
+ return false;
+}
+
+// The word under the modal cursor as a pane-local selection in paneText coords
+// (row 0 is the tag; file panes carry the line-number prefix). Feed it through
+// expandSel + selectionText to get the acme chord text for the Enter/Tab keys.
+fn cursorWordSel(t: *Term) Term.Sel {
+ const vrow = (t.cur_row - paneScroll(t)) + @as(i32, BOX_H);
+ const vcol = if (t.file != null) t.cur_col + @as(i32, PREFIX_W) else t.cur_col;
+ return .{ .state = .done, .c0 = vcol, .c1 = vcol, .r0 = vrow, .r1 = vrow };
+}
+
pub fn main(init: std.process.Init) !void {
const io = init.io;
const gpa = init.gpa;
@@ -1423,6 +1687,9 @@ pub fn main(init: std.process.Init) !void {
terms[0] = try forkTerm(gpa, screen_w, screen_h, null);
terms[1] = try forkTerm(gpa, screen_w, screen_h, null);
terms[2] = try forkTerm(gpa, screen_w, screen_h, null);
+ // a fresh shell greets you with its directory listing (the readers below pick
+ // up the output once they start).
+ for (terms[0..3]) |slot| if (slot) |t| termWrite(t, "ls\r");
layout.ncol = 2;
layout.col_n[0] = 2;
layout.col_terms[0][0] = 0;
@@ -1526,19 +1793,21 @@ pub fn main(init: std.process.Init) !void {
}
}
} else if (mouse.type == .press and (mouse.button == .middle or mouse.button == .right)) {
- // middle/right select text too (own state + color), text area only
+ // middle/right select text too (own state + color), text area only.
+ // Right focuses the pane (acme "look" lands you there); middle
+ // (execute) does NOT — you stay where you were editing.
const b: Button = if (mouse.button == .middle) .middle else .right;
if (hovered) |id| {
const r = rects[id];
if (mcol < r.x + GUTTER and mrow >= r.y + BOX_H) {
// gutter (scrollbar): right scrolls down, mirroring left's up
- active = id;
+ if (b == .right) active = id;
if (terms[id]) |t| {
const local: i32 = @as(i32, mrow) - @as(i32, r.y + BOX_H);
paneScrollBy(t, if (b == .right) local else -local);
}
} else if (mcol >= r.x + GUTTER) {
- active = id;
+ if (b == .right) active = id;
if (terms[id]) |t| {
const c: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER);
const v: i32 = @as(i32, mrow) - @as(i32, r.y);
@@ -1623,197 +1892,33 @@ pub fn main(init: std.process.Init) !void {
.select => |s| {
if (terms[s.id]) |t| {
t.sel[@intFromEnum(s.button)].state = .done;
- // a no-drag middle/right click expands to the word
- // under the cursor (acme button-2/3 behavior).
- if (s.button != .left) expandSel(gpa, t, &t.sel[@intFromEnum(s.button)]);
- if (selectionText(gpa, t, t.sel[@intFromEnum(s.button)])) |txt| {
- defer gpa.free(txt);
- if (s.button == .right) {
- // acme "look": resolve the selected word
- // (peeling a :line suffix) relative to the
- // window's dir. A directory opens/focuses a
- // terminal there + ls; a file opens/focuses a
- // file pane scrolled to :line.
- const word_raw = std.mem.trim(u8, txt, " \t\r\n");
- const pl = parsePathLine(word_raw);
- const word = pl.path;
- var cwdbuf: [1024]u8 = undefined;
- var joinbuf: [2048]u8 = undefined;
- var realbuf: [4096]u8 = undefined;
- const cwd = if (t.file) |f| (std.fs.path.dirname(f.path) orelse "/") else termCwd(t, &cwdbuf);
- const joined: ?[:0]u8 = if (word.len == 0) null else if (word[0] == '/')
- (std.fmt.bufPrintZ(&joinbuf, "{s}", .{word}) catch null)
- else
- (std.fmt.bufPrintZ(&joinbuf, "{s}/{s}", .{ cwd, word }) catch null);
- if (joined) |jz| if (realpath(jz.ptr, &realbuf)) |rp| if (isDir(rp)) {
- const dir = std.mem.span(rp);
- // focus an existing terminal on this dir
- var found: ?usize = null;
- for (&terms, 0..) |*slot, i| if (slot.*) |tt| {
- var cb: [1024]u8 = undefined;
- if (std.mem.eql(u8, termCwd(tt, &cb), dir)) {
- found = i;
- break;
- }
- };
- if (found) |id| {
- active = id;
- if (terms[id]) |tt| termWrite(tt, "ls\r");
- } else {
- const slot: ?usize = for (terms, 0..) |sl, i| {
- if (sl == null) break i;
- } else null;
- if (slot) |free| {
- if (forkTerm(gpa, screen_w, screen_h, rp)) |nt| {
- terms[free] = nt;
- startReader(io, gpa, &loop, nt, free) catch {};
- const f = layoutFindTerm(&layout, s.id).?;
- layoutInsert(&layout, f.col, f.idx + 1, free);
- splitBelow(t, nt, rects[s.id].h);
- active = free;
- termWrite(nt, "ls\r");
- } else |_| {}
- }
- }
- } else {
- const fp = std.mem.span(rp);
- // focus an existing file pane for this path
- var found: ?usize = null;
- for (&terms, 0..) |*slot, i| if (slot.*) |tt| {
- if (tt.file) |ff| if (std.mem.eql(u8, ff.path, fp)) {
- found = i;
- break;
- };
- };
- if (found) |id| {
- active = id;
- if (pl.line > 0) if (terms[id]) |tt| if (tt.file) |*ff|
- if (pl.line <= fileLineCount(ff.content)) {
- ff.scroll = pl.line - 1;
- };
- } else {
- const slot: ?usize = for (terms, 0..) |sl, i| {
- if (sl == null) break i;
- } else null;
- if (slot) |free| {
- if (openFileView(gpa, rp, pl.line, screen_w, screen_h)) |nt| {
- terms[free] = nt;
- // files live in the LEFT column (acme
- // convention). Stack at its top with a
- // fair share of the column height, then
- // drop a leftover unused shell.
- const lc = 0;
- const ln = layout.col_n[lc];
- if (ln > 0) {
- var vsum: f32 = 0;
- for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
- vsum += p.vweight;
- };
- nt.vweight = vsum / @as(f32, @floatFromInt(ln));
- }
- layoutInsert(&layout, lc, 0, free);
- active = free;
- evictLonePristineTty(&layout, &terms, io, gpa, lc, free, &active);
- } else |_| {}
- }
- }
- };
- } else switch (onSelect(s.button, txt)) {
- .none => {},
- .send => {
- // run the selection in the terminal it came from
- termWrite(t, std.mem.trim(u8, txt, " \t\r\n"));
- termWrite(t, "\r");
- },
- .newcol => {
- // new terminal in a fresh column
- const slot: ?usize = for (terms, 0..) |sl, i| {
- if (sl == null) break i;
- } else null;
- if (slot) |free| if (layout.ncol < MAX_COLS) {
- if (forkTerm(gpa, screen_w, screen_h, null)) |nt| {
- terms[free] = nt;
- startReader(io, gpa, &loop, nt, free) catch {};
- layoutAppendColumn(&layout, free);
- active = free;
- } else |_| {}
- };
- },
- .del => {
- // close just this window
- absorbVWeight(&layout, &terms, s.id);
- layoutRemove(&layout, s.id);
- t.deinit(io, gpa);
- gpa.destroy(t);
- terms[s.id] = null;
- if (active == s.id) active = for (terms, 0..) |sl, i| {
- if (sl != null) break i;
- } else break;
- },
- .delcol => {
- // close every terminal in this one's column
- if (layoutFindTerm(&layout, s.id)) |f| {
- var ids: [MAX_TERMS]usize = undefined;
- const nids = layout.col_n[f.col];
- for (0..nids) |k| ids[k] = layout.col_terms[f.col][k];
- for (ids[0..nids]) |tid| {
- if (terms[tid]) |tt| {
- layoutRemove(&layout, tid);
- tt.deinit(io, gpa);
- gpa.destroy(tt);
- terms[tid] = null;
- }
- }
- if (terms[active] == null) active = for (terms, 0..) |sl, i| {
- if (sl != null) break i;
- } else break;
- }
- },
- .tutor => {
- // spawn the Tutor as a file pane in the LEFT
- // column (acme convention for files), stacked
- // at the top with a fair share of the column.
- const slot: ?usize = for (terms, 0..) |sl, i| {
- if (sl == null) break i;
- } else null;
- if (slot) |free| {
- if (openTutorView(gpa, screen_w, screen_h)) |nt| {
- terms[free] = nt;
- const lc = 0;
- const ln = layout.col_n[lc];
- if (ln > 0) {
- var vsum: f32 = 0;
- for (0..ln) |i| if (terms[layout.col_terms[lc][i]]) |p| {
- vsum += p.vweight;
- };
- nt.vweight = vsum / @as(f32, @floatFromInt(ln));
- }
- layoutInsert(&layout, lc, 0, free);
- active = free;
- evictLonePristineTty(&layout, &terms, io, gpa, lc, free, &active);
- } else |_| {}
- }
- },
- }
- } else |_| {}
if (s.button == .left) {
// left-click in the body enters NORMAL mode and
- // anchors the cursor at the selection end (a
- // visual column). The row is stored absolute
- // (visual + scroll off) so it rides the scrollback.
- // clicks on the tag (row < BOX_H) don't edit.
+ // anchors the cursor at the selection end. The row is
+ // stored absolute (visual + scroll off) so it rides
+ // the scrollback. tag clicks (row < BOX_H) don't edit.
const sl = t.sel[@intFromEnum(Button.left)];
const body_vis = sl.r1 - @as(i32, BOX_H);
if (body_vis >= 0) {
const off: i32 = paneScroll(t);
t.cur_row = body_vis + off;
- t.cur_col = sl.c1;
+ // file panes render a line-number prefix; cur_col
+ // is content-relative, so peel PREFIX_W off the click.
+ t.cur_col = if (t.file != null) @max(0, sl.c1 - @as(i32, PREFIX_W)) else sl.c1;
t.cur_off = if (runAt(t, t.cur_row, t.cur_col)) |i| t.edits[i].len else 0;
t.cur_pinned = true;
t.mode = .normal;
t.msel.active = false;
t.pending = 0;
}
+ } else {
+ // middle/right: acme execute/look. A no-drag click
+ // expands to the file-ish word under the cursor first.
+ expandSel(gpa, t, &t.sel[@intFromEnum(s.button)]);
+ if (selectionText(gpa, t, t.sel[@intFromEnum(s.button)])) |txt| {
+ defer gpa.free(txt);
+ if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, s.button, s.id, txt)) break;
+ } else |_| {}
}
}
},
@@ -1857,6 +1962,7 @@ pub fn main(init: std.process.Init) !void {
const f = layoutFindTerm(&layout, active).?;
layoutInsert(&layout, f.col, f.idx + 1, slot);
active = slot;
+ termWrite(nt, "ls\r");
} else |_| {}
}
break :kpress;
@@ -1871,9 +1977,37 @@ pub fn main(init: std.process.Init) !void {
break :kpress;
}
+ // Ctrl-b: hidden toggle in/out of raw tty mode (terminals only).
+ // tty forwards every key straight to the pty, so it's deliberately
+ // off the normal editing path — this toggle is the only way in/out.
+ if (key.matches('b', .{ .ctrl = true })) {
+ if (at.file == null) {
+ if (at.mode == .tty) {
+ at.mode = .normal;
+ at.pending = 0;
+ } else enterTty(at);
+ }
+ break :kpress;
+ }
+
+ // Normal-mode Enter/Tab mirror the acme mouse chords on the word
+ // under the cursor: Enter = right-click "look" (open the file/dir),
+ // Tab = middle-click "execute" (run it / builtin).
+ if (at.mode == .normal and (key.matches(vaxis.Key.enter, .{}) or key.matches(vaxis.Key.tab, .{}))) {
+ pinCursor(at);
+ const btn: Button = if (key.matches(vaxis.Key.tab, .{})) .middle else .right;
+ var sel = cursorWordSel(at);
+ expandSel(gpa, at, &sel);
+ if (selectionText(gpa, at, sel)) |word| {
+ defer gpa.free(word);
+ if (actOnSelection(io, gpa, &loop, &terms, &layout, &rects, &active, screen_w, screen_h, btn, active, word)) break;
+ } else |_| {}
+ break :kpress;
+ }
+
// per-mode dispatch. Esc: insert -> normal (normal Esc is a
- // no-op, like helix). Enter from normal reaches tty on terminals
- // (handled in handleNormal). tty forwards every key to the pty.
+ // no-op, like helix). Ctrl-b toggles raw tty on terminals (above).
+ // tty forwards every key to the pty.
switch (at.mode) {
.normal => handleNormal(gpa, frame_arena.allocator(), at, key),
.insert => {
@@ -1889,7 +2023,15 @@ pub fn main(init: std.process.Init) !void {
},
.pty_read => |pr| {
defer gpa.free(pr.bytes);
- if (terms[pr.id]) |t| t.stream.nextSlice(pr.bytes);
+ if (terms[pr.id]) |t| {
+ t.stream.nextSlice(pr.bytes);
+ // follow new output to the bottom, so a command's result shows
+ // even in a pane you didn't focus. Exception: the pane you're
+ // actively navigating in a text mode — don't yank the cursor out
+ // from under you (it still tracks the bottom on its own there).
+ if (!(pr.id == active and t.mode != .tty))
+ t.term.screens.active.scroll(.active);
+ }
},
.pty_eof => |id| {
if (terms[id]) |t| {
@@ -2174,15 +2316,13 @@ fn bodyText(alloc: std.mem.Allocator, t: *Term) ![]u8 {
const off: i32 = @intCast(t.term.screens.active.pages.scrollbar().offset);
const body = try t.term.plainString(alloc);
defer alloc.free(body);
- // text mode hides shell prompt rows only in INSERT mode (the acme compose
- // surface: a clean blank line to type into). NORMAL mode shows the real
- // terminal — prompts + typed input visible — so the modal cursor can navigate
- // to the input line and Enter drops into tty at that spot (start vs end).
- // tty mode shows everything.
+ // text mode (normal AND insert) hides shell prompt rows: a clean acme-style
+ // page to navigate and type on. Only raw tty mode (the Ctrl-b toggle) shows
+ // the real terminal with its prompts + typed input.
var flagbuf: [512]bool = undefined;
const nrows: usize = @min(t.rows, flagbuf.len);
const flags = flagbuf[0..nrows];
- if (t.mode == .insert) promptFlags(t, flags) else @memset(flags, false);
+ if (t.mode != .tty) promptFlags(t, flags) else @memset(flags, false);
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(alloc);
// pad to the full grid height: plainString trims trailing blank rows, but we