Compare commits

..

73 commits

Author SHA1 Message Date
2fd907345a
refactor: add toggle_maximize_panel binding to file_in_files mode 2026-03-29 19:27:48 +02:00
524372ed33
Merge branch 'master' into terminal 2026-03-24 21:36:53 +01:00
42d0e541e8
Merge branch 'master' into terminal 2026-03-23 13:10:45 +01:00
1e037375bd
Merge branch 'master' into terminal 2026-03-03 10:30:54 +01:00
3553fbf0d2
refactor(terminal): merge focus_terminal and open_terminal commands 2026-03-02 17:00:58 +01:00
2f5d4ded3c
refactor: deduplicate toggle_panel_view
By allowing any panel to accept arguments.
2026-03-01 21:50:37 +01:00
c4f6b6c945
refactor(terminal): render terminal panel as unfocused if outer terminal looses focus 2026-03-01 21:21:56 +01:00
581bbdb210
fix(terminal): render software cursor in terminal if enable_terminal_cursor is false 2026-03-01 21:21:53 +01:00
ce240c534c
fix(tv): fix windows gui build 2026-03-01 19:34:33 +01:00
8027096f3e
fix(vt): detect windows pty child exit via registerWaitForSingleObject 2026-03-01 19:31:11 +01:00
a21b1318ed
fix(terminal): file_stream.init call for pty_out should be in the pty actor 2026-03-01 18:13:55 +01:00
97f8d024c6
feat(terminal): initial version of conpty windows support 2026-02-28 21:49:56 +01:00
646db3b374
fix(terminal): build terminal on macos and freebsd 2026-02-28 20:40:54 +01:00
b1e13f036d
feat(terminal): report mouse events to terminal applications 2026-02-27 23:15:04 +01:00
df5c426383
fix(terminal): set terminal hold when running tasks in terminal 2026-02-27 20:21:04 +01:00
5f9b7b7c13
fix(terminal): run posix shell if no command specified and no SHELL found 2026-02-27 20:11:00 +01:00
21b7995393
feat(terminal): add terminal_on_exit config option 2026-02-27 16:29:18 +01:00
29c3424913
fix(terminal): reset terminal if closed when exited 2026-02-27 16:07:01 +01:00
0a37c2b05b
refactor(terminal): close terminal on escape keypress if exited 2026-02-27 16:06:23 +01:00
57aae0d45c
feat(terminal): add close_terminal command 2026-02-27 16:05:40 +01:00
fc78e8cf02
refactor: add argv module with helper functions 2026-02-27 14:07:06 +01:00
a35edeaa9b
refactor(terminal): add re-run command message 2026-02-27 13:00:08 +01:00
94f6b342fa
fix(terminal): setup SIGCHLD handler to catch exits with no writes 2026-02-27 12:57:43 +01:00
632a7c4453
refactor(terminal): add pty read_error handler 2026-02-27 11:49:12 +01:00
737236db01
fix(terminal): avoid leaking ESC \ 2026-02-26 22:31:41 +01:00
bd507d48e2
fix(terminal): prevent terminal disconnect on invalid UTF-8 2026-02-26 22:26:18 +01:00
d98a40ab9e
refactor(terminal): update libvaxis for various terminal features and fixes 2026-02-26 22:21:09 +01:00
49d4cda7ef
refactor(terminal): add detailed exit debug logging 2026-02-26 22:18:58 +01:00
7e7cb511a8
refactor(terminal): handle color_change events 2026-02-26 22:12:47 +01:00
4bba8d9715
feat(terminal): handle OSC 52 clipboard requests 2026-02-26 21:30:40 +01:00
885c9682eb
refactor(terminal): add merged move_tab_next/prev_or_scroll_terminal_down/up commands 2026-02-26 21:18:28 +01:00
8a3cd776e9
refactor(terminal): update libvaxis for Terminal 2026-02-26 20:57:03 +01:00
424fd3efc3
refactor(terminal): add terminal to home screen menu 2026-02-26 20:54:06 +01:00
519d8dd886
feat(terminal): support OSC 10/11 query terminal fg/bg color 2026-02-26 20:52:06 +01:00
f68102e448
feat: open terminal as default panel 2026-02-26 20:31:16 +01:00
770fa884cd
feat: add keybinds for toggle_maximize_panel 2026-02-26 20:30:56 +01:00
871d40f906
refactor: add toggle_panel_maximize command 2026-02-26 20:30:37 +01:00
05cba52397
fix: crash in View when panel is maximized 2026-02-26 20:29:36 +01:00
ec8379ce51
refactor(terminal): add restart keybind to terminal mode 2026-02-25 21:48:58 +01:00
598c2a58aa
refactor(terminal): add some debug logs for pty lifetime tracking 2026-02-25 21:48:49 +01:00
3ad37b3b70
refactor(terminal): shutdown terminal on exit or project switch 2026-02-25 21:18:58 +01:00
35ef58d0e1
refactor(terminal): fix vt cursor during scrollback 2026-02-25 21:12:35 +01:00
4affdf5688
refactor(terminal): add keyboard scrolling keybinds 2026-02-25 20:59:22 +01:00
4157638892
refactor(terminal): add force quit keybinding to terminal mode 2026-02-25 20:34:49 +01:00
f88f779410
refactor(terminal): add scrollback size configuration option 2026-02-25 20:34:49 +01:00
3e265dade5
feat(terminal): add scrollback support 2026-02-25 20:34:42 +01:00
69b0885f4b
fix(terminal): properly catch child EOF
And be much more explicit about error handling.
2026-02-25 19:12:49 +01:00
61a509cf2f
refactor(terminal): persist terminal state across terminal view/show operations 2026-02-25 16:31:29 +01:00
316b65a0f7
refactor: add support for dotnet test output file links 2026-02-25 14:57:48 +01:00
582d3d1066
refactor(terminal): reduce terminal logging in release builds 2026-02-25 12:31:39 +01:00
45de943d84
refactor(terminal): store/restore keyboard_focus when entering/exiting overlay modes 2026-02-25 12:26:09 +01:00
f17ceb282a
refactor(terminal): add run_task keybind to terminal mode 2026-02-25 12:26:08 +01:00
7e01eae389
refactor(terminal): add palette keybindings to terminal mode 2026-02-25 12:26:08 +01:00
ee7a3ed2ce
refactor(terminal): add more terminal mode keybinds 2026-02-25 12:26:08 +01:00
330d2b1f66
fix(terminal): focus switching 2026-02-25 12:26:08 +01:00
aff2a7919b
fix: don't dispatch mouse and widget events to keyboard_focus widget 2026-02-25 12:26:08 +01:00
7d51b09aac
refactor(terminal): add click-to-focus handling for terminal 2026-02-25 12:26:08 +01:00
558c59368b
refactor(terminal): report child exit status 2026-02-25 12:26:08 +01:00
5c2ae84602
refactor(terminal): render terminal unfocused state 2026-02-25 12:26:08 +01:00
43b46d179f
fix: don't insert when in terminal mode 2026-02-25 12:26:08 +01:00
cc6c84be15
refactor: add flow mode keybinds to focus_terminal 2026-02-25 12:26:08 +01:00
341c652333
refactor: process terminal mode keybindings 2026-02-25 12:26:08 +01:00
3d81631679
refactor: add binding set on_match_failure nothing mode 2026-02-25 12:26:08 +01:00
6643341574
refactor: support direct calling of keybind.BindingSet 2026-02-25 12:26:08 +01:00
613b95c2af
refactor: make focus_termimal a toggle 2026-02-25 12:26:08 +01:00
3d1658541a
refactor: allow tui.keyboard_focus widget to ignore input 2026-02-25 12:26:08 +01:00
7de0d27a54
refactor(terminal): update libvaxis for external pty read loop support 2026-02-25 12:26:08 +01:00
f8dd9f85b6
refactor(terminal): move pty input processing to an actor 2026-02-25 12:26:07 +01:00
d423696e7e
refactor(terminal): handle title_change and pwd_change events 2026-02-25 12:26:07 +01:00
367c532596
refactor(terminal): route input to terminal_view when it is focused 2026-02-25 12:25:54 +01:00
9a68918ada
refactor: make Widget.focus/unfocus const 2026-02-24 18:26:36 +01:00
f1a8efa318
feat: add {{project_name}} expansion variable 2026-02-24 17:30:13 +01:00
ff0495a265
feat: add basic terminal_view 2026-02-24 17:10:32 +01:00
34 changed files with 1326 additions and 723 deletions

View file

@ -392,6 +392,13 @@ pub fn build_exe(
},
});
const argv_mod = b.createModule(.{
.root_source_file = b.path("src/argv.zig"),
.imports = &.{
.{ .name = "cbor", .module = cbor_mod },
},
});
const lsp_config_mod = b.createModule(.{
.root_source_file = b.path("src/lsp_config.zig"),
.imports = &.{
@ -660,6 +667,7 @@ pub fn build_exe(
.{ .name = "project_manager", .module = project_manager_mod },
.{ .name = "syntax", .module = syntax_mod },
.{ .name = "text_manip", .module = text_manip_mod },
.{ .name = "argv", .module = argv_mod },
.{ .name = "Buffer", .module = Buffer_mod },
.{ .name = "keybind", .module = keybind_mod },
.{ .name = "shell", .module = shell_mod },
@ -709,6 +717,7 @@ pub fn build_exe(
exe.root_module.addImport("cbor", cbor_mod);
exe.root_module.addImport("config", config_mod);
exe.root_module.addImport("text_manip", text_manip_mod);
exe.root_module.addImport("argv", argv_mod);
exe.root_module.addImport("Buffer", Buffer_mod);
exe.root_module.addImport("tui", tui_mod);
exe.root_module.addImport("thespian", thespian_mod);
@ -759,6 +768,7 @@ pub fn build_exe(
check_exe.root_module.addImport("cbor", cbor_mod);
check_exe.root_module.addImport("config", config_mod);
check_exe.root_module.addImport("text_manip", text_manip_mod);
check_exe.root_module.addImport("argv", argv_mod);
check_exe.root_module.addImport("Buffer", Buffer_mod);
check_exe.root_module.addImport("tui", tui_mod);
check_exe.root_module.addImport("thespian", thespian_mod);

View file

@ -6,8 +6,8 @@
.dependencies = .{
.syntax = .{
.url = "git+https://github.com/neurocyte/flow-syntax?ref=master#7b1fd3a97f00aba3a95cc65b95f34162347ed1ea",
.hash = "flow_syntax-0.7.2-X8jOoQhWAQBPt1rBRmttAGI0Z2QC-hCSZuoBZoZgr6Vv",
.url = "git+https://github.com/neurocyte/flow-syntax?ref=master#56929f0c523b59153e17919be2cd09d8bef32cd0",
.hash = "flow_syntax-0.7.2-X8jOoeFTAQBeP2Tn08Tw1jsMdifLEDBgPLqPqNelAupy",
},
.flags = .{
.url = "git+https://github.com/neurocyte/flags?ref=main#984b27948da3e4e40a253f76c85b51ec1a9ada11",
@ -22,16 +22,16 @@
.hash = "thespian-0.0.1-owFOjlgiBgC8w4XqkCOegxz5vMy6kNErcssWQWf2QHeE",
},
.themes = .{
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-750400d02ea8cacaabc869cd4d34dcebf04a53c8/flow-themes.tar.gz",
.hash = "N-V-__8AAEWxJQAyUV_rvRIWHB8EhIBxpQXqCB68SpilIjEt",
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-c6c7f18cfb2e3945cd0b71dab24271465074dbc3/flow-themes.tar.gz",
.hash = "N-V-__8AAOKzJACguNxU76WX9M7RIhOYGuLnlasJ1-GDdhqT",
},
.fuzzig = .{
.url = "https://github.com/fjebaker/fuzzig/archive/4251fe4230d38e721514394a485db62ee1667ff3.tar.gz",
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
},
.vaxis = .{
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#1f6c7222f59607bff0ee8d7c6a0637a05bceffcd",
.hash = "vaxis-0.5.1-BWNV_CNLCQDmr-D_UzqGRAngktQt7hiGTRf1gyozwxcG",
.url = "git+https://github.com/neurocyte/libvaxis?ref=main#cecc97d9ff8da9df13499da0d0b19c5cd18742c3",
.hash = "vaxis-0.5.1-BWNV_BcgCgDG3wpSPxCHxaRAZukEfnnKrBa-52zjnjex",
},
.zeit = .{
.url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",

33
src/argv.zig Normal file
View file

@ -0,0 +1,33 @@
const std = @import("std");
/// Write a `[]const []const u8` argv array as a space-separated command string.
/// Args that contain spaces are wrapped in double-quotes.
/// Writes nothing if argv is null or empty.
pub fn write(writer: *std.Io.Writer, argv: ?[]const []const u8) error{WriteFailed}!usize {
const args = argv orelse return 0;
var count: usize = 0;
for (args, 0..) |arg, i| {
if (i > 0) {
try writer.writeByte(' ');
count += 1;
}
const needs_quote = std.mem.indexOfScalar(u8, arg, ' ') != null;
if (needs_quote) {
try writer.writeByte('"');
count += 1;
}
try writer.writeAll(arg);
count += arg.len;
if (needs_quote) {
try writer.writeByte('"');
count += 1;
}
}
return count;
}
/// Return the display length of an argv array rendered by write_argv.
pub fn len(argv: ?[]const []const u8) usize {
var discard: std.Io.Writer.Discarding = .init(&.{});
return write(&discard.writer, argv) catch return 0;
}

View file

@ -109,8 +109,8 @@ fn clamp_row(self: *Self, cursor: *const Cursor, abs: bool, bottom_offset: usize
}
if (cursor.row < self.row) {
self.row = 0;
} else if (cursor.row > self.row + self.rows - bottom_min_border_distance) {
self.row = cursor.row + bottom_min_border_distance - self.rows;
} else if (cursor.row > self.row + self.rows -| bottom_min_border_distance) {
self.row = cursor.row + bottom_min_border_distance -| self.rows;
}
}

View file

@ -12,6 +12,8 @@ gutter_width_minimum: usize = 4,
gutter_width_maximum: usize = 8,
enable_terminal_cursor: bool = true,
enable_terminal_color_scheme: bool = false,
terminal_scrollback_size: u16 = 500,
terminal_on_exit: TerminalOnExit = .hold_on_error,
enable_sgr_pixel_mode_support: bool = true,
enable_modal_dim: bool = true,
highlight_current_line: bool = true,
@ -247,3 +249,9 @@ pub const AgeFormat = enum {
short,
long,
};
pub const TerminalOnExit = enum {
hold_on_error,
close,
hold,
};

View file

@ -43,6 +43,11 @@ pub fn parse(link: []const u8) error{InvalidFileLink}!Dest {
file.path = link;
break :blk null;
};
} else if (line_.len > 5 and std.mem.eql(u8, "line ", line_[0..5])) {
file.line = std.fmt.parseInt(usize, line_[5..], 10) catch blk: {
file.path = link;
break :blk null;
};
} else {
file.line = std.fmt.parseInt(usize, line_, 10) catch blk: {
file.path = link;

View file

@ -116,7 +116,7 @@ pub const make = .{};
pub const markdown = .{
.language_server = .{ "marksman", "server" },
.formatter = .{ "prettier", "--parser", "markdown", "--prose-wrap", "always", "--print-width", "{{reflow_width}}" },
.formatter = .{ "prettier", "--parser", "markdown" },
};
pub const @"markdown-inline" = .{};
@ -183,9 +183,7 @@ pub const python = .{
pub const regex = .{};
pub const rpmspec = .{
.language_server = .{ "python3", "-mrpm_spec_language_server", "--stdio" },
};
pub const rpmspec = .{};
pub const rst = .{
.language_server = .{"esbonio"},
@ -216,10 +214,7 @@ pub const verilog = .{
.formatter = .{ "verible-verilog-format", "-" },
};
pub const toml = .{
.language_server = .{ "tombi", "lsp" },
.formatter = .{ "tombi", "format" },
};
pub const toml = .{};
pub const typescript = .{
.language_server = .{ "typescript-language-server", "--stdio" },
@ -232,21 +227,13 @@ pub const typst = .{
pub const uxntal = .{};
pub const v = .{
.language_server = .{"v-analyzer"},
.formatter = .{ "v", "fmt", "-" },
};
pub const vim = .{};
pub const xml = .{
.formatter = .{ "xmllint", "--format", "-" },
};
pub const yaml = .{
.language_server = .{ "yaml-language-server", "--stdio" },
.formatter = .{ "prettier", "--parser", "yaml" },
};
pub const yaml = .{};
pub const zig = .{
.language_server = .{"zls"},

View file

@ -23,7 +23,9 @@
["ctrl+6", "focus_split", 5],
["ctrl+7", "focus_split", 6],
["ctrl+8", "focus_split", 7],
["ctrl+`", "open_terminal"],
["ctrl+j", "toggle_panel"],
["ctrl+shift+j", "toggle_maximize_panel"],
["ctrl+q", "quit"],
["ctrl+w", "close_split"],
["ctrl+o", "open_file"],
@ -61,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"],
@ -349,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"],
@ -517,6 +520,7 @@
["shift+f1", "scroll_keybind_hints"],
["ctrl+alt+?", "scroll_keybind_hints"],
["alt+f9", "panel_next_widget_style"],
["ctrl+shift+j", "toggle_maximize_panel"],
["ctrl+q", "quit"],
["ctrl+v", "system_paste"],
["ctrl+u", "mini_mode_reset"],
@ -581,5 +585,30 @@
["enter", "mini_mode_select"],
["backspace", "mini_mode_delete_backwards"]
]
},
"terminal": {
"on_match_failure": "nothing",
"press": [
["ctrl+1", "focus_split", 0],
["ctrl+2", "focus_split", 1],
["ctrl+3", "focus_split", 2],
["ctrl+4", "focus_split", 3],
["ctrl+5", "focus_split", 4],
["ctrl+6", "focus_split", 5],
["ctrl+7", "focus_split", 6],
["ctrl+8", "focus_split", 7],
["ctrl+`", "unfocus_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"],
["alt+f9", "panel_next_widget_style"],
["ctrl+shift+q", "quit_without_saving"],
["ctrl+alt+shift+r", "restart"]
]
}
}

View file

@ -81,62 +81,14 @@
["dgg", "cut_buffer_begin"],
["\"_dd", "delete_line"],
["diw", "cut_inside_word"],
["di(", "cut_inside_parentheses"],
["di)", "cut_inside_parentheses"],
["di[", "cut_inside_square_brackets"],
["di]", "cut_inside_square_brackets"],
["di{", "cut_inside_braces"],
["di}", "cut_inside_braces"],
["daw", "cut_around_word"],
["da(", "cut_around_parentheses"],
["da)", "cut_around_parentheses"],
["da[", "cut_around_square_brackets"],
["da]", "cut_around_square_brackets"],
["da{", "cut_around_braces"],
["da}", "cut_around_braces"],
["cc", ["enter_mode", "insert"], ["cut_internal_vim"]],
["C", ["enter_mode", "insert"], ["cut_to_end_vim"]],
["D", "cut_to_end_vim"],
["cw", ["enter_mode", "insert"], ["cut_word_right_vim"]],
["cb", ["enter_mode", "insert"], ["cut_word_left_vim"]],
["ciw", ["enter_mode", "insert"], ["cut_inside_word"]],
["ci(", ["enter_mode", "insert"], ["cut_inside_parentheses"]],
["ci)", ["enter_mode", "insert"], ["cut_inside_parentheses"]],
["ci[", ["enter_mode", "insert"], ["cut_inside_square_brackets"]],
["ci]", ["enter_mode", "insert"], ["cut_inside_square_brackets"]],
["ci{", ["enter_mode", "insert"], ["cut_inside_braces"]],
["ci}", ["enter_mode", "insert"], ["cut_inside_braces"]],
["caw", ["enter_mode", "insert"], ["cut_around_word"]],
["ca(", ["enter_mode", "insert"], ["cut_around_parentheses"]],
["ca)", ["enter_mode", "insert"], ["cut_around_parentheses"]],
["ca[", ["enter_mode", "insert"], ["cut_around_square_brackets"]],
["ca]", ["enter_mode", "insert"], ["cut_around_square_brackets"]],
["ca{", ["enter_mode", "insert"], ["cut_around_braces"]],
["ca}", ["enter_mode", "insert"], ["cut_around_braces"]],
["yy", ["copy_line_internal_vim"], ["cancel"]],
["yiw", ["copy_inside_word"], ["cancel"]],
["yi(", ["copy_inside_parentheses"], ["cancel"]],
["yi)", ["copy_inside_parentheses"], ["cancel"]],
["yi[", ["copy_inside_square_brackets"], ["cancel"]],
["yi]", ["copy_inside_square_brackets"], ["cancel"]],
["yi{", ["copy_inside_braces"], ["cancel"]],
["yi}", ["copy_inside_braces"], ["cancel"]],
["yaw", ["copy_around_word"], ["cancel"]],
["ya(", ["copy_around_parentheses"], ["cancel"]],
["ya)", ["copy_around_parentheses"], ["cancel"]],
["ya[", ["copy_around_square_brackets"], ["cancel"]],
["ya]", ["copy_around_square_brackets"], ["cancel"]],
["ya{", ["copy_around_braces"], ["cancel"]],
["ya}", ["copy_around_braces"], ["cancel"]],
["<C-u>", "move_scroll_half_page_up_vim"],
["<C-d>", "move_scroll_half_page_down_vim"],
@ -207,22 +159,6 @@
["B", "select_word_left"],
["e", "select_word_right_end_vim"],
["iw", "select_inside_word"],
["i(", "select_inside_parentheses"],
["i)", "select_inside_parentheses"],
["i[", "select_inside_square_brackets"],
["i]", "select_inside_square_brackets"],
["i{", "select_inside_braces"],
["i}", "select_inside_braces"],
["aw", "select_around_word"],
["a(", "select_around_parentheses"],
["a)", "select_around_parentheses"],
["a[", "select_around_square_brackets"],
["a]", "select_around_square_brackets"],
["a{", "select_around_braces"],
["a}", "select_around_braces"],
["^", "smart_move_begin"],
["$", "select_end"],
[":", "open_command_palette"],

View file

@ -485,7 +485,7 @@ const BindingSet = struct {
deinit_command: ?Command = null,
const KeySyntax = enum { flow, vim };
const OnMatchFailure = enum { insert, ignore };
const OnMatchFailure = enum { insert, ignore, nothing };
fn load(allocator: std.mem.Allocator, namespace_name: []const u8, config_section: []const u8, mode_bindings: std.json.Value, fallback: ?*const BindingSet, namespace: *Namespace) (error{ OutOfMemory, WriteFailed } || parse_flow.ParseError || parse_vim.ParseError || std.json.ParseFromValueError)!@This() {
var self: @This() = .{ .name = undefined, .config_section = config_section, .selection_style = undefined };
@ -667,7 +667,7 @@ const BindingSet = struct {
}
}
fn receive(self: *const @This(), _: tp.pid_ref, m: tp.message) error{Exit}!bool {
pub fn receive(self: *const @This(), _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var event: input.Event = 0;
var keypress: input.Key = 0;
var keypress_shifted: input.Key = 0;
@ -696,6 +696,7 @@ const BindingSet = struct {
}
for (binding.commands) |*cmd| try cmd.execute();
return true;
}
} else if (try m.match(.{"F"})) {
self.flush() catch |e| return tp.exit_error(e, @errorReturnTrace());
@ -786,6 +787,7 @@ const BindingSet = struct {
else
log_keyhints_message(),
.ignore => log_keyhints_message(),
.nothing => {},
}
globals.current_sequence.clearRetainingCapacity();
globals.current_sequence_egc.clearRetainingCapacity();

View file

@ -3,6 +3,7 @@ const file_type_config = @import("file_type_config");
const text_manip = @import("text_manip");
const write_string = text_manip.write_string;
const write_padding = text_manip.write_padding;
const argv = @import("argv");
const builtin = @import("builtin");
const RGB = @import("color").RGB;
@ -22,9 +23,9 @@ pub fn list(allocator: std.mem.Allocator, writer: *std.io.Writer, tty_config: st
for (file_type_config.get_all_names()) |file_type_name| {
const file_type = try file_type_config.get(file_type_name) orelse unreachable;
max_language_len = @max(max_language_len, file_type.name.len);
max_langserver_len = @max(max_langserver_len, args_string_length(file_type.language_server));
max_formatter_len = @max(max_formatter_len, args_string_length(file_type.formatter));
max_extensions_len = @max(max_extensions_len, args_string_length(file_type.extensions));
max_langserver_len = @max(max_langserver_len, argv.len(file_type.language_server));
max_formatter_len = @max(max_formatter_len, argv.len(file_type.formatter));
max_extensions_len = @max(max_extensions_len, argv.len(file_type.extensions));
}
try tty_config.setColor(writer, .yellow);
@ -43,59 +44,42 @@ pub fn list(allocator: std.mem.Allocator, writer: *std.io.Writer, tty_config: st
try tty_config.setColor(writer, .reset);
try writer.writeAll(" ");
try write_string(writer, file_type.name, max_language_len + 1);
try write_segmented(writer, file_type.extensions, ",", max_extensions_len + 1, tty_config);
{
const exts = file_type.extensions orelse &.{};
var ext_len: usize = 0;
for (exts, 0..) |ext, i| {
if (i > 0) {
try writer.writeByte(',');
ext_len += 1;
}
try writer.writeAll(ext);
ext_len += ext.len;
}
try tty_config.setColor(writer, .reset);
try write_padding(writer, ext_len, max_extensions_len + 1);
}
if (file_type.language_server) |language_server|
try write_checkmark(writer, bin_path.can_execute(allocator, language_server[0]), tty_config);
try write_segmented(writer, file_type.language_server, " ", max_langserver_len + 1, tty_config);
const len = try argv.write(writer, file_type.language_server);
try tty_config.setColor(writer, .reset);
try write_padding(writer, len, max_langserver_len + 1);
if (file_type.formatter) |formatter|
try write_checkmark(writer, bin_path.can_execute(allocator, formatter[0]), tty_config);
try write_segmented(writer, file_type.formatter, " ", null, tty_config);
_ = try argv.write(writer, file_type.formatter);
try tty_config.setColor(writer, .reset);
try writer.writeAll("\n");
}
}
fn args_string_length(args_: ?[]const []const u8) usize {
const args = args_ orelse return 0;
var len: usize = 0;
var first: bool = true;
for (args) |arg| {
if (first) first = false else len += 1;
len += arg.len;
}
return len;
}
fn write_checkmark(writer: anytype, success: bool, tty_config: std.io.tty.Config) !void {
try tty_config.setColor(writer, if (success) .green else .red);
if (success) try writer.writeAll(success_mark) else try writer.writeAll(fail_mark);
}
fn write_segmented(
writer: anytype,
args_: ?[]const []const u8,
sep: []const u8,
pad: ?usize,
tty_config: std.io.tty.Config,
) !void {
const args = args_ orelse return;
var len: usize = 0;
var first: bool = true;
for (args) |arg| {
if (first) first = false else {
len += 1;
try writer.writeAll(sep);
}
len += arg.len;
try writer.writeAll(arg);
}
try tty_config.setColor(writer, .reset);
if (pad) |pad_| try write_padding(writer, len, pad_);
}
fn setColorRgb(writer: anytype, color: u24) !void {
const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m";
const rgb = RGB.from_u24(color);

View file

@ -882,10 +882,7 @@ pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const
const file_name = get_theme_file_name(theme_name) catch return null;
var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null;
defer file.close();
return file.readToEndAlloc(allocator, 512 * 1024) catch |e| {
std.log.err("Error reading theme file: {t}", .{e});
return null;
};
return file.readToEndAlloc(allocator, 64 * 1024) catch null;
}
pub fn write_theme(theme_name: []const u8, content: []const u8) !void {
@ -898,15 +895,15 @@ pub fn write_theme(theme_name: []const u8, content: []const u8) !void {
pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 {
var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true });
defer dir.close();
var result: std.ArrayList([]const u8) = .empty;
var result = std.ArrayList([]const u8).init(allocator);
var iter = dir.iterateAssumeFirstIteration();
while (try iter.next()) |entry| {
switch (entry.kind) {
.file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))),
.file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))),
else => continue,
}
}
return result.toOwnedSlice(allocator);
return result.toOwnedSlice();
}
pub fn get_config_dir() ConfigDirError![]const u8 {

View file

@ -4,7 +4,7 @@ const cbor = @import("cbor");
const log = @import("log");
const Style = @import("theme").Style;
const Color = @import("theme").Color;
const vaxis = @import("vaxis");
pub const vaxis = @import("vaxis");
const input = @import("input");
const builtin = @import("builtin");
const RGB = @import("color").RGB;
@ -627,9 +627,6 @@ fn filter_mods(key_: vaxis.Key) vaxis.Key {
.shift = key_.mods.shift,
.alt = key_.mods.alt,
.ctrl = key_.mods.ctrl,
.super = key_.mods.super,
.hyper = key_.mods.hyper,
.meta = key_.mods.meta,
};
return key__;
}

View file

@ -3,7 +3,7 @@ pub const log_name = "renderer";
const std = @import("std");
const cbor = @import("cbor");
const vaxis = @import("vaxis");
pub const vaxis = @import("vaxis");
const Style = @import("theme").Style;
const Color = @import("theme").Color;
pub const CursorShape = vaxis.Cell.CursorShape;

View file

@ -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
// TODO: add quotes parsing and workspace variables, etc.
var args: std.ArrayList([]const u8) = .empty;

View file

@ -29,7 +29,6 @@ pub const root = struct {
pub const read_theme = if (@hasDecl(hard_root, "read_theme")) hard_root.read_theme else dummy.read_theme;
pub const write_theme = if (@hasDecl(hard_root, "write_theme")) hard_root.write_theme else dummy.write_theme;
pub const list_themes = if (@hasDecl(hard_root, "list_themes")) hard_root.list_themes else dummy.list_themes;
pub const get_theme_file_name = if (@hasDecl(hard_root, "get_theme_file_name")) hard_root.get_theme_file_name else dummy.get_theme_file_name;
pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit;
@ -110,10 +109,6 @@ const dummy = struct {
pub fn write_theme(_: []const u8, _: []const u8) !void {
@panic("dummy write_theme call");
}
pub fn list_themes(_: std.mem.Allocator) ![]const []const u8 {
@panic("dummy list_themes call");
}
pub fn get_theme_file_name(_: []const u8) ![]const u8 {
@panic("dummy get_theme_file_name call");
}

View file

@ -1,7 +1,6 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const tp = @import("thespian");
const root = @import("soft_root").root;
const Plane = @import("renderer").Plane;
const EventHandler = @import("EventHandler");
@ -10,6 +9,7 @@ const tui = @import("tui.zig");
pub const Box = @import("Box.zig");
pub const Pos = struct { y: i32 = 0, x: i32 = 0 };
pub const Theme = @import("theme");
pub const themes = @import("themes").themes;
pub const scopes = @import("themes").scopes;
pub const Type = @import("config").WidgetType;
pub const StyleTag = @import("config").WidgetStyle;
@ -42,114 +42,6 @@ pub const Layout = union(enum) {
}
};
pub const ThemeInfo = struct {
name: []const u8,
storage: ?std.json.Parsed(Theme) = null,
pub fn get(self: *@This(), allocator: std.mem.Allocator) ?Theme {
if (load_theme_file(allocator, self.name) catch null) |parsed_theme| {
self.storage = parsed_theme;
return self.storage.?.value;
}
for (static_themes) |theme_| {
if (std.mem.eql(u8, theme_.name, self.name))
return theme_;
}
return null;
}
fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) {
return load_theme_file_internal(allocator, theme_name) catch |e| {
std.log.err("Error loading theme '{s}' from file: {t}", .{ theme_name, e });
return e;
};
}
fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Theme) {
const json_str = root.read_theme(allocator, theme_name) orelse return null;
defer allocator.free(json_str);
return try std.json.parseFromSlice(Theme, allocator, json_str, .{ .allocate = .alloc_always });
}
};
var themes_: ?std.StringHashMap(*ThemeInfo) = null;
var theme_names_: ?[]const []const u8 = null;
const static_themes = @import("themes").themes;
fn get_themes(allocator: std.mem.Allocator) *std.StringHashMap(*ThemeInfo) {
if (themes_) |*themes__| return themes__;
const theme_files = root.list_themes(allocator) catch @panic("OOM get_themes");
var themes: std.StringHashMap(*ThemeInfo) = .init(allocator);
defer allocator.free(theme_files);
for (theme_files) |file| {
const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes");
theme_info.* = .{
.name = file,
};
themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes");
}
for (static_themes) |theme_| if (!themes.contains(theme_.name)) {
const theme_info = allocator.create(ThemeInfo) catch @panic("OOM get_themes");
theme_info.* = .{
.name = theme_.name,
};
themes.put(theme_info.name, theme_info) catch @panic("OOM get_themes");
};
themes_ = themes;
return &themes_.?;
}
fn get_theme_names() []const []const u8 {
if (theme_names_) |names_| return names_;
const themes = themes_ orelse return &.{};
var i = get_themes(themes.allocator).iterator();
var names: std.ArrayList([]const u8) = .empty;
while (i.next()) |theme_| names.append(themes.allocator, theme_.value_ptr.*.name) catch @panic("OOM get_theme_names");
std.mem.sort([]const u8, names.items, {}, struct {
fn cmp(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs) == .lt;
}
}.cmp);
theme_names_ = names.toOwnedSlice(themes.allocator) catch @panic("OOM get_theme_names");
return theme_names_.?;
}
pub fn get_theme_by_name(allocator: std.mem.Allocator, name_: []const u8) ?Theme {
const themes = get_themes(allocator);
const theme = themes.get(name_) orelse return null;
return theme.get(allocator);
}
pub fn get_next_theme_by_name(name_: []const u8) []const u8 {
const theme_names = get_theme_names();
var next = false;
for (theme_names) |theme_name| {
if (next)
return theme_name;
if (std.mem.eql(u8, theme_name, name_))
next = true;
}
return theme_names[0];
}
pub fn get_prev_theme_by_name(name_: []const u8) []const u8 {
const theme_names = get_theme_names();
const last = theme_names[theme_names.len - 1];
var prev: ?[]const u8 = null;
for (theme_names) |theme_name| {
if (std.mem.eql(u8, theme_name, name_))
return prev orelse last;
prev = theme_name;
}
return last;
}
pub fn list_themes() []const []const u8 {
return get_theme_names();
}
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,
@ -345,11 +237,11 @@ pub fn walk(self: *const Self, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (self.vtable.walk(self.ptr, walk_ctx, f)) true else f(walk_ctx, self.*);
}
pub fn focus(self: *Self) void {
pub fn focus(self: *const Self) void {
self.vtable.focus(self.ptr);
}
pub fn unfocus(self: *Self) void {
pub fn unfocus(self: *const Self) void {
self.vtable.unfocus(self.ptr);
}

View file

@ -41,7 +41,7 @@ const double_click_time_ms = 350;
const syntax_full_reparse_time_limit = 0; // ms (0 = always use incremental)
const syntax_full_reparse_error_threshold = 3; // number of tree-sitter errors that trigger a full reparse
pub const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536;
const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536;
pub const max_matches = if (builtin.mode == std.builtin.OptimizeMode.Debug) 10_000 else 100_000;
pub const max_match_lines = 15;
@ -2628,15 +2628,7 @@ pub const Editor = struct {
return cursor.test_at(root, is_whitespace, metrics);
}
pub fn is_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return cursor.test_at(root, is_whitespace_or_eol, metrics);
}
pub fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_whitespace, metrics);
}
pub fn is_non_whitespace_or_eol_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_whitespace_or_eol, metrics);
}
@ -3753,7 +3745,7 @@ pub const Editor = struct {
}
pub fn move_cursor_right_until_non_whitespace(root: Buffer.Root, cursor: *Cursor, metrics: Buffer.Metrics) error{Stop}!void {
move_cursor_right_until(root, cursor, is_non_whitespace_or_eol_at_cursor, metrics);
move_cursor_right_until(root, cursor, is_non_whitespace_at_cursor, metrics);
}
pub fn move_word_left(self: *Self, ctx: Context) Result {
@ -7487,7 +7479,7 @@ pub const EditorWidget = struct {
fn mouse_click_event(self: *Self, event: input.Event, btn: input.Mouse, y: c_int, x: c_int, ypx: c_int, xpx: c_int) Result {
if (event != input.event.press) return;
if (!self.focused) switch (btn) {
if (!self.focused or tui.is_keyboard_focused()) switch (btn) {
input.mouse.BUTTON1, input.mouse.BUTTON2, input.mouse.BUTTON3 => _ = tui.set_focus_by_mouse_event(),
else => {},
};

View file

@ -1,5 +1,6 @@
/// Expand variables in arg
/// {{project}} - The path to the current project directory
/// {{project_name}} - The basename of the current project directory
/// {{file}} - The path to the current file
/// {{line}} - The line number of the primary cursor
/// {{column}} - The column of the primary cursor
@ -9,7 +10,6 @@
/// {{selections*}} - All current selections expanded to multiple quoted arguments
/// {{indent_mode}} - The current indent mode ("tabs" or "spaces")
/// {{indent_size}} - The current indent size (in columns)
/// {{reflow_width}} - The current reflow width (in columns)
/// {{blame_commit}} - The blame commit ID at the line number of the primary cursor
pub fn expand(allocator: Allocator, arg: []const u8) Error![]const u8 {
var result: std.Io.Writer.Allocating = .init(allocator);
@ -77,6 +77,13 @@ const functions = struct {
return try allocator.dupe(u8, tp.env.get().str("project"));
}
pub fn project_name(allocator: Allocator) Error![]const u8 {
const project_ = tp.env.get().str("project");
const basename_begin = std.mem.lastIndexOfScalar(u8, project_, std.fs.path.sep);
const basename = if (basename_begin) |begin| project_[begin + 1 ..] else project_;
return try allocator.dupe(u8, basename);
}
pub fn file(allocator: Allocator) Error![]const u8 {
const mv = tui.mainview() orelse return &.{};
const ed = mv.get_active_editor() orelse return &.{};
@ -163,15 +170,6 @@ const functions = struct {
return stream.toOwnedSlice();
}
/// {{reflow_width}} - The current reflow width (in columns)
pub fn reflow_width(allocator: Allocator) Error![]const u8 {
const mv = tui.mainview() orelse return &.{};
const ed = mv.get_active_editor() orelse return &.{};
var stream: std.Io.Writer.Allocating = .init(allocator);
try stream.writer.print("{d}", .{ed.reflow_width orelse tui.config().reflow_width});
return stream.toOwnedSlice();
}
/// {{blame_commit}} - The blame commit ID at the line number of the primary cursor
pub fn blame_commit(allocator: Allocator) Error![]const u8 {
const mv = tui.mainview() orelse return &.{};

View file

@ -57,7 +57,7 @@ const Entry = struct {
pos_type: editor.PosType,
};
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget {
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
self.* = .{

View file

@ -34,6 +34,7 @@ const style = struct {
\\open_recent_project
\\find_in_files
\\open_command_palette
\\open_terminal
\\run_task
\\add_task
\\open_config
@ -52,6 +53,7 @@ const style = struct {
\\open_recent_project
\\find_in_files
\\open_command_palette
\\open_terminal
\\run_task
\\add_task
\\open_config

View file

@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = @import("std").mem.Allocator;
const Plane = @import("renderer").Plane;
const command = @import("command");
const Widget = @import("Widget.zig");
const WidgetList = @import("WidgetList.zig");
const reflow = @import("Buffer").reflow;
@ -19,7 +20,7 @@ widget_type: Widget.Type,
const default_widget_type: Widget.Type = .panel;
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget {
return create_widget_type(allocator, parent, default_widget_type);
}

View file

@ -10,6 +10,7 @@ const cbor = @import("cbor");
const Plane = @import("renderer").Plane;
const EventHandler = @import("EventHandler");
const input = @import("input");
const command = @import("command");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
@ -33,7 +34,7 @@ const Entry = struct {
};
const Buffer = ArrayList(Entry);
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget {
var n = try Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent);
errdefer n.deinit();
const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type);

View file

@ -9,6 +9,7 @@ const Plane = @import("renderer").Plane;
const style = @import("renderer").style;
const styles = @import("renderer").styles;
const EventHandler = @import("EventHandler");
const command = @import("command");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
@ -25,7 +26,7 @@ last_node: usize = 0,
const Self = @This();
const widget_type: Widget.Type = .panel;
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget {
const editor = tui.get_active_editor() orelse return error.NotFound;
const self = try allocator.create(Self);
errdefer allocator.destroy(self);

View file

@ -10,6 +10,7 @@ const cbor = @import("cbor");
const Plane = @import("renderer").Plane;
const input = @import("input");
const command = @import("command");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
@ -33,7 +34,7 @@ const Entry = struct {
};
const Buffer = ArrayList(Entry);
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget {
var n = try Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent);
errdefer n.deinit();
const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type);

View file

@ -6,6 +6,7 @@ const array_list = @import("std").array_list;
const tp = @import("thespian");
const cbor = @import("cbor");
const command = @import("command");
const Plane = @import("renderer").Plane;
@ -39,7 +40,7 @@ const Level = enum {
err,
};
pub fn create(allocator: Allocator, parent: Plane) !Widget {
pub fn create(allocator: Allocator, parent: Plane, _: 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);

View file

@ -34,6 +34,7 @@ const filelist_view = @import("filelist_view.zig");
const info_view = @import("info_view.zig");
const input_view = @import("inputview.zig");
const keybind_view = @import("keybindview.zig");
const terminal_view = @import("terminal_view.zig");
const Self = @This();
const Commands = command.Collection(cmds);
@ -58,6 +59,7 @@ buffer_manager: Buffer.Manager,
find_in_files_state: enum { init, adding, done } = .done,
file_list_type: FileListType = .find_in_files,
panel_height: ?usize = null,
panel_maximized: bool = false,
symbols: std.ArrayListUnmanaged(u8) = .empty,
symbols_complete: bool = true,
closing_project: bool = false,
@ -124,6 +126,7 @@ pub fn create(allocator: std.mem.Allocator) CreateError!Widget {
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
self.close_all_panel_views();
terminal_view.shutdown(allocator);
self.commands.deinit();
self.widgets.deinit(allocator);
self.symbols.deinit(allocator);
@ -258,6 +261,10 @@ pub fn handle_resize(self: *Self, pos: Box) void {
if (self.panel_height) |h| if (h >= self.box().h) {
self.panel_height = null;
};
if (self.panel_maximized) {
if (self.panels) |panels|
panels.layout_ = .{ .static = self.box().h -| 1 };
}
self.widgets.handle_resize(pos);
self.floating_views.resize(pos);
}
@ -279,6 +286,7 @@ fn bottom_bar_primary_drag(self: *Self, y: usize) tp.result {
};
const h = self.plane.dim_y();
self.panel_height = @max(1, h - @min(h, y + 1));
self.panel_maximized = false;
panels.layout_ = .{ .static = self.panel_height.? };
if (self.panel_height == 1) {
self.panel_height = null;
@ -290,7 +298,13 @@ pub fn get_panel_height(self: *Self) usize {
return self.panel_height orelse self.box().h / 5;
}
fn toggle_panel_view(self: *Self, view: anytype, mode: enum { toggle, enable, disable }) !void {
pub const PanelToggleMode = enum { toggle, enable, disable };
fn toggle_panel_view(self: *Self, view: anytype, mode: PanelToggleMode) !void {
return self.toggle_panel_view_with_args(view, mode, .{});
}
fn toggle_panel_view_with_args(self: *Self, view: anytype, mode: PanelToggleMode, ctx: command.Context) !void {
if (self.panels) |panels| {
if (self.get_panel(@typeName(view))) |w| {
if (mode != .enable) {
@ -302,12 +316,12 @@ fn toggle_panel_view(self: *Self, view: anytype, mode: enum { toggle, enable, di
}
} else {
if (mode != .disable)
try panels.add(try view.create(self.allocator, self.widgets.plane));
try panels.add(try view.create(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(self.allocator, self.widgets.plane));
try panels.add(try view.create(self.allocator, self.widgets.plane, ctx));
self.panels = panels;
}
tui.resize();
@ -475,6 +489,7 @@ const cmds = struct {
{
self.closing_project = true;
defer self.closing_project = false;
terminal_view.shutdown(self.allocator);
try close_splits(self, .{});
try self.close_all_editors();
self.delete_all_buffers();
@ -911,11 +926,29 @@ const cmds = struct {
try self.toggle_panel_view(keybind_view, .toggle)
else if (self.is_panel_view_showing(input_view))
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
try self.toggle_panel_view(logview, .toggle);
try open_terminal(self, .{});
}
pub const toggle_panel_meta: Meta = .{ .description = "Toggle panel" };
pub fn toggle_maximize_panel(self: *Self, _: Ctx) Result {
const panels = self.panels orelse return;
const max_h = self.box().h -| 1;
if (self.panel_maximized) {
// Restore previous height
self.panel_maximized = false;
panels.layout_ = .{ .static = self.get_panel_height() };
} else {
// Maximize: fill screen minus status bar
self.panel_maximized = true;
panels.layout_ = .{ .static = max_h };
}
tui.resize();
}
pub const toggle_maximize_panel_meta: Meta = .{ .description = "Toggle maximize panel" };
pub fn toggle_logview(self: *Self, _: Ctx) Result {
try self.toggle_panel_view(logview, .toggle);
}
@ -946,6 +979,53 @@ const cmds = struct {
}
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 {
const have_args = ctx.args.buf.len > 0 and try ctx.args.match(.{ tp.string, tp.more });
if (have_args and terminal_view.is_vt_running()) {
var msg: std.Io.Writer.Allocating = .init(self.allocator);
defer msg.deinit();
try msg.writer.writeAll("terminal is already running '");
try terminal_view.get_running_cmd(&msg.writer);
try msg.writer.writeAll("'");
return tp.exit(msg.written());
}
if (terminal_view.is_vt_running()) if (self.get_panel_view(terminal_view)) |vt| {
std.log.debug("open_terminal: toggle_focus", .{});
vt.toggle_focus();
return;
};
var buf: [tp.max_message_size]u8 = undefined;
std.log.debug("open_terminal: {s}", .{if (ctx.args.buf.len > 0) ctx.args.to_json(&buf) catch "(error)" else "(none)"});
if (self.get_panel_view(terminal_view)) |vt| {
try vt.run_cmd(ctx);
} else {
try self.toggle_panel_view_with_args(terminal_view, .enable, ctx);
if (self.get_panel_view(terminal_view)) |vt|
vt.focus();
}
}
pub const open_terminal_meta: Meta = .{ .description = "Open terminal" };
pub fn unfocus_terminal(self: *Self, _: Ctx) Result {
if (self.get_panel_view(terminal_view)) |vt|
vt.toggle_focus();
}
pub const unfocus_terminal_meta: Meta = .{};
pub fn close_terminal(self: *Self, _: Ctx) Result {
if (self.get_panel_view(terminal_view)) |_|
try self.toggle_panel_view(terminal_view, .disable);
}
pub const close_terminal_meta: Meta = .{ .description = "Close terminal" };
pub fn close_find_in_files_results(self: *Self, _: Ctx) Result {
if (self.file_list_type == .find_in_files)
try self.toggle_panel_view(filelist_view, .disable);
@ -1522,6 +1602,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;
@ -1780,6 +1876,7 @@ pub fn focus_view_by_widget(self: *Self, w: Widget) tui.FocusAction {
}
pub fn focus_view(self: *Self, n: usize) !void {
tui.clear_keyboard_focus();
if (n == self.active_view) return;
if (n > self.views.widgets.items.len) return;
if (n == self.views.widgets.items.len)

View file

@ -87,9 +87,8 @@ pub fn load_entries(self: *Type) !usize {
const less_fn = struct {
fn less_fn(_: void, lhs: Entry, rhs: Entry) bool {
const sort_text_equal = std.mem.eql(u8, lhs.sort_text, rhs.sort_text);
const lhs_str = if (!sort_text_equal and lhs.sort_text.len > 0) lhs.sort_text else lhs.label;
const rhs_str = if (!sort_text_equal and rhs.sort_text.len > 0) rhs.sort_text else rhs.label;
const lhs_str = if (lhs.sort_text.len > 0) lhs.sort_text else lhs.label;
const rhs_str = if (rhs.sort_text.len > 0) rhs.sort_text else rhs.label;
return std.mem.order(u8, lhs_str, rhs_str) == .lt;
}
}.less_fn;

View file

@ -69,9 +69,8 @@ pub fn load_entries(palette: *Type) !usize {
const less_fn = struct {
fn less_fn(_: void, lhs: Entry, rhs: Entry) bool {
const sort_text_equal = std.mem.eql(u8, lhs.sort_text, rhs.sort_text);
const lhs_str = if (!sort_text_equal and lhs.sort_text.len > 0) lhs.sort_text else lhs.label;
const rhs_str = if (!sort_text_equal and rhs.sort_text.len > 0) rhs.sort_text else rhs.label;
const lhs_str = if (lhs.sort_text.len > 0) lhs.sort_text else lhs.label;
const rhs_str = if (rhs.sort_text.len > 0) rhs.sort_text else rhs.label;
return std.mem.order(u8, lhs_str, rhs_str) == .lt;
}
}.less_fn;

View file

@ -38,6 +38,7 @@ pub fn load_entries(palette: *Type) !usize {
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, "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;
}
@ -129,13 +130,18 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void {
var entry: Entry = undefined;
var iter = button.opts.label;
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| {
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);
} else {
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 {};
tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
(switch (activate) {
.normal => tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }),
.alternate => tp.self_pid().send(.{ "cmd", "run_task_in_terminal", .{ entry.label, "hold" } }),
}) catch |e| menu.*.opts.ctx.logger.err(module_name, e);
}
}

View file

@ -33,8 +33,7 @@ pub fn load_entries(palette: *Type) !usize {
var longest_hint: usize = 0;
var idx: usize = 0;
try set_previous_theme(palette, tui.theme().name);
for (Widget.list_themes()) |theme_name_| {
const theme = Widget.get_theme_by_name(palette.allocator, theme_name_) orelse continue;
for (Widget.themes) |theme| {
idx += 1;
(try palette.entries.addOne(palette.allocator)).* = .{
.label = theme.description,

View file

@ -2,14 +2,6 @@ const std = @import("std");
const command = @import("command");
const cmd = command.executeName;
const tui = @import("../tui.zig");
const Buffer = @import("Buffer");
const Cursor = Buffer.Cursor;
const CurSel = @import("../editor.zig").CurSel;
const Editor = @import("../editor.zig").Editor;
const bracket_search_radius = @import("../editor.zig").bracket_search_radius;
var commands: Commands = undefined;
pub fn init() !void {
@ -146,398 +138,6 @@ const cmds_ = struct {
//TODO
return undefined;
}
pub const copy_line_meta: Meta = .{ .description = "Copies the current line" };
pub fn select_inside_word(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
}
pub const select_inside_word_meta: Meta = .{ .description = "Select inside word" };
pub fn select_around_word(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
}
pub const select_around_word_meta: Meta = .{ .description = "Select around word" };
pub fn select_inside_parentheses(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics);
}
pub const select_inside_parentheses_meta: Meta = .{ .description = "Select inside ()" };
pub fn select_around_parentheses(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics);
}
pub const select_around_parentheses_meta: Meta = .{ .description = "Select around ()" };
pub fn select_inside_square_brackets(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics);
}
pub const select_inside_square_brackets_meta: Meta = .{ .description = "Select inside []" };
pub fn select_around_square_brackets(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics);
}
pub const select_around_square_brackets_meta: Meta = .{ .description = "Select around []" };
pub fn select_inside_braces(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics);
}
pub const select_inside_braces_meta: Meta = .{ .description = "Select inside {}" };
pub fn select_around_braces(_: *void, _: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics);
}
pub const select_around_braces_meta: Meta = .{ .description = "Select around {}" };
pub fn cut_inside_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_word_meta: Meta = .{ .description = "Cut inside word" };
pub fn cut_around_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_word_meta: Meta = .{ .description = "Cut around word" };
pub fn cut_inside_parentheses(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_parentheses_meta: Meta = .{ .description = "Cut inside ()" };
pub fn cut_around_parentheses(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_parentheses_meta: Meta = .{ .description = "Cut around ()" };
pub fn cut_inside_square_brackets(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_square_brackets_meta: Meta = .{ .description = "Cut inside []" };
pub fn cut_around_square_brackets(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_square_brackets_meta: Meta = .{ .description = "Cut around []" };
pub fn cut_inside_braces(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_inside_braces_meta: Meta = .{ .description = "Cut inside {}" };
pub fn cut_around_braces(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics);
try ed.cut_internal_vim(ctx);
}
pub const cut_around_braces_meta: Meta = .{ .description = "Cut around {}" };
pub fn copy_inside_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_word_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_word_meta: Meta = .{ .description = "Copy inside word" };
pub fn copy_around_word(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_word_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_word_meta: Meta = .{ .description = "Copy around word" };
pub fn copy_inside_parentheses(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_parentheses_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_parentheses_meta: Meta = .{ .description = "Copy inside ()" };
pub fn copy_around_parentheses(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_parentheses_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_parentheses_meta: Meta = .{ .description = "Copy around ()" };
pub fn copy_inside_square_brackets(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_square_brackets_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_square_brackets_meta: Meta = .{ .description = "Copy inside []" };
pub fn copy_around_square_brackets(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_square_brackets_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_square_brackets_meta: Meta = .{ .description = "Copy around []" };
pub fn copy_inside_braces(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_inside_braces_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_inside_braces_meta: Meta = .{ .description = "Copy inside {}" };
pub fn copy_around_braces(_: *void, ctx: Ctx) Result {
const mv = tui.mainview() orelse return;
const ed = mv.get_active_editor() orelse return;
const root = ed.buf_root() catch return;
try ed.with_cursels_const(root, select_around_braces_textobject, ed.metrics);
try ed.copy_internal_vim(ctx);
}
pub const copy_around_braces_meta: Meta = .{ .description = "Copy around {}" };
};
fn is_tab_or_space(c: []const u8) bool {
return (c[0] == ' ') or (c[0] == '\t');
}
fn is_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return cursor.test_at(root, is_tab_or_space, metrics);
}
fn is_not_tab_or_space_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool {
return !cursor.test_at(root, is_tab_or_space, metrics);
}
fn select_inside_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_word_textobject(root, cursel, metrics, .inside);
}
fn select_around_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_word_textobject(root, cursel, metrics, .around);
}
fn select_word_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, scope: enum { inside, around }) !void {
var prev = cursel.cursor;
var next = cursel.cursor;
if (cursel.cursor.test_at(root, Editor.is_non_word_char, metrics)) {
if (cursel.cursor.test_at(root, Editor.is_whitespace_or_eol, metrics)) {
Editor.move_cursor_left_until(root, &prev, Editor.is_non_whitespace_at_cursor, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_non_whitespace_at_cursor, metrics);
} else {
Editor.move_cursor_left_until(root, &prev, Editor.is_whitespace_or_eol_at_cursor, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_whitespace_or_eol_at_cursor, metrics);
}
prev.move_right(root, metrics) catch {};
} else {
Editor.move_cursor_left_until(root, &prev, Editor.is_word_boundary_left_vim, metrics);
Editor.move_cursor_right_until(root, &next, Editor.is_word_boundary_right_vim, metrics);
next.move_right(root, metrics) catch {};
}
if (scope == .around) {
const inside_prev = prev;
const inside_next = next;
if (next.test_at(root, is_tab_or_space, metrics)) {
Editor.move_cursor_right_until(root, &next, is_not_tab_or_space_at_cursor, metrics);
} else {
next = inside_next;
prev.move_left(root, metrics) catch {};
if (prev.test_at(root, is_tab_or_space, metrics)) {
Editor.move_cursor_left_until(root, &prev, is_not_tab_or_space_at_cursor, metrics);
prev.move_right(root, metrics) catch {};
} else {
prev = inside_prev;
}
}
}
const sel = cursel.enable_selection(root, metrics);
sel.begin = prev;
sel.end = next;
cursel.*.cursor = next;
}
fn select_inside_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "(", ")", .inside);
}
fn select_around_parentheses_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "(", ")", .around);
}
fn select_inside_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "[", "]", .inside);
}
fn select_around_square_brackets_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "[", "]", .around);
}
fn select_inside_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "{", "}", .inside);
}
fn select_around_braces_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void {
return try select_bracket_textobject(root, cursel, metrics, "{", "}", .around);
}
fn select_bracket_textobject(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, opening_char: []const u8, closing_char: []const u8, scope: enum { inside, around }) !void {
const current = cursel.cursor;
var prev = cursel.cursor;
var next = cursel.cursor;
const bracket_egc, _, _ = root.egc_at(current.row, current.col, metrics) catch {
return error.Stop;
};
if (std.mem.eql(u8, bracket_egc, opening_char)) {
const closing_row, const closing_col = try Editor.match_bracket(root, current, metrics);
prev = current;
next.row = closing_row;
next.col = closing_col;
} else if (std.mem.eql(u8, bracket_egc, closing_char)) {
const opening_row, const opening_col = try Editor.match_bracket(root, current, metrics);
prev.row = opening_row;
prev.col = opening_col;
next = current;
} else {
const opening_pos, const closing_pos = find_bracket_pair(root, cursel, metrics, .left, opening_char) catch try find_bracket_pair(root, cursel, metrics, .right, opening_char);
prev.row = opening_pos[0];
prev.col = opening_pos[1];
next.row = closing_pos[0];
next.col = closing_pos[1];
}
prev.move_right(root, metrics) catch {};
if (scope == .around) {
prev.move_left(root, metrics) catch {};
next.move_right(root, metrics) catch {};
}
const sel = cursel.enable_selection(root, metrics);
sel.begin = prev;
sel.end = next;
cursel.*.cursor = next;
}
fn find_bracket_pair(root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics, direction: enum { left, right }, char: []const u8) error{Stop}!struct { struct { usize, usize }, struct { usize, usize } } {
const start = cursel.cursor;
var moving_cursor = cursel.cursor;
var i: usize = 0;
while (i < bracket_search_radius) : (i += 1) {
switch (direction) {
.left => try moving_cursor.move_left(root, metrics),
.right => try moving_cursor.move_right(root, metrics),
}
const curr_egc, _, _ = root.egc_at(moving_cursor.row, moving_cursor.col, metrics) catch {
return error.Stop;
};
if (std.mem.eql(u8, char, curr_egc)) {
const closing_row, const closing_col = try Editor.match_bracket(root, moving_cursor, metrics);
switch (direction) {
.left => if (closing_row > start.row or (closing_row == start.row and closing_col > start.col)) {
return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } };
} else {
continue;
},
.right => {
return .{ .{ moving_cursor.row, moving_cursor.col }, .{ closing_row, closing_col } };
},
}
}
}
return error.Stop;
}

909
src/tui/terminal_view.zig Normal file
View file

@ -0,0 +1,909 @@
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
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 argv = @import("argv");
const config = @import("config");
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");
const input = @import("input");
const keybind = @import("keybind");
pub const Mode = keybind.Mode;
const RGB = @import("color").RGB;
pub const name = @typeName(Self);
const Self = @This();
const widget_type: Widget.Type = .panel;
const Terminal = vaxis.widgets.Terminal;
const TerminalOnExit = config.TerminalOnExit;
allocator: Allocator,
plane: Plane,
focused: bool = false,
input_mode: Mode,
hover: bool = false,
vt: *Vt,
last_cmd: ?[]const u8,
commands: Commands = undefined,
pub fn create(allocator: Allocator, parent: Plane, ctx: command.Context) !Widget {
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();
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.plane = plane,
.input_mode = try keybind.mode("terminal", allocator, .{ .insert_command = "do_nothing" }),
.vt = undefined,
.last_cmd = null,
};
try self.run_cmd(ctx);
try self.commands.init(self);
try tui.message_filters().add(MessageFilter.bind(self, receive_filter));
container.ctx = self;
try container.add(Widget.to(self));
return container.widget();
}
pub fn run_cmd(self: *Self, ctx: command.Context) !void {
var env = try std.process.getEnvMap(self.allocator);
errdefer env.deinit();
var cmd_arg: []const u8 = "";
var on_exit: TerminalOnExit = tui.config().terminal_on_exit;
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(self.allocator, &cmd_arg)
else if (ctx.args.match(.{ tp.extract(&cmd_arg), tp.extract(&on_exit) }) catch false and cmd_arg.len > 0)
try shell.parse_arg0_to_argv(self.allocator, &cmd_arg)
else
null;
defer if (argv_msg) |msg| self.allocator.free(msg.buf);
var argv_list: std.ArrayListUnmanaged([]const u8) = .empty;
defer argv_list.deinit(self.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(self.allocator, arg);
}
} else {
const default_shell = if (builtin.os.tag == .windows)
env.get("COMSPEC") orelse "cmd.exe"
else
env.get("SHELL") orelse "/bin/sh";
try argv_list.append(self.allocator, default_shell);
}
// 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, self.plane.dim_x()));
const rows: u16 = @intCast(@max(24, self.plane.dim_y()));
if (global_vt) |*vt| {
if (!vt.process_exited) {
var msg: std.Io.Writer.Allocating = .init(self.allocator);
defer msg.deinit();
try msg.writer.writeAll("terminal is already running '");
try get_running_cmd(&msg.writer);
try msg.writer.writeAll("'");
return tp.exit(msg.written());
}
vt.deinit(self.allocator);
global_vt = null;
}
try Vt.init(self.allocator, argv_list.items, env, rows, cols, on_exit);
self.vt = &global_vt.?;
if (self.last_cmd) |cmd| {
self.allocator.free(cmd);
self.last_cmd = null;
}
self.last_cmd = try self.allocator.dupe(u8, ctx.args.buf);
}
fn re_run_cmd(self: *Self) !void {
return if (self.last_cmd) |cmd|
self.run_cmd(.{ .args = .{ .buf = cmd } })
else
tp.exit("no command to re-run");
}
pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "terminal_view", "output" })) {
tui.need_render(@src());
return true;
} else if (try m.match(.{ "H", tp.extract(&self.hover) })) {
tui.rdr().request_mouse_cursor_default(self.hover);
tui.need_render(@src());
return true;
}
// Mouse button press - set focus first, then forward to terminal if reporting is on
{
var btn: i64 = 0;
var col: i64 = 0;
var row: i64 = 0;
var xoffset: i64 = 0;
var yoffset: i64 = 0;
if (try m.match(.{ "B", input.event.press, tp.extract(&btn), tp.any, tp.extract(&col), tp.extract(&row), tp.extract(&xoffset), tp.extract(&yoffset) }) or
try m.match(.{ "B", input.event.release, tp.extract(&btn), tp.any, tp.extract(&col), tp.extract(&row), tp.extract(&xoffset), tp.extract(&yoffset) }))
{
const button: vaxis.Mouse.Button = @enumFromInt(btn);
const is_press = try m.match(.{ "B", input.event.press, tp.more });
// Set focus on left/middle/right button press
if (is_press) switch (button) {
.left, .middle, .right => switch (tui.set_focus_by_mouse_event()) {
.changed => return true,
.same, .notfound => {},
},
// Scroll wheel: forward to vt if reporting active, else scroll scrollback
.wheel_up => {
if (self.vt.vt.mode.mouse == .none) {
if (self.vt.vt.scroll(3)) tui.need_render(@src());
return true;
}
},
.wheel_down => {
if (self.vt.vt.mode.mouse == .none) {
if (self.vt.vt.scroll(-3)) tui.need_render(@src());
return true;
}
},
else => {},
};
// Forward to vt if terminal mouse reporting is active
if (self.focused and self.vt.vt.mode.mouse != .none) {
const rel = self.plane.abs_yx_to_rel(@intCast(row), @intCast(col));
const mouse_event: vaxis.Mouse = .{
.col = @intCast(rel[1]),
.row = @intCast(rel[0]),
.xoffset = @intCast(xoffset),
.yoffset = @intCast(yoffset),
.button = button,
.mods = .{},
.type = if (is_press) .press else .release,
};
self.vt.vt.update(.{ .mouse = mouse_event }) catch {};
tui.need_render(@src());
return true;
}
return false;
}
// Mouse drag
if (try m.match(.{ "D", input.event.press, tp.extract(&btn), tp.any, tp.extract(&col), tp.extract(&row), tp.extract(&xoffset), tp.extract(&yoffset) })) {
if (self.focused and self.vt.vt.mode.mouse != .none) {
const rel = self.plane.abs_yx_to_rel(@intCast(row), @intCast(col));
const mouse_event: vaxis.Mouse = .{
.col = @intCast(rel[1]),
.row = @intCast(rel[0]),
.xoffset = @intCast(xoffset),
.yoffset = @intCast(yoffset),
.button = @enumFromInt(btn),
.mods = .{},
.type = .drag,
};
self.vt.vt.update(.{ .mouse = mouse_event }) catch {};
tui.need_render(@src());
return true;
}
return false;
}
// Mouse motion (no button held)
if (try m.match(.{ "M", tp.extract(&col), tp.extract(&row), tp.extract(&xoffset), tp.extract(&yoffset) })) {
if (self.focused and self.vt.vt.mode.mouse == .any_event) {
const rel = self.plane.abs_yx_to_rel(@intCast(row), @intCast(col));
const mouse_event: vaxis.Mouse = .{
.col = @intCast(rel[1]),
.row = @intCast(rel[0]),
.xoffset = @intCast(xoffset),
.yoffset = @intCast(yoffset),
.button = .none,
.mods = .{},
.type = .motion,
};
self.vt.vt.update(.{ .mouse = mouse_event }) catch {};
tui.need_render(@src());
return true;
}
return false;
}
}
if (!(try m.match(.{ "I", tp.more })))
return false;
if (!self.focused) return false;
if (try self.input_mode.bindings.receive(from, m))
return true;
var event: input.Event = 0;
var keypress: input.Key = 0;
var keypress_shifted: input.Key = 0;
var text: []const u8 = "";
var modifiers: u8 = 0;
if (!try m.match(.{ "I", tp.extract(&event), tp.extract(&keypress), tp.extract(&keypress_shifted), tp.extract(&text), tp.extract(&modifiers) }))
return false;
// Only forward press and repeat events; ignore releases.
if (event != input.event.press and event != input.event.repeat) return true;
const key: vaxis.Key = .{
.codepoint = keypress,
.shifted_codepoint = if (keypress_shifted != keypress) keypress_shifted else null,
.mods = @bitCast(modifiers),
.text = if (text.len > 0) text else null,
};
if (self.vt.process_exited) {
if (keypress == input.key.enter) {
self.re_run_cmd() catch |e|
std.log.err("terminal_view: restart failed: {}", .{e});
tui.need_render(@src());
return true;
}
if (keypress == input.key.escape) {
tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {};
return true;
}
}
self.vt.vt.scrollToBottom();
self.vt.vt.update(.{ .key_press = key }) catch |e|
std.log.err("terminal_view: input failed: {}", .{e});
tui.need_render(@src());
return true;
}
pub fn toggle_focus(self: *Self) void {
if (self.focused) self.unfocus() else self.focus();
}
pub fn focus(self: *Self) void {
self.focused = true;
tui.set_keyboard_focus(Widget.to(self));
}
pub fn unfocus(self: *Self) void {
self.focused = false;
tui.release_keyboard_focus(Widget.to(self));
}
pub fn deinit(self: *Self, allocator: Allocator) void {
if (self.last_cmd) |cmd| {
self.allocator.free(cmd);
self.last_cmd = null;
}
if (global_vt) |*vt| if (vt.process_exited) {
vt.deinit(allocator);
global_vt = null;
};
if (self.focused) tui.release_keyboard_focus(Widget.to(self));
self.commands.unregister();
self.plane.deinit();
allocator.destroy(self);
}
pub fn shutdown(allocator: Allocator) void {
if (global_vt) |*vt| {
vt.deinit(allocator);
global_vt = null;
}
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
// Drain the vt event queue.
while (self.vt.vt.tryEvent()) |event| {
switch (event) {
.exited => |code| {
self.vt.process_exited = true;
self.handle_child_exit(code);
tui.need_render(@src());
},
.redraw, .bell => {},
.pwd_change => |path| {
self.vt.cwd.clearRetainingCapacity();
self.vt.cwd.appendSlice(self.allocator, path) catch {};
},
.title_change => |t| {
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.
const software_cursor = build_options.gui or !tui.config().enable_terminal_cursor;
const focused_cursor_color: ?[3]u8 = if (theme.editor_cursor.bg) |bg| RGB.to_u8s(RGB.from_u24(bg.color)) else null;
const unfocused_cursor_color: ?[3]u8 = if (theme.editor_cursor_secondary.bg) |bg| RGB.to_u8s(RGB.from_u24(bg.color)) else focused_cursor_color;
self.vt.vt.draw(self.allocator, self.plane.window, self.focused and tui.terminal_has_focus(), software_cursor, focused_cursor_color, unfocused_cursor_color) catch |e| {
std.log.err("terminal_view: draw failed: {}", .{e});
};
return false;
}
fn handle_child_exit(self: *Self, code: u8) void {
switch (self.vt.on_exit) {
.hold => self.show_exit_message(code),
.hold_on_error => if (code == 0)
tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {}
else
self.show_exit_message(code),
.close => tp.self_pid().send(.{ "cmd", "close_terminal", .{} }) catch {},
}
}
fn show_exit_message(self: *Self, code: u8) void {
var msg: std.Io.Writer.Allocating = .init(self.allocator);
defer msg.deinit();
const w = &msg.writer;
w.writeAll("\r\n") catch {};
w.writeAll("\x1b[0m\x1b[2m") catch {};
w.writeAll("[process exited") catch {};
if (code != 0)
w.print(" with code {d}", .{code}) catch {};
w.writeAll("]") catch {};
// Re-run prompt
const cmd_argv = self.vt.vt.cmd.argv;
if (cmd_argv.len > 0) {
w.writeAll(" Press enter to re-run '") catch {};
_ = argv.write(w, cmd_argv) catch {};
w.writeAll("' or escape to close") catch {};
} else {
w.writeAll(" Press esc to close") catch {};
}
w.writeAll("\x1b[0m\r\n") catch {};
var parser: pty.Parser = .{ .buf = .init(self.allocator) };
defer parser.buf.deinit();
_ = self.vt.vt.processOutput(&parser, msg.written()) catch {};
}
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;
self.vt.resize(pos);
}
fn receive_filter(_: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
if (m.match(.{ "terminal_view", "output" }) catch false) {
tui.need_render(@src());
return true;
}
return false;
}
const Commands = command.Collection(cmds);
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
const Meta = command.Metadata;
const Result = command.Result;
pub fn terminal_scroll_up(self: *Self, _: Ctx) Result {
const half_page = @max(1, self.vt.vt.front_screen.height / 2);
if (self.vt.vt.scroll(@intCast(half_page)))
tui.need_render(@src());
}
pub const terminal_scroll_up_meta: Meta = .{ .description = "Terminal: Scroll up" };
pub fn terminal_scroll_down(self: *Self, _: Ctx) Result {
const half_page = @max(1, self.vt.vt.front_screen.height / 2);
if (self.vt.vt.scroll(-@as(i32, @intCast(half_page))))
tui.need_render(@src());
}
pub const terminal_scroll_down_meta: Meta = .{ .description = "Terminal: Scroll down" };
};
const Vt = struct {
vt: Terminal,
env: std.process.EnvMap,
write_buf: [4096]u8,
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,
process_exited: bool = false,
on_exit: TerminalOnExit,
fn init(allocator: std.mem.Allocator, cmd_argv: []const []const u8, env: std.process.EnvMap, rows: u16, cols: u16, on_exit: TerminalOnExit) !void {
const home = env.get("HOME") orelse "/tmp";
global_vt = .{
.vt = undefined,
.env = env,
.write_buf = undefined, // managed via self.vt's pty_writer pointer
.pty_pid = null,
.on_exit = on_exit,
};
const self = &global_vt.?;
self.vt = try Terminal.init(
allocator,
cmd_argv,
&env,
.{
.winsize = .{ .rows = rows, .cols = cols, .x_pixel = 0, .y_pixel = 0 },
.scrollback_size = tui.config().terminal_scrollback_size,
.initial_working_directory = blk: {
const project = tp.env.get().str("project");
break :blk if (project.len > 0) project else home;
},
},
&self.write_buf,
);
try self.vt.spawn();
self.pty_pid = try pty.spawn(allocator, &self.vt);
}
fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.cwd.deinit(allocator);
self.title.deinit(allocator);
if (self.pty_pid) |pid| {
pid.send(.{"quit"}) catch {};
pid.deinit();
self.pty_pid = null;
}
self.vt.deinit();
self.env.deinit();
std.log.debug("terminal: vt destroyed", .{});
}
pub fn resize(self: *@This(), pos: Widget.Box) void {
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: resize failed: {}", .{e});
};
}
};
var global_vt: ?Vt = null;
pub fn is_vt_running() bool {
return if (global_vt) |vt| !vt.process_exited else false;
}
pub fn get_running_cmd(writer: *std.Io.Writer) std.Io.Writer.Error!void {
const cmd_argv = if (global_vt) |vt| vt.vt.cmd.argv else &.{};
if (cmd_argv.len > 0) {
_ = argv.write(writer, cmd_argv) catch {};
}
}
// Platform-specific pty actor: POSIX uses tp.file_descriptor + SIGCHLD,
// Windows uses tp.file_stream with IOCP overlapped reads on the ConPTY output pipe.
const pty = if (builtin.os.tag == .windows) pty_windows else pty_posix;
const pty_posix = struct {
const Parser = Terminal.Parser;
const Receiver = tp.Receiver(*@This());
allocator: std.mem.Allocator,
vt: *Terminal,
fd: tp.file_descriptor,
pty_fd: std.posix.fd_t,
parser: Parser,
receiver: Receiver,
parent: tp.pid,
err_code: i64 = 0,
sigchld: ?tp.signal = null,
pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid {
const self = try allocator.create(@This());
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.vt = vt,
.fd = undefined,
.pty_fd = vt.ptyFd(),
.parser = .{ .buf = try .initCapacity(allocator, 128) },
.receiver = Receiver.init(pty_receive, self),
.parent = tp.self_pid().clone(),
};
return tp.spawn_link(allocator, self, start, "pty_actor");
}
fn deinit(self: *@This()) void {
std.log.debug("terminal: pty actor deinit (pid={?})", .{self.vt.cmd.pid});
if (self.sigchld) |s| s.deinit();
self.fd.deinit();
self.parser.buf.deinit();
self.parent.deinit();
self.allocator.destroy(self);
}
fn start(self: *@This()) tp.result {
errdefer self.deinit();
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());
};
self.sigchld = tp.signal.init(std.posix.SIG.CHLD, tp.message.fmt(.{"sigchld"})) catch |e| {
std.log.debug("terminal: SIGCHLD signal init failed: {}", .{e});
return tp.exit_error(e, @errorReturnTrace());
};
tp.receive(&self.receiver);
}
fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
if (try m.match(.{ "fd", "pty", "read_ready" })) {
self.read_and_process() catch |e| return switch (e) {
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(.{ "fd", "pty", "read_error", tp.extract(&self.err_code), tp.more })) {
// thespian fires read_error when the pty fd signals an error condition
// Treat it the same as EIO: reap the child and signal exit.
const code = self.vt.cmd.wait();
std.log.debug("terminal: read_error from fd (err={d}), process exited with code={d}", .{ self.err_code, code });
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return tp.exit_normal();
} else if (try m.match(.{"sigchld"})) {
// SIGCHLD fires when any child exits. Check if it's our child.
if (self.vt.cmd.try_wait()) |code| {
std.log.debug("terminal: child exited (SIGCHLD) with code={d}", .{code});
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return tp.exit_normal();
}
// Not our child (or already reaped) - re-arm the signal and continue.
if (self.sigchld) |s| s.deinit();
self.sigchld = tp.signal.init(std.posix.SIG.CHLD, tp.message.fmt(.{"sigchld"})) catch null;
} 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);
}
}
fn read_and_process(self: *@This()) error{ Terminated, InputOutput, SendFailed, Unexpected }!void {
var buf: [4096]u8 = undefined;
while (true) {
const n = std.posix.read(self.vt.ptyFd(), &buf) catch |e| switch (e) {
error.WouldBlock => {
// No more data right now. On Linux, a clean child exit may not
// generate a readable event on the pty master - it just starts
// returning EIO. Poll for exit here before sleeping in wait_read.
// On macOS/FreeBSD the pty master raises EIO directly, so the
// try_wait check here is just an extra safety net.
if (self.vt.cmd.try_wait()) |code| {
std.log.debug("terminal: child exited (detected via try_wait) with code={d}", .{code});
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return error.InputOutput;
}
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;
},
error.SystemResources,
error.IsDir,
error.OperationAborted,
error.BrokenPipe,
error.ConnectionResetByPeer,
error.ConnectionTimedOut,
error.NotOpenForReading,
error.SocketNotConnected,
error.Canceled,
error.AccessDenied,
error.ProcessNotFound,
error.LockViolation,
error.Unexpected,
=> {
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;
}
defer self.parent.send(.{ "terminal_view", "output" }) catch {};
switch (self.vt.processOutput(&self.parser, buf[0..n]) catch |e| switch (e) {
error.WriteFailed,
error.ReadFailed,
error.OutOfMemory,
=> {
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 => {},
}
}
// Check for child exit once more before sleeping in wait_read.
// A clean exit with no final output will never make the pty fd readable,
// so we must detect it here rather than waiting forever.
if (self.vt.cmd.try_wait()) |code| {
std.log.debug("terminal: child exited (pre-wait_read check) with code={d}", .{code});
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return error.InputOutput;
}
self.fd.wait_read() catch |e| switch (e) {
error.ThespianFileDescriptorWaitReadFailed => {
std.log.debug("terminal: wait_read failed: {} (pid={?})", .{ e, self.vt.cmd.pid });
return error.Unexpected;
},
};
}
};
/// Windows pty actor: reads ConPTY output pipe via tp.file_stream (IOCP overlapped I/O).
///
/// Exit detection: ConPTY does NOT close the output pipe when the child process exits -
/// it keeps it open until ClosePseudoConsole is called. So a pending async read would
/// block forever. Instead we use RegisterWaitForSingleObject on the process handle;
/// when it fires the threadpool callback posts "child_exited" to this actor, which
/// cancels the stream and tears down cleanly.
const pty_windows = struct {
const Parser = Terminal.Parser;
const Receiver = tp.Receiver(*@This());
const windows = std.os.windows;
// Context struct allocated on the heap and passed to the wait callback.
// Heap-allocated so its lifetime is independent of the actor.
const WaitCtx = struct {
self_pid: tp.pid,
allocator: std.mem.Allocator,
};
allocator: std.mem.Allocator,
vt: *Terminal,
stream: ?tp.file_stream = null,
parser: Parser,
receiver: Receiver,
parent: tp.pid,
wait_handle: ?windows.HANDLE = null,
pub fn spawn(allocator: std.mem.Allocator, vt: *Terminal) !tp.pid {
const self = try allocator.create(@This());
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.vt = vt,
.parser = .{ .buf = try .initCapacity(allocator, 128) },
.receiver = Receiver.init(pty_receive, self),
.parent = tp.self_pid().clone(),
};
return tp.spawn_link(allocator, self, start, "pty_actor");
}
fn deinit(self: *@This()) void {
std.log.debug("terminal: pty actor (windows) deinit", .{});
if (self.wait_handle) |wh| {
_ = UnregisterWait(wh);
self.wait_handle = null;
}
if (self.stream) |s| s.deinit();
self.parser.buf.deinit();
self.parent.deinit();
self.allocator.destroy(self);
}
fn start(self: *@This()) tp.result {
errdefer self.deinit();
self.stream = tp.file_stream.init("pty_out", self.vt.ptyOutputHandle()) catch |e| {
std.log.debug("terminal: pty stream init failed: {}", .{e});
return tp.exit_error(e, @errorReturnTrace());
};
self.stream.?.start_read() catch |e| {
std.log.debug("terminal: pty stream start_read failed: {}", .{e});
return tp.exit_error(e, @errorReturnTrace());
};
// Register a one-shot wait on the process handle. When the child exits
// the threadpool fires on_child_exit, which sends "child_exited" to us.
// This is the only reliable way to detect ConPTY child exit without polling,
// since ConPTY keeps the output pipe open until ClosePseudoConsole.
const process_handle = self.vt.cmd.process_handle orelse {
std.log.debug("terminal: pty actor: no process handle to wait on", .{});
return tp.exit_error(error.NoProcessHandle, @errorReturnTrace());
};
const ctx = self.allocator.create(WaitCtx) catch |e|
return tp.exit_error(e, @errorReturnTrace());
ctx.* = .{
.self_pid = tp.self_pid().clone(),
.allocator = self.allocator,
};
var wh: windows.HANDLE = undefined;
// WT_EXECUTEONLYONCE: callback fires once then the wait is auto-unregistered.
const WT_EXECUTEONLYONCE: windows.ULONG = 0x00000008;
if (RegisterWaitForSingleObject(&wh, process_handle, on_child_exit, ctx, windows.INFINITE, WT_EXECUTEONLYONCE) == windows.FALSE) {
ctx.self_pid.deinit();
self.allocator.destroy(ctx);
std.log.debug("terminal: RegisterWaitForSingleObject failed", .{});
return tp.exit_error(error.RegisterWaitFailed, @errorReturnTrace());
}
self.wait_handle = wh;
tp.receive(&self.receiver);
}
/// Threadpool callback - called when the process handle becomes signaled.
/// Must be fast and non-blocking. Sends "child_exited" to the pty actor.
fn on_child_exit(ctx_ptr: ?*anyopaque, _: windows.BOOLEAN) callconv(.winapi) void {
const ctx: *WaitCtx = @ptrCast(@alignCast(ctx_ptr orelse return));
defer {
ctx.self_pid.deinit();
ctx.allocator.destroy(ctx);
}
ctx.self_pid.send(.{"child_exited"}) catch {};
}
fn pty_receive(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var bytes: []const u8 = "";
var err_code: i64 = 0;
var err_msg: []const u8 = "";
if (try m.match(.{"child_exited"})) {
self.wait_handle = null;
if (self.stream) |s| s.cancel() catch {};
const code = self.vt.cmd.wait();
std.log.debug("terminal: child exited (process wait), code={d}", .{code});
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return tp.exit_normal();
} else if (try m.match(.{ "stream", "pty_out", "read_complete", tp.extract(&bytes) })) {
defer self.parent.send(.{ "terminal_view", "output" }) catch {};
switch (self.vt.processOutput(&self.parser, bytes) catch |e| {
std.log.debug("terminal: processOutput error: {}", .{e});
return tp.exit_normal();
}) {
.exited => {
std.log.debug("terminal: processOutput returned .exited", .{});
return tp.exit_normal();
},
.running => {},
}
self.stream.?.start_read() catch |e| {
std.log.debug("terminal: pty stream re-arm failed: {}", .{e});
return tp.exit_normal();
};
} else if (try m.match(.{ "stream", "pty_out", "read_error", tp.extract(&err_code), tp.extract(&err_msg) })) {
std.log.debug("terminal: ConPTY stream error: {d} {s}", .{ err_code, err_msg });
const code = self.vt.cmd.wait();
self.vt.event_queue.push(.{ .exited = code });
self.parent.send(.{ "terminal_view", "output" }) catch {};
return tp.exit_normal();
} else if (try m.match(.{"quit"})) {
std.log.debug("terminal: pty actor (windows) received quit", .{});
return tp.exit_normal();
} else {
std.log.debug("terminal: pty actor (windows) unexpected message", .{});
return tp.unexpected(m);
}
}
// Win32 extern declarations
extern "kernel32" fn RegisterWaitForSingleObject(
phNewWaitObject: *windows.HANDLE,
hObject: windows.HANDLE,
Callback: *const fn (?*anyopaque, windows.BOOLEAN) callconv(.winapi) void,
Context: ?*anyopaque,
dwMilliseconds: windows.DWORD,
dwFlags: windows.ULONG,
) callconv(.winapi) windows.BOOL;
extern "kernel32" fn UnregisterWait(
WaitHandle: windows.HANDLE,
) callconv(.winapi) windows.BOOL;
};

View file

@ -53,8 +53,10 @@ delayed_init_input_mode: ?Mode = null,
input_mode_outer_: ?Mode = null,
input_listeners_: EventHandler.List,
keyboard_focus: ?Widget = null,
keyboard_focus_outer: ?Widget = null,
mini_mode_: ?MiniMode = null,
hover_focus: ?Widget = null,
terminal_focus: bool = true,
last_hover_x: c_int = -1,
last_hover_y: c_int = -1,
commands: Commands = undefined,
@ -62,7 +64,9 @@ logger: log.Logger,
drag_source: ?Widget = null,
drag_button: input.MouseType = 0,
dark_theme: Widget.Theme,
dark_parsed_theme: ?std.json.Parsed(Widget.Theme),
light_theme: Widget.Theme,
light_parsed_theme: ?std.json.Parsed(Widget.Theme),
idle_frame_count: usize = 0,
unrendered_input_events_count: usize = 0,
init_timer: ?tp.timeout,
@ -158,9 +162,9 @@ fn init(allocator: Allocator) InitError!*Self {
if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash)
renderer.jit_debugger_enabled = true;
const dark_theme = Widget.get_theme_by_name(allocator, conf.theme) orelse Widget.get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme;
const dark_theme, const dark_parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme;
conf.theme = dark_theme.name;
const light_theme = Widget.get_theme_by_name(allocator, conf.light_theme) orelse Widget.get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme;
const light_theme, const light_parsed_theme = get_theme_by_name(allocator, conf.light_theme) orelse get_theme_by_name(allocator, "default-light") orelse return error.UnknownTheme;
conf.light_theme = light_theme.name;
if (build_options.gui) conf.enable_terminal_cursor = false;
@ -201,6 +205,8 @@ fn init(allocator: Allocator) InitError!*Self {
.query_cache_ = try syntax.QueryCache.create(allocator, .{}),
.dark_theme = dark_theme,
.light_theme = light_theme,
.dark_parsed_theme = dark_parsed_theme,
.light_parsed_theme = light_parsed_theme,
};
instance_ = self;
defer instance_ = null;
@ -514,15 +520,19 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
return;
if (try m.match(.{"focus_in"})) {
self.terminal_focus = true;
std.log.debug("focus_in", .{});
need_render(@src());
return;
}
if (try m.match(.{"focus_out"})) {
self.terminal_focus = false;
std.log.debug("focus_out", .{});
self.clear_hover_focus(@src()) catch {};
self.last_hover_x = -1;
self.last_hover_y = -1;
need_render(@src());
return;
}
@ -829,22 +839,54 @@ pub const FocusAction = enum { same, changed, notfound };
pub fn set_focus_by_widget(w: Widget) FocusAction {
const mv = mainview() orelse return .notfound;
clear_keyboard_focus();
return mv.focus_view_by_widget(w);
}
pub fn set_focus_by_mouse_event() FocusAction {
const self = current();
const mv = mainview() orelse return .notfound;
return mv.focus_view_by_widget(self.hover_focus orelse return .notfound);
const hover_focus = self.hover_focus orelse return .notfound;
const keyboard_focus = if (self.keyboard_focus) |prev| prev.ptr else null;
if (hover_focus.ptr == keyboard_focus) return .same;
clear_keyboard_focus();
switch (mv.focus_view_by_widget(hover_focus)) {
.notfound => {},
else => |action| return action,
}
hover_focus.focus();
return .changed;
}
pub fn is_keyboard_focused() bool {
const self = current();
return self.keyboard_focus != null;
}
pub fn set_keyboard_focus(w: Widget) void {
const self = current();
if (self.keyboard_focus) |prev| prev.unfocus();
self.keyboard_focus = w;
}
pub fn release_keyboard_focus(w: Widget) void {
const self = current();
if (self.keyboard_focus) |cur| if (cur.ptr == w.ptr) {
self.keyboard_focus = null;
};
}
pub fn clear_keyboard_focus() void {
const self = current();
if (self.keyboard_focus) |prev| prev.unfocus();
self.keyboard_focus = null;
}
fn send_widgets(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
const frame = tracy.initZone(@src(), .{ .name = "tui widgets" });
defer frame.deinit();
tp.trace(tp.channel.widget, m);
return if (self.keyboard_focus) |w|
w.send(from, m)
else if (self.mainview_) |mv|
return if (self.mainview_) |mv|
mv.send(from, m)
else
false;
@ -853,10 +895,6 @@ fn send_widgets(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
fn send_mouse(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) tp.result {
tp.trace(tp.channel.input, m);
_ = self.input_listeners_.send(from, m) catch {};
if (self.keyboard_focus) |w| {
_ = try w.send(from, m);
return;
}
if (try self.update_hover(y, x)) |w|
_ = try w.send(from, m);
}
@ -864,10 +902,6 @@ fn send_mouse(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message)
fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) tp.result {
tp.trace(tp.channel.input, m);
_ = self.input_listeners_.send(from, m) catch {};
if (self.keyboard_focus) |w| {
_ = try w.send(from, m);
return;
}
_ = try self.update_hover(y, x);
if (self.drag_source) |w| _ = try w.send(from, m);
}
@ -920,10 +954,12 @@ pub fn save_config() (root.ConfigDirError || root.ConfigWriteError)!void {
pub fn is_mainview_focused() bool {
const self = current();
return self.mini_mode_ == null and self.input_mode_outer_ == null;
return self.mini_mode_ == null and self.input_mode_outer_ == null and !is_keyboard_focused() and self.terminal_focus;
}
fn enter_overlay_mode(self: *Self, mode: type) command.Result {
self.keyboard_focus_outer = self.keyboard_focus;
clear_keyboard_focus();
command.executeName("disable_fast_scroll", .{}) catch {};
command.executeName("disable_alt_scroll", .{}) catch {};
command.executeName("disable_jump_mode", .{}) catch {};
@ -983,13 +1019,21 @@ fn refresh_input_mode(self: *Self) command.Result {
}
fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store }) !void {
const theme_ = Widget.get_theme_by_name(self.allocator, name) orelse {
const theme_, const parsed_theme = get_theme_by_name(self.allocator, name) orelse {
self.logger.print("theme not found: {s}", .{name});
return;
};
switch (self.color_scheme) {
.dark => self.dark_theme = theme_,
.light => self.light_theme = theme_,
.dark => {
if (self.dark_parsed_theme) |p| p.deinit();
self.dark_parsed_theme = parsed_theme;
self.dark_theme = theme_;
},
.light => {
if (self.light_parsed_theme) |p| p.deinit();
self.light_parsed_theme = parsed_theme;
self.light_theme = theme_;
},
}
self.set_terminal_style(&theme_);
self.logger.print("theme: {s}", .{theme_.description});
@ -1129,13 +1173,13 @@ const cmds = struct {
pub const set_theme_meta: Meta = .{ .arguments = &.{.string} };
pub fn theme_next(self: *Self, _: Ctx) Result {
const name = Widget.get_next_theme_by_name(self.current_theme().name);
const name = get_next_theme_by_name(self.current_theme().name);
return self.set_theme_by_name(name, .store);
}
pub const theme_next_meta: Meta = .{ .description = "Next color theme" };
pub fn theme_prev(self: *Self, _: Ctx) Result {
const name = Widget.get_prev_theme_by_name(self.current_theme().name);
const name = get_prev_theme_by_name(self.current_theme().name);
return self.set_theme_by_name(name, .store);
}
pub const theme_prev_meta: Meta = .{ .description = "Previous color theme" };
@ -1471,6 +1515,30 @@ const cmds = struct {
.arguments = &.{.string},
};
pub fn run_task_in_terminal(self: *Self, ctx: Ctx) Result {
var buf: [tp.max_message_size]u8 = undefined;
std.log.debug("run_task_in_terminal: {s}", .{if (ctx.args.buf.len > 0) ctx.args.to_json(&buf) catch "(error)" else "(none)"});
const expansion = @import("expansion.zig");
var task: []const u8 = undefined;
var on_exit: @import("config").TerminalOnExit = self.config_.terminal_on_exit;
if (!(try ctx.args.match(.{tp.extract(&task)}) or
try ctx.args.match(.{ tp.extract(&task), tp.extract(&on_exit) }))) 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);
try command.executeName("open_terminal", try command.fmtbuf(&buf, .{ cmd, on_exit }));
}
pub const run_task_in_terminal_meta: Meta = .{
.description = "Run a task in terminal",
.arguments = &.{.string},
};
pub fn delete_task(_: *Self, ctx: Ctx) Result {
var task: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&task)}))
@ -1495,6 +1563,8 @@ const cmds = struct {
if (self.input_mode_) |*mode| mode.deinit();
self.input_mode_ = self.input_mode_outer_;
self.input_mode_outer_ = null;
if (self.keyboard_focus_outer) |widget| if (self.is_live_widget_ptr(widget))
widget.focus();
refresh_hover(@src());
}
pub const exit_overlay_mode_meta: Meta = .{};
@ -2008,6 +2078,40 @@ pub fn theme() *const Widget.Theme {
return current().current_theme();
}
pub fn get_theme_by_name(allocator: std.mem.Allocator, name: []const u8) ?struct { Widget.Theme, ?std.json.Parsed(Widget.Theme) } {
if (load_theme_file(allocator, name) catch null) |parsed_theme| {
std.log.info("loaded theme from file: {s}", .{name});
return .{ parsed_theme.value, parsed_theme };
}
for (Widget.themes) |theme_| {
if (std.mem.eql(u8, theme_.name, name))
return .{ theme_, null };
}
return null;
}
fn get_next_theme_by_name(name: []const u8) []const u8 {
var next = false;
for (Widget.themes) |theme_| {
if (next)
return theme_.name;
if (std.mem.eql(u8, theme_.name, name))
next = true;
}
return Widget.themes[0].name;
}
fn get_prev_theme_by_name(name: []const u8) []const u8 {
var prev: ?Widget.Theme = null;
for (Widget.themes) |theme_| {
if (std.mem.eql(u8, theme_.name, name))
return (prev orelse Widget.themes[Widget.themes.len - 1]).name;
prev = theme_;
}
return Widget.themes[Widget.themes.len - 1].name;
}
pub fn find_scope_style(theme_: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token {
return if (find_scope_fallback(scope)) |tm_scope|
scope_to_theme_token(theme_, tm_scope) orelse
@ -2396,6 +2500,19 @@ fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const
return try root.get_theme_file_name(theme_name);
}
fn load_theme_file(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) {
return load_theme_file_internal(allocator, theme_name) catch |e| {
std.log.err("loaded theme from file failed: {}", .{e});
return e;
};
}
fn load_theme_file_internal(allocator: std.mem.Allocator, theme_name: []const u8) !?std.json.Parsed(Widget.Theme) {
_ = std.json.Scanner;
const json_str = root.read_theme(allocator, theme_name) orelse return null;
defer allocator.free(json_str);
return try std.json.parseFromSlice(Widget.Theme, allocator, json_str, .{ .allocate = .alloc_always });
}
pub const WidgetType = @import("config").WidgetType;
pub const ConfigWidgetStyle = @import("config").WidgetStyle;
pub const WidgetStyle = @import("WidgetStyle.zig");
@ -2600,3 +2717,8 @@ pub fn jump_mode() bool {
const self = current();
return self.jump_mode_;
}
pub fn terminal_has_focus() bool {
const self = current();
return self.terminal_focus;
}