diff --git a/src/main.zig b/src/main.zig index 7a73f41..7e30ebe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -385,13 +385,12 @@ pub fn main() anyerror!void { ctx.run(); - if (want_restart) if (want_restart_with_sudo) restart_with_sudo() else restart(); + if (want_restart) restart(); exit(final_exit_status); } var final_exit_status: u8 = 0; var want_restart: bool = false; -var want_restart_with_sudo: bool = false; pub fn print_exit_status(_: void, msg: []const u8) void { if (std.mem.eql(u8, msg, "normal")) { @@ -407,10 +406,6 @@ pub fn print_exit_status(_: void, msg: []const u8) void { } } -pub fn set_restart_with_sudo() void { - want_restart_with_sudo = true; -} - fn count_args() usize { var args = std.process.args(); _ = args.next(); @@ -1112,41 +1107,25 @@ pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, theme_name }); } -fn resolve_executable(executable: [:0]const u8) [:0]const u8 { - return for (executable) |char| { - if (std.fs.path.isSep(char)) break executable; - } else bin_path.find_binary_in_path(std.heap.c_allocator, executable) catch executable orelse executable; -} - fn restart() noreturn { - const executable = resolve_executable(std.mem.span(std.os.argv[0])); + var executable: [:0]const u8 = std.mem.span(std.os.argv[0]); + var is_basename = true; + for (executable) |char| if (std.fs.path.isSep(char)) { + is_basename = false; + }; + if (is_basename) { + const a = std.heap.c_allocator; + executable = bin_path.find_binary_in_path(a, executable) catch executable orelse executable; + } const argv = [_]?[*:0]const u8{ executable, "--restore-session", null, }; const ret = std.c.execve(executable, @ptrCast(&argv), @ptrCast(std.os.environ)); - restart_failed(ret); -} - -fn restart_with_sudo() noreturn { - const sudo_executable = resolve_executable("sudo"); - const flow_executable = resolve_executable(std.mem.span(std.os.argv[0])); - const argv = [_]?[*:0]const u8{ - sudo_executable, - "--preserve-env", - flow_executable, - "--restore-session", - null, - }; - const ret = std.c.execve(sudo_executable, @ptrCast(&argv), @ptrCast(std.os.environ)); - restart_failed(ret); -} - -fn restart_failed(ret: c_int) noreturn { var stderr_buffer: [1024]u8 = undefined; var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); - stderr_writer.interface.print("\nrestart failed: {s}", .{@tagName(std.posix.errno(ret))}) catch {}; + stderr_writer.interface.print("\nrestart failed: {d}", .{ret}) catch {}; stderr_writer.interface.flush() catch {}; exit(234); } diff --git a/src/soft_root.zig b/src/soft_root.zig index a792e73..b19caae 100644 --- a/src/soft_root.zig +++ b/src/soft_root.zig @@ -33,7 +33,6 @@ pub const root = struct { pub const exit = if (@hasDecl(hard_root, "exit")) hard_root.exit else dummy.exit; pub const print_exit_status = if (@hasDecl(hard_root, "print_exit_status")) hard_root.print_exit_status else dummy.print_exit_status; - pub const set_restart_with_sudo = if (@hasDecl(hard_root, "set_restart_with_sudo")) hard_root.set_restart_with_sudo else dummy.set_restart_with_sudo; pub const is_directory = if (@hasDecl(hard_root, "is_directory")) hard_root.is_directory else dummy.is_directory; pub const is_file = if (@hasDecl(hard_root, "is_file")) hard_root.is_file else dummy.is_file; @@ -119,9 +118,6 @@ const dummy = struct { pub fn print_exit_status(_: void, _: []const u8) void { @panic("dummy print_exit_status call"); } - pub fn set_restart_with_sudo() void { - @panic("dummy set_restart_with_sudo call"); - } pub fn is_directory(_: []const u8) bool { @panic("dummy is_directory call"); diff --git a/src/tui/home.zig b/src/tui/home.zig index 3555a65..2820007 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -17,12 +17,9 @@ const keybind = @import("keybind"); const fonts = @import("fonts.zig"); -pub const subtext_root = "I am groot!"; - const style = struct { title: []const u8 = root.application_title, subtext: []const u8 = root.application_subtext, - subtext_root: []const u8 = subtext_root, centered: bool = false, @@ -79,7 +76,6 @@ menu_count: usize = 0, menu_len: usize = 0, max_desc_len: usize = 0, input_namespace: []const u8, -root_mode: bool = false, home_style: style, home_style_bufs: [][]const u8, @@ -115,9 +111,6 @@ pub fn create(allocator: std.mem.Allocator, parent: Widget) !Widget { .home_style = home_style, .home_style_bufs = home_style_bufs, }; - if (builtin.os.tag != .windows and std.posix.geteuid() == 0) { - self.root_mode = true; - } self.commands.init_unregistered(self); var it = std.mem.splitAny(u8, self.home_style.menu_commands, "\n "); while (it.next()) |command_name| { @@ -303,46 +296,34 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { self.plane.set_base_style(theme.editor); const style_title = if (tui.find_scope_style(theme, "function")) |sty| sty.style else theme.editor; - const style_subtext = if (self.root_mode) - theme.editor_error - else if (tui.find_scope_style(theme, "comment")) |sty| - sty.style - else - theme.editor; - - const title = self.home_style.title; - - const subtext = if (self.root_mode) - self.home_style.subtext_root - else - self.home_style.subtext; + const style_subtext = if (tui.find_scope_style(theme, "comment")) |sty| sty.style else theme.editor; if (self.plane.dim_x() > 120 and self.plane.dim_y() > 22) { - self.plane.cursor_move_yx(2, self.centerI(4, title.len * 8)) catch return false; - fonts.print_string_large(&self.plane, title, style_title) catch return false; + self.plane.cursor_move_yx(2, self.centerI(4, self.home_style.title.len * 8)) catch return false; + fonts.print_string_large(&self.plane, self.home_style.title, style_title) catch return false; - self.plane.cursor_move_yx(10, self.centerI(8, subtext.len * 4)) catch return false; - fonts.print_string_medium(&self.plane, subtext, style_subtext) catch return false; + self.plane.cursor_move_yx(10, self.centerI(8, self.home_style.subtext.len * 4)) catch return false; + fonts.print_string_medium(&self.plane, self.home_style.subtext, style_subtext) catch return false; self.position_menu(self.v_center(15, self.menu_len, 15), self.center(10, self.menu_w)); } else if (self.plane.dim_x() > 55 and self.plane.dim_y() > 16) { - self.plane.cursor_move_yx(2, self.centerI(4, title.len * 4)) catch return false; - fonts.print_string_medium(&self.plane, title, style_title) catch return false; + self.plane.cursor_move_yx(2, self.centerI(4, self.home_style.title.len * 4)) catch return false; + fonts.print_string_medium(&self.plane, self.home_style.title, style_title) catch return false; self.plane.set_style_bg_transparent(style_subtext); - self.plane.cursor_move_yx(7, self.centerI(6, subtext.len)) catch return false; - _ = self.plane.print("{s}", .{subtext}) catch {}; + self.plane.cursor_move_yx(7, self.centerI(6, self.home_style.subtext.len)) catch return false; + _ = self.plane.print("{s}", .{self.home_style.subtext}) catch {}; self.plane.set_style(theme.editor); self.position_menu(self.v_center(9, self.menu_len, 9), self.center(8, self.menu_w)); } else { self.plane.set_style_bg_transparent(style_title); - self.plane.cursor_move_yx(1, self.centerI(4, title.len)) catch return false; - _ = self.plane.print("{s}", .{title}) catch return false; + self.plane.cursor_move_yx(1, self.centerI(4, self.home_style.title.len)) catch return false; + _ = self.plane.print("{s}", .{self.home_style.title}) catch return false; self.plane.set_style_bg_transparent(style_subtext); - self.plane.cursor_move_yx(3, self.centerI(6, subtext.len)) catch return false; - _ = self.plane.print("{s}", .{subtext}) catch {}; + self.plane.cursor_move_yx(3, self.centerI(6, self.home_style.subtext.len)) catch return false; + _ = self.plane.print("{s}", .{self.home_style.subtext}) catch {}; self.plane.set_style(theme.editor); const x = @min(self.plane.dim_x() -| 32, 8); diff --git a/src/tui/status/modestate.zig b/src/tui/status/modestate.zig index 03d32a5..f95f632 100644 --- a/src/tui/status/modestate.zig +++ b/src/tui/status/modestate.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const Plane = @import("renderer").Plane; @@ -16,8 +15,6 @@ const CreateError = @import("widget.zig").CreateError; const Style = enum { plain, fancy, - plain_root, - fancy_root, }; const default_style = .fancy; @@ -26,11 +23,7 @@ const ButtonType = Button.Options(Style).ButtonType; pub fn create(allocator: Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) CreateError!Widget { const style_ = if (arg) |str_style| std.meta.stringToEnum(Style, str_style) orelse default_style else default_style; return Button.create_widget(Style, allocator, parent, .{ - .ctx = if (builtin.os.tag != .windows and std.posix.geteuid() == 0) switch (style_) { - .fancy => .fancy_root, - .plain => .plain_root, - else => style_, - } else style_, + .ctx = style_, .label = tui.get_mode(), .on_click = on_click, .on_click2 = toggle_panel, @@ -60,8 +53,8 @@ fn is_overlay_mode() bool { pub fn render(ctx: *Style, self: *ButtonType, theme: *const Widget.Theme) bool { const style_base = theme.statusbar; const style_label = switch (ctx.*) { - .fancy, .fancy_root => if (self.active) theme.editor_cursor else if (self.hover) theme.editor_selection else theme.statusbar_hover, - .plain, .plain_root => if (self.active) theme.editor_cursor else if (self.hover or is_mini_mode()) theme.statusbar_hover else style_base, + .fancy => if (self.active) theme.editor_cursor else if (self.hover) theme.editor_selection else theme.statusbar_hover, + .plain => if (self.active) theme.editor_cursor else if (self.hover or is_mini_mode()) theme.statusbar_hover else style_base, }; self.plane.set_base_style(theme.editor); self.plane.erase(); @@ -75,7 +68,7 @@ pub fn render(ctx: *Style, self: *ButtonType, theme: *const Widget.Theme) bool { self.plane.on_styles(styles.bold); var buf: [31:0]u8 = undefined; if (!is_mini_mode() and !is_overlay_mode()) { - render_logo(ctx, self, theme, style_label); + render_logo(self, theme, style_label); } else { _ = self.plane.putstr(" ") catch {}; } @@ -96,26 +89,16 @@ fn render_separator(self: *ButtonType, theme: *const Widget.Theme) void { const left = " "; const symbol = "󱞏"; -const symbol_root = ""; const right = " "; -fn render_logo(ctx: *Style, self: *ButtonType, theme: *const Widget.Theme, style_label: Widget.Theme.Style) void { - const style_root = theme.editor_error; +fn render_logo(self: *ButtonType, theme: *const Widget.Theme, style_label: Widget.Theme.Style) void { const style_braces: Widget.Theme.Style = if (tui.find_scope_style(theme, "punctuation")) |sty| .{ .fg = sty.style.fg, .bg = style_label.bg, .fs = style_label.fs } else style_label; if (left.len > 0) { self.plane.set_style(style_braces); _ = self.plane.putstr(" " ++ left) catch {}; } - switch (ctx.*) { - .fancy_root, .plain_root => { - self.plane.set_style(style_root); - _ = self.plane.putstr(symbol_root) catch {}; - }, - else => { - self.plane.set_style(style_label); - _ = self.plane.putstr(symbol) catch {}; - }, - } + self.plane.set_style(style_label); + _ = self.plane.putstr(symbol) catch {}; if (right.len > 0) { self.plane.set_style(style_braces); _ = self.plane.putstr(right) catch {}; diff --git a/src/tui/tui.zig b/src/tui/tui.zig index bba3e28..34ce1d4 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -915,13 +915,7 @@ const cmds = struct { pub fn restart(_: *Self, _: Ctx) Result { try tp.self_pid().send("restart"); } - pub const restart_meta: Meta = .{ .description = "Restart session" }; - - pub fn restart_with_sudo(_: *Self, _: Ctx) Result { - root.set_restart_with_sudo(); - try tp.self_pid().send("restart"); - } - pub const restart_with_sudo_meta: Meta = .{ .description = "Restart with sudo" }; + pub const restart_meta: Meta = .{ .description = "Restart (without saving)" }; pub fn force_terminate(self: *Self, _: Ctx) Result { self.deinit(); diff --git a/test/tests_buffer_input.txt b/test/tests_buffer_input.txt index 52df48f..1c4f2fe 100644 --- a/test/tests_buffer_input.txt +++ b/test/tests_buffer_input.txt @@ -1,134 +1,1105 @@ -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz +const std = @import("std"); +const tui = @import("tui"); +const cbor = @import("cbor"); +const thespian = @import("thespian"); +const color = @import("color"); +const flags = @import("flags"); +const builtin = @import("builtin"); +const bin_path = @import("bin_path"); +const sep = std.fs.path.sep; + +const list_languages = @import("list_languages.zig"); +const file_link = @import("file_link"); + +const c = @cImport({ + @cInclude("locale.h"); +}); + +const build_options = @import("build_options"); +const log = @import("log"); + +pub const version = @embedFile("version"); +pub const version_info = @embedFile("version_info"); + +pub const max_diff_lines: usize = 50000; +pub const max_syntax_lines: usize = 50000; + +pub const application_name = "flow"; +pub const application_title = "Flow Control"; +pub const application_subtext = "a programmer's text editor"; +pub const application_description = application_title ++ ": " ++ application_subtext; + +pub const std_options: std.Options = .{ + // .log_level = if (builtin.mode == .Debug) .debug else .warn, + .log_level = if (builtin.mode == .Debug) .info else .warn, + .logFn = log.std_log_function, +}; + +const renderer = @import("renderer"); + +pub const panic = if (@hasDecl(renderer, "panic")) renderer.panic else default_panic; + +fn default_panic(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { + return std.debug.defaultPanic(msg, ret_addr); +} + +pub fn main() anyerror!void { + if (builtin.os.tag == .linux) { + // drain stdin so we don't pickup junk from previous application/shell + _ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.posix.STDIN_FILENO))), std.os.linux.T.CFLSH, 0); + } + + const a = std.heap.c_allocator; + + const Flags = struct { + pub const description = + application_title ++ ": " ++ application_subtext ++ + \\ + \\ + \\Pass in file names to be opened with an optional :LINE or :LINE:COL appended to the + \\file name to specify a specific location, or pass + separately to set the line. + ; + + pub const descriptions = .{ + .project = "Set project directory (default: cwd)", + .no_persist = "Do not persist new projects", + .frame_rate = "Set target frame rate (default: 60)", + .debug_wait = "Wait for key press before starting UI", + .debug_dump_on_error = "Dump stack traces on errors", + .no_sleep = "Do not sleep the main loop when idle", + .no_alternate = "Do not use the alternate terminal screen", + .trace_level = "Enable internal tracing (level of detail from 1-5)", + .no_trace = "Do not enable internal tracing", + .restore_session = "Restore restart session", + .show_input = "Open the input view on start", + .show_log = "Open the log view on start", + .language = "Force the language of the file to be opened", + .list_languages = "Show available languages", + .no_syntax = "Disable syntax highlighting", + .syntax_report_timing = "Report syntax highlighting time", + .exec = "Execute a command on startup", + .literal = "Disable :LINE and +LINE syntax", + .scratch = "Open a scratch (temporary) buffer on start", + .new_file = "Create a new untitled file on start", + .dark = "Use dark color scheme", + .light = "Use light color scheme", + .version = "Show build version and exit", + }; + + pub const formats = .{ .frame_rate = "num", .trace_level = "num", .exec = "cmds" }; + + pub const switches = .{ + .project = 'p', + .no_persist = 'N', + .frame_rate = 'f', + .trace_level = 't', + .language = 'l', + .exec = 'e', + .literal = 'L', + .scratch = 'S', + .new_file = 'n', + .version = 'v', + }; + + project: ?[]const u8, + no_persist: bool, + frame_rate: ?usize, + debug_wait: bool, + debug_dump_on_error: bool, + no_sleep: bool, + no_alternate: bool, + trace_level: u8 = 0, + no_trace: bool, + restore_session: bool, + show_input: bool, + show_log: bool, + language: ?[]const u8, + list_languages: bool, + no_syntax: bool, + syntax_report_timing: bool, + exec: ?[]const u8, + literal: bool, + scratch: bool, + new_file: bool, + dark: bool, + light: bool, + version: bool, + + positional: struct { + trailing: []const []const u8, + }, + }; + + const args_alloc = try std.process.argsAlloc(a); + defer std.process.argsFree(a, args_alloc); + + var diag: flags.Diagnostics = undefined; + + const args = flags.parse(args_alloc, "flow", Flags, .{ + .diagnostics = &diag, + }) catch |err| { + if (err == error.PrintedHelp) exit(0); + try diag.printUsage(&flags.ColorScheme.default); + exit(1); + return err; + }; + + var stdout_buf: [4096]u8 = undefined; + var stdout_file = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_file.interface; + defer stdout.flush() catch {}; + var stderr_buf: [4096]u8 = undefined; + var stderr_file = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_file.interface; + defer stderr.flush() catch {}; + + if (args.version) + return std.fs.File.stdout().writeAll(version_info); + + if (args.list_languages) { + const tty_config = std.io.tty.detectConfig(std.fs.File.stdout()); + return list_languages.list(a, stdout, tty_config); + } + + if (builtin.os.tag != .windows and @hasDecl(renderer, "install_crash_handler")) { + if (std.posix.getenv("JITDEBUG")) |_| renderer.jit_debugger_enabled = true; + renderer.install_crash_handler(); + } + + if (args.debug_wait) { + std.debug.print("press return to start", .{}); + var buf: [1]u8 = undefined; + _ = try std.fs.File.stdin().read(&buf); + } + + if (c.setlocale(c.LC_ALL, "") == null) { + try stderr.print("Failed to set locale. Is your locale valid?\n", .{}); + stderr.flush() catch {}; + exit(1); + } + + thespian.stack_trace_on_errors = args.debug_dump_on_error; + + var ctx = try thespian.context.init(a); + defer ctx.deinit(); + + const env = thespian.env.init(); + defer env.deinit(); + if (build_options.enable_tracy) { + if (!args.no_trace) { + env.enable_all_channels(); + env.on_trace(trace); + } + } else { + if (args.trace_level != 0) { + var threshold: usize = 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.debug); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.widget); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.event); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.input); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.receive); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.metronome); + env.enable(thespian.channel.execute); + env.enable(thespian.channel.link); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.send); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable_all_channels(); + } + + env.on_trace(trace_to_file); + } + } + + const log_proc = try log.spawn(&ctx, a, &env); + defer log_proc.deinit(); + log.set_std_log_pid(log_proc.ref()); + defer log.set_std_log_pid(null); + + env.set("no-persist", args.no_persist); + env.set("restore-session", args.restore_session); + env.set("no-alternate", args.no_alternate); + env.set("show-input", args.show_input); + env.set("show-log", args.show_log); + env.set("no-sleep", args.no_sleep); + env.set("no-syntax", args.no_syntax); + env.set("syntax-report-timing", args.syntax_report_timing); + env.set("dump-stack-trace", args.debug_dump_on_error); + if (args.frame_rate) |s| env.num_set("frame-rate", @intCast(s)); + env.proc_set("log", log_proc.ref()); + if (args.language) |s| env.str_set("language", s); + + var eh = thespian.make_exit_handler({}, print_exit_status); + const tui_proc = try tui.spawn(a, &ctx, &eh, &env); + defer tui_proc.deinit(); + + var links: std.ArrayList(file_link.Dest) = .empty; + defer links.deinit(a); + var prev: ?*file_link.Dest = null; + var line_next: ?usize = null; + var offset_next: ?usize = null; + for (args.positional.trailing) |arg| { + if (arg.len == 0) continue; + + if (!args.literal and arg[0] == '+') { + if (arg.len > 2 and arg[1] == 'b') { + const offset = try std.fmt.parseInt(usize, arg[2..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.offset = offset; + continue; + }, + else => {}, + }; + offset_next = offset; + line_next = null; + } else { + const line = try std.fmt.parseInt(usize, arg[1..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.line = line; + continue; + }, + else => {}, + }; + line_next = line; + offset_next = null; + } + continue; + } + + const curr = try links.addOne(a); + curr.* = if (!args.literal) try file_link.parse(arg) else .{ .file = .{ .path = arg } }; + prev = curr; + + if (line_next) |line| { + switch (curr.*) { + .file => |*file| { + file.line = line; + line_next = null; + }, + else => {}, + } + } + if (offset_next) |offset| { + switch (curr.*) { + .file => |*file| { + file.offset = offset; + offset_next = null; + }, + else => {}, + } + } + } + + var have_project = false; + var have_file = false; + if (args.project) |project| { + try tui_proc.send(.{ "cmd", "open_project_dir", .{project} }); + have_project = true; + } + for (links.items) |link| switch (link) { + .dir => |dir| { + if (have_project) { + std.debug.print("more than one project directory is not allowed\n", .{}); + exit(1); + } + try tui_proc.send(.{ "cmd", "open_project_dir", .{dir.path} }); + have_project = true; + }, + else => { + have_file = true; + }, + }; + + for (links.items) |link| { + try file_link.navigate(tui_proc.ref(), &link); + } + + if (!have_file) { + if (!have_project) + try tui_proc.send(.{ "cmd", "open_project_cwd" }); + try tui_proc.send(.{ "cmd", "show_home" }); + } + + if (args.new_file) { + try tui_proc.send(.{ "cmd", "create_new_file", .{} }); + } else if (args.scratch) { + try tui_proc.send(.{ "cmd", "create_scratch_buffer", .{} }); + } + + if (args.dark) + try tui_proc.send(.{ "cmd", "force_color_scheme", .{"dark"} }) + else if (args.light) + try tui_proc.send(.{ "cmd", "force_color_scheme", .{"light"} }); + + if (args.exec) |exec_str| { + var cmds = std.mem.splitScalar(u8, exec_str, ';'); + while (cmds.next()) |cmd| { + var count_args_ = std.mem.splitScalar(u8, cmd, ':'); + var count: usize = 0; + while (count_args_.next()) |_| count += 1; + if (count == 0) break; + + var msg: std.Io.Writer.Allocating = .init(a); + defer msg.deinit(); + const writer = &msg.writer; + + var cmd_args = std.mem.splitScalar(u8, cmd, ':'); + const cmd_ = cmd_args.next(); + try cbor.writeArrayHeader(writer, 3); + try cbor.writeValue(writer, "cmd"); + try cbor.writeValue(writer, cmd_); + try cbor.writeArrayHeader(writer, count - 1); + + while (cmd_args.next()) |arg| { + if (std.fmt.parseInt(isize, arg, 10) catch null) |i| + try cbor.writeValue(writer, i) + else + try cbor.writeValue(writer, arg); + } + + try tui_proc.send_raw(.{ .buf = msg.written() }); + } + } + + ctx.run(); + + if (want_restart) restart(); + exit(final_exit_status); +} + +var final_exit_status: u8 = 0; +var want_restart: bool = false; + +pub fn print_exit_status(_: void, msg: []const u8) void { + if (std.mem.eql(u8, msg, "normal")) { + return; + } else if (std.mem.eql(u8, msg, "restart")) { + want_restart = true; + } else { + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + stderr_writer.interface.print("\n" ++ application_name ++ " ERROR: {s}\n", .{msg}) catch {}; + stderr_writer.interface.flush() catch {}; + final_exit_status = 1; + } +} + +fn count_args() usize { + var args = std.process.args(); + _ = args.next(); + var count: usize = 0; + while (args.next()) |_| { + count += 1; + } + return count; +} + +fn trace(m: thespian.message.c_buffer_type) callconv(.c) void { + thespian.message.from(m).to_json_cb(trace_json); +} + +fn trace_json(json: thespian.message.json_string_view) callconv(.c) void { + const callstack_depth = 10; + ___tracy_emit_message(json.base, json.len, callstack_depth); +} +extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void; + +var trace_mutex: std.Thread.Mutex = .{}; + +fn trace_to_file(m: thespian.message.c_buffer_type) callconv(.c) void { + trace_mutex.lock(); + defer trace_mutex.unlock(); + + const State = struct { + file: std.fs.File, + file_writer: std.fs.File.Writer, + last_time: i64, + var state: ?@This() = null; + var trace_buffer: [4096]u8 = undefined; + + fn write_tdiff(writer: anytype, tdiff: i64) !void { + const msi = @divFloor(tdiff, std.time.us_per_ms); + if (msi < 10) { + const d: f64 = @floatFromInt(tdiff); + const ms = d / std.time.us_per_ms; + _ = try writer.print("{d:6.2} ", .{ms}); + } else { + const ms: u64 = @intCast(msi); + _ = try writer.print("{d:6} ", .{ms}); + } + } + }; + const a = std.heap.c_allocator; + var state: *State = &(State.state orelse init: { + var path: std.Io.Writer.Allocating = .init(a); + defer path.deinit(); + path.writer.print("{s}{c}trace.log", .{ get_state_dir() catch return, sep }) catch return; + const file = std.fs.createFileAbsolute(path.written(), .{ .truncate = true }) catch return; + State.state = .{ + .file = file, + .file_writer = file.writer(&State.trace_buffer), + .last_time = std.time.microTimestamp(), + }; + break :init State.state.?; + }); + const writer = &state.file_writer.interface; + + const ts = std.time.microTimestamp(); + State.write_tdiff(writer, ts - state.last_time) catch {}; + state.last_time = ts; + + var stream: std.json.Stringify = .{ .writer = writer }; + var iter: []const u8 = m.base[0..m.len]; + cbor.JsonWriter.jsonWriteValue(&stream, &iter) catch {}; + _ = writer.write("\n") catch {}; + writer.flush() catch {}; +} + +pub fn exit(status: u8) noreturn { + if (builtin.os.tag == .linux) { + // drain stdin so we don't leave junk at the next prompt + _ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.posix.STDIN_FILENO))), std.os.linux.T.CFLSH, 0); + } + std.posix.exit(status); +} + +pub fn free_config(allocator: std.mem.Allocator, bufs: [][]const u8) void { + for (bufs) |buf| allocator.free(buf); +} + +var config_mutex: std.Thread.Mutex = .{}; + +pub fn exists_config(T: type) bool { + config_mutex.lock(); + defer config_mutex.unlock(); + const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return false; + const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; + var file = std.fs.openFileAbsolute(text_file_name, .{ .mode = .read_only }) catch return false; + defer file.close(); + return true; +} + +fn get_default(T: type) T { + return switch (@typeInfo(T)) { + .array => &.{}, + .pointer => |info| switch (info.size) { + .slice => &.{}, + else => @compileError("unsupported config type " ++ @typeName(T)), + }, + else => .{}, + }; +} + +pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } { + config_mutex.lock(); + defer config_mutex.unlock(); + var bufs: [][]const u8 = &[_][]const u8{}; + const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ get_default(T), bufs }; + const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; + var conf: T = get_default(T); + if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { + _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); + } + read_nested_include_files(T, allocator, &conf, &bufs); + return .{ conf, bufs }; +} + +// returns true if the file was found +fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8, file_name: []const u8) bool { + std.log.info("loading {s}", .{file_name}); + const err: anyerror = blk: { + if (std.mem.endsWith(u8, file_name, ".json")) if (read_json_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; + if (read_text_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; + }; + switch (err) { + error.FileNotFound => return false, + else => |e| std.log.err("error reading config file '{s}': {s}", .{ file_name, @errorName(e) }), + } + return true; +} + +fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { + var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); + defer file.close(); + const content = try file.readToEndAlloc(allocator, 64 * 1024); + defer allocator.free(content); + return parse_text_config_file(T, allocator, conf, bufs_, file_name, content); +} + +pub fn parse_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8, content: []const u8) !void { + var cbor_buf: std.Io.Writer.Allocating = .init(allocator); + defer cbor_buf.deinit(); + const writer = &cbor_buf.writer; + var it = std.mem.splitScalar(u8, content, '\n'); + var lineno: u32 = 0; + while (it.next()) |line| { + lineno += 1; + if (line.len == 0 or line[0] == '#') + continue; + const spc = std.mem.indexOfScalar(u8, line, ' ') orelse { + std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line }); + continue; + }; + const name = line[0..spc]; + const value_str = line[spc + 1 ..]; + const cb = cbor.fromJsonAlloc(allocator, value_str) catch { + std.log.err("{s}:{}: {s} has bad value: {s}", .{ file_name, lineno, name, value_str }); + continue; + }; + defer allocator.free(cb); + try cbor.writeValue(writer, name); + try writer.writeAll(cb); + } + const cb = try cbor_buf.toOwnedSlice(); + var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); + bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file"); + bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_text_config_file"); + return read_cbor_config(T, allocator, conf, file_name, cb); +} + +fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { + var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); + defer file.close(); + const json = try file.readToEndAlloc(allocator, 64 * 1024); + defer allocator.free(json); + const cbor_buf: []u8 = try allocator.alloc(u8, json.len); + var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); + bufs.append(allocator, cbor_buf) catch @panic("OOM:read_json_config_file"); + bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_json_config_file"); + const cb = try cbor.fromJson(json, cbor_buf); + var iter = cb; + _ = try cbor.decodeMapHeader(&iter); + return read_cbor_config(T, allocator, conf, file_name, iter); +} + +fn read_cbor_config( + T: type, + allocator: std.mem.Allocator, + conf: *T, + file_name: []const u8, + cb: []const u8, +) !void { + var iter = cb; + var field_name: []const u8 = undefined; + while (cbor.matchString(&iter, &field_name) catch |e| switch (e) { + error.TooShort => return, + else => return e, + }) { + var known = false; + inline for (@typeInfo(T).@"struct".fields) |field_info| + if (comptime std.mem.eql(u8, "include_files", field_info.name)) { + if (std.mem.eql(u8, field_name, field_info.name)) { + known = true; + var value: field_info.type = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&value))) { + if (conf.include_files.len > 0) { + std.log.err("{s}: ignoring nested 'include_files' value '{s}'", .{ file_name, value }); + } else { + @field(conf, field_info.name) = value; + } + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + } + } else if (std.mem.eql(u8, field_name, field_info.name)) { + known = true; + switch (field_info.type) { + u24, ?u24 => { + var value: []const u8 = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&value))) { + const color_ = color.RGB.from_string(value); + if (color_) |color__| + @field(conf, field_info.name) = color__.to_u24() + else + std.log.err("invalid value for key '{s}'", .{field_name}); + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, + else => { + var value: field_info.type = undefined; + if (try cbor.matchValue(&iter, cbor.extractAlloc(&value, allocator))) { + @field(conf, field_info.name) = value; + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, + } + }; + if (!known) { + try cbor.skipValue(&iter); + std.log.err("unknown config value '{s}' ignored", .{field_name}); + } + } +} + +fn read_nested_include_files(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8) void { + if (conf.include_files.len == 0) return; + var it = std.mem.splitScalar(u8, conf.include_files, std.fs.path.delimiter); + while (it.next()) |path| if (!read_config_file(T, allocator, conf, bufs, path)) { + std.log.err("config include file '{s}' is not found", .{path}); + }; +} + +pub const ConfigWriteError = error{ CreateConfigFileFailed, WriteConfigFileFailed, WriteFailed }; + +pub fn write_config(conf: anytype, allocator: std.mem.Allocator) (ConfigDirError || ConfigWriteError)!void { + config_mutex.lock(); + defer config_mutex.unlock(); + _ = allocator; + const file_name = try get_app_config_file_name(application_name, @typeName(@TypeOf(conf))); + return write_text_config_file(@TypeOf(conf), conf, file_name[0 .. file_name.len - 5]); + // return write_json_file(@TypeOf(conf), conf, allocator, try get_app_config_file_name(application_name, @typeName(@TypeOf(conf)))); +} + +fn write_text_config_file(comptime T: type, data: T, file_name: []const u8) ConfigWriteError!void { + var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch |e| { + std.log.err("createFileAbsolute failed with {any} for: {s}", .{ e, file_name }); + return error.CreateConfigFileFailed; + }; + defer file.close(); + var buf: [4096]u8 = undefined; + var writer = file.writer(&buf); + write_config_to_writer(T, data, &writer.interface) catch |e| { + std.log.err("write file failed with {any} for: {s}", .{ e, file_name }); + return error.WriteConfigFileFailed; + }; + try writer.interface.flush(); +} + +pub fn write_config_to_writer(comptime T: type, data: T, writer: *std.Io.Writer) std.Io.Writer.Error!void { + const default: T = .{}; + inline for (@typeInfo(T).@"struct".fields) |field_info| { + if (config_eql( + field_info.type, + @field(data, field_info.name), + @field(default, field_info.name), + )) { + try writer.print("# {s} ", .{field_info.name}); + } else { + try writer.print("{s} ", .{field_info.name}); + } + switch (field_info.type) { + u24 => try write_color_value(@field(data, field_info.name), writer), + ?u24 => if (@field(data, field_info.name)) |value| + try write_color_value(value, writer) + else + try writer.writeAll("null"), + else => { + var s: std.json.Stringify = .{ .writer = writer, .options = .{ .whitespace = .minified } }; + try s.write(@field(data, field_info.name)); + }, + } + try writer.print("\n", .{}); + } +} + +fn write_color_value(value: u24, writer: *std.Io.Writer) std.Io.Writer.Error!void { + var hex: [7]u8 = undefined; + try writer.writeByte('"'); + try writer.writeAll(color.RGB.to_string(color.RGB.from_u24(value), &hex)); + try writer.writeByte('"'); +} + +fn config_eql(comptime T: type, a: T, b: T) bool { + switch (T) { + []const u8 => return std.mem.eql(u8, a, b), + []const []const u8 => { + if (a.len != b.len) return false; + for (a, 0..) |x, i| if (!config_eql([]const u8, x, b[i])) return false; + return true; + }, + else => {}, + } + switch (@typeInfo(T)) { + .bool, .int, .float, .@"enum" => return a == b, + .optional => |info| { + if (a == null and b == null) + return true; + if (a == null or b == null) + return false; + return config_eql(info.child, a.?, b.?); + }, + .pointer => |info| switch (info.size) { + .slice => { + if (a.len != b.len) return false; + for (a, 0..) |x, i| if (!config_eql(info.child, x, b[i])) return false; + return true; + }, + else => @compileError("unsupported config type " ++ @typeName(T)), + }, + else => {}, + } + @compileError("unsupported config type " ++ @typeName(T)); +} + +fn write_json_file(comptime T: type, data: T, allocator: std.mem.Allocator, file_name: []const u8) !void { + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + + var cb = std.ArrayList(u8).init(allocator); + defer cb.deinit(); + try cbor.writeValue(cb.writer(), data); + + var s = std.json.writeStream(file.writer(), .{ .whitespace = .indent_4 }); + var iter: []const u8 = cb.items; + try cbor.JsonStream(std.fs.File).jsonWriteValue(&s, &iter); +} + +pub fn read_keybind_namespace(allocator: std.mem.Allocator, namespace_name: []const u8) ?[]const u8 { + const file_name = get_keybind_namespace_file_name(namespace_name) catch return null; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; + defer file.close(); + return file.readToEndAlloc(allocator, 64 * 1024) catch null; +} + +pub fn write_keybind_namespace(namespace_name: []const u8, content: []const u8) !void { + const file_name = try get_keybind_namespace_file_name(namespace_name); + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(content); +} + +pub fn list_keybind_namespaces(allocator: std.mem.Allocator) ![]const []const u8 { + var dir = try std.fs.openDirAbsolute(try get_keybind_namespaces_directory(), .{ .iterate = true }); + defer dir.close(); + var result: std.ArrayList([]const u8) = .empty; + var iter = dir.iterateAssumeFirstIteration(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))), + else => continue, + } + } + return result.toOwnedSlice(allocator); +} + +pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const u8 { + const file_name = get_theme_file_name(theme_name) catch return null; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; + defer file.close(); + return file.readToEndAlloc(allocator, 64 * 1024) catch null; +} + +pub fn write_theme(theme_name: []const u8, content: []const u8) !void { + const file_name = try get_theme_file_name(theme_name); + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(content); +} + +pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { + var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); + defer dir.close(); + var result = std.ArrayList([]const u8).init(allocator); + var iter = dir.iterateAssumeFirstIteration(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + else => continue, + } + } + return result.toOwnedSlice(); +} + +pub fn get_config_dir() ConfigDirError![]const u8 { + return get_app_config_dir(application_name); +} + +pub const ConfigDirError = error{ + NoSpaceLeft, + MakeConfigDirFailed, + MakeHomeConfigDirFailed, + MakeAppConfigDirFailed, + AppConfigDirUnavailable, +}; + +fn make_dir_error(path: []const u8, err: anytype) @TypeOf(err) { + std.log.err("failed to create directory: '{s}'", .{path}); + return err; +} + +fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var config_dir: ?[]const u8 = null; + }; + const config_dir = if (local.config_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_CONFIG_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, error.MakeHomeConfigDirFailed), + }; + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config{c}{s}", .{ home, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, error.MakeAppConfigDirFailed), + }; + break :ret dir; + } else return error.AppConfigDirUnavailable; + } else return error.AppConfigDirUnavailable; + + local.config_dir = config_dir; + std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(config_dir, error.MakeConfigDirFailed), + }; + + var keybind_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&keybind_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, keybind_dir })) catch {}; + + var theme_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, theme_dir })) catch {}; + + return config_dir; +} + +pub fn get_cache_dir() ![]const u8 { + return get_app_cache_dir(application_name); +} + +fn get_app_cache_dir(appname: []const u8) ![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var cache_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var cache_dir: ?[]const u8 = null; + }; + const cache_dir = if (local.cache_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_CACHE_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache{c}{s}", .{ home, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret dir; + } else return error.AppCacheDirUnavailable; + } else return error.AppCacheDirUnavailable; + + local.cache_dir = cache_dir; + std.fs.makeDirAbsolute(cache_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(cache_dir, e), + }; + return cache_dir; +} + +pub fn get_state_dir() ![]const u8 { + return get_app_state_dir(application_name); +} + +fn get_app_state_dir(appname: []const u8) ![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var state_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var state_dir: ?[]const u8 = null; + }; + const state_dir = if (local.state_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_STATE_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + var dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state", .{ home, sep, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state{c}{s}", .{ home, sep, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret dir; + } else return error.AppCacheDirUnavailable; + } else return error.AppCacheDirUnavailable; + + local.state_dir = state_dir; + std.fs.makeDirAbsolute(state_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(state_dir, e), + }; + return state_dir; +} + +fn get_app_config_file_name(appname: []const u8, comptime base_name: []const u8) ConfigDirError![]const u8 { + return get_app_config_dir_file_name(appname, base_name ++ ".json"); +} + +fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: []const u8) ConfigDirError![]const u8 { + const local = struct { + var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return std.fmt.bufPrint(&local.config_file_buffer, "{s}{c}{s}", .{ try get_app_config_dir(appname), sep, config_file_name }); +} + +pub fn get_config_file_name(T: type) ![]const u8 { + return get_app_config_file_name(application_name, @typeName(T)); +} + +pub fn get_restore_file_name() ![]const u8 { + const local = struct { + var restore_file_buffer: [std.posix.PATH_MAX]u8 = undefined; + var restore_file: ?[]const u8 = null; + }; + const restore_file_name = "restore"; + const restore_file = if (local.restore_file) |file| + file + else + try std.fmt.bufPrint(&local.restore_file_buffer, "{s}{c}{s}", .{ try get_app_state_dir(application_name), sep, restore_file_name }); + local.restore_file = restore_file; + return restore_file; +} + +const keybind_dir = "keys"; + +fn get_keybind_namespaces_directory() ![]const u8 { + const local = struct { + var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + const a = std.heap.c_allocator; + if (std.process.getEnvVarOwned(a, "FLOW_KEYS_DIR") catch null) |dir| { + defer a.free(dir); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); + } + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, keybind_dir }); +} + +pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { + const dir = try get_keybind_namespaces_directory(); + const local = struct { + var file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, namespace_name }); +} + +const theme_dir = "themes"; + +fn get_theme_directory() ![]const u8 { + const local = struct { + var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + const a = std.heap.c_allocator; + if (std.process.getEnvVarOwned(a, "FLOW_THEMES_DIR") catch null) |dir| { + defer a.free(dir); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); + } + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, theme_dir }); +} + +pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { + const dir = try get_theme_directory(); + const local = struct { + var file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, theme_name }); +} + +fn restart() noreturn { + var executable: [:0]const u8 = std.mem.span(std.os.argv[0]); + var is_basename = true; + for (executable) |char| if (std.fs.path.isSep(char)) { + is_basename = false; + }; + if (is_basename) { + const a = std.heap.c_allocator; + executable = bin_path.find_binary_in_path(a, executable) catch executable orelse executable; + } + const argv = [_]?[*:0]const u8{ + executable, + "--restore-session", + null, + }; + const ret = std.c.execve(executable, @ptrCast(&argv), @ptrCast(std.os.environ)); + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + stderr_writer.interface.print("\nrestart failed: {d}", .{ret}) catch {}; + stderr_writer.interface.flush() catch {}; + exit(234); +} + +pub fn is_directory(rel_path: []const u8) bool { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const abs_path = std.fs.cwd().realpath(rel_path, &path_buf) catch return false; + var dir = std.fs.openDirAbsolute(abs_path, .{}) catch return false; + dir.close(); + return true; +} + +pub fn is_file(rel_path: []const u8) bool { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const abs_path = std.fs.cwd().realpath(rel_path, &path_buf) catch return false; + var file = std.fs.openFileAbsolute(abs_path, .{ .mode = .read_only }) catch return false; + defer file.close(); + return true; +} + +pub fn shorten_path(buf: []u8, path: []const u8, removed_prefix: *usize, max_len: usize) []const u8 { + removed_prefix.* = 0; + if (path.len <= max_len) return path; + const ellipsis = "…"; + const prefix = path.len - max_len; + defer removed_prefix.* = prefix - 1; + @memcpy(buf[0..ellipsis.len], ellipsis); + @memcpy(buf[ellipsis.len .. max_len + ellipsis.len], path[prefix..]); + return buf[0 .. max_len + ellipsis.len]; +} diff --git a/test/tests_buffer_output.txt b/test/tests_buffer_output.txt index 52df48f..1c4f2fe 100644 --- a/test/tests_buffer_output.txt +++ b/test/tests_buffer_output.txt @@ -1,134 +1,1105 @@ -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz -01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 -abcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyzabcdefghijklmnopqrstuvxyz +const std = @import("std"); +const tui = @import("tui"); +const cbor = @import("cbor"); +const thespian = @import("thespian"); +const color = @import("color"); +const flags = @import("flags"); +const builtin = @import("builtin"); +const bin_path = @import("bin_path"); +const sep = std.fs.path.sep; + +const list_languages = @import("list_languages.zig"); +const file_link = @import("file_link"); + +const c = @cImport({ + @cInclude("locale.h"); +}); + +const build_options = @import("build_options"); +const log = @import("log"); + +pub const version = @embedFile("version"); +pub const version_info = @embedFile("version_info"); + +pub const max_diff_lines: usize = 50000; +pub const max_syntax_lines: usize = 50000; + +pub const application_name = "flow"; +pub const application_title = "Flow Control"; +pub const application_subtext = "a programmer's text editor"; +pub const application_description = application_title ++ ": " ++ application_subtext; + +pub const std_options: std.Options = .{ + // .log_level = if (builtin.mode == .Debug) .debug else .warn, + .log_level = if (builtin.mode == .Debug) .info else .warn, + .logFn = log.std_log_function, +}; + +const renderer = @import("renderer"); + +pub const panic = if (@hasDecl(renderer, "panic")) renderer.panic else default_panic; + +fn default_panic(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { + return std.debug.defaultPanic(msg, ret_addr); +} + +pub fn main() anyerror!void { + if (builtin.os.tag == .linux) { + // drain stdin so we don't pickup junk from previous application/shell + _ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.posix.STDIN_FILENO))), std.os.linux.T.CFLSH, 0); + } + + const a = std.heap.c_allocator; + + const Flags = struct { + pub const description = + application_title ++ ": " ++ application_subtext ++ + \\ + \\ + \\Pass in file names to be opened with an optional :LINE or :LINE:COL appended to the + \\file name to specify a specific location, or pass + separately to set the line. + ; + + pub const descriptions = .{ + .project = "Set project directory (default: cwd)", + .no_persist = "Do not persist new projects", + .frame_rate = "Set target frame rate (default: 60)", + .debug_wait = "Wait for key press before starting UI", + .debug_dump_on_error = "Dump stack traces on errors", + .no_sleep = "Do not sleep the main loop when idle", + .no_alternate = "Do not use the alternate terminal screen", + .trace_level = "Enable internal tracing (level of detail from 1-5)", + .no_trace = "Do not enable internal tracing", + .restore_session = "Restore restart session", + .show_input = "Open the input view on start", + .show_log = "Open the log view on start", + .language = "Force the language of the file to be opened", + .list_languages = "Show available languages", + .no_syntax = "Disable syntax highlighting", + .syntax_report_timing = "Report syntax highlighting time", + .exec = "Execute a command on startup", + .literal = "Disable :LINE and +LINE syntax", + .scratch = "Open a scratch (temporary) buffer on start", + .new_file = "Create a new untitled file on start", + .dark = "Use dark color scheme", + .light = "Use light color scheme", + .version = "Show build version and exit", + }; + + pub const formats = .{ .frame_rate = "num", .trace_level = "num", .exec = "cmds" }; + + pub const switches = .{ + .project = 'p', + .no_persist = 'N', + .frame_rate = 'f', + .trace_level = 't', + .language = 'l', + .exec = 'e', + .literal = 'L', + .scratch = 'S', + .new_file = 'n', + .version = 'v', + }; + + project: ?[]const u8, + no_persist: bool, + frame_rate: ?usize, + debug_wait: bool, + debug_dump_on_error: bool, + no_sleep: bool, + no_alternate: bool, + trace_level: u8 = 0, + no_trace: bool, + restore_session: bool, + show_input: bool, + show_log: bool, + language: ?[]const u8, + list_languages: bool, + no_syntax: bool, + syntax_report_timing: bool, + exec: ?[]const u8, + literal: bool, + scratch: bool, + new_file: bool, + dark: bool, + light: bool, + version: bool, + + positional: struct { + trailing: []const []const u8, + }, + }; + + const args_alloc = try std.process.argsAlloc(a); + defer std.process.argsFree(a, args_alloc); + + var diag: flags.Diagnostics = undefined; + + const args = flags.parse(args_alloc, "flow", Flags, .{ + .diagnostics = &diag, + }) catch |err| { + if (err == error.PrintedHelp) exit(0); + try diag.printUsage(&flags.ColorScheme.default); + exit(1); + return err; + }; + + var stdout_buf: [4096]u8 = undefined; + var stdout_file = std.fs.File.stdout().writer(&stdout_buf); + const stdout = &stdout_file.interface; + defer stdout.flush() catch {}; + var stderr_buf: [4096]u8 = undefined; + var stderr_file = std.fs.File.stderr().writer(&stderr_buf); + const stderr = &stderr_file.interface; + defer stderr.flush() catch {}; + + if (args.version) + return std.fs.File.stdout().writeAll(version_info); + + if (args.list_languages) { + const tty_config = std.io.tty.detectConfig(std.fs.File.stdout()); + return list_languages.list(a, stdout, tty_config); + } + + if (builtin.os.tag != .windows and @hasDecl(renderer, "install_crash_handler")) { + if (std.posix.getenv("JITDEBUG")) |_| renderer.jit_debugger_enabled = true; + renderer.install_crash_handler(); + } + + if (args.debug_wait) { + std.debug.print("press return to start", .{}); + var buf: [1]u8 = undefined; + _ = try std.fs.File.stdin().read(&buf); + } + + if (c.setlocale(c.LC_ALL, "") == null) { + try stderr.print("Failed to set locale. Is your locale valid?\n", .{}); + stderr.flush() catch {}; + exit(1); + } + + thespian.stack_trace_on_errors = args.debug_dump_on_error; + + var ctx = try thespian.context.init(a); + defer ctx.deinit(); + + const env = thespian.env.init(); + defer env.deinit(); + if (build_options.enable_tracy) { + if (!args.no_trace) { + env.enable_all_channels(); + env.on_trace(trace); + } + } else { + if (args.trace_level != 0) { + var threshold: usize = 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.debug); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.widget); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.event); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.input); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.receive); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.metronome); + env.enable(thespian.channel.execute); + env.enable(thespian.channel.link); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable(thespian.channel.send); + } + threshold += 1; + if (args.trace_level >= threshold) { + env.enable_all_channels(); + } + + env.on_trace(trace_to_file); + } + } + + const log_proc = try log.spawn(&ctx, a, &env); + defer log_proc.deinit(); + log.set_std_log_pid(log_proc.ref()); + defer log.set_std_log_pid(null); + + env.set("no-persist", args.no_persist); + env.set("restore-session", args.restore_session); + env.set("no-alternate", args.no_alternate); + env.set("show-input", args.show_input); + env.set("show-log", args.show_log); + env.set("no-sleep", args.no_sleep); + env.set("no-syntax", args.no_syntax); + env.set("syntax-report-timing", args.syntax_report_timing); + env.set("dump-stack-trace", args.debug_dump_on_error); + if (args.frame_rate) |s| env.num_set("frame-rate", @intCast(s)); + env.proc_set("log", log_proc.ref()); + if (args.language) |s| env.str_set("language", s); + + var eh = thespian.make_exit_handler({}, print_exit_status); + const tui_proc = try tui.spawn(a, &ctx, &eh, &env); + defer tui_proc.deinit(); + + var links: std.ArrayList(file_link.Dest) = .empty; + defer links.deinit(a); + var prev: ?*file_link.Dest = null; + var line_next: ?usize = null; + var offset_next: ?usize = null; + for (args.positional.trailing) |arg| { + if (arg.len == 0) continue; + + if (!args.literal and arg[0] == '+') { + if (arg.len > 2 and arg[1] == 'b') { + const offset = try std.fmt.parseInt(usize, arg[2..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.offset = offset; + continue; + }, + else => {}, + }; + offset_next = offset; + line_next = null; + } else { + const line = try std.fmt.parseInt(usize, arg[1..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.line = line; + continue; + }, + else => {}, + }; + line_next = line; + offset_next = null; + } + continue; + } + + const curr = try links.addOne(a); + curr.* = if (!args.literal) try file_link.parse(arg) else .{ .file = .{ .path = arg } }; + prev = curr; + + if (line_next) |line| { + switch (curr.*) { + .file => |*file| { + file.line = line; + line_next = null; + }, + else => {}, + } + } + if (offset_next) |offset| { + switch (curr.*) { + .file => |*file| { + file.offset = offset; + offset_next = null; + }, + else => {}, + } + } + } + + var have_project = false; + var have_file = false; + if (args.project) |project| { + try tui_proc.send(.{ "cmd", "open_project_dir", .{project} }); + have_project = true; + } + for (links.items) |link| switch (link) { + .dir => |dir| { + if (have_project) { + std.debug.print("more than one project directory is not allowed\n", .{}); + exit(1); + } + try tui_proc.send(.{ "cmd", "open_project_dir", .{dir.path} }); + have_project = true; + }, + else => { + have_file = true; + }, + }; + + for (links.items) |link| { + try file_link.navigate(tui_proc.ref(), &link); + } + + if (!have_file) { + if (!have_project) + try tui_proc.send(.{ "cmd", "open_project_cwd" }); + try tui_proc.send(.{ "cmd", "show_home" }); + } + + if (args.new_file) { + try tui_proc.send(.{ "cmd", "create_new_file", .{} }); + } else if (args.scratch) { + try tui_proc.send(.{ "cmd", "create_scratch_buffer", .{} }); + } + + if (args.dark) + try tui_proc.send(.{ "cmd", "force_color_scheme", .{"dark"} }) + else if (args.light) + try tui_proc.send(.{ "cmd", "force_color_scheme", .{"light"} }); + + if (args.exec) |exec_str| { + var cmds = std.mem.splitScalar(u8, exec_str, ';'); + while (cmds.next()) |cmd| { + var count_args_ = std.mem.splitScalar(u8, cmd, ':'); + var count: usize = 0; + while (count_args_.next()) |_| count += 1; + if (count == 0) break; + + var msg: std.Io.Writer.Allocating = .init(a); + defer msg.deinit(); + const writer = &msg.writer; + + var cmd_args = std.mem.splitScalar(u8, cmd, ':'); + const cmd_ = cmd_args.next(); + try cbor.writeArrayHeader(writer, 3); + try cbor.writeValue(writer, "cmd"); + try cbor.writeValue(writer, cmd_); + try cbor.writeArrayHeader(writer, count - 1); + + while (cmd_args.next()) |arg| { + if (std.fmt.parseInt(isize, arg, 10) catch null) |i| + try cbor.writeValue(writer, i) + else + try cbor.writeValue(writer, arg); + } + + try tui_proc.send_raw(.{ .buf = msg.written() }); + } + } + + ctx.run(); + + if (want_restart) restart(); + exit(final_exit_status); +} + +var final_exit_status: u8 = 0; +var want_restart: bool = false; + +pub fn print_exit_status(_: void, msg: []const u8) void { + if (std.mem.eql(u8, msg, "normal")) { + return; + } else if (std.mem.eql(u8, msg, "restart")) { + want_restart = true; + } else { + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + stderr_writer.interface.print("\n" ++ application_name ++ " ERROR: {s}\n", .{msg}) catch {}; + stderr_writer.interface.flush() catch {}; + final_exit_status = 1; + } +} + +fn count_args() usize { + var args = std.process.args(); + _ = args.next(); + var count: usize = 0; + while (args.next()) |_| { + count += 1; + } + return count; +} + +fn trace(m: thespian.message.c_buffer_type) callconv(.c) void { + thespian.message.from(m).to_json_cb(trace_json); +} + +fn trace_json(json: thespian.message.json_string_view) callconv(.c) void { + const callstack_depth = 10; + ___tracy_emit_message(json.base, json.len, callstack_depth); +} +extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void; + +var trace_mutex: std.Thread.Mutex = .{}; + +fn trace_to_file(m: thespian.message.c_buffer_type) callconv(.c) void { + trace_mutex.lock(); + defer trace_mutex.unlock(); + + const State = struct { + file: std.fs.File, + file_writer: std.fs.File.Writer, + last_time: i64, + var state: ?@This() = null; + var trace_buffer: [4096]u8 = undefined; + + fn write_tdiff(writer: anytype, tdiff: i64) !void { + const msi = @divFloor(tdiff, std.time.us_per_ms); + if (msi < 10) { + const d: f64 = @floatFromInt(tdiff); + const ms = d / std.time.us_per_ms; + _ = try writer.print("{d:6.2} ", .{ms}); + } else { + const ms: u64 = @intCast(msi); + _ = try writer.print("{d:6} ", .{ms}); + } + } + }; + const a = std.heap.c_allocator; + var state: *State = &(State.state orelse init: { + var path: std.Io.Writer.Allocating = .init(a); + defer path.deinit(); + path.writer.print("{s}{c}trace.log", .{ get_state_dir() catch return, sep }) catch return; + const file = std.fs.createFileAbsolute(path.written(), .{ .truncate = true }) catch return; + State.state = .{ + .file = file, + .file_writer = file.writer(&State.trace_buffer), + .last_time = std.time.microTimestamp(), + }; + break :init State.state.?; + }); + const writer = &state.file_writer.interface; + + const ts = std.time.microTimestamp(); + State.write_tdiff(writer, ts - state.last_time) catch {}; + state.last_time = ts; + + var stream: std.json.Stringify = .{ .writer = writer }; + var iter: []const u8 = m.base[0..m.len]; + cbor.JsonWriter.jsonWriteValue(&stream, &iter) catch {}; + _ = writer.write("\n") catch {}; + writer.flush() catch {}; +} + +pub fn exit(status: u8) noreturn { + if (builtin.os.tag == .linux) { + // drain stdin so we don't leave junk at the next prompt + _ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.posix.STDIN_FILENO))), std.os.linux.T.CFLSH, 0); + } + std.posix.exit(status); +} + +pub fn free_config(allocator: std.mem.Allocator, bufs: [][]const u8) void { + for (bufs) |buf| allocator.free(buf); +} + +var config_mutex: std.Thread.Mutex = .{}; + +pub fn exists_config(T: type) bool { + config_mutex.lock(); + defer config_mutex.unlock(); + const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return false; + const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; + var file = std.fs.openFileAbsolute(text_file_name, .{ .mode = .read_only }) catch return false; + defer file.close(); + return true; +} + +fn get_default(T: type) T { + return switch (@typeInfo(T)) { + .array => &.{}, + .pointer => |info| switch (info.size) { + .slice => &.{}, + else => @compileError("unsupported config type " ++ @typeName(T)), + }, + else => .{}, + }; +} + +pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } { + config_mutex.lock(); + defer config_mutex.unlock(); + var bufs: [][]const u8 = &[_][]const u8{}; + const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ get_default(T), bufs }; + const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; + var conf: T = get_default(T); + if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { + _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); + } + read_nested_include_files(T, allocator, &conf, &bufs); + return .{ conf, bufs }; +} + +// returns true if the file was found +fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8, file_name: []const u8) bool { + std.log.info("loading {s}", .{file_name}); + const err: anyerror = blk: { + if (std.mem.endsWith(u8, file_name, ".json")) if (read_json_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; + if (read_text_config_file(T, allocator, conf, bufs, file_name)) return true else |e| break :blk e; + }; + switch (err) { + error.FileNotFound => return false, + else => |e| std.log.err("error reading config file '{s}': {s}", .{ file_name, @errorName(e) }), + } + return true; +} + +fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { + var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); + defer file.close(); + const content = try file.readToEndAlloc(allocator, 64 * 1024); + defer allocator.free(content); + return parse_text_config_file(T, allocator, conf, bufs_, file_name, content); +} + +pub fn parse_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8, content: []const u8) !void { + var cbor_buf: std.Io.Writer.Allocating = .init(allocator); + defer cbor_buf.deinit(); + const writer = &cbor_buf.writer; + var it = std.mem.splitScalar(u8, content, '\n'); + var lineno: u32 = 0; + while (it.next()) |line| { + lineno += 1; + if (line.len == 0 or line[0] == '#') + continue; + const spc = std.mem.indexOfScalar(u8, line, ' ') orelse { + std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line }); + continue; + }; + const name = line[0..spc]; + const value_str = line[spc + 1 ..]; + const cb = cbor.fromJsonAlloc(allocator, value_str) catch { + std.log.err("{s}:{}: {s} has bad value: {s}", .{ file_name, lineno, name, value_str }); + continue; + }; + defer allocator.free(cb); + try cbor.writeValue(writer, name); + try writer.writeAll(cb); + } + const cb = try cbor_buf.toOwnedSlice(); + var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); + bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file"); + bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_text_config_file"); + return read_cbor_config(T, allocator, conf, file_name, cb); +} + +fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { + var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); + defer file.close(); + const json = try file.readToEndAlloc(allocator, 64 * 1024); + defer allocator.free(json); + const cbor_buf: []u8 = try allocator.alloc(u8, json.len); + var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); + bufs.append(allocator, cbor_buf) catch @panic("OOM:read_json_config_file"); + bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_json_config_file"); + const cb = try cbor.fromJson(json, cbor_buf); + var iter = cb; + _ = try cbor.decodeMapHeader(&iter); + return read_cbor_config(T, allocator, conf, file_name, iter); +} + +fn read_cbor_config( + T: type, + allocator: std.mem.Allocator, + conf: *T, + file_name: []const u8, + cb: []const u8, +) !void { + var iter = cb; + var field_name: []const u8 = undefined; + while (cbor.matchString(&iter, &field_name) catch |e| switch (e) { + error.TooShort => return, + else => return e, + }) { + var known = false; + inline for (@typeInfo(T).@"struct".fields) |field_info| + if (comptime std.mem.eql(u8, "include_files", field_info.name)) { + if (std.mem.eql(u8, field_name, field_info.name)) { + known = true; + var value: field_info.type = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&value))) { + if (conf.include_files.len > 0) { + std.log.err("{s}: ignoring nested 'include_files' value '{s}'", .{ file_name, value }); + } else { + @field(conf, field_info.name) = value; + } + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + } + } else if (std.mem.eql(u8, field_name, field_info.name)) { + known = true; + switch (field_info.type) { + u24, ?u24 => { + var value: []const u8 = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&value))) { + const color_ = color.RGB.from_string(value); + if (color_) |color__| + @field(conf, field_info.name) = color__.to_u24() + else + std.log.err("invalid value for key '{s}'", .{field_name}); + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, + else => { + var value: field_info.type = undefined; + if (try cbor.matchValue(&iter, cbor.extractAlloc(&value, allocator))) { + @field(conf, field_info.name) = value; + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, + } + }; + if (!known) { + try cbor.skipValue(&iter); + std.log.err("unknown config value '{s}' ignored", .{field_name}); + } + } +} + +fn read_nested_include_files(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][]const u8) void { + if (conf.include_files.len == 0) return; + var it = std.mem.splitScalar(u8, conf.include_files, std.fs.path.delimiter); + while (it.next()) |path| if (!read_config_file(T, allocator, conf, bufs, path)) { + std.log.err("config include file '{s}' is not found", .{path}); + }; +} + +pub const ConfigWriteError = error{ CreateConfigFileFailed, WriteConfigFileFailed, WriteFailed }; + +pub fn write_config(conf: anytype, allocator: std.mem.Allocator) (ConfigDirError || ConfigWriteError)!void { + config_mutex.lock(); + defer config_mutex.unlock(); + _ = allocator; + const file_name = try get_app_config_file_name(application_name, @typeName(@TypeOf(conf))); + return write_text_config_file(@TypeOf(conf), conf, file_name[0 .. file_name.len - 5]); + // return write_json_file(@TypeOf(conf), conf, allocator, try get_app_config_file_name(application_name, @typeName(@TypeOf(conf)))); +} + +fn write_text_config_file(comptime T: type, data: T, file_name: []const u8) ConfigWriteError!void { + var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch |e| { + std.log.err("createFileAbsolute failed with {any} for: {s}", .{ e, file_name }); + return error.CreateConfigFileFailed; + }; + defer file.close(); + var buf: [4096]u8 = undefined; + var writer = file.writer(&buf); + write_config_to_writer(T, data, &writer.interface) catch |e| { + std.log.err("write file failed with {any} for: {s}", .{ e, file_name }); + return error.WriteConfigFileFailed; + }; + try writer.interface.flush(); +} + +pub fn write_config_to_writer(comptime T: type, data: T, writer: *std.Io.Writer) std.Io.Writer.Error!void { + const default: T = .{}; + inline for (@typeInfo(T).@"struct".fields) |field_info| { + if (config_eql( + field_info.type, + @field(data, field_info.name), + @field(default, field_info.name), + )) { + try writer.print("# {s} ", .{field_info.name}); + } else { + try writer.print("{s} ", .{field_info.name}); + } + switch (field_info.type) { + u24 => try write_color_value(@field(data, field_info.name), writer), + ?u24 => if (@field(data, field_info.name)) |value| + try write_color_value(value, writer) + else + try writer.writeAll("null"), + else => { + var s: std.json.Stringify = .{ .writer = writer, .options = .{ .whitespace = .minified } }; + try s.write(@field(data, field_info.name)); + }, + } + try writer.print("\n", .{}); + } +} + +fn write_color_value(value: u24, writer: *std.Io.Writer) std.Io.Writer.Error!void { + var hex: [7]u8 = undefined; + try writer.writeByte('"'); + try writer.writeAll(color.RGB.to_string(color.RGB.from_u24(value), &hex)); + try writer.writeByte('"'); +} + +fn config_eql(comptime T: type, a: T, b: T) bool { + switch (T) { + []const u8 => return std.mem.eql(u8, a, b), + []const []const u8 => { + if (a.len != b.len) return false; + for (a, 0..) |x, i| if (!config_eql([]const u8, x, b[i])) return false; + return true; + }, + else => {}, + } + switch (@typeInfo(T)) { + .bool, .int, .float, .@"enum" => return a == b, + .optional => |info| { + if (a == null and b == null) + return true; + if (a == null or b == null) + return false; + return config_eql(info.child, a.?, b.?); + }, + .pointer => |info| switch (info.size) { + .slice => { + if (a.len != b.len) return false; + for (a, 0..) |x, i| if (!config_eql(info.child, x, b[i])) return false; + return true; + }, + else => @compileError("unsupported config type " ++ @typeName(T)), + }, + else => {}, + } + @compileError("unsupported config type " ++ @typeName(T)); +} + +fn write_json_file(comptime T: type, data: T, allocator: std.mem.Allocator, file_name: []const u8) !void { + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + + var cb = std.ArrayList(u8).init(allocator); + defer cb.deinit(); + try cbor.writeValue(cb.writer(), data); + + var s = std.json.writeStream(file.writer(), .{ .whitespace = .indent_4 }); + var iter: []const u8 = cb.items; + try cbor.JsonStream(std.fs.File).jsonWriteValue(&s, &iter); +} + +pub fn read_keybind_namespace(allocator: std.mem.Allocator, namespace_name: []const u8) ?[]const u8 { + const file_name = get_keybind_namespace_file_name(namespace_name) catch return null; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; + defer file.close(); + return file.readToEndAlloc(allocator, 64 * 1024) catch null; +} + +pub fn write_keybind_namespace(namespace_name: []const u8, content: []const u8) !void { + const file_name = try get_keybind_namespace_file_name(namespace_name); + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(content); +} + +pub fn list_keybind_namespaces(allocator: std.mem.Allocator) ![]const []const u8 { + var dir = try std.fs.openDirAbsolute(try get_keybind_namespaces_directory(), .{ .iterate = true }); + defer dir.close(); + var result: std.ArrayList([]const u8) = .empty; + var iter = dir.iterateAssumeFirstIteration(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file, .sym_link => try result.append(allocator, try allocator.dupe(u8, std.fs.path.stem(entry.name))), + else => continue, + } + } + return result.toOwnedSlice(allocator); +} + +pub fn read_theme(allocator: std.mem.Allocator, theme_name: []const u8) ?[]const u8 { + const file_name = get_theme_file_name(theme_name) catch return null; + var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; + defer file.close(); + return file.readToEndAlloc(allocator, 64 * 1024) catch null; +} + +pub fn write_theme(theme_name: []const u8, content: []const u8) !void { + const file_name = try get_theme_file_name(theme_name); + var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true }); + defer file.close(); + return file.writeAll(content); +} + +pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { + var dir = try std.fs.openDirAbsolute(try get_theme_directory(), .{ .iterate = true }); + defer dir.close(); + var result = std.ArrayList([]const u8).init(allocator); + var iter = dir.iterateAssumeFirstIteration(); + while (try iter.next()) |entry| { + switch (entry.kind) { + .file, .sym_link => try result.append(try allocator.dupe(u8, std.fs.path.stem(entry.name))), + else => continue, + } + } + return result.toOwnedSlice(); +} + +pub fn get_config_dir() ConfigDirError![]const u8 { + return get_app_config_dir(application_name); +} + +pub const ConfigDirError = error{ + NoSpaceLeft, + MakeConfigDirFailed, + MakeHomeConfigDirFailed, + MakeAppConfigDirFailed, + AppConfigDirUnavailable, +}; + +fn make_dir_error(path: []const u8, err: anytype) @TypeOf(err) { + std.log.err("failed to create directory: '{s}'", .{path}); + return err; +} + +fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var config_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var config_dir: ?[]const u8 = null; + }; + const config_dir = if (local.config_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_CONFIG_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, error.MakeHomeConfigDirFailed), + }; + break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}.config{c}{s}", .{ home, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.config_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, error.MakeAppConfigDirFailed), + }; + break :ret dir; + } else return error.AppConfigDirUnavailable; + } else return error.AppConfigDirUnavailable; + + local.config_dir = config_dir; + std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(config_dir, error.MakeConfigDirFailed), + }; + + var keybind_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&keybind_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, keybind_dir })) catch {}; + + var theme_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, theme_dir })) catch {}; + + return config_dir; +} + +pub fn get_cache_dir() ![]const u8 { + return get_app_cache_dir(application_name); +} + +fn get_app_cache_dir(appname: []const u8) ![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var cache_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var cache_dir: ?[]const u8 = null; + }; + const cache_dir = if (local.cache_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_CACHE_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}.cache{c}{s}", .{ home, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret dir; + } else return error.AppCacheDirUnavailable; + } else return error.AppCacheDirUnavailable; + + local.cache_dir = cache_dir; + std.fs.makeDirAbsolute(cache_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(cache_dir, e), + }; + return cache_dir; +} + +pub fn get_state_dir() ![]const u8 { + return get_app_state_dir(application_name); +} + +fn get_app_state_dir(appname: []const u8) ![]const u8 { + const a = std.heap.c_allocator; + const local = struct { + var state_dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + var state_dir: ?[]const u8 = null; + }; + const state_dir = if (local.state_dir) |dir| + dir + else if (std.process.getEnvVarOwned(a, "XDG_STATE_HOME") catch null) |xdg| ret: { + defer a.free(xdg); + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ xdg, sep, appname }); + } else if (std.process.getEnvVarOwned(a, "HOME") catch null) |home| ret: { + defer a.free(home); + var dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local", .{ home, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state", .{ home, sep, sep }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}.local{c}state{c}{s}", .{ home, sep, sep, sep, appname }); + } else if (builtin.os.tag == .windows) ret: { + if (std.process.getEnvVarOwned(a, "APPDATA") catch null) |appdata| { + defer a.free(appdata); + const dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}{c}{s}", .{ appdata, sep, appname }); + std.fs.makeDirAbsolute(dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(dir, e), + }; + break :ret dir; + } else return error.AppCacheDirUnavailable; + } else return error.AppCacheDirUnavailable; + + local.state_dir = state_dir; + std.fs.makeDirAbsolute(state_dir) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return make_dir_error(state_dir, e), + }; + return state_dir; +} + +fn get_app_config_file_name(appname: []const u8, comptime base_name: []const u8) ConfigDirError![]const u8 { + return get_app_config_dir_file_name(appname, base_name ++ ".json"); +} + +fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: []const u8) ConfigDirError![]const u8 { + const local = struct { + var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return std.fmt.bufPrint(&local.config_file_buffer, "{s}{c}{s}", .{ try get_app_config_dir(appname), sep, config_file_name }); +} + +pub fn get_config_file_name(T: type) ![]const u8 { + return get_app_config_file_name(application_name, @typeName(T)); +} + +pub fn get_restore_file_name() ![]const u8 { + const local = struct { + var restore_file_buffer: [std.posix.PATH_MAX]u8 = undefined; + var restore_file: ?[]const u8 = null; + }; + const restore_file_name = "restore"; + const restore_file = if (local.restore_file) |file| + file + else + try std.fmt.bufPrint(&local.restore_file_buffer, "{s}{c}{s}", .{ try get_app_state_dir(application_name), sep, restore_file_name }); + local.restore_file = restore_file; + return restore_file; +} + +const keybind_dir = "keys"; + +fn get_keybind_namespaces_directory() ![]const u8 { + const local = struct { + var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + const a = std.heap.c_allocator; + if (std.process.getEnvVarOwned(a, "FLOW_KEYS_DIR") catch null) |dir| { + defer a.free(dir); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); + } + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, keybind_dir }); +} + +pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { + const dir = try get_keybind_namespaces_directory(); + const local = struct { + var file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, namespace_name }); +} + +const theme_dir = "themes"; + +fn get_theme_directory() ![]const u8 { + const local = struct { + var dir_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + const a = std.heap.c_allocator; + if (std.process.getEnvVarOwned(a, "FLOW_THEMES_DIR") catch null) |dir| { + defer a.free(dir); + return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); + } + return try std.fmt.bufPrint(&local.dir_buffer, "{s}{c}{s}", .{ try get_app_config_dir(application_name), sep, theme_dir }); +} + +pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { + const dir = try get_theme_directory(); + const local = struct { + var file_buffer: [std.posix.PATH_MAX]u8 = undefined; + }; + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, theme_name }); +} + +fn restart() noreturn { + var executable: [:0]const u8 = std.mem.span(std.os.argv[0]); + var is_basename = true; + for (executable) |char| if (std.fs.path.isSep(char)) { + is_basename = false; + }; + if (is_basename) { + const a = std.heap.c_allocator; + executable = bin_path.find_binary_in_path(a, executable) catch executable orelse executable; + } + const argv = [_]?[*:0]const u8{ + executable, + "--restore-session", + null, + }; + const ret = std.c.execve(executable, @ptrCast(&argv), @ptrCast(std.os.environ)); + var stderr_buffer: [1024]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + stderr_writer.interface.print("\nrestart failed: {d}", .{ret}) catch {}; + stderr_writer.interface.flush() catch {}; + exit(234); +} + +pub fn is_directory(rel_path: []const u8) bool { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const abs_path = std.fs.cwd().realpath(rel_path, &path_buf) catch return false; + var dir = std.fs.openDirAbsolute(abs_path, .{}) catch return false; + dir.close(); + return true; +} + +pub fn is_file(rel_path: []const u8) bool { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const abs_path = std.fs.cwd().realpath(rel_path, &path_buf) catch return false; + var file = std.fs.openFileAbsolute(abs_path, .{ .mode = .read_only }) catch return false; + defer file.close(); + return true; +} + +pub fn shorten_path(buf: []u8, path: []const u8, removed_prefix: *usize, max_len: usize) []const u8 { + removed_prefix.* = 0; + if (path.len <= max_len) return path; + const ellipsis = "…"; + const prefix = path.len - max_len; + defer removed_prefix.* = prefix - 1; + @memcpy(buf[0..ellipsis.len], ellipsis); + @memcpy(buf[ellipsis.len .. max_len + ellipsis.len], path[prefix..]); + return buf[0 .. max_len + ellipsis.len]; +}