diff --git a/src/buffer/Manager.zig b/src/buffer/Manager.zig index 9097d58..86422b8 100644 --- a/src/buffer/Manager.zig +++ b/src/buffer/Manager.zig @@ -80,7 +80,7 @@ pub fn extract_state(self: *Self, iter: *[]const u8) !void { } } -pub fn get_buffer_for_file(self: *Self, file_path: []const u8) ?*Buffer { +pub fn get_buffer_for_file(self: *const Self, file_path: []const u8) ?*Buffer { return self.buffers.get(file_path); } diff --git a/src/command.zig b/src/command.zig index 8577a9a..16ad9f6 100644 --- a/src/command.zig +++ b/src/command.zig @@ -33,6 +33,7 @@ const Vtable = struct { pub const Metadata = struct { description: []const u8 = &[_]u8{}, arguments: []const ArgumentType = &[_]ArgumentType{}, + icon: ?[]const u8 = null, }; pub const ArgumentType = enum { @@ -188,6 +189,11 @@ pub fn get_arguments(id: ID) ?[]const ArgumentType { return (commands.items[id] orelse return null).meta.arguments; } +pub fn get_icon(id: ID) ?[]const u8 { + if (id >= commands.items.len) return null; + return (commands.items[id] orelse return null).meta.icon; +} + const suppressed_errors = std.StaticStringMap(void).initComptime(.{ .{ "enable_fast_scroll", void }, .{ "disable_fast_scroll", void }, diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 80e7e8f..aafa86e 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -29,7 +29,8 @@ ["f10", "theme_next"], ["f11", "toggle_panel"], ["f12", "toggle_inputview"], - ["alt+!", "select_task"], + ["alt+!", "run_task"], + ["ctrl+1", "add_task"], ["ctrl+tab", "next_tab"], ["ctrl+shift+tab", "previous_tab"], ["ctrl+shift+e", "switch_buffers"], @@ -253,6 +254,7 @@ "inherit": "project", "on_match_failure": "ignore", "press": [ + ["alt+f9", "home_next_widget_style"], ["ctrl+e", "find_file"], ["f", "find_file"], ["e", "find_file"], @@ -283,6 +285,8 @@ }, "overlay/palette": { "press": [ + ["alt+f9", "overlay_next_widget_style"], + ["alt+!", "add_task"], ["ctrl+j", "toggle_panel"], ["ctrl+q", "quit"], ["ctrl+w", "close_file"], diff --git a/src/project_manager.zig b/src/project_manager.zig index 1c4d6e5..925294a 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -27,7 +27,7 @@ const OutOfMemoryError = error{OutOfMemory}; const FileSystemError = error{FileSystem}; const SetCwdError = if (builtin.os.tag == .windows) error{UnrecognizedVolume} else error{}; const CallError = tp.CallError; -const ProjectManagerError = (SpawnError || error{ProjectManagerFailed}); +const ProjectManagerError = (SpawnError || error{ ProjectManagerFailed, InvalidProjectDirectory }); pub fn get() SpawnError!Self { const pid = tp.env.get().proc(module_name); @@ -63,6 +63,7 @@ pub fn open(rel_project_directory: []const u8) (ProjectManagerError || FileSyste const project_directory = std.fs.cwd().realpath(rel_project_directory, &path_buf) catch "(none)"; const current_project = tp.env.get().str("project"); if (std.mem.eql(u8, current_project, project_directory)) return; + if (!root.is_directory(project_directory)) return error.InvalidProjectDirectory; var dir = try std.fs.openDirAbsolute(project_directory, .{}); try dir.setAsCwd(); dir.close(); diff --git a/src/tui/InputBox.zig b/src/tui/InputBox.zig index 1efd3d2..706eaa5 100644 --- a/src/tui/InputBox.zig +++ b/src/tui/InputBox.zig @@ -12,6 +12,8 @@ pub fn Options(context: type) type { label: []const u8 = "Enter text", pos: Widget.Box = .{ .y = 0, .x = 0, .w = 12, .h = 1 }, ctx: Context, + padding: u8 = 1, + icon: ?[]const u8 = null, on_click: *const fn (ctx: context, button: *State(Context)) void = do_nothing, on_render: *const fn (ctx: context, button: *State(Context), theme: *const Widget.Theme) bool = on_render_default, @@ -29,18 +31,21 @@ pub fn Options(context: type) type { self.plane.set_style(style_label); self.plane.fill(" "); self.plane.home(); + for (0..self.opts.padding) |_| _ = self.plane.putchar(" "); + if (self.opts.icon) |icon| + _ = self.plane.print("{s}", .{icon}) catch {}; if (self.text.items.len > 0) { - _ = self.plane.print(" {s} ", .{self.text.items}) catch {}; + _ = self.plane.print("{s} ", .{self.text.items}) catch {}; } else { - _ = self.plane.print(" {s} ", .{self.label.items}) catch {}; + _ = self.plane.print("{s} ", .{self.label.items}) catch {}; } if (self.cursor) |cursor| { const pos: c_int = @intCast(cursor); if (tui.config().enable_terminal_cursor) { - const y, const x = self.plane.rel_yx_to_abs(0, pos + 1); + const y, const x = self.plane.rel_yx_to_abs(0, pos + self.opts.padding + self.icon_width); tui.rdr().cursor_enable(y, x, tui.get_cursor_shape()) catch {}; } else { - self.plane.cursor_move_yx(0, pos + 1) catch return false; + self.plane.cursor_move_yx(0, pos + self.opts.padding + self.icon_width) catch return false; var cell = self.plane.cell_init(); _ = self.plane.at_cursor_cell(&cell) catch return false; cell.set_style(theme.editor_cursor); @@ -68,6 +73,7 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: .opts = opts, .label = std.ArrayList(u8).init(allocator), .text = std.ArrayList(u8).init(allocator), + .icon_width = @intCast(if (opts.icon) |icon| n.egc_chunk_width(icon, 0, 1) else 0), }; try self.label.appendSlice(self.opts.label); self.opts.label = self.label.items; @@ -83,6 +89,7 @@ pub fn State(ctx_type: type) type { label: std.ArrayList(u8), opts: Options(ctx_type), text: std.ArrayList(u8), + icon_width: c_int, cursor: ?usize = 0, const Self = @This(); diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 284dc03..58b832b 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -14,13 +14,15 @@ pub const scroll_lines = 3; pub fn Options(context: type) type { return struct { ctx: Context, + style: Widget.Style.Type, on_click: *const fn (ctx: context, button: *Button.State(*State(Context))) void = do_nothing, on_click4: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, on_click5: *const fn (menu: **State(Context), button: *Button.State(*State(Context))) void = do_nothing_click, on_render: *const fn (ctx: context, button: *Button.State(*State(Context)), theme: *const Widget.Theme, selected: bool) bool = on_render_default, on_layout: *const fn (ctx: context, button: *Button.State(*State(Context))) Widget.Layout = on_layout_default, - on_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = on_resize_default, + prepare_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) Widget.Box = prepare_resize_default, + after_resize: *const fn (ctx: context, menu: *State(Context), box: Widget.Box) void = after_resize_default, on_scroll: ?EventHandler = null, pub const Context = context; @@ -46,23 +48,26 @@ pub fn Options(context: type) type { return .{ .static = 1 }; } - pub fn on_resize_default(_: context, state: *State(Context), box_: Widget.Box) void { + pub fn prepare_resize_default(_: context, state: *State(Context), box_: Widget.Box) Widget.Box { var box = box_; box.h = if (box_.h == 0) state.menu.widgets.items.len else box_.h; - state.container.resize(box); + return box; } + + pub fn after_resize_default(_: context, _: *State(Context), _: Widget.Box) void {} }; } pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: Options(ctx_type)) !*State(ctx_type) { const self = try allocator.create(State(ctx_type)); errdefer allocator.destroy(self); - const container = try WidgetList.createH(allocator, parent, @typeName(@This()), .dynamic); + const container = try WidgetList.createHStyled(allocator, parent, @typeName(@This()), .dynamic, opts.style); self.* = .{ .allocator = allocator, .menu = try WidgetList.createV(allocator, container.plane, @typeName(@This()), .dynamic), .container = container, .container_widget = container.widget(), + .frame_widget = null, .scrollbar = if (tui.config().show_scrollbars) if (opts.on_scroll) |on_scroll| (try scrollbar_v.create(allocator, parent, null, on_scroll)).dynamic_cast(scrollbar_v).? else null else @@ -72,7 +77,8 @@ pub fn create(ctx_type: type, allocator: std.mem.Allocator, parent: Plane, opts: self.menu.ctx = self; self.menu.on_render = State(ctx_type).on_render_menu; container.ctx = self; - container.on_resize = State(ctx_type).on_resize_container; + container.prepare_resize = State(ctx_type).prepare_resize; + container.after_resize = State(ctx_type).after_resize; try container.add(self.menu.widget()); if (self.scrollbar) |sb| try container.add(sb.widget()); return self; @@ -84,6 +90,7 @@ pub fn State(ctx_type: type) type { menu: *WidgetList, container: *WidgetList, container_widget: Widget, + frame_widget: ?Widget, scrollbar: ?*scrollbar_v, opts: options_type, selected: ?usize = null, @@ -146,9 +153,14 @@ pub fn State(ctx_type: type) type { self.render_idx = 0; } - fn on_resize_container(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + fn prepare_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) Widget.Box { const self: *Self = @ptrCast(@alignCast(ctx)); - self.opts.on_resize(self.*.opts.ctx, self, box); + return self.opts.prepare_resize(self.*.opts.ctx, self, box); + } + + fn after_resize(ctx: ?*anyopaque, _: *WidgetList, box: Widget.Box) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + self.opts.after_resize(self.*.opts.ctx, self, box); } pub fn on_layout(self: **Self, button: *Button.State(*Self)) Widget.Layout { @@ -170,7 +182,7 @@ pub fn State(ctx_type: type) type { } pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { - return self.menu.walk(walk_ctx, f, &self.container_widget); + return self.menu.walk(walk_ctx, f, if (self.frame_widget) |*frame| frame else &self.container_widget); } pub fn count(self: *Self) usize { diff --git a/src/tui/MessageFilter.zig b/src/tui/MessageFilter.zig index afc1a37..6676e12 100644 --- a/src/tui/MessageFilter.zig +++ b/src/tui/MessageFilter.zig @@ -14,6 +14,7 @@ pub const Error = (cbor.Error || cbor.JsonEncodeError || error{ ThespianSpawnFailed, NoProject, ProjectManagerFailed, + InvalidProjectDirectory, SendFailed, }); diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 0f0c96f..653f2ea 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -10,6 +10,7 @@ pub const Box = @import("Box.zig"); pub const Theme = @import("theme"); pub const themes = @import("themes").themes; pub const scopes = @import("themes").scopes; +pub const Style = @import("WidgetStyle.zig"); ptr: *anyopaque, plane: *Plane, diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index c40ff2c..012dcd1 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -26,25 +26,35 @@ widgets: ArrayList(WidgetState), layout_: Layout, layout_empty: bool = true, direction: Direction, -box: ?Widget.Box = null, +deco_box: Widget.Box, ctx: ?*anyopaque = null, on_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, after_render: *const fn (ctx: ?*anyopaque, theme: *const Widget.Theme) void = on_render_default, -on_resize: *const fn (ctx: ?*anyopaque, self: *Self, pos_: Widget.Box) void = on_resize_default, +prepare_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) Widget.Box = prepare_resize_default, +after_resize: *const fn (ctx: ?*anyopaque, self: *Self, box: Widget.Box) void = after_resize_default, on_layout: *const fn (ctx: ?*anyopaque, self: *Self) Widget.Layout = on_layout_default, +style: Widget.Style.Type, pub fn createH(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) error{OutOfMemory}!*Self { + return createHStyled(allocator, parent, name, layout_, .none); +} + +pub fn createHStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) error{OutOfMemory}!*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}); + self.* = try init(allocator, parent, name, .horizontal, layout_, Box{}, style); self.plane.hide(); return self; } pub fn createV(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout) !*Self { + return createVStyled(allocator, parent, name, layout_, .none); +} + +pub fn createVStyled(allocator: Allocator, parent: Plane, name: [:0]const u8, layout_: Layout, style: Widget.Style.Type) !*Self { const self = try allocator.create(Self); errdefer allocator.destroy(self); - self.* = try init(allocator, parent, name, .vertical, layout_, Box{}); + self.* = try init(allocator, parent, name, .vertical, layout_, Box{}, style); self.plane.hide(); return self; } @@ -57,15 +67,21 @@ pub fn createBox(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: D return self; } -fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box: Box) !Self { - return .{ - .plane = try Plane.init(&box.opts(name), parent), +fn init(allocator: Allocator, parent: Plane, name: [:0]const u8, dir: Direction, layout_: Layout, box_: Box, style: Widget.Style.Type) !Self { + var self: Self = .{ + .plane = undefined, .parent = parent, .allocator = allocator, .widgets = ArrayList(WidgetState).init(allocator), .layout_ = layout_, .direction = dir, + .style = style, + .deco_box = undefined, }; + const padding = Widget.Style.from_type(self.style).padding; + self.deco_box = self.from_client_box(box_, padding); + self.plane = try Plane.init(&self.deco_box.opts(name), parent); + return self; } pub fn widget(self: *Self) Widget { @@ -147,18 +163,27 @@ pub fn update(self: *Self) void { } pub fn render(self: *Self, theme: *const Widget.Theme) bool { + const widget_style = Widget.Style.from_type(self.style); + const padding = widget_style.padding; for (self.widgets.items) |*w| if (!w.layout.eql(w.widget.layout())) { - self.refresh_layout(); + self.refresh_layout(padding); break; }; self.on_render(self.ctx, theme); + self.render_decoration(theme, widget_style); + + const client_box = self.to_client_box(self.deco_box, padding); var more = false; - for (self.widgets.items) |*w| + for (self.widgets.items) |*w| { + const widget_box = w.widget.box(); + if (client_box.y + client_box.h <= widget_box.y) break; + if (client_box.x + client_box.w <= widget_box.x) break; if (w.widget.render(theme)) { more = true; - }; + } + } self.after_render(self.ctx, theme); return more; @@ -166,6 +191,41 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} +fn render_decoration(self: *Self, theme: *const Widget.Theme, widget_style: *const Widget.Style) void { + const style = Widget.Style.theme_style_from_type(self.style, theme); + const padding = widget_style.padding; + const border = widget_style.border; + const plane = &self.plane; + const box = self.deco_box; + + plane.set_style(style); + plane.fill(" "); + + if (padding.top > 0 and padding.left > 0) put_at_pos(plane, 0, 0, border.nw); + if (padding.top > 0 and padding.right > 0) put_at_pos(plane, 0, box.w - 1, border.ne); + if (padding.bottom > 0 and padding.left > 0 and box.h > 0) put_at_pos(plane, box.h - 1, 0, border.sw); + if (padding.bottom > 0 and padding.right > 0 and box.h > 0) put_at_pos(plane, box.h - 1, box.w - 1, border.se); + + { + const start: usize = if (padding.left > 0) 1 else 0; + const end: usize = if (padding.right > 0 and box.w > 0) box.w - 1 else box.w; + if (padding.top > 0) for (start..end) |x| put_at_pos(plane, 0, x, border.n); + if (padding.bottom > 0) for (start..end) |x| put_at_pos(plane, box.h - 1, x, border.s); + } + + { + const start: usize = if (padding.top > 0) 1 else 0; + const end: usize = if (padding.bottom > 0 and box.h > 0) box.h - 1 else box.h; + if (padding.left > 0) for (start..end) |y| put_at_pos(plane, y, 0, border.w); + if (padding.right > 0) for (start..end) |y| put_at_pos(plane, y, box.w - 1, border.e); + } +} + +inline fn put_at_pos(plane: *Plane, y: usize, x: usize, egc: []const u8) void { + plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; + plane.putchar(egc); +} + pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { if (try m.match(.{ "H", tp.more })) return false; @@ -176,6 +236,13 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { return false; } +fn get_size_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.h, + .horizontal => pos.w, + }; +} + fn get_size_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.h, @@ -183,6 +250,13 @@ fn get_size_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_size_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.w, + .horizontal => pos.h, + }; +} + fn get_size_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.w, @@ -190,6 +264,13 @@ fn get_size_b(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_a_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.y, + .horizontal => pos.x, + }; +} + fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.y, @@ -197,6 +278,13 @@ fn get_loc_a(self: *Self, pos: *Widget.Box) *usize { }; } +fn get_loc_b_const(self: *Self, pos: *const Widget.Box) usize { + return switch (self.direction) { + .vertical => pos.x, + .horizontal => pos.y, + }; +} + fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { return switch (self.direction) { .vertical => &pos.x, @@ -204,28 +292,62 @@ fn get_loc_b(self: *Self, pos: *Widget.Box) *usize { }; } -fn refresh_layout(self: *Self) void { - return if (self.box) |box| self.handle_resize(box); +fn refresh_layout(self: *Self, padding: Widget.Style.Margin) void { + return self.handle_resize(self.to_client_box(self.deco_box, padding)); } -pub fn handle_resize(self: *Self, pos: Widget.Box) void { - self.on_resize(self.ctx, self, pos); +pub fn handle_resize(self: *Self, box: Widget.Box) void { + const padding = Widget.Style.from_type(self.style).padding; + const client_box_ = self.prepare_resize(self.ctx, self, self.to_client_box(box, padding)); + self.deco_box = self.from_client_box(client_box_, padding); + self.do_resize(padding); + self.after_resize(self.ctx, self, self.to_client_box(self.deco_box, padding)); } -fn on_resize_default(_: ?*anyopaque, self: *Self, pos: Widget.Box) void { - self.resize(pos); +pub inline fn to_client_box(_: *const Self, box_: Widget.Box, padding: Widget.Style.Margin) Widget.Box { + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + var box = box_; + box.y += padding.top; + box.h -= if (box.h > total_y_padding) total_y_padding else box.h; + box.x += padding.left; + box.w -= if (box.w > total_x_padding) total_x_padding else box.w; + return box; } +inline fn from_client_box(_: *const Self, box_: Widget.Box, padding: Widget.Style.Margin) Widget.Box { + const total_y_padding = padding.top + padding.bottom; + const total_x_padding = padding.left + padding.right; + const y = if (box_.y < padding.top) padding.top else box_.y; + const x = if (box_.x < padding.left) padding.left else box_.x; + var box = box_; + box.y = y - padding.top; + box.h += total_y_padding; + box.x = x - padding.left; + box.w += total_x_padding; + return box; +} + +fn prepare_resize_default(_: ?*anyopaque, _: *Self, box: Widget.Box) Widget.Box { + return box; +} + +fn after_resize_default(_: ?*anyopaque, _: *Self, _: Widget.Box) void {} + fn on_layout_default(_: ?*anyopaque, self: *Self) Widget.Layout { return self.layout_; } -pub fn resize(self: *Self, pos_: Widget.Box) void { - self.box = pos_; - var pos = pos_; - self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; - self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; - const total = self.get_size_a(&pos).*; +pub fn resize(self: *Self, box: Widget.Box) void { + return self.handle_resize(box); +} + +fn do_resize(self: *Self, padding: Widget.Style.Margin) void { + const client_box = self.to_client_box(self.deco_box, padding); + const deco_box = self.deco_box; + self.plane.move_yx(@intCast(deco_box.y), @intCast(deco_box.x)) catch return; + self.plane.resize_simple(@intCast(deco_box.h), @intCast(deco_box.w)) catch return; + const total = self.get_size_a_const(&client_box); var avail = total; var statics: usize = 0; var dynamics: usize = 0; @@ -245,7 +367,7 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { const dyn_size = avail / if (dynamics > 0) dynamics else 1; const rounded: usize = if (dyn_size * dynamics < avail) avail - dyn_size * dynamics else 0; - var cur_loc: usize = self.get_loc_a(&pos).*; + var cur_loc: usize = self.get_loc_a_const(&client_box); var first = true; for (self.widgets.items) |*w| { @@ -261,8 +383,8 @@ pub fn resize(self: *Self, pos_: Widget.Box) void { self.get_loc_a(&w_pos).* = cur_loc; cur_loc += size; - self.get_size_b(&w_pos).* = self.get_size_b(&pos).*; - self.get_loc_b(&w_pos).* = self.get_loc_b(&pos).*; + self.get_size_b(&w_pos).* = self.get_size_b_const(&client_box); + self.get_loc_b(&w_pos).* = self.get_loc_b_const(&client_box); w.widget.resize(w_pos); } } diff --git a/src/tui/WidgetStyle.zig b/src/tui/WidgetStyle.zig new file mode 100644 index 0000000..6af0bad --- /dev/null +++ b/src/tui/WidgetStyle.zig @@ -0,0 +1,220 @@ +padding: Margin = Margin.@"0", +border: Border = Border.blank, + +pub const Type = enum { + none, + palette, + panel, + home, +}; + +pub const Padding = struct { + pub const Unit = u16; +}; + +pub const Margin = struct { + const Unit = Padding.Unit; + + top: Unit, + bottom: Unit, + left: Unit, + right: Unit, + + const @"0": Margin = .{ .top = 0, .bottom = 0, .left = 0, .right = 0 }; + const @"1": Margin = .{ .top = 1, .bottom = 1, .left = 1, .right = 1 }; + const @"2": Margin = .{ .top = 2, .bottom = 2, .left = 2, .right = 2 }; + const @"3": Margin = .{ .top = 3, .bottom = 3, .left = 3, .right = 3 }; + const @"1/2": Margin = .{ .top = 1, .bottom = 1, .left = 2, .right = 2 }; + const @"2/1": Margin = .{ .top = 2, .bottom = 2, .left = 1, .right = 1 }; + const @"2/3": Margin = .{ .top = 2, .bottom = 2, .left = 3, .right = 3 }; + const @"2/4": Margin = .{ .top = 2, .bottom = 2, .left = 4, .right = 4 }; + + const @"top/bottom/1": Margin = .{ .top = 1, .bottom = 1, .left = 0, .right = 0 }; + const @"top/bottom/2": Margin = .{ .top = 2, .bottom = 2, .left = 0, .right = 0 }; + const @"left/right/1": Margin = .{ .top = 0, .bottom = 0, .left = 1, .right = 1 }; + const @"left/right/2": Margin = .{ .top = 0, .bottom = 0, .left = 2, .right = 2 }; +}; + +pub const Border = struct { + nw: []const u8, + n: []const u8, + ne: []const u8, + e: []const u8, + se: []const u8, + s: []const u8, + sw: []const u8, + w: []const u8, + + const blank: Border = .{ .nw = " ", .n = " ", .ne = " ", .e = " ", .se = " ", .s = " ", .sw = " ", .w = " " }; + const box: Border = .{ .nw = "┌", .n = "─", .ne = "┐", .e = "│", .se = "┘", .s = "─", .sw = "└", .w = "│" }; + const @"rounded box": Border = .{ .nw = "╭", .n = "─", .ne = "╮", .e = "│", .se = "╯", .s = "─", .sw = "╰", .w = "│" }; + const @"double box": Border = .{ .nw = "╔", .n = "═", .ne = "╗", .e = "║", .se = "╝", .s = "═", .sw = "╚", .w = "║" }; + const @"single/double box (top/bottom)": Border = .{ .nw = "╓", .n = "─", .ne = "╖", .e = "║", .se = "╜", .s = "─", .sw = "╙", .w = "║" }; + const @"single/double box (left/right)": Border = .{ .nw = "╒", .n = "═", .ne = "╕", .e = "│", .se = "╛", .s = "═", .sw = "╘", .w = "│" }; + const @"dotted box (braille)": Border = .{ .nw = "⡏", .n = "⠉", .ne = "⢹", .e = "⢸", .se = "⣸", .s = "⣀", .sw = "⣇", .w = "⡇" }; + const @"thick box (half)": Border = .{ .nw = "▛", .n = "▀", .ne = "▜", .e = "▐", .se = "▟", .s = "▄", .sw = "▙", .w = "▌" }; + const @"thick box (sextant)": Border = .{ .nw = "🬕", .n = "🬂", .ne = "🬨", .e = "▐", .se = "🬷", .s = "🬭", .sw = "🬲", .w = "▌" }; + const @"thick box (octant)": Border = .{ .nw = "𜵊", .n = "🮂", .ne = "𜶘", .e = "▐", .se = "𜷕", .s = "▂", .sw = "𜷀", .w = "▌" }; + const @"extra thick box": Border = .{ .nw = "█", .n = "▀", .ne = "█", .e = "█", .se = "█", .s = "▄", .sw = "█", .w = "█" }; + const @"round thick box": Border = .{ .nw = "█", .n = "▀", .ne = "█", .e = "█", .se = "█", .s = "▄", .sw = "█", .w = "█" }; +}; + +const compact: @This() = .{}; + +const spacious: @This() = .{ + .padding = Margin.@"1", + .border = Border.blank, +}; + +const boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.box, +}; + +const rounded_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"rounded box", +}; + +const double_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"double box", +}; + +const single_double_top_bottom_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"single/double box (top/bottom)", +}; + +const single_double_left_right_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"single/double box (left/right)", +}; + +const dotted_boxed: @This() = .{ + .padding = Margin.@"1", + .border = Border.@"dotted box (braille)", +}; + +const thick_boxed: @This() = .{ + .padding = Margin.@"1/2", + .border = Border.@"thick box (octant)", +}; + +const extra_thick_boxed: @This() = .{ + .padding = Margin.@"1/2", + .border = Border.@"extra thick box", +}; + +const bars_top_bottom: @This() = .{ + .padding = Margin.@"top/bottom/1", + .border = Border.@"thick box (octant)", +}; + +const bars_left_right: @This() = .{ + .padding = Margin.@"left/right/1", + .border = Border.@"thick box (octant)", +}; + +pub fn from_type(style_type: Type) *const @This() { + return switch (style_type) { + .none => none_style, + .palette => palette_style, + .panel => panel_style, + .home => home_style, + }; +} + +pub const Styles = enum { + compact, + spacious, + boxed, + double_boxed, + rounded_boxed, + single_double_top_bottom_boxed, + single_double_left_right_boxed, + dotted_boxed, + thick_boxed, + extra_thick_boxed, + bars_top_bottom, + bars_left_right, +}; + +pub fn from_tag(tag: Styles) *const @This() { + return switch (tag) { + .compact => &compact, + .spacious => &spacious, + .boxed => &boxed, + .double_boxed => &double_boxed, + .rounded_boxed => &rounded_boxed, + .single_double_top_bottom_boxed => &single_double_top_bottom_boxed, + .single_double_left_right_boxed => &single_double_left_right_boxed, + .dotted_boxed => &dotted_boxed, + .thick_boxed => &thick_boxed, + .extra_thick_boxed => &extra_thick_boxed, + .bars_top_bottom => &bars_top_bottom, + .bars_left_right => &bars_left_right, + }; +} + +pub fn next_tag(tag: Styles) Styles { + const new_value = @intFromEnum(tag) + 1; + return if (new_value > @intFromEnum(Styles.bars_left_right)) .compact else @enumFromInt(new_value); +} + +pub fn set_type_style(style_type: Type, tag: Styles) void { + const ref = type_style(style_type); + ref.* = from_tag(tag); +} + +pub fn set_next_style(style_type: Type) void { + const tag_ref = type_tag(style_type); + const new_tag = next_tag(tag_ref.*); + const style_ref = type_style(style_type); + tag_ref.* = new_tag; + style_ref.* = from_tag(new_tag); +} + +var none_style: *const @This() = from_tag(none_tag_default); +var palette_style: *const @This() = from_tag(palette_tag_default); +var panel_style: *const @This() = from_tag(panel_tag_default); +var home_style: *const @This() = from_tag(home_tag_default); + +fn type_style(style_type: Type) **const @This() { + return switch (style_type) { + .none => &none_style, + .palette => &palette_style, + .panel => &panel_style, + .home => &home_style, + }; +} + +const none_tag_default: Styles = .compact; +const palette_tag_default: Styles = .compact; +const panel_tag_default: Styles = .compact; +const home_tag_default: Styles = .compact; + +var none_tag: Styles = none_tag_default; +var palette_tag: Styles = palette_tag_default; +var panel_tag: Styles = panel_tag_default; +var home_tag: Styles = home_tag_default; + +fn type_tag(style_type: Type) *Styles { + return switch (style_type) { + .none => &none_tag, + .palette => &palette_tag, + .panel => &panel_tag, + .home => &home_tag, + }; +} + +const Widget = @import("Widget.zig"); + +pub fn theme_style_from_type(style_type: Type, theme: *const Widget.Theme) Widget.Theme.Style { + return switch (style_type) { + .none => theme.editor, + .palette => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor_widget.bg }, + .panel => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor.bg }, + .home => .{ .fg = theme.editor_widget_border.fg, .bg = theme.editor.bg }, + }; +} diff --git a/src/tui/filelist_view.zig b/src/tui/filelist_view.zig index 6f4e824..4108be0 100644 --- a/src/tui/filelist_view.zig +++ b/src/tui/filelist_view.zig @@ -33,8 +33,10 @@ view_rows: usize = 0, view_cols: usize = 0, entries: std.ArrayList(Entry) = undefined, selected: ?usize = null, +box: Widget.Box = .{}, const path_column_ratio = 4; +const widget_style_type: Widget.Style.Type = .panel; const Entry = struct { path: []const u8, @@ -56,6 +58,7 @@ pub fn create(allocator: Allocator, parent: Plane) !Widget { .entries = std.ArrayList(Entry).init(allocator), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = handle_render_menu, .on_scroll = EventHandler.bind(self, Self.handle_scroll), .on_click4 = mouse_click_button4, @@ -84,11 +87,14 @@ fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.St } pub fn handle_resize(self: *Self, pos: Widget.Box) void { + const padding = Widget.Style.from_type(widget_style_type).padding; 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.menu.container_widget.resize(pos); - self.view_rows = pos.h; - self.view_cols = pos.w; + self.box = pos; + self.menu.container.resize(self.box); + const client_box = self.menu.container.to_client_box(pos, padding); + self.view_rows = client_box.h; + self.view_cols = client_box.w; self.update_scrollbar(); } @@ -107,7 +113,7 @@ pub fn add_item(self: *Self, entry_: Entry) !void { const writer = label.writer(); cbor.writeValue(writer, idx) catch return; self.menu.add_item_with_handler(label.items, handle_menu_action) catch return; - self.menu.container_widget.resize(Widget.Box.from(self.plane)); + self.menu.resize(self.box); self.update_scrollbar(); } @@ -160,8 +166,8 @@ fn handle_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), th button.plane.home(); } const entry = &self.entries.items[idx]; - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s} ", .{pointer}) catch {}; + button.plane.set_style(style_label); + tui.render_pointer(&button.plane, selected); var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; const max_len = self.view_cols / path_column_ratio; diff --git a/src/tui/home.zig b/src/tui/home.zig index e00a16a..b1ffcdf 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -30,7 +30,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\select_task + \\run_task \\add_task \\open_config \\open_gui_config @@ -48,7 +48,7 @@ const style = struct { \\open_recent_project \\find_in_files \\open_command_palette - \\select_task + \\run_task \\add_task \\open_config \\open_keybind_config @@ -70,6 +70,8 @@ fire: ?Fire = null, commands: Commands = undefined, menu: *Menu.State(*Self), menu_w: usize = 0, +menu_label_max: usize = 0, +menu_count: usize = 0, menu_len: usize = 0, max_desc_len: usize = 0, input_namespace: []const u8, @@ -79,6 +81,8 @@ home_style_bufs: [][]const u8, const Self = @This(); +const widget_style_type: Widget.Style.Type = .home; + pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { const logger = log.logger("home"); const self = try allocator.create(Self); @@ -95,7 +99,11 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { .allocator = allocator, .parent = parent.plane.*, .plane = n, - .menu = try Menu.create(*Self, allocator, w.plane.*, .{ .ctx = self, .on_render = menu_on_render }), + .menu = try Menu.create(*Self, allocator, w.plane.*, .{ + .ctx = self, + .style = widget_style_type, + .on_render = menu_on_render, + }), .input_namespace = keybind.get_namespace(), .home_style = home_style, .home_style_bufs = home_style_bufs, @@ -103,7 +111,6 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { try self.commands.init(self); var it = std.mem.splitAny(u8, self.home_style.menu_commands, "\n "); while (it.next()) |command_name| { - self.menu_len += 1; const id = command.get_id(command_name) orelse { logger.print("{s} is not defined", .{command_name}); continue; @@ -112,11 +119,14 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { logger.print("{s} has no description", .{command_name}); continue; }; + self.menu_count += 1; var hints = std.mem.splitScalar(u8, keybind_mode.keybind_hints.get(command_name) orelse "", ','); const hint = hints.first(); self.max_desc_len = @max(self.max_desc_len, description.len + hint.len + 5); try self.add_menu_command(command_name, description, hint, self.menu); } + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_len = self.menu_count + padding.top + padding.bottom; self.position_menu(15, 9); return w; } @@ -145,7 +155,9 @@ fn add_menu_command(self: *Self, command_name: []const u8, description: []const _ = try writer.write(leader); try writer.print(" :{s}", .{hint}); const label = fis.getWritten(); - self.menu_w = @max(self.menu_w, label.len + 1); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_label_max = @max(self.menu_label_max, label.len); + self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; } var value = std.ArrayList(u8).init(self.allocator); @@ -228,8 +240,8 @@ fn menu_on_render(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: } else { button.plane.set_style_bg_transparent(style_text); } - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}{s}", .{ pointer, description }) catch {}; + tui.render_pointer(&button.plane, selected); + _ = button.plane.print("{s}", .{description}) catch {}; if (button.active or button.hover or selected) { button.plane.set_style(style_leader); } else { @@ -323,13 +335,13 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { _ = self.plane.print("{s}", .{debug_warning_text}) catch return false; } - const more = self.menu.render(theme); + const more = self.menu.container.render(theme); return more or self.fire != null; } fn position_menu(self: *Self, y: usize, x: usize) void { const box = Widget.Box.from(self.plane); - self.menu.resize(.{ .y = box.y + y, .x = box.x + x, .w = self.menu_w }); + self.menu.resize(.{ .y = box.y + y, .x = box.x + x, .w = self.menu_w, .h = self.menu_len }); } fn center(self: *Self, non_centered: usize, w: usize) usize { @@ -388,6 +400,15 @@ const cmds = struct { } pub const home_menu_activate_meta: Meta = .{}; + pub fn home_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.menu_len = self.menu_count + padding.top + padding.bottom; + self.menu_w = self.menu_label_max + 2 + padding.left + padding.right; + tui.need_render(); + } + pub const home_next_widget_style_meta: Meta = .{}; + pub fn home_sheeran(self: *Self, _: Ctx) Result { self.fire = if (self.fire) |*fire| ret: { fire.deinit(); diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index c726c6f..197fbcf 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -322,6 +322,7 @@ const cmds = struct { if (!try ctx.args.match(.{tp.extract(&project_dir)})) return; try self.check_all_not_dirty(); + try project_manager.open(project_dir); for (self.editors.items) |editor| { editor.clear_diagnostics(); try editor.close_file(.{}); @@ -332,7 +333,6 @@ const cmds = struct { try self.toggle_panel_view(filelist_view, false); self.buffer_manager.deinit(); self.buffer_manager = Buffer.Manager.init(self.allocator); - try project_manager.open(project_dir); const project = tp.env.get().str("project"); tui.rdr().set_terminal_working_directory(project); if (self.top_bar) |bar| _ = try bar.msg(.{ "PRJ", "open" }); diff --git a/src/tui/mode/overlay/buffer_palette.zig b/src/tui/mode/overlay/buffer_palette.zig index 794ca16..ee58850 100644 --- a/src/tui/mode/overlay/buffer_palette.zig +++ b/src/tui/mode/overlay/buffer_palette.zig @@ -12,8 +12,7 @@ const Widget = @import("../../Widget.zig"); pub const label = "Switch buffers"; pub const name = " buffer"; pub const description = "buffer"; -const dirty_indicator = ""; -const hidden_indicator = "-"; +pub const icon = "󰈞 "; pub const Entry = struct { label: []const u8, @@ -27,12 +26,7 @@ pub fn load_entries(palette: *Type) !usize { const buffers = try buffer_manager.list_most_recently_used(palette.allocator); defer palette.allocator.free(buffers); for (buffers) |buffer| { - const indicator = if (buffer.is_dirty()) - dirty_indicator - else if (buffer.is_hidden()) - hidden_indicator - else - ""; + const indicator = tui.get_buffer_state_indicator(buffer); (try palette.entries.addOne()).* = .{ .label = buffer.get_file_path(), .icon = buffer.file_type_icon orelse "", @@ -61,50 +55,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v } pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { - const style_base = theme.editor_widget; - const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; - const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label; - button.plane.set_base_style(style_base); - button.plane.erase(); - button.plane.home(); - button.plane.set_style(style_label); - if (button.active or button.hover or selected) { - button.plane.fill(" "); - button.plane.home(); - } - - button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; - - var iter = button.opts.label; - var file_path_: []const u8 = undefined; - var icon: []const u8 = undefined; - var color: u24 = 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 (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, icon, color); - _ = button.plane.print(" ", .{}) catch {}; - } - button.plane.set_style(style_label); - _ = button.plane.print(" {s} ", .{file_path_}) catch {}; - - var indicator: []const u8 = undefined; - if (!(cbor.matchString(&iter, &indicator) catch false)) - indicator = ""; - button.plane.set_style(style_hint); - _ = button.plane.print_aligned_right(0, "{s} ", .{indicator}) catch {}; - - 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) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; - } else break; - } - return false; + return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); } fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 90f1305..9660df1 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -14,6 +14,7 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ pub const label = label_; pub const name = " file type"; pub const description = "file type"; + pub const icon = " "; pub const Entry = struct { label: []const u8, @@ -84,20 +85,18 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ } button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); var iter = button.opts.label; var description_: []const u8 = undefined; - var icon: []const u8 = undefined; + var icon_: []const u8 = undefined; var color: u24 = undefined; if (!(cbor.matchString(&iter, &description_) catch false)) @panic("invalid file_type description"); - if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid file_type icon"); + if (!(cbor.matchString(&iter, &icon_) catch false)) @panic("invalid file_type icon"); if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid file_type color"); - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, icon, color); - _ = button.plane.print(" ", .{}) catch {}; - } + + tui.render_file_icon(&button.plane, icon_, color); + button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{description_}) catch {}; @@ -119,12 +118,12 @@ pub fn Variant(comptime command: []const u8, comptime label_: []const u8, allow_ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var description_: []const u8 = undefined; - var icon: []const u8 = undefined; + var icon_: []const u8 = undefined; var color: u24 = undefined; var name_: []const u8 = undefined; var iter = button.opts.label; if (!(cbor.matchString(&iter, &description_) catch false)) return; - if (!(cbor.matchString(&iter, &icon) catch false)) return; + if (!(cbor.matchString(&iter, &icon_) catch false)) return; if (!(cbor.matchInt(u24, &iter, &color) catch false)) return; if (!(cbor.matchString(&iter, &name_) catch false)) return; if (!allow_previous) if (previous_file_type) |prev| if (std.mem.eql(u8, prev, name_)) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index d284a84..6126315 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -23,6 +23,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); const Self = @This(); const max_recent_files: usize = 25; +const widget_style_type: Widget.Style.Type = .palette; allocator: std.mem.Allocator, f: usize = 0, @@ -33,7 +34,7 @@ logger: log.Logger, query_pending: bool = false, need_reset: bool = false, need_select_first: bool = true, -longest: usize = 0, +longest: usize, commands: Commands = undefined, buffer_manager: ?*BufferManager, @@ -48,21 +49,25 @@ pub fn create(allocator: std.mem.Allocator) !tui.Mode { .modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ .ctx = self }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = on_render_menu, - .on_resize = on_resize_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_recent_files(max_recent_files); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = max_menu_width() + 2 }); + 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, .{ @@ -85,7 +90,7 @@ pub fn deinit(self: *Self) void { } inline fn menu_width(self: *Self) usize { - return @max(@min(self.longest, max_menu_width()) + 2, inputbox_label.len + 2); + return @max(@min(self.longest + 3, max_menu_width()) + 5, inputbox_label.len + 2); } inline fn menu_pos_x(self: *Self) usize { @@ -99,58 +104,23 @@ inline fn max_menu_width() usize { return @max(15, width - (width / 5)); } -fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { - const style_base = theme.editor_widget; - const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; - const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; - button.plane.set_base_style(style_base); - button.plane.erase(); - button.plane.home(); - button.plane.set_style(style_label); - if (button.active or button.hover or selected) { - button.plane.fill(" "); - button.plane.home(); - } - var file_path: []const u8 = undefined; - var file_type: []const u8 = undefined; - var file_icon: []const u8 = undefined; - var file_color: u24 = undefined; - var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes - if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; - if (!(cbor.matchString(&iter, &file_type) catch false)) file_type = file_type_config.default.name; - if (!(cbor.matchString(&iter, &file_icon) catch false)) file_icon = file_type_config.default.icon; - if (!(cbor.matchInt(u24, &iter, &file_color) catch false)) file_icon = file_type_config.default.icon; - - button.plane.set_style(style_keybind); - const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; - const pointer = if (selected) "⏵" else dirty; - _ = button.plane.print("{s}", .{pointer}) catch {}; - - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, file_icon, file_color); - _ = button.plane.print(" ", .{}) catch {}; - } - - var buf: [std.fs.max_path_bytes]u8 = undefined; - var removed_prefix: usize = 0; - const max_len = max_menu_width() - 2; - button.plane.set_style(style_label); - _ = button.plane.print("{s} ", .{ - if (file_path.len > max_len) root.shorten_path(&buf, file_path, &removed_prefix, max_len) else file_path, - }) catch {}; - - 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) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; - } else break; - } - return false; +fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { + return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); } -fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); +fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: 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: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void { @@ -164,19 +134,19 @@ fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu. fn add_item( self: *Self, file_name: []const u8, - file_type: []const u8, file_icon: []const u8, file_color: u24, + indicator: []const u8, matches: ?[]const u8, ) !void { var label = std.ArrayList(u8).init(self.allocator); defer label.deinit(); const writer = label.writer(); try cbor.writeValue(writer, file_name); - try cbor.writeValue(writer, file_type); try cbor.writeValue(writer, file_icon); try cbor.writeValue(writer, file_color); - if (matches) |cb| _ = try writer.write(cb); + try cbor.writeValue(writer, indicator); + if (matches) |cb| _ = try writer.write(cb) else try cbor.writeValue(writer, &[_]usize{}); try self.menu.add_item_with_handler(label.items, menu_action_open_file); } @@ -206,8 +176,9 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void tp.extract_cbor(&matches), })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, file_type, file_icon, file_color, matches); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + 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, indicator, matches); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; @@ -223,8 +194,9 @@ fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void tp.extract(&file_color), })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, file_type, file_icon, file_color, null); - self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); + 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, indicator, null); + self.do_resize(); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; @@ -398,6 +370,13 @@ const cmds = struct { } pub const overlay_toggle_inputview_meta: Meta = .{}; + pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + self.do_resize(); + tui.need_render(); + } + pub const overlay_next_widget_style_meta: Meta = .{}; + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { return overlay_insert_bytes(self, ctx); } diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 1e0e18a..d5c2472 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -20,6 +20,7 @@ const ModalBackground = @import("../../ModalBackground.zig"); pub const Menu = @import("../../Menu.zig"); const max_menu_width = 80; +const widget_style_type: Widget.Style.Type = .palette; pub fn Create(options: type) type { return struct { @@ -57,8 +58,10 @@ pub fn Create(options: type) type { }), .menu = try Menu.create(*Self, allocator, tui.plane(), .{ .ctx = self, + .style = widget_style_type, .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, - .on_resize = on_resize_menu, + .prepare_resize = prepare_resize_menu, + .after_resize = after_resize_menu, .on_scroll = EventHandler.bind(self, Self.on_scroll), .on_click4 = mouse_click_button4, .on_click5 = mouse_click_button5, @@ -67,6 +70,8 @@ pub fn Create(options: type) type { .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{ .ctx = self, .label = options.label, + .padding = 2, + .icon = if (@hasDecl(options, "icon")) options.icon else null, }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, .view_rows = get_view_rows(tui.screen()), .entries = std.ArrayList(Entry).init(allocator), @@ -130,8 +135,7 @@ pub fn Create(options: type) type { if (!(cbor.matchString(&iter, &hint) catch false)) hint = ""; button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); button.plane.set_style(style_label); _ = button.plane.print("{s} ", .{label}) catch {}; button.plane.set_style(style_hint); @@ -140,25 +144,38 @@ pub fn Create(options: type) type { var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - tui.render_match_cell(&button.plane, 0, index + 1, theme) catch break; + tui.render_match_cell(&button.plane, 0, index + 2, theme) catch break; } else break; } return false; } - fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.do_resize(); - // self.start_query(0) catch {}; + fn prepare_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) Widget.Box { + return self.prepare_resize(); } - fn do_resize(self: *Self) void { + fn prepare_resize(self: *Self) Widget.Box { const screen = tui.screen(); - const w = @max(@min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint, options.label.len + 2); + const w = @max(@min(self.longest + 3, max_menu_width) + 2 + self.longest_hint, options.label.len + 2); const x = if (screen.w > w) (screen.w - w) / 2 else 0; self.view_rows = get_view_rows(screen); const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); - self.menu.container.resize(.{ .y = 0, .x = x, .w = w, .h = h }); + return .{ .y = 0, .x = x, .w = w, .h = h }; + } + + fn after_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { + return self.after_resize(); + } + + fn after_resize(self: *Self) void { self.update_scrollbar(); + // self.start_query(0) catch {}; + } + + fn do_resize(self: *Self, padding: Widget.Style.Margin) void { + const box = self.prepare_resize(); + self.menu.resize(self.menu.container.to_client_box(box, padding)); + self.after_resize(); } fn get_view_rows(screen: Widget.Box) usize { @@ -239,7 +256,8 @@ pub fn Create(options: type) type { var i = n; while (i > 0) : (i -= 1) self.menu.select_down(); - self.do_resize(); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.do_resize(padding); tui.refresh_hover(); self.selection_updated(); } @@ -457,7 +475,10 @@ pub fn Create(options: type) type { } } } - pub const palette_menu_delete_item_meta: Meta = .{}; + pub const palette_menu_delete_item_meta: Meta = .{ + .description = "Delete item", + .icon = "󰗨", + }; pub fn palette_menu_activate(self: *Self, _: Ctx) Result { self.menu.activate_selected(); @@ -511,6 +532,14 @@ pub fn Create(options: type) type { } pub const overlay_toggle_inputview_meta: Meta = .{}; + pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result { + Widget.Style.set_next_style(widget_style_type); + const padding = Widget.Style.from_type(widget_style_type).padding; + self.do_resize(padding); + tui.need_render(); + } + pub const overlay_next_widget_style_meta: Meta = .{}; + pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { return overlay_insert_bytes(self, ctx); } diff --git a/src/tui/mode/overlay/task_palette.zig b/src/tui/mode/overlay/task_palette.zig index 0baae7b..8d7368f 100644 --- a/src/tui/mode/overlay/task_palette.zig +++ b/src/tui/mode/overlay/task_palette.zig @@ -33,10 +33,8 @@ pub fn load_entries(palette: *Type) !usize { (try palette.entries.addOne()).* = .{ .label = try palette.allocator.dupe(u8, task) }; } else return error.InvalidTaskMessageField; } - (try palette.entries.addOne()).* = .{ - .label = try palette.allocator.dupe(u8, " Add new task"), - .command = "add_task", - }; + (try palette.entries.addOne()).* = .{ .label = "", .command = "add_task" }; + (try palette.entries.addOne()).* = .{ .label = "", .command = "palette_menu_delete_item" }; return if (palette.entries.items.len == 0) label.len else blk: { var longest: usize = 0; for (palette.entries.items) |item| longest = @max(longest, item.label.len); @@ -60,7 +58,7 @@ pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !v palette.items += 1; } -pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { +pub fn on_render_menu(palette: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { var entry: Entry = undefined; var iter = button.opts.label; // label contains cbor entry object and matches if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) @@ -84,11 +82,30 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget. button.plane.set_style(style_label); button.plane.fill(" "); button.plane.home(); + button.plane.set_style(style_hint); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; + tui.render_pointer(&button.plane, selected); + button.plane.set_style(style_label); - _ = button.plane.print("{s} ", .{entry.label}) catch {}; + if (entry.command) |command_name| blk: { + button.plane.set_style(style_hint); + var label_: std.ArrayListUnmanaged(u8) = .empty; + defer label_.deinit(palette.allocator); + + const id = command.get_id(command_name) orelse break :blk; + if (command.get_icon(id)) |icon| + label_.writer(palette.allocator).print("{s} ", .{icon}) catch {}; + if (command.get_description(id)) |desc| + label_.writer(palette.allocator).print("{s}", .{desc}) catch {}; + _ = button.plane.print("{s} ", .{label_.items}) catch {}; + + const hints = if (tui.input_mode()) |m| m.keybind_hints else @panic("no keybind hints"); + if (hints.get(command_name)) |hint| + _ = button.plane.print_aligned_right(0, "{s} ", .{hint}) catch {}; + } else { + _ = button.plane.print("{s} ", .{entry.label}) catch {}; + } + var index: usize = 0; var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { @@ -103,17 +120,13 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var entry: Entry = undefined; var iter = button.opts.label; if (!(cbor.matchValue(&iter, cbor.extract(&entry)) catch false)) return; - var buffer_name = std.ArrayList(u8).init(menu.*.opts.ctx.allocator); - defer buffer_name.deinit(); - buffer_name.writer().print("*{s}*", .{entry.label}) catch {}; - if (entry.command) |cmd| { + 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", cmd, .{entry.label} }) 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 { - project_manager.add_task(entry.label) catch {}; tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); - tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); + project_manager.add_task(entry.label) catch {}; + tp.self_pid().send(.{ "cmd", "run_task", .{entry.label} }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); } } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 0969e7c..416dc50 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -916,12 +916,11 @@ const cmds = struct { } pub const switch_buffers_meta: Meta = .{ .description = "Switch buffers" }; - pub fn select_task(self: *Self, _: Ctx) Result { - return self.enter_overlay_mode(@import("mode/overlay/task_palette.zig").Type); - } - pub const select_task_meta: Meta = .{ .description = "Run task" }; - pub fn add_task(self: *Self, ctx: Ctx) Result { + var task: []const u8 = undefined; + if (try ctx.args.match(.{tp.extract(&task)})) + return call_add_task(task); + return enter_mini_mode(self, struct { pub const Type = @import("mode/mini/buffer.zig").Create(@This()); pub const create = Type.create; @@ -929,17 +928,42 @@ const cmds = struct { return @import("mode/overlay/task_palette.zig").name; } pub fn select(self_: *Type) void { - project_manager.add_task(self_.input.items) catch |e| { - const logger = log.logger("tui"); - logger.err("add_task", e); - logger.deinit(); - }; + tp.self_pid().send(.{ "cmd", "run_task", .{self_.input.items} }) catch {}; command.executeName("exit_mini_mode", .{}) catch {}; - command.executeName("select_task", .{}) catch {}; } }, ctx); } - pub const add_task_meta: Meta = .{ .description = "Add task" }; + pub const add_task_meta: Meta = .{ + .description = "Add new task", + .arguments = &.{.string}, + .icon = "", + }; + + fn call_add_task(task: []const u8) void { + project_manager.add_task(task) catch |e| { + const logger = log.logger("tui"); + logger.err("add_task", e); + logger.deinit(); + }; + } + + pub fn run_task(self: *Self, ctx: Ctx) Result { + var task: []const u8 = undefined; + if (try ctx.args.match(.{tp.extract(&task)})) { + var buffer_name = std.ArrayList(u8).init(self.allocator); + defer buffer_name.deinit(); + buffer_name.writer().print("*{s}*", .{task}) catch {}; + call_add_task(task); + tp.self_pid().send(.{ "cmd", "create_scratch_buffer", .{ buffer_name.items, "", "conf" } }) catch |e| self.logger.err("task", e); + tp.self_pid().send(.{ "cmd", "shell_execute_stream", .{task} }) catch |e| self.logger.err("task", e); + } else { + return self.enter_overlay_mode(@import("mode/overlay/task_palette.zig").Type); + } + } + pub const run_task_meta: Meta = .{ + .description = "Run a task", + .arguments = &.{.string}, + }; pub fn delete_task(_: *Self, ctx: Ctx) Result { var task: []const u8 = undefined; @@ -1428,7 +1452,19 @@ pub fn message(comptime fmt: anytype, args: anytype) void { tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {}; } +const dirty_indicator = ""; +const hidden_indicator = "-"; + +pub fn get_file_state_indicator(buffer_manager: *const @import("Buffer").Manager, file_name: []const u8) []const u8 { + return if (buffer_manager.get_buffer_for_file(file_name)) |buffer| get_buffer_state_indicator(buffer) else ""; +} + +pub fn get_buffer_state_indicator(buffer: *const @import("Buffer")) []const u8 { + return if (buffer.is_dirty()) dirty_indicator else if (buffer.is_hidden()) hidden_indicator else ""; +} + pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) void { + if (!config().show_fileicons) return; var cell = self.cell_init(); _ = self.at_cursor_cell(&cell) catch return; if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) { @@ -1437,6 +1473,7 @@ pub fn render_file_icon(self: *renderer.Plane, icon: []const u8, color: u24) voi _ = self.cell_load(&cell, icon) catch {}; _ = self.putc(&cell) catch {}; self.cursor_move_rel(0, 1) catch {}; + _ = self.print(" ", .{}) catch {}; } pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *const Widget.Theme) !void { @@ -1447,6 +1484,56 @@ pub fn render_match_cell(self: *renderer.Plane, y: usize, x: usize, theme_: *con _ = self.putc(&cell) catch {}; } +pub fn render_pointer(self: *renderer.Plane, selected: bool) void { + const pointer = if (selected) "⏵ " else " "; + _ = self.print("{s}", .{pointer}) catch {}; +} + +pub fn render_file_item_cbor(self: *renderer.Plane, file_item_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(self, selected); + + var iter = file_item_cbor; + var file_path_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = 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"); + + render_file_icon(self, icon, color); + + self.set_style(style_label); + _ = self.print("{s} ", .{file_path_}) catch {}; + + var indicator: []const u8 = undefined; + if (!(cbor.matchString(&iter, &indicator) catch false)) + indicator = ""; + self.set_style(style_hint); + _ = self.print_aligned_right(0, "{s} ", .{indicator}) catch {}; + + 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 + 5, theme_) catch break; + } else break; + } + return false; +} + fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const u8 { const theme_name = self.theme_.name; if (root.read_theme(allocator, theme_name)) |content| {