Compare commits

...

40 commits

Author SHA1 Message Date
a27c212461
feat: add widget style switching command to open_recent palette 2025-08-13 22:53:26 +02:00
e95b232184
feat: add more widget box styles 2025-08-13 22:50:07 +02:00
c67c0b0c94
feat: add style switching command to palette (alt+f9) 2025-08-13 22:35:58 +02:00
af9b097077
fix: rename run_task command on home screen 2025-08-13 22:33:51 +02:00
17b3f152d5
feat: add style switching command (alt+f9) to home screen 2025-08-13 22:33:17 +02:00
4f912cebeb
feat: add basic widget style switching support 2025-08-13 22:32:31 +02:00
d872e2e734
feat: add palette_menu_delete_item hint to task_palette 2025-08-13 19:20:04 +02:00
8107a0d2b8
feat: display command hints in task_palette 2025-08-13 19:19:45 +02:00
cef495cb53
feat: make add_task directly run the new task if called interactively 2025-08-13 19:18:28 +02:00
7bd9c972e7
feat: add flow mode keybindings for add_task 2025-08-13 19:17:52 +02:00
5ce458e636
feat: add icon to add_task command meta 2025-08-13 19:17:34 +02:00
5f77a48afe
feat: add description and icon for palette_menu_delete_item command 2025-08-13 19:16:32 +02:00
469e10d4d9
feat: add support for icons to command module 2025-08-13 19:13:37 +02:00
cea8edecb9
feat: rename select_task to run_task and add a string parameter 2025-08-13 18:03:17 +02:00
c640c3f04b
fix: task_palette should not pass palette entry text to add_task 2025-08-13 17:52:23 +02:00
2414f3b00f
feat: add string parameter to add_task command for cli usage 2025-08-13 17:45:48 +02:00
12f6b884df
feat: add icon in open recent palette 2025-08-13 17:36:15 +02:00
5294ace5da
feat: add icon in file type palette 2025-08-13 17:35:42 +02:00
3f61e46dfe
feat: add icon in buffer palette 2025-08-13 17:35:22 +02:00
92b1354d4d
feat: add support for input box icons 2025-08-13 17:35:04 +02:00
4d2c7d8a8c
refactor: unify list pointer rendering 2025-08-13 17:34:38 +02:00
c50ab782ec
refactor: share file item menu rendering 2025-08-13 14:44:03 +02:00
f3296482d0
refactor: unify file icon rendering 2025-08-13 14:43:30 +02:00
38236bd93a
refactor: Buffer.Manager.get_buffer_for_file can be const 2025-08-13 14:40:59 +02:00
d07f0f5f35
feat: tweak home and palette styles 2025-08-13 14:03:42 +02:00
ea1ae2228e
fix: home menu rendering 2025-08-13 14:03:07 +02:00
b46e6edbca
fix: add padding to home menu length 2025-08-13 14:02:52 +02:00
bcfd17a0e2
feat: select widget styles based on widget type 2025-08-13 12:58:05 +02:00
fbc49c3dab
fix: home menu length 2025-08-13 12:54:00 +02:00
21f7b14970
refactor: remove widget debug output 2025-08-13 12:09:14 +02:00
5b852fdb3d
fix: prevent crash on invalid project directory 2025-08-13 12:09:06 +02:00
deee1afe13
feat: move widget styles to separate module
And add a few more border styles.
2025-08-13 11:47:49 +02:00
ea5843bc2c
fix: home screen menu padding 2025-08-13 01:46:38 +02:00
794f8be2be
feat: add more padding styles 2025-08-13 01:46:19 +02:00
d2c4fb66bd
fix: typo in WidgetList 2025-08-13 01:28:21 +02:00
b5a506450b
feat: add more boxed styles 2025-08-13 01:27:58 +02:00
d132df2d78
fix: use client_box correctly in filelist_view 2025-08-13 01:13:11 +02:00
c12f384a4f
feat: add thick_box border style 2025-08-13 01:12:15 +02:00
ae39016f03
fix: stop rendering widget list contents that are outside of it's box 2025-08-13 01:10:31 +02:00
83a0adccc7
feat: add menu border styles 2025-08-12 22:29:10 +02:00
19 changed files with 678 additions and 219 deletions

View file

@ -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);
}

View file

@ -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 },

View file

@ -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"],

View file

@ -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();

View file

@ -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();

View file

@ -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 {

View file

@ -14,6 +14,7 @@ pub const Error = (cbor.Error || cbor.JsonEncodeError || error{
ThespianSpawnFailed,
NoProject,
ProjectManagerFailed,
InvalidProjectDirectory,
SendFailed,
});

View file

@ -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,

View file

@ -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);
}
}

220
src/tui/WidgetStyle.zig Normal file
View file

@ -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 },
};
}

View file

@ -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;

View file

@ -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();

View file

@ -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" });

View file

@ -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 {

View file

@ -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_))

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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| {