diff --git a/src/Project.zig b/src/Project.zig index 23eaae1..6613215 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -8,6 +8,7 @@ const Buffer = @import("Buffer"); const fuzzig = @import("fuzzig"); const tracy = @import("tracy"); const git = @import("git"); +const file_type_config = @import("file_type_config"); const builtin = @import("builtin"); const LSP = @import("LSP.zig"); @@ -52,6 +53,9 @@ pub const LspOrClientError = (LspError || ClientError); const File = struct { path: []const u8, + type: []const u8, + icon: []const u8, + color: u24, mtime: i128, pos: FilePos = .{}, visited: bool = false, @@ -341,7 +345,7 @@ pub fn request_n_most_recent_file(self: *Self, from: tp.pid_ref, n: usize) Clien pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) ClientError!void { defer from.send(.{ "PRJ", "recent_done", self.longest_file_path, "" }) catch {}; for (self.files.items, 0..) |file, i| { - from.send(.{ "PRJ", "recent", self.longest_file_path, file.path }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, file.type, file.icon, file.color }) catch return error.ClientFailed; if (i >= max) return; } } @@ -356,7 +360,7 @@ fn simple_query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: [ defer self.allocator.free(matches); var n: usize = 0; while (n < query.len) : (n += 1) matches[n] = idx + n; - from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, matches }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, file.path, file.type, file.icon, file.color, matches }) catch return error.ClientFailed; i += 1; if (i >= max) return i; } @@ -379,6 +383,9 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co const Match = struct { path: []const u8, + type: []const u8, + icon: []const u8, + color: u24, score: i32, matches: []const usize, }; @@ -389,6 +396,9 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co if (match.score) |score| { (try matches.addOne()).* = .{ .path = file.path, + .type = file.type, + .icon = file.icon, + .color = file.color, .score = score, .matches = try self.allocator.dupe(usize, match.matches), }; @@ -404,13 +414,24 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co std.mem.sort(Match, matches.items, {}, less_fn); for (matches.items[0..@min(max, matches.items.len)]) |match| - from.send(.{ "PRJ", "recent", self.longest_file_path, match.path, match.matches }) catch return error.ClientFailed; + from.send(.{ "PRJ", "recent", self.longest_file_path, match.path, match.type, match.icon, match.color, match.matches }) catch return error.ClientFailed; return @min(max, matches.items.len); } -pub fn walk_tree_entry(self: *Self, file_path: []const u8, mtime: i128) OutOfMemoryError!void { +pub fn walk_tree_entry( + self: *Self, + file_path: []const u8, + mtime: i128, +) OutOfMemoryError!void { + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path); self.longest_file_path = @max(self.longest_file_path, file_path.len); - (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), .mtime = mtime }; + (try self.pending.addOne(self.allocator)).* = .{ + .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, + .mtime = mtime, + }; } pub fn walk_tree_done(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void { @@ -420,6 +441,35 @@ pub fn walk_tree_done(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void { return self.loaded(parent); } +fn default_ft() struct { []const u8, []const u8, u24 } { + return .{ + file_type_config.default.name, + file_type_config.default.icon, + file_type_config.default.color, + }; +} + +pub fn guess_path_file_type(path: []const u8, file_name: []const u8) struct { []const u8, []const u8, u24 } { + var buf: [4096]u8 = undefined; + const file_path = std.fmt.bufPrint(&buf, "{s}{}{s}", .{ path, std.fs.path.sep, file_name }) catch return default_ft(); + return guess_file_type(file_path); +} + +pub fn guess_file_type(file_path: []const u8) struct { []const u8, []const u8, u24 } { + var buf: [1024]u8 = undefined; + const content: []const u8 = blk: { + const file = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch break :blk &.{}; + defer file.close(); + const size = file.read(&buf) catch break :blk &.{}; + break :blk buf[0..size]; + }; + return if (file_type_config.guess_file_type(file_path, content)) |ft| .{ + ft.name, + ft.icon orelse file_type_config.default.icon, + ft.color orelse file_type_config.default.color, + } else default_ft(); +} + fn merge_pending_files(self: *Self) OutOfMemoryError!void { defer self.sort_files_by_mtime(); const existing = try self.files.toOwnedSlice(self.allocator); @@ -469,9 +519,13 @@ fn update_mru_internal(self: *Self, file_path: []const u8, mtime: i128, row: usi } return; } + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path); if (row != 0) { (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, .mtime = mtime, .pos = .{ .row = row, .col = col }, .visited = true, @@ -479,6 +533,9 @@ fn update_mru_internal(self: *Self, file_path: []const u8, mtime: i128, row: usi } else { (try self.files.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, file_path), + .type = file_type, + .icon = file_icon, + .color = file_color, .mtime = mtime, }; } @@ -1941,7 +1998,14 @@ pub fn process_git(self: *Self, parent: tp.pid_ref, m: tp.message) (OutOfMemoryE } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.extract(&path) })) { self.longest_file_path = @max(self.longest_file_path, path.len); const stat = std.fs.cwd().statFile(path) catch return; - (try self.pending.addOne(self.allocator)).* = .{ .path = try self.allocator.dupe(u8, path), .mtime = stat.mtime }; + const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(path); + (try self.pending.addOne(self.allocator)).* = .{ + .path = try self.allocator.dupe(u8, path), + .type = file_type, + .icon = file_icon, + .color = file_color, + .mtime = stat.mtime, + }; } else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.null_ })) { self.state.workspace_files = .done; try self.loaded(parent); diff --git a/src/file_type_config.zig b/src/file_type_config.zig index 008bb9d..944ea16 100644 --- a/src/file_type_config.zig +++ b/src/file_type_config.zig @@ -20,6 +20,8 @@ pub const default = struct { pub const color = 0x000000; }; +pub const folder_icon = ""; + fn from_file_type(file_type: syntax.FileType) @This() { return .{ .name = file_type.name, @@ -84,7 +86,7 @@ pub fn get(file_type_name: []const u8) !?@This() { break :file_type if (syntax.FileType.get_by_name_static(file_type_name)) |ft| from_file_type(ft) else null; } }; - try cache.put(cache_allocator, file_type_name, file_type); + try cache.put(cache_allocator, try cache_allocator.dupe(u8, file_type_name), file_type); break :self file_type; }; } diff --git a/src/log.zig b/src/log.zig index 71d3521..1beff73 100644 --- a/src/log.zig +++ b/src/log.zig @@ -11,6 +11,8 @@ subscriber: ?tp.pid, heap: [32 + 1024]u8, fba: std.heap.FixedBufferAllocator, msg_store: MsgStoreT, +no_stdout: bool = false, +no_stderr: bool = false, const MsgStoreT = std.DoublyLinkedList([]u8); const Receiver = tp.Receiver(*Self); @@ -79,12 +81,23 @@ fn store_reset(self: *Self) void { fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { errdefer self.deinit(); - if (try m.match(.{ "log", tp.more })) { + var output: []const u8 = undefined; + if (try m.match(.{ "log", "error", tp.string, "std.log", "->", tp.extract(&output) })) { if (self.subscriber) |subscriber| { subscriber.send_raw(m) catch {}; } else { self.store(m); } + if (!self.no_stderr) + std.io.getStdErr().writer().print("{s}\n", .{output}) catch {}; + } else if (try m.match(.{ "log", tp.string, tp.extract(&output) })) { + if (self.subscriber) |subscriber| { + subscriber.send_raw(m) catch {}; + } else { + self.store(m); + } + if (!self.no_stdout) + std.io.getStdOut().writer().print("{s}\n", .{output}) catch {}; } else if (try m.match(.{"subscribe"})) { // log("subscribed"); if (self.subscriber) |*s| s.deinit(); @@ -95,6 +108,14 @@ fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { if (self.subscriber) |*s| s.deinit(); self.subscriber = null; self.store_reset(); + } else if (try m.match(.{ "stdout", "enable" })) { + self.no_stdout = false; + } else if (try m.match(.{ "stdout", "disable" })) { + self.no_stdout = true; + } else if (try m.match(.{ "stderr", "enable" })) { + self.no_stderr = false; + } else if (try m.match(.{ "stderr", "disable" })) { + self.no_stderr = true; } else if (try m.match(.{"shutdown"})) { return tp.exit_normal(); } @@ -202,6 +223,14 @@ pub fn unsubscribe() tp.result { return tp.env.get().proc("log").send(.{"unsubscribe"}); } +pub fn stdout(state: enum { enable, disable }) void { + tp.env.get().proc("log").send(.{ "stdout", state }) catch {}; +} + +pub fn stderr(state: enum { enable, disable }) void { + tp.env.get().proc("log").send(.{ "stderr", state }) catch {}; +} + var std_log_pid: ?tp.pid_ref = null; pub fn set_std_log_pid(pid: ?tp.pid_ref) void { diff --git a/src/main.zig b/src/main.zig index f337409..f36f642 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ 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"); pub const file_link = @import("file_link.zig"); @@ -398,7 +399,7 @@ fn trace_to_file(m: thespian.message.c_buffer_type) callconv(.C) void { const a = std.heap.c_allocator; var path = std.ArrayList(u8).init(a); defer path.deinit(); - path.writer().print("{s}/trace.log", .{get_state_dir() catch return}) catch return; + path.writer().print("{s}{c}trace.log", .{ get_state_dir() catch return, sep }) catch return; const file = std.fs.createFileAbsolute(path.items, .{ .truncate = true }) catch return; State.state = .{ .file = file, @@ -502,12 +503,12 @@ pub fn parse_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, b lineno += 1; if (line.len == 0 or line[0] == '#') continue; - const sep = std.mem.indexOfScalar(u8, line, ' ') orelse { + const spc = std.mem.indexOfScalar(u8, line, ' ') orelse { std.log.err("{s}:{}: {s} missing value", .{ file_name, lineno, line }); continue; }; - const name = line[0..sep]; - const value_str = line[sep + 1 ..]; + 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; @@ -781,6 +782,11 @@ pub const ConfigDirError = error{ 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 { @@ -791,22 +797,22 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { 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}/{s}", .{ xdg, appname }); + 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}/.config", .{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 error.MakeHomeConfigDirFailed, + else => return make_dir_error(dir, error.MakeHomeConfigDirFailed), }; - break :ret try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname }); + 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}/{s}", .{ appdata, appname }); + 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 error.MakeAppConfigDirFailed, + else => return make_dir_error(dir, error.MakeAppConfigDirFailed), }; break :ret dir; } else return error.AppConfigDirUnavailable; @@ -815,14 +821,14 @@ fn get_app_config_dir(appname: []const u8) ConfigDirError![]const u8 { local.config_dir = config_dir; std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return error.MakeConfigDirFailed, + 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}/{s}", .{ config_dir, keybind_dir })) catch {}; + 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}/{s}", .{ config_dir, theme_dir })) catch {}; + std.fs.makeDirAbsolute(try std.fmt.bufPrint(&theme_dir_buffer, "{s}{c}{s}", .{ config_dir, sep, theme_dir })) catch {}; return config_dir; } @@ -841,22 +847,22 @@ fn get_app_cache_dir(appname: []const u8) ![]const u8 { 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}/{s}", .{ xdg, appname }); + 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}/.cache", .{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 e, + else => return make_dir_error(dir, e), }; - break :ret try std.fmt.bufPrint(&local.cache_dir_buffer, "{s}/.cache/{s}", .{ home, appname }); + 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}/{s}", .{ appdata, appname }); + 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 e, + else => return make_dir_error(dir, e), }; break :ret dir; } else return error.AppCacheDirUnavailable; @@ -865,7 +871,7 @@ fn get_app_cache_dir(appname: []const u8) ![]const u8 { local.cache_dir = cache_dir; std.fs.makeDirAbsolute(cache_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(cache_dir, e), }; return cache_dir; } @@ -884,27 +890,27 @@ fn get_app_state_dir(appname: []const u8) ![]const u8 { 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}/{s}", .{ xdg, appname }); + 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}/.local", .{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 e, + else => return make_dir_error(dir, e), }; - dir = try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/.local/state", .{home}); + 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 e, + else => return make_dir_error(dir, e), }; - break :ret try std.fmt.bufPrint(&local.state_dir_buffer, "{s}/.local/state/{s}", .{ home, appname }); + 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}/{s}", .{ appdata, appname }); + 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 e, + else => return make_dir_error(dir, e), }; break :ret dir; } else return error.AppCacheDirUnavailable; @@ -913,7 +919,7 @@ fn get_app_state_dir(appname: []const u8) ![]const u8 { local.state_dir = state_dir; std.fs.makeDirAbsolute(state_dir) catch |e| switch (e) { error.PathAlreadyExists => {}, - else => return e, + else => return make_dir_error(state_dir, e), }; return state_dir; } @@ -926,7 +932,7 @@ fn get_app_config_dir_file_name(appname: []const u8, comptime config_file_name: const local = struct { var config_file_buffer: [std.posix.PATH_MAX]u8 = undefined; }; - return std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name }); + 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 { @@ -942,7 +948,7 @@ pub fn get_restore_file_name() ![]const u8 { const restore_file = if (local.restore_file) |file| file else - try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_state_dir(application_name), restore_file_name }); + 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; } @@ -958,7 +964,7 @@ fn get_keybind_namespaces_directory() ![]const u8 { defer a.free(dir); return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); } - return try std.fmt.bufPrint(&local.dir_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), keybind_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 { @@ -966,7 +972,7 @@ pub fn get_keybind_namespace_file_name(namespace_name: []const u8) ![]const u8 { const local = struct { var file_buffer: [std.posix.PATH_MAX]u8 = undefined; }; - return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, namespace_name }); + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, namespace_name }); } const theme_dir = "themes"; @@ -980,7 +986,7 @@ fn get_theme_directory() ![]const u8 { defer a.free(dir); return try std.fmt.bufPrint(&local.dir_buffer, "{s}", .{dir}); } - return try std.fmt.bufPrint(&local.dir_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), theme_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 { @@ -988,7 +994,7 @@ pub fn get_theme_file_name(theme_name: []const u8) ![]const u8 { const local = struct { var file_buffer: [std.posix.PATH_MAX]u8 = undefined; }; - return try std.fmt.bufPrint(&local.file_buffer, "{s}/{s}.json", .{ dir, theme_name }); + return try std.fmt.bufPrint(&local.file_buffer, "{s}{c}{s}.json", .{ dir, sep, theme_name }); } fn restart() noreturn { diff --git a/src/project_manager.zig b/src/project_manager.zig index c29a78a..1c4d6e5 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -793,12 +793,19 @@ fn request_path_files_async(a_: std.mem.Allocator, parent_: tp.pid_ref, project_ var iter = self.dir.iterateAssumeFirstIteration(); errdefer |e| self.parent.send(.{ "PRJ", "path_error", self.project_name, self.path, e }) catch {}; while (try iter.next()) |entry| { - switch (entry.kind) { - .directory => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "DIR", entry.name }), - .sym_link => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "LINK", entry.name }), - .file => try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, "FILE", entry.name }), + const event_type = switch (entry.kind) { + .directory => "DIR", + .sym_link => "LINK", + .file => "FILE", else => continue, - } + }; + const default = file_type_config.default; + const file_type, const icon, const color = switch (entry.kind) { + .directory => .{ "directory", file_type_config.folder_icon, default.color }, + .sym_link, .file => Project.guess_path_file_type(self.path, entry.name), + else => .{ default.name, default.icon, default.color }, + }; + try self.parent.send(.{ "PRJ", "path_entry", self.project_name, self.path, event_type, entry.name, file_type, icon, color }); count += 1; if (count >= self.max) break; } diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index ce8c911..2ac5a5f 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -1315,7 +1315,7 @@ pub fn write_restore_info(self: *Self) void { fn read_restore_info(self: *Self) !void { const file_name = try root.get_restore_file_name(); - const file = try std.fs.cwd().openFile(file_name, .{ .mode = .read_only }); + const file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); defer file.close(); const stat = try file.stat(); var buf = try self.allocator.alloc(u8, @intCast(stat.size)); diff --git a/src/tui/mode/mini/file_browser.zig b/src/tui/mode/mini/file_browser.zig index c47d088..37747d1 100644 --- a/src/tui/mode/mini/file_browser.zig +++ b/src/tui/mode/mini/file_browser.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); +const file_type_config = @import("file_type_config"); const root = @import("root"); const input = @import("input"); @@ -20,6 +21,7 @@ pub fn Create(options: type) type { return struct { allocator: std.mem.Allocator, file_path: std.ArrayList(u8), + rendered_mini_buffer: std.ArrayListUnmanaged(u8) = .empty, query: std.ArrayList(u8), match: std.ArrayList(u8), entries: std.ArrayList(Entry), @@ -33,8 +35,12 @@ pub fn Create(options: type) type { const Entry = struct { name: []const u8, - type: enum { dir, file, link }, + type: EntryType, + file_type: []const u8, + icon: []const u8, + color: u24, }; + const EntryType = enum { dir, file, link }; pub fn create(allocator: std.mem.Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { const self = try allocator.create(Self); @@ -66,6 +72,7 @@ pub fn Create(options: type) type { self.match.deinit(); self.query.deinit(); self.file_path.deinit(); + self.rendered_mini_buffer.deinit(self.allocator); self.allocator.destroy(self); } @@ -80,7 +87,11 @@ pub fn Create(options: type) type { } fn clear_entries(self: *Self) void { - for (self.entries.items) |entry| self.allocator.free(entry.name); + for (self.entries.items) |entry| { + self.allocator.free(entry.name); + self.allocator.free(entry.file_type); + self.allocator.free(entry.icon); + } self.entries.clearRetainingCapacity(); } @@ -112,14 +123,11 @@ pub fn Create(options: type) type { self.complete_trigger_count = 0; self.file_path.clearRetainingCapacity(); if (self.match.items.len > 0) { - try self.construct_path(self.query.items, .{ .name = self.match.items, .type = .file }, 0); + try self.construct_path(self.query.items, self.match.items, .file, 0); } else { try self.file_path.appendSlice(self.query.items); } - if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); - } + self.update_mini_mode_text(); return; } self.complete_trigger_count -= 1; @@ -139,12 +147,7 @@ pub fn Create(options: type) type { } fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void { - defer { - if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); - } - } + defer self.update_mini_mode_text(); var count: usize = undefined; if (try cbor.match(m.buf, .{ "PRJ", "path_entry", tp.more })) { return self.process_path_entry(m); @@ -158,18 +161,31 @@ pub fn Create(options: type) type { fn process_path_entry(self: *Self, m: tp.message) MessageFilter.Error!void { var path: []const u8 = undefined; var file_name: []const u8 = undefined; - if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "DIR", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .dir }; - } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "LINK", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .link }; - } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "FILE", tp.extract(&file_name) })) { - (try self.entries.addOne()).* = .{ .name = try self.allocator.dupe(u8, file_name), .type = .file }; + var file_type: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "DIR", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .dir, file_type, icon, color); + } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "LINK", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .link, file_type, icon, color); + } else if (try cbor.match(m.buf, .{ tp.any, tp.any, tp.any, tp.extract(&path), "FILE", tp.extract(&file_name), tp.extract(&file_type), tp.extract(&icon), tp.extract(&color) })) { + try self.add_entry(file_name, .file, file_type, icon, color); } else { log.logger("file_browser").err("receive", tp.unexpected(m)); } tui.need_render(); } + fn add_entry(self: *Self, file_name: []const u8, entry_type: EntryType, file_type: []const u8, icon: []const u8, color: u24) !void { + (try self.entries.addOne()).* = .{ + .name = try self.allocator.dupe(u8, file_name), + .type = entry_type, + .file_type = try self.allocator.dupe(u8, file_type), + .icon = try self.allocator.dupe(u8, icon), + .color = color, + }; + } + fn do_complete(self: *Self) !void { self.complete_trigger_count = @min(self.complete_trigger_count, self.entries.items.len); self.file_path.clearRetainingCapacity(); @@ -179,9 +195,10 @@ pub fn Create(options: type) type { if (self.total_matches == 1) self.complete_trigger_count = 0; } else if (self.entries.items.len > 0) { - try self.construct_path(self.query.items, self.entries.items[self.complete_trigger_count - 1], self.complete_trigger_count - 1); + const entry = self.entries.items[self.complete_trigger_count - 1]; + try self.construct_path(self.query.items, entry.name, entry.type, self.complete_trigger_count - 1); } else { - try self.construct_path(self.query.items, .{ .name = "", .type = .file }, 0); + try self.construct_path(self.query.items, "", .file, 0); } if (self.match.items.len > 0) if (self.total_matches > 1) @@ -192,14 +209,14 @@ pub fn Create(options: type) type { message("{d}/{d}", .{ self.matched_entry + 1, self.entries.items.len }); } - fn construct_path(self: *Self, path_: []const u8, entry: Entry, entry_no: usize) error{OutOfMemory}!void { + fn construct_path(self: *Self, path_: []const u8, entry_name: []const u8, entry_type: EntryType, entry_no: usize) error{OutOfMemory}!void { self.matched_entry = entry_no; const path = project_manager.normalize_file_path(path_); try self.file_path.appendSlice(path); if (path.len > 0 and path[path.len - 1] != std.fs.path.sep) try self.file_path.append(std.fs.path.sep); - try self.file_path.appendSlice(entry.name); - if (entry.type == .dir) + try self.file_path.appendSlice(entry_name); + if (entry_type == .dir) try self.file_path.append(std.fs.path.sep); } @@ -212,7 +229,7 @@ pub fn Create(options: type) type { if (try prefix_compare_icase(self.allocator, self.match.items, entry.name)) { matched += 1; if (matched == self.complete_trigger_count) { - try self.construct_path(self.query.items, entry, i); + try self.construct_path(self.query.items, entry.name, entry.type, i); found_match = i; } last = entry; @@ -222,11 +239,11 @@ pub fn Create(options: type) type { self.total_matches = matched; if (found_match) |_| return; if (last) |entry| { - try self.construct_path(self.query.items, entry, last_no); + try self.construct_path(self.query.items, entry.name, entry.type, last_no); self.complete_trigger_count = matched; } else { message("no match for '{s}'", .{self.match.items}); - try self.construct_path(self.query.items, .{ .name = self.match.items, .type = .file }, 0); + try self.construct_path(self.query.items, self.match.items, .file, 0); } } @@ -264,8 +281,15 @@ pub fn Create(options: type) type { fn update_mini_mode_text(self: *Self) void { if (tui.mini_mode()) |mini_mode| { - mini_mode.text = self.file_path.items; - mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1); + const icon = if (self.entries.items.len > 0 and self.complete_trigger_count > 0) + self.entries.items[self.complete_trigger_count - 1].icon + else + " "; + self.rendered_mini_buffer.clearRetainingCapacity(); + const writer = self.rendered_mini_buffer.writer(self.allocator); + writer.print("{s} {s}", .{ icon, self.file_path.items }) catch {}; + mini_mode.text = self.rendered_mini_buffer.items; + mini_mode.cursor = tui.egc_chunk_width(self.file_path.items, 0, 1) + 3; } } diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index b3c05dc..d284a84 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tp = @import("thespian"); const log = @import("log"); const cbor = @import("cbor"); +const file_type_config = @import("file_type_config"); const root = @import("root"); const Plane = @import("renderer").Plane; @@ -111,13 +112,25 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: button.plane.home(); } var file_path: []const u8 = undefined; + var file_type: []const u8 = undefined; + var file_icon: []const u8 = undefined; + var file_color: u24 = undefined; var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes - if (!(cbor.matchString(&iter, &file_path) catch false)) - file_path = "#ERROR#"; + if (!(cbor.matchString(&iter, &file_path) catch false)) file_path = "#ERROR#"; + if (!(cbor.matchString(&iter, &file_type) catch false)) file_type = file_type_config.default.name; + if (!(cbor.matchString(&iter, &file_icon) catch false)) file_icon = file_type_config.default.icon; + if (!(cbor.matchInt(u24, &iter, &file_color) catch false)) file_icon = file_type_config.default.icon; + button.plane.set_style(style_keybind); const dirty = if (self.buffer_manager) |bm| if (bm.is_buffer_dirty(file_path)) "" else " " else " "; const pointer = if (selected) "⏵" else dirty; _ = button.plane.print("{s}", .{pointer}) catch {}; + + if (tui.config().show_fileicons) { + tui.render_file_icon(&button.plane, file_icon, file_color); + _ = button.plane.print(" ", .{}) catch {}; + } + var buf: [std.fs.max_path_bytes]u8 = undefined; var removed_prefix: usize = 0; const max_len = max_menu_width() - 2; @@ -125,25 +138,17 @@ fn on_render_menu(self: *Self, button: *Button.State(*Menu.State(*Self)), theme: _ = button.plane.print("{s} ", .{ if (file_path.len > max_len) root.shorten_path(&buf, file_path, &removed_prefix, max_len) else file_path, }) catch {}; + var index: usize = 0; var len = cbor.decodeArrayHeader(&iter) catch return false; while (len > 0) : (len -= 1) { if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - const cell_idx = if (index < removed_prefix) 1 else index + 1 - removed_prefix; - render_cell(&button.plane, 0, cell_idx, theme.editor_match) catch break; + tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; } else break; } return false; } -fn render_cell(plane: *Plane, y: usize, x: usize, style: Widget.Theme.Style) !void { - plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; - var cell = plane.cell_init(); - _ = plane.at_cursor_cell(&cell) catch return; - cell.set_style(style); - _ = plane.putc(&cell) catch {}; -} - fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); } @@ -156,11 +161,21 @@ fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu. tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); } -fn add_item(self: *Self, file_name: []const u8, matches: ?[]const u8) !void { +fn add_item( + self: *Self, + file_name: []const u8, + file_type: []const u8, + file_icon: []const u8, + file_color: u24, + matches: ?[]const u8, +) !void { var label = std.ArrayList(u8).init(self.allocator); defer label.deinit(); const writer = label.writer(); try cbor.writeValue(writer, file_name); + try cbor.writeValue(writer, file_type); + try cbor.writeValue(writer, file_icon); + try cbor.writeValue(writer, file_color); if (matches) |cb| _ = try writer.write(cb); try self.menu.add_item_with_handler(label.items, menu_action_open_file); } @@ -175,20 +190,40 @@ fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) MessageFil fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void { var file_name: []const u8 = undefined; + var file_type: []const u8 = undefined; + var file_icon: []const u8 = undefined; + var file_color: u24 = undefined; var matches: []const u8 = undefined; var query: []const u8 = undefined; - if (try cbor.match(m.buf, .{ "PRJ", "recent", tp.extract(&self.longest), tp.extract(&file_name), tp.extract_cbor(&matches) })) { + if (try cbor.match(m.buf, .{ + "PRJ", + "recent", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + tp.extract_cbor(&matches), + })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, matches); + try self.add_item(file_name, file_type, file_icon, file_color, matches); self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); if (self.need_select_first) { self.menu.select_down(); self.need_select_first = false; } tui.need_render(); - } else if (try cbor.match(m.buf, .{ "PRJ", "recent", tp.extract(&self.longest), tp.extract(&file_name) })) { + } else if (try cbor.match(m.buf, .{ + "PRJ", + "recent", + tp.extract(&self.longest), + tp.extract(&file_name), + tp.extract(&file_type), + tp.extract(&file_icon), + tp.extract(&file_color), + })) { if (self.need_reset) self.reset_results(); - try self.add_item(file_name, null); + try self.add_item(file_name, file_type, file_icon, file_color, null); self.menu.resize(.{ .y = 0, .x = self.menu_pos_x(), .w = self.menu_width() }); if (self.need_select_first) { self.menu.select_down(); diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 5aa13f6..3a3e7c8 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -103,6 +103,8 @@ const InitError = error{ keybind.LoadError; fn init(allocator: Allocator) InitError!*Self { + log.stdout(.disable); + var conf, const conf_bufs = root.read_config(@import("config"), allocator); if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) @@ -157,6 +159,8 @@ fn init(allocator: Allocator) InitError!*Self { self.rdr_.dispatch_event = dispatch_event; try self.rdr_.run(); + log.stderr(.disable); + try project_manager.start(); try frame_clock.start();