781 lines
27 KiB
Zig
781 lines
27 KiB
Zig
const std = @import("std");
|
|
const tp = @import("thespian");
|
|
const log = @import("log");
|
|
const config = @import("config");
|
|
const project_manager = @import("project_manager");
|
|
const build_options = @import("build_options");
|
|
const root = @import("root");
|
|
const tracy = @import("tracy");
|
|
const builtin = @import("builtin");
|
|
|
|
pub const renderer = @import("renderer");
|
|
|
|
const command = @import("command.zig");
|
|
const WidgetStack = @import("WidgetStack.zig");
|
|
const Widget = @import("Widget.zig");
|
|
const MessageFilter = @import("MessageFilter.zig");
|
|
const EventHandler = @import("EventHandler.zig");
|
|
const mainview = @import("mainview.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const ArrayList = std.ArrayList;
|
|
const eql = std.mem.eql;
|
|
const STDIN_FILENO = std.os.STDIN_FILENO;
|
|
const Timer = std.time.Timer;
|
|
const Mutex = std.Thread.Mutex;
|
|
const maxInt = std.math.maxInt;
|
|
|
|
a: 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,
|
|
input_mode_outer: ?Mode = null,
|
|
input_listeners: EventHandler.List,
|
|
keyboard_focus: ?Widget = null,
|
|
mini_mode: ?MiniModeState = null,
|
|
hover_focus: ?*Widget = null,
|
|
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",
|
|
|
|
const idle_frames = 1;
|
|
|
|
const init_delay = 1; // ms
|
|
|
|
const Self = @This();
|
|
|
|
const Receiver = tp.Receiver(*Self);
|
|
const Commands = command.Collection(cmds);
|
|
|
|
const StartArgs = struct { a: Allocator };
|
|
|
|
pub fn spawn(a: Allocator, ctx: *tp.context, eh: anytype, env: ?*const tp.env) !tp.pid {
|
|
return try ctx.spawn_link(StartArgs{ .a = a }, start, "tui", eh, env);
|
|
}
|
|
|
|
fn start(args: StartArgs) tp.result {
|
|
_ = tp.set_trap(true);
|
|
var self = init(args.a) catch |e| return tp.exit_error(e);
|
|
errdefer self.deinit();
|
|
need_render();
|
|
tp.receive(&self.receiver);
|
|
}
|
|
|
|
fn init(a: Allocator) !*Self {
|
|
var self = try a.create(Self);
|
|
var conf_buf: ?[]const u8 = null;
|
|
var conf = root.read_config(a, &conf_buf);
|
|
defer if (conf_buf) |buf| a.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.input_mode = try a.dupe(u8, conf.input_mode);
|
|
|
|
const frame_rate: usize = @intCast(tp.env.get().num("frame-rate"));
|
|
if (frame_rate != 0)
|
|
conf.frame_rate = frame_rate;
|
|
const frame_time = std.time.us_per_s / conf.frame_rate;
|
|
const frame_clock = try tp.metronome.init(frame_time);
|
|
|
|
self.* = .{
|
|
.a = a,
|
|
.config = conf,
|
|
.rdr = try renderer.init(a, 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(a),
|
|
.input_mode = null,
|
|
.input_listeners = EventHandler.List.init(a),
|
|
.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();
|
|
const n = self.rdr.stdplane();
|
|
|
|
try frame_clock.start();
|
|
try self.commands.init(self);
|
|
errdefer self.deinit();
|
|
if (builtin.os.tag != .windows)
|
|
try self.listen_sigwinch();
|
|
self.mainview = try mainview.create(a, n);
|
|
self.resize();
|
|
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", .{});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
fn init_delayed(self: *Self) tp.result {
|
|
if (self.input_mode) |_| {} else return cmds.enter_mode(self, command.Context.fmt(.{self.config.input_mode}));
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
if (self.input_mode) |*m| m.deinit();
|
|
self.commands.deinit();
|
|
self.mainview.deinit(self.a);
|
|
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.a.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);
|
|
}
|
|
|
|
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 self.deinit();
|
|
|
|
self.receive_safe(from, m) catch |e| {
|
|
if (std.mem.eql(u8, "normal", tp.error_text()))
|
|
return e;
|
|
if (std.mem.eql(u8, "restart", tp.error_text()))
|
|
return e;
|
|
self.logger.err("UI", e);
|
|
};
|
|
}
|
|
|
|
fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) tp.result {
|
|
var input: []const u8 = undefined;
|
|
if (try m.match(.{ "VXS", tp.extract(&input) })) {
|
|
self.rdr.process_input_event(input) catch |e| return tp.exit_error(e);
|
|
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 ctx: cmds.Ctx = .{};
|
|
if (try m.match(.{ "cmd", tp.extract(&cmd) }))
|
|
return command.executeName(cmd, ctx) catch |e| self.logger.err(cmd, 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(.{"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.query_resize() 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.input_mode) |mode|
|
|
mode.handler.send(tp.self_pid(), m) catch |e| self.logger.err("clipboard handler", e);
|
|
return;
|
|
}
|
|
|
|
if (try m.match(.{"render"})) {
|
|
if (!self.frame_clock_running)
|
|
self.render(std.time.microTimestamp());
|
|
return;
|
|
}
|
|
|
|
var counter: usize = undefined;
|
|
if (try m.match(.{ "tick", tp.extract(&counter) })) {
|
|
tracy.frameMark();
|
|
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) {
|
|
self.frame_last_time = current_time;
|
|
self.render(current_time);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (try m.match(.{"init"})) {
|
|
try self.init_delayed();
|
|
self.render(std.time.microTimestamp());
|
|
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", "normal" }))
|
|
return;
|
|
|
|
if (try m.match(.{ "exit", "timeout_error", 125, "Operation aborted." }))
|
|
return;
|
|
|
|
if (try m.match(.{ "exit", "DEADSEND", 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;
|
|
|
|
return tp.unexpected(m);
|
|
}
|
|
|
|
fn render(self: *Self, current_time: i64) void {
|
|
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);
|
|
}
|
|
|
|
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 dispatch_flush_input_event(self: *Self) tp.result {
|
|
var buf: [32]u8 = undefined;
|
|
if (self.input_mode) |mode|
|
|
try mode.handler.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{"F"}) catch |e| return tp.exit_error(e));
|
|
}
|
|
|
|
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.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));
|
|
const m: tp.message = .{ .buf = cbor_msg };
|
|
const from = tp.self_pid();
|
|
self.unrendered_input_events_count += 1;
|
|
self.send_mouse(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, dragging: bool, 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;
|
|
if (dragging) {
|
|
if (self.drag_source == null) self.drag_source = self.find_coord_widget(@intCast(y), @intCast(x));
|
|
} else {
|
|
self.drag_source = null;
|
|
}
|
|
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);
|
|
} else 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));
|
|
}
|
|
self.hover_focus = w;
|
|
_ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e));
|
|
}
|
|
_ = try w.send(from, m);
|
|
} else {
|
|
if (self.hover_focus) |h| {
|
|
var buf: [256]u8 = undefined;
|
|
_ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e));
|
|
}
|
|
self.hover_focus = null;
|
|
}
|
|
}
|
|
|
|
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;
|
|
} else 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));
|
|
}
|
|
self.hover_focus = w;
|
|
_ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e));
|
|
}
|
|
} else {
|
|
if (self.hover_focus) |h| {
|
|
var buf: [256]u8 = undefined;
|
|
_ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e));
|
|
}
|
|
self.hover_focus = null;
|
|
}
|
|
if (self.drag_source) |w| _ = try w.send(from, m);
|
|
}
|
|
|
|
pub fn save_config(self: *const Self) !void {
|
|
try root.write_config(self.config, self.a);
|
|
}
|
|
|
|
const cmds = struct {
|
|
pub const Target = Self;
|
|
const Ctx = command.Context;
|
|
|
|
pub fn restart(_: *Self, _: Ctx) tp.result {
|
|
try tp.self_pid().send("restart");
|
|
}
|
|
|
|
pub fn log_widgets(self: *Self, _: Ctx) tp.result {
|
|
const l = log.logger("z stack");
|
|
defer l.deinit();
|
|
var buf: [256]u8 = undefined;
|
|
var z: i32 = 0;
|
|
var n = self.rdr.stdplane();
|
|
while (n.below()) |n_| : (n = n_) {
|
|
z -= 1;
|
|
l.print("{d} {s}", .{ z, n_.name(&buf) });
|
|
}
|
|
z = 0;
|
|
n = self.rdr.stdplane();
|
|
while (n.above()) |n_| : (n = n_) {
|
|
z += 1;
|
|
l.print("{d} {s}", .{ z, n_.name(&buf) });
|
|
}
|
|
}
|
|
|
|
pub fn theme_next(self: *Self, _: Ctx) tp.result {
|
|
self.theme = get_next_theme_by_name(self.theme.name);
|
|
self.config.theme = self.theme.name;
|
|
self.logger.print("theme: {s}", .{self.theme.description});
|
|
self.save_config() catch |e| return tp.exit_error(e);
|
|
}
|
|
|
|
pub fn theme_prev(self: *Self, _: Ctx) tp.result {
|
|
self.theme = get_prev_theme_by_name(self.theme.name);
|
|
self.config.theme = self.theme.name;
|
|
self.logger.print("theme: {s}", .{self.theme.description});
|
|
self.save_config() catch |e| return tp.exit_error(e);
|
|
}
|
|
|
|
pub fn toggle_whitespace(self: *Self, _: Ctx) tp.result {
|
|
self.config.show_whitespace = !self.config.show_whitespace;
|
|
self.logger.print("show_whitspace {s}", .{if (self.config.show_whitespace) "enabled" else "disabled"});
|
|
self.save_config() catch |e| return tp.exit_error(e);
|
|
var buf: [32]u8 = undefined;
|
|
const m = tp.message.fmtbuf(&buf, .{ "show_whitespace", self.config.show_whitespace }) catch |e| return tp.exit_error(e);
|
|
_ = try self.send_widgets(tp.self_pid(), m);
|
|
}
|
|
|
|
pub fn toggle_input_mode(self: *Self, _: Ctx) tp.result {
|
|
self.config.input_mode = if (std.mem.eql(u8, self.config.input_mode, "flow")) "vim/normal" else "flow";
|
|
self.save_config() catch |e| return tp.exit_error(e);
|
|
return enter_mode(self, Ctx.fmt(.{self.config.input_mode}));
|
|
}
|
|
|
|
pub fn enter_mode(self: *Self, ctx: Ctx) tp.result {
|
|
var mode: []const u8 = undefined;
|
|
if (!try ctx.args.match(.{tp.extract(&mode)}))
|
|
return tp.exit_error(error.InvalidArgument);
|
|
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 = if (std.mem.eql(u8, mode, "vim/normal"))
|
|
@import("mode/input/vim/normal.zig").create(self.a) catch |e| return tp.exit_error(e)
|
|
else if (std.mem.eql(u8, mode, "vim/insert"))
|
|
@import("mode/input/vim/insert.zig").create(self.a) catch |e| return tp.exit_error(e)
|
|
else if (std.mem.eql(u8, mode, "vim/visual"))
|
|
@import("mode/input/vim/visual.zig").create(self.a) catch |e| return tp.exit_error(e)
|
|
else if (std.mem.eql(u8, mode, "flow"))
|
|
@import("mode/input/flow.zig").create(self.a) catch |e| return tp.exit_error(e)
|
|
else if (std.mem.eql(u8, mode, "home"))
|
|
@import("mode/input/home.zig").create(self.a) catch |e| return tp.exit_error(e)
|
|
else ret: {
|
|
self.logger.print("unknown mode {s}", .{mode});
|
|
break :ret @import("mode/input/flow.zig").create(self.a) catch |e| return tp.exit_error(e);
|
|
};
|
|
// self.logger.print("input mode: {s}", .{(self.input_mode orelse return).description});
|
|
}
|
|
|
|
pub fn enter_mode_default(self: *Self, _: Ctx) tp.result {
|
|
return enter_mode(self, Ctx.fmt(.{self.config.input_mode}));
|
|
}
|
|
|
|
pub fn enter_overlay_mode(self: *Self, ctx: Ctx) tp.result {
|
|
var mode: []const u8 = undefined;
|
|
if (!try ctx.args.match(.{tp.extract(&mode)}))
|
|
return tp.exit_error(error.InvalidArgument);
|
|
if (self.mini_mode) |_| try exit_mini_mode(self, .{});
|
|
if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{});
|
|
self.input_mode = if (std.mem.eql(u8, mode, "open_recent")) ret: {
|
|
self.input_mode_outer = self.input_mode;
|
|
break :ret @import("mode/overlay/open_recent.zig").create(self.a) catch |e| return tp.exit_error(e);
|
|
} else {
|
|
self.logger.print("unknown mode {s}", .{mode});
|
|
return;
|
|
};
|
|
// self.logger.print("input mode: {s}", .{(self.input_mode orelse return).description});
|
|
}
|
|
|
|
pub fn exit_overlay_mode(self: *Self, _: Ctx) tp.result {
|
|
if (self.input_mode_outer == null) return;
|
|
defer {
|
|
self.input_mode = self.input_mode_outer;
|
|
self.input_mode_outer = null;
|
|
}
|
|
if (self.input_mode) |*mode| mode.deinit();
|
|
}
|
|
|
|
pub fn enter_find_mode(self: *Self, ctx: Ctx) tp.result {
|
|
return enter_mini_mode(self, @import("mode/mini/find.zig"), ctx);
|
|
}
|
|
|
|
pub fn enter_find_in_files_mode(self: *Self, ctx: Ctx) tp.result {
|
|
return enter_mini_mode(self, @import("mode/mini/find_in_files.zig"), ctx);
|
|
}
|
|
|
|
pub fn enter_goto_mode(self: *Self, ctx: Ctx) tp.result {
|
|
return enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx);
|
|
}
|
|
|
|
pub fn enter_move_to_char_mode(self: *Self, ctx: Ctx) tp.result {
|
|
return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx);
|
|
}
|
|
|
|
pub fn enter_open_file_mode(self: *Self, ctx: Ctx) tp.result {
|
|
return enter_mini_mode(self, @import("mode/mini/open_file.zig"), ctx);
|
|
}
|
|
|
|
const MiniModeFactory = fn (Allocator, Ctx) error{ NotFound, OutOfMemory }!EventHandler;
|
|
|
|
fn enter_mini_mode(self: *Self, comptime mode: anytype, ctx: Ctx) tp.result {
|
|
if (self.mini_mode) |_| try exit_mini_mode(self, .{});
|
|
if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{});
|
|
self.input_mode_outer = self.input_mode;
|
|
errdefer {
|
|
self.input_mode = self.input_mode_outer;
|
|
self.input_mode_outer = null;
|
|
self.mini_mode = null;
|
|
}
|
|
const mode_instance = mode.create(self.a, ctx) catch |e| return tp.exit_error(e);
|
|
self.input_mode = .{
|
|
.handler = mode_instance.handler(),
|
|
.name = mode_instance.name(),
|
|
.description = mode_instance.name(),
|
|
};
|
|
self.mini_mode = .{};
|
|
}
|
|
|
|
pub fn exit_mini_mode(self: *Self, _: Ctx) tp.result {
|
|
if (self.mini_mode) |_| {} else return;
|
|
defer {
|
|
self.input_mode = self.input_mode_outer;
|
|
self.input_mode_outer = null;
|
|
self.mini_mode = null;
|
|
}
|
|
if (self.input_mode) |*mode| mode.deinit();
|
|
}
|
|
};
|
|
|
|
pub const Mode = struct {
|
|
handler: EventHandler,
|
|
name: []const u8,
|
|
description: []const u8,
|
|
line_numbers: enum { absolute, relative } = .absolute,
|
|
|
|
fn deinit(self: *Mode) void {
|
|
self.handler.deinit();
|
|
}
|
|
};
|
|
|
|
pub const MiniModeState = struct {
|
|
text: []const u8 = "",
|
|
cursor: ?usize = null,
|
|
};
|
|
|
|
threadlocal var instance_: ?*Self = null;
|
|
|
|
pub fn current() *Self {
|
|
return if (instance_) |p| p else @panic("tui call out of context");
|
|
}
|
|
|
|
pub fn get_mode() []const u8 {
|
|
return if (current().input_mode) |m| m.name else "INI";
|
|
}
|
|
|
|
pub fn need_render() void {
|
|
tp.self_pid().send(.{"render"}) catch {};
|
|
}
|
|
|
|
pub fn resize(self: *Self) void {
|
|
self.mainview.resize(Widget.Box.from(self.rdr.stdplane()));
|
|
need_render();
|
|
}
|
|
|
|
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| find_scope_style_nofallback(theme, tm_scope) orelse find_scope_style_nofallback(theme, scope) else find_scope_style_nofallback(theme, scope);
|
|
}
|
|
|
|
fn find_scope_style_nofallback(theme: *const Widget.Theme, scope: []const u8) ?Widget.Theme.Token {
|
|
var idx = theme.tokens.len - 1;
|
|
var done = false;
|
|
while (!done) : (if (idx == 0) {
|
|
done = true;
|
|
} else {
|
|
idx -= 1;
|
|
}) {
|
|
const token = theme.tokens[idx];
|
|
const name = Widget.scopes[token.id];
|
|
if (name.len > scope.len)
|
|
continue;
|
|
if (std.mem.eql(u8, name, scope[0..name.len]))
|
|
return token;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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", .tm = "entity.name.type" },
|
|
.{ .ts = "type.defaultLibrary", .tm = "support.type" },
|
|
.{ .ts = "struct", .tm = "storage.type.struct" },
|
|
.{ .ts = "class", .tm = "entity.name.type.class" },
|
|
.{ .ts = "class.defaultLibrary", .tm = "support.class" },
|
|
.{ .ts = "interface", .tm = "entity.name.type.interface" },
|
|
.{ .ts = "enum", .tm = "entity.name.type.enum" },
|
|
.{ .ts = "function", .tm = "entity.name.function" },
|
|
.{ .ts = "function.defaultLibrary", .tm = "support.function" },
|
|
.{ .ts = "method", .tm = "entity.name.function.member" },
|
|
.{ .ts = "macro", .tm = "entity.name.function.macro" },
|
|
.{ .ts = "variable", .tm = "variable.other.readwrite , entity.name.variable" },
|
|
.{ .ts = "variable.readonly", .tm = "variable.other.constant" },
|
|
.{ .ts = "variable.readonly.defaultLibrary", .tm = "support.constant" },
|
|
.{ .ts = "parameter", .tm = "variable.parameter" },
|
|
.{ .ts = "property", .tm = "variable.other.property" },
|
|
.{ .ts = "property.readonly", .tm = "variable.other.constant.property" },
|
|
.{ .ts = "enumMember", .tm = "variable.other.enummember" },
|
|
.{ .ts = "event", .tm = "variable.other.event" },
|
|
|
|
// zig
|
|
.{ .ts = "attribute", .tm = "keyword" },
|
|
.{ .ts = "number", .tm = "constant.numeric" },
|
|
.{ .ts = "conditional", .tm = "keyword.control.conditional" },
|
|
.{ .ts = "operator", .tm = "keyword.operator" },
|
|
.{ .ts = "boolean", .tm = "keyword.constant.bool" },
|
|
.{ .ts = "string", .tm = "string.quoted" },
|
|
.{ .ts = "repeat", .tm = "keyword.control.flow" },
|
|
.{ .ts = "field", .tm = "variable" },
|
|
};
|