diff --git a/build.zig b/build.zig index 3168c030..8d3c126e 100644 --- a/build.zig +++ b/build.zig @@ -392,13 +392,6 @@ 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 = &.{ @@ -667,7 +660,6 @@ 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 }, @@ -717,7 +709,6 @@ 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); @@ -768,7 +759,6 @@ 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); diff --git a/build.zig.zon b/build.zig.zon index c7b77d23..71a537a6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .dependencies = .{ .syntax = .{ - .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#56929f0c523b59153e17919be2cd09d8bef32cd0", - .hash = "flow_syntax-0.7.2-X8jOoeFTAQBeP2Tn08Tw1jsMdifLEDBgPLqPqNelAupy", + .url = "git+https://github.com/neurocyte/flow-syntax?ref=master#7b1fd3a97f00aba3a95cc65b95f34162347ed1ea", + .hash = "flow_syntax-0.7.2-X8jOoQhWAQBPt1rBRmttAGI0Z2QC-hCSZuoBZoZgr6Vv", }, .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-c6c7f18cfb2e3945cd0b71dab24271465074dbc3/flow-themes.tar.gz", - .hash = "N-V-__8AAOKzJACguNxU76WX9M7RIhOYGuLnlasJ1-GDdhqT", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-750400d02ea8cacaabc869cd4d34dcebf04a53c8/flow-themes.tar.gz", + .hash = "N-V-__8AAEWxJQAyUV_rvRIWHB8EhIBxpQXqCB68SpilIjEt", }, .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#cecc97d9ff8da9df13499da0d0b19c5cd18742c3", - .hash = "vaxis-0.5.1-BWNV_BcgCgDG3wpSPxCHxaRAZukEfnnKrBa-52zjnjex", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#1f6c7222f59607bff0ee8d7c6a0637a05bceffcd", + .hash = "vaxis-0.5.1-BWNV_CNLCQDmr-D_UzqGRAngktQt7hiGTRf1gyozwxcG", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88", diff --git a/src/argv.zig b/src/argv.zig deleted file mode 100644 index 3e6d5ec9..00000000 --- a/src/argv.zig +++ /dev/null @@ -1,33 +0,0 @@ -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; -} diff --git a/src/buffer/View.zig b/src/buffer/View.zig index 379785fc..1d5a89be 100644 --- a/src/buffer/View.zig +++ b/src/buffer/View.zig @@ -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; } } diff --git a/src/config.zig b/src/config.zig index 5cf6325f..194a60d7 100644 --- a/src/config.zig +++ b/src/config.zig @@ -12,8 +12,6 @@ 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, @@ -249,9 +247,3 @@ pub const AgeFormat = enum { short, long, }; - -pub const TerminalOnExit = enum { - hold_on_error, - close, - hold, -}; diff --git a/src/file_link.zig b/src/file_link.zig index 7f9409c2..bde12b11 100644 --- a/src/file_link.zig +++ b/src/file_link.zig @@ -43,11 +43,6 @@ 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; diff --git a/src/file_type_lsp.zig b/src/file_type_lsp.zig index d7bf03d3..a359e8a7 100644 --- a/src/file_type_lsp.zig +++ b/src/file_type_lsp.zig @@ -116,7 +116,7 @@ pub const make = .{}; pub const markdown = .{ .language_server = .{ "marksman", "server" }, - .formatter = .{ "prettier", "--parser", "markdown" }, + .formatter = .{ "prettier", "--parser", "markdown", "--prose-wrap", "always", "--print-width", "{{reflow_width}}" }, }; pub const @"markdown-inline" = .{}; @@ -183,7 +183,9 @@ pub const python = .{ pub const regex = .{}; -pub const rpmspec = .{}; +pub const rpmspec = .{ + .language_server = .{ "python3", "-mrpm_spec_language_server", "--stdio" }, +}; pub const rst = .{ .language_server = .{"esbonio"}, @@ -214,7 +216,10 @@ pub const verilog = .{ .formatter = .{ "verible-verilog-format", "-" }, }; -pub const toml = .{}; +pub const toml = .{ + .language_server = .{ "tombi", "lsp" }, + .formatter = .{ "tombi", "format" }, +}; pub const typescript = .{ .language_server = .{ "typescript-language-server", "--stdio" }, @@ -227,13 +232,21 @@ 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 = .{}; +pub const yaml = .{ + .language_server = .{ "yaml-language-server", "--stdio" }, + .formatter = .{ "prettier", "--parser", "yaml" }, +}; pub const zig = .{ .language_server = .{"zls"}, diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index c3c912d1..9ce0934d 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -23,9 +23,7 @@ ["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"], @@ -63,8 +61,8 @@ ["ctrl+shift+tab", "previous_tab"], ["ctrl+page_down", "next_tab"], ["ctrl+page_up", "previous_tab"], - ["ctrl+shift+page_down", "move_tab_next_or_scroll_terminal_down"], - ["ctrl+shift+page_up", "move_tab_previous_or_scroll_terminal_up"], + ["ctrl+shift+page_down", "move_tab_next"], + ["ctrl+shift+page_up", "move_tab_previous"], ["ctrl+k e", "switch_buffers"], ["alt+shift+v", "clipboard_history"], ["ctrl+0", "reset_fontsize"], @@ -351,7 +349,6 @@ ["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"], @@ -520,7 +517,6 @@ ["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"], @@ -585,30 +581,5 @@ ["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"] - ] } } diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index a844be61..07d82099 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -81,14 +81,62 @@ ["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"]], + ["", "move_scroll_half_page_up_vim"], ["", "move_scroll_half_page_down_vim"], @@ -159,6 +207,22 @@ ["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"], diff --git a/src/keybind/keybind.zig b/src/keybind/keybind.zig index fd6b4441..632c049c 100644 --- a/src/keybind/keybind.zig +++ b/src/keybind/keybind.zig @@ -485,7 +485,7 @@ const BindingSet = struct { deinit_command: ?Command = null, const KeySyntax = enum { flow, vim }; - const OnMatchFailure = enum { insert, ignore, nothing }; + const OnMatchFailure = enum { insert, ignore }; 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 { } } - pub fn receive(self: *const @This(), _: tp.pid_ref, m: tp.message) error{Exit}!bool { + 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,7 +696,6 @@ 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()); @@ -787,7 +786,6 @@ const BindingSet = struct { else log_keyhints_message(), .ignore => log_keyhints_message(), - .nothing => {}, } globals.current_sequence.clearRetainingCapacity(); globals.current_sequence_egc.clearRetainingCapacity(); diff --git a/src/list_languages.zig b/src/list_languages.zig index 669c7863..8296e87f 100644 --- a/src/list_languages.zig +++ b/src/list_languages.zig @@ -3,7 +3,6 @@ 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; @@ -23,9 +22,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, 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)); + 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)); } try tty_config.setColor(writer, .yellow); @@ -44,42 +43,59 @@ 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); - { - 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); - } + try write_segmented(writer, file_type.extensions, ",", max_extensions_len + 1, tty_config); if (file_type.language_server) |language_server| try write_checkmark(writer, bin_path.can_execute(allocator, language_server[0]), 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); + try write_segmented(writer, file_type.language_server, " ", max_langserver_len + 1, tty_config); if (file_type.formatter) |formatter| try write_checkmark(writer, bin_path.can_execute(allocator, formatter[0]), tty_config); - _ = try argv.write(writer, file_type.formatter); - try tty_config.setColor(writer, .reset); + try write_segmented(writer, file_type.formatter, " ", null, tty_config); 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); diff --git a/src/main.zig b/src/main.zig index b191faf5..054340ca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -882,7 +882,10 @@ 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, 64 * 1024) catch null; + return file.readToEndAlloc(allocator, 512 * 1024) catch |e| { + std.log.err("Error reading theme file: {t}", .{e}); + return null; + }; } pub fn write_theme(theme_name: []const u8, content: []const u8) !void { @@ -895,15 +898,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).init(allocator); + var result: std.ArrayList([]const u8) = .empty; var iter = dir.iterateAssumeFirstIteration(); while (try iter.next()) |entry| { switch (entry.kind) { - .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))), else => continue, } } - return result.toOwnedSlice(); + return result.toOwnedSlice(allocator); } pub fn get_config_dir() ConfigDirError![]const u8 { diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 99df9a73..384d0d7f 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -4,7 +4,7 @@ const cbor = @import("cbor"); const log = @import("log"); const Style = @import("theme").Style; const Color = @import("theme").Color; -pub const vaxis = @import("vaxis"); +const vaxis = @import("vaxis"); const input = @import("input"); const builtin = @import("builtin"); const RGB = @import("color").RGB; @@ -627,6 +627,9 @@ 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__; } diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index 94827b6e..dcf27528 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -3,7 +3,7 @@ pub const log_name = "renderer"; const std = @import("std"); const cbor = @import("cbor"); -pub const vaxis = @import("vaxis"); +const vaxis = @import("vaxis"); const Style = @import("theme").Style; const Color = @import("theme").Color; pub const CursorShape = vaxis.Cell.CursorShape; diff --git a/src/shell.zig b/src/shell.zig index b5ddfa71..7e303908 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -304,7 +304,7 @@ const Process = struct { } }; -pub fn parse_arg0_to_argv(allocator: std.mem.Allocator, arg0: *[]const u8) !tp.message { +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; diff --git a/src/soft_root.zig b/src/soft_root.zig index a792e730..d076e942 100644 --- a/src/soft_root.zig +++ b/src/soft_root.zig @@ -29,6 +29,7 @@ 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; @@ -109,6 +110,10 @@ 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"); } diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 6ee799ef..9a473ecf 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -1,6 +1,7 @@ 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"); @@ -9,7 +10,6 @@ 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,6 +42,114 @@ 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, @@ -237,11 +345,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: *const Self) void { +pub fn focus(self: *Self) void { self.vtable.focus(self.ptr); } -pub fn unfocus(self: *const Self) void { +pub fn unfocus(self: *Self) void { self.vtable.unfocus(self.ptr); } diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 2c4fbe32..74a99168 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -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 -const bracket_search_radius = if (builtin.mode == std.builtin.OptimizeMode.Debug) 8_192 else 65_536; +pub 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,7 +2628,15 @@ pub const Editor = struct { return cursor.test_at(root, is_whitespace, metrics); } - fn is_non_whitespace_at_cursor(root: Buffer.Root, cursor: *const Cursor, metrics: Buffer.Metrics) bool { + 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 { return !cursor.test_at(root, is_whitespace_or_eol, metrics); } @@ -3745,7 +3753,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_at_cursor, metrics); + move_cursor_right_until(root, cursor, is_non_whitespace_or_eol_at_cursor, metrics); } pub fn move_word_left(self: *Self, ctx: Context) Result { @@ -7479,7 +7487,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 or tui.is_keyboard_focused()) switch (btn) { + if (!self.focused) switch (btn) { input.mouse.BUTTON1, input.mouse.BUTTON2, input.mouse.BUTTON3 => _ = tui.set_focus_by_mouse_event(), else => {}, }; diff --git a/src/tui/expansion.zig b/src/tui/expansion.zig index d7050788..427a78f1 100644 --- a/src/tui/expansion.zig +++ b/src/tui/expansion.zig @@ -1,6 +1,5 @@ /// 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 @@ -10,6 +9,7 @@ /// {{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,13 +77,6 @@ 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 &.{}; @@ -170,6 +163,15 @@ 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 &.{}; diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index f597885a..8e2da8a7 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -57,7 +57,7 @@ const Entry = struct { pos_type: editor.PosType, }; -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !Widget { const self = try allocator.create(Self); errdefer allocator.destroy(self); self.* = .{ diff --git a/src/tui/home.zig b/src/tui/home.zig index 478a8b2e..9cdb16f0 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -34,7 +34,6 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\open_terminal \\run_task \\add_task \\open_config @@ -53,7 +52,6 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\open_terminal \\run_task \\add_task \\open_config diff --git a/src/tui/info_view.zig b/src/tui/info_view.zig index 9628df0f..1bc72dc3 100644 --- a/src/tui/info_view.zig +++ b/src/tui/info_view.zig @@ -1,7 +1,6 @@ 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; @@ -20,7 +19,7 @@ widget_type: Widget.Type, const default_widget_type: Widget.Type = .panel; -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !Widget { return create_widget_type(allocator, parent, default_widget_type); } diff --git a/src/tui/inputview.zig b/src/tui/inputview.zig index b1e732c0..00e2b553 100644 --- a/src/tui/inputview.zig +++ b/src/tui/inputview.zig @@ -10,7 +10,6 @@ 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"); @@ -34,7 +33,7 @@ const Entry = struct { }; const Buffer = ArrayList(Entry); -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !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); diff --git a/src/tui/inspector_view.zig b/src/tui/inspector_view.zig index 69bf41b7..a7c26a0c 100644 --- a/src/tui/inspector_view.zig +++ b/src/tui/inspector_view.zig @@ -9,7 +9,6 @@ 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"); @@ -26,7 +25,7 @@ last_node: usize = 0, const Self = @This(); const widget_type: Widget.Type = .panel; -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !Widget { const editor = tui.get_active_editor() orelse return error.NotFound; const self = try allocator.create(Self); errdefer allocator.destroy(self); diff --git a/src/tui/keybindview.zig b/src/tui/keybindview.zig index 40645778..339d7c24 100644 --- a/src/tui/keybindview.zig +++ b/src/tui/keybindview.zig @@ -10,7 +10,6 @@ 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"); @@ -34,7 +33,7 @@ const Entry = struct { }; const Buffer = ArrayList(Entry); -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !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); diff --git a/src/tui/logview.zig b/src/tui/logview.zig index 04ceedc9..92a756a4 100644 --- a/src/tui/logview.zig +++ b/src/tui/logview.zig @@ -6,7 +6,6 @@ const array_list = @import("std").array_list; const tp = @import("thespian"); const cbor = @import("cbor"); -const command = @import("command"); const Plane = @import("renderer").Plane; @@ -40,7 +39,7 @@ const Level = enum { err, }; -pub fn create(allocator: Allocator, parent: Plane, _: command.Context) !Widget { +pub fn create(allocator: Allocator, parent: Plane) !Widget { const self = try allocator.create(Self); errdefer allocator.destroy(self); const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type); diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index f6b18f66..fa07775b 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -34,7 +34,6 @@ 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); @@ -59,7 +58,6 @@ 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, @@ -126,7 +124,6 @@ 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); @@ -261,10 +258,6 @@ 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); } @@ -286,7 +279,6 @@ 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; @@ -298,13 +290,7 @@ pub fn get_panel_height(self: *Self) usize { return self.panel_height orelse self.box().h / 5; } -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 { +fn toggle_panel_view(self: *Self, view: anytype, mode: enum { toggle, enable, disable }) !void { if (self.panels) |panels| { if (self.get_panel(@typeName(view))) |w| { if (mode != .enable) { @@ -316,12 +302,12 @@ fn toggle_panel_view_with_args(self: *Self, view: anytype, mode: PanelToggleMode } } else { if (mode != .disable) - try panels.add(try view.create(self.allocator, self.widgets.plane, ctx)); + try panels.add(try view.create(self.allocator, self.widgets.plane)); } } 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, ctx)); + try panels.add(try view.create(self.allocator, self.widgets.plane)); self.panels = panels; } tui.resize(); @@ -489,7 +475,6 @@ 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(); @@ -926,29 +911,11 @@ 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 open_terminal(self, .{}); + try self.toggle_panel_view(logview, .toggle); } 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); } @@ -979,53 +946,6 @@ 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); @@ -1602,22 +1522,6 @@ 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; @@ -1876,7 +1780,6 @@ 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) diff --git a/src/tui/mode/overlay/completion_dropdown.zig b/src/tui/mode/overlay/completion_dropdown.zig index ceb7a340..a9a20786 100644 --- a/src/tui/mode/overlay/completion_dropdown.zig +++ b/src/tui/mode/overlay/completion_dropdown.zig @@ -87,8 +87,9 @@ pub fn load_entries(self: *Type) !usize { const less_fn = struct { fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { - 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; + 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; return std.mem.order(u8, lhs_str, rhs_str) == .lt; } }.less_fn; diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index ae8f9d26..c6e6723a 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -69,8 +69,9 @@ pub fn load_entries(palette: *Type) !usize { const less_fn = struct { fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { - 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; + 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; return std.mem.order(u8, lhs_str, rhs_str) == .lt; } }.less_fn; diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 4f1a1604..6dbaa1e7 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -38,7 +38,6 @@ 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; } @@ -130,18 +129,13 @@ 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 {}; - (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); + tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } } diff --git a/src/tui/mode/overlay/theme_palette.zig b/src/tui/mode/overlay/theme_palette.zig index a58e9844..1f9396df 100644 --- a/src/tui/mode/overlay/theme_palette.zig +++ b/src/tui/mode/overlay/theme_palette.zig @@ -33,7 +33,8 @@ 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.themes) |theme| { + for (Widget.list_themes()) |theme_name_| { + const theme = Widget.get_theme_by_name(palette.allocator, theme_name_) orelse continue; idx += 1; (try palette.entries.addOne(palette.allocator)).* = .{ .label = theme.description, diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index 38eacbee..043ed35a 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -2,6 +2,14 @@ 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 { @@ -138,6 +146,398 @@ 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; +} diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig deleted file mode 100644 index 8b80de76..00000000 --- a/src/tui/terminal_view.zig +++ /dev/null @@ -1,909 +0,0 @@ -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; -}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 7908d4e7..6b04a30c 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -53,10 +53,8 @@ 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, @@ -64,9 +62,7 @@ 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, @@ -162,9 +158,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, const dark_parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; + const dark_theme = Widget.get_theme_by_name(allocator, conf.theme) orelse Widget.get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; conf.theme = dark_theme.name; - 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; + 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; conf.light_theme = light_theme.name; if (build_options.gui) conf.enable_terminal_cursor = false; @@ -205,8 +201,6 @@ 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; @@ -520,19 +514,15 @@ 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; } @@ -839,54 +829,22 @@ 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; - 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; + return mv.focus_view_by_widget(self.hover_focus orelse return .notfound); } 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.mainview_) |mv| + return if (self.keyboard_focus) |w| + w.send(from, m) + else if (self.mainview_) |mv| mv.send(from, m) else false; @@ -895,6 +853,10 @@ 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); } @@ -902,6 +864,10 @@ 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); } @@ -954,12 +920,10 @@ 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 and !is_keyboard_focused() and self.terminal_focus; + return self.mini_mode_ == null and self.input_mode_outer_ == null; } 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 {}; @@ -1019,21 +983,13 @@ 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_, const parsed_theme = get_theme_by_name(self.allocator, name) orelse { + const theme_ = Widget.get_theme_by_name(self.allocator, name) orelse { self.logger.print("theme not found: {s}", .{name}); return; }; switch (self.color_scheme) { - .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_; - }, + .dark => self.dark_theme = theme_, + .light => self.light_theme = theme_, } self.set_terminal_style(&theme_); self.logger.print("theme: {s}", .{theme_.description}); @@ -1173,13 +1129,13 @@ const cmds = struct { pub const set_theme_meta: Meta = .{ .arguments = &.{.string} }; pub fn theme_next(self: *Self, _: Ctx) Result { - const name = get_next_theme_by_name(self.current_theme().name); + const name = Widget.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 = get_prev_theme_by_name(self.current_theme().name); + const name = Widget.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" }; @@ -1515,30 +1471,6 @@ 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)})) @@ -1563,8 +1495,6 @@ 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 = .{}; @@ -2078,40 +2008,6 @@ 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 @@ -2500,19 +2396,6 @@ 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"); @@ -2717,8 +2600,3 @@ 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; -}