diff --git a/build.zig.zon b/build.zig.zon index cecde0f5..15896194 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#855da7eb7e0991b360e1dcb630691465b725c761", - .hash = "vaxis-0.5.1-BWNV_OxtCQD40bC4JBVvCa4GjVRU4ESeFmYOUPcsuROG", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#1f16837cc6444f9323ac21a7988860f6a424b9d0", + .hash = "vaxis-0.5.1-BWNV_BazCQACrQg5CxqJoXx6A0SusCFlc8Rr-vgePzLr", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 0415135b..4e6b6dbe 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -40,8 +40,6 @@ ["ctrl+shift+f", "find_in_files"], ["ctrl+shift+l", "toggle_panel"], ["alt+shift+p", "open_command_palette"], - ["ctrl+shift+page_up", "terminal_scroll_up"], - ["ctrl+shift+page_down", "terminal_scroll_down"], ["alt+n", "goto_next_file_or_diagnostic"], ["alt+p", "goto_prev_file_or_diagnostic"], ["alt+l", "toggle_panel"], @@ -65,8 +63,8 @@ ["ctrl+shift+tab", "previous_tab"], ["ctrl+page_down", "next_tab"], ["ctrl+page_up", "previous_tab"], - ["ctrl+shift+page_down", "move_tab_next"], - ["ctrl+shift+page_up", "move_tab_previous"], + ["ctrl+shift+page_down", "move_tab_next_or_scroll_terminal_down"], + ["ctrl+shift+page_up", "move_tab_previous_or_scroll_terminal_up"], ["ctrl+k e", "switch_buffers"], ["alt+shift+v", "clipboard_history"], ["ctrl+0", "reset_fontsize"], @@ -353,6 +351,7 @@ ["alt+f9", "overlay_next_widget_style"], ["alt+!", "add_task"], ["ctrl+j", "toggle_panel"], + ["ctrl+shift+j", "toggle_maximize_panel"], ["ctrl+q", "quit"], ["ctrl+w", "close_file"], ["ctrl+shift+f", "find_in_files"], @@ -599,11 +598,13 @@ ["ctrl+8", "focus_split", 7], ["ctrl+`", "focus_terminal"], ["ctrl+j", "toggle_panel"], + ["ctrl+shift+page_down", "terminal_scroll_down"], + ["ctrl+shift+page_up", "terminal_scroll_up"], + ["ctrl+shift+j", "toggle_maximize_panel"], ["ctrl+shift+p", "open_command_palette"], ["alt+shift+p", "open_command_palette"], ["alt+x", "open_command_palette"], ["alt+!", "run_task"], - ["ctrl+shift+j", "toggle_maximize_panel"], ["alt+f9", "panel_next_widget_style"], ["ctrl+shift+q", "quit_without_saving"], ["ctrl+alt+shift+r", "restart"] diff --git a/src/tui/home.zig b/src/tui/home.zig index 9cdb16f0..40b41c03 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -34,6 +34,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette + \\focus_terminal \\run_task \\add_task \\open_config @@ -52,6 +53,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette + \\focus_terminal \\run_task \\add_task \\open_config diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 985819c6..76ad8461 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1014,7 +1014,7 @@ const cmds = struct { vt.focus(); } } - pub const focus_terminal_meta: Meta = .{ .description = "Focus terminal panel" }; + pub const focus_terminal_meta: Meta = .{ .description = "Terminal" }; pub fn close_find_in_files_results(self: *Self, _: Ctx) Result { if (self.file_list_type == .find_in_files) @@ -1591,6 +1591,22 @@ const cmds = struct { } pub const move_tab_previous_meta: Meta = .{ .description = "Move tab to previous position" }; + pub fn move_tab_next_or_scroll_terminal_down(self: *Self, _: Ctx) Result { + if (self.is_panel_view_showing(terminal_view)) + try command.executeName("terminal_scroll_down", .{}) + else + _ = try self.widgets_widget.msg(.{"move_tab_next"}); + } + pub const move_tab_next_or_scroll_terminal_down_meta: Meta = .{ .description = "Move tab next or scroll terminal down" }; + + pub fn move_tab_previous_or_scroll_terminal_up(self: *Self, _: Ctx) Result { + if (self.is_panel_view_showing(terminal_view)) + try command.executeName("terminal_scroll_up", .{}) + else + _ = try self.widgets_widget.msg(.{"move_tab_previous"}); + } + pub const move_tab_previous_or_scroll_terminal_up_meta: Meta = .{ .description = "Move tab previous or scroll terminal up" }; + pub fn place_next_tab(self: *Self, ctx: Ctx) Result { var pos: enum { before, after } = undefined; var buffer_ref: Buffer.Ref = undefined; diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index a6fc4129..0b84ae35 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -189,7 +189,7 @@ pub fn shutdown(allocator: Allocator) void { } } -pub fn render(self: *Self, _: *const Widget.Theme) bool { +pub fn render(self: *Self, theme: *const Widget.Theme) bool { // Drain the vt event queue. while (self.vt.vt.tryEvent()) |event| { switch (event) { @@ -206,9 +206,48 @@ pub fn render(self: *Self, _: *const Widget.Theme) bool { self.vt.title.clearRetainingCapacity(); self.vt.title.appendSlice(self.allocator, t) catch {}; }, + .color_change => |cc| { + self.vt.app_fg = cc.fg; + self.vt.app_bg = cc.bg; + self.vt.app_cursor = cc.cursor; + }, + .osc_copy => |text| { + // Terminal app wrote to clipboard via OSC 52. + // Add to flow clipboard history and forward to system clipboard. + const owned = tui.clipboard_allocator().dupe(u8, text) catch break; + tui.clipboard_clear_all(); + tui.clipboard_start_group(); + tui.clipboard_add_chunk(owned); + tui.clipboard_send_to_system() catch {}; + }, + .osc_paste_request => { + // Terminal app requested clipboard contents via OSC 52. + // Assemble from flow clipboard history and respond. + if (tui.clipboard_get_history()) |history| { + var buf: std.Io.Writer.Allocating = .init(self.allocator); + defer buf.deinit(); + var first = true; + for (history) |chunk| { + if (first) first = false else buf.writer.writeByte('\n') catch break; + buf.writer.writeAll(chunk.text) catch break; + } + self.vt.vt.respondOsc52Paste(buf.written()); + } + }, } } + // Update the terminal's fg/bg color cache from the current theme so that + // OSC 10/11 colour queries return accurate values. + if (theme.editor.fg) |fg| { + const c = fg.color; + self.vt.vt.fg_color = .{ @truncate(c >> 16), @truncate(c >> 8), @truncate(c) }; + } + if (theme.editor.bg) |bg| { + const c = bg.color; + self.vt.vt.bg_color = .{ @truncate(c >> 16), @truncate(c >> 8), @truncate(c) }; + } + // Blit the terminal's front screen into our vaxis.Window. self.vt.vt.draw(self.allocator, self.plane.window, self.focused) catch |e| { std.log.err("terminal_view: draw failed: {}", .{e}); @@ -276,6 +315,10 @@ const Vt = struct { pty_pid: ?tp.pid = null, cwd: std.ArrayListUnmanaged(u8) = .empty, title: std.ArrayListUnmanaged(u8) = .empty, + /// App-specified override colours (from OSC 10/11/12). null = use theme. + app_fg: ?[3]u8 = null, + app_bg: ?[3]u8 = null, + app_cursor: ?[3]u8 = null, fn init(allocator: std.mem.Allocator, argv: []const []const u8, env: std.process.EnvMap, rows: u16, cols: u16) !void { const home = env.get("HOME") orelse "/tmp"; @@ -363,17 +406,23 @@ const pty = struct { } fn deinit(self: *@This()) void { + std.log.debug("terminal: pty actor deinit (pid={?})", .{self.vt.cmd.pid}); self.fd.deinit(); self.parser.buf.deinit(); self.parent.deinit(); self.allocator.destroy(self); - std.log.debug("terminal: pty destroyed", .{}); } fn start(self: *@This()) tp.result { errdefer self.deinit(); - self.fd = tp.file_descriptor.init("pty", self.pty_fd) catch |e| return tp.exit_error(e, @errorReturnTrace()); - self.fd.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.fd = tp.file_descriptor.init("pty", self.pty_fd) catch |e| { + std.log.debug("terminal: pty fd init failed: {}", .{e}); + return tp.exit_error(e, @errorReturnTrace()); + }; + self.fd.wait_read() catch |e| { + std.log.debug("terminal: pty initial wait_read failed: {}", .{e}); + return tp.exit_error(e, @errorReturnTrace()); + }; tp.receive(&self.receiver); } @@ -382,14 +431,28 @@ const pty = struct { if (try m.match(.{ "fd", "pty", "read_ready" })) { self.read_and_process() catch |e| return switch (e) { - error.Terminated => tp.exit_normal(), - error.InputOutput => tp.exit_normal(), - error.SendFailed => tp.exit_normal(), - error.Unexpected => tp.exit_normal(), + error.Terminated => { + std.log.debug("terminal: pty exiting: read loop terminated (process exited)", .{}); + return tp.exit_normal(); + }, + error.InputOutput => { + std.log.debug("terminal: pty exiting: EIO on read (process exited)", .{}); + return tp.exit_normal(); + }, + error.SendFailed => { + std.log.debug("terminal: pty exiting: send to parent failed", .{}); + return tp.exit_normal(); + }, + error.Unexpected => { + std.log.debug("terminal: pty exiting: unexpected error (see preceding log)", .{}); + return tp.exit_normal(); + }, }; } else if (try m.match(.{"quit"})) { + std.log.debug("terminal: pty exiting: received quit", .{}); return tp.exit_normal(); } else { + std.log.debug("terminal: pty exiting: unexpected message", .{}); return tp.unexpected(m); } } @@ -402,6 +465,7 @@ const pty = struct { error.WouldBlock => break, error.InputOutput => { const code = self.vt.cmd.wait(); + std.log.debug("terminal: read EIO, process exited with code={d}", .{code}); self.vt.event_queue.push(.{ .exited = code }); self.parent.send(.{ "terminal_view", "output" }) catch {}; return error.InputOutput; @@ -420,12 +484,13 @@ const pty = struct { error.LockViolation, error.Unexpected, => { - std.log.err("terminal_view: read unexpected: {}", .{e}); + std.log.debug("terminal: read unexpected error: {} (pid={?})", .{ e, self.vt.cmd.pid }); return error.Unexpected; }, }; if (n == 0) { const code = self.vt.cmd.wait(); + std.log.debug("terminal: read returned 0 bytes (EOF), process exited with code={d}", .{code}); self.vt.event_queue.push(.{ .exited = code }); self.parent.send(.{ "terminal_view", "output" }) catch {}; return error.Terminated; @@ -439,11 +504,12 @@ const pty = struct { error.OutOfMemory, error.Utf8InvalidStartByte, => { - std.log.err("terminal_view: processOutput unexpected: {}", .{e}); + std.log.debug("terminal: processOutput error: {} (pid={?})", .{ e, self.vt.cmd.pid }); return error.Unexpected; }, }) { .exited => { + std.log.debug("terminal: processOutput returned .exited (process EOF)", .{}); return error.Terminated; }, .running => {}, @@ -452,7 +518,7 @@ const pty = struct { self.fd.wait_read() catch |e| switch (e) { error.ThespianFileDescriptorWaitReadFailed => { - std.log.err("terminal_view: wait_read unexpected: {}", .{e}); + std.log.debug("terminal: wait_read failed: {} (pid={?})", .{ e, self.vt.cmd.pid }); return error.Unexpected; }, };