diff --git a/src/tui/mode/overlay/open_changed.zig b/src/tui/mode/overlay/open_changed.zig new file mode 100644 index 0000000..3be62ba --- /dev/null +++ b/src/tui/mode/overlay/open_changed.zig @@ -0,0 +1,391 @@ +const std = @import("std"); +const tp = @import("thespian"); +const log = @import("log"); +const cbor = @import("cbor"); +const file_type_config = @import("file_type_config"); + +const Plane = @import("renderer").Plane; +const input = @import("input"); +const keybind = @import("keybind"); +const project_manager = @import("project_manager"); +const command = @import("command"); +const EventHandler = @import("EventHandler"); +const BufferManager = @import("Buffer").Manager; + +const tui = @import("../../tui.zig"); +const MessageFilter = @import("../../MessageFilter.zig"); +const Button = @import("../../Button.zig"); +const InputBox = @import("../../InputBox.zig"); +const Menu = @import("../../Menu.zig"); +const Widget = @import("../../Widget.zig"); +const ModalBackground = @import("../../ModalBackground.zig"); + +const Self = @This(); +const max_recent_files: usize = 25; +const widget_type: Widget.Type = .palette; + +allocator: std.mem.Allocator, +f: usize = 0, +modal: *ModalBackground.State(*Self), +menu: *MenuType, +inputbox: *InputBox.State(*Self), +logger: log.Logger, +query_pending: bool = false, +need_reset: bool = false, +need_select_first: bool = true, +longest: usize, +commands: Commands = undefined, +buffer_manager: ?*BufferManager, + +const inputbox_label = "Open changed files by name"; +const MenuType = Menu.Options(*Self).MenuType; +const ButtonType = MenuType.ButtonType; + +pub fn create(allocator: std.mem.Allocator) !tui.Mode { + const mv = tui.mainview() orelse return error.NotFound; + const self = try allocator.create(Self); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ .ctx = self }), + .menu = try Menu.create(*Self, allocator, tui.plane(), .{ + .ctx = self, + .style = widget_type, + .on_render = on_render_menu, + .prepare_resize = prepare_resize_menu, + }), + .logger = log.logger(@typeName(Self)), + .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ + .ctx = self, + .label = inputbox_label, + .padding = 2, + .icon = "󰈞 ", + }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, + .buffer_manager = tui.get_buffer_manager(), + .longest = inputbox_label.len, + }; + try self.commands.init(self); + try tui.message_filters().add(MessageFilter.bind(self, receive_project_manager)); + self.query_pending = true; + try project_manager.request_new_or_modified_files(max_recent_files); + self.do_resize(); + try mv.floating_views.add(self.modal.widget()); + try mv.floating_views.add(self.menu.container_widget); + var mode = try keybind.mode("overlay/palette", allocator, .{ + .insert_command = "overlay_insert_bytes", + }); + mode.event_handler = EventHandler.to_owned(self); + mode.name = "󰈞 open changed"; + return mode; +} + +pub fn deinit(self: *Self) void { + self.commands.deinit(); + tui.message_filters().remove_ptr(self); + if (tui.mainview()) |mv| { + mv.floating_views.remove(self.menu.container_widget); + mv.floating_views.remove(self.modal.widget()); + } + self.logger.deinit(); + self.allocator.destroy(self); +} + +inline fn menu_width(self: *Self) usize { + return @max(@min(self.longest + 3, max_menu_width()) + 5, inputbox_label.len + 2); +} + +inline fn menu_pos_x(self: *Self) usize { + const screen_width = tui.screen().w; + const width = self.menu_width(); + return if (screen_width <= width) 0 else (screen_width - width) / 2; +} + +inline fn max_menu_width() usize { + const width = tui.screen().w; + return @max(15, width - (width / 5)); +} + +fn on_render_menu(_: *Self, button: *ButtonType, theme: *const Widget.Theme, selected: bool) bool { + return tui.render_file_vcs_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); +} + +fn prepare_resize_menu(self: *Self, _: *MenuType, _: Widget.Box) Widget.Box { + return self.prepare_resize(); +} + +fn prepare_resize(self: *Self) Widget.Box { + const w = self.menu_width(); + const x = self.menu_pos_x(); + const h = self.menu.menu.widgets.items.len; + return .{ .y = 0, .x = x, .w = w, .h = h }; +} + +fn do_resize(self: *Self) void { + self.menu.resize(self.prepare_resize()); +} + +fn menu_action_open_file(menu: **MenuType, button: *ButtonType, _: Widget.Pos) void { + var file_path: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &file_path) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); + tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); +} + +fn add_item( + self: *Self, + file_name: []const u8, + file_icon: []const u8, + file_color: u24, + vcs_status: u8, + indicator: []const u8, + matches: ?[]const u8, +) !void { + var label: std.Io.Writer.Allocating = .init(self.allocator); + defer label.deinit(); + const writer = &label.writer; + try cbor.writeValue(writer, file_name); + try cbor.writeValue(writer, file_icon); + try cbor.writeValue(writer, file_color); + try cbor.writeValue(writer, indicator); + try cbor.writeValue(writer, vcs_status); + if (matches) |cb| _ = try writer.write(cb) else try cbor.writeValue(writer, &[_]usize{}); + try self.menu.add_item_with_handler(label.written(), menu_action_open_file); +} + +fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { + if (cbor.match(m.buf, .{ "PRJ", tp.more }) catch false) { + try self.process_project_manager(m); + return true; + } + return false; +} + +fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void { + var file_name: []const u8 = undefined; + var file_type: []const u8 = undefined; + var file_icon: []const u8 = undefined; + var file_color: u24 = undefined; + var vcs_status: u8 = undefined; + var matches: []const u8 = undefined; + var query: []const u8 = undefined; + if (try cbor.match(m.buf, .{ + "PRJ", + "new_or_modified_files", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + tp.extract(&vcs_status), + tp.extract_cbor(&matches), + })) { + if (self.need_reset) self.reset_results(); + const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else ""; + try self.add_item(file_name, file_icon, file_color, vcs_status, indicator, matches); + self.do_resize(); + if (self.need_select_first) { + self.menu.select_down(); + self.need_select_first = false; + } + tui.need_render(); + } else if (try cbor.match(m.buf, .{ + "PRJ", + "new_or_modified_files", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + tp.extract(&vcs_status), + })) { + if (self.need_reset) self.reset_results(); + const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else ""; + try self.add_item(file_name, file_icon, file_color, vcs_status, indicator, null); + self.do_resize(); + if (self.need_select_first) { + self.menu.select_down(); + self.need_select_first = false; + } + tui.need_render(); + } else if (try cbor.match(m.buf, .{ "PRJ", "new_or_modified_files_done", tp.extract(&self.longest), tp.extract(&query) })) { + self.query_pending = false; + self.need_reset = true; + if (!std.mem.eql(u8, self.inputbox.text.items, query)) + try self.start_query(); + } else if (try cbor.match(m.buf, .{ "PRJ", "open_done", tp.string, tp.extract(&self.longest), tp.any })) { + self.query_pending = false; + self.need_reset = true; + try self.start_query(); + } else { + self.logger.err("receive", tp.unexpected(m)); + } +} + +pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { + var text: []const u8 = undefined; + + if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { + self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + return false; +} + +fn reset_results(self: *Self) void { + self.need_reset = false; + self.menu.reset_items(); + self.menu.selected = null; + self.need_select_first = true; +} + +fn start_query(self: *Self) MessageFilter.Error!void { + if (self.query_pending) return; + self.query_pending = true; + try project_manager.query_new_or_modified_files(max_recent_files, self.inputbox.text.items); +} + +fn delete_word(self: *Self) !void { + if (std.mem.lastIndexOfAny(u8, self.inputbox.text.items, "/\\. -_")) |pos| { + self.inputbox.text.shrinkRetainingCapacity(pos); + } else { + self.inputbox.text.shrinkRetainingCapacity(0); + } + self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8); + return self.start_query(); +} + +fn delete_code_point(self: *Self) !void { + if (self.inputbox.text.items.len > 0) { + self.inputbox.text.shrinkRetainingCapacity(self.inputbox.text.items.len - tui.egc_last(self.inputbox.text.items).len); + self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8); + } + return self.start_query(); +} + +fn insert_code_point(self: *Self, c: u32) !void { + var buf: [6]u8 = undefined; + const bytes = try input.ucs32_to_utf8(&[_]u32{c}, &buf); + try self.inputbox.text.appendSlice(self.allocator, buf[0..bytes]); + self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8); + return self.start_query(); +} + +fn insert_bytes(self: *Self, bytes: []const u8) !void { + try self.inputbox.text.appendSlice(self.allocator, bytes); + self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8); + return self.start_query(); +} + +fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { + try command.executeName(name_, ctx); +} + +fn msg(_: *Self, text: []const u8) tp.result { + return tp.self_pid().send(.{ "log", "home", text }); +} + +fn cmd_async(_: *Self, name_: []const u8) tp.result { + return tp.self_pid().send(.{ "cmd", name_ }); +} + +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 palette_menu_down(self: *Self, _: Ctx) Result { + self.menu.select_down(); + } + pub const palette_menu_down_meta: Meta = .{}; + + pub fn palette_menu_up(self: *Self, _: Ctx) Result { + self.menu.select_up(); + } + pub const palette_menu_up_meta: Meta = .{}; + + pub fn palette_menu_pagedown(self: *Self, _: Ctx) Result { + self.menu.select_last(); + } + pub const palette_menu_pagedown_meta: Meta = .{}; + + pub fn palette_menu_pageup(self: *Self, _: Ctx) Result { + self.menu.select_first(); + } + pub const palette_menu_pageup_meta: Meta = .{}; + + pub fn palette_menu_bottom(self: *Self, _: Ctx) Result { + self.menu.select_last(); + } + pub const palette_menu_bottom_meta: Meta = .{}; + + pub fn palette_menu_top(self: *Self, _: Ctx) Result { + self.menu.select_first(); + } + pub const palette_menu_top_meta: Meta = .{}; + + pub fn palette_menu_activate(self: *Self, _: Ctx) Result { + self.menu.activate_selected(); + } + pub const palette_menu_activate_meta: Meta = .{}; + + pub fn palette_menu_activate_quick(self: *Self, _: Ctx) Result { + if (self.menu.selected orelse 0 > 0) self.menu.activate_selected(); + } + pub const palette_menu_activate_quick_meta: Meta = .{}; + + pub fn palette_menu_cancel(self: *Self, _: Ctx) Result { + try self.cmd("exit_overlay_mode", .{}); + } + pub const palette_menu_cancel_meta: Meta = .{}; + + pub fn overlay_delete_word_left(self: *Self, _: Ctx) Result { + self.delete_word() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_delete_word_left_meta: Meta = .{ .description = "Delete word to the left" }; + + pub fn overlay_delete_backwards(self: *Self, _: Ctx) Result { + self.delete_code_point() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; + + pub fn overlay_insert_code_point(self: *Self, ctx: Ctx) Result { + var egc: u32 = 0; + if (!try ctx.args.match(.{tp.extract(&egc)})) + return error.InvalidOpenRecentInsertCodePointArgument; + self.insert_code_point(egc) catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; + + pub fn overlay_insert_bytes(self: *Self, ctx: Ctx) Result { + var bytes: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&bytes)})) + return error.InvalidOpenRecentInsertBytesArgument; + self.insert_bytes(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + pub const overlay_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; + + pub fn overlay_toggle_panel(self: *Self, _: Ctx) Result { + return self.cmd_async("toggle_panel"); + } + pub const overlay_toggle_panel_meta: Meta = .{}; + + pub fn overlay_toggle_inputview(self: *Self, _: Ctx) Result { + return self.cmd_async("toggle_inputview"); + } + pub const overlay_toggle_inputview_meta: Meta = .{}; + + pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { + tui.set_next_style(widget_type); + self.do_resize(); + tui.need_render(); + try tui.save_config(); + } + pub const overlay_next_widget_style_meta: Meta = .{}; + + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { + return overlay_insert_bytes(self, ctx); + } + pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; +}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 8c9551c..a0dbbff 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1032,6 +1032,11 @@ const cmds = struct { } pub const open_recent_meta: Meta = .{ .description = "Open recent" }; + pub fn open_changed_files(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/open_changed.zig")); + } + pub const open_changed_files_meta: Meta = .{ .description = "Open changed files (vcs)" }; + pub fn open_recent_project(_: *Self, _: Ctx) Result { try project_manager.request_recent_projects(); } @@ -1679,6 +1684,15 @@ pub fn render_pointer(self: *renderer.Plane, selected: bool) void { _ = self.print("{s}", .{pointer}) catch {}; } +pub fn render_pointer_vcs(self: *renderer.Plane, vcs_status: u8, selected: bool) void { + const pointer = "⏵"; + if (selected) { + _ = self.print("{s}{c}", .{ pointer, vcs_status }) catch {}; + } else { + _ = self.print("{c} ", .{vcs_status}) catch {}; + } +} + pub fn render_file_item( self: *renderer.Plane, file_path_: []const u8, @@ -1742,6 +1756,72 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, return render_file_item(self, file_path_, icon, color, indicator, matches_cbor, active, selected, hover, theme_); } +pub fn render_file_vcs_item( + self: *renderer.Plane, + file_path_: []const u8, + icon: []const u8, + color: u24, + indicator: []const u8, + vcs_status: u8, + matches_cbor: []const u8, + active: bool, + selected: bool, + hover: bool, + theme_: *const Widget.Theme, +) bool { + const style_base = theme_.editor_widget; + const style_label = if (active) theme_.editor_cursor else if (hover or selected) theme_.editor_selection else theme_.editor_widget; + const style_hint = if (find_scope_style(theme_, "entity.name")) |sty| sty.style else style_label; + self.set_base_style(style_base); + self.erase(); + self.home(); + self.set_style(style_label); + if (active or hover or selected) { + self.fill(" "); + self.home(); + } + + self.set_style(style_hint); + render_pointer_vcs(self, vcs_status, selected); + + const icon_width = render_file_icon(self, icon, color); + + self.set_style(style_label); + _ = self.print("{s} ", .{file_path_}) catch {}; + + self.set_style(style_hint); + _ = self.print_aligned_right(0, "{s} ", .{indicator}) catch {}; + + var iter = matches_cbor; + var index: usize = 0; + var len = cbor.decodeArrayHeader(&iter) catch return false; + while (len > 0) : (len -= 1) { + if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { + render_match_cell(self, 0, index + 2 + icon_width, theme_) catch break; + } else break; + } + return false; +} + +pub fn render_file_vcs_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool { + var iter = file_item_cbor; + var file_path_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + var indicator: []const u8 = undefined; + var vcs_status: u8 = undefined; + var matches_cbor: []const u8 = undefined; + + if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path"); + if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon"); + if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color"); + if (!(cbor.matchString(&iter, &indicator) catch false)) indicator = ""; + if (!(cbor.matchInt(u8, &iter, &vcs_status) catch false)) indicator = " "; + + if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) @panic("invalid matches cbor"); + return render_file_vcs_item(self, file_path_, icon, color, indicator, vcs_status, matches_cbor, active, selected, hover, theme_); +} + fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const u8 { const theme_name = self.current_theme().name; if (root.read_theme(allocator, theme_name)) |content| {