diff options
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 534 |
1 files changed, 337 insertions, 197 deletions
@@ -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 |
