flow/src/tui/tui.zig

1054 lines
38 KiB
Zig

const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
const config = @import("config");
const project_manager = @import("project_manager");
const root = @import("root");
const tracy = @import("tracy");
const builtin = @import("builtin");
pub const renderer = @import("renderer");
const command = @import("command");
const EventHandler = @import("EventHandler");
const keybind = @import("keybind");
const Widget = @import("Widget.zig");
const MessageFilter = @import("MessageFilter.zig");
const mainview = @import("mainview.zig");
const Allocator = std.mem.Allocator;
allocator: Allocator,
rdr: renderer,
config: config,
frame_time: usize, // in microseconds
frame_clock: tp.metronome,
frame_clock_running: bool = false,
frame_last_time: i64 = 0,
receiver: Receiver,
mainview: Widget,
message_filters: MessageFilter.List,
input_mode: ?Mode = null,
delayed_init_done: bool = false,
delayed_init_input_mode: ?[]const u8 = null,
input_mode_outer: ?Mode = null,
input_listeners: EventHandler.List,
keyboard_focus: ?Widget = null,
mini_mode: ?MiniMode = null,
hover_focus: ?*Widget = null,
last_hover_x: c_int = -1,
last_hover_y: c_int = -1,
commands: Commands = undefined,
logger: log.Logger,
drag_source: ?*Widget = null,
theme: Widget.Theme,
idle_frame_count: usize = 0,
unrendered_input_events_count: usize = 0,
init_timer: ?tp.timeout,
sigwinch_signal: ?tp.signal = null,
no_sleep: bool = false,
final_exit: []const u8 = "normal",
render_pending: bool = false,
keepalive_timer: ?tp.Cancellable = null,
mouse_idle_timer: ?tp.Cancellable = null,
const keepalive = std.time.us_per_day * 365; // one year
const idle_frames = 0;
const mouse_idle_time_milliseconds = 3000;
const init_delay = 1; // ms
const Self = @This();
const Receiver = tp.Receiver(*Self);
const Commands = command.Collection(cmds);
const StartArgs = struct { allocator: Allocator };
pub fn spawn(allocator: Allocator, ctx: *tp.context, eh: anytype, env: ?*const tp.env) !tp.pid {
return try ctx.spawn_link(StartArgs{ .allocator = allocator }, start, "tui", eh, env);
}
fn start(args: StartArgs) tp.result {
command.context_check = &context_check;
_ = tp.set_trap(true);
var self = init(args.allocator) catch |e| return tp.exit_error(e, @errorReturnTrace());
errdefer self.deinit();
tp.receive(&self.receiver);
}
fn init(allocator: Allocator) !*Self {
var self = try allocator.create(Self);
var conf_buf: ?[]const u8 = null;
var conf = root.read_config(allocator, &conf_buf);
defer if (conf_buf) |buf| allocator.free(buf);
const theme = get_theme_by_name(conf.theme) orelse get_theme_by_name("dark_modern") orelse return tp.exit("unknown theme");
conf.theme = theme.name;
conf.whitespace_mode = try allocator.dupe(u8, conf.whitespace_mode);
conf.input_mode = try allocator.dupe(u8, conf.input_mode);
conf.top_bar = try allocator.dupe(u8, conf.top_bar);
conf.bottom_bar = try allocator.dupe(u8, conf.bottom_bar);
const frame_rate: usize = @intCast(tp.env.get().num("frame-rate"));
if (frame_rate != 0)
conf.frame_rate = frame_rate;
tp.env.get().num_set("frame-rate", @intCast(conf.frame_rate));
tp.env.get().num_set("lsp-request-timeout", @intCast(conf.lsp_request_timeout));
const frame_time = std.time.us_per_s / conf.frame_rate;
const frame_clock = try tp.metronome.init(frame_time);
self.* = .{
.allocator = allocator,
.config = conf,
.rdr = try renderer.init(allocator, self, tp.env.get().is("no-alternate")),
.frame_time = frame_time,
.frame_clock = frame_clock,
.frame_clock_running = true,
.receiver = Receiver.init(receive, self),
.mainview = undefined,
.message_filters = MessageFilter.List.init(allocator),
.input_listeners = EventHandler.List.init(allocator),
.logger = log.logger("tui"),
.init_timer = try tp.timeout.init_ms(init_delay, tp.message.fmt(.{"init"})),
.theme = theme,
.no_sleep = tp.env.get().is("no-sleep"),
};
instance_ = self;
defer instance_ = null;
self.rdr.handler_ctx = self;
self.rdr.dispatch_input = dispatch_input;
self.rdr.dispatch_mouse = dispatch_mouse;
self.rdr.dispatch_mouse_drag = dispatch_mouse_drag;
self.rdr.dispatch_event = dispatch_event;
try self.rdr.run();
try frame_clock.start();
try self.commands.init(self);
errdefer self.deinit();
switch (builtin.os.tag) {
.windows => {
self.keepalive_timer = try tp.self_pid().delay_send_cancellable(allocator, "tui.keepalive", keepalive, .{"keepalive"});
},
else => {
try self.listen_sigwinch();
},
}
self.mainview = try mainview.create(allocator);
self.resize();
self.set_terminal_style();
try self.rdr.render();
try self.save_config();
if (tp.env.get().is("restore-session")) {
command.executeName("restore_session", .{}) catch |e| self.logger.err("restore_session", e);
self.logger.print("session restored", .{});
}
need_render();
return self;
}
fn init_delayed(self: *Self) !void {
self.delayed_init_done = true;
if (self.input_mode) |_| {} else {
return cmds.enter_mode(self, command.Context.fmt(.{
self.delayed_init_input_mode orelse self.config.input_mode,
}));
}
}
fn deinit(self: *Self) void {
if (self.mouse_idle_timer) |*t| {
t.cancel() catch {};
t.deinit();
self.mouse_idle_timer = null;
}
if (self.keepalive_timer) |*t| {
t.cancel() catch {};
t.deinit();
self.keepalive_timer = null;
}
if (self.input_mode) |*m| {
m.deinit();
self.input_mode = null;
}
if (self.delayed_init_input_mode) |mode| self.allocator.free(mode);
self.commands.deinit();
self.mainview.deinit(self.allocator);
self.message_filters.deinit();
self.input_listeners.deinit();
if (self.frame_clock_running)
self.frame_clock.stop() catch {};
if (self.sigwinch_signal) |sig| sig.deinit();
self.frame_clock.deinit();
self.rdr.stop();
self.rdr.deinit();
self.logger.deinit();
self.allocator.destroy(self);
}
fn listen_sigwinch(self: *Self) tp.result {
if (self.sigwinch_signal) |old| old.deinit();
self.sigwinch_signal = tp.signal.init(std.posix.SIG.WINCH, tp.message.fmt(.{"sigwinch"})) catch |e| return tp.exit_error(e, @errorReturnTrace());
}
fn update_mouse_idle_timer(self: *Self) void {
const delay = std.time.us_per_ms * @as(u64, mouse_idle_time_milliseconds);
if (self.mouse_idle_timer) |*t| {
t.cancel() catch {};
t.deinit();
self.mouse_idle_timer = null;
}
self.mouse_idle_timer = tp.self_pid().delay_send_cancellable(self.allocator, "tui.mouse_idle_timer", delay, .{"MOUSE_IDLE"}) catch return;
}
fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result {
const frame = tracy.initZone(@src(), .{ .name = "tui" });
defer frame.deinit();
instance_ = self;
defer instance_ = null;
errdefer {
var err: tp.ScopedError = .{};
tp.store_error(&err);
defer tp.restore_error(&err);
self.deinit();
}
self.receive_safe(from, m) catch |e| {
if (std.mem.eql(u8, "normal", tp.error_text()))
return error.Exit;
if (std.mem.eql(u8, "restart", tp.error_text()))
return error.Exit;
self.logger.err("UI", tp.exit_error(e, @errorReturnTrace()));
};
}
fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
var input: []const u8 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "VXS", tp.extract(&input), tp.extract(&text) })) {
try self.rdr.process_input_event(input, if (text.len > 0) text else null);
try self.dispatch_flush_input_event();
if (self.unrendered_input_events_count > 0 and !self.frame_clock_running)
need_render();
return;
}
if (self.message_filters.filter(from, m) catch |e| return self.logger.err("filter", e))
return;
var cmd: []const u8 = undefined;
var cmd_id: command.ID = undefined;
var ctx: cmds.Ctx = .{};
if (try m.match(.{ "cmd", tp.extract(&cmd) }))
return command.executeName(cmd, ctx) catch |e| self.logger.err(cmd, e);
if (try m.match(.{ "cmd", tp.extract(&cmd_id) }))
return command.execute(cmd_id, ctx) catch |e| self.logger.err("command", e);
var arg: []const u8 = undefined;
if (try m.match(.{ "cmd", tp.extract(&cmd), tp.extract_cbor(&arg) })) {
ctx.args = .{ .buf = arg };
return command.executeName(cmd, ctx) catch |e| self.logger.err(cmd, e);
}
if (try m.match(.{ "cmd", tp.extract(&cmd_id), tp.extract_cbor(&arg) })) {
ctx.args = .{ .buf = arg };
return command.execute(cmd_id, ctx) catch |e| self.logger.err("command", e);
}
if (try m.match(.{"quit"})) {
project_manager.shutdown();
return;
}
if (try m.match(.{ "project_manager", "shutdown" })) {
return tp.exit(self.final_exit);
}
if (try m.match(.{"restart"})) {
_ = try self.mainview.msg(.{"write_restore_info"});
project_manager.shutdown();
self.final_exit = "restart";
return;
}
if (builtin.os.tag != .windows)
if (try m.match(.{"sigwinch"})) {
try self.listen_sigwinch();
self.rdr.sigwinch() catch |e| return self.logger.err("query_resize", e);
return;
};
if (try m.match(.{"resize"})) {
self.resize();
return;
}
if (try m.match(.{ "system_clipboard", tp.string })) {
if (self.active_event_handler()) |eh|
eh.send(tp.self_pid(), m) catch |e| self.logger.err("clipboard handler", e);
return;
}
if (try m.match(.{"render"})) {
self.render_pending = false;
if (!self.frame_clock_running)
self.render();
return;
}
var counter: usize = undefined;
if (try m.match(.{ "tick", tp.extract(&counter) })) {
self.render();
return;
}
if (try m.match(.{"init"})) {
try self.init_delayed();
self.render();
if (self.init_timer) |*timer| {
timer.deinit();
self.init_timer = null;
} else {
return tp.unexpected(m);
}
return;
}
if (try m.match(.{"focus_in"}))
return;
if (try m.match(.{"focus_out"}))
return;
if (try self.send_widgets(from, m))
return;
if (try m.match(.{ "exit", tp.more })) {
if (try m.match(.{ tp.string, "normal" }) or
try m.match(.{ tp.string, "timeout_error", 125, "Operation aborted." }) or
try m.match(.{ tp.string, "DEADSEND", tp.more }) or
try m.match(.{ tp.string, "error.LspFailed", tp.more }) or
try m.match(.{ tp.string, "error.NoLsp", tp.more }))
return;
}
var msg: []const u8 = undefined;
if (try m.match(.{ "exit", tp.extract(&msg) }) or try m.match(.{ "exit", tp.extract(&msg), tp.more })) {
self.logger.err_msg("tui", msg);
return;
}
if (try m.match(.{ "PRJ", tp.more })) // drop late project manager query responses
return;
if (try m.match(.{"MOUSE_IDLE"})) {
if (self.mouse_idle_timer) |*t| t.deinit();
self.mouse_idle_timer = null;
try self.clear_hover_focus();
return;
}
return tp.unexpected(m);
}
fn render(self: *Self) void {
const current_time = std.time.microTimestamp();
if (current_time < self.frame_last_time) { // clock moved backwards
self.frame_last_time = current_time;
return;
}
const time_delta = current_time - self.frame_last_time;
if (!(time_delta >= self.frame_time * 2 / 3)) {
if (self.frame_clock_running)
return;
}
self.frame_last_time = current_time;
{
const frame = tracy.initZone(@src(), .{ .name = "tui update" });
defer frame.deinit();
self.mainview.update();
}
const more = ret: {
const frame = tracy.initZone(@src(), .{ .name = "tui render" });
defer frame.deinit();
self.rdr.stdplane().erase();
break :ret self.mainview.render(&self.theme);
};
{
const frame = tracy.initZone(@src(), .{ .name = renderer.log_name ++ " render" });
defer frame.deinit();
self.rdr.render() catch |e| self.logger.err("render", e);
tracy.frameMark();
}
self.idle_frame_count = if (self.unrendered_input_events_count > 0)
0
else
self.idle_frame_count + 1;
if (more or self.idle_frame_count < idle_frames or self.no_sleep) {
self.unrendered_input_events_count = 0;
if (!self.frame_clock_running) {
self.frame_clock.start() catch {};
self.frame_clock_running = true;
}
} else {
if (self.frame_clock_running) {
self.frame_clock.stop() catch {};
self.frame_clock_running = false;
}
}
}
fn active_event_handler(self: *Self) ?EventHandler {
const mode = self.input_mode orelse return null;
return mode.event_handler orelse mode.input_handler;
}
fn dispatch_flush_input_event(self: *Self) !void {
var buf: [32]u8 = undefined;
const mode = self.input_mode orelse return;
try mode.input_handler.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"}));
if (mode.event_handler) |eh| try eh.send(tp.self_pid(), try tp.message.fmtbuf(&buf, .{"F"}));
}
fn dispatch_input(ctx: *anyopaque, cbor_msg: []const u8) void {
const self: *Self = @ptrCast(@alignCast(ctx));
const m: tp.message = .{ .buf = cbor_msg };
const from = tp.self_pid();
self.unrendered_input_events_count += 1;
tp.trace(tp.channel.input, m);
self.input_listeners.send(from, m) catch {};
if (self.keyboard_focus) |w|
if (w.send(from, m) catch |e| ret: {
self.logger.err("focus", e);
break :ret false;
})
return;
if (self.input_mode) |mode|
mode.input_handler.send(from, m) catch |e| self.logger.err("input handler", e);
}
fn dispatch_mouse(ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void {
const self: *Self = @ptrCast(@alignCast(ctx));
self.update_mouse_idle_timer();
const m: tp.message = .{ .buf = cbor_msg };
const from = tp.self_pid();
self.unrendered_input_events_count += 1;
const send_func = if (self.drag_source) |_| &send_mouse_drag else &send_mouse;
send_func(self, y, x, from, m) catch |e| self.logger.err("dispatch mouse", e);
self.drag_source = null;
}
fn dispatch_mouse_drag(ctx: *anyopaque, y: c_int, x: c_int, cbor_msg: []const u8) void {
const self: *Self = @ptrCast(@alignCast(ctx));
self.update_mouse_idle_timer();
const m: tp.message = .{ .buf = cbor_msg };
const from = tp.self_pid();
self.unrendered_input_events_count += 1;
if (self.drag_source == null) self.drag_source = self.find_coord_widget(@intCast(y), @intCast(x));
self.send_mouse_drag(y, x, from, m) catch |e| self.logger.err("dispatch mouse", e);
}
fn dispatch_event(ctx: *anyopaque, cbor_msg: []const u8) void {
const self: *Self = @ptrCast(@alignCast(ctx));
const m: tp.message = .{ .buf = cbor_msg };
self.unrendered_input_events_count += 1;
self.dispatch_flush_input_event() catch |e| self.logger.err("dispatch event flush", e);
tp.self_pid().send_raw(m) catch |e| self.logger.err("dispatch event", e);
}
fn find_coord_widget(self: *Self, y: usize, x: usize) ?*Widget {
const Ctx = struct {
widget: ?*Widget = null,
y: usize,
x: usize,
fn find(ctx_: *anyopaque, w: *Widget) bool {
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
if (w.box().is_abs_coord_inside(ctx.y, ctx.x)) {
ctx.widget = w;
return true;
}
return false;
}
};
var ctx: Ctx = .{ .y = y, .x = x };
_ = self.mainview.walk(&ctx, Ctx.find);
return ctx.widget;
}
pub fn is_abs_coord_in_widget(w: *const Widget, y: usize, x: usize) bool {
return w.box().is_abs_coord_inside(y, x);
}
fn is_live_widget_ptr(self: *Self, w_: *Widget) bool {
const Ctx = struct {
w: *Widget,
fn find(ctx_: *anyopaque, w: *Widget) bool {
const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_)));
return ctx.w == w;
}
};
var ctx: Ctx = .{ .w = w_ };
return self.mainview.walk(&ctx, Ctx.find);
}
fn send_widgets(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
const frame = tracy.initZone(@src(), .{ .name = "tui widgets" });
defer frame.deinit();
tp.trace(tp.channel.widget, m);
return if (self.keyboard_focus) |w|
w.send(from, m)
else
self.mainview.send(from, m);
}
fn send_mouse(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) tp.result {
tp.trace(tp.channel.input, m);
_ = self.input_listeners.send(from, m) catch {};
if (self.keyboard_focus) |w| {
_ = try w.send(from, m);
return;
}
if (try self.update_hover(y, x)) |w|
_ = try w.send(from, m);
}
fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) tp.result {
tp.trace(tp.channel.input, m);
_ = self.input_listeners.send(from, m) catch {};
if (self.keyboard_focus) |w| {
_ = try w.send(from, m);
return;
}
_ = try self.update_hover(y, x);
if (self.drag_source) |w| _ = try w.send(from, m);
}
fn update_hover(self: *Self, y: c_int, x: c_int) !?*Widget {
self.last_hover_y = y;
self.last_hover_x = x;
if (y > 0 and x > 0) if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| {
if (if (self.hover_focus) |h| h != w else true) {
var buf: [256]u8 = undefined;
if (self.hover_focus) |h| {
if (self.is_live_widget_ptr(h))
_ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace()));
}
self.hover_focus = w;
_ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e, @errorReturnTrace()));
}
return w;
};
try self.clear_hover_focus();
return null;
}
fn clear_hover_focus(self: *Self) tp.result {
if (self.hover_focus) |h| {
var buf: [256]u8 = undefined;
if (self.is_live_widget_ptr(h))
_ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace()));
}
self.hover_focus = null;
}
pub fn refresh_hover(self: *Self) void {
self.clear_hover_focus() catch return;
_ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {};
}
pub fn save_config(self: *const Self) !void {
try root.write_config(self.config, self.allocator);
}
fn enter_overlay_mode(self: *Self, mode: type) command.Result {
if (self.mini_mode) |_| try cmds.exit_mini_mode(self, .{});
if (self.input_mode_outer) |_| try cmds.exit_overlay_mode(self, .{});
self.input_mode_outer = self.input_mode;
self.input_mode = try mode.create(self.allocator);
self.refresh_hover();
}
fn get_input_mode(self: *Self, mode: anytype, name: []const u8, opts: anytype) !Mode {
const input_handler, const keybind_hints = try mode.create(self.allocator, opts);
return .{
.input_handler = input_handler,
.keybind_hints = keybind_hints,
.name = name,
.line_numbers = if (@hasField(@TypeOf(opts), "line_numbers_relative"))
if (opts.line_numbers_relative)
.relative
else
.absolute
else
.absolute,
.cursor_shape = if (@hasField(@TypeOf(opts), "cursor_shape")) opts.cursor_shape else .block,
};
}
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
const Result = command.Result;
pub fn restart(_: *Self, _: Ctx) Result {
try tp.self_pid().send("restart");
}
pub const restart_meta = .{ .description = "Restart flow (without saving)" };
pub fn force_terminate(self: *Self, _: Ctx) Result {
self.deinit();
root.print_exit_status({}, "FORCE TERMINATE");
root.exit(99);
}
pub const force_terminate_meta = .{ .description = "Force quit without saving" };
pub fn set_theme(self: *Self, ctx: Ctx) Result {
var name: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&name)}))
return tp.exit_error(error.InvalidArgument, null);
self.theme = get_theme_by_name(name) orelse {
self.logger.print("theme not found: {s}", .{name});
return;
};
self.config.theme = self.theme.name;
self.set_terminal_style();
self.logger.print("theme: {s}", .{self.theme.description});
try self.save_config();
}
pub const set_theme_meta = .{ .arguments = &.{.string} };
pub fn theme_next(self: *Self, _: Ctx) Result {
self.theme = get_next_theme_by_name(self.theme.name);
self.config.theme = self.theme.name;
self.set_terminal_style();
self.logger.print("theme: {s}", .{self.theme.description});
try self.save_config();
}
pub const theme_next_meta = .{ .description = "Switch to next color theme" };
pub fn theme_prev(self: *Self, _: Ctx) Result {
self.theme = get_prev_theme_by_name(self.theme.name);
self.config.theme = self.theme.name;
self.set_terminal_style();
self.logger.print("theme: {s}", .{self.theme.description});
try self.save_config();
}
pub const theme_prev_meta = .{ .description = "Switch to previous color theme" };
pub fn toggle_whitespace_mode(self: *Self, _: Ctx) Result {
self.config.whitespace_mode = if (std.mem.eql(u8, self.config.whitespace_mode, "none"))
"indent"
else if (std.mem.eql(u8, self.config.whitespace_mode, "indent"))
"visible"
else
"none";
try self.save_config();
var buf: [32]u8 = undefined;
const m = try tp.message.fmtbuf(&buf, .{ "whitespace_mode", self.config.whitespace_mode });
_ = try self.send_widgets(tp.self_pid(), m);
self.logger.print("whitespace rendering {s}", .{self.config.whitespace_mode});
}
pub const toggle_whitespace_mode_meta = .{ .description = "Switch to next whitespace rendering mode" };
pub fn toggle_input_mode(self: *Self, _: Ctx) Result {
self.config.input_mode = if (std.mem.eql(u8, self.config.input_mode, "flow"))
"vim/normal"
else if (std.mem.eql(u8, self.config.input_mode, "vim/normal"))
"helix/normal"
else
"flow";
try self.save_config();
var it = std.mem.splitScalar(u8, self.config.input_mode, '/');
self.logger.print("input mode {s}", .{it.first()});
return enter_mode(self, Ctx.fmt(.{self.config.input_mode}));
}
pub const toggle_input_mode_meta = .{ .description = "Switch to next input mode" };
pub fn enter_mode(self: *Self, ctx: Ctx) Result {
var mode: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&mode)}))
return tp.exit_error(error.InvalidArgument, null);
if (!self.delayed_init_done) {
self.delayed_init_input_mode = try self.allocator.dupe(u8, mode);
return;
}
if (self.mini_mode) |_| try exit_mini_mode(self, .{});
if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{});
if (self.input_mode) |*m| {
m.deinit();
self.input_mode = null;
}
self.input_mode = if (std.mem.eql(u8, mode, "vim/normal"))
try self.get_input_mode(keybind.mode.input.vim.normal, "NORMAL", .{
.line_numbers_relative = self.config.vim_normal_gutter_line_numbers_relative,
.cursor_shape = .block,
})
else if (std.mem.eql(u8, mode, "vim/insert"))
try self.get_input_mode(keybind.mode.input.vim.insert, "INSERT", .{
.enable_chording = self.config.vim_insert_chording_keybindings,
.line_numbers_relative = self.config.vim_insert_gutter_line_numbers_relative,
.cursor_shape = .beam,
})
else if (std.mem.eql(u8, mode, "vim/visual"))
try self.get_input_mode(keybind.mode.input.vim.visual, "VISUAL", .{
.line_numbers_relative = self.config.vim_visual_gutter_line_numbers_relative,
.cursor_shape = .underline,
})
else if (std.mem.eql(u8, mode, "helix/normal"))
try self.get_input_mode(keybind.mode.input.helix.normal, "NOR", .{
.line_numbers_relative = self.config.vim_normal_gutter_line_numbers_relative,
.cursor_shape = .block,
})
else if (std.mem.eql(u8, mode, "helix/insert"))
try self.get_input_mode(keybind.mode.input.helix.insert, "INS", .{
.line_numbers_relative = self.config.vim_insert_gutter_line_numbers_relative,
.cursor_shape = .beam,
})
else if (std.mem.eql(u8, mode, "helix/select"))
try self.get_input_mode(keybind.mode.input.helix.visual, "SEL", .{
.line_numbers_relative = self.config.vim_visual_gutter_line_numbers_relative,
.cursor_shape = .block,
})
else if (std.mem.eql(u8, mode, "flow"))
try self.get_input_mode(keybind.mode.input.flow, "flow", .{})
else if (std.mem.eql(u8, mode, "home"))
try self.get_input_mode(keybind.mode.input.home, "flow", .{})
else ret: {
self.logger.print("unknown mode {s}", .{mode});
break :ret try self.get_input_mode(keybind.mode.input.flow, "flow", .{});
};
// self.logger.print("input mode: {s}", .{(self.input_mode orelse return).description});
}
pub const enter_mode_meta = .{ .arguments = &.{.string} };
pub fn enter_mode_default(self: *Self, _: Ctx) Result {
return enter_mode(self, Ctx.fmt(.{self.config.input_mode}));
}
pub const enter_mode_default_meta = .{};
pub fn open_command_palette(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/command_palette.zig").Type);
}
pub const open_command_palette_meta = .{};
pub fn insert_command_name(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/list_all_commands_palette.zig").Type);
}
pub const insert_command_name_meta = .{ .description = "Insert command name" };
pub fn open_recent(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/open_recent.zig"));
}
pub const open_recent_meta = .{ .description = "Open recent file" };
pub fn open_recent_project(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/open_recent_project.zig").Type);
}
pub const open_recent_project_meta = .{ .description = "Open recent project" };
pub fn change_theme(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type);
}
pub const change_theme_meta = .{ .description = "Select color theme" };
pub fn exit_overlay_mode(self: *Self, _: Ctx) Result {
if (self.input_mode_outer == null) return;
if (self.input_mode) |*mode| mode.deinit();
self.input_mode = self.input_mode_outer;
self.input_mode_outer = null;
self.refresh_hover();
}
pub const exit_overlay_mode_meta = .{};
pub fn find(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/find.zig"), ctx);
}
pub const find_meta = .{ .description = "Find in current file" };
pub fn find_in_files(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/find_in_files.zig"), ctx);
}
pub const find_in_files_meta = .{ .description = "Find in all project files" };
pub fn goto(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx);
}
pub const goto_meta = .{ .description = "Goto line" };
pub fn move_to_char(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx);
}
pub const move_to_char_meta = .{ .description = "Move cursor to matching character" };
pub fn open_file(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/open_file.zig"), ctx);
}
pub const open_file_meta = .{ .description = "Open file" };
pub fn save_as(self: *Self, ctx: Ctx) Result {
return enter_mini_mode(self, @import("mode/mini/save_as.zig"), ctx);
}
pub const save_as_meta = .{ .description = "Save as" };
fn enter_mini_mode(self: *Self, comptime mode: anytype, ctx: Ctx) !void {
const input_mode, const mini_mode = try mode.create(self.allocator, ctx);
if (self.mini_mode) |_| try exit_mini_mode(self, .{});
if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{});
if (self.input_mode_outer != null) @panic("exit_overlay_mode failed");
self.input_mode_outer = self.input_mode;
self.input_mode = input_mode;
self.mini_mode = mini_mode;
}
pub fn exit_mini_mode(self: *Self, _: Ctx) Result {
if (self.mini_mode) |_| {} else return;
if (self.input_mode) |*mode| mode.deinit();
self.input_mode = self.input_mode_outer;
self.input_mode_outer = null;
self.mini_mode = null;
}
pub const exit_mini_mode_meta = .{};
pub fn open_keybind_config(self: *Self, _: Ctx) Result {
var mode_parts = std.mem.splitScalar(u8, self.config.input_mode, '/');
const namespace_name = mode_parts.first();
const file_name = try keybind.get_or_create_namespace_config_file(self.allocator, namespace_name);
try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } });
self.logger.print("restart flow to use changed key bindings", .{});
}
pub const open_keybind_config_meta = .{ .description = "Edit key bindings" };
pub fn run_async(self: *Self, ctx: Ctx) Result {
var iter = ctx.args.buf;
var len = try cbor.decodeArrayHeader(&iter);
if (len < 1)
return tp.exit_error(error.InvalidArgument, null);
var cmd: []const u8 = undefined;
if (!try cbor.matchValue(&iter, cbor.extract(&cmd)))
return tp.exit_error(error.InvalidArgument, null);
len -= 1;
var args = std.ArrayList([]const u8).init(self.allocator);
defer args.deinit();
while (len > 0) : (len -= 1) {
var arg: []const u8 = undefined;
if (try cbor.matchValue(&iter, cbor.extract_cbor(&arg))) {
try args.append(arg);
} else return tp.exit_error(error.InvalidArgument, null);
}
var args_cb = std.ArrayList(u8).init(self.allocator);
defer args_cb.deinit();
{
const writer = args_cb.writer();
try cbor.writeArrayHeader(writer, args.items.len);
for (args.items) |arg| try writer.writeAll(arg);
}
var msg_cb = std.ArrayList(u8).init(self.allocator);
defer msg_cb.deinit();
{
const writer = msg_cb.writer();
try cbor.writeArrayHeader(writer, 3);
try cbor.writeValue(writer, "cmd");
try cbor.writeValue(writer, cmd);
try writer.writeAll(args_cb.items);
}
try tp.self_pid().send_raw(.{ .buf = msg_cb.items });
}
pub const run_async_meta = .{};
};
pub const MiniMode = struct {
name: []const u8,
text: []const u8 = "",
cursor: ?usize = null,
};
pub const Mode = keybind.Mode;
pub const KeybindHints = keybind.KeybindHints;
threadlocal var instance_: ?*Self = null;
pub fn current() *Self {
return instance_ orelse @panic("tui call out of context");
}
fn context_check() void {
if (instance_ == null) @panic("tui call out of context");
}
pub fn get_mode() []const u8 {
return if (current().mini_mode) |m|
m.name
else if (current().input_mode) |m|
m.name
else
"INI";
}
pub fn reset_drag_context() void {
const self = current();
self.drag_source = null;
}
pub fn need_render() void {
const self = current();
if (!(self.render_pending or self.frame_clock_running)) {
self.render_pending = true;
tp.self_pid().send(.{"render"}) catch {};
}
}
pub fn resize(self: *Self) void {
self.mainview.resize(self.screen());
self.refresh_hover();
need_render();
}
pub fn stdplane(self: *Self) renderer.Plane {
return self.rdr.stdplane();
}
pub fn screen(self: *Self) Widget.Box {
return Widget.Box.from(self.rdr.stdplane());
}
pub fn get_theme_by_name(name: []const u8) ?Widget.Theme {
for (Widget.themes) |theme| {
if (std.mem.eql(u8, theme.name, name))
return theme;
}
return null;
}
pub fn get_next_theme_by_name(name: []const u8) Widget.Theme {
var next = false;
for (Widget.themes) |theme| {
if (next)
return theme;
if (std.mem.eql(u8, theme.name, name))
next = true;
}
return Widget.themes[0];
}
pub fn get_prev_theme_by_name(name: []const u8) Widget.Theme {
var prev: ?Widget.Theme = null;
for (Widget.themes) |theme| {
if (std.mem.eql(u8, theme.name, name))
return prev orelse Widget.themes[Widget.themes.len - 1];
prev = theme;
}
return Widget.themes[Widget.themes.len - 1];
}
pub fn find_scope_style(theme: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token {
return if (find_scope_fallback(scope)) |tm_scope|
scope_to_theme_token(theme, tm_scope) orelse
scope_to_theme_token(theme, scope)
else
scope_to_theme_token(theme, scope);
}
fn scope_to_theme_token(theme: *const Widget.Theme, document_scope: []const u8) ?Widget.Theme.Token {
var idx = theme.tokens.len - 1;
var matched: ?Widget.Theme.Token = null;
var done = false;
while (!done) : (if (idx == 0) {
done = true;
} else {
idx -= 1;
}) {
const token = theme.tokens[idx];
const theme_scope = Widget.scopes[token.id];
const last_matched_scope = if (matched) |tok| Widget.scopes[tok.id] else "";
if (theme_scope.len < last_matched_scope.len) continue;
if (theme_scope.len < document_scope.len and document_scope[theme_scope.len] != '.') continue;
if (theme_scope.len > document_scope.len) continue;
const prefix = @min(theme_scope.len, document_scope.len);
if (std.mem.eql(u8, theme_scope[0..prefix], document_scope[0..prefix]))
matched = token;
}
return matched;
}
fn find_scope_fallback(scope: []const u8) ?[]const u8 {
for (fallbacks) |fallback| {
if (fallback.ts.len > scope.len)
continue;
if (std.mem.eql(u8, fallback.ts, scope[0..fallback.ts.len]))
return fallback.tm;
}
return null;
}
pub const FallBack = struct { ts: []const u8, tm: []const u8 };
pub const fallbacks: []const FallBack = &[_]FallBack{
.{ .ts = "namespace", .tm = "entity.name.namespace" },
.{ .ts = "type.builtin", .tm = "keyword.type" },
.{ .ts = "type.defaultLibrary", .tm = "support.type" },
.{ .ts = "type", .tm = "entity.name.type" },
.{ .ts = "struct", .tm = "storage.type.struct" },
.{ .ts = "class.defaultLibrary", .tm = "support.class" },
.{ .ts = "class", .tm = "entity.name.type.class" },
.{ .ts = "interface", .tm = "entity.name.type.interface" },
.{ .ts = "enum", .tm = "entity.name.type.enum" },
.{ .ts = "enumMember", .tm = "variable.other.enummember" },
.{ .ts = "constant", .tm = "entity.name.constant" },
.{ .ts = "function.defaultLibrary", .tm = "support.function" },
.{ .ts = "function.builtin", .tm = "entity.name.function" },
.{ .ts = "function.call", .tm = "entity.name.function.function-call" },
.{ .ts = "function", .tm = "entity.name.function" },
.{ .ts = "method", .tm = "entity.name.function.member" },
.{ .ts = "macro", .tm = "entity.name.function.macro" },
.{ .ts = "variable.readonly.defaultLibrary", .tm = "support.constant" },
.{ .ts = "variable.readonly", .tm = "variable.other.constant" },
.{ .ts = "variable.member", .tm = "property" },
// .{ .ts = "variable.parameter", .tm = "variable" },
// .{ .ts = "variable", .tm = "entity.name.variable" },
.{ .ts = "label", .tm = "entity.name.label" },
.{ .ts = "parameter", .tm = "variable.parameter" },
.{ .ts = "property.readonly", .tm = "variable.other.constant.property" },
.{ .ts = "property", .tm = "variable.other.property" },
.{ .ts = "event", .tm = "variable.other.event" },
.{ .ts = "attribute", .tm = "keyword" },
.{ .ts = "number", .tm = "constant.numeric" },
.{ .ts = "operator", .tm = "keyword.operator" },
.{ .ts = "boolean", .tm = "keyword.constant.bool" },
.{ .ts = "string", .tm = "string.quoted.double" },
.{ .ts = "character", .tm = "string.quoted.single" },
.{ .ts = "field", .tm = "variable" },
.{ .ts = "repeat", .tm = "keyword.control.repeat" },
.{ .ts = "keyword.conditional", .tm = "keyword.control.conditional" },
.{ .ts = "keyword.repeat", .tm = "keyword.control.repeat" },
.{ .ts = "keyword.modifier", .tm = "keyword.storage" },
.{ .ts = "keyword.type", .tm = "keyword.structure" },
.{ .ts = "keyword.function", .tm = "storage.type.function" },
.{ .ts = "constant.builtin", .tm = "keyword.constant" },
};
fn set_terminal_style(self: *Self) void {
if (self.config.enable_terminal_color_scheme) {
self.rdr.set_terminal_style(self.theme.editor);
self.rdr.set_terminal_cursor_color(self.theme.editor_cursor.bg.?);
}
}
pub fn translate_cursor_shape(in: keybind.CursorShape) renderer.CursorShape {
return switch (in) {
.default => .default,
.block_blink => .block_blink,
.block => .block,
.underline_blink => .underline_blink,
.underline => .underline,
.beam_blink => .beam_blink,
.beam => .beam,
};
}