feat: add basic terminal_view
This commit is contained in:
parent
1cf22db498
commit
ff0495a265
6 changed files with 264 additions and 3 deletions
|
|
@ -4,7 +4,7 @@ const cbor = @import("cbor");
|
||||||
const log = @import("log");
|
const log = @import("log");
|
||||||
const Style = @import("theme").Style;
|
const Style = @import("theme").Style;
|
||||||
const Color = @import("theme").Color;
|
const Color = @import("theme").Color;
|
||||||
const vaxis = @import("vaxis");
|
pub const vaxis = @import("vaxis");
|
||||||
const input = @import("input");
|
const input = @import("input");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const RGB = @import("color").RGB;
|
const RGB = @import("color").RGB;
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ const Process = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn parse_arg0_to_argv(allocator: std.mem.Allocator, arg0: *[]const u8) !tp.message {
|
pub fn parse_arg0_to_argv(allocator: std.mem.Allocator, arg0: *[]const u8) !tp.message {
|
||||||
// this is horribly simplistic
|
// this is horribly simplistic
|
||||||
// TODO: add quotes parsing and workspace variables, etc.
|
// TODO: add quotes parsing and workspace variables, etc.
|
||||||
var args: std.ArrayList([]const u8) = .empty;
|
var args: std.ArrayList([]const u8) = .empty;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ const filelist_view = @import("filelist_view.zig");
|
||||||
const info_view = @import("info_view.zig");
|
const info_view = @import("info_view.zig");
|
||||||
const input_view = @import("inputview.zig");
|
const input_view = @import("inputview.zig");
|
||||||
const keybind_view = @import("keybindview.zig");
|
const keybind_view = @import("keybindview.zig");
|
||||||
|
const terminal_view = @import("terminal_view.zig");
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
const Commands = command.Collection(cmds);
|
const Commands = command.Collection(cmds);
|
||||||
|
|
@ -313,6 +314,28 @@ fn toggle_panel_view(self: *Self, view: anytype, mode: enum { toggle, enable, di
|
||||||
tui.resize();
|
tui.resize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_panel_view_with_args(self: *Self, view: anytype, mode: enum { toggle, enable, disable }, ctx: command.Context) !void {
|
||||||
|
if (self.panels) |panels| {
|
||||||
|
if (self.get_panel(@typeName(view))) |w| {
|
||||||
|
if (mode != .enable) {
|
||||||
|
panels.remove(w.*);
|
||||||
|
if (panels.empty()) {
|
||||||
|
self.widgets.remove(panels.widget());
|
||||||
|
self.panels = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode != .disable)
|
||||||
|
try panels.add(try view.create_with_args(self.allocator, self.widgets.plane, ctx));
|
||||||
|
}
|
||||||
|
} else if (mode != .disable) {
|
||||||
|
const panels = try WidgetList.createH(self.allocator, self.widgets.plane, "panel", .{ .static = self.get_panel_height() });
|
||||||
|
try self.widgets.add(panels.widget());
|
||||||
|
try panels.add(try view.create_with_args(self.allocator, self.widgets.plane, ctx));
|
||||||
|
self.panels = panels;
|
||||||
|
}
|
||||||
|
tui.resize();
|
||||||
|
}
|
||||||
fn get_panel(self: *Self, name_: []const u8) ?*Widget {
|
fn get_panel(self: *Self, name_: []const u8) ?*Widget {
|
||||||
if (self.panels) |panels|
|
if (self.panels) |panels|
|
||||||
for (panels.widgets.items) |*w|
|
for (panels.widgets.items) |*w|
|
||||||
|
|
@ -911,6 +934,8 @@ const cmds = struct {
|
||||||
try self.toggle_panel_view(keybind_view, .toggle)
|
try self.toggle_panel_view(keybind_view, .toggle)
|
||||||
else if (self.is_panel_view_showing(input_view))
|
else if (self.is_panel_view_showing(input_view))
|
||||||
try self.toggle_panel_view(input_view, .toggle)
|
try self.toggle_panel_view(input_view, .toggle)
|
||||||
|
else if (self.is_panel_view_showing(terminal_view))
|
||||||
|
try self.toggle_panel_view(terminal_view, .toggle)
|
||||||
else
|
else
|
||||||
try self.toggle_panel_view(logview, .toggle);
|
try self.toggle_panel_view(logview, .toggle);
|
||||||
}
|
}
|
||||||
|
|
@ -946,6 +971,16 @@ const cmds = struct {
|
||||||
}
|
}
|
||||||
pub const show_inspector_view_meta: Meta = .{};
|
pub const show_inspector_view_meta: Meta = .{};
|
||||||
|
|
||||||
|
pub fn toggle_terminal_view(self: *Self, _: Ctx) Result {
|
||||||
|
try self.toggle_panel_view(terminal_view, .toggle);
|
||||||
|
}
|
||||||
|
pub const toggle_terminal_view_meta: Meta = .{ .description = "Toggle terminal" };
|
||||||
|
|
||||||
|
pub fn open_terminal(self: *Self, ctx: Ctx) Result {
|
||||||
|
try self.toggle_panel_view_with_args(terminal_view, .enable, ctx);
|
||||||
|
}
|
||||||
|
pub const open_terminal_meta: Meta = .{ .description = "Open terminal", .arguments = &.{.string} };
|
||||||
|
|
||||||
pub fn close_find_in_files_results(self: *Self, _: Ctx) Result {
|
pub fn close_find_in_files_results(self: *Self, _: Ctx) Result {
|
||||||
if (self.file_list_type == .find_in_files)
|
if (self.file_list_type == .find_in_files)
|
||||||
try self.toggle_panel_view(filelist_view, .disable);
|
try self.toggle_panel_view(filelist_view, .disable);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ pub fn load_entries(palette: *Type) !usize {
|
||||||
var longest_hint: usize = 0;
|
var longest_hint: usize = 0;
|
||||||
longest_hint = @max(longest_hint, try add_palette_command(palette, "add_task", hints));
|
longest_hint = @max(longest_hint, try add_palette_command(palette, "add_task", hints));
|
||||||
longest_hint = @max(longest_hint, try add_palette_command(palette, "palette_menu_delete_item", hints));
|
longest_hint = @max(longest_hint, try add_palette_command(palette, "palette_menu_delete_item", hints));
|
||||||
|
longest_hint = @max(longest_hint, try add_palette_command(palette, "run_task_in_terminal", hints));
|
||||||
return longest_hint - @min(longest_hint, longest) + 3;
|
return longest_hint - @min(longest_hint, longest) + 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,13 +130,19 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void {
|
||||||
var entry: Entry = undefined;
|
var entry: Entry = undefined;
|
||||||
var iter = button.opts.label;
|
var iter = button.opts.label;
|
||||||
if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return;
|
if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return;
|
||||||
|
const activate = menu.*.opts.ctx.activate;
|
||||||
|
menu.*.opts.ctx.activate = .normal;
|
||||||
if (entry.command) |command_name| {
|
if (entry.command) |command_name| {
|
||||||
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||||
tp.self_pid().send(.{ "cmd", command_name, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
tp.self_pid().send(.{ "cmd", command_name, .{} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||||
} else {
|
} else {
|
||||||
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||||
project_manager.add_task(entry.label) catch {};
|
project_manager.add_task(entry.label) catch {};
|
||||||
tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
const run_cmd = switch (activate) {
|
||||||
|
.normal => "run_task",
|
||||||
|
.alternate => "run_task_in_terminal",
|
||||||
|
};
|
||||||
|
tp.self_pid().send(.{ "cmd", run_cmd, .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
198
src/tui/terminal_view.zig
Normal file
198
src/tui/terminal_view.zig
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const tp = @import("thespian");
|
||||||
|
const cbor = @import("cbor");
|
||||||
|
const command = @import("command");
|
||||||
|
const vaxis = @import("renderer").vaxis;
|
||||||
|
const shell = @import("shell");
|
||||||
|
|
||||||
|
const Plane = @import("renderer").Plane;
|
||||||
|
const Widget = @import("Widget.zig");
|
||||||
|
const WidgetList = @import("WidgetList.zig");
|
||||||
|
const MessageFilter = @import("MessageFilter.zig");
|
||||||
|
const tui = @import("tui.zig");
|
||||||
|
|
||||||
|
pub const name = @typeName(Self);
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
const widget_type: Widget.Type = .panel;
|
||||||
|
|
||||||
|
const Terminal = vaxis.widgets.Terminal;
|
||||||
|
|
||||||
|
/// Poll interval in microseconds – how often we check the pty for new output.
|
||||||
|
/// 16 ms ≈ 60 Hz; Flow's render loop will coalesce multiple need_render calls.
|
||||||
|
const poll_interval_us: u64 = 16 * std.time.us_per_ms;
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
plane: Plane,
|
||||||
|
vt: Terminal,
|
||||||
|
env: std.process.EnvMap,
|
||||||
|
write_buf: [4096]u8,
|
||||||
|
poll_timer: ?tp.Cancellable = null,
|
||||||
|
|
||||||
|
pub fn create(allocator: Allocator, parent: Plane) !Widget {
|
||||||
|
return create_with_args(allocator, parent, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_with_args(allocator: Allocator, parent: Plane, ctx: command.Context) !Widget {
|
||||||
|
const self = try allocator.create(Self);
|
||||||
|
errdefer allocator.destroy(self);
|
||||||
|
|
||||||
|
const container = try WidgetList.createHStyled(
|
||||||
|
allocator,
|
||||||
|
parent,
|
||||||
|
"panel_frame",
|
||||||
|
.dynamic,
|
||||||
|
widget_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
var plane = try Plane.init(&(Widget.Box{}).opts(name), parent);
|
||||||
|
errdefer plane.deinit();
|
||||||
|
|
||||||
|
var env = try std.process.getEnvMap(allocator);
|
||||||
|
errdefer env.deinit();
|
||||||
|
|
||||||
|
var cmd_arg: []const u8 = "";
|
||||||
|
const argv_msg: ?tp.message = if (ctx.args.match(.{tp.extract(&cmd_arg)}) catch false and cmd_arg.len > 0)
|
||||||
|
try shell.parse_arg0_to_argv(allocator, &cmd_arg)
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
defer if (argv_msg) |msg| allocator.free(msg.buf);
|
||||||
|
|
||||||
|
var argv_list: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||||
|
defer argv_list.deinit(allocator);
|
||||||
|
if (argv_msg) |msg| {
|
||||||
|
var iter = msg.buf;
|
||||||
|
var len = try cbor.decodeArrayHeader(&iter);
|
||||||
|
while (len > 0) : (len -= 1) {
|
||||||
|
var arg: []const u8 = undefined;
|
||||||
|
if (try cbor.matchValue(&iter, cbor.extract(&arg)))
|
||||||
|
try argv_list.append(allocator, arg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try argv_list.append(allocator, env.get("SHELL") orelse "bash");
|
||||||
|
}
|
||||||
|
const argv: []const []const u8 = argv_list.items;
|
||||||
|
const home = env.get("HOME") orelse "/tmp";
|
||||||
|
|
||||||
|
// Use the current plane dimensions for the initial pty size. The plane
|
||||||
|
// starts at 0×0 before the first resize, so use a sensible fallback
|
||||||
|
// so the pty isn't created with a zero-cell screen.
|
||||||
|
const cols: u16 = @intCast(@max(80, plane.dim_x()));
|
||||||
|
const rows: u16 = @intCast(@max(24, plane.dim_y()));
|
||||||
|
|
||||||
|
// write_buf must outlive the Terminal because the pty writer holds a
|
||||||
|
// pointer into it. It lives inside Self so the lifetimes match.
|
||||||
|
self.write_buf = undefined;
|
||||||
|
const vt = try Terminal.init(
|
||||||
|
allocator,
|
||||||
|
argv,
|
||||||
|
&env,
|
||||||
|
.{
|
||||||
|
.winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 },
|
||||||
|
.scrollback_size = 0,
|
||||||
|
.initial_working_directory = blk: {
|
||||||
|
const project = tp.env.get().str("project");
|
||||||
|
break :blk if (project.len > 0) project else home;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&self.write_buf,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.* = .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.plane = plane,
|
||||||
|
.vt = vt,
|
||||||
|
.env = env,
|
||||||
|
.write_buf = undefined, // managed via self.vt's pty_writer pointer
|
||||||
|
.poll_timer = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try self.vt.spawn();
|
||||||
|
|
||||||
|
try tui.message_filters().add(MessageFilter.bind(self, receive_filter));
|
||||||
|
|
||||||
|
container.ctx = self;
|
||||||
|
try container.add(Widget.to(self));
|
||||||
|
|
||||||
|
self.schedule_poll();
|
||||||
|
|
||||||
|
return container.widget();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||||
|
tui.message_filters().remove_ptr(self);
|
||||||
|
if (self.poll_timer) |*t| {
|
||||||
|
t.cancel() catch {};
|
||||||
|
t.deinit();
|
||||||
|
}
|
||||||
|
self.vt.deinit();
|
||||||
|
self.env.deinit();
|
||||||
|
self.plane.deinit();
|
||||||
|
allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(self: *Self, _: *const Widget.Theme) bool {
|
||||||
|
// Drain the vt event queue.
|
||||||
|
while (self.vt.tryEvent()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.exited => {
|
||||||
|
tp.self_pid().send(.{ "cmd", "toggle_terminal_view" }) catch {};
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
.redraw, .bell, .title_change, .pwd_change => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit the terminal's front screen into our vaxis.Window.
|
||||||
|
self.vt.draw(self.allocator, self.plane.window) catch |e| {
|
||||||
|
std.log.err("terminal_view: draw failed: {}", .{e});
|
||||||
|
};
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
|
||||||
|
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
|
||||||
|
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
|
||||||
|
|
||||||
|
const cols: u16 = @intCast(@max(1, pos.w));
|
||||||
|
const rows: u16 = @intCast(@max(1, pos.h));
|
||||||
|
self.vt.resize(.{
|
||||||
|
.rows = rows,
|
||||||
|
.cols = cols,
|
||||||
|
.x_pixel = 0,
|
||||||
|
.y_pixel = 0,
|
||||||
|
}) catch |e| {
|
||||||
|
std.log.err("terminal_view: resize failed: {}", .{e});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pty read thread pushes output into vt asynchronously. We use a
|
||||||
|
// recurring thespian delay_send to wake up every ~16 ms and check whether
|
||||||
|
// new output has arrived, requesting a render frame when it has.
|
||||||
|
fn schedule_poll(self: *Self) void {
|
||||||
|
self.poll_timer = tp.self_pid().delay_send_cancellable(
|
||||||
|
self.allocator,
|
||||||
|
"terminal_view.poll",
|
||||||
|
poll_interval_us,
|
||||||
|
.{"TERMINAL_VIEW_POLL"},
|
||||||
|
) catch null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_filter(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
|
||||||
|
if (try cbor.match(m.buf, .{"TERMINAL_VIEW_POLL"})) {
|
||||||
|
if (self.poll_timer) |*t| {
|
||||||
|
t.deinit();
|
||||||
|
self.poll_timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.vt.dirty)
|
||||||
|
tui.need_render(@src());
|
||||||
|
|
||||||
|
self.schedule_poll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -1483,6 +1483,27 @@ const cmds = struct {
|
||||||
.arguments = &.{.string},
|
.arguments = &.{.string},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn run_task_in_terminal(self: *Self, ctx: Ctx) Result {
|
||||||
|
const expansion = @import("expansion.zig");
|
||||||
|
var task: []const u8 = undefined;
|
||||||
|
if (!try ctx.args.match(.{tp.extract(&task)})) return;
|
||||||
|
const args = expansion.expand_cbor(self.allocator, ctx.args.buf) catch |e| switch (e) {
|
||||||
|
error.NotFound => return error.Stop,
|
||||||
|
else => |e_| return e_,
|
||||||
|
};
|
||||||
|
defer self.allocator.free(args);
|
||||||
|
var cmd: []const u8 = undefined;
|
||||||
|
if (!try cbor.match(args, .{tp.extract(&cmd)}))
|
||||||
|
cmd = task;
|
||||||
|
call_add_task(task);
|
||||||
|
var buf: [tp.max_message_size]u8 = undefined;
|
||||||
|
try command.executeName("open_terminal", try command.fmtbuf(&buf, .{cmd}));
|
||||||
|
}
|
||||||
|
pub const run_task_in_terminal_meta: Meta = .{
|
||||||
|
.description = "Run a task in terminal",
|
||||||
|
.arguments = &.{.string},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn delete_task(_: *Self, ctx: Ctx) Result {
|
pub fn delete_task(_: *Self, ctx: Ctx) Result {
|
||||||
var task: []const u8 = undefined;
|
var task: []const u8 = undefined;
|
||||||
if (!try ctx.args.match(.{tp.extract(&task)}))
|
if (!try ctx.args.match(.{tp.extract(&task)}))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue