flow/src/tui/tui.zig

1237 lines
43 KiB
Zig

const std = @import("std");
const nc = @import("notcurses");
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 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,
nc: nc.Context,
config: config,
frame_time: usize, // in microseconds
frame_clock: tp.metronome,
frame_clock_running: bool = false,
frame_last_time: i64 = 0,
fd_stdin: tp.file_descriptor,
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: bool = false,
drag_event: nc.Input = nc.input(),
drag_source: ?*Widget = null,
theme: Widget.Theme,
escape_state: EscapeState = .none,
escape_initial: ?nc.Input = null,
escape_code: ArrayList(u8),
bracketed_paste: bool = false,
bracketed_paste_buffer: ArrayList(u8),
idle_frame_count: usize = 0,
unrendered_input_events_count: usize = 0,
unflushed_events_count: usize = 0,
init_timer: ?tp.timeout,
sigwinch_signal: ?tp.signal = null,
no_sleep: bool = false,
mods: ModState = .{},
const idle_frames = 1;
const ModState = struct {
ctrl: bool = false,
shift: bool = false,
alt: bool = false,
};
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 opts = nc.Context.Options{
.termtype = null,
.loglevel = @intFromEnum(nc.LogLevel.silent),
.margin_t = 0,
.margin_r = 0,
.margin_b = 0,
.margin_l = 0,
.flags = nc.Context.option.SUPPRESS_BANNERS | nc.Context.option.INHIBIT_SETLOCALE | nc.Context.option.NO_WINCH_SIGHANDLER,
};
if (tp.env.get().is("no-alternate"))
opts.flags |= nc.Context.option.NO_ALTERNATE_SCREEN;
const nc_ = try nc.Context.core_init(&opts, null);
nc_.mice_enable(nc.mice.ALL_EVENTS) catch {};
try nc_.linesigs_disable();
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);
const fd_stdin = try tp.file_descriptor.init("stdin", nc_.inputready_fd());
// const fd_stdin = try tp.file_descriptor.init("stdin", std.os.STDIN_FILENO);
const n = nc_.stdplane();
try frame_clock.start();
try fd_stdin.wait_read();
var self = try a.create(Self);
self.* = .{
.a = a,
.config = conf,
.nc = nc_,
.frame_time = frame_time,
.frame_clock = frame_clock,
.frame_clock_running = true,
.fd_stdin = fd_stdin,
.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"),
.escape_code = ArrayList(u8).init(a),
.bracketed_paste_buffer = ArrayList(u8).init(a),
.init_timer = try tp.timeout.init_ms(init_delay, tp.message.fmt(.{"init"})),
.theme = theme,
.no_sleep = tp.env.get().is("no-sleep"),
};
try self.commands.init(self);
errdefer self.deinit();
instance_ = self;
defer instance_ = null;
try self.listen_sigwinch();
self.mainview = try mainview.create(a, n);
try self.initUI();
try nc_.render();
try self.save_config();
// self.request_mouse_cursor_support_detect();
self.bracketed_paste_enable();
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;
}
pub fn initUI(self: *Self) !void {
const n = self.nc.stdplane();
var channels: u64 = 0;
try nc.channels_set_fg_rgb(&channels, 0x88aa00);
try nc.channels_set_bg_rgb(&channels, 0x000088);
try nc.channels_set_bg_alpha(&channels, nc.ALPHA_TRANSPARENT);
_ = try n.set_base(" ", 0, channels);
try n.set_fg_rgb(0x40f040);
try n.set_fg_rgb(0x00dddd);
}
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 {
self.bracketed_paste_buffer.deinit();
self.escape_code.deinit();
if (self.input_mode) |*m| m.deinit();
self.commands.deinit();
self.fd_stdin.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.nc.stop();
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();
errdefer self.fd_stdin.cancel() catch {};
errdefer self.nc.leave_alternate_screen();
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 {
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 tp.exit_normal();
}
if (try m.match(.{ "project_manager", "shutdown" })) {
return tp.exit_normal();
}
if (try m.match(.{"restart"})) {
_ = try self.mainview.msg(.{"write_restore_info"});
project_manager.shutdown();
return tp.exit("restart");
}
if (try m.match(.{"sigwinch"})) {
try self.listen_sigwinch();
self.nc.refresh() catch |e| return self.logger.err("refresh", e);
self.mainview.resize(Widget.Box.from(self.nc.stdplane()));
need_render();
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 (self.dispatch_input(m) catch |e| b: {
self.logger.err("input dispatch", e);
break :b true;
})
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 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;
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.nc.stdplane().erase();
break :ret self.mainview.render(&self.theme);
};
{
const frame = tracy.initZone(@src(), .{ .name = "notcurses render" });
defer frame.deinit();
self.nc.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_input(self: *Self, m: tp.message) error{Exit}!bool {
const frame = tracy.initZone(@src(), .{ .name = "tui input" });
defer frame.deinit();
var err: i64 = 0;
var err_msg: []u8 = "";
if (try m.match(.{ "fd", "stdin", "read_ready" })) {
self.fd_stdin.wait_read() catch |e| return tp.exit_error(e);
try self.dispatch_notcurses();
return true; // consume message
}
if (try m.match(.{ "fd", "stdin", "read_error", tp.extract(&err), tp.extract(&err_msg) })) {
return tp.exit(err_msg);
}
return false;
}
fn dispatch_notcurses(self: *Self) tp.result {
var input_buffer: [256]nc.Input = undefined;
while (true) {
const nivec = self.nc.getvec_nblock(&input_buffer) catch |e| return tp.exit_error(e);
if (nivec.len == 0)
break;
for (nivec) |*ni| {
if (ni.id == 27 or self.escape_state != .none) {
try self.handle_escape(ni);
continue;
}
self.dispatch_input_event(ni) catch |e|
self.logger.err("input dispatch", e);
}
}
if (self.escape_state == .init)
try self.handle_escape_short();
if (self.unflushed_events_count > 0)
_ = try self.dispatch_flush_input_event();
if (self.unrendered_input_events_count > 0 and !self.frame_clock_running)
need_render();
}
fn dispatch_input_event(self: *Self, ni: *nc.Input) tp.result {
const keypress: u32 = ni.id;
var buf: [256]u8 = undefined;
self.unrendered_input_events_count += 1;
ni.modifiers &= nc.mod.CTRL | nc.mod.SHIFT | nc.mod.ALT | nc.mod.SUPER | nc.mod.META | nc.mod.HYPER;
if (keypress == nc.key.RESIZE) return;
try self.sync_mod_state(keypress, ni.modifiers);
if (keypress == nc.key.MOTION) {
if (ni.y == 0 and ni.x == 0 and ni.ypx == -1 and ni.xpx == -1) return;
self.dispatch_mouse(ni.y, ni.x, tp.self_pid(), tp.message.fmtbuf(&buf, .{
"M",
ni.x,
ni.y,
ni.xpx,
ni.ypx,
}) catch |e| return tp.exit_error(e));
} else if (keypress > nc.key.MOTION and keypress <= nc.key.BUTTON11) {
if (ni.y == 0 and ni.x == 0 and ni.ypx == -1 and ni.xpx == -1) return;
if (try self.detect_drag(ni)) return;
self.dispatch_mouse(ni.y, ni.x, tp.self_pid(), tp.message.fmtbuf(&buf, .{
"B",
ni.evtype,
keypress,
nc.key_string(ni),
ni.x,
ni.y,
ni.xpx,
ni.ypx,
}) catch |e| return tp.exit_error(e));
} else {
self.unflushed_events_count += 1;
self.send_input(tp.self_pid(), tp.message.fmtbuf(&buf, .{
"I",
normalized_evtype(ni.evtype),
keypress,
if (@hasField(nc.Input, "eff_text")) ni.eff_text[0] else keypress,
nc.key_string(ni),
ni.modifiers,
}) catch |e| return tp.exit_error(e));
}
}
fn dispatch_flush_input_event(self: *Self) error{Exit}!bool {
var buf: [32]u8 = undefined;
self.unflushed_events_count = 0;
if (self.input_mode) |mode|
try mode.handler.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{"F"}) catch |e| return tp.exit_error(e));
return false;
}
fn detect_drag(self: *Self, ni: *nc.Input) error{Exit}!bool {
return switch (ni.id) {
nc.key.BUTTON1...nc.key.BUTTON3, nc.key.BUTTON6...nc.key.BUTTON9 => if (self.drag) self.detect_drag_end(ni) else self.detect_drag_begin(ni),
else => false,
};
}
fn detect_drag_begin(self: *Self, ni: *nc.Input) error{Exit}!bool {
if (ni.evtype == nc.event_type.PRESS and self.drag_event.id == ni.id) {
self.drag_source = self.find_coord_widget(@intCast(self.drag_event.y), @intCast(self.drag_event.x));
self.drag_event = ni.*;
self.drag = true;
var buf: [256]u8 = undefined;
_ = try self.send_mouse_drag(tp.self_pid(), tp.message.fmtbuf(&buf, .{
"D",
nc.event_type.PRESS,
ni.id,
nc.key_string(ni),
ni.x,
ni.y,
ni.xpx,
ni.ypx,
}) catch |e| return tp.exit_error(e));
return true;
}
if (ni.evtype == nc.event_type.PRESS)
self.drag_event = ni.*
else
self.drag_event = nc.input();
return false;
}
fn detect_drag_end(self: *Self, ni: *nc.Input) error{Exit}!bool {
var buf: [256]u8 = undefined;
if (ni.id == self.drag_event.id and ni.evtype != nc.event_type.PRESS) {
_ = try self.send_mouse_drag(tp.self_pid(), tp.message.fmtbuf(&buf, .{
"D",
nc.event_type.RELEASE,
ni.id,
nc.key_string(ni),
ni.x,
ni.y,
ni.xpx,
ni.ypx,
}) catch |e| return tp.exit_error(e));
self.drag = false;
self.drag_event = nc.input();
self.drag_source = null;
return true;
}
_ = try self.send_mouse_drag(tp.self_pid(), tp.message.fmtbuf(&buf, .{
"D",
ni.evtype,
ni.id,
nc.key_string(ni),
ni.x,
ni.y,
ni.xpx,
ni.ypx,
}) catch |e| return tp.exit_error(e));
return true;
}
fn normalized_evtype(evtype: c_uint) c_uint {
return if (evtype == nc.event_type.UNKNOWN) @as(c_uint, @intCast(nc.event_type.PRESS)) else evtype;
}
const EscapeState = enum { none, init, OSC, st, CSI };
fn handle_escape(self: *Self, ni: *nc.Input) tp.result {
switch (self.escape_state) {
.none => switch (ni.id) {
'\x1B' => {
self.escape_state = .init;
self.escape_initial = ni.*;
},
else => unreachable,
},
.init => switch (ni.id) {
']' => self.escape_state = .OSC,
'[' => self.escape_state = .CSI,
else => {
try self.handle_escape_short();
_ = try self.dispatch_input_event(ni);
},
},
.OSC => switch (ni.id) {
'\x1B' => self.escape_state = .st,
'\\' => try self.handle_OSC_escape_code(),
' '...'\\' - 1, '\\' + 1...127 => {
const p = self.escape_code.addOne() catch |e| return tp.exit_error(e);
p.* = @intCast(ni.id);
},
else => try self.handle_OSC_escape_code(),
},
.st => switch (ni.id) {
'\\' => try self.handle_OSC_escape_code(),
else => try self.handle_OSC_escape_code(),
},
.CSI => switch (ni.id) {
'0'...'9', ';', ' ', '-', '?' => {
const p = self.escape_code.addOne() catch |e| return tp.exit_error(e);
p.* = @intCast(ni.id);
},
else => {
const p = self.escape_code.addOne() catch |e| return tp.exit_error(e);
p.* = @intCast(ni.id);
try self.handle_CSI_escape_code();
},
},
}
}
fn handle_escape_short(self: *Self) tp.result {
self.escape_code.clearAndFree();
self.escape_state = .none;
defer self.escape_initial = null;
if (self.escape_initial) |*ni|
_ = try self.dispatch_input_event(ni);
}
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 dispatch_mouse(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) void {
self.send_mouse(y, x, from, m) catch |e|
self.logger.err("dispatch mouse", e);
}
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, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
tp.trace(tp.channel.input, m);
_ = self.input_listeners.send(from, m) catch {};
return if (self.keyboard_focus) |w|
w.send(from, m)
else if (self.drag_source) |w|
w.send(from, m)
else
false;
}
fn send_input(self: *Self, from: tp.pid_ref, m: tp.message) void {
tp.trace(tp.channel.input, m);
if (self.bracketed_paste and self.handle_bracketed_paste_input(m) catch |e| {
self.bracketed_paste_buffer.clearAndFree();
self.bracketed_paste = false;
return self.logger.err("bracketed paste input handler", e);
}) {
return;
}
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);
}
pub fn save_config(self: *const Self) !void {
try root.write_config(self.config, self.a);
}
fn sync_mod_state(self: *Self, keypress: u32, modifiers: u32) tp.result {
if (keypress == nc.key.LCTRL or keypress == nc.key.RCTRL or keypress == nc.key.LALT or keypress == nc.key.RALT or
keypress == nc.key.LSHIFT or keypress == nc.key.RSHIFT or keypress == nc.key.LSUPER or keypress == nc.key.RSUPER) return;
if (nc.isCtrl(modifiers) and !self.mods.ctrl)
try self.send_key(nc.event_type.PRESS, nc.key.LCTRL, "lctrl", modifiers);
if (!nc.isCtrl(modifiers) and self.mods.ctrl)
try self.send_key(nc.event_type.RELEASE, nc.key.LCTRL, "lctrl", modifiers);
if (nc.isAlt(modifiers) and !self.mods.alt)
try self.send_key(nc.event_type.PRESS, nc.key.LALT, "lalt", modifiers);
if (!nc.isAlt(modifiers) and self.mods.alt)
try self.send_key(nc.event_type.RELEASE, nc.key.LALT, "lalt", modifiers);
if (nc.isShift(modifiers) and !self.mods.shift)
try self.send_key(nc.event_type.PRESS, nc.key.LSHIFT, "lshift", modifiers);
if (!nc.isShift(modifiers) and self.mods.shift)
try self.send_key(nc.event_type.RELEASE, nc.key.LSHIFT, "lshift", modifiers);
self.mods = .{
.ctrl = nc.isCtrl(modifiers),
.alt = nc.isAlt(modifiers),
.shift = nc.isShift(modifiers),
};
}
fn send_key(self: *Self, event_type: c_int, keypress: u32, key_string: []const u8, modifiers: u32) tp.result {
var buf: [256]u8 = undefined;
self.send_input(tp.self_pid(), tp.message.fmtbuf(&buf, .{
"I",
event_type,
keypress,
keypress,
key_string,
modifiers,
}) catch |e| return tp.exit_error(e));
}
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 buf_parent: [256]u8 = undefined;
var z: i32 = 0;
var n = self.nc.stdplane();
while (n.below()) |n_| : (n = n_) {
z -= 1;
l.print("{d} {s} {s}", .{ z, n_.name(&buf), n_.parent().name(&buf_parent) });
}
z = 0;
n = self.nc.stdplane();
while (n.above()) |n_| : (n = n_) {
z += 1;
l.print("{d} {s} {s}", .{ z, n_.name(&buf), n_.parent().name(&buf_parent) });
}
}
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 cmds.exit_mini_mode(self, .{});
if (self.input_mode) |*m| m.deinit();
if (self.input_mode_outer) |*m| {
m.deinit();
self.input_mode_outer = null;
}
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 cmds.exit_mini_mode(self, .{});
if (self.input_mode_outer) |*m| {
m.deinit();
self.input_mode_outer = null;
}
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) |_| {} else 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 {
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");
}
const OSC = "\x1B]"; // Operating System Command
const ST = "\x1B\\"; // String Terminator
const BEL = "\x07";
const OSC0_title = OSC ++ "0;";
const OSC52_clipboard = OSC ++ "52;c;";
const OSC52_clipboard_paste = OSC ++ "52;p;";
const OSC22_cursor = OSC ++ "22;";
const OSC22_cursor_reply = OSC ++ "22:";
pub fn set_terminal_title(text: []const u8) void {
var writer = std.io.getStdOut().writer();
var buf: [std.posix.PATH_MAX]u8 = undefined;
const term_cmd = std.fmt.bufPrint(&buf, OSC0_title ++ "{s}" ++ BEL, .{text}) catch return;
_ = writer.write(term_cmd) catch return;
}
pub fn copy_to_system_clipboard(self: *const Self, text: []const u8) void {
self.copy_to_system_clipboard_with_errors(text) catch |e| self.logger.err("copy_to_system_clipboard", e);
}
pub fn copy_to_system_clipboard_with_errors(self: *const Self, text: []const u8) !void {
var writer = std.io.getStdOut().writer();
const encoder = std.base64.standard.Encoder;
const size = OSC52_clipboard.len + encoder.calcSize(text.len) + ST.len;
const buf = try self.a.alloc(u8, size);
defer self.a.free(buf);
@memcpy(buf[0..OSC52_clipboard.len], OSC52_clipboard);
const b64 = encoder.encode(buf[OSC52_clipboard.len..], text);
@memcpy(buf[OSC52_clipboard.len + b64.len ..], ST);
_ = try writer.write(buf);
}
pub fn write_stdout(self: *const Self, bytes: []const u8) void {
_ = std.io.getStdOut().writer().write(bytes) catch |e| self.logger.err("stdout", e);
}
pub fn request_system_clipboard(self: *const Self) void {
self.write_stdout(OSC52_clipboard ++ "?" ++ ST);
}
pub fn request_mouse_cursor_text(self: *const Self, push_or_pop: bool) void {
if (push_or_pop) self.mouse_cursor_push("text") else self.mouse_cursor_pop();
}
pub fn request_mouse_cursor_pointer(self: *const Self, push_or_pop: bool) void {
if (push_or_pop) self.mouse_cursor_push("pointer") else self.mouse_cursor_pop();
}
pub fn request_mouse_cursor_default(self: *const Self, push_or_pop: bool) void {
if (push_or_pop) self.mouse_cursor_push("default") else self.mouse_cursor_pop();
}
fn mouse_cursor_push(self: *const Self, comptime name: []const u8) void {
self.write_stdout(OSC22_cursor ++ name ++ ST);
}
fn mouse_cursor_pop(self: *const Self) void {
self.write_stdout(OSC22_cursor ++ "default" ++ ST);
}
fn match_code(self: *const Self, match: []const u8, skip: usize) bool {
const code = self.escape_code.items;
if (!(code.len >= match.len - skip)) return false;
const code_prefix = code[0 .. match.len - skip];
return std.mem.eql(u8, match[skip..], code_prefix);
}
fn handle_OSC_escape_code(self: *Self) tp.result {
self.escape_state = .none;
self.escape_initial = null;
defer self.escape_code.clearAndFree();
const code = self.escape_code.items;
if (self.match_code(OSC52_clipboard, OSC.len))
return self.handle_system_clipboard(code[OSC52_clipboard.len - OSC.len ..]);
if (self.match_code(OSC52_clipboard_paste, OSC.len))
return self.handle_system_clipboard(code[OSC52_clipboard_paste.len - OSC.len ..]);
if (self.match_code(OSC22_cursor_reply, OSC.len))
return self.handle_mouse_cursor(code[OSC22_cursor_reply.len - OSC.len ..]);
self.logger.print("ignored escape code: OSC {s}", .{std.fmt.fmtSliceEscapeLower(code)});
}
fn handle_system_clipboard(self: *Self, base64: []const u8) tp.result {
const decoder = std.base64.standard.Decoder;
// try self.logger.print("clipboard: b64 {s}", .{base64});
const text = self.a.alloc(
u8,
decoder.calcSizeForSlice(base64) catch |e| return tp.exit_error(e),
) catch |e| return tp.exit_error(e);
decoder.decode(text, base64) catch |e| return tp.exit_error(e);
// try self.logger.print("clipboard: txt {s}", .{std.fmt.fmtSliceEscapeLower(text)});
return tp.self_pid().send(.{ "system_clipboard", text });
}
fn handle_mouse_cursor(self: *Self, text: []const u8) tp.result {
self.logger.print("mouse cursor report: {s}", .{text});
}
const CSI = "\x1B["; // Control Sequence Introducer
const CSI_bracketed_paste_enable = CSI ++ "?2004h";
const CSI_bracketed_paste_disable = CSI ++ "?2004h";
const CIS_bracketed_paste_begin = CSI ++ "200~";
const CIS_bracketed_paste_end = CSI ++ "201~";
fn handle_CSI_escape_code(self: *Self) tp.result {
self.escape_state = .none;
self.escape_initial = null;
defer self.escape_code.clearAndFree();
const code = self.escape_code.items;
if (self.match_code(CIS_bracketed_paste_begin, CSI.len))
return self.handle_bracketed_paste_begin();
if (self.match_code(CIS_bracketed_paste_end, CSI.len))
return self.handle_bracketed_paste_end();
self.logger.print("ignored escape code: CSI {s}", .{std.fmt.fmtSliceEscapeLower(code)});
}
fn handle_bracketed_paste_begin(self: *Self) tp.result {
_ = try self.dispatch_flush_input_event();
self.bracketed_paste_buffer.clearAndFree();
self.bracketed_paste = true;
}
fn handle_bracketed_paste_input(self: *Self, m: tp.message) !bool {
var keypress: u32 = undefined;
var egc: u32 = undefined;
if (try m.match(.{ "I", tp.number, tp.extract(&keypress), tp.extract(&egc), tp.string, 0 })) {
switch (keypress) {
nc.key.ENTER => try self.bracketed_paste_buffer.appendSlice("\n"),
else => if (!nc.key.synthesized_p(keypress)) {
var buf: [6]u8 = undefined;
const bytes = try nc.ucs32_to_utf8(&[_]u32{egc}, &buf);
try self.bracketed_paste_buffer.appendSlice(buf[0..bytes]);
} else {
try self.handle_bracketed_paste_end();
return false;
},
}
return true;
}
return false;
}
fn handle_bracketed_paste_end(self: *Self) tp.result {
defer self.bracketed_paste_buffer.clearAndFree();
if (!self.bracketed_paste) return;
self.bracketed_paste = false;
return tp.self_pid().send(.{ "system_clipboard", self.bracketed_paste_buffer.items });
}
fn bracketed_paste_enable(self: *const Self) void {
self.write_stdout(CSI_bracketed_paste_enable);
}
fn bracketed_paste_disable(self: *const Self) void {
self.write_stdout(CSI_bracketed_paste_disable);
}
pub inline fn fg_channels_from_style(channels: *u64, style: Widget.Theme.Style) void {
if (style.fg) |fg| {
nc.channels_set_fg_rgb(channels, fg) catch {};
nc.channels_set_fg_alpha(channels, nc.ALPHA_OPAQUE) catch {};
}
}
pub inline fn bg_channels_from_style(channels: *u64, style: Widget.Theme.Style) void {
if (style.bg) |bg| {
nc.channels_set_bg_rgb(channels, bg) catch {};
nc.channels_set_bg_alpha(channels, nc.ALPHA_OPAQUE) catch {};
}
}
pub inline fn channels_from_style(channels: *u64, style: Widget.Theme.Style) void {
fg_channels_from_style(channels, style);
bg_channels_from_style(channels, style);
}
pub inline fn set_cell_style(cell: *nc.Cell, style: Widget.Theme.Style) void {
channels_from_style(&cell.channels, style);
if (style.fs) |fs| switch (fs) {
.normal => nc.cell_set_styles(cell, nc.style.none),
.bold => nc.cell_set_styles(cell, nc.style.bold),
.italic => nc.cell_set_styles(cell, nc.style.italic),
.underline => nc.cell_set_styles(cell, nc.style.underline),
.strikethrough => nc.cell_set_styles(cell, nc.style.struck),
};
}
pub inline fn set_cell_style_fg(cell: *nc.Cell, style: Widget.Theme.Style) void {
fg_channels_from_style(&cell.channels, style);
}
pub inline fn set_cell_style_bg(cell: *nc.Cell, style: Widget.Theme.Style) void {
bg_channels_from_style(&cell.channels, style);
}
pub inline fn set_base_style(plane: *const nc.Plane, egc: [*c]const u8, style: Widget.Theme.Style) void {
var channels: u64 = 0;
channels_from_style(&channels, style);
if (style.fg) |fg| plane.set_fg_rgb(fg) catch {};
if (style.bg) |bg| plane.set_bg_rgb(bg) catch {};
_ = plane.set_base(egc, 0, channels) catch {};
}
pub fn set_base_style_alpha(plane: nc.Plane, egc: [*:0]const u8, style: Widget.Theme.Style, fg_alpha: c_uint, bg_alpha: c_uint) !void {
var channels: u64 = 0;
if (style.fg) |fg| {
nc.channels_set_fg_rgb(&channels, fg) catch {};
nc.channels_set_fg_alpha(&channels, fg_alpha) catch {};
}
if (style.bg) |bg| {
nc.channels_set_bg_rgb(&channels, bg) catch {};
nc.channels_set_bg_alpha(&channels, bg_alpha) catch {};
}
if (style.fg) |fg| plane.set_fg_rgb(fg) catch {};
if (style.bg) |bg| plane.set_bg_rgb(bg) catch {};
_ = plane.set_base(egc, 0, channels) catch {};
}
pub inline fn set_style(plane: *const nc.Plane, style: Widget.Theme.Style) void {
var channels: u64 = 0;
channels_from_style(&channels, style);
plane.set_channels(channels);
if (style.fs) |fs| switch (fs) {
.normal => plane.set_styles(nc.style.none),
.bold => plane.set_styles(nc.style.bold),
.italic => plane.set_styles(nc.style.italic),
.underline => plane.set_styles(nc.style.underline),
.strikethrough => plane.set_styles(nc.style.struck),
};
}
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 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" },
};