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", 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 { 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, @errorReturnTrace()); errdefer self.deinit(); 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(); 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(a, keepalive, .{"keepalive"}); }, else => { try self.listen_sigwinch(); }, } self.mainview = try mainview.create(a); self.resize(); self.rdr.set_terminal_style(self.theme.editor); 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 { if (self.input_mode) |_| {} else return cmds.enter_mode(self, command.Context.fmt(.{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.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, @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.a, 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 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 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.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.input_mode) |mode| mode.handler.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", "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; 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 dispatch_flush_input_event(self: *Self) !void { var buf: [32]u8 = undefined; if (self.input_mode) |mode| try mode.handler.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.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); } 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, @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())); } _ = try w.send(from, m); } else { 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; } } 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, @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())); } } else { try self.clear_hover_focus(); } if (self.drag_source) |w| _ = try w.send(from, m); } 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 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; const Result = command.Result; pub fn restart(_: *Self, _: Ctx) Result { try tp.self_pid().send("restart"); } pub fn force_terminate(self: *Self, _: Ctx) Result { self.deinit(); root.print_exit_status({}, "FORCE TERMINATE"); root.exit(99); } 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.rdr.set_terminal_style(self.theme.editor); self.logger.print("theme: {s}", .{self.theme.description}); try self.save_config(); } 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.rdr.set_terminal_style(self.theme.editor); self.logger.print("theme: {s}", .{self.theme.description}); try self.save_config(); } pub fn toggle_whitespace(self: *Self, _: Ctx) Result { self.config.show_whitespace = !self.config.show_whitespace; self.logger.print("show_whitspace {s}", .{if (self.config.show_whitespace) "enabled" else "disabled"}); try self.save_config(); var buf: [32]u8 = undefined; const m = try tp.message.fmtbuf(&buf, .{ "show_whitespace", self.config.show_whitespace }); _ = try self.send_widgets(tp.self_pid(), m); } 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 "flow"; try self.save_config(); return enter_mode(self, Ctx.fmt(.{self.config.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.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")) try @import("mode/input/vim/normal.zig").create(self.a) else if (std.mem.eql(u8, mode, "vim/insert")) try @import("mode/input/vim/insert.zig").create(self.a) else if (std.mem.eql(u8, mode, "vim/visual")) try @import("mode/input/vim/visual.zig").create(self.a) else if (std.mem.eql(u8, mode, "flow")) try @import("mode/input/flow.zig").create(self.a) else if (std.mem.eql(u8, mode, "home")) try @import("mode/input/home.zig").create(self.a) else ret: { self.logger.print("unknown mode {s}", .{mode}); break :ret try @import("mode/input/flow.zig").create(self.a); }; // self.logger.print("input mode: {s}", .{(self.input_mode orelse return).description}); } pub fn enter_mode_default(self: *Self, _: Ctx) Result { return enter_mode(self, Ctx.fmt(.{self.config.input_mode})); } pub fn open_command_palette(self: *Self, _: Ctx) 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; self.input_mode = try @import("mode/overlay/command_palette.zig").create(self.a); } pub fn open_recent(self: *Self, _: Ctx) 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; self.input_mode = try @import("mode/overlay/open_recent.zig").create(self.a); } pub fn exit_overlay_mode(self: *Self, _: Ctx) 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) Result { return enter_mini_mode(self, @import("mode/mini/find.zig"), ctx); } pub fn enter_find_in_files_mode(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/find_in_files.zig"), ctx); } pub fn enter_goto_mode(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx); } pub fn enter_move_to_char_mode(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx); } pub fn enter_open_file_mode(self: *Self, ctx: Ctx) 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) 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 = try mode.create(self.a, ctx); 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) 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, keybind_hints: ?*const KeybindHints = null, fn deinit(self: *Mode) void { self.handler.deinit(); } }; pub const MiniModeState = struct { text: []const u8 = "", cursor: ?usize = null, }; pub const KeybindHints = std.static_string_map.StaticStringMap([]const u8); 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 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()); 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| 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" }, };