flow/src/tui/filelist_view.zig

296 lines
11 KiB
Zig

const std = @import("std");
const cbor = @import("cbor");
const Allocator = @import("std").mem.Allocator;
const Plane = @import("renderer").Plane;
const tp = @import("thespian");
const log = @import("log");
const root = @import("root");
const command = @import("command");
const EventHandler = @import("EventHandler");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const Menu = @import("Menu.zig");
const Button = @import("Button.zig");
const scrollbar_v = @import("scrollbar_v.zig");
const editor = @import("editor.zig");
pub const name = @typeName(Self);
const Self = @This();
const Commands = command.Collection(cmds);
allocator: std.mem.Allocator,
plane: Plane,
menu: *Menu.State(*Self),
logger: log.Logger,
commands: Commands = undefined,
items: usize = 0,
view_pos: usize = 0,
view_rows: usize = 0,
view_cols: usize = 0,
entries: std.ArrayList(Entry) = undefined,
selected: ?usize = null,
const path_column_ratio = 4;
const Entry = struct {
path: []const u8,
begin_line: usize,
begin_pos: usize,
end_line: usize,
end_pos: usize,
lines: []const u8,
severity: editor.Diagnostic.Severity = .Information,
};
pub fn create(allocator: Allocator, parent: Plane) !Widget {
const self: *Self = try allocator.create(Self);
self.* = .{
.allocator = allocator,
.plane = try Plane.init(&(Widget.Box{}).opts(name), parent),
.logger = log.logger(@typeName(Self)),
.entries = std.ArrayList(Entry).init(allocator),
.menu = try Menu.create(*Self, allocator, tui.current().mainview, .{
.ctx = self,
.on_render = handle_render_menu,
.on_scroll = EventHandler.bind(self, Self.handle_scroll),
.on_click4 = mouse_click_button4,
.on_click5 = mouse_click_button5,
}),
};
self.menu.scrollbar.?.style_factory = scrollbar_style;
try self.commands.init(self);
return Widget.to(self);
}
pub fn deinit(self: *Self, allocator: Allocator) void {
self.plane.deinit();
self.commands.deinit();
allocator.destroy(self);
}
fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.Style {
return if (sb.active)
.{ .fg = theme.scrollbar_active.fg, .bg = theme.panel.bg }
else if (sb.hover)
.{ .fg = theme.scrollbar_hover.fg, .bg = theme.panel.bg }
else
.{ .fg = theme.scrollbar.fg, .bg = theme.panel.bg };
}
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
self.menu.container_widget.resize(pos);
self.view_rows = pos.h;
self.view_cols = pos.w;
self.update_scrollbar();
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn, w: *Widget) bool {
return self.menu.container_widget.walk(walk_ctx, f) or f(walk_ctx, w);
}
pub fn add_item(self: *Self, entry_: Entry) !void {
const idx = self.entries.items.len;
const entry = (try self.entries.addOne());
entry.* = entry_;
entry.path = try self.allocator.dupe(u8, entry_.path);
entry.lines = try self.allocator.dupe(u8, entry_.lines);
var label = std.ArrayList(u8).init(self.allocator);
defer label.deinit();
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.update_scrollbar();
}
pub fn reset(self: *Self) void {
for (self.entries.items) |entry| {
self.allocator.free(entry.path);
self.allocator.free(entry.lines);
}
self.entries.clearRetainingCapacity();
self.menu.reset_items();
self.selected = null;
self.menu.selected = null;
self.view_pos = 0;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
self.plane.set_base_style(" ", theme.panel);
self.plane.erase();
self.plane.home();
return self.menu.container_widget.render(theme);
}
fn handle_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool {
const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.panel;
const style_hint: Widget.Theme.Style = .{ .fg = theme.editor_hint.fg, .fs = theme.editor_hint.fs, .bg = style_base.bg };
const style_information: Widget.Theme.Style = .{ .fg = theme.editor_information.fg, .fs = theme.editor_information.fs, .bg = style_base.bg };
const style_warning: Widget.Theme.Style = .{ .fg = theme.editor_warning.fg, .fs = theme.editor_warning.fs, .bg = style_base.bg };
const style_error: Widget.Theme.Style = .{ .fg = theme.editor_error.fg, .fs = theme.editor_error.fs, .bg = style_base.bg };
const style_separator: Widget.Theme.Style = .{ .fg = theme.editor_selection.bg, .bg = style_base.bg };
// const style_error: Widget.Theme.Style = .{ .fg = theme.editor_error.fg, .fs = theme.editor_error.fs, .bg = style_base.bg };
var idx: usize = undefined;
var iter = button.opts.label; // label contains cbor, just the index
if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) {
const json = cbor.toJsonAlloc(self.allocator, iter) catch return false;
defer self.allocator.free(json);
self.logger.print_err(name, "invalid table entry: {s}", .{json});
return false;
}
idx += self.view_pos;
if (idx >= self.entries.items.len) {
return false;
}
button.plane.set_base_style(" ", style_base);
button.plane.erase();
button.plane.home();
const entry = &self.entries.items[idx];
const pointer = if (selected) "" else " ";
_ = button.plane.print("{s} ", .{pointer}) catch {};
var buf: [std.fs.max_path_bytes]u8 = undefined;
var removed_prefix: usize = 0;
const max_len = self.view_cols / path_column_ratio;
button.plane.set_style(style_base);
_ = button.plane.print("{s}:{d}", .{ root.shorten_path(&buf, entry.path, &removed_prefix, max_len - 7), entry.begin_line + 1 }) catch {};
button.plane.cursor_move_yx(0, @intCast(max_len)) catch return false;
button.plane.set_style(style_separator);
_ = button.plane.print("", .{}) catch {};
switch (entry.severity) {
.Hint => button.plane.set_style(style_hint),
.Information => button.plane.set_style(style_information),
.Warning => button.plane.set_style(style_warning),
.Error => button.plane.set_style(style_error),
}
_ = button.plane.print("{s}", .{entry.lines}) catch {};
return false;
}
fn render_cell(plane: *Plane, y: usize, x: usize, style: Widget.Theme.Style) !void {
plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return;
var cell = plane.cell_init();
_ = plane.at_cursor_cell(&cell) catch return;
cell.set_style(style);
_ = plane.putc(&cell) catch {};
}
fn handle_scroll(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!void {
_ = try m.match(.{ "scroll_to", tp.extract(&self.view_pos) });
self.update_selected();
}
fn update_scrollbar(self: *Self) void {
self.menu.scrollbar.?.set(@intCast(self.entries.items.len), @intCast(self.view_rows), @intCast(self.view_pos));
}
fn mouse_click_button4(menu: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void {
const self = &menu.*.opts.ctx.*;
self.selected = if (self.menu.selected) |sel_| sel_ + self.view_pos else self.selected;
if (self.view_pos < Menu.scroll_lines) {
self.view_pos = 0;
} else {
self.view_pos -= Menu.scroll_lines;
}
self.update_selected();
self.update_scrollbar();
}
fn mouse_click_button5(menu: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void {
const self = &menu.*.opts.ctx.*;
self.selected = if (self.menu.selected) |sel_| sel_ + self.view_pos else self.selected;
if (self.view_pos < @max(self.entries.items.len, self.view_rows) - self.view_rows)
self.view_pos += Menu.scroll_lines;
self.update_selected();
self.update_scrollbar();
}
fn update_selected(self: *Self) void {
if (self.selected) |sel| {
if (sel >= self.view_pos and sel < self.view_pos + self.view_rows) {
self.menu.selected = sel - self.view_pos;
} else {
self.menu.selected = null;
}
}
}
fn handle_menu_action(menu: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void {
const self = menu.*.opts.ctx;
var idx: usize = undefined;
var iter = button.opts.label;
if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch return)) {
const json = cbor.toJsonAlloc(self.allocator, button.opts.label) catch return;
self.logger.print_err(name, "invalid table entry: {s}", .{json});
return;
}
idx += self.view_pos;
if (idx >= self.entries.items.len) return;
self.selected = idx;
self.update_selected();
const entry = &self.entries.items[idx];
tp.self_pid().send(.{ "cmd", "navigate", .{
.file = entry.path,
.goto = .{
entry.end_line + 1,
entry.end_pos + 2,
entry.begin_line,
if (entry.begin_pos == 0) 0 else entry.begin_pos + 1,
entry.end_line,
entry.end_pos + 1,
},
} }) catch |e| self.logger.err("navigate", e);
}
fn select_next(self: *Self, dir: enum { up, down }) void {
self.selected = if (self.menu.selected) |sel_| sel_ + self.view_pos else self.selected;
const sel = switch (dir) {
.up => if (self.selected) |sel_| if (sel_ > 0) sel_ - 1 else self.entries.items.len - 1 else self.entries.items.len - 1,
.down => if (self.selected) |sel_| if (sel_ < self.entries.items.len - 1) sel_ + 1 else 0 else 0,
};
self.selected = sel;
if (sel < self.view_pos) self.view_pos = sel;
if (sel > self.view_pos + self.view_rows - 1) self.view_pos = sel - @min(sel, self.view_rows - 1);
self.update_selected();
self.update_scrollbar();
}
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
const Result = command.Result;
pub fn goto_prev_file(self: *Self, _: Ctx) Result {
self.select_next(.up);
self.menu.activate_selected();
}
pub const goto_prev_file_meta = .{ .description = "Navigate to previous file in the file list" };
pub fn goto_next_file(self: *Self, _: Ctx) Result {
self.select_next(.down);
self.menu.activate_selected();
}
pub const goto_next_file_meta = .{ .description = "Navigate to next file in the file list" };
pub fn select_prev_file(self: *Self, _: Ctx) Result {
self.select_next(.up);
}
pub const select_prev_file_meta = .{ .description = "Select previous file in the file list" };
pub fn select_next_file(self: *Self, _: Ctx) Result {
self.select_next(.down);
}
pub const select_next_file_meta = .{ .description = "Select next file in the file list" };
pub fn goto_selected_file(self: *Self, _: Ctx) Result {
if (self.menu.selected == null) return tp.exit_error(error.NoSelectedFile, @errorReturnTrace());
self.menu.activate_selected();
}
pub const goto_selected_file_meta = .{ .interactive = false };
};