├── .gitattributes ├── .gitignore ├── src ├── win32 │ ├── flow.ico │ ├── xy.zig │ ├── flow.manifest │ ├── win32ext.zig │ ├── flow.rc │ ├── FontFace.zig │ ├── builtin.hlsl │ ├── xterm.zig │ └── DwriteRenderer.zig ├── gui_config.zig ├── tracy_noop.zig ├── renderer │ └── vaxis │ │ ├── style.zig │ │ ├── GraphemeCache.zig │ │ ├── Layer.zig │ │ └── Cell.zig ├── tui │ ├── mode │ │ ├── mini │ │ │ ├── underline.zig │ │ │ ├── match.zig │ │ │ ├── tab_width.zig │ │ │ ├── save_as.zig │ │ │ ├── goto_offset.zig │ │ │ ├── open_file.zig │ │ │ ├── move_to_char.zig │ │ │ ├── replace.zig │ │ │ ├── goto.zig │ │ │ ├── get_char.zig │ │ │ ├── buffer.zig │ │ │ └── find_in_files.zig │ │ ├── overlay │ │ │ ├── fontface_palette.zig │ │ │ ├── buffer_palette.zig │ │ │ ├── open_recent_project.zig │ │ │ ├── list_all_commands_palette.zig │ │ │ ├── theme_palette.zig │ │ │ ├── clipboard_palette.zig │ │ │ └── command_palette.zig │ │ └── vim.zig │ ├── status │ │ ├── bar.zig │ │ ├── keybindstate.zig │ │ ├── blank.zig │ │ ├── widget.zig │ │ ├── diagstate.zig │ │ ├── modstate.zig │ │ ├── selectionstate.zig │ │ ├── clock.zig │ │ ├── modestate.zig │ │ └── linenumstate.zig │ ├── Box.zig │ ├── info_view.zig │ ├── WidgetStack.zig │ ├── Fire.zig │ ├── inputview.zig │ ├── keybindview.zig │ └── MessageFilter.zig ├── VcsStatus.zig ├── service_template.zig ├── completion.zig ├── bin_path.zig ├── lsp_config.zig ├── lsp_types.zig ├── keybind │ ├── parse_flow.zig │ └── builtin │ │ └── emacs.json ├── text_manip.zig ├── file_link.zig ├── color.zig ├── buffer │ ├── View.zig │ └── Selection.zig ├── list_languages.zig └── config.zig ├── contrib ├── icons │ └── 192x192 │ │ └── flow-control.png ├── fortune ├── flow-control.desktop ├── test_race.sh ├── make_release └── make_nightly_build ├── test ├── tests.zig ├── tests_color.zig └── tests_project_manager.zig └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache/ 2 | /zig-out/ 3 | /.zig-cache/ 4 | -------------------------------------------------------------------------------- /src/win32/flow.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurocyte/flow/HEAD/src/win32/flow.ico -------------------------------------------------------------------------------- /contrib/icons/192x192/flow-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neurocyte/flow/HEAD/contrib/icons/192x192/flow-control.png -------------------------------------------------------------------------------- /src/gui_config.zig: -------------------------------------------------------------------------------- 1 | fontface: []const u8 = "Cascadia Code", 2 | fontsize: u8 = 14, 3 | 4 | initial_window_x: u16 = 1087, 5 | initial_window_y: u16 = 1014, 6 | 7 | include_files: []const u8 = "", 8 | -------------------------------------------------------------------------------- /src/tracy_noop.zig: -------------------------------------------------------------------------------- 1 | pub fn initZone(_: anytype, _: anytype) Zone { 2 | return .{}; 3 | } 4 | 5 | pub const Zone = struct { 6 | pub fn deinit(_: @This()) void {} 7 | }; 8 | 9 | pub fn frameMark() void {} 10 | -------------------------------------------------------------------------------- /contrib/fortune: -------------------------------------------------------------------------------- 1 | A ceditpede was happy, quite! 2 | Until a nerd, in fun, 3 | Said "Pray which app loads after which?" 4 | This raised her clocks to such a pitch, 5 | She crashed and tripped the breaker switch, 6 | Not knowing how to run. 7 | -- "The ceditpede", Eleanor Bartle 8 | % 9 | -------------------------------------------------------------------------------- /contrib/flow-control.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Flow Control 3 | Comment=A programmer's text editor. 4 | GenericName=Text Editor 5 | Type=Application 6 | Keywords=editor;flow 7 | Exec=flow %F 8 | Icon=flow-control 9 | Type=Application 10 | Categories=TextEditor;Development;IDE; 11 | StartupNotify=false 12 | Terminal=true 13 | -------------------------------------------------------------------------------- /test/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | pub const buffer = @import("tests_buffer.zig"); 3 | pub const color = @import("tests_color.zig"); 4 | pub const helix = @import("tests_helix.zig"); 5 | pub const project_manager = @import("tests_project_manager.zig"); 6 | 7 | test { 8 | std.testing.refAllDecls(@This()); 9 | } 10 | -------------------------------------------------------------------------------- /src/win32/xy.zig: -------------------------------------------------------------------------------- 1 | pub fn XY(comptime T: type) type { 2 | return struct { 3 | x: T, 4 | y: T, 5 | pub fn init(x: T, y: T) @This() { 6 | return .{ .x = x, .y = y }; 7 | } 8 | 9 | const Self = @This(); 10 | pub fn eql(self: Self, other: Self) bool { 11 | return self.x == other.x and self.y == other.y; 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/vaxis/style.zig: -------------------------------------------------------------------------------- 1 | pub const StyleBits = packed struct(u5) { 2 | struck: bool = false, 3 | bold: bool = false, 4 | undercurl: bool = false, 5 | underline: bool = false, 6 | italic: bool = false, 7 | }; 8 | 9 | pub const struck: StyleBits = .{ .struck = true }; 10 | pub const bold: StyleBits = .{ .bold = true }; 11 | pub const undercurl: StyleBits = .{ .undercurl = true }; 12 | pub const underline: StyleBits = .{ .underline = true }; 13 | pub const italic: StyleBits = .{ .italic = true }; 14 | pub const normal: StyleBits = .{}; 15 | -------------------------------------------------------------------------------- /contrib/test_race.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "$1" == "--build" ]; then 5 | shift 6 | echo "building..." 7 | zig build -freference-trace --prominent-compile-errors 8 | fi 9 | 10 | for i in {1..60}; do 11 | echo "running $i ..." 12 | # flow --exec quit "$@" || exit 1 13 | strace -f -t \ 14 | -e trace=open,openat,close,socket,pipe,pipe2,dup,dup2,dup3,fcntl,accept,accept4,epoll_create,epoll_create1,eventfd,timerfd_create,signalfd,execve,fork \ 15 | -o trace.log \ 16 | flow --exec quit --trace-level 9 "$@" 17 | done 18 | -------------------------------------------------------------------------------- /src/tui/mode/mini/underline.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const command = @import("command"); 4 | 5 | const tui = @import("../../tui.zig"); 6 | 7 | pub const Type = @import("get_char.zig").Create(@This()); 8 | pub const create = Type.create; 9 | 10 | pub fn name(_: *Type) []const u8 { 11 | return "underline"; 12 | } 13 | 14 | pub fn process_egc(_: *Type, egc: []const u8) command.Result { 15 | try command.executeName("underline_with_char", command.fmt(.{ egc, "solid" })); 16 | try command.executeName("exit_mini_mode", .{}); 17 | } 18 | -------------------------------------------------------------------------------- /src/VcsStatus.zig: -------------------------------------------------------------------------------- 1 | branch: ?[]const u8 = null, 2 | ahead: ?[]const u8 = null, 3 | behind: ?[]const u8 = null, 4 | stash: ?[]const u8 = null, 5 | changed: usize = 0, 6 | untracked: usize = 0, 7 | 8 | pub fn reset(self: *@This(), allocator: std.mem.Allocator) void { 9 | if (self.branch) |p| allocator.free(p); 10 | if (self.ahead) |p| allocator.free(p); 11 | if (self.behind) |p| allocator.free(p); 12 | if (self.stash) |p| allocator.free(p); 13 | self.branch = null; 14 | self.ahead = null; 15 | self.behind = null; 16 | self.stash = null; 17 | self.changed = 0; 18 | self.untracked = 0; 19 | } 20 | 21 | const std = @import("std"); 22 | -------------------------------------------------------------------------------- /src/renderer/vaxis/GraphemeCache.zig: -------------------------------------------------------------------------------- 1 | storage: *Storage, 2 | 3 | pub const GraphemeCache = @This(); 4 | 5 | pub inline fn put(self: *@This(), bytes: []const u8) []u8 { 6 | return self.storage.put(bytes); 7 | } 8 | 9 | pub const Storage = struct { 10 | buf: [1024 * 512]u8 = undefined, 11 | idx: usize = 0, 12 | 13 | pub fn put(self: *@This(), bytes: []const u8) []u8 { 14 | if (self.idx + bytes.len > self.buf.len) self.idx = 0; 15 | defer self.idx += bytes.len; 16 | @memcpy(self.buf[self.idx .. self.idx + bytes.len], bytes); 17 | return self.buf[self.idx .. self.idx + bytes.len]; 18 | } 19 | 20 | pub fn cache(self: *@This()) GraphemeCache { 21 | return .{ .storage = self }; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/win32/flow.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true/pm 6 | PerMonitorV2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tui/mode/mini/match.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const command = @import("command"); 4 | const tp = @import("thespian"); 5 | const log = @import("log"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | 9 | pub const Type = @import("get_char.zig").Create(@This()); 10 | pub const create = Type.create; 11 | 12 | pub fn name(self: *Type) []const u8 { 13 | var suffix: []const u8 = ""; 14 | if ((self.ctx.args.match(.{tp.extract(&suffix)}) catch false)) { 15 | return suffix; 16 | } 17 | return "󰅪 match"; 18 | } 19 | 20 | pub fn process_egc(self: *Type, egc: []const u8) command.Result { 21 | var action: []const u8 = ""; 22 | if ((self.ctx.args.match(.{tp.extract(&action)}) catch false)) { 23 | try command.executeName(action, command.fmt(.{egc})); 24 | } 25 | try command.executeName("exit_mini_mode", .{}); 26 | } 27 | -------------------------------------------------------------------------------- /src/tui/mode/mini/tab_width.zig: -------------------------------------------------------------------------------- 1 | const cbor = @import("cbor"); 2 | const command = @import("command"); 3 | 4 | const tui = @import("../../tui.zig"); 5 | 6 | pub const Type = @import("numeric_input.zig").Create(@This()); 7 | pub const create = Type.create; 8 | 9 | pub fn name(_: *Type) []const u8 { 10 | return " tab size"; 11 | } 12 | 13 | pub fn start(self: *Type) usize { 14 | const tab_width = if (tui.get_active_editor()) |editor| editor.tab_width else tui.get_tab_width(); 15 | self.input = tab_width; 16 | return tab_width; 17 | } 18 | 19 | const default_cmd = "set_editor_tab_width"; 20 | 21 | pub const cancel = preview; 22 | 23 | pub fn preview(self: *Type, _: command.Context) void { 24 | command.executeName(default_cmd, command.fmt(.{self.input orelse self.start})) catch {}; 25 | } 26 | 27 | pub fn apply(self: *Type, ctx: command.Context) void { 28 | var cmd: []const u8 = undefined; 29 | if (!(ctx.args.match(.{cbor.extract(&cmd)}) catch false)) 30 | cmd = default_cmd; 31 | command.executeName(cmd, command.fmt(.{self.input orelse self.start})) catch {}; 32 | } 33 | -------------------------------------------------------------------------------- /src/win32/win32ext.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const win32 = @import("win32").everything; 3 | 4 | // TODO: update zigwin32 with a way to get the corresponding IID for any COM interface 5 | pub fn queryInterface(obj: anytype, comptime Interface: type) *Interface { 6 | const obj_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(@TypeOf(obj)), '.')) |i| (i + 1) else 0; 7 | const obj_basename = @typeName(@TypeOf(obj))[obj_basename_start..]; 8 | const iface_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(Interface), '.')) |i| (i + 1) else 0; 9 | const iface_basename = @typeName(Interface)[iface_basename_start..]; 10 | 11 | const iid_name = "IID_" ++ iface_basename; 12 | const iid = @field(win32, iid_name); 13 | 14 | var iface: *Interface = undefined; 15 | const hr = obj.IUnknown.QueryInterface(iid, @ptrCast(&iface)); 16 | if (hr < 0) std.debug.panic( 17 | "QueryInferface on " ++ obj_basename ++ " as " ++ iface_basename ++ " failed, hresult=0x{x}", 18 | .{@as(u32, @bitCast(hr))}, 19 | ); 20 | return iface; 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CJ van den Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/win32/flow.rc: -------------------------------------------------------------------------------- 1 | #define ID_ICON_FLOW 1 2 | 3 | // LANG_NEUTRAL(0), SUBLANG_NEUTRAL(0) 4 | LANGUAGE 0, 0 5 | 6 | ID_ICON_FLOW ICON "flow.ico" 7 | 8 | VS_VERSION_INFO VERSIONINFO 9 | //FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT 10 | //PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,VERSION_COMMIT_HEIGHT 11 | //FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 12 | //FILEFLAGS VER_DBG 13 | //FILEOS VOS_NT 14 | //FILETYPE VFT_APP 15 | //FILESUBTYPE VFT2_UNKNOWN 16 | BEGIN 17 | BLOCK "StringFileInfo" 18 | BEGIN 19 | BLOCK "040904b0" 20 | BEGIN 21 | //VALUE "CompanyName", "???" 22 | //VALUE "FileDescription", "???" 23 | //VALUE "FileVersion", VERSION 24 | //VALUE "LegalCopyright", "(C) 2024 ???" 25 | VALUE "OriginalFilename", "flow.exe" 26 | VALUE "ProductName", "Flow Control" 27 | //VALUE "ProductVersion", VERSION 28 | END 29 | END 30 | BLOCK "VarFileInfo" 31 | BEGIN 32 | VALUE "Translation", 0x0409,1200 33 | END 34 | END 35 | -------------------------------------------------------------------------------- /src/win32/FontFace.zig: -------------------------------------------------------------------------------- 1 | const FontFace = @This(); 2 | 3 | const std = @import("std"); 4 | 5 | // it seems that Windows only supports font faces with up to 31 characters, 6 | // but we use a larger buffer here because GetFamilyNames can apparently 7 | // return longer strings 8 | pub const max = 254; 9 | 10 | buf: [max + 1]u16, 11 | len: usize, 12 | 13 | pub fn initUtf8(utf8: []const u8) error{ TooLong, InvalidUtf8 }!FontFace { 14 | const utf16_len = std.unicode.calcUtf16LeLen(utf8) catch return error.InvalidUtf8; 15 | if (utf16_len > max) 16 | return error.TooLong; 17 | var self: FontFace = .{ .buf = undefined, .len = utf16_len }; 18 | const actual_len = try std.unicode.utf8ToUtf16Le(&self.buf, utf8); 19 | std.debug.assert(actual_len == utf16_len); 20 | self.buf[actual_len] = 0; 21 | return self; 22 | } 23 | 24 | pub fn ptr(self: *const FontFace) [*:0]const u16 { 25 | return self.slice().ptr; 26 | } 27 | pub fn slice(self: *const FontFace) [:0]const u16 { 28 | std.debug.assert(self.buf[self.len] == 0); 29 | return self.buf[0..self.len :0]; 30 | } 31 | pub fn eql(self: *const FontFace, other: *const FontFace) bool { 32 | return std.mem.eql(u16, self.slice(), other.slice()); 33 | } 34 | -------------------------------------------------------------------------------- /test/tests_color.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const color = @import("color"); 3 | 4 | const RGB = color.RGB; 5 | const rgb = RGB.from_u24; 6 | 7 | test "contrast white/yellow" { 8 | const a: u24 = 0xFFFFFF; // white 9 | const b: u24 = 0x00FFFF; // yellow 10 | const ratio = RGB.contrast(rgb(a), rgb(b)); 11 | try std.testing.expectApproxEqAbs(ratio, 1.25388109, 0.000001); 12 | } 13 | 14 | test "contrast white/blue" { 15 | const a: u24 = 0xFFFFFF; // white 16 | const b: u24 = 0x0000FF; // blue 17 | const ratio = RGB.contrast(rgb(a), rgb(b)); 18 | try std.testing.expectApproxEqAbs(ratio, 8.59247135, 0.000001); 19 | } 20 | 21 | test "contrast black/yellow" { 22 | const a: u24 = 0x000000; // black 23 | const b: u24 = 0x00FFFF; // yellow 24 | const ratio = RGB.contrast(rgb(a), rgb(b)); 25 | try std.testing.expectApproxEqAbs(ratio, 16.7479991, 0.000001); 26 | } 27 | 28 | test "contrast black/blue" { 29 | const a: u24 = 0x000000; // black 30 | const b: u24 = 0x0000FF; // blue 31 | const ratio = RGB.contrast(rgb(a), rgb(b)); 32 | try std.testing.expectApproxEqAbs(ratio, 2.444, 0.000001); 33 | } 34 | 35 | test "best contrast black/white to yellow" { 36 | const best = color.max_contrast(0x00FFFF, 0xFFFFFF, 0x000000); 37 | try std.testing.expectEqual(best, 0x000000); 38 | } 39 | 40 | test "best contrast black/white to blue" { 41 | const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000); 42 | try std.testing.expectEqual(best, 0xFFFFFF); 43 | } 44 | -------------------------------------------------------------------------------- /src/tui/mode/mini/save_as.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const root = @import("soft_root").root; 4 | const command = @import("command"); 5 | const project_manager = @import("project_manager"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | 9 | pub const Type = @import("file_browser.zig").Create(@This()); 10 | 11 | pub const create = Type.create; 12 | 13 | pub fn load_entries(self: *Type) !void { 14 | const editor = tui.get_active_editor() orelse return; 15 | try self.file_path.appendSlice(self.allocator, editor.file_path orelse ""); 16 | if (editor.get_primary().selection) |sel| ret: { 17 | const text = editor.get_selection(sel, self.allocator) catch break :ret; 18 | defer self.allocator.free(text); 19 | if (!(text.len > 2 and std.mem.eql(u8, text[0..2], ".."))) 20 | self.file_path.clearRetainingCapacity(); 21 | try self.file_path.appendSlice(self.allocator, text); 22 | } 23 | } 24 | 25 | pub fn name(_: *Type) []const u8 { 26 | return " save as"; 27 | } 28 | 29 | pub fn select(self: *Type) void { 30 | { 31 | var buf: std.ArrayList(u8) = .empty; 32 | defer buf.deinit(self.allocator); 33 | const file_path = project_manager.expand_home(self.allocator, &buf, self.file_path.items); 34 | if (root.is_directory(file_path)) return; 35 | if (file_path.len > 0) 36 | tp.self_pid().send(.{ "cmd", "save_file_as", .{file_path} }) catch {}; 37 | } 38 | command.executeName("exit_mini_mode", .{}) catch {}; 39 | } 40 | -------------------------------------------------------------------------------- /contrib/make_release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | DESTDIR="$(pwd)/release" 5 | BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" 6 | APPNAME="$(basename "$BASEDIR")" 7 | 8 | cd "$BASEDIR" 9 | 10 | if [ -e "$DESTDIR" ]; then 11 | echo directory \"release\" already exists 12 | exit 1 13 | fi 14 | 15 | echo running tests... 16 | 17 | zig build test 18 | 19 | echo building... 20 | 21 | zig build -Dall_targets --release --prefix "$DESTDIR/build" 22 | 23 | VERSION=$(/bin/cat "$DESTDIR/build/version") 24 | 25 | git archive --format=tar.gz --output="$DESTDIR/flow-$VERSION-source.tar.gz" HEAD 26 | git archive --format=zip --output="$DESTDIR/flow-$VERSION-source.zip" HEAD 27 | 28 | cd "$DESTDIR/build" 29 | 30 | TARGETS=$(/bin/ls) 31 | 32 | for target in $TARGETS; do 33 | if [ -d "$target" ]; then 34 | cd "$target" 35 | if [ "${target:0:8}" == "windows-" ]; then 36 | echo packing zip "$target"... 37 | zip -r "../../${APPNAME}-${VERSION}-${target}.zip" ./* 38 | cd .. 39 | else 40 | echo packing tar "$target"... 41 | tar -czf "../../${APPNAME}-${VERSION}-${target}.tar.gz" -- * 42 | cd .. 43 | fi 44 | fi 45 | done 46 | 47 | cd .. 48 | rm -r build 49 | 50 | TARFILES=$(/bin/ls) 51 | 52 | for tarfile in $TARFILES; do 53 | echo signing "$tarfile"... 54 | gpg --local-user 4E6CF7234FFC4E14531074F98EB1E1BB660E3FB9 --detach-sig "$tarfile" 55 | sha256sum -b "$tarfile" >"${tarfile}.sha256" 56 | done 57 | 58 | echo "done making release $VERSION @ $DESTDIR" 59 | echo 60 | 61 | /bin/ls -lah 62 | -------------------------------------------------------------------------------- /src/tui/status/bar.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const EventHandler = @import("EventHandler"); 3 | 4 | const status_widget = @import("widget.zig"); 5 | const Widget = @import("../Widget.zig"); 6 | const WidgetList = @import("../WidgetList.zig"); 7 | const Plane = @import("renderer").Plane; 8 | 9 | pub const Style = enum { none, grip }; 10 | 11 | pub fn create(allocator: std.mem.Allocator, parent: Plane, config: []const u8, style: Style, event_handler: ?EventHandler) error{OutOfMemory}!Widget { 12 | var w = try WidgetList.createH(allocator, parent, "statusbar", .{ .static = 1 }); 13 | if (style == .grip) w.after_render = render_grip; 14 | w.ctx = w; 15 | w.on_layout = on_layout; 16 | var it = std.mem.splitScalar(u8, config, ' '); 17 | while (it.next()) |widget_name| { 18 | try w.add(status_widget.create(widget_name, allocator, w.plane, event_handler) catch |e| switch (e) { 19 | error.OutOfMemory => return error.OutOfMemory, 20 | error.WidgetInitFailed => null, 21 | } orelse continue); 22 | } 23 | return w.widget(); 24 | } 25 | 26 | fn on_layout(_: ?*anyopaque, w: *WidgetList) Widget.Layout { 27 | return if (w.layout_empty) 28 | .{ .static = 0 } 29 | else 30 | .{ .static = 1 }; 31 | } 32 | 33 | fn render_grip(ctx: ?*anyopaque, theme: *const Widget.Theme) void { 34 | const w: *WidgetList = @ptrCast(@alignCast(ctx.?)); 35 | if (w.hover()) { 36 | w.plane.set_style(theme.statusbar_hover); 37 | w.plane.cursor_move_yx(0, 0) catch {}; 38 | _ = w.plane.putstr("  ") catch {}; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/tui/mode/mini/goto_offset.zig: -------------------------------------------------------------------------------- 1 | const fmt = @import("std").fmt; 2 | const command = @import("command"); 3 | 4 | const tui = @import("../../tui.zig"); 5 | const Cursor = @import("../../editor.zig").Cursor; 6 | 7 | pub const Type = @import("numeric_input.zig").Create(@This()); 8 | pub const create = Type.create; 9 | 10 | pub const ValueType = struct { 11 | cursor: Cursor = .{}, 12 | offset: usize = 0, 13 | }; 14 | 15 | pub fn name(_: *Type) []const u8 { 16 | return "#goto byte"; 17 | } 18 | 19 | pub fn start(_: *Type) ValueType { 20 | const editor = tui.get_active_editor() orelse return .{}; 21 | return .{ .cursor = editor.get_primary().cursor }; 22 | } 23 | 24 | pub fn process_digit(self: *Type, digit: u8) void { 25 | switch (digit) { 26 | 0...9 => { 27 | if (self.input) |*input| { 28 | input.offset = input.offset * 10 + digit; 29 | } else { 30 | self.input = .{ .offset = digit }; 31 | } 32 | }, 33 | else => unreachable, 34 | } 35 | } 36 | 37 | pub fn delete(self: *Type, input: *ValueType) void { 38 | const newval = if (input.offset < 10) 0 else input.offset / 10; 39 | if (newval == 0) self.input = null else input.offset = newval; 40 | } 41 | 42 | pub fn format_value(_: *Type, input_: ?ValueType, buf: []u8) []const u8 { 43 | return if (input_) |input| 44 | fmt.bufPrint(buf, "{d}", .{input.offset}) catch "" 45 | else 46 | ""; 47 | } 48 | 49 | pub const preview = goto; 50 | pub const apply = goto; 51 | pub const cancel = goto; 52 | 53 | fn goto(self: *Type, _: command.Context) void { 54 | if (self.input) |input| 55 | command.executeName("goto_byte_offset", command.fmt(.{input.offset})) catch {} 56 | else 57 | command.executeName("goto_line_and_column", command.fmt(.{ self.start.cursor.row, self.start.cursor.col })) catch {}; 58 | } 59 | -------------------------------------------------------------------------------- /src/tui/status/keybindstate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const EventHandler = @import("EventHandler"); 4 | const Plane = @import("renderer").Plane; 5 | const keybind = @import("keybind"); 6 | 7 | const Widget = @import("../Widget.zig"); 8 | 9 | allocator: std.mem.Allocator, 10 | plane: Plane, 11 | 12 | const Self = @This(); 13 | 14 | pub fn create(allocator: std.mem.Allocator, parent: Plane, _: ?EventHandler, _: ?[]const u8) @import("widget.zig").CreateError!Widget { 15 | const self = try allocator.create(Self); 16 | errdefer allocator.destroy(self); 17 | self.* = .{ 18 | .allocator = allocator, 19 | .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), 20 | }; 21 | return Widget.to(self); 22 | } 23 | 24 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 25 | self.plane.deinit(); 26 | allocator.destroy(self); 27 | } 28 | 29 | pub fn layout(_: *Self) Widget.Layout { 30 | var buf: [256]u8 = undefined; 31 | var fbs = std.io.fixedBufferStream(&buf); 32 | const writer = fbs.writer(); 33 | writer.print(" ", .{}) catch {}; 34 | if (keybind.current_integer_argument()) |integer_argument| 35 | writer.print("{}", .{integer_argument}) catch {}; 36 | writer.print("{f} ", .{keybind.current_key_event_sequence_fmt()}) catch {}; 37 | const len = fbs.getWritten().len; 38 | return .{ .static = if (len > 0) len else 0 }; 39 | } 40 | 41 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 42 | self.plane.set_base_style(theme.editor); 43 | self.plane.erase(); 44 | self.plane.home(); 45 | self.plane.set_style(theme.statusbar); 46 | self.plane.fill(" "); 47 | self.plane.home(); 48 | _ = self.plane.print(" ", .{}) catch {}; 49 | if (keybind.current_integer_argument()) |integer_argument| 50 | _ = self.plane.print("{}", .{integer_argument}) catch {}; 51 | _ = self.plane.print("{f} ", .{keybind.current_key_event_sequence_fmt()}) catch {}; 52 | return false; 53 | } 54 | -------------------------------------------------------------------------------- /src/tui/mode/mini/open_file.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const root = @import("soft_root").root; 4 | const command = @import("command"); 5 | const project_manager = @import("project_manager"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | 9 | pub const Type = @import("file_browser.zig").Create(@This()); 10 | 11 | pub const create = Type.create; 12 | 13 | pub fn load_entries(self: *Type) error{ Exit, OutOfMemory }!void { 14 | var project_name_buf: [512]u8 = undefined; 15 | const project_path = tp.env.get().str("project"); 16 | const project_name = project_manager.abbreviate_home(&project_name_buf, project_path); 17 | try self.file_path.appendSlice(self.allocator, project_name); 18 | try self.file_path.append(self.allocator, std.fs.path.sep); 19 | const editor = tui.get_active_editor() orelse return; 20 | if (editor.file_path) |old_path| 21 | if (std.mem.lastIndexOf(u8, old_path, "/")) |pos| 22 | try self.file_path.appendSlice(self.allocator, old_path[0 .. pos + 1]); 23 | if (editor.get_primary().selection) |sel| ret: { 24 | const text = editor.get_selection(sel, self.allocator) catch break :ret; 25 | defer self.allocator.free(text); 26 | if (!(text.len > 2 and std.mem.eql(u8, text[0..2], ".."))) 27 | self.file_path.clearRetainingCapacity(); 28 | try self.file_path.appendSlice(self.allocator, text); 29 | } 30 | } 31 | 32 | pub fn name(_: *Type) []const u8 { 33 | return " open"; 34 | } 35 | 36 | pub fn select(self: *Type) void { 37 | { 38 | var buf: std.ArrayList(u8) = .empty; 39 | defer buf.deinit(self.allocator); 40 | const file_path = project_manager.expand_home(self.allocator, &buf, self.file_path.items); 41 | if (root.is_directory(file_path)) 42 | tp.self_pid().send(.{ "cmd", "change_project", .{file_path} }) catch {} 43 | else if (file_path.len > 0) 44 | tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch {}; 45 | } 46 | command.executeName("exit_mini_mode", .{}) catch {}; 47 | } 48 | -------------------------------------------------------------------------------- /src/tui/status/blank.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const Plane = @import("renderer").Plane; 4 | const EventHandler = @import("EventHandler"); 5 | 6 | const Widget = @import("../Widget.zig"); 7 | 8 | plane: Plane, 9 | layout_: Widget.Layout, 10 | on_event: ?EventHandler, 11 | 12 | const Self = @This(); 13 | 14 | pub fn Create(comptime layout_: Widget.Layout) @import("widget.zig").CreateFunction { 15 | return struct { 16 | fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) @import("widget.zig").CreateError!Widget { 17 | const layout__ = if (layout_ == .static) blk: { 18 | if (arg) |str_size| { 19 | const size = std.fmt.parseInt(usize, str_size, 10) catch break :blk layout_; 20 | break :blk Widget.Layout{ .static = size }; 21 | } else break :blk layout_; 22 | } else layout_; 23 | const self = try allocator.create(Self); 24 | errdefer allocator.destroy(self); 25 | self.* = .{ 26 | .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), 27 | .layout_ = layout__, 28 | .on_event = event_handler, 29 | }; 30 | return Widget.to(self); 31 | } 32 | }.create; 33 | } 34 | 35 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 36 | self.plane.deinit(); 37 | allocator.destroy(self); 38 | } 39 | 40 | pub fn layout(self: *Self) Widget.Layout { 41 | return self.layout_; 42 | } 43 | 44 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 45 | self.plane.set_base_style(theme.editor); 46 | self.plane.erase(); 47 | self.plane.home(); 48 | self.plane.set_style(theme.statusbar); 49 | self.plane.fill(" "); 50 | return false; 51 | } 52 | 53 | pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { 54 | var btn: u32 = 0; 55 | if (try m.match(.{ "D", tp.any, tp.extract(&btn), tp.more })) { 56 | if (self.on_event) |h| h.send(from, m) catch {}; 57 | return true; 58 | } 59 | return false; 60 | } 61 | -------------------------------------------------------------------------------- /src/tui/status/widget.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const EventHandler = @import("EventHandler"); 3 | const Plane = @import("renderer").Plane; 4 | const log = @import("log"); 5 | 6 | const Widget = @import("../Widget.zig"); 7 | 8 | const widgets = std.StaticStringMap(CreateFunction).initComptime(.{ 9 | .{ "mode", @import("modestate.zig").create }, 10 | .{ "file", @import("filestate.zig").create }, 11 | .{ "log", @import("minilog.zig").create }, 12 | .{ "selection", @import("selectionstate.zig").create }, 13 | .{ "diagnostics", @import("diagstate.zig").create }, 14 | .{ "linenumber", @import("linenumstate.zig").create }, 15 | .{ "modifiers", @import("modstate.zig").create }, 16 | .{ "keystate", @import("keystate.zig").create }, 17 | .{ "expander", @import("blank.zig").Create(.dynamic) }, 18 | .{ "spacer", @import("blank.zig").Create(.{ .static = 1 }) }, 19 | .{ "clock", @import("clock.zig").create }, 20 | .{ "keybind", @import("keybindstate.zig").create }, 21 | .{ "tabs", @import("tabs.zig").create }, 22 | .{ "branch", @import("branch.zig").create }, 23 | }); 24 | pub const CreateError = error{ OutOfMemory, WidgetInitFailed }; 25 | pub const CreateFunction = *const fn (allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) CreateError!Widget; 26 | 27 | pub fn create(descriptor: []const u8, allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler) CreateError!?Widget { 28 | var it = std.mem.splitScalar(u8, descriptor, ':'); 29 | const name = it.next() orelse { 30 | const logger = log.logger("statusbar"); 31 | logger.print_err("config", "bad widget descriptor \"{s}\" (see log)", .{descriptor}); 32 | return null; 33 | }; 34 | const arg = it.next(); 35 | 36 | const create_ = widgets.get(name) orelse { 37 | const logger = log.logger("statusbar"); 38 | logger.print_err("config", "unknown widget \"{s}\" (see log)", .{name}); 39 | log_widgets(logger); 40 | return null; 41 | }; 42 | return try create_(allocator, parent, event_handler, arg); 43 | } 44 | 45 | fn log_widgets(logger: anytype) void { 46 | logger.print("available widgets:", .{}); 47 | for (widgets.keys()) |name| 48 | logger.print(" {s}", .{name}); 49 | } 50 | -------------------------------------------------------------------------------- /src/tui/mode/mini/move_to_char.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const command = @import("command"); 4 | 5 | const tui = @import("../../tui.zig"); 6 | 7 | pub const Type = @import("get_char.zig").Create(@This()); 8 | pub const create = Type.create; 9 | 10 | pub const ValueType = struct { 11 | direction: Direction, 12 | operation_command: []const u8, 13 | operation: Operation, 14 | }; 15 | 16 | const Direction = enum { 17 | left, 18 | right, 19 | }; 20 | 21 | const Operation = enum { 22 | move, 23 | select, 24 | extend, 25 | }; 26 | 27 | pub fn start(self: *Type) ValueType { 28 | var operation_command: []const u8 = "move_to_char_left"; 29 | _ = self.ctx.args.match(.{cbor.extract(&operation_command)}) catch {}; 30 | 31 | const direction: Direction = if (std.mem.indexOf(u8, operation_command, "_left")) |_| .left else .right; 32 | var operation: Operation = undefined; 33 | if (std.mem.indexOf(u8, operation_command, "extend_")) |_| { 34 | operation = .extend; 35 | } else if (std.mem.indexOf(u8, operation_command, "select_")) |_| { 36 | operation = .select; 37 | } else if (tui.get_active_editor()) |editor| if (editor.get_primary().selection) |_| { 38 | operation = .select; 39 | } else { 40 | operation = .move; 41 | } else { 42 | operation = .move; 43 | } 44 | 45 | return .{ 46 | .direction = direction, 47 | .operation_command = operation_command, 48 | .operation = operation, 49 | }; 50 | } 51 | 52 | pub fn name(self: *Type) []const u8 { 53 | return switch (self.value.operation) { 54 | .move => switch (self.value.direction) { 55 | .left => "↶ move", 56 | .right => "↷ move", 57 | }, 58 | .select => switch (self.value.direction) { 59 | .left => "󰒅 ↶ select", 60 | .right => "󰒅 ↷ select", 61 | }, 62 | .extend => switch (self.value.direction) { 63 | .left => "󰒅 ↶ extend", 64 | .right => "󰒅 ↷ extend", 65 | }, 66 | }; 67 | } 68 | 69 | pub fn process_egc(self: *Type, egc: []const u8) command.Result { 70 | try command.executeName(self.value.operation_command, command.fmt(.{egc})); 71 | try command.executeName("exit_mini_mode", .{}); 72 | } 73 | -------------------------------------------------------------------------------- /src/service_template.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const log = @import("log"); 4 | 5 | pid: ?tp.pid, 6 | 7 | const Self = @This(); 8 | const module_name = @typeName(Self); 9 | pub const Error = error{ OutOfMemory, Exit }; 10 | 11 | pub fn create(allocator: std.mem.Allocator) Error!Self { 12 | return .{ .pid = try Process.create(allocator) }; 13 | } 14 | 15 | pub fn from_pid(pid: tp.pid_ref) Error!Self { 16 | return .{ .pid = pid.clone() }; 17 | } 18 | 19 | pub fn deinit(self: *Self) void { 20 | if (self.pid) |pid| { 21 | self.pid = null; 22 | pid.deinit(); 23 | } 24 | } 25 | 26 | pub fn shutdown(self: *Self) void { 27 | if (self.pid) |pid| { 28 | pid.send(.{"shutdown"}) catch {}; 29 | self.deinit(); 30 | } 31 | } 32 | 33 | // pub fn send(self: *Self, m: tp.message) tp.result { 34 | // const pid = self.pid orelse return tp.exit_error(error.Shutdown); 35 | // try pid.send(m); 36 | // } 37 | 38 | const Process = struct { 39 | allocator: std.mem.Allocator, 40 | parent: tp.pid, 41 | logger: log.Logger, 42 | receiver: Receiver, 43 | 44 | const Receiver = tp.Receiver(*Process); 45 | 46 | pub fn create(allocator: std.mem.Allocator) Error!tp.pid { 47 | const self = try allocator.create(Process); 48 | errdefer allocator.destroy(self); 49 | self.* = .{ 50 | .allocator = allocator, 51 | .parent = tp.self_pid().clone(), 52 | .logger = log.logger(module_name), 53 | .receiver = Receiver.init(Process.receive, self), 54 | }; 55 | return tp.spawn_link(self.a, self, Process.start) catch |e| tp.exit_error(e); 56 | } 57 | 58 | fn deinit(self: *Process) void { 59 | self.parent.deinit(); 60 | self.logger.deinit(); 61 | self.a.destroy(self); 62 | } 63 | 64 | fn start(self: *Process) tp.result { 65 | _ = tp.set_trap(true); 66 | tp.receive(&self.receiver); 67 | } 68 | 69 | fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result { 70 | errdefer self.deinit(); 71 | 72 | if (try m.match(.{"shutdown"})) { 73 | return tp.exit_normal(); 74 | } else { 75 | self.logger.err("receive", tp.unexpected(m)); 76 | return tp.unexpected(m); 77 | } 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /src/completion.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | 4 | const OutOfMemoryError = error{OutOfMemory}; 5 | const SpawnError = error{ThespianSpawnFailed}; 6 | 7 | pub fn send( 8 | allocator: std.mem.Allocator, 9 | to: tp.pid_ref, 10 | m: anytype, 11 | ctx: anytype, 12 | ) (OutOfMemoryError || SpawnError)!void { 13 | return RequestContext(@TypeOf(ctx)).send(allocator, to, ctx, tp.message.fmt(m)); 14 | } 15 | 16 | fn RequestContext(T: type) type { 17 | return struct { 18 | receiver: ReceiverT, 19 | ctx: T, 20 | to: tp.pid, 21 | request: tp.message, 22 | response: ?tp.message, 23 | a: std.mem.Allocator, 24 | 25 | const Self = @This(); 26 | const ReceiverT = tp.Receiver(*@This()); 27 | 28 | fn send(a: std.mem.Allocator, to: tp.pid_ref, ctx: T, request: tp.message) (OutOfMemoryError || SpawnError)!void { 29 | const self = try a.create(@This()); 30 | self.* = .{ 31 | .receiver = undefined, 32 | .ctx = if (@hasDecl(T, "clone")) ctx.clone() else ctx, 33 | .to = to.clone(), 34 | .request = try request.clone(std.heap.c_allocator), 35 | .response = null, 36 | .a = a, 37 | }; 38 | self.receiver = ReceiverT.init(receive_, self); 39 | const proc = try tp.spawn_link(a, self, start, @typeName(@This())); 40 | defer proc.deinit(); 41 | } 42 | 43 | fn deinit(self: *@This()) void { 44 | if (@hasDecl(T, "deinit")) self.ctx.deinit(); 45 | std.heap.c_allocator.free(self.request.buf); 46 | self.to.deinit(); 47 | self.a.destroy(self); 48 | } 49 | 50 | fn start(self: *@This()) tp.result { 51 | _ = tp.set_trap(true); 52 | if (@hasDecl(T, "link")) try self.ctx.link(); 53 | errdefer self.deinit(); 54 | try self.to.link(); 55 | try self.to.send_raw(self.request); 56 | tp.receive(&self.receiver); 57 | } 58 | 59 | fn receive_(self: *@This(), _: tp.pid_ref, m: tp.message) tp.result { 60 | defer self.deinit(); 61 | self.ctx.receive(m) catch |e| return tp.exit_error(e, @errorReturnTrace()); 62 | return tp.exit_normal(); 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/tui/Box.zig: -------------------------------------------------------------------------------- 1 | const Plane = @import("renderer").Plane; 2 | const Layer = @import("renderer").Layer; 3 | const WidgetStyle = @import("WidgetStyle.zig"); 4 | 5 | const Self = @This(); 6 | 7 | y: usize = 0, 8 | x: usize = 0, 9 | h: usize = 1, 10 | w: usize = 1, 11 | 12 | pub fn opts(self: Self, name_: [:0]const u8) Plane.Options { 13 | return self.opts_flags(name_, Plane.option.none); 14 | } 15 | 16 | pub fn opts_vscroll(self: Self, name_: [:0]const u8) Plane.Options { 17 | return self.opts_flags(name_, Plane.option.VSCROLL); 18 | } 19 | 20 | fn opts_flags(self: Self, name_: [:0]const u8, flags: Plane.option) Plane.Options { 21 | return Plane.Options{ 22 | .y = @intCast(self.y), 23 | .x = @intCast(self.x), 24 | .rows = @intCast(self.h), 25 | .cols = @intCast(self.w), 26 | .name = name_, 27 | .flags = flags, 28 | }; 29 | } 30 | 31 | pub fn from(n: Plane) Self { 32 | return .{ 33 | .y = @intCast(n.abs_y()), 34 | .x = @intCast(n.abs_x()), 35 | .h = @intCast(n.dim_y()), 36 | .w = @intCast(n.dim_x()), 37 | }; 38 | } 39 | 40 | pub fn from_client_box(self: Self, padding: WidgetStyle.Margin) Self { 41 | const total_y_padding = padding.top + padding.bottom; 42 | const total_x_padding = padding.left + padding.right; 43 | const y = if (self.y < padding.top) padding.top else self.y; 44 | const x = if (self.x < padding.left) padding.left else self.x; 45 | var box = self; 46 | box.y = y - padding.top; 47 | box.h += total_y_padding; 48 | box.x = x - padding.left; 49 | box.w += total_x_padding; 50 | return box; 51 | } 52 | 53 | pub fn to_client_box(self: Self, padding: WidgetStyle.Margin) Self { 54 | const total_y_padding = padding.top + padding.bottom; 55 | const total_x_padding = padding.left + padding.right; 56 | var box = self; 57 | box.y += padding.top; 58 | box.h -= if (box.h > total_y_padding) total_y_padding else box.h; 59 | box.x += padding.left; 60 | box.w -= if (box.w > total_x_padding) total_x_padding else box.w; 61 | return box; 62 | } 63 | 64 | pub fn to_layer(self: Self) Layer.Options { 65 | return .{ 66 | .y = @intCast(self.y), 67 | .x = @intCast(self.x), 68 | .h = @intCast(self.h), 69 | .w = @intCast(self.w), 70 | }; 71 | } 72 | 73 | pub fn is_abs_coord_inside(self: Self, y: usize, x: usize) bool { 74 | return y >= self.y and y < self.y + self.h and x >= self.x and x < self.x + self.w; 75 | } 76 | -------------------------------------------------------------------------------- /src/tui/info_view.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = @import("std").mem.Allocator; 3 | const Plane = @import("renderer").Plane; 4 | const Widget = @import("Widget.zig"); 5 | const WidgetList = @import("WidgetList.zig"); 6 | 7 | pub const name = @typeName(Self); 8 | 9 | const Self = @This(); 10 | 11 | allocator: std.mem.Allocator, 12 | plane: Plane, 13 | 14 | view_rows: usize = 0, 15 | lines: std.ArrayList([]const u8), 16 | 17 | const widget_type: Widget.Type = .panel; 18 | 19 | pub fn create(allocator: Allocator, parent: Plane) !Widget { 20 | const self = try allocator.create(Self); 21 | errdefer allocator.destroy(self); 22 | const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type); 23 | self.* = .{ 24 | .allocator = allocator, 25 | .plane = try Plane.init(&(Widget.Box{}).opts(name), parent), 26 | .lines = .empty, 27 | }; 28 | container.ctx = self; 29 | try container.add(Widget.to(self)); 30 | return container.widget(); 31 | } 32 | 33 | pub fn deinit(self: *Self, allocator: Allocator) void { 34 | self.clear(); 35 | self.lines.deinit(self.allocator); 36 | self.plane.deinit(); 37 | allocator.destroy(self); 38 | } 39 | 40 | pub fn clear(self: *Self) void { 41 | for (self.lines.items) |line| 42 | self.allocator.free(line); 43 | self.lines.clearRetainingCapacity(); 44 | } 45 | 46 | pub fn handle_resize(self: *Self, pos: Widget.Box) void { 47 | self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return; 48 | self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return; 49 | self.view_rows = pos.h; 50 | } 51 | 52 | pub fn append_content(self: *Self, content: []const u8) !void { 53 | var iter = std.mem.splitScalar(u8, content, '\n'); 54 | while (iter.next()) |line| 55 | (try self.lines.addOne(self.allocator)).* = try self.allocator.dupe(u8, line); 56 | } 57 | 58 | pub fn set_content(self: *Self, content: []const u8) !void { 59 | self.clear(); 60 | return self.append_content(content); 61 | } 62 | 63 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 64 | self.plane.set_base_style(theme.panel); 65 | self.plane.erase(); 66 | self.plane.home(); 67 | for (self.lines.items) |line| { 68 | _ = self.plane.putstr(line) catch {}; 69 | if (self.plane.cursor_y() >= self.view_rows - 1) 70 | return false; 71 | self.plane.cursor_move_yx(-1, 0) catch {}; 72 | self.plane.cursor_move_rel(1, 0) catch {}; 73 | } 74 | return false; 75 | } 76 | -------------------------------------------------------------------------------- /test/tests_project_manager.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pm = @import("project_manager"); 3 | const builtin = @import("builtin"); 4 | 5 | test "normalize_file_path_dot_prefix" { 6 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2("example.txt"))); 7 | try std.testing.expectEqualStrings(P1("/example.txt"), pm.normalize_file_path_dot_prefix(P2("/example.txt"))); 8 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2("./example.txt"))); 9 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2("././example.txt"))); 10 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2(".//example.txt"))); 11 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2(".//./example.txt"))); 12 | try std.testing.expectEqualStrings(P1("example.txt"), pm.normalize_file_path_dot_prefix(P2(".//.//example.txt"))); 13 | try std.testing.expectEqualStrings(P1("../example.txt"), pm.normalize_file_path_dot_prefix(P2("./../example.txt"))); 14 | try std.testing.expectEqualStrings(P1("../example.txt"), pm.normalize_file_path_dot_prefix(P2(".//../example.txt"))); 15 | try std.testing.expectEqualStrings(P1("../example.txt"), pm.normalize_file_path_dot_prefix(P2("././../example.txt"))); 16 | try std.testing.expectEqualStrings(P1("../example.txt"), pm.normalize_file_path_dot_prefix(P2("././/../example.txt"))); 17 | try std.testing.expectEqualStrings(P1("../example.txt"), pm.normalize_file_path_dot_prefix(P2(".//.//../example.txt"))); 18 | try std.testing.expectEqualStrings(P1("./"), pm.normalize_file_path_dot_prefix(P2("./"))); 19 | try std.testing.expectEqualStrings(P1("."), pm.normalize_file_path_dot_prefix(P2("."))); 20 | } 21 | 22 | fn P1(file_path: []const u8) []const u8 { 23 | const local = struct { 24 | var fixed_file_path: [256]u8 = undefined; 25 | }; 26 | return fix_path(&local.fixed_file_path, file_path); 27 | } 28 | fn P2(file_path: []const u8) []const u8 { 29 | const local = struct { 30 | var fixed_file_path: [256]u8 = undefined; 31 | }; 32 | return fix_path(&local.fixed_file_path, file_path); 33 | } 34 | fn fix_path(dest: []u8, src: []const u8) []const u8 { 35 | if (builtin.os.tag == .windows) { 36 | for (src, 0..) |c, i| switch (c) { 37 | std.fs.path.sep_posix => dest[i] = std.fs.path.sep_windows, 38 | else => dest[i] = c, 39 | }; 40 | return dest[0..src.len]; 41 | } else return src; 42 | } 43 | -------------------------------------------------------------------------------- /src/tui/status/diagstate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const tp = @import("thespian"); 4 | 5 | const Plane = @import("renderer").Plane; 6 | const command = @import("command"); 7 | const EventHandler = @import("EventHandler"); 8 | 9 | const Widget = @import("../Widget.zig"); 10 | const Button = @import("../Button.zig"); 11 | 12 | errors: usize = 0, 13 | warnings: usize = 0, 14 | info: usize = 0, 15 | hints: usize = 0, 16 | buf: [256]u8 = undefined, 17 | rendered: [:0]const u8 = "", 18 | 19 | const Self = @This(); 20 | const ButtonType = Button.Options(Self).ButtonType; 21 | 22 | pub fn create(allocator: Allocator, parent: Plane, event_handler: ?EventHandler, _: ?[]const u8) @import("widget.zig").CreateError!Widget { 23 | return Button.create_widget(Self, allocator, parent, .{ 24 | .ctx = .{}, 25 | .label = "", 26 | .on_click = on_click, 27 | .on_layout = layout, 28 | .on_render = render, 29 | .on_receive = receive, 30 | .on_event = event_handler, 31 | }); 32 | } 33 | 34 | fn on_click(_: *Self, _: *ButtonType, _: Widget.Pos) void { 35 | command.executeName("show_diagnostics", .{}) catch {}; 36 | } 37 | 38 | pub fn layout(self: *Self, _: *ButtonType) Widget.Layout { 39 | return .{ .static = self.rendered.len }; 40 | } 41 | 42 | pub fn render(self: *Self, btn: *ButtonType, theme: *const Widget.Theme) bool { 43 | const bg_style = if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar; 44 | btn.plane.set_base_style(theme.editor); 45 | btn.plane.erase(); 46 | btn.plane.home(); 47 | btn.plane.set_style(bg_style); 48 | btn.plane.fill(" "); 49 | btn.plane.home(); 50 | _ = btn.plane.putstr(self.rendered) catch {}; 51 | return false; 52 | } 53 | 54 | fn format(self: *Self) void { 55 | var fbs = std.io.fixedBufferStream(&self.buf); 56 | const writer = fbs.writer(); 57 | if (self.errors > 0) std.fmt.format(writer, "  {d}", .{self.errors}) catch {}; 58 | if (self.warnings > 0) std.fmt.format(writer, "  {d}", .{self.warnings}) catch {}; 59 | if (self.info > 0) std.fmt.format(writer, "  {d}", .{self.info}) catch {}; 60 | if (self.hints > 0) std.fmt.format(writer, "  {d}", .{self.hints}) catch {}; 61 | self.rendered = @ptrCast(fbs.getWritten()); 62 | self.buf[self.rendered.len] = 0; 63 | } 64 | 65 | pub fn receive(self: *Self, _: *ButtonType, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 66 | if (try m.match(.{ "E", "diag", tp.extract(&self.errors), tp.extract(&self.warnings), tp.extract(&self.info), tp.extract(&self.hints) })) 67 | self.format(); 68 | return false; 69 | } 70 | -------------------------------------------------------------------------------- /src/win32/builtin.hlsl: -------------------------------------------------------------------------------- 1 | cbuffer GridConfig : register(b0) 2 | { 3 | uint2 cell_size; 4 | uint col_count; 5 | uint row_count; 6 | } 7 | 8 | struct Cell 9 | { 10 | uint glyph_index; 11 | uint bg; 12 | uint fg; 13 | // todo: underline flags, single/double/curly/dotted/dashed 14 | // todo: underline color 15 | }; 16 | StructuredBuffer cells : register(t0); 17 | Texture2D glyph_texture : register(t1); 18 | 19 | float4 VertexMain(uint id : SV_VERTEXID) : SV_POSITION 20 | { 21 | return float4( 22 | 2.0 * (float(id & 1) - 0.5), 23 | -(float(id >> 1) - 0.5) * 2.0, 24 | 0, 1 25 | ); 26 | } 27 | 28 | float4 UnpackRgba(uint packed) 29 | { 30 | float4 unpacked; 31 | unpacked.r = (float)((packed >> 24) & 0xFF) / 255.0f; 32 | unpacked.g = (float)((packed >> 16) & 0xFF) / 255.0f; 33 | unpacked.b = (float)((packed >> 8) & 0xFF) / 255.0f; 34 | unpacked.a = (float)(packed & 0xFF) / 255.0f; 35 | return unpacked; 36 | } 37 | 38 | float3 Pixel(float2 pos, float4 bg, float4 fg, float glyph_texel) 39 | { 40 | return lerp(bg.rgb, fg.rgb, fg.a * glyph_texel); 41 | } 42 | 43 | float4 PixelMain(float4 sv_pos : SV_POSITION) : SV_TARGET { 44 | uint col = sv_pos.x / cell_size.x; 45 | uint row = sv_pos.y / cell_size.y; 46 | uint cell_index = row * col_count + col; 47 | 48 | const uint DEBUG_MODE_NONE = 0; 49 | const uint DEBUG_MODE_GLYPH_TEXTURE = 2; 50 | 51 | const uint DEBUG_MODE = DEBUG_MODE_NONE; 52 | // const uint DEBUG_MODE = DEBUG_MODE_GLYPH_TEXTURE; 53 | 54 | Cell cell = cells[cell_index]; 55 | float4 bg = UnpackRgba(cell.bg); 56 | float4 fg = UnpackRgba(cell.fg); 57 | 58 | if (DEBUG_MODE == DEBUG_MODE_GLYPH_TEXTURE) { 59 | float4 glyph_texel = glyph_texture.Load(int3(sv_pos.xy, 0)); 60 | return lerp(bg, fg, glyph_texel.a); 61 | } 62 | 63 | uint texture_width, texture_height; 64 | glyph_texture.GetDimensions(texture_width, texture_height); 65 | uint2 texture_size = uint2(texture_width, texture_height); 66 | uint cells_per_row = texture_width / cell_size.x; 67 | 68 | uint2 glyph_cell_pos = uint2( 69 | cell.glyph_index % cells_per_row, 70 | cell.glyph_index / cells_per_row 71 | ); 72 | uint2 cell_pixel = uint2(sv_pos.xy) % cell_size; 73 | uint2 texture_coord = glyph_cell_pos * cell_size + cell_pixel; 74 | float4 glyph_texel = glyph_texture.Load(int3(texture_coord, 0)); 75 | 76 | float2 pos = (sv_pos.xy - 0.5) / (float2(cell_size) * float2(col_count, row_count)); 77 | float4 p = float4(Pixel(pos, bg, fg, glyph_texel.a), 1.0); 78 | // return red/green for out-of-bound pixels for now 79 | if (pos.x > 1) return float4(1,0,0,1); 80 | if (pos.y > 1) return float4(0,1,0,1); 81 | return p; 82 | } 83 | -------------------------------------------------------------------------------- /src/win32/xterm.zig: -------------------------------------------------------------------------------- 1 | pub const colors: [256]u24 = .{ 2 | 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, 3 | 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, 4 | 5 | 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 6 | 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, 7 | 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, 8 | 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 9 | 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 10 | 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, 11 | 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, 12 | 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 13 | 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 14 | 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, 15 | 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, 16 | 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 17 | 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, 18 | 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 19 | 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, 20 | 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, 21 | 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, 22 | 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, 23 | 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 24 | 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 25 | 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, 26 | 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, 27 | 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, 28 | 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 29 | 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, 30 | 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 31 | 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, 32 | 33 | 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 34 | 0x585858, 0x606060, 0x666666, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 35 | 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, 36 | }; 37 | -------------------------------------------------------------------------------- /src/bin_path.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub const find_binary_in_path = switch (builtin.os.tag) { 5 | .windows => find_binary_in_path_windows, 6 | else => find_binary_in_path_posix, 7 | }; 8 | 9 | fn find_binary_in_path_posix(allocator: std.mem.Allocator, binary_name: []const u8) std.mem.Allocator.Error!?[:0]const u8 { 10 | const bin_paths = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { 11 | error.OutOfMemory => return error.OutOfMemory, 12 | error.EnvironmentVariableNotFound, error.InvalidWtf8 => &.{}, 13 | }; 14 | defer allocator.free(bin_paths); 15 | var bin_path_iterator = std.mem.splitScalar(u8, bin_paths, std.fs.path.delimiter); 16 | while (bin_path_iterator.next()) |bin_path| { 17 | const resolved_binary_path = try std.fs.path.resolve(allocator, &.{ bin_path, binary_name }); 18 | defer allocator.free(resolved_binary_path); 19 | std.posix.access(resolved_binary_path, std.posix.X_OK) catch continue; 20 | return try allocator.dupeZ(u8, resolved_binary_path); 21 | } 22 | return null; 23 | } 24 | 25 | fn find_binary_in_path_windows(allocator: std.mem.Allocator, binary_name_: []const u8) std.mem.Allocator.Error!?[:0]const u8 { 26 | const bin_paths = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { 27 | error.OutOfMemory => return error.OutOfMemory, 28 | error.EnvironmentVariableNotFound, error.InvalidWtf8 => &.{}, 29 | }; 30 | defer allocator.free(bin_paths); 31 | const bin_extensions = std.process.getEnvVarOwned(allocator, "PATHEXT") catch |err| switch (err) { 32 | error.OutOfMemory => return error.OutOfMemory, 33 | error.EnvironmentVariableNotFound, error.InvalidWtf8 => &.{}, 34 | }; 35 | defer allocator.free(bin_extensions); 36 | var bin_path_iterator = std.mem.splitScalar(u8, bin_paths, std.fs.path.delimiter); 37 | while (bin_path_iterator.next()) |bin_path| { 38 | if (!std.fs.path.isAbsolute(bin_path)) continue; 39 | var dir = std.fs.openDirAbsolute(bin_path, .{}) catch continue; 40 | defer dir.close(); 41 | var bin_extensions_iterator = std.mem.splitScalar(u8, bin_extensions, ';'); 42 | while (bin_extensions_iterator.next()) |bin_extension| { 43 | var path: std.ArrayList(u8) = .empty; 44 | try path.appendSlice(allocator, binary_name_); 45 | try path.appendSlice(allocator, bin_extension); 46 | const binary_name = try path.toOwnedSlice(allocator); 47 | defer allocator.free(binary_name); 48 | _ = dir.statFile(binary_name) catch continue; 49 | const resolved_binary_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_path, binary_name }); 50 | defer allocator.free(resolved_binary_path); 51 | return try allocator.dupeZ(u8, resolved_binary_path); 52 | } 53 | } 54 | return null; 55 | } 56 | -------------------------------------------------------------------------------- /src/tui/status/modstate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const tp = @import("thespian"); 4 | const tracy = @import("tracy"); 5 | 6 | const Plane = @import("renderer").Plane; 7 | const input = @import("input"); 8 | const command = @import("command"); 9 | const EventHandler = @import("EventHandler"); 10 | 11 | const Widget = @import("../Widget.zig"); 12 | const tui = @import("../tui.zig"); 13 | 14 | plane: Plane, 15 | mods: input.ModSet = .{}, 16 | hover: bool = false, 17 | 18 | const Self = @This(); 19 | 20 | pub const width = 8; 21 | 22 | pub fn create(allocator: Allocator, parent: Plane, _: ?EventHandler, _: ?[]const u8) @import("widget.zig").CreateError!Widget { 23 | const self = try allocator.create(Self); 24 | errdefer allocator.destroy(self); 25 | self.* = .{ 26 | .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), 27 | }; 28 | try tui.input_listeners().add(EventHandler.bind(self, listen)); 29 | return self.widget(); 30 | } 31 | 32 | pub fn widget(self: *Self) Widget { 33 | return Widget.to(self); 34 | } 35 | 36 | pub fn deinit(self: *Self, allocator: Allocator) void { 37 | tui.input_listeners().remove_ptr(self); 38 | self.plane.deinit(); 39 | allocator.destroy(self); 40 | } 41 | 42 | pub fn layout(_: *Self) Widget.Layout { 43 | return .{ .static = width }; 44 | } 45 | 46 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 47 | const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" }); 48 | defer frame.deinit(); 49 | self.plane.set_base_style(theme.editor); 50 | self.plane.erase(); 51 | self.plane.home(); 52 | self.plane.set_style(if (self.hover) theme.statusbar_hover else theme.statusbar); 53 | self.plane.fill(" "); 54 | self.plane.home(); 55 | 56 | _ = self.plane.print(" {s}{s}{s} ", .{ 57 | mode(self.mods.ctrl, "Ⓒ ", "🅒 "), 58 | mode(self.mods.shift, "Ⓢ ", "🅢 "), 59 | mode(self.mods.alt, "Ⓐ ", "🅐 "), 60 | }) catch {}; 61 | return false; 62 | } 63 | 64 | inline fn mode(state: bool, off: [:0]const u8, on: [:0]const u8) [:0]const u8 { 65 | return if (state) on else off; 66 | } 67 | 68 | fn render_modifier(self: *Self, state: bool, off: [:0]const u8, on: [:0]const u8) void { 69 | _ = self.plane.putstr(if (state) on else off) catch {}; 70 | } 71 | 72 | pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result { 73 | var mods: input.Mods = 0; 74 | if (try m.match(.{ "I", tp.any, tp.any, tp.any, tp.any, tp.extract(&mods), tp.more })) { 75 | self.mods = @bitCast(mods); 76 | } 77 | } 78 | 79 | pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 80 | if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.any, tp.any, tp.any })) { 81 | command.executeName("toggle_inputview", .{}) catch {}; 82 | return true; 83 | } 84 | return try m.match(.{ "H", tp.extract(&self.hover) }); 85 | } 86 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/fontface_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | 5 | const Widget = @import("../../Widget.zig"); 6 | const tui = @import("../../tui.zig"); 7 | 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | 10 | pub const label = "Select font face"; 11 | pub const name = " font"; 12 | pub const description = "font"; 13 | 14 | pub const Entry = struct { 15 | label: []const u8, 16 | }; 17 | 18 | pub const Match = struct { 19 | label: []const u8, 20 | score: i32, 21 | matches: []const usize, 22 | }; 23 | 24 | var previous_fontface: ?[]const u8 = null; 25 | 26 | pub fn deinit(palette: *Type) void { 27 | if (previous_fontface) |fontface| 28 | palette.allocator.free(fontface); 29 | previous_fontface = null; 30 | for (palette.entries.items) |entry| 31 | palette.allocator.free(entry.label); 32 | } 33 | 34 | pub fn load_entries(palette: *Type) !usize { 35 | var idx: usize = 0; 36 | previous_fontface = try palette.allocator.dupe(u8, tui.fontface()); 37 | const fontfaces = try tui.fontfaces(palette.allocator); 38 | defer palette.allocator.free(fontfaces); 39 | for (fontfaces) |fontface| { 40 | idx += 1; 41 | (try palette.entries.addOne(palette.allocator)).* = .{ .label = fontface }; 42 | if (previous_fontface) |previous_fontface_| if (std.mem.eql(u8, fontface, previous_fontface_)) { 43 | palette.initial_selected = idx; 44 | }; 45 | } 46 | return 0; 47 | } 48 | 49 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 50 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 51 | defer value.deinit(); 52 | const writer = &value.writer; 53 | try cbor.writeValue(writer, entry.label); 54 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 55 | try palette.menu.add_item_with_handler(value.written(), select); 56 | palette.items += 1; 57 | } 58 | 59 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 60 | var label_: []const u8 = undefined; 61 | var iter = button.opts.label; 62 | if (!(cbor.matchString(&iter, &label_) catch false)) return; 63 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("fontface_palette", e); 64 | tp.self_pid().send(.{ "cmd", "set_fontface", .{label_} }) catch |e| menu.*.opts.ctx.logger.err("fontface_palette", e); 65 | } 66 | 67 | pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { 68 | const button = button_ orelse return cancel(palette); 69 | var label_: []const u8 = undefined; 70 | var iter = button.opts.label; 71 | if (!(cbor.matchString(&iter, &label_) catch false)) return; 72 | tp.self_pid().send(.{ "cmd", "set_fontface", .{label_} }) catch |e| palette.logger.err("fontface_palette upated", e); 73 | } 74 | 75 | pub fn cancel(palette: *Type) !void { 76 | if (previous_fontface) |prev| 77 | tp.self_pid().send(.{ "cmd", "set_fontface", .{prev} }) catch |e| palette.logger.err("fontface_palette cancel", e); 78 | } 79 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/buffer_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | const root = @import("soft_root").root; 5 | const command = @import("command"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | const module_name = @typeName(@This()); 10 | const Widget = @import("../../Widget.zig"); 11 | 12 | pub const label = "Switch buffers"; 13 | pub const name = " buffer"; 14 | pub const description = "buffer"; 15 | pub const icon = "󰈞 "; 16 | 17 | pub const Entry = struct { 18 | label: []const u8, 19 | icon: []const u8, 20 | color: ?u24, 21 | indicator: []const u8, 22 | }; 23 | 24 | pub fn load_entries(palette: *Type) !usize { 25 | const buffer_manager = tui.get_buffer_manager() orelse return 0; 26 | const buffers = try buffer_manager.list_most_recently_used(palette.allocator); 27 | defer palette.allocator.free(buffers); 28 | for (buffers) |buffer| { 29 | const indicator = tui.get_buffer_state_indicator(buffer); 30 | (try palette.entries.addOne(palette.allocator)).* = .{ 31 | .label = buffer.get_file_path(), 32 | .icon = buffer.file_type_icon orelse "", 33 | .color = buffer.file_type_color, 34 | .indicator = indicator, 35 | }; 36 | } 37 | return if (palette.entries.items.len == 0) label.len + 3 else 4; 38 | } 39 | 40 | pub fn clear_entries(palette: *Type) void { 41 | palette.entries.clearRetainingCapacity(); 42 | } 43 | 44 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 45 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 46 | defer value.deinit(); 47 | const writer = &value.writer; 48 | try cbor.writeValue(writer, entry.label); 49 | try cbor.writeValue(writer, entry.icon); 50 | try cbor.writeValue(writer, entry.color); 51 | try cbor.writeValue(writer, entry.indicator); 52 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 53 | try palette.menu.add_item_with_handler(value.written(), select); 54 | palette.items += 1; 55 | } 56 | 57 | pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.Theme, selected: bool) bool { 58 | return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme); 59 | } 60 | 61 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 62 | var file_path: []const u8 = undefined; 63 | var iter = button.opts.label; 64 | if (!(cbor.matchString(&iter, &file_path) catch false)) return; 65 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); 66 | tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err(module_name, e); 67 | } 68 | 69 | pub fn delete_item(menu: *Type.MenuType, button: *Type.ButtonType) bool { 70 | var file_path: []const u8 = undefined; 71 | var iter = button.opts.label; 72 | if (!(cbor.matchString(&iter, &file_path) catch false)) return false; 73 | command.executeName("delete_buffer", command.fmt(.{file_path})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); 74 | return true; //refresh list 75 | } 76 | -------------------------------------------------------------------------------- /src/tui/mode/mini/replace.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const log = @import("log"); 4 | const input = @import("input"); 5 | const keybind = @import("keybind"); 6 | const command = @import("command"); 7 | const EventHandler = @import("EventHandler"); 8 | 9 | const tui = @import("../../tui.zig"); 10 | 11 | const Allocator = @import("std").mem.Allocator; 12 | 13 | const Self = @This(); 14 | 15 | const Commands = command.Collection(cmds); 16 | 17 | allocator: Allocator, 18 | commands: Commands = undefined, 19 | 20 | pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tui.MiniMode } { 21 | var operation_command: []const u8 = undefined; 22 | _ = ctx.args.match(.{tp.extract(&operation_command)}) catch return error.InvalidReplaceArgument; 23 | 24 | const self = try allocator.create(Self); 25 | errdefer allocator.destroy(self); 26 | self.* = .{ 27 | .allocator = allocator, 28 | }; 29 | try self.commands.init(self); 30 | var mode = try keybind.mode("mini/replace", allocator, .{ 31 | .insert_command = "mini_mode_insert_bytes", 32 | }); 33 | mode.event_handler = EventHandler.to_owned(self); 34 | return .{ mode, .{ .name = self.name() } }; 35 | } 36 | 37 | pub fn deinit(self: *Self) void { 38 | self.commands.deinit(); 39 | self.allocator.destroy(self); 40 | } 41 | 42 | fn name(_: *Self) []const u8 { 43 | return "🗘 replace"; 44 | } 45 | 46 | pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { 47 | return false; 48 | } 49 | 50 | fn execute_operation(_: *Self, ctx: command.Context) command.Result { 51 | try command.executeName("replace_with_character_helix", ctx); 52 | try command.executeName("exit_mini_mode", .{}); 53 | } 54 | 55 | const cmds = struct { 56 | pub const Target = Self; 57 | const Ctx = command.Context; 58 | const Meta = command.Metadata; 59 | const Result = command.Result; 60 | 61 | pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { 62 | var code_point: u32 = 0; 63 | if (!try ctx.args.match(.{tp.extract(&code_point)})) 64 | return error.InvalidRepaceInsertCodePointArgument; 65 | 66 | log.logger("replace").print("replacement '{d}'", .{code_point}); 67 | var buf: [6]u8 = undefined; 68 | const bytes = input.ucs32_to_utf8(&[_]u32{code_point}, &buf) catch return error.InvalidReplaceCodePoint; 69 | log.logger("replace").print("replacement '{s}'", .{buf[0..bytes]}); 70 | return self.execute_operation(ctx); 71 | } 72 | pub const mini_mode_insert_code_point_meta: Meta = .{ .description = "🗘 Replace" }; 73 | 74 | pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { 75 | var bytes: []const u8 = undefined; 76 | if (!try ctx.args.match(.{tp.extract(&bytes)})) 77 | return error.InvalidReplaceInsertBytesArgument; 78 | log.logger("replace").print("replacement '{s}'", .{bytes}); 79 | return self.execute_operation(ctx); 80 | } 81 | pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; 82 | 83 | pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { 84 | command.executeName("exit_mini_mode", .{}) catch {}; 85 | } 86 | pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel replace" }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/open_recent_project.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | const project_manager = @import("project_manager"); 5 | const command = @import("command"); 6 | 7 | pub const Type = @import("palette.zig").Create(@This()); 8 | const module_name = @typeName(@This()); 9 | 10 | pub const label = "Search projects"; 11 | pub const name = " project"; 12 | pub const description = "project"; 13 | 14 | pub const Entry = struct { 15 | label: []const u8, 16 | open: bool, 17 | }; 18 | 19 | pub const Match = struct { 20 | name: []const u8, 21 | score: i32, 22 | matches: []const usize, 23 | }; 24 | 25 | pub fn deinit(palette: *Type) void { 26 | for (palette.entries.items) |entry| 27 | palette.allocator.free(entry.label); 28 | } 29 | 30 | pub fn load_entries_with_args(palette: *Type, ctx: command.Context) !usize { 31 | var items_cbor: []const u8 = undefined; 32 | if (!(cbor.match(ctx.args.buf, .{ "PRJ", "recent_projects", tp.extract_cbor(&items_cbor) }) catch false)) 33 | return error.InvalidRecentProjects; 34 | 35 | var iter: []const u8 = items_cbor; 36 | var len = try cbor.decodeArrayHeader(&iter); 37 | while (len > 0) : (len -= 1) { 38 | var name_: []const u8 = undefined; 39 | var open: bool = false; 40 | if (try cbor.decodeArrayHeader(&iter) != 2) 41 | return error.InvalidMessageField; 42 | if (!try cbor.matchValue(&iter, cbor.extract(&name_))) 43 | return error.InvalidMessageField; 44 | if (!try cbor.matchValue(&iter, cbor.extract(&open))) 45 | return error.InvalidMessageField; 46 | (try palette.entries.addOne(palette.allocator)).* = .{ .label = try palette.allocator.dupe(u8, name_), .open = open }; 47 | } 48 | return 1; 49 | } 50 | 51 | pub fn clear_entries(palette: *Type) void { 52 | palette.entries.clearRetainingCapacity(); 53 | } 54 | 55 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 56 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 57 | defer value.deinit(); 58 | const writer = &value.writer; 59 | try cbor.writeValue(writer, entry.label); 60 | try cbor.writeValue(writer, if (entry.open) "-" else ""); 61 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 62 | try palette.menu.add_item_with_handler(value.written(), select); 63 | palette.items += 1; 64 | } 65 | 66 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 67 | var name_: []const u8 = undefined; 68 | var iter = button.opts.label; 69 | if (!(cbor.matchString(&iter, &name_) catch false)) return; 70 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("open_recent_project", e); 71 | tp.self_pid().send(.{ "cmd", "change_project", .{name_} }) catch |e| menu.*.opts.ctx.logger.err("open_recent_project", e); 72 | } 73 | 74 | pub fn delete_item(menu: *Type.MenuType, button: *Type.ButtonType) bool { 75 | var name_: []const u8 = undefined; 76 | var iter = button.opts.label; 77 | if (!(cbor.matchString(&iter, &name_) catch false)) return false; 78 | command.executeName("close_project", command.fmt(.{name_})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); 79 | return true; //refresh list 80 | } 81 | -------------------------------------------------------------------------------- /src/lsp_config.zig: -------------------------------------------------------------------------------- 1 | pub fn get(project: []const u8, lsp_name: []const u8) ?[]const u8 { 2 | if (project.len == 0) return get_global(lsp_name); 3 | if (get_project(project, lsp_name)) |conf| return conf; 4 | return get_global(lsp_name); 5 | } 6 | 7 | fn get_project(project: []const u8, lsp_name: []const u8) ?[]const u8 { 8 | const file_name = get_config_file_path(project, lsp_name, .project, .no_create) catch return null; 9 | defer allocator.free(file_name); 10 | const file: std.fs.File = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; 11 | defer file.close(); 12 | return file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch return null; 13 | } 14 | 15 | fn get_global(lsp_name: []const u8) ?[]const u8 { 16 | const file_name = get_config_file_path(&.{}, lsp_name, .global, .no_create) catch return null; 17 | defer allocator.free(file_name); 18 | const file: std.fs.File = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch return null; 19 | defer file.close(); 20 | return file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch return null; 21 | } 22 | 23 | pub fn get_config_file_path(project: ?[]const u8, lsp_name: []const u8, scope: Scope, mode: Mode) ![]u8 { 24 | const config_dir_path = try get_config_dir_path(project, scope, mode); 25 | defer allocator.free(config_dir_path); 26 | var stream: std.Io.Writer.Allocating = .init(allocator); 27 | defer stream.deinit(); 28 | try stream.writer.print("{s}{s}.json", .{ config_dir_path, lsp_name }); 29 | return stream.toOwnedSlice(); 30 | } 31 | 32 | fn get_config_dir_path(project: ?[]const u8, scope: Scope, mode: Mode) ![]u8 { 33 | var stream: std.Io.Writer.Allocating = .init(allocator); 34 | defer stream.deinit(); 35 | const writer = &stream.writer; 36 | try writer.writeAll(try root.get_config_dir()); 37 | try writer.writeByte(std.fs.path.sep); 38 | switch (scope) { 39 | .project => { 40 | try writer.writeAll("project"); 41 | try writer.writeByte(std.fs.path.sep); 42 | if (mode == .mk_parents) std.fs.makeDirAbsolute(stream.written()) catch |e| switch (e) { 43 | error.PathAlreadyExists => {}, 44 | else => return e, 45 | }; 46 | if (project) |prj| { 47 | for (prj) |c| { 48 | _ = if (std.fs.path.isSep(c)) 49 | try writer.write("__") 50 | else if (c == ':') 51 | try writer.write("___") 52 | else 53 | try writer.writeByte(c); 54 | } 55 | _ = try writer.writeByte(std.fs.path.sep); 56 | } 57 | }, 58 | .global => { 59 | try writer.writeAll("lsp"); 60 | try writer.writeByte(std.fs.path.sep); 61 | }, 62 | } 63 | if (mode == .mk_parents) std.fs.makeDirAbsolute(stream.written()) catch |e| switch (e) { 64 | error.PathAlreadyExists => {}, 65 | else => return e, 66 | }; 67 | return stream.toOwnedSlice(); 68 | } 69 | 70 | pub const Scope = enum { project, global }; 71 | pub const Mode = enum { mk_parents, no_create }; 72 | 73 | pub const allocator = std.heap.c_allocator; 74 | const std = @import("std"); 75 | const root = @import("soft_root").root; 76 | -------------------------------------------------------------------------------- /src/lsp_types.zig: -------------------------------------------------------------------------------- 1 | pub const SymbolKind = enum(u8) { 2 | None = 0, 3 | File = 1, 4 | Module = 2, 5 | Namespace = 3, 6 | Package = 4, 7 | Class = 5, 8 | Method = 6, 9 | Property = 7, 10 | Field = 8, 11 | Constructor = 9, 12 | Enum = 10, 13 | Interface = 11, 14 | Function = 12, 15 | Variable = 13, 16 | Constant = 14, 17 | String = 15, 18 | Number = 16, 19 | Boolean = 17, 20 | Array = 18, 21 | Object = 19, 22 | Key = 20, 23 | Null = 21, 24 | EnumMember = 22, 25 | Struct = 23, 26 | Event = 24, 27 | Operator = 25, 28 | TypeParameter = 26, 29 | 30 | pub fn icon(kind: SymbolKind) []const u8 { 31 | return switch (kind) { 32 | .None => " ", 33 | .File => "", 34 | .Module => "", 35 | .Namespace => "", 36 | .Package => "", 37 | .Class => "", 38 | .Method => "", 39 | .Property => "", 40 | .Field => "", 41 | .Constructor => "", 42 | .Enum => "", 43 | .Interface => "", 44 | .Function => "󰊕", 45 | .Variable => "", 46 | .Constant => "", 47 | .String => "", 48 | .Number => "", 49 | .Boolean => "", 50 | .Array => "", 51 | .Object => "", 52 | .Key => "", 53 | .Null => "󰟢", 54 | .EnumMember => "", 55 | .Struct => "", 56 | .Event => "", 57 | .Operator => "", 58 | .TypeParameter => "", 59 | }; 60 | } 61 | }; 62 | 63 | pub const CompletionItemKind = enum(u8) { 64 | None = 0, 65 | Text = 1, 66 | Method = 2, 67 | Function = 3, 68 | Constructor = 4, 69 | Field = 5, 70 | Variable = 6, 71 | Class = 7, 72 | Interface = 8, 73 | Module = 9, 74 | Property = 10, 75 | Unit = 11, 76 | Value = 12, 77 | Enum = 13, 78 | Keyword = 14, 79 | Snippet = 15, 80 | Color = 16, 81 | File = 17, 82 | Reference = 18, 83 | Folder = 19, 84 | EnumMember = 20, 85 | Constant = 21, 86 | Struct = 22, 87 | Event = 23, 88 | Operator = 24, 89 | TypeParameter = 25, 90 | 91 | pub fn icon(kind: CompletionItemKind) []const u8 { 92 | return switch (kind) { 93 | .None => " ", 94 | .Text => "󰊄", 95 | .Method => "", 96 | .Function => "󰊕", 97 | .Constructor => "", 98 | .Field => "", 99 | .Variable => "", 100 | .Class => "", 101 | .Interface => "", 102 | .Module => "", 103 | .Property => "", 104 | .Unit => "󱔁", 105 | .Value => "󱔁", 106 | .Enum => "", 107 | .Keyword => "", 108 | .Snippet => "", 109 | .Color => "", 110 | .File => "", 111 | .Reference => "※", 112 | .Folder => "🗀", 113 | .EnumMember => "", 114 | .Constant => "", 115 | .Struct => "", 116 | .Event => "", 117 | .Operator => "", 118 | .TypeParameter => "", 119 | }; 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/tui/WidgetStack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | const eql = std.mem.eql; 5 | 6 | const tp = @import("thespian"); 7 | const Widget = @import("Widget.zig"); 8 | 9 | const Self = @This(); 10 | 11 | allocator: Allocator, 12 | widgets: ArrayList(Widget), 13 | 14 | pub fn init(allocator: Allocator) Self { 15 | return .{ 16 | .allocator = allocator, 17 | .widgets = .empty, 18 | }; 19 | } 20 | 21 | pub fn deinit(self: *Self) void { 22 | for (self.widgets.items) |*widget| 23 | widget.deinit(self.allocator); 24 | self.widgets.deinit(self.allocator); 25 | } 26 | 27 | pub fn add(self: *Self, widget: Widget) !void { 28 | (try self.widgets.addOne(self.allocator)).* = widget; 29 | } 30 | 31 | pub fn swap(self: *Self, n: usize, widget: Widget) Widget { 32 | const old = self.widgets.items[n]; 33 | self.widgets.items[n] = widget; 34 | return old; 35 | } 36 | 37 | pub fn replace(self: *Self, n: usize, widget: Widget) void { 38 | const old = self.swapWidget(n, widget); 39 | old.deinit(self.a); 40 | } 41 | 42 | pub fn remove(self: *Self, w: Widget) void { 43 | for (self.widgets.items, 0..) |p, i| if (p.ptr == w.ptr) 44 | self.widgets.orderedRemove(i).deinit(self.allocator); 45 | } 46 | 47 | pub fn delete(self: *Self, name: []const u8) bool { 48 | for (self.widgets.items, 0..) |*widget, i| { 49 | var buf: [64]u8 = undefined; 50 | const wname = widget.name(&buf); 51 | if (eql(u8, wname, name)) { 52 | self.widgets.orderedRemove(i).deinit(self.a); 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | pub fn find(self: *Self, name: []const u8) ?*Widget { 60 | for (self.widgets.items) |*widget| { 61 | var buf: [64]u8 = undefined; 62 | const wname = widget.name(&buf); 63 | if (eql(u8, wname, name)) 64 | return widget; 65 | } 66 | return null; 67 | } 68 | 69 | pub fn send(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { 70 | for (self.widgets.items) |*widget| 71 | if (try widget.send(from, m)) 72 | return true; 73 | return false; 74 | } 75 | 76 | pub fn update(self: *Self) void { 77 | for (self.widgets.items) |*widget| widget.update(); 78 | } 79 | 80 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 81 | var more = false; 82 | for (self.widgets.items) |*widget| 83 | if (widget.render(theme)) { 84 | more = true; 85 | }; 86 | return more; 87 | } 88 | 89 | pub fn resize(self: *Self, pos: Widget.Box) void { 90 | for (self.widgets.items) |*widget| 91 | widget.resize(pos); 92 | } 93 | 94 | pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool { 95 | const len = self.widgets.items.len; 96 | for (0..len) |i| { 97 | const n = len - i - 1; 98 | const w = &self.widgets.items[n]; 99 | if (w.walk(walk_ctx, f)) return true; 100 | } 101 | return false; 102 | } 103 | 104 | pub fn focus(self: *Self) void { 105 | for (self.widgets.items) |*w| w.widget.focus(); 106 | } 107 | 108 | pub fn unfocus(self: *Self) void { 109 | for (self.widgets.items) |*w| w.widget.unfocus(); 110 | } 111 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/list_all_commands_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | const root = @import("soft_root").root; 5 | const command = @import("command"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | 10 | pub const label = "Search commands"; 11 | pub const name = "󱊒 command"; 12 | pub const description = "command"; 13 | 14 | pub const Entry = struct { 15 | label: []const u8, 16 | hint: []const u8, 17 | id: command.ID, 18 | }; 19 | 20 | pub fn deinit(palette: *Type) void { 21 | for (palette.entries.items) |entry| 22 | palette.allocator.free(entry.label); 23 | } 24 | 25 | pub fn load_entries(palette: *Type) !usize { 26 | const hints = if (tui.input_mode()) |m| m.keybind_hints else @panic("no keybind hints"); 27 | var longest_hint: usize = 0; 28 | for (command.commands.items) |cmd_| if (cmd_) |p| { 29 | var label_: std.Io.Writer.Allocating = .init(palette.allocator); 30 | defer label_.deinit(); 31 | const writer = &label_.writer; 32 | try writer.writeAll(p.name); 33 | if (p.meta.description.len > 0) try writer.print(" ({s})", .{p.meta.description}); 34 | if (p.meta.arguments.len > 0) { 35 | try writer.writeAll(" {"); 36 | var first = true; 37 | for (p.meta.arguments) |arg| { 38 | if (first) { 39 | first = false; 40 | try writer.print("{t}", .{arg}); 41 | } else { 42 | try writer.print(", {t}", .{arg}); 43 | } 44 | } 45 | try writer.writeAll("}"); 46 | } 47 | 48 | const hint = hints.get(p.name) orelse ""; 49 | longest_hint = @max(longest_hint, hint.len); 50 | 51 | (try palette.entries.addOne(palette.allocator)).* = .{ 52 | .label = try label_.toOwnedSlice(), 53 | .hint = hint, 54 | .id = p.id, 55 | }; 56 | }; 57 | return longest_hint; 58 | } 59 | 60 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 61 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 62 | defer value.deinit(); 63 | const writer = &value.writer; 64 | try cbor.writeValue(writer, entry.label); 65 | try cbor.writeValue(writer, entry.hint); 66 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 67 | try cbor.writeValue(writer, entry.id); 68 | try palette.menu.add_item_with_handler(value.written(), select); 69 | palette.items += 1; 70 | } 71 | 72 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 73 | var unused: []const u8 = undefined; 74 | var command_id: command.ID = undefined; 75 | var iter = button.opts.label; 76 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 77 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 78 | var len = cbor.decodeArrayHeader(&iter) catch return; 79 | while (len > 0) : (len -= 1) 80 | cbor.skipValue(&iter) catch break; 81 | if (!(cbor.matchValue(&iter, cbor.extract(&command_id)) catch false)) return; 82 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("command_palette", e); 83 | tp.self_pid().send(.{ "cmd", "paste", .{command.get_name(command_id) orelse return} }) catch {}; 84 | } 85 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/theme_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | 5 | const Widget = @import("../../Widget.zig"); 6 | const tui = @import("../../tui.zig"); 7 | 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | 10 | pub const label = "Search themes"; 11 | pub const name = " theme"; 12 | pub const description = "theme"; 13 | pub const modal_dim = false; 14 | pub const placement = .top_right; 15 | 16 | pub const Entry = struct { 17 | label: []const u8, 18 | name: []const u8, 19 | }; 20 | 21 | pub const Match = struct { 22 | name: []const u8, 23 | score: i32, 24 | matches: []const usize, 25 | }; 26 | 27 | var previous_theme: ?[]const u8 = null; 28 | 29 | pub fn load_entries(palette: *Type) !usize { 30 | var longest_hint: usize = 0; 31 | var idx: usize = 0; 32 | previous_theme = tui.theme().name; 33 | for (Widget.themes) |theme| { 34 | idx += 1; 35 | (try palette.entries.addOne(palette.allocator)).* = .{ 36 | .label = theme.description, 37 | .name = theme.name, 38 | }; 39 | if (previous_theme) |theme_name| if (std.mem.eql(u8, theme.name, theme_name)) { 40 | palette.initial_selected = idx; 41 | }; 42 | longest_hint = @max(longest_hint, theme.name.len); 43 | } 44 | palette.quick_activate_enabled = false; 45 | return longest_hint; 46 | } 47 | 48 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 49 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 50 | defer value.deinit(); 51 | const writer = &value.writer; 52 | try cbor.writeValue(writer, entry.label); 53 | try cbor.writeValue(writer, entry.name); 54 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 55 | try palette.menu.add_item_with_handler(value.written(), select); 56 | palette.items += 1; 57 | } 58 | 59 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 60 | var description_: []const u8 = undefined; 61 | var name_: []const u8 = undefined; 62 | var iter = button.opts.label; 63 | if (!(cbor.matchString(&iter, &description_) catch false)) return; 64 | if (!(cbor.matchString(&iter, &name_) catch false)) return; 65 | if (previous_theme) |prev| if (std.mem.eql(u8, prev, name_)) 66 | return; 67 | tp.self_pid().send(.{ "cmd", "set_theme", .{name_} }) catch |e| menu.*.opts.ctx.logger.err("theme_palette", e); 68 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("theme_palette", e); 69 | } 70 | 71 | pub fn updated(palette: *Type, button_: ?*Type.ButtonType) !void { 72 | const button = button_ orelse return cancel(palette); 73 | var description_: []const u8 = undefined; 74 | var name_: []const u8 = undefined; 75 | var iter = button.opts.label; 76 | if (!(cbor.matchString(&iter, &description_) catch false)) return; 77 | if (!(cbor.matchString(&iter, &name_) catch false)) return; 78 | tp.self_pid().send(.{ "cmd", "set_theme", .{name_} }) catch |e| palette.logger.err("theme_palette upated", e); 79 | } 80 | 81 | pub fn cancel(palette: *Type) !void { 82 | if (previous_theme) |name_| if (!std.mem.eql(u8, name_, tui.theme().name)) { 83 | previous_theme = null; 84 | tp.self_pid().send(.{ "cmd", "set_theme", .{name_} }) catch |e| palette.logger.err("theme_palette cancel", e); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/keybind/parse_flow.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const input = @import("input"); 3 | 4 | pub const ParseError = error{ 5 | OutOfMemory, 6 | InvalidFormat, 7 | }; 8 | 9 | var parse_error_buf: [256]u8 = undefined; 10 | pub var parse_error_message: []const u8 = ""; 11 | 12 | fn parse_error_reset() void { 13 | parse_error_message = ""; 14 | } 15 | 16 | fn parse_error(comptime format: anytype, args: anytype) ParseError { 17 | parse_error_message = std.fmt.bufPrint(&parse_error_buf, format, args) catch "error in parse_error"; 18 | return error.InvalidFormat; 19 | } 20 | 21 | pub fn parse_key_events(allocator: std.mem.Allocator, str: []const u8) ParseError![]input.KeyEvent { 22 | parse_error_reset(); 23 | if (str.len == 0) return parse_error("empty", .{}); 24 | var result_events: std.ArrayList(input.KeyEvent) = .empty; 25 | var iter_sequence = std.mem.tokenizeScalar(u8, str, ' '); 26 | while (iter_sequence.next()) |item| { 27 | var key: ?input.Key = null; 28 | var mods = input.ModSet{}; 29 | var iter = std.mem.tokenizeScalar(u8, item, '+'); 30 | loop: while (iter.next()) |part| { 31 | if (part.len == 0) return parse_error("empty part in '{s}'", .{str}); 32 | const modsInfo = @typeInfo(input.ModSet).@"struct"; 33 | inline for (modsInfo.fields) |field| { 34 | if (std.mem.eql(u8, part, field.name)) { 35 | if (@field(mods, field.name)) return parse_error("duplicate modifier '{s}' in '{s}'", .{ part, str }); 36 | @field(mods, field.name) = true; 37 | continue :loop; 38 | } 39 | } 40 | const alias_mods = .{ 41 | .{ "cmd", "super" }, 42 | .{ "command", "super" }, 43 | .{ "opt", "alt" }, 44 | .{ "option", "alt" }, 45 | .{ "control", "ctrl" }, 46 | }; 47 | inline for (alias_mods) |pair| { 48 | if (std.mem.eql(u8, part, pair[0])) { 49 | if (@field(mods, pair[1])) return parse_error("duplicate modifier '{s}' in '{s}'", .{ part, str }); 50 | @field(mods, pair[1]) = true; 51 | continue :loop; 52 | } 53 | } 54 | 55 | if (key != null) return parse_error("multiple keys in '{s}'", .{str}); 56 | key = input.key.name_map.get(part); 57 | if (key == null) key = name_map.get(part); 58 | if (key == null) unicode: { 59 | const view = std.unicode.Utf8View.init(part) catch break :unicode; 60 | var it = view.iterator(); 61 | const cp = it.nextCodepoint() orelse break :unicode; 62 | if (it.nextCodepoint() != null) break :unicode; 63 | key = cp; 64 | } 65 | if (key == null) return parse_error("unknown key '{s}' in '{s}'", .{ part, str }); 66 | } 67 | if (key) |k| 68 | try result_events.append(allocator, input.KeyEvent.from_key_modset(k, mods)) 69 | else 70 | return parse_error("no key defined in '{s}'", .{str}); 71 | } 72 | return result_events.toOwnedSlice(allocator); 73 | } 74 | 75 | pub const name_map = blk: { 76 | @setEvalBranchQuota(2000); 77 | break :blk std.StaticStringMap(u21).initComptime(.{ 78 | .{ "tab", input.key.tab }, 79 | .{ "enter", input.key.enter }, 80 | .{ "escape", input.key.escape }, 81 | .{ "space", input.key.space }, 82 | .{ "backspace", input.key.backspace }, 83 | .{ "lt", '<' }, 84 | .{ "gt", '>' }, 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /src/tui/mode/mini/goto.zig: -------------------------------------------------------------------------------- 1 | const fmt = @import("std").fmt; 2 | const cbor = @import("cbor"); 3 | const command = @import("command"); 4 | 5 | const tui = @import("../../tui.zig"); 6 | const Cursor = @import("../../editor.zig").Cursor; 7 | 8 | pub const Type = @import("numeric_input.zig").Create(@This()); 9 | pub const create = Type.create; 10 | 11 | pub const ValueType = struct { 12 | cursor: Cursor = .{}, 13 | part: enum { row, col } = .row, 14 | }; 15 | pub const Separator = ':'; 16 | 17 | pub fn name(_: *Type) []const u8 { 18 | return "#goto"; 19 | } 20 | 21 | pub fn start(_: *Type) ValueType { 22 | const editor = tui.get_active_editor() orelse return .{}; 23 | return .{ .cursor = editor.get_primary().cursor }; 24 | } 25 | 26 | pub fn process_digit(self: *Type, digit: u8) void { 27 | const part = if (self.input) |input| input.part else .row; 28 | switch (part) { 29 | .row => switch (digit) { 30 | 0 => { 31 | if (self.input) |*input| input.cursor.row = input.cursor.row * 10; 32 | }, 33 | 1...9 => { 34 | if (self.input) |*input| { 35 | input.cursor.row = input.cursor.row * 10 + digit; 36 | } else { 37 | self.input = .{ .cursor = .{ .row = digit } }; 38 | } 39 | }, 40 | else => unreachable, 41 | }, 42 | .col => if (self.input) |*input| { 43 | input.cursor.col = input.cursor.col * 10 + digit; 44 | }, 45 | } 46 | } 47 | 48 | pub fn process_separator(self: *Type) void { 49 | if (self.input) |*input| switch (input.part) { 50 | .row => input.part = .col, 51 | else => {}, 52 | }; 53 | } 54 | 55 | pub fn delete(self: *Type, input: *ValueType) void { 56 | switch (input.part) { 57 | .row => { 58 | const newval = if (input.cursor.row < 10) 0 else input.cursor.row / 10; 59 | if (newval == 0) self.input = null else input.cursor.row = newval; 60 | }, 61 | .col => { 62 | const newval = if (input.cursor.col < 10) 0 else input.cursor.col / 10; 63 | if (newval == 0) { 64 | input.part = .row; 65 | input.cursor.col = 0; 66 | } else input.cursor.col = newval; 67 | }, 68 | } 69 | } 70 | 71 | pub fn format_value(_: *Type, input: ?ValueType, buf: []u8) []const u8 { 72 | return if (input) |value| blk: { 73 | switch (value.part) { 74 | .row => break :blk fmt.bufPrint(buf, "{d}", .{value.cursor.row}) catch "", 75 | .col => if (value.cursor.col == 0) 76 | break :blk fmt.bufPrint(buf, "{d}:", .{value.cursor.row}) catch "" 77 | else 78 | break :blk fmt.bufPrint(buf, "{d}:{d}", .{ value.cursor.row, value.cursor.col }) catch "", 79 | } 80 | } else ""; 81 | } 82 | 83 | pub const preview = goto; 84 | pub const apply = goto; 85 | pub const cancel = goto; 86 | 87 | const Mode = enum { 88 | goto, 89 | select, 90 | }; 91 | 92 | fn goto(self: *Type, ctx: command.Context) void { 93 | var mode: Mode = .goto; 94 | _ = ctx.args.match(.{cbor.extract(&mode)}) catch {}; 95 | send_goto(mode, if (self.input) |input| input.cursor else self.start.cursor); 96 | } 97 | 98 | fn send_goto(mode: Mode, cursor: Cursor) void { 99 | switch (mode) { 100 | .goto => command.executeName("goto_line_and_column", command.fmt(.{ cursor.row, cursor.col })) catch {}, 101 | .select => command.executeName("select_to_line", command.fmt(.{cursor.row})) catch {}, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/vaxis/Layer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vaxis = @import("vaxis"); 3 | 4 | pub const Plane = @import("Plane.zig"); 5 | const GraphemeCache = @import("GraphemeCache.zig"); 6 | 7 | const Layer = @This(); 8 | 9 | view: View, 10 | y_off: i32 = 0, 11 | x_off: i32 = 0, 12 | plane_: Plane, 13 | 14 | const View = struct { 15 | allocator: std.mem.Allocator, 16 | screen: vaxis.Screen, 17 | cache_storage: GraphemeCache.Storage = .{}, 18 | 19 | pub const Config = struct { 20 | h: u16, 21 | w: u16, 22 | }; 23 | 24 | pub fn init(allocator: std.mem.Allocator, config: Config) std.mem.Allocator.Error!View { 25 | return .{ 26 | .allocator = allocator, 27 | .screen = try vaxis.Screen.init(allocator, .{ 28 | .rows = config.h, 29 | .cols = config.w, 30 | .x_pixel = 0, 31 | .y_pixel = 0, 32 | }), 33 | }; 34 | } 35 | 36 | pub fn deinit(self: *View) void { 37 | self.screen.deinit(self.allocator); 38 | } 39 | }; 40 | 41 | pub const Options = struct { 42 | y: i32 = 0, 43 | x: i32 = 0, 44 | h: u16 = 0, 45 | w: u16 = 0, 46 | }; 47 | 48 | pub fn init(allocator: std.mem.Allocator, opts: Options) std.mem.Allocator.Error!*Layer { 49 | const self = try allocator.create(Layer); 50 | self.* = .{ 51 | .view = try View.init(allocator, .{ 52 | .h = opts.h, 53 | .w = opts.w, 54 | }), 55 | .y_off = opts.y, 56 | .x_off = opts.x, 57 | .plane_ = undefined, 58 | }; 59 | const name = "layer"; 60 | self.plane_ = .{ 61 | .window = self.window(), 62 | .cache = self.view.cache_storage.cache(), 63 | .name_buf = undefined, 64 | .name_len = name.len, 65 | }; 66 | @memcpy(self.plane_.name_buf[0..name.len], name); 67 | return self; 68 | } 69 | 70 | pub fn deinit(self: *Layer) void { 71 | const allocator = self.view.allocator; 72 | self.view.deinit(); 73 | allocator.destroy(self); 74 | } 75 | 76 | fn window(self: *Layer) vaxis.Window { 77 | return .{ 78 | .x_off = 0, 79 | .y_off = 0, 80 | .parent_x_off = 0, 81 | .parent_y_off = 0, 82 | .width = self.view.screen.width, 83 | .height = self.view.screen.height, 84 | .screen = &self.view.screen, 85 | }; 86 | } 87 | 88 | pub fn plane(self: *Layer) *Plane { 89 | return &self.plane_; 90 | } 91 | 92 | pub fn draw(self: *const Layer, plane_: Plane) void { 93 | if (self.x_off >= plane_.window.width) return; 94 | if (self.y_off >= plane_.window.height) return; 95 | 96 | const src_y = 0; 97 | const src_x = 0; 98 | const src_h: usize = self.view.screen.height; 99 | const src_w = self.view.screen.width; 100 | 101 | const dst_dim_y: i32 = @intCast(plane_.dim_y()); 102 | const dst_dim_x: i32 = @intCast(plane_.dim_x()); 103 | const dst_y = self.y_off; 104 | const dst_x = self.x_off; 105 | const dst_w = @min(src_w, dst_dim_x - dst_x); 106 | 107 | for (src_y..src_h) |src_row_| { 108 | const src_row: i32 = @intCast(src_row_); 109 | const src_row_offset = src_row * src_w; 110 | const dst_row_offset = (dst_y + src_row) * plane_.window.screen.width; 111 | if (dst_y + src_row >= dst_dim_y) return; 112 | @memcpy( 113 | plane_.window.screen.buf[@intCast(dst_row_offset + dst_x)..@intCast(dst_row_offset + dst_x + dst_w)], 114 | self.view.screen.buf[@intCast(src_row_offset + src_x)..@intCast(src_row_offset + dst_w)], 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/renderer/vaxis/Cell.zig: -------------------------------------------------------------------------------- 1 | const vaxis = @import("vaxis"); 2 | const Style = @import("theme").Style; 3 | const Color = @import("theme").Color; 4 | const FontStyle = @import("theme").FontStyle; 5 | const color = @import("color"); 6 | 7 | const Cell = @This(); 8 | 9 | cell: vaxis.Cell = .{}, 10 | 11 | pub inline fn set_style(self: *Cell, style_: Style) void { 12 | self.set_style_fg(style_); 13 | self.set_style_bg(style_); 14 | if (style_.fs) |fs| self.set_font_style(fs); 15 | } 16 | 17 | pub inline fn set_font_style(self: *Cell, fs: FontStyle) void { 18 | self.cell.style.ul = .default; 19 | self.cell.style.ul_style = .off; 20 | self.cell.style.bold = false; 21 | self.cell.style.dim = false; 22 | self.cell.style.italic = false; 23 | self.cell.style.blink = false; 24 | self.cell.style.reverse = false; 25 | self.cell.style.invisible = false; 26 | self.cell.style.strikethrough = false; 27 | 28 | switch (fs) { 29 | .normal => {}, 30 | .bold => self.cell.style.bold = true, 31 | .italic => self.cell.style.italic = true, 32 | .underline => self.cell.style.ul_style = .single, 33 | .undercurl => self.cell.style.ul_style = .curly, 34 | .strikethrough => self.cell.style.strikethrough = true, 35 | } 36 | } 37 | 38 | pub inline fn set_under_color(self: *Cell, arg_rgb: c_uint) void { 39 | self.cell.style.ul = vaxis.Cell.Color.rgbFromUint(@intCast(arg_rgb)); 40 | } 41 | 42 | inline fn apply_alpha(base_vaxis: vaxis.Cell.Color, over_theme: Color) vaxis.Cell.Color { 43 | const alpha = over_theme.alpha; 44 | return if (alpha == 0xFF or base_vaxis != .rgb) 45 | vaxis.Cell.Color.rgbFromUint(over_theme.color) 46 | else blk: { 47 | const base = color.RGB.from_u8s(base_vaxis.rgb); 48 | const over = color.RGB.from_u24(over_theme.color); 49 | const result = color.apply_alpha(base, over, alpha); 50 | break :blk .{ .rgb = result.to_u8s() }; 51 | }; 52 | } 53 | 54 | pub inline fn set_style_fg(self: *Cell, style_: Style) void { 55 | if (style_.fg) |fg| self.cell.style.fg = apply_alpha(self.cell.style.bg, fg); 56 | } 57 | 58 | pub inline fn set_style_bg_opaque(self: *Cell, style_: Style) void { 59 | if (style_.bg) |bg| self.cell.style.bg = vaxis.Cell.Color.rgbFromUint(bg.color); 60 | } 61 | 62 | pub inline fn set_style_bg(self: *Cell, style_: Style) void { 63 | if (style_.bg) |bg| self.cell.style.bg = apply_alpha(self.cell.style.bg, bg); 64 | } 65 | 66 | pub inline fn set_fg_rgb(self: *Cell, arg_rgb: c_uint) !void { 67 | self.cell.style.fg = vaxis.Cell.Color.rgbFromUint(@intCast(arg_rgb)); 68 | } 69 | pub inline fn set_bg_rgb(self: *Cell, arg_rgb: c_uint) !void { 70 | self.cell.style.bg = vaxis.Cell.Color.rgbFromUint(@intCast(arg_rgb)); 71 | } 72 | 73 | pub fn columns(self: *const Cell) usize { 74 | // return if (self.cell.char.width == 0) self.window.gwidth(self.cell.char.grapheme) else self.cell.char.width; // FIXME? 75 | return self.cell.char.width; 76 | } 77 | 78 | pub fn dim(self: *Cell, alpha: u8) void { 79 | self.dim_fg(alpha); 80 | self.dim_bg(alpha); 81 | } 82 | 83 | pub fn dim_bg(self: *Cell, alpha: u8) void { 84 | self.cell.style.bg = apply_alpha_value(self.cell.style.bg, alpha); 85 | } 86 | 87 | pub fn dim_fg(self: *Cell, alpha: u8) void { 88 | self.cell.style.fg = apply_alpha_value(self.cell.style.fg, alpha); 89 | } 90 | 91 | fn apply_alpha_value(c: vaxis.Cell.Color, a: u8) vaxis.Cell.Color { 92 | var rgb = if (c == .rgb) c.rgb else return c; 93 | rgb[0] = @intCast((@as(u32, @intCast(rgb[0])) * a) / 256); 94 | rgb[1] = @intCast((@as(u32, @intCast(rgb[1])) * a) / 256); 95 | rgb[2] = @intCast((@as(u32, @intCast(rgb[2])) * a) / 256); 96 | return .{ .rgb = rgb }; 97 | } 98 | -------------------------------------------------------------------------------- /src/text_manip.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const TextWriter = std.ArrayList(u8).Writer; 3 | 4 | pub fn find_first_non_ws(text: []const u8) ?usize { 5 | for (text, 0..) |c, i| if (c == ' ' or c == '\t') continue else return i; 6 | return null; 7 | } 8 | 9 | pub fn find_prefix(prefix: []const u8, text: []const u8) ?usize { 10 | var start: usize = 0; 11 | var pos: usize = 0; 12 | var in_prefix: bool = false; 13 | for (text, 0..) |c, i| { 14 | if (!in_prefix) { 15 | if (c == ' ' or c == '\t') 16 | continue 17 | else { 18 | in_prefix = true; 19 | start = i; 20 | } 21 | } 22 | 23 | if (in_prefix) { 24 | if (c == prefix[pos]) { 25 | pos += 1; 26 | if (prefix.len > pos) continue else return start; 27 | } else return null; 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | fn add_prefix_in_line(prefix: []const u8, text: []const u8, writer: TextWriter, pos: usize) !void { 34 | if (text.len >= pos and find_first_non_ws(text) != null) { 35 | _ = try writer.write(text[0..pos]); 36 | _ = try writer.write(prefix); 37 | _ = try writer.write(" "); 38 | _ = try writer.write(text[pos..]); 39 | } else { 40 | _ = try writer.write(text); 41 | } 42 | } 43 | 44 | fn remove_prefix_in_line(prefix: []const u8, text: []const u8, writer: TextWriter) !void { 45 | if (find_prefix(prefix, text)) |pos| { 46 | _ = try writer.write(text[0..pos]); 47 | if (text.len > pos + prefix.len) { 48 | _ = try if (text[pos + prefix.len] == ' ') 49 | writer.write(text[pos + 1 + prefix.len ..]) 50 | else 51 | writer.write(text[pos + prefix.len ..]); 52 | } 53 | } else { 54 | _ = try writer.write(text); 55 | } 56 | } 57 | 58 | pub fn toggle_prefix_in_text(prefix: []const u8, text: []const u8, allocator: std.mem.Allocator) ![]const u8 { 59 | var result = try std.ArrayList(u8).initCapacity(allocator, prefix.len + text.len); 60 | const writer = result.writer(allocator); 61 | var pos: usize = 0; 62 | var prefix_pos: usize = std.math.maxInt(usize); 63 | var have_prefix = true; 64 | while (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| { 65 | if (find_prefix(prefix, text[pos..next])) |_| {} else { 66 | if (find_first_non_ws(text[pos..next])) |_| { 67 | have_prefix = false; 68 | break; 69 | } 70 | } 71 | pos = next + 1; 72 | } 73 | pos = 0; 74 | if (!have_prefix) 75 | while (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| { 76 | if (find_first_non_ws(text[pos..next])) |prefix_pos_| 77 | prefix_pos = @min(prefix_pos, prefix_pos_); 78 | pos = next + 1; 79 | }; 80 | pos = 0; 81 | while (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| { 82 | if (have_prefix) { 83 | try remove_prefix_in_line(prefix, text[pos..next], writer); 84 | } else { 85 | try add_prefix_in_line(prefix, text[pos..next], writer, prefix_pos); 86 | } 87 | _ = try writer.write("\n"); 88 | pos = next + 1; 89 | } 90 | return result.toOwnedSlice(allocator); 91 | } 92 | 93 | pub fn write_string(writer: *std.Io.Writer, string: []const u8, pad: ?usize) !void { 94 | try writer.writeAll(string); 95 | if (pad) |pad_| try write_padding(writer, string.len, pad_); 96 | } 97 | 98 | pub fn write_padding(writer: *std.Io.Writer, len: usize, pad_len: usize) !void { 99 | for (0..pad_len - len) |_| try writer.writeAll(" "); 100 | } 101 | -------------------------------------------------------------------------------- /src/tui/mode/mini/get_char.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | 4 | const input = @import("input"); 5 | const key = @import("renderer").input.key; 6 | const mod = @import("renderer").input.modifier; 7 | const event_type = @import("renderer").input.event_type; 8 | const keybind = @import("keybind"); 9 | const command = @import("command"); 10 | const EventHandler = @import("EventHandler"); 11 | 12 | const tui = @import("../../tui.zig"); 13 | 14 | const Allocator = @import("std").mem.Allocator; 15 | const fmt = @import("std").fmt; 16 | 17 | pub fn Create(options: type) type { 18 | return struct { 19 | const Self = @This(); 20 | 21 | const Commands = command.Collection(cmds); 22 | 23 | const ValueType = if (@hasDecl(options, "ValueType")) options.ValueType else void; 24 | 25 | allocator: Allocator, 26 | input: ?ValueType = null, 27 | value: ValueType, 28 | ctx: command.Context, 29 | commands: Commands = undefined, 30 | 31 | pub fn create(allocator: Allocator, ctx: command.Context) !struct { tui.Mode, tui.MiniMode } { 32 | const self = try allocator.create(Self); 33 | errdefer allocator.destroy(self); 34 | self.* = .{ 35 | .allocator = allocator, 36 | .ctx = .{ .args = try ctx.args.clone(allocator) }, 37 | .value = if (@hasDecl(options, "start")) options.start(self) else {}, 38 | }; 39 | try self.commands.init(self); 40 | var mode = try keybind.mode("mini/get_char", allocator, .{ 41 | .insert_command = "mini_mode_insert_bytes", 42 | }); 43 | mode.event_handler = EventHandler.to_owned(self); 44 | return .{ mode, .{ .name = options.name(self) } }; 45 | } 46 | 47 | pub fn deinit(self: *Self) void { 48 | if (@hasDecl(options, "deinit")) 49 | options.deinit(self); 50 | self.allocator.free(self.ctx.args.buf); 51 | self.commands.deinit(); 52 | self.allocator.destroy(self); 53 | } 54 | 55 | pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { 56 | return false; 57 | } 58 | 59 | const cmds = struct { 60 | pub const Target = Self; 61 | const Ctx = command.Context; 62 | const Meta = command.Metadata; 63 | const Result = command.Result; 64 | 65 | pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { 66 | var code_point: u32 = 0; 67 | if (!try ctx.args.match(.{tp.extract(&code_point)})) 68 | return error.InvalidMoveToCharInsertCodePointArgument; 69 | var buf: [6]u8 = undefined; 70 | const bytes = input.ucs32_to_utf8(&[_]u32{code_point}, &buf) catch return error.InvalidMoveToCharCodePoint; 71 | return options.process_egc(self, buf[0..bytes]); 72 | } 73 | pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; 74 | 75 | pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { 76 | var bytes: []const u8 = undefined; 77 | if (!try ctx.args.match(.{tp.extract(&bytes)})) 78 | return error.InvalidMoveToCharInsertBytesArgument; 79 | const egc = tui.egc_last(bytes); 80 | var buf: [6]u8 = undefined; 81 | @memcpy(buf[0..egc.len], egc); 82 | return options.process_egc(self, buf[0..egc.len]); 83 | } 84 | pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; 85 | 86 | pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { 87 | command.executeName("exit_mini_mode", .{}) catch {}; 88 | } 89 | pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; 90 | }; 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/tui/status/selectionstate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const tp = @import("thespian"); 4 | const tracy = @import("tracy"); 5 | const EventHandler = @import("EventHandler"); 6 | const Plane = @import("renderer").Plane; 7 | 8 | const Widget = @import("../Widget.zig"); 9 | const ed = @import("../editor.zig"); 10 | 11 | plane: Plane, 12 | matches: usize = 0, 13 | cursels: usize = 0, 14 | selection: ?ed.Selection = null, 15 | buf: [256]u8 = undefined, 16 | rendered: [:0]const u8 = "", 17 | on_event: ?EventHandler, 18 | 19 | const Self = @This(); 20 | 21 | pub fn create(allocator: Allocator, parent: Plane, event_handler: ?EventHandler, _: ?[]const u8) @import("widget.zig").CreateError!Widget { 22 | const self = try allocator.create(Self); 23 | errdefer allocator.destroy(self); 24 | self.* = .{ 25 | .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), 26 | .on_event = event_handler, 27 | }; 28 | return Widget.to(self); 29 | } 30 | 31 | pub fn deinit(self: *Self, allocator: Allocator) void { 32 | self.plane.deinit(); 33 | allocator.destroy(self); 34 | } 35 | 36 | pub fn layout(self: *Self) Widget.Layout { 37 | return .{ .static = self.rendered.len }; 38 | } 39 | 40 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 41 | const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" }); 42 | defer frame.deinit(); 43 | self.plane.set_base_style(theme.editor); 44 | self.plane.erase(); 45 | self.plane.home(); 46 | self.plane.set_style(theme.statusbar); 47 | self.plane.fill(" "); 48 | self.plane.home(); 49 | _ = self.plane.putstr(self.rendered) catch {}; 50 | return false; 51 | } 52 | 53 | fn format(self: *Self) void { 54 | var fbs = std.io.fixedBufferStream(&self.buf); 55 | const writer = fbs.writer(); 56 | _ = writer.write(" ") catch {}; 57 | if (self.matches > 1) { 58 | std.fmt.format(writer, "({d} matches)", .{self.matches}) catch {}; 59 | if (self.selection) |_| 60 | _ = writer.write(" ") catch {}; 61 | } 62 | if (self.cursels > 1) { 63 | std.fmt.format(writer, "({d} cursors)", .{self.cursels}) catch {}; 64 | if (self.selection) |_| 65 | _ = writer.write(" ") catch {}; 66 | } 67 | if (self.selection) |sel_| { 68 | var sel = sel_; 69 | sel.normalize(); 70 | const lines = sel.end.row - sel.begin.row; 71 | if (lines == 0) { 72 | std.fmt.format(writer, "({d} columns selected)", .{sel.end.col - sel.begin.col}) catch {}; 73 | } else { 74 | std.fmt.format(writer, "({d} lines selected)", .{if (sel.end.col == 0) lines else lines + 1}) catch {}; 75 | } 76 | } 77 | _ = writer.write(" ") catch {}; 78 | self.rendered = @ptrCast(fbs.getWritten()); 79 | self.buf[self.rendered.len] = 0; 80 | } 81 | 82 | pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { 83 | var btn: u32 = 0; 84 | if (try m.match(.{ "D", tp.any, tp.extract(&btn), tp.more })) { 85 | if (self.on_event) |h| h.send(from, m) catch {}; 86 | return true; 87 | } 88 | if (try m.match(.{ "E", "match", tp.extract(&self.matches) })) 89 | self.format(); 90 | if (try m.match(.{ "E", "cursels", tp.extract(&self.cursels) })) 91 | self.format(); 92 | if (try m.match(.{ "E", "close" })) { 93 | self.matches = 0; 94 | self.selection = null; 95 | self.format(); 96 | } else if (try m.match(.{ "E", "sel", tp.more })) { 97 | var sel: ed.Selection = undefined; 98 | if (try m.match(.{ tp.any, tp.any, "none" })) { 99 | self.matches = 0; 100 | self.selection = null; 101 | } else if (try m.match(.{ tp.any, tp.any, tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) })) { 102 | self.selection = sel; 103 | } 104 | self.format(); 105 | } 106 | return false; 107 | } 108 | -------------------------------------------------------------------------------- /src/file_link.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const root = @import("soft_root").root; 4 | 5 | pub const Dest = union(enum) { 6 | file: FileDest, 7 | dir: DirDest, 8 | }; 9 | 10 | pub const FileDest = struct { 11 | path: []const u8, 12 | line: ?usize = null, 13 | column: ?usize = null, 14 | end_column: ?usize = null, 15 | exists: bool = false, 16 | offset: ?usize = null, 17 | }; 18 | 19 | pub const DirDest = struct { 20 | path: []const u8, 21 | }; 22 | 23 | pub fn parse(link: []const u8) error{InvalidFileLink}!Dest { 24 | if (link.len == 0) return error.InvalidFileLink; 25 | 26 | if (std.mem.lastIndexOfScalar(u8, link, '(')) |pos| blk: { 27 | for (link[pos + 1 ..]) |c| switch (c) { 28 | '0'...'9', ',', ')', ':', ' ' => continue, 29 | else => break :blk, 30 | }; 31 | return parse_bracket_link(link); 32 | } 33 | 34 | var it = std.mem.splitScalar(u8, link, ':'); 35 | var dest: Dest = if (root.is_directory(link)) 36 | .{ .dir = .{ .path = link } } 37 | else 38 | .{ .file = .{ .path = it.first() } }; 39 | switch (dest) { 40 | .file => |*file| { 41 | if (it.next()) |line_| if (line_.len > 0 and line_[0] == 'b') { 42 | file.offset = std.fmt.parseInt(usize, line_[1..], 10) catch blk: { 43 | file.path = link; 44 | break :blk null; 45 | }; 46 | } else { 47 | file.line = std.fmt.parseInt(usize, line_, 10) catch blk: { 48 | file.path = link; 49 | break :blk null; 50 | }; 51 | }; 52 | if (file.line) |_| if (it.next()) |col_| { 53 | file.column = std.fmt.parseInt(usize, col_, 10) catch null; 54 | }; 55 | if (file.column) |_| if (it.next()) |col_| { 56 | file.end_column = std.fmt.parseInt(usize, col_, 10) catch null; 57 | }; 58 | file.exists = root.is_file(file.path); 59 | }, 60 | .dir => {}, 61 | } 62 | return dest; 63 | } 64 | 65 | pub fn parse_bracket_link(link: []const u8) error{InvalidFileLink}!Dest { 66 | var it_ = std.mem.splitScalar(u8, link, '('); 67 | var dest: Dest = if (root.is_directory(link)) 68 | .{ .dir = .{ .path = link } } 69 | else 70 | .{ .file = .{ .path = it_.first() } }; 71 | 72 | const rest = it_.next() orelse ""; 73 | var it = std.mem.splitAny(u8, rest, ",):"); 74 | 75 | switch (dest) { 76 | .file => |*file| { 77 | if (it.next()) |line_| 78 | file.line = std.fmt.parseInt(usize, line_, 10) catch blk: { 79 | file.path = link; 80 | break :blk null; 81 | }; 82 | if (file.line) |_| if (it.next()) |col_| { 83 | file.column = std.fmt.parseInt(usize, col_, 10) catch null; 84 | }; 85 | if (file.column) |_| if (it.next()) |col_| { 86 | file.end_column = std.fmt.parseInt(usize, col_, 10) catch null; 87 | }; 88 | file.exists = root.is_file(file.path); 89 | }, 90 | .dir => {}, 91 | } 92 | return dest; 93 | } 94 | 95 | pub fn navigate(to: tp.pid_ref, link: *const Dest) anyerror!void { 96 | switch (link.*) { 97 | .file => |file| { 98 | if (file.offset) |offset| { 99 | return to.send(.{ "cmd", "navigate", .{ .file = file.path, .offset = offset } }); 100 | } 101 | if (file.line) |l| { 102 | if (file.column) |col| { 103 | try to.send(.{ "cmd", "navigate", .{ .file = file.path, .line = l, .column = col } }); 104 | if (file.end_column) |end| 105 | try to.send(.{ "A", l, col - 1, end - 1 }); 106 | return; 107 | } 108 | return to.send(.{ "cmd", "navigate", .{ .file = file.path, .line = l } }); 109 | } 110 | return to.send(.{ "cmd", "navigate", .{ .file = file.path } }); 111 | }, 112 | else => {}, 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/win32/DwriteRenderer.zig: -------------------------------------------------------------------------------- 1 | const DwriteRenderer = @This(); 2 | 3 | const std = @import("std"); 4 | const win32 = @import("win32").everything; 5 | const win32ext = @import("win32ext.zig"); 6 | 7 | const dwrite = @import("dwrite.zig"); 8 | const XY = @import("xy.zig").XY; 9 | 10 | pub const Font = dwrite.Font; 11 | pub const Fonts = dwrite.Fonts; 12 | 13 | pub const needs_direct2d = true; 14 | 15 | render_target: *win32.ID2D1RenderTarget, 16 | white_brush: *win32.ID2D1SolidColorBrush, 17 | pub fn init( 18 | d2d_factory: *win32.ID2D1Factory, 19 | texture: *win32.ID3D11Texture2D, 20 | ) DwriteRenderer { 21 | const dxgi_surface = win32ext.queryInterface(texture, win32.IDXGISurface); 22 | defer _ = dxgi_surface.IUnknown.Release(); 23 | 24 | var render_target: *win32.ID2D1RenderTarget = undefined; 25 | { 26 | const props = win32.D2D1_RENDER_TARGET_PROPERTIES{ 27 | .type = .DEFAULT, 28 | .pixelFormat = .{ 29 | .format = .A8_UNORM, 30 | .alphaMode = .PREMULTIPLIED, 31 | }, 32 | .dpiX = 0, 33 | .dpiY = 0, 34 | .usage = .{}, 35 | .minLevel = .DEFAULT, 36 | }; 37 | const hr = d2d_factory.CreateDxgiSurfaceRenderTarget( 38 | dxgi_surface, 39 | &props, 40 | &render_target, 41 | ); 42 | if (hr < 0) fatalHr("CreateDxgiSurfaceRenderTarget", hr); 43 | } 44 | errdefer _ = render_target.IUnknown.Release(); 45 | 46 | { 47 | const dc = win32ext.queryInterface(render_target, win32.ID2D1DeviceContext); 48 | defer _ = dc.IUnknown.Release(); 49 | dc.SetUnitMode(win32.D2D1_UNIT_MODE_PIXELS); 50 | } 51 | 52 | var white_brush: *win32.ID2D1SolidColorBrush = undefined; 53 | { 54 | const hr = render_target.CreateSolidColorBrush( 55 | &.{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }, 56 | null, 57 | &white_brush, 58 | ); 59 | if (hr < 0) fatalHr("CreateSolidColorBrush", hr); 60 | } 61 | errdefer _ = white_brush.IUnknown.Release(); 62 | 63 | return .{ 64 | .render_target = render_target, 65 | .white_brush = white_brush, 66 | }; 67 | } 68 | pub fn deinit(self: *DwriteRenderer) void { 69 | _ = self.white_brush.IUnknown.Release(); 70 | _ = self.render_target.IUnknown.Release(); 71 | self.* = undefined; 72 | } 73 | 74 | pub fn render( 75 | self: *const DwriteRenderer, 76 | font: Font, 77 | utf8: []const u8, 78 | double_width: bool, 79 | ) void { 80 | var utf16_buf: [10]u16 = undefined; 81 | const utf16_len = std.unicode.utf8ToUtf16Le(&utf16_buf, utf8) catch unreachable; 82 | const utf16 = utf16_buf[0..utf16_len]; 83 | std.debug.assert(utf16.len <= 2); 84 | 85 | { 86 | const rect: win32.D2D_RECT_F = .{ 87 | .left = 0, 88 | .top = 0, 89 | .right = if (double_width) 90 | @as(f32, @floatFromInt(font.cell_size.x)) * 2 91 | else 92 | @as(f32, @floatFromInt(font.cell_size.x)), 93 | .bottom = @floatFromInt(font.cell_size.y), 94 | }; 95 | self.render_target.BeginDraw(); 96 | { 97 | const color: win32.D2D_COLOR_F = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; 98 | self.render_target.Clear(&color); 99 | } 100 | self.render_target.DrawText( 101 | @ptrCast(utf16.ptr), 102 | @intCast(utf16.len), 103 | if (double_width) font.text_format_double else font.text_format_single, 104 | &rect, 105 | &self.white_brush.ID2D1Brush, 106 | .{}, 107 | .NATURAL, 108 | ); 109 | var tag1: u64 = undefined; 110 | var tag2: u64 = undefined; 111 | const hr = self.render_target.EndDraw(&tag1, &tag2); 112 | if (hr < 0) std.debug.panic( 113 | "D2D DrawText failed, hresult=0x{x}, tag1={}, tag2={}", 114 | .{ @as(u32, @bitCast(hr)), tag1, tag2 }, 115 | ); 116 | } 117 | } 118 | 119 | fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { 120 | std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); 121 | } 122 | -------------------------------------------------------------------------------- /src/color.zig: -------------------------------------------------------------------------------- 1 | const pow = @import("std").math.pow; 2 | 3 | pub const RGB = struct { 4 | r: u8, 5 | g: u8, 6 | b: u8, 7 | 8 | pub inline fn from_u24(v: u24) RGB { 9 | const r = @as(u8, @intCast(v >> 16 & 0xFF)); 10 | const g = @as(u8, @intCast(v >> 8 & 0xFF)); 11 | const b = @as(u8, @intCast(v & 0xFF)); 12 | return .{ .r = r, .g = g, .b = b }; 13 | } 14 | 15 | pub inline fn to_u24(v: RGB) u24 { 16 | const r = @as(u24, @intCast(v.r)) << 16; 17 | const g = @as(u24, @intCast(v.g)) << 8; 18 | const b = @as(u24, @intCast(v.b)); 19 | return r | b | g; 20 | } 21 | 22 | pub inline fn from_u8s(v: [3]u8) RGB { 23 | return .{ .r = v[0], .g = v[1], .b = v[2] }; 24 | } 25 | 26 | pub fn from_string(s: []const u8) ?RGB { 27 | const nib = struct { 28 | fn f(c: u8) ?u8 { 29 | return switch (c) { 30 | '0'...'9' => c - '0', 31 | 'A'...'F' => c - 'A' + 10, 32 | 'a'...'f' => c - 'a' + 10, 33 | else => null, 34 | }; 35 | } 36 | }.f; 37 | 38 | if (s.len != 7) return null; 39 | if (s[0] != '#') return null; 40 | const r = (nib(s[1]) orelse return null) << 4 | (nib(s[2]) orelse return null); 41 | const g = (nib(s[3]) orelse return null) << 4 | (nib(s[4]) orelse return null); 42 | const b = (nib(s[5]) orelse return null) << 4 | (nib(s[6]) orelse return null); 43 | return .{ .r = r, .g = g, .b = b }; 44 | } 45 | 46 | pub fn to_u8s(v: RGB) [3]u8 { 47 | return [_]u8{ v.r, v.g, v.b }; 48 | } 49 | 50 | pub fn to_string(v: RGB, s: *[7]u8) []u8 { 51 | const nib = struct { 52 | fn f(n: u8) u8 { 53 | return switch (n) { 54 | 0...9 => '0' + n, 55 | 0xA...0xF => 'A' + n - 10, 56 | else => unreachable, 57 | }; 58 | } 59 | }.f; 60 | 61 | s[0] = '#'; 62 | s[1] = nib(v.r >> 4); 63 | s[2] = nib(v.r & 0b00001111); 64 | s[3] = nib(v.g >> 4); 65 | s[4] = nib(v.g & 0b00001111); 66 | s[5] = nib(v.b >> 4); 67 | s[6] = nib(v.b & 0b00001111); 68 | return s; 69 | } 70 | 71 | pub fn contrast(a_: RGB, b_: RGB) f32 { 72 | const a = RGBf.from_RGB(a_).luminance(); 73 | const b = RGBf.from_RGB(b_).luminance(); 74 | return (@max(a, b) + 0.05) / (@min(a, b) + 0.05); 75 | } 76 | 77 | pub fn max_contrast(v: RGB, a: RGB, b: RGB) RGB { 78 | return if (contrast(v, a) > contrast(v, b)) a else b; 79 | } 80 | }; 81 | 82 | pub const RGBf = struct { 83 | r: f32, 84 | g: f32, 85 | b: f32, 86 | 87 | pub inline fn from_RGB(v: RGB) RGBf { 88 | return .{ .r = tof(v.r), .g = tof(v.g), .b = tof(v.b) }; 89 | } 90 | 91 | pub fn luminance(v: RGBf) f32 { 92 | return linear(v.r) * RED + linear(v.g) * GREEN + linear(v.b) * BLUE; 93 | } 94 | 95 | inline fn tof(c: u8) f32 { 96 | return @as(f32, @floatFromInt(c)) / 255.0; 97 | } 98 | 99 | inline fn linear(v: f32) f32 { 100 | return if (v <= 0.03928) v / 12.92 else pow(f32, (v + 0.055) / 1.055, GAMMA); 101 | } 102 | 103 | const RED = 0.2126; 104 | const GREEN = 0.7152; 105 | const BLUE = 0.0722; 106 | const GAMMA = 2.4; 107 | }; 108 | 109 | pub fn max_contrast(v: u24, a: u24, b: u24) u24 { 110 | return RGB.max_contrast(RGB.from_u24(v), RGB.from_u24(a), RGB.from_u24(b)).to_u24(); 111 | } 112 | 113 | pub fn apply_alpha(base: RGB, over: RGB, alpha_u8: u8) RGB { 114 | const alpha: f64 = @as(f64, @floatFromInt(alpha_u8)) / @as(f64, @floatFromInt(0xFF)); 115 | return .{ 116 | .r = component_apply_alpha(base.r, over.r, alpha), 117 | .g = component_apply_alpha(base.g, over.g, alpha), 118 | .b = component_apply_alpha(base.b, over.b, alpha), 119 | }; 120 | } 121 | 122 | inline fn component_apply_alpha(base_u8: u8, over_u8: u8, alpha: f64) u8 { 123 | const base: f64 = @floatFromInt(base_u8); 124 | const over: f64 = @floatFromInt(over_u8); 125 | const result = ((1 - alpha) * base) + (alpha * over); 126 | return @intFromFloat(result); 127 | } 128 | -------------------------------------------------------------------------------- /src/tui/status/clock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const cbor = @import("cbor"); 4 | const zeit = @import("zeit"); 5 | 6 | const EventHandler = @import("EventHandler"); 7 | const Plane = @import("renderer").Plane; 8 | 9 | const Widget = @import("../Widget.zig"); 10 | const MessageFilter = @import("../MessageFilter.zig"); 11 | const tui = @import("../tui.zig"); 12 | const fonts = @import("../fonts.zig"); 13 | 14 | const DigitStyle = fonts.DigitStyle; 15 | 16 | allocator: std.mem.Allocator, 17 | plane: Plane, 18 | tick_timer: ?tp.Cancellable = null, 19 | on_event: ?EventHandler, 20 | tz: zeit.timezone.TimeZone, 21 | style: ?DigitStyle, 22 | 23 | const Self = @This(); 24 | 25 | pub fn create(allocator: std.mem.Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) @import("widget.zig").CreateError!Widget { 26 | const style: ?DigitStyle = if (arg) |style| std.meta.stringToEnum(DigitStyle, style) orelse null else null; 27 | 28 | var env = std.process.getEnvMap(allocator) catch |e| { 29 | std.log.err("clock: std.process.getEnvMap failed with {any}", .{e}); 30 | return error.WidgetInitFailed; 31 | }; 32 | defer env.deinit(); 33 | const self = try allocator.create(Self); 34 | errdefer allocator.destroy(self); 35 | self.* = .{ 36 | .allocator = allocator, 37 | .plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent), 38 | .on_event = event_handler, 39 | .tz = zeit.local(allocator, &env) catch |e| { 40 | std.log.err("clock: zeit.local failed with {any}", .{e}); 41 | return error.WidgetInitFailed; 42 | }, 43 | .style = style, 44 | }; 45 | try tui.message_filters().add(MessageFilter.bind(self, receive_tick)); 46 | self.update_tick_timer(.init); 47 | return Widget.to(self); 48 | } 49 | 50 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 51 | tui.message_filters().remove_ptr(self); 52 | if (self.tick_timer) |*t| { 53 | t.cancel() catch {}; 54 | t.deinit(); 55 | self.tick_timer = null; 56 | } 57 | self.tz.deinit(); 58 | self.plane.deinit(); 59 | allocator.destroy(self); 60 | } 61 | 62 | pub fn receive(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool { 63 | var btn: u32 = 0; 64 | if (try m.match(.{ "D", tp.any, tp.extract(&btn), tp.more })) { 65 | if (self.on_event) |h| h.send(from, m) catch {}; 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | pub fn layout(_: *Self) Widget.Layout { 72 | return .{ .static = 5 }; 73 | } 74 | 75 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 76 | self.plane.set_base_style(theme.editor); 77 | self.plane.erase(); 78 | self.plane.home(); 79 | self.plane.set_style(theme.statusbar); 80 | self.plane.fill(" "); 81 | self.plane.home(); 82 | 83 | const now = zeit.instant(.{ .timezone = &self.tz }) catch return false; 84 | const dt = now.time(); 85 | 86 | var buf: [64]u8 = undefined; 87 | var fbs = std.io.fixedBufferStream(&buf); 88 | const writer = fbs.writer(); 89 | std.fmt.format(writer, "{d:0>2}:{d:0>2}", .{ dt.hour, dt.minute }) catch {}; 90 | 91 | const value_str = fbs.getWritten(); 92 | for (value_str, 0..) |_, i| _ = self.plane.putstr(fonts.get_digit_ascii(value_str[i .. i + 1], self.style orelse .ascii)) catch {}; 93 | return false; 94 | } 95 | 96 | fn receive_tick(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { 97 | if (try cbor.match(m.buf, .{"CLOCK"})) { 98 | tui.need_render(); 99 | self.update_tick_timer(.ticked); 100 | return true; 101 | } 102 | return false; 103 | } 104 | 105 | fn update_tick_timer(self: *Self, event: enum { init, ticked }) void { 106 | if (self.tick_timer) |*t| { 107 | if (event != .ticked) t.cancel() catch {}; 108 | t.deinit(); 109 | self.tick_timer = null; 110 | } 111 | const current = zeit.instant(.{ .timezone = &self.tz }) catch return; 112 | var next = current.time(); 113 | next.minute += 1; 114 | next.second = 0; 115 | next.millisecond = 0; 116 | next.microsecond = 0; 117 | next.nanosecond = 0; 118 | const delay_us: u64 = @intCast(@divTrunc(next.instant().timestamp - current.timestamp, std.time.ns_per_us)); 119 | self.tick_timer = tp.self_pid().delay_send_cancellable(self.allocator, "clock.tick_timer", delay_us, .{"CLOCK"}) catch null; 120 | } 121 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/clipboard_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | const root = @import("soft_root").root; 5 | const command = @import("command"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | const module_name = @typeName(@This()); 10 | 11 | pub const label = "Clipboard history"; 12 | pub const name = " clipboard"; 13 | pub const description = "clipboard"; 14 | pub const icon = " "; 15 | 16 | pub const Entry = struct { 17 | label: []const u8, 18 | idx: usize, 19 | group: usize, 20 | }; 21 | 22 | pub fn load_entries(palette: *Type) !usize { 23 | const history = tui.clipboard_get_history() orelse &.{}; 24 | 25 | if (history.len > 0) { 26 | var idx = history.len - 1; 27 | while (true) : (idx -= 1) { 28 | const entry = &history[idx]; 29 | var label_ = entry.text; 30 | while (label_.len > 0) switch (label_[0]) { 31 | ' ', '\t', '\n' => label_ = label_[1..], 32 | else => break, 33 | }; 34 | (try palette.entries.addOne(palette.allocator)).* = .{ 35 | .label = label_, 36 | .idx = idx, 37 | .group = entry.group, 38 | }; 39 | if (idx == 0) break; 40 | } 41 | } 42 | return if (palette.entries.items.len == 0) label.len + 3 else 10; 43 | } 44 | 45 | pub fn clear_entries(palette: *Type) void { 46 | palette.entries.clearRetainingCapacity(); 47 | } 48 | 49 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 50 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 51 | defer value.deinit(); 52 | const writer = &value.writer; 53 | try cbor.writeValue(writer, entry.label); 54 | 55 | var hint: std.Io.Writer.Allocating = .init(palette.allocator); 56 | defer hint.deinit(); 57 | const clipboard_ = tui.clipboard_get_history(); 58 | const clipboard = clipboard_ orelse &.{}; 59 | const clipboard_entry: tui.ClipboardEntry = if (clipboard_) |_| clipboard[entry.idx] else .{}; 60 | const group_idx = tui.clipboard_current_group() - clipboard_entry.group; 61 | const item = clipboard_entry.text; 62 | var line_count: usize = 1; 63 | for (0..item.len) |i| if (item[i] == '\n') { 64 | line_count += 1; 65 | }; 66 | if (line_count > 1) 67 | try hint.writer.print(" {d} lines", .{line_count}) 68 | else 69 | try hint.writer.print(" {d} {s}", .{ item.len, if (item.len == 1) "byte " else "bytes" }); 70 | try hint.writer.print(" :{d}", .{group_idx}); 71 | try cbor.writeValue(writer, hint.written()); 72 | 73 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 74 | try cbor.writeValue(writer, entry.idx); 75 | try palette.menu.add_item_with_handler(value.written(), select); 76 | palette.items += 1; 77 | } 78 | 79 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 80 | var unused: []const u8 = undefined; 81 | var idx: usize = undefined; 82 | var iter = button.opts.label; 83 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 84 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 85 | var len = cbor.decodeArrayHeader(&iter) catch return; 86 | while (len > 0) : (len -= 1) 87 | cbor.skipValue(&iter) catch return; 88 | if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return; 89 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("clipboard_palette", e); 90 | 91 | const history = tui.clipboard_get_history() orelse return; 92 | if (history.len <= idx) return; 93 | tp.self_pid().send(.{ "cmd", "paste", .{history[idx].text} }) catch {}; 94 | } 95 | 96 | pub fn delete_item(menu: *Type.MenuType, button: *Type.ButtonType) bool { 97 | var unused: []const u8 = undefined; 98 | var idx: usize = undefined; 99 | var iter = button.opts.label; 100 | if (!(cbor.matchString(&iter, &unused) catch false)) return false; 101 | if (!(cbor.matchString(&iter, &unused) catch false)) return false; 102 | var len = cbor.decodeArrayHeader(&iter) catch return false; 103 | while (len > 0) : (len -= 1) 104 | cbor.skipValue(&iter) catch return false; 105 | if (!(cbor.matchValue(&iter, cbor.extract(&idx)) catch false)) return false; 106 | command.executeName("clipboard_delete", command.fmt(.{idx})) catch |e| menu.*.opts.ctx.logger.err(module_name, e); 107 | return true; //refresh list 108 | } 109 | -------------------------------------------------------------------------------- /src/buffer/View.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const Buffer = @import("Buffer.zig"); 4 | const Cursor = @import("Cursor.zig"); 5 | const Selection = @import("Selection.zig"); 6 | 7 | row: usize = 0, 8 | col: usize = 0, 9 | rows: usize = 0, 10 | cols: usize = 0, 11 | 12 | const scroll_cursor_min_border_distance = 5; 13 | const scroll_cursor_min_border_distance_mouse = 1; 14 | 15 | const Self = @This(); 16 | 17 | pub inline fn invalid() Self { 18 | return .{ 19 | .row = std.math.maxInt(u32), 20 | .col = std.math.maxInt(u32), 21 | }; 22 | } 23 | 24 | inline fn reset(self: *Self) void { 25 | self.* = .{}; 26 | } 27 | 28 | pub inline fn eql(self: Self, other: Self) bool { 29 | return self.row == other.row and self.col == other.col and self.rows == other.rows and self.cols == other.cols; 30 | } 31 | 32 | pub fn move_left(self: *Self) !void { 33 | if (self.col > 0) { 34 | self.col -= 1; 35 | } else return error.Stop; 36 | } 37 | 38 | pub fn move_right(self: *Self) !void { 39 | self.col += 1; 40 | } 41 | 42 | pub fn move_up(self: *Self) !void { 43 | if (!self.is_at_top()) { 44 | self.row -= 1; 45 | } else return error.Stop; 46 | } 47 | 48 | pub fn move_down(self: *Self, root: Buffer.Root) !void { 49 | if (!self.is_at_bottom(root)) { 50 | self.row += 1; 51 | } else return error.Stop; 52 | } 53 | 54 | pub fn move_to(self: *Self, root: Buffer.Root, row: usize) !void { 55 | if (row < root.lines() - self.rows - 1) { 56 | self.row = row; 57 | } else return error.Stop; 58 | } 59 | 60 | inline fn is_at_top(self: *const Self) bool { 61 | return self.row == 0; 62 | } 63 | 64 | inline fn is_at_bottom(self: *const Self, root: Buffer.Root) bool { 65 | if (root.lines() < self.rows) return true; 66 | return self.row >= root.lines() - scroll_cursor_min_border_distance; 67 | } 68 | 69 | pub inline fn is_visible(self: *const Self, cursor: *const Cursor) bool { 70 | if (self.rows == 0) return false; 71 | const row_min = self.row; 72 | const row_max = row_min + self.rows - 1; 73 | const col_min = self.col; 74 | const col_max = col_min + self.cols; 75 | return row_min <= cursor.row and cursor.row <= row_max and 76 | col_min <= cursor.col and cursor.col < col_max; 77 | } 78 | 79 | inline fn is_visible_selection(self: *const Self, sel: *const Selection) bool { 80 | const row_min = self.row; 81 | const row_max = row_min + self.rows; 82 | return self.is_visible(sel.begin) or is_visible(sel.end) or 83 | (sel.begin.row < row_min and sel.end.row > row_max); 84 | } 85 | 86 | inline fn to_cursor_top(self: *const Self) Cursor { 87 | return .{ .row = self.row, .col = 0 }; 88 | } 89 | 90 | inline fn to_cursor_bottom(self: *const Self, root: Buffer.Root) Cursor { 91 | const bottom = @min(root.lines(), self.row + self.rows + 1); 92 | return .{ .row = bottom, .col = 0 }; 93 | } 94 | 95 | fn clamp_row(self: *Self, cursor: *const Cursor, abs: bool) void { 96 | const min_border_distance: usize = if (abs) scroll_cursor_min_border_distance_mouse else scroll_cursor_min_border_distance; 97 | if (cursor.row < min_border_distance) { 98 | self.row = 0; 99 | return; 100 | } 101 | if (self.row > 0 and cursor.row >= min_border_distance) { 102 | if (cursor.row < self.row + min_border_distance) { 103 | self.row = cursor.row - min_border_distance; 104 | return; 105 | } 106 | } 107 | if (cursor.row < self.row) { 108 | self.row = 0; 109 | } else if (cursor.row > self.row + self.rows - min_border_distance) { 110 | self.row = cursor.row + min_border_distance - self.rows; 111 | } 112 | } 113 | 114 | fn clamp_col(self: *Self, cursor: *const Cursor, _: bool) void { 115 | if (cursor.col < self.col) { 116 | self.col = cursor.col; 117 | } else if (cursor.col > self.col + self.cols - 1) { 118 | self.col = cursor.col - self.cols + 1; 119 | } 120 | } 121 | 122 | pub fn clamp(self: *Self, cursor: *const Cursor, abs: bool) void { 123 | self.clamp_row(cursor, abs); 124 | self.clamp_col(cursor, abs); 125 | } 126 | 127 | pub fn write(self: *const Self, writer: *std.Io.Writer) !void { 128 | try cbor.writeValue(writer, .{ 129 | self.row, 130 | self.col, 131 | self.rows, 132 | self.cols, 133 | }); 134 | } 135 | 136 | pub fn extract(self: *Self, iter: *[]const u8) !bool { 137 | return cbor.matchValue(iter, .{ 138 | cbor.extract(&self.row), 139 | cbor.extract(&self.col), 140 | cbor.extract(&self.rows), 141 | cbor.extract(&self.cols), 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /src/buffer/Selection.zig: -------------------------------------------------------------------------------- 1 | const cbor = @import("cbor"); 2 | const Writer = @import("std").Io.Writer; 3 | 4 | const Buffer = @import("Buffer.zig"); 5 | const Cursor = @import("Cursor.zig"); 6 | 7 | begin: Cursor = Cursor{}, 8 | end: Cursor = Cursor{}, 9 | 10 | const Self = @This(); 11 | 12 | pub const Style = enum { normal, inclusive }; 13 | 14 | pub inline fn eql(self: Self, other: Self) bool { 15 | return self.begin.eql(other.begin) and self.end.eql(other.end); 16 | } 17 | 18 | pub fn from_cursor(cursor: *const Cursor) Self { 19 | return .{ .begin = cursor.*, .end = cursor.* }; 20 | } 21 | 22 | pub fn from_pos(sel: Self, root: Buffer.Root, metrics: Buffer.Metrics) Self { 23 | return .{ 24 | .begin = .{ 25 | .row = sel.begin.row, 26 | .col = root.pos_to_width(sel.begin.row, sel.begin.col, metrics) catch root.line_width(sel.begin.row, metrics) catch 0, 27 | }, 28 | .end = .{ 29 | .row = sel.end.row, 30 | .col = root.pos_to_width(sel.end.row, sel.end.col, metrics) catch root.line_width(sel.end.row, metrics) catch 0, 31 | }, 32 | }; 33 | } 34 | 35 | pub fn from_range(range: anytype, root: Buffer.Root, metrics: Buffer.Metrics) Self { 36 | return from_pos(from_range_raw(range), root, metrics); 37 | } 38 | 39 | pub fn from_range_raw(range: anytype) Self { 40 | return .{ 41 | .begin = .{ .row = range.start_point.row, .col = range.start_point.column }, 42 | .end = .{ .row = range.end_point.row, .col = range.end_point.column }, 43 | }; 44 | } 45 | 46 | pub fn line_from_cursor(cursor: Cursor, root: Buffer.Root, mtrx: Buffer.Metrics) Self { 47 | var begin = cursor; 48 | var end = cursor; 49 | begin.move_begin(); 50 | end.move_end(root, mtrx); 51 | end.move_right(root, mtrx) catch {}; 52 | return .{ .begin = begin, .end = end }; 53 | } 54 | 55 | pub fn empty(self: *const Self) bool { 56 | return self.begin.eql(self.end); 57 | } 58 | 59 | pub fn reverse(self: *Self) void { 60 | const tmp = self.begin; 61 | self.begin = self.end; 62 | self.end = tmp; 63 | } 64 | 65 | pub inline fn is_reversed(self: *const Self) bool { 66 | return self.begin.right_of(self.end); 67 | } 68 | 69 | pub fn normalize(self: *Self) void { 70 | if (self.is_reversed()) self.reverse(); 71 | } 72 | 73 | pub fn write(self: *const Self, writer: *Writer) !void { 74 | try cbor.writeArrayHeader(writer, 2); 75 | try self.begin.write(writer); 76 | try self.end.write(writer); 77 | } 78 | 79 | pub fn extract(self: *Self, iter: *[]const u8) !bool { 80 | var iter2 = iter.*; 81 | const len = cbor.decodeArrayHeader(&iter2) catch return false; 82 | if (len != 2) return false; 83 | if (!try self.begin.extract(&iter2)) return false; 84 | if (!try self.end.extract(&iter2)) return false; 85 | iter.* = iter2; 86 | return true; 87 | } 88 | 89 | pub fn nudge_insert(self: *Self, nudge: Self) void { 90 | self.begin.nudge_insert(nudge); 91 | self.end.nudge_insert(nudge); 92 | } 93 | 94 | pub fn nudge_delete(self: *Self, nudge: Self) bool { 95 | if (!self.begin.nudge_delete(nudge)) 96 | return false; 97 | return self.end.nudge_delete(nudge); 98 | } 99 | 100 | pub fn merge(self: *Self, other_: Self) bool { 101 | var other = other_; 102 | other.normalize(); 103 | if (self.is_reversed()) { 104 | var this = self.*; 105 | this.normalize(); 106 | if (this.merge_normal(other)) { 107 | self.begin = this.end; 108 | self.end = this.begin; 109 | return true; 110 | } 111 | return false; 112 | } 113 | return self.merge_normal(other); 114 | } 115 | 116 | fn merge_normal(self: *Self, other: Self) bool { 117 | var merged = false; 118 | if (self.begin.within(other)) { 119 | self.begin = other.begin; 120 | merged = true; 121 | } 122 | if (self.end.within(other)) { 123 | self.end = other.end; 124 | merged = true; 125 | } 126 | return merged or 127 | (other.begin.right_of(self.begin) and 128 | self.end.right_of(other.end)); 129 | } 130 | 131 | pub fn expand(self: *Self, other_: Self) void { 132 | var other = other_; 133 | other.normalize(); 134 | if (self.is_reversed()) { 135 | var this = self.*; 136 | this.normalize(); 137 | this.expand_normal(other); 138 | self.begin = this.end; 139 | self.end = this.begin; 140 | } else self.expand_normal(other); 141 | } 142 | 143 | fn expand_normal(self: *Self, other: Self) void { 144 | if (self.begin.right_of(other.begin)) 145 | self.begin = other.begin; 146 | if (other.end.right_of(self.end)) 147 | self.end = other.end; 148 | } 149 | -------------------------------------------------------------------------------- /src/list_languages.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const file_type_config = @import("file_type_config"); 3 | const text_manip = @import("text_manip"); 4 | const write_string = text_manip.write_string; 5 | const write_padding = text_manip.write_padding; 6 | const builtin = @import("builtin"); 7 | const RGB = @import("color").RGB; 8 | 9 | const bin_path = @import("bin_path"); 10 | 11 | const checkmark_width = if (builtin.os.tag != .windows) 2 else 3; 12 | 13 | const success_mark = if (builtin.os.tag != .windows) "✓ " else "[y]"; 14 | const fail_mark = if (builtin.os.tag != .windows) "✘ " else "[n]"; 15 | 16 | pub fn list(allocator: std.mem.Allocator, writer: *std.io.Writer, tty_config: std.io.tty.Config) !void { 17 | var max_language_len: usize = 0; 18 | var max_langserver_len: usize = 0; 19 | var max_formatter_len: usize = 0; 20 | var max_extensions_len: usize = 0; 21 | 22 | for (file_type_config.get_all_names()) |file_type_name| { 23 | const file_type = try file_type_config.get(file_type_name) orelse unreachable; 24 | max_language_len = @max(max_language_len, file_type.name.len); 25 | max_langserver_len = @max(max_langserver_len, args_string_length(file_type.language_server)); 26 | max_formatter_len = @max(max_formatter_len, args_string_length(file_type.formatter)); 27 | max_extensions_len = @max(max_extensions_len, args_string_length(file_type.extensions)); 28 | } 29 | 30 | try tty_config.setColor(writer, .yellow); 31 | try write_string(writer, " Language", max_language_len + 1 + 4); 32 | try write_string(writer, "Extensions", max_extensions_len + 1 + checkmark_width); 33 | try write_string(writer, "Language Server", max_langserver_len + 1 + checkmark_width); 34 | try write_string(writer, "Formatter", null); 35 | try tty_config.setColor(writer, .reset); 36 | try writer.writeAll("\n"); 37 | 38 | for (file_type_config.get_all_names()) |file_type_name| { 39 | const file_type = try file_type_config.get(file_type_name) orelse unreachable; 40 | try writer.writeAll(" "); 41 | try setColorRgb(writer, file_type.color orelse file_type_config.default.color); 42 | try writer.writeAll(file_type.icon orelse file_type_config.default.icon); 43 | try tty_config.setColor(writer, .reset); 44 | try writer.writeAll(" "); 45 | try write_string(writer, file_type.name, max_language_len + 1); 46 | try write_segmented(writer, file_type.extensions, ",", max_extensions_len + 1, tty_config); 47 | 48 | if (file_type.language_server) |language_server| 49 | try write_checkmark(writer, can_execute(allocator, language_server[0]), tty_config); 50 | 51 | try write_segmented(writer, file_type.language_server, " ", max_langserver_len + 1, tty_config); 52 | 53 | if (file_type.formatter) |formatter| 54 | try write_checkmark(writer, can_execute(allocator, formatter[0]), tty_config); 55 | 56 | try write_segmented(writer, file_type.formatter, " ", null, tty_config); 57 | try writer.writeAll("\n"); 58 | } 59 | } 60 | 61 | fn args_string_length(args_: ?[]const []const u8) usize { 62 | const args = args_ orelse return 0; 63 | var len: usize = 0; 64 | var first: bool = true; 65 | for (args) |arg| { 66 | if (first) first = false else len += 1; 67 | len += arg.len; 68 | } 69 | return len; 70 | } 71 | 72 | fn write_checkmark(writer: anytype, success: bool, tty_config: std.io.tty.Config) !void { 73 | try tty_config.setColor(writer, if (success) .green else .red); 74 | if (success) try writer.writeAll(success_mark) else try writer.writeAll(fail_mark); 75 | } 76 | 77 | fn write_segmented( 78 | writer: anytype, 79 | args_: ?[]const []const u8, 80 | sep: []const u8, 81 | pad: ?usize, 82 | tty_config: std.io.tty.Config, 83 | ) !void { 84 | const args = args_ orelse return; 85 | var len: usize = 0; 86 | var first: bool = true; 87 | for (args) |arg| { 88 | if (first) first = false else { 89 | len += 1; 90 | try writer.writeAll(sep); 91 | } 92 | len += arg.len; 93 | try writer.writeAll(arg); 94 | } 95 | try tty_config.setColor(writer, .reset); 96 | if (pad) |pad_| try write_padding(writer, len, pad_); 97 | } 98 | 99 | fn can_execute(allocator: std.mem.Allocator, binary_name: []const u8) bool { 100 | const resolved_binary_path = bin_path.find_binary_in_path(allocator, binary_name) catch return false; 101 | defer if (resolved_binary_path) |path| allocator.free(path); 102 | return resolved_binary_path != null; 103 | } 104 | 105 | fn setColorRgb(writer: anytype, color: u24) !void { 106 | const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m"; 107 | const rgb = RGB.from_u24(color); 108 | try writer.print(fg_rgb_legacy, .{ rgb.r, rgb.g, rgb.b }); 109 | } 110 | -------------------------------------------------------------------------------- /src/tui/Fire.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Plane = @import("renderer").Plane; 3 | const Widget = @import("Widget.zig"); 4 | 5 | const px = "▀"; 6 | 7 | const Fire = @This(); 8 | 9 | allocator: std.mem.Allocator, 10 | plane: Plane, 11 | prng: std.Random.DefaultPrng, 12 | 13 | //scope cache - spread fire 14 | spread_px: u8 = 0, 15 | spread_rnd_idx: u8 = 0, 16 | spread_dst: usize = 0, 17 | 18 | FIRE_H: u16, 19 | FIRE_W: u16, 20 | FIRE_SZ: usize, 21 | FIRE_LAST_ROW: usize, 22 | 23 | screen_buf: []u8, 24 | 25 | const MAX_COLOR = 256; 26 | const LAST_COLOR = MAX_COLOR - 1; 27 | 28 | pub fn init(allocator: std.mem.Allocator, plane: Plane) !Fire { 29 | const pos = Widget.Box.from(plane); 30 | const FIRE_H = @as(u16, @intCast(pos.h)) * 2; 31 | const FIRE_W = @as(u16, @intCast(pos.w)); 32 | var self: Fire = .{ 33 | .allocator = allocator, 34 | .plane = plane, 35 | .prng = std.Random.DefaultPrng.init(blk: { 36 | var seed: u64 = undefined; 37 | try std.posix.getrandom(std.mem.asBytes(&seed)); 38 | break :blk seed; 39 | }), 40 | .FIRE_H = FIRE_H, 41 | .FIRE_W = FIRE_W, 42 | .FIRE_SZ = @as(usize, @intCast(FIRE_H)) * FIRE_W, 43 | .FIRE_LAST_ROW = @as(usize, @intCast(FIRE_H - 1)) * FIRE_W, 44 | .screen_buf = try allocator.alloc(u8, @as(usize, @intCast(FIRE_H)) * FIRE_W), 45 | }; 46 | 47 | var buf_idx: usize = 0; 48 | while (buf_idx < self.FIRE_SZ) : (buf_idx += 1) { 49 | self.screen_buf[buf_idx] = fire_black; 50 | } 51 | 52 | // last row is white...white is "fire source" 53 | buf_idx = 0; 54 | while (buf_idx < self.FIRE_W) : (buf_idx += 1) { 55 | self.screen_buf[self.FIRE_LAST_ROW + buf_idx] = fire_white; 56 | } 57 | return self; 58 | } 59 | 60 | pub fn deinit(self: *Fire) void { 61 | self.allocator.free(self.screen_buf); 62 | } 63 | 64 | const fire_palette = [_]u8{ 0, 233, 234, 52, 53, 88, 89, 94, 95, 96, 130, 131, 132, 133, 172, 214, 215, 220, 220, 221, 3, 226, 227, 230, 195, 230 }; 65 | const fire_black: u8 = 0; 66 | const fire_white: u8 = fire_palette.len - 1; 67 | 68 | pub fn render(self: *Fire) void { 69 | self.plane.home(); 70 | const transparent = self.plane.transparent; 71 | self.plane.transparent = false; 72 | defer self.plane.transparent = transparent; 73 | 74 | var rand = self.prng.random(); 75 | 76 | //update fire buf 77 | var doFire_x: u16 = 0; 78 | while (doFire_x < self.FIRE_W) : (doFire_x += 1) { 79 | var doFire_y: u16 = 0; 80 | while (doFire_y < self.FIRE_H) : (doFire_y += 1) { 81 | const doFire_idx = @as(usize, @intCast(doFire_y)) * self.FIRE_W + doFire_x; 82 | 83 | //spread fire 84 | self.spread_px = self.screen_buf[doFire_idx]; 85 | 86 | //bounds checking 87 | if ((self.spread_px == 0) and (doFire_idx >= self.FIRE_W)) { 88 | self.screen_buf[doFire_idx - self.FIRE_W] = 0; 89 | } else { 90 | self.spread_rnd_idx = rand.intRangeAtMost(u8, 0, 3); 91 | if (doFire_idx >= (self.spread_rnd_idx + 1)) { 92 | self.spread_dst = doFire_idx - self.spread_rnd_idx + 1; 93 | } else { 94 | self.spread_dst = doFire_idx; 95 | } 96 | if (self.spread_dst >= self.FIRE_W) { 97 | if (self.spread_px > (self.spread_rnd_idx & 1)) { 98 | self.screen_buf[self.spread_dst - self.FIRE_W] = self.spread_px - (self.spread_rnd_idx & 1); 99 | } else { 100 | self.screen_buf[self.spread_dst - self.FIRE_W] = 0; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | //scope cache - fire 2 screen buffer 108 | var frame_x: u16 = 0; 109 | var frame_y: u16 = 0; 110 | 111 | // for each row 112 | frame_y = 0; 113 | while (frame_y < self.FIRE_H) : (frame_y += 2) { // 'paint' two rows at a time because of half height char 114 | // for each col 115 | frame_x = 0; 116 | while (frame_x < self.FIRE_W) : (frame_x += 1) { 117 | //each character rendered is actually to rows of 'pixels' 118 | // - "hi" (current px row => fg char) 119 | // - "low" (next row => bg color) 120 | const px_hi = self.screen_buf[@as(usize, @intCast(frame_y)) * self.FIRE_W + frame_x]; 121 | const px_lo = self.screen_buf[@as(usize, @intCast(frame_y + 1)) * self.FIRE_W + frame_x]; 122 | 123 | self.plane.set_fg_palindex(fire_palette[px_hi]) catch {}; 124 | self.plane.set_bg_palindex(fire_palette[px_lo]) catch {}; 125 | _ = self.plane.putchar(px); 126 | } 127 | self.plane.cursor_move_yx(-1, 0) catch {}; 128 | self.plane.cursor_move_rel(1, 0) catch {}; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | 3 | frame_rate: usize = 60, 4 | theme: []const u8 = "ayu-mirage-bordered", 5 | light_theme: []const u8 = "ayu-light", 6 | input_mode: []const u8 = "flow", 7 | gutter_line_numbers_mode: ?LineNumberMode = null, 8 | gutter_line_numbers_style: DigitStyle = .ascii, 9 | gutter_symbols: bool = true, 10 | enable_terminal_cursor: bool = true, 11 | enable_terminal_color_scheme: bool = false, 12 | enable_modal_dim: bool = true, 13 | highlight_current_line: bool = true, 14 | highlight_current_line_gutter: bool = true, 15 | highlight_columns: []const u16 = &.{ 80, 100, 120 }, 16 | highlight_columns_alpha: u8 = 240, 17 | highlight_columns_enabled: bool = false, 18 | whitespace_mode: WhitespaceMode = .indent, 19 | inline_diagnostics: bool = true, 20 | animation_min_lag: usize = 0, //milliseconds 21 | animation_max_lag: usize = 50, //milliseconds 22 | hover_time_ms: usize = 500, //milliseconds 23 | input_idle_time_ms: usize = 150, //milliseconds 24 | idle_actions: []const IdleAction = &default_actions, 25 | idle_commands: ?[]const []const u8 = null, // a list of simple commands 26 | enable_format_on_save: bool = false, 27 | restore_last_cursor_position: bool = true, 28 | follow_cursor_on_buffer_switch: bool = false, //scroll cursor into view on buffer switch 29 | default_cursor: CursorShape = .default, 30 | modes_can_change_cursor: bool = true, 31 | enable_auto_save: bool = false, 32 | limit_auto_save_file_types: ?[]const []const u8 = null, // null means *all* 33 | enable_prefix_keyhints: bool = true, 34 | enable_auto_find: bool = true, 35 | initial_find_query: InitialFindQuery = .selection, 36 | ignore_filter_stderr: bool = false, 37 | 38 | auto_run_time_seconds: usize = 120, //seconds 39 | auto_run_commands: ?[]const []const u8 = &.{"save_session_quiet"}, // a list of simple commands 40 | 41 | indent_size: usize = 4, 42 | tab_width: usize = 8, 43 | indent_mode: IndentMode = .auto, 44 | 45 | top_bar: []const u8 = "tabs", 46 | bottom_bar: []const u8 = "mode file log selection diagnostics keybind branch linenumber clock spacer", 47 | show_scrollbars: bool = true, 48 | show_fileicons: bool = true, 49 | show_local_diagnostics_in_panel: bool = false, 50 | scrollbar_auto_hide: bool = false, 51 | 52 | start_debugger_on_crash: bool = false, 53 | 54 | completion_trigger: CompletionTrigger = .manual, 55 | completion_style: CompletionStyle = .palette, 56 | 57 | widget_style: WidgetStyle = .compact, 58 | palette_style: WidgetStyle = .bars_top_bottom, 59 | dropdown_style: WidgetStyle = .compact, 60 | panel_style: WidgetStyle = .compact, 61 | home_style: WidgetStyle = .bars_top_bottom, 62 | pane_left_style: WidgetStyle = .bar_right, 63 | pane_right_style: WidgetStyle = .bar_left, 64 | pane_style: PaneStyle = .panel, 65 | hint_window_style: WidgetStyle = .thick_boxed, 66 | 67 | centered_view: bool = false, 68 | centered_view_width: usize = 145, 69 | centered_view_min_screen_width: usize = 145, 70 | 71 | lsp_output: enum { quiet, verbose } = .quiet, 72 | 73 | keybind_mode: KeybindMode = .normal, 74 | 75 | include_files: []const u8 = "", 76 | 77 | const default_actions = [_]IdleAction{}; 78 | pub const IdleAction = enum { 79 | hover, 80 | highlight_references, 81 | }; 82 | 83 | pub const DigitStyle = enum { 84 | ascii, 85 | digital, 86 | subscript, 87 | superscript, 88 | }; 89 | 90 | pub const LineNumberMode = enum { 91 | none, 92 | relative, 93 | absolute, 94 | }; 95 | 96 | pub const IndentMode = enum { 97 | auto, 98 | spaces, 99 | tabs, 100 | }; 101 | 102 | pub const WidgetType = enum { 103 | none, 104 | palette, 105 | panel, 106 | home, 107 | pane_left, 108 | pane_right, 109 | hint_window, 110 | dropdown, 111 | }; 112 | 113 | pub const WidgetStyle = enum { 114 | bars_top_bottom, 115 | bars_left_right, 116 | bar_left, 117 | bar_right, 118 | thick_boxed, 119 | extra_thick_boxed, 120 | dotted_boxed, 121 | rounded_boxed, 122 | double_boxed, 123 | single_double_top_bottom_boxed, 124 | single_double_left_right_boxed, 125 | boxed, 126 | spacious, 127 | compact, 128 | }; 129 | 130 | pub const WhitespaceMode = enum { 131 | indent, 132 | leading, 133 | eol, 134 | tabs, 135 | external, 136 | visible, 137 | full, 138 | none, 139 | }; 140 | 141 | pub const CursorShape = enum { 142 | default, 143 | block_blink, 144 | block, 145 | underline_blink, 146 | underline, 147 | beam_blink, 148 | beam, 149 | }; 150 | 151 | pub const PaneStyle = enum { 152 | panel, 153 | editor, 154 | }; 155 | 156 | pub const KeybindMode = enum { 157 | normal, 158 | ignore_alt_text_modifiers, 159 | }; 160 | 161 | pub const InitialFindQuery = enum { 162 | empty, 163 | selection, 164 | last_query, 165 | selection_or_last_query, 166 | }; 167 | 168 | pub const CompletionTrigger = enum { 169 | manual, 170 | automatic, 171 | }; 172 | 173 | pub const CompletionStyle = enum { 174 | palette, 175 | dropdown, 176 | }; 177 | -------------------------------------------------------------------------------- /src/tui/inputview.zig: -------------------------------------------------------------------------------- 1 | const eql = @import("std").mem.eql; 2 | const time = @import("std").time; 3 | const Allocator = @import("std").mem.Allocator; 4 | const ArrayList = @import("std").ArrayList; 5 | const Writer = @import("std").Io.Writer; 6 | 7 | const tp = @import("thespian"); 8 | const cbor = @import("cbor"); 9 | 10 | const Plane = @import("renderer").Plane; 11 | const EventHandler = @import("EventHandler"); 12 | const input = @import("input"); 13 | 14 | const tui = @import("tui.zig"); 15 | const Widget = @import("Widget.zig"); 16 | const WidgetList = @import("WidgetList.zig"); 17 | 18 | pub const name = "inputview"; 19 | 20 | allocator: Allocator, 21 | parent: Plane, 22 | plane: Plane, 23 | last_count: u64 = 0, 24 | buffer: Buffer, 25 | 26 | const Self = @This(); 27 | const widget_type: Widget.Type = .panel; 28 | 29 | const Entry = struct { 30 | time: i64, 31 | tdiff: i64, 32 | json: [:0]u8, 33 | }; 34 | const Buffer = ArrayList(Entry); 35 | 36 | pub fn create(allocator: Allocator, parent: Plane) !Widget { 37 | var n = try Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent); 38 | errdefer n.deinit(); 39 | const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type); 40 | const self = try allocator.create(Self); 41 | errdefer allocator.destroy(self); 42 | self.* = .{ 43 | .allocator = allocator, 44 | .parent = parent, 45 | .plane = n, 46 | .buffer = .empty, 47 | }; 48 | try tui.input_listeners().add(EventHandler.bind(self, listen)); 49 | container.ctx = self; 50 | try container.add(Widget.to(self)); 51 | return container.widget(); 52 | } 53 | 54 | pub fn deinit(self: *Self, allocator: Allocator) void { 55 | tui.input_listeners().remove_ptr(self); 56 | for (self.buffer.items) |item| 57 | self.allocator.free(item.json); 58 | self.buffer.deinit(self.allocator); 59 | self.plane.deinit(); 60 | allocator.destroy(self); 61 | } 62 | 63 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 64 | self.plane.set_base_style(theme.panel); 65 | self.plane.erase(); 66 | self.plane.home(); 67 | const height = self.plane.dim_y(); 68 | var first = true; 69 | const count = self.buffer.items.len; 70 | const begin_at = if (height > count) 0 else count - height; 71 | for (self.buffer.items[begin_at..]) |item| { 72 | if (first) first = false else _ = self.plane.putstr("\n") catch return false; 73 | self.output_tdiff(item.tdiff) catch return false; 74 | _ = self.plane.putstr(item.json) catch return false; 75 | } 76 | if (self.last_count > 0) 77 | _ = self.plane.print(" ({})", .{self.last_count}) catch {}; 78 | return false; 79 | } 80 | 81 | fn output_tdiff(self: *Self, tdiff: i64) !void { 82 | const msi = @divFloor(tdiff, time.us_per_ms); 83 | if (msi == 0) { 84 | const d: f64 = @floatFromInt(tdiff); 85 | const ms = d / time.us_per_ms; 86 | _ = try self.plane.print("{d:6.2} ▏", .{ms}); 87 | } else { 88 | const ms: u64 = @intCast(msi); 89 | _ = try self.plane.print("{d:6} ▏", .{ms}); 90 | } 91 | } 92 | 93 | fn append(self: *Self, json: []const u8) !void { 94 | const ts = time.microTimestamp(); 95 | const tdiff = if (self.buffer.getLastOrNull()) |last| ret: { 96 | if (eql(u8, json, last.json)) { 97 | self.last_count += 1; 98 | return; 99 | } 100 | break :ret ts - last.time; 101 | } else 0; 102 | self.last_count = 0; 103 | (try self.buffer.addOne(self.allocator)).* = .{ 104 | .time = ts, 105 | .tdiff = tdiff, 106 | .json = try self.allocator.dupeZ(u8, json), 107 | }; 108 | } 109 | 110 | fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result { 111 | if (try m.match(.{ "M", tp.more })) return; 112 | var buf: [4096]u8 = undefined; 113 | const json = m.to_json(&buf) catch |e| return tp.exit_error(e, @errorReturnTrace()); 114 | var result: Writer.Allocating = .init(self.allocator); 115 | defer result.deinit(); 116 | const writer = &result.writer; 117 | writer.writeAll(json) catch |e| return tp.exit_error(e, @errorReturnTrace()); 118 | 119 | var event: input.Event = 0; 120 | var keypress: input.Key = 0; 121 | var keypress_shifted: input.Key = 0; 122 | var text: []const u8 = ""; 123 | var modifiers: input.Mods = 0; 124 | if (try m.match(.{ 125 | "I", 126 | tp.extract(&event), 127 | tp.extract(&keypress), 128 | tp.extract(&keypress_shifted), 129 | tp.extract(&text), 130 | tp.extract(&modifiers), 131 | })) { 132 | const key_event = input.KeyEvent.from_message(event, keypress, keypress_shifted, text, modifiers); 133 | writer.print(" -> {f}", .{key_event}) catch |e| return tp.exit_error(e, @errorReturnTrace()); 134 | } 135 | self.append(result.written()) catch |e| return tp.exit_error(e, @errorReturnTrace()); 136 | } 137 | 138 | pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { 139 | return false; 140 | } 141 | -------------------------------------------------------------------------------- /src/tui/keybindview.zig: -------------------------------------------------------------------------------- 1 | const eql = @import("std").mem.eql; 2 | const time = @import("std").time; 3 | const Allocator = @import("std").mem.Allocator; 4 | const ArrayList = @import("std").ArrayList; 5 | const Writer = @import("std").Io.Writer; 6 | const hexEscape = @import("std").ascii.hexEscape; 7 | 8 | const tp = @import("thespian"); 9 | const cbor = @import("cbor"); 10 | 11 | const Plane = @import("renderer").Plane; 12 | const input = @import("input"); 13 | 14 | const tui = @import("tui.zig"); 15 | const Widget = @import("Widget.zig"); 16 | const WidgetList = @import("WidgetList.zig"); 17 | const MessageFilter = @import("MessageFilter.zig"); 18 | 19 | pub const name = "keybindview"; 20 | 21 | allocator: Allocator, 22 | parent: Plane, 23 | plane: Plane, 24 | buffer: Buffer, 25 | 26 | const Self = @This(); 27 | const widget_type: Widget.Type = .panel; 28 | 29 | const Entry = struct { 30 | time: i64, 31 | tdiff: i64, 32 | msg: []const u8, 33 | }; 34 | const Buffer = ArrayList(Entry); 35 | 36 | pub fn create(allocator: Allocator, parent: Plane) !Widget { 37 | var n = try Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent); 38 | errdefer n.deinit(); 39 | const container = try WidgetList.createHStyled(allocator, parent, "panel_frame", .dynamic, widget_type); 40 | const self = try allocator.create(Self); 41 | errdefer allocator.destroy(self); 42 | self.* = .{ 43 | .allocator = allocator, 44 | .parent = parent, 45 | .plane = n, 46 | .buffer = .empty, 47 | }; 48 | try tui.message_filters().add(MessageFilter.bind(self, keybind_match)); 49 | tui.enable_match_events(); 50 | container.ctx = self; 51 | try container.add(Widget.to(self)); 52 | return container.widget(); 53 | } 54 | 55 | pub fn deinit(self: *Self, allocator: Allocator) void { 56 | tui.disable_match_events(); 57 | tui.message_filters().remove_ptr(self); 58 | for (self.buffer.items) |item| 59 | self.allocator.free(item.msg); 60 | self.buffer.deinit(self.allocator); 61 | self.plane.deinit(); 62 | allocator.destroy(self); 63 | } 64 | 65 | pub fn render(self: *Self, theme: *const Widget.Theme) bool { 66 | self.plane.set_base_style(theme.panel); 67 | self.plane.erase(); 68 | self.plane.home(); 69 | const height = self.plane.dim_y(); 70 | var first = true; 71 | const count = self.buffer.items.len; 72 | const begin_at = if (height > count) 0 else count - height; 73 | for (self.buffer.items[begin_at..]) |item| { 74 | if (first) first = false else _ = self.plane.putstr("\n") catch return false; 75 | self.output_tdiff(item.tdiff) catch return false; 76 | _ = self.plane.putstr(item.msg) catch return false; 77 | } 78 | return false; 79 | } 80 | 81 | fn output_tdiff(self: *Self, tdiff: i64) !void { 82 | const msi = @divFloor(tdiff, time.us_per_ms); 83 | if (msi == 0) { 84 | const d: f64 = @floatFromInt(tdiff); 85 | const ms = d / time.us_per_ms; 86 | _ = try self.plane.print("{d:6.2} ▏", .{ms}); 87 | } else { 88 | const ms: u64 = @intCast(msi); 89 | _ = try self.plane.print("{d:6} ▏", .{ms}); 90 | } 91 | } 92 | 93 | fn keybind_match(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { 94 | var namespace: []const u8 = undefined; 95 | var section: []const u8 = undefined; 96 | var key_event: []const u8 = undefined; 97 | var cmds: []const u8 = undefined; 98 | var insert_cmd: []const u8 = undefined; 99 | var bytes: []const u8 = undefined; 100 | 101 | if (m.match(.{ "K", tp.extract(&namespace), tp.extract(§ion), tp.extract(&key_event), tp.extract_cbor(&cmds) }) catch false) { 102 | var result: Writer.Allocating = .init(self.allocator); 103 | defer result.deinit(); 104 | const writer = &result.writer; 105 | 106 | writer.print("{s}:{s} {s} => ", .{ namespace, section, key_event }) catch return true; 107 | cbor.toJsonWriter(cmds, writer, .{}) catch return true; 108 | 109 | self.append(result.toOwnedSlice() catch return true); 110 | return true; 111 | } else if (m.match(.{ "N", tp.extract(&namespace), tp.extract(§ion), tp.extract(&insert_cmd), tp.extract(&bytes) }) catch false) { 112 | var result: Writer.Allocating = .init(self.allocator); 113 | defer result.deinit(); 114 | result.writer.print("{s}:{s} insert => [\"{s}\", \"{f}\"] ", .{ namespace, section, insert_cmd, hexEscape(bytes, .lower) }) catch return true; 115 | self.append(result.toOwnedSlice() catch return true); 116 | return true; 117 | } 118 | return false; 119 | } 120 | 121 | pub fn append(self: *Self, msg: []const u8) void { 122 | const ts = time.microTimestamp(); 123 | const tdiff = if (self.buffer.items.len > 0) ts -| self.buffer.items[self.buffer.items.len - 1].time else 0; 124 | (self.buffer.addOne(self.allocator) catch return).* = .{ 125 | .time = ts, 126 | .tdiff = tdiff, 127 | .msg = msg, 128 | }; 129 | } 130 | 131 | pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool { 132 | return false; 133 | } 134 | -------------------------------------------------------------------------------- /src/tui/mode/vim.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const command = @import("command"); 3 | const cmd = command.executeName; 4 | 5 | var commands: Commands = undefined; 6 | 7 | pub fn init() !void { 8 | var v: void = {}; 9 | try commands.init(&v); 10 | } 11 | 12 | pub fn deinit() void { 13 | commands.deinit(); 14 | } 15 | 16 | const Commands = command.Collection(cmds_); 17 | const cmds_ = struct { 18 | pub const Target = void; 19 | const Ctx = command.Context; 20 | const Meta = command.Metadata; 21 | const Result = command.Result; 22 | 23 | pub fn w(_: *void, _: Ctx) Result { 24 | try cmd("save_file", .{}); 25 | } 26 | pub const w_meta: Meta = .{ .description = "w (write file)" }; 27 | 28 | pub fn q(_: *void, _: Ctx) Result { 29 | try cmd("quit", .{}); 30 | } 31 | pub const q_meta: Meta = .{ .description = "q (quit)" }; 32 | 33 | pub fn @"q!"(_: *void, _: Ctx) Result { 34 | try cmd("quit_without_saving", .{}); 35 | } 36 | pub const @"q!_meta": Meta = .{ .description = "q! (quit without saving)" }; 37 | 38 | pub fn @"qa!"(_: *void, _: Ctx) Result { 39 | try cmd("quit_without_saving", .{}); 40 | } 41 | pub const @"qa!_meta": Meta = .{ .description = "qa! (quit without saving anything)" }; 42 | 43 | pub fn wq(_: *void, _: Ctx) Result { 44 | try cmd("save_file", command.fmt(.{ "then", .{ "quit", .{} } })); 45 | } 46 | pub const wq_meta: Meta = .{ .description = "wq (write file and quit)" }; 47 | 48 | pub fn @"wq!"(_: *void, _: Ctx) Result { 49 | cmd("save_file", .{}) catch {}; 50 | try cmd("quit_without_saving", .{}); 51 | } 52 | pub const @"wq!_meta": Meta = .{ .description = "wq! (write file and quit without saving)" }; 53 | 54 | pub fn @"e!"(_: *void, _: Ctx) Result { 55 | try cmd("reload_file", .{}); 56 | } 57 | pub const @"e!_meta": Meta = .{ .description = "e! (force reload current file)" }; 58 | 59 | pub fn bd(_: *void, _: Ctx) Result { 60 | try cmd("close_file", .{}); 61 | } 62 | pub const bd_meta: Meta = .{ .description = "bd (Close file)" }; 63 | 64 | pub fn bw(_: *void, _: Ctx) Result { 65 | try cmd("delete_buffer", .{}); 66 | } 67 | pub const bw_meta: Meta = .{ .description = "bw (Delete buffer)" }; 68 | 69 | pub fn bnext(_: *void, _: Ctx) Result { 70 | try cmd("next_tab", .{}); 71 | } 72 | pub const bnext_meta: Meta = .{ .description = "bnext (Next buffer/tab)" }; 73 | 74 | pub fn bprevious(_: *void, _: Ctx) Result { 75 | try cmd("next_tab", .{}); 76 | } 77 | pub const bprevious_meta: Meta = .{ .description = "bprevious (Previous buffer/tab)" }; 78 | 79 | pub fn ls(_: *void, _: Ctx) Result { 80 | try cmd("switch_buffers", .{}); 81 | } 82 | pub const ls_meta: Meta = .{ .description = "ls (List/switch buffers)" }; 83 | 84 | pub fn move_begin_or_add_integer_argument_zero(_: *void, _: Ctx) Result { 85 | return if (@import("keybind").current_integer_argument()) |_| 86 | command.executeName("add_integer_argument_digit", command.fmt(.{0})) 87 | else 88 | command.executeName("move_begin", .{}); 89 | } 90 | pub const move_begin_or_add_integer_argument_zero_meta: Meta = .{ .description = "Move cursor to beginning of line (vim)" }; 91 | 92 | pub fn enter_mode_at_next_char(self: *void, ctx: Ctx) Result { 93 | _ = self; // autofix 94 | _ = ctx; // autofix 95 | //TODO 96 | return undefined; 97 | } 98 | 99 | pub const enter_mode_at_next_char_meta: Meta = .{ .description = "Move forward one char and change mode" }; 100 | 101 | pub fn enter_mode_on_newline_down(self: *void, ctx: Ctx) Result { 102 | _ = self; // autofix 103 | _ = ctx; // autofix 104 | //TODO 105 | return undefined; 106 | } 107 | 108 | pub const enter_mode_on_newline_down_meta: Meta = .{ .description = "Insert a newline and change mode" }; 109 | 110 | pub fn enter_mode_on_newline_up(self: *void, ctx: Ctx) Result { 111 | _ = self; // autofix 112 | _ = ctx; // autofix 113 | //TODO 114 | return undefined; 115 | } 116 | pub const enter_mode_on_newline_up_meta: Meta = .{ .description = "Insert a newline above the current line and change mode" }; 117 | 118 | pub fn enter_mode_at_line_begin(self: *void, ctx: Ctx) Result { 119 | _ = self; // autofix 120 | _ = ctx; // autofix 121 | //TODO 122 | return undefined; 123 | } 124 | 125 | pub const enter_mode_at_line_begin_meta: Meta = .{ .description = "Goto line begin and change mode" }; 126 | 127 | pub fn enter_mode_at_line_end(self: *void, ctx: Ctx) Result { 128 | _ = self; // autofix 129 | _ = ctx; // autofix 130 | //TODO 131 | return undefined; 132 | } 133 | pub const enter_mode_at_line_end_meta: Meta = .{ .description = "Goto line end and change mode" }; 134 | 135 | pub fn copy_line(self: *void, ctx: Ctx) Result { 136 | _ = self; // autofix 137 | _ = ctx; // autofix 138 | //TODO 139 | return undefined; 140 | } 141 | 142 | pub const copy_line_meta: Meta = .{ .description = "Copies the current line" }; 143 | }; 144 | -------------------------------------------------------------------------------- /src/tui/MessageFilter.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const cbor = @import("cbor"); 4 | const Allocator = std.mem.Allocator; 5 | const ArrayList = std.ArrayList; 6 | const Self = @This(); 7 | const MessageFilter = Self; 8 | 9 | ptr: *anyopaque, 10 | vtable: *const VTable, 11 | 12 | pub const Error = (cbor.Error || cbor.JsonEncodeError || error{ 13 | OutOfMemory, 14 | ThespianSpawnFailed, 15 | NoProject, 16 | ProjectManagerFailed, 17 | InvalidProjectDirectory, 18 | SendFailed, 19 | }); 20 | 21 | pub const VTable = struct { 22 | deinit: *const fn (ctx: *anyopaque) void, 23 | filter: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) Error!bool, 24 | type_name: []const u8, 25 | }; 26 | 27 | pub fn to_owned(pimpl: anytype) Self { 28 | const impl = @typeInfo(@TypeOf(pimpl)); 29 | const child: type = impl.Pointer.child; 30 | return .{ 31 | .ptr = pimpl, 32 | .vtable = comptime &.{ 33 | .type_name = @typeName(child), 34 | .deinit = struct { 35 | pub fn deinit(ctx: *anyopaque) void { 36 | return child.deinit(@as(*child, @ptrCast(@alignCast(ctx)))); 37 | } 38 | }.deinit, 39 | .filter = struct { 40 | pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) Error!bool { 41 | return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m); 42 | } 43 | }.filter, 44 | }, 45 | }; 46 | } 47 | 48 | pub fn to_unowned(pimpl: anytype) Self { 49 | const impl = @typeInfo(@TypeOf(pimpl)); 50 | const child: type = impl.Pointer.child; 51 | return .{ 52 | .ptr = pimpl, 53 | .vtable = comptime &.{ 54 | .type_name = @typeName(child), 55 | .deinit = struct { 56 | pub fn deinit(_: *anyopaque) void {} 57 | }.deinit, 58 | .filter = struct { 59 | pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) Error!bool { 60 | return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m); 61 | } 62 | }.filter, 63 | }, 64 | }; 65 | } 66 | 67 | pub fn bind(pimpl: anytype, comptime f: *const fn (ctx: @TypeOf(pimpl), from: tp.pid_ref, m: tp.message) Error!bool) Self { 68 | const impl = @typeInfo(@TypeOf(pimpl)); 69 | const child: type = impl.pointer.child; 70 | return .{ 71 | .ptr = pimpl, 72 | .vtable = comptime &.{ 73 | .type_name = @typeName(child), 74 | .deinit = struct { 75 | pub fn deinit(_: *anyopaque) void {} 76 | }.deinit, 77 | .filter = struct { 78 | pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) Error!bool { 79 | return @call(.auto, f, .{ @as(*child, @ptrCast(@alignCast(ctx))), from_, m }); 80 | } 81 | }.filter, 82 | }, 83 | }; 84 | } 85 | 86 | pub fn deinit(self: Self) void { 87 | return self.vtable.deinit(self.ptr); 88 | } 89 | 90 | pub fn dynamic_cast(self: Self, comptime T: type) ?*T { 91 | return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T))) 92 | @as(*T, @ptrCast(@alignCast(self.ptr))) 93 | else 94 | null; 95 | } 96 | 97 | pub fn filter(self: Self, from_: tp.pid_ref, m: tp.message) Error!bool { 98 | return self.vtable.filter(self.ptr, from_, m); 99 | } 100 | 101 | pub const List = struct { 102 | allocator: Allocator, 103 | list: ArrayList(MessageFilter), 104 | 105 | pub fn init(allocator: Allocator) List { 106 | return .{ 107 | .allocator = allocator, 108 | .list = .empty, 109 | }; 110 | } 111 | 112 | pub fn deinit(self: *List) void { 113 | for (self.list.items) |*i| 114 | i.deinit(); 115 | self.list.deinit(self.allocator); 116 | } 117 | 118 | pub fn add(self: *List, h: MessageFilter) error{OutOfMemory}!void { 119 | (try self.list.addOne(self.allocator)).* = h; 120 | // @import("log").print("MessageFilter", "add: {d} {s}", .{ self.list.items.len, self.list.items[self.list.items.len - 1].vtable.type_name }); 121 | } 122 | 123 | pub fn remove(self: *List, h: MessageFilter) !void { 124 | return self.remove_ptr(h.ptr); 125 | } 126 | 127 | pub fn remove_ptr(self: *List, p_: *anyopaque) void { 128 | for (self.list.items, 0..) |*p, i| 129 | if (p.ptr == p_) 130 | self.list.orderedRemove(i).deinit(); 131 | } 132 | 133 | pub fn filter(self: *const List, from: tp.pid_ref, m: tp.message) Error!bool { 134 | var sfa = std.heap.stackFallback(4096, self.allocator); 135 | const a = sfa.get(); 136 | const buf = try a.alloc(u8, m.buf.len); 137 | defer a.free(buf); 138 | @memcpy(buf[0..m.buf.len], m.buf); 139 | const m_: tp.message = .{ .buf = buf[0..m.buf.len] }; 140 | var e: ?Error = null; 141 | for (self.list.items) |*i| { 142 | const consume = i.filter(from, m_) catch |e_| ret: { 143 | e = e_; 144 | break :ret false; 145 | }; 146 | if (consume) 147 | return true; 148 | } 149 | return e orelse false; 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /src/keybind/builtin/emacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "normal": { 3 | "press": [ 4 | ["f4", "toggle_input_mode"], 5 | ["ctrl+0", "reset_fontsize"], 6 | ["ctrl+=", "adjust_fontsize", 1.0], 7 | ["ctrl+-", "adjust_fontsize", -1.0], 8 | ["ctrl+r", "find_file"], 9 | ["ctrl+h ctrl+a", "open_help"], 10 | ["ctrl+c ctrl+o", "open_recent_project"], 11 | ["ctrl+c g", "find_in_files"], 12 | ["alt+x", "open_command_palette"], 13 | ["ctrl+x ctrl+c", "quit"], 14 | 15 | ["ctrl+g", "cancel"], 16 | ["ctrl+_", "undo"], 17 | ["\u001f", "undo"], 18 | ["ctrl+k", ["select_end"], ["cut"]], 19 | ["ctrl+w", "cut"], 20 | 21 | ["ctrl+p", "move_up"], 22 | ["ctrl+n", "move_down"], 23 | ["ctrl+b", "move_left"], 24 | ["ctrl+f", "move_right"], 25 | ["alt+b", "move_word_left"], 26 | ["alt+f", "move_word_right"], 27 | ["ctrl+a", "move_begin"], 28 | ["ctrl+e", "move_end"], 29 | ["alt+<", "move_buffer_begin"], 30 | ["alt+>", "move_buffer_end"], 31 | ["alt+v", "move_page_up"], 32 | ["ctrl+v", "move_page_down"], 33 | ["ctrl+alt+n", "goto_bracket"], 34 | ["ctrl+alt+p", "goto_bracket"], 35 | 36 | ["ctrl+s", "find"], 37 | ["ctrl+d", "delete_forward"], 38 | ["alt+d", ["select_word_right"], ["cut"]], 39 | ["ctrl+y", "system_paste"], 40 | ["ctrl+x ctrl+f", "open_file"], 41 | ["ctrl+x k", "close_file"], 42 | ["ctrl+x ctrl+s", "save_file"], 43 | ["ctrl+x b", "switch_buffers"], 44 | ["ctrl+x ctrl+r", "open_recent"], 45 | ["ctrl+space", "enter_mode", "select"], 46 | 47 | ["alt+0", "add_integer_argument_digit", 0], 48 | ["alt+1", "add_integer_argument_digit", 1], 49 | ["alt+2", "add_integer_argument_digit", 2], 50 | ["alt+3", "add_integer_argument_digit", 3], 51 | ["alt+4", "add_integer_argument_digit", 4], 52 | ["alt+5", "add_integer_argument_digit", 5], 53 | ["alt+6", "add_integer_argument_digit", 6], 54 | ["alt+7", "add_integer_argument_digit", 7], 55 | ["alt+8", "add_integer_argument_digit", 8], 56 | ["alt+9", "add_integer_argument_digit", 9], 57 | 58 | ["ctrl+c l = =", "format"], 59 | ["ctrl+c l = r", "format"], 60 | ["ctrl+c l g g", "goto_definition"], 61 | ["ctrl+c l g i", "goto_implementation"], 62 | ["ctrl+c l g d", "goto_declaration"], 63 | ["ctrl+c l g r", "references"], 64 | ["ctrl+c l h h", "hover"], 65 | ["ctrl+c l r r", "rename_symbol"], 66 | ["ctrl+c f", "copy_file_name"], 67 | 68 | ["super+l = =", "format"], 69 | ["super+l = r", "format"], 70 | ["super+l g g", "goto_definition"], 71 | ["super+l g i", "goto_implementation"], 72 | ["super+l g d", "goto_declaration"], 73 | ["super+l g r", "references"], 74 | ["super+l h h", "hover"], 75 | ["super+l r r", "rename_symbol"] 76 | ] 77 | }, 78 | "select": { 79 | "name": "SELECT", 80 | "inherit": "normal", 81 | "press": [ 82 | ["ctrl+space", ["enter_mode", "normal"], ["cancel"]], 83 | ["ctrl+g", ["enter_mode", "normal"], ["cancel"]], 84 | ["ctrl+w", ["cut"], ["enter_mode", "normal"], ["cancel"]], 85 | ["alt+w", ["copy"], ["enter_mode", "normal"], ["cancel"]], 86 | 87 | ["ctrl+p", "select_up"], 88 | ["ctrl+n", "select_down"], 89 | ["ctrl+b", "select_left"], 90 | ["ctrl+f", "select_right"], 91 | ["alt+b", "select_word_left"], 92 | ["alt+f", "select_word_right"], 93 | ["ctrl+a", "select_begin"], 94 | ["ctrl+e", "select_end"], 95 | ["alt+<", "select_buffer_begin"], 96 | ["alt+>", "select_buffer_end"], 97 | ["alt+v", "select_page_up"], 98 | ["ctrl+v", "select_page_down"] 99 | ] 100 | }, 101 | "overlay/palette": { 102 | "press": [ 103 | ["ctrl+a", "palette_menu_top"], 104 | ["ctrl+f", "palette_menu_down"] 105 | ] 106 | }, 107 | "mini/file_browser": { 108 | "press": [ 109 | ["alt+backspace", "mini_mode_delete_to_previous_path_segment"] 110 | ] 111 | }, 112 | "home": { 113 | "on_match_failure": "ignore", 114 | "press": [ 115 | ["f", "change_fontface"], 116 | ["f4", "toggle_input_mode"], 117 | ["ctrl+0", "reset_fontsize"], 118 | ["ctrl+=", "adjust_fontsize", 1.0], 119 | ["ctrl+-", "adjust_fontsize", -1.0], 120 | ["ctrl+r", "find_file"], 121 | ["ctrl+h ctrl+a", "open_help"], 122 | ["ctrl+x ctrl+f", "open_file"], 123 | ["ctrl+x ctrl+r", "open_recent"], 124 | ["ctrl+x b", "switch_buffers"], 125 | ["ctrl+c ctrl+o", "open_recent_project"], 126 | ["ctrl+c g", "find_in_files"], 127 | ["alt+x", "open_command_palette"], 128 | ["ctrl+x ctrl+c", "quit"] 129 | ] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/tui/status/modestate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const Allocator = std.mem.Allocator; 4 | 5 | const Plane = @import("renderer").Plane; 6 | const style = @import("renderer").style; 7 | const styles = @import("renderer").styles; 8 | const command = @import("command"); 9 | const EventHandler = @import("EventHandler"); 10 | 11 | const Widget = @import("../Widget.zig"); 12 | const Button = @import("../Button.zig"); 13 | const tui = @import("../tui.zig"); 14 | const CreateError = @import("widget.zig").CreateError; 15 | 16 | const Style = enum { 17 | plain, 18 | fancy, 19 | plain_root, 20 | fancy_root, 21 | }; 22 | const default_style = .fancy; 23 | 24 | const ButtonType = Button.Options(Style).ButtonType; 25 | 26 | pub fn create(allocator: Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) CreateError!Widget { 27 | const style_ = if (arg) |str_style| std.meta.stringToEnum(Style, str_style) orelse default_style else default_style; 28 | return Button.create_widget(Style, allocator, parent, .{ 29 | .ctx = if (builtin.os.tag != .windows and std.posix.geteuid() == 0) switch (style_) { 30 | .fancy => .fancy_root, 31 | .plain => .plain_root, 32 | else => style_, 33 | } else style_, 34 | .label = tui.get_mode(), 35 | .on_click = on_click, 36 | .on_click2 = toggle_panel, 37 | .on_click3 = toggle_panel, 38 | .on_layout = layout, 39 | .on_render = render, 40 | .on_event = event_handler, 41 | }); 42 | } 43 | 44 | pub fn layout(_: *Style, btn: *ButtonType) Widget.Layout { 45 | const name = btn.plane.egc_chunk_width(tui.get_mode(), 0, 1); 46 | const logo = if (is_mini_mode() or is_overlay_mode()) 1 else btn.plane.egc_chunk_width(left ++ symbol ++ right, 0, 1); 47 | const padding: usize = 2; 48 | const minimode_sep: usize = if (is_mini_mode()) 1 else 0; 49 | return .{ .static = logo + name + padding + minimode_sep }; 50 | } 51 | 52 | fn is_mini_mode() bool { 53 | return tui.mini_mode() != null; 54 | } 55 | 56 | fn is_overlay_mode() bool { 57 | return tui.input_mode_outer() != null; 58 | } 59 | 60 | pub fn render(ctx: *Style, self: *ButtonType, theme: *const Widget.Theme) bool { 61 | const style_base = theme.statusbar; 62 | const style_label = switch (ctx.*) { 63 | .fancy, .fancy_root => if (self.active) theme.editor_cursor else if (self.hover) theme.editor_selection else theme.statusbar_hover, 64 | .plain, .plain_root => if (self.active) theme.editor_cursor else if (self.hover or is_mini_mode()) theme.statusbar_hover else style_base, 65 | }; 66 | self.plane.set_base_style(theme.editor); 67 | self.plane.erase(); 68 | self.plane.home(); 69 | self.plane.set_style(style_base); 70 | self.plane.fill(" "); 71 | self.plane.home(); 72 | self.plane.set_style(style_label); 73 | self.plane.fill(" "); 74 | self.plane.home(); 75 | self.plane.on_styles(styles.bold); 76 | var buf: [31:0]u8 = undefined; 77 | if (!is_mini_mode() and !is_overlay_mode()) { 78 | render_logo(ctx, self, theme, style_label); 79 | } else { 80 | _ = self.plane.putstr(" ") catch {}; 81 | } 82 | self.plane.set_style(style_label); 83 | self.plane.on_styles(styles.bold); 84 | _ = self.plane.putstr(std.fmt.bufPrintZ(&buf, "{s} ", .{tui.get_mode()}) catch return false) catch {}; 85 | if (is_mini_mode()) 86 | render_separator(self, theme); 87 | return false; 88 | } 89 | 90 | fn render_separator(self: *ButtonType, theme: *const Widget.Theme) void { 91 | self.plane.reverse_style(); 92 | self.plane.set_base_style(.{ .bg = theme.editor.bg }); 93 | if (theme.statusbar.bg) |bg| self.plane.set_style(.{ .bg = bg }); 94 | _ = self.plane.putstr("") catch {}; 95 | } 96 | 97 | const left = " "; 98 | const symbol = "󱞏"; 99 | const symbol_root = ""; 100 | const right = " "; 101 | 102 | fn render_logo(ctx: *Style, self: *ButtonType, theme: *const Widget.Theme, style_label: Widget.Theme.Style) void { 103 | const style_root = theme.editor_error; 104 | 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; 105 | if (left.len > 0) { 106 | self.plane.set_style(style_braces); 107 | _ = self.plane.putstr(" " ++ left) catch {}; 108 | } 109 | switch (ctx.*) { 110 | .fancy_root, .plain_root => { 111 | self.plane.set_style(style_root); 112 | _ = self.plane.putstr(symbol_root) catch {}; 113 | }, 114 | else => { 115 | self.plane.set_style(style_label); 116 | _ = self.plane.putstr(symbol) catch {}; 117 | }, 118 | } 119 | if (right.len > 0) { 120 | self.plane.set_style(style_braces); 121 | _ = self.plane.putstr(right) catch {}; 122 | } 123 | } 124 | 125 | fn on_click(_: *Style, _: *ButtonType, _: Widget.Pos) void { 126 | if (is_mini_mode()) { 127 | command.executeName("exit_mini_mode", .{}) catch {}; 128 | } else if (is_overlay_mode()) { 129 | command.executeName("exit_overlay_mode", .{}) catch {}; 130 | } else { 131 | command.executeName("open_command_palette", .{}) catch {}; 132 | } 133 | } 134 | 135 | fn toggle_panel(_: *Style, _: *ButtonType, _: Widget.Pos) void { 136 | command.executeName("toggle_panel", .{}) catch {}; 137 | } 138 | -------------------------------------------------------------------------------- /src/tui/mode/mini/buffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tp = @import("thespian"); 3 | const cbor = @import("cbor"); 4 | const log = @import("log"); 5 | const root = @import("soft_root").root; 6 | 7 | const input = @import("input"); 8 | const keybind = @import("keybind"); 9 | const command = @import("command"); 10 | const EventHandler = @import("EventHandler"); 11 | 12 | const tui = @import("../../tui.zig"); 13 | 14 | pub fn Create(options: type) type { 15 | return struct { 16 | allocator: std.mem.Allocator, 17 | input: std.ArrayList(u8), 18 | commands: Commands = undefined, 19 | 20 | const Commands = command.Collection(cmds); 21 | const Self = @This(); 22 | 23 | pub fn create(allocator: std.mem.Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { 24 | const self = try allocator.create(Self); 25 | errdefer allocator.destroy(self); 26 | self.* = .{ 27 | .allocator = allocator, 28 | .input = .empty, 29 | }; 30 | try self.commands.init(self); 31 | if (@hasDecl(options, "restore_state")) 32 | options.restore_state(self) catch {}; 33 | var mode = try keybind.mode("mini/buffer", allocator, .{ 34 | .insert_command = "mini_mode_insert_bytes", 35 | }); 36 | mode.event_handler = EventHandler.to_owned(self); 37 | return .{ mode, .{ .name = options.name(self) } }; 38 | } 39 | 40 | pub fn deinit(self: *Self) void { 41 | self.commands.deinit(); 42 | self.input.deinit(self.allocator); 43 | self.allocator.destroy(self); 44 | } 45 | 46 | pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 47 | var text: []const u8 = undefined; 48 | 49 | if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { 50 | self.input.appendSlice(self.allocator, text) catch |e| return tp.exit_error(e, @errorReturnTrace()); 51 | } 52 | self.update_mini_mode_text(); 53 | return false; 54 | } 55 | 56 | fn message(comptime fmt: anytype, args: anytype) void { 57 | var buf: [256]u8 = undefined; 58 | tp.self_pid().send(.{ "message", std.fmt.bufPrint(&buf, fmt, args) catch @panic("too large") }) catch {}; 59 | } 60 | 61 | fn update_mini_mode_text(self: *Self) void { 62 | if (tui.mini_mode()) |mini_mode| { 63 | mini_mode.text = self.input.items; 64 | mini_mode.cursor = tui.egc_chunk_width(self.input.items, 0, 1); 65 | } 66 | } 67 | 68 | const cmds = struct { 69 | pub const Target = Self; 70 | const Ctx = command.Context; 71 | const Meta = command.Metadata; 72 | const Result = command.Result; 73 | 74 | pub fn mini_mode_reset(self: *Self, _: Ctx) Result { 75 | self.input.clearRetainingCapacity(); 76 | self.update_mini_mode_text(); 77 | } 78 | pub const mini_mode_reset_meta: Meta = .{ .description = "Clear input" }; 79 | 80 | pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { 81 | command.executeName("exit_mini_mode", .{}) catch {}; 82 | } 83 | pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; 84 | 85 | pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { 86 | if (self.input.items.len > 0) { 87 | self.input.shrinkRetainingCapacity(self.input.items.len - tui.egc_last(self.input.items).len); 88 | } 89 | self.update_mini_mode_text(); 90 | } 91 | pub const mini_mode_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; 92 | 93 | pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { 94 | var egc: u32 = 0; 95 | if (!try ctx.args.match(.{tp.extract(&egc)})) 96 | return error.InvalidMiniBufferInsertCodePointArgument; 97 | var buf: [32]u8 = undefined; 98 | const bytes = try input.ucs32_to_utf8(&[_]u32{egc}, &buf); 99 | try self.input.appendSlice(self.allocator, buf[0..bytes]); 100 | self.update_mini_mode_text(); 101 | } 102 | pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; 103 | 104 | pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { 105 | var bytes: []const u8 = undefined; 106 | if (!try ctx.args.match(.{tp.extract(&bytes)})) 107 | return error.InvalidMiniBufferInsertBytesArgument; 108 | try self.input.appendSlice(self.allocator, bytes); 109 | self.update_mini_mode_text(); 110 | } 111 | pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; 112 | 113 | pub fn mini_mode_select(self: *Self, _: Ctx) Result { 114 | options.select(self); 115 | self.update_mini_mode_text(); 116 | } 117 | pub const mini_mode_select_meta: Meta = .{ .description = "Select" }; 118 | 119 | pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { 120 | return mini_mode_insert_bytes(self, ctx); 121 | } 122 | pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; 123 | }; 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/tui/status/linenumstate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const tp = @import("thespian"); 4 | const Buffer = @import("Buffer"); 5 | const config = @import("config"); 6 | 7 | const Plane = @import("renderer").Plane; 8 | const command = @import("command"); 9 | const EventHandler = @import("EventHandler"); 10 | 11 | const Widget = @import("../Widget.zig"); 12 | const Button = @import("../Button.zig"); 13 | const fonts = @import("../fonts.zig"); 14 | 15 | const DigitStyle = fonts.DigitStyle; 16 | 17 | const utf8_sanitized_warning = "  UTF"; 18 | 19 | line: usize = 0, 20 | lines: usize = 0, 21 | column: usize = 0, 22 | buf: [256]u8 = undefined, 23 | rendered: [:0]const u8 = "", 24 | eol_mode: Buffer.EolMode = .lf, 25 | utf8_sanitized: bool = false, 26 | indent_mode: config.IndentMode = .spaces, 27 | padding: ?usize, 28 | leader: ?Leader, 29 | style: ?DigitStyle, 30 | 31 | const Leader = enum { 32 | space, 33 | zero, 34 | }; 35 | const Self = @This(); 36 | const ButtonType = Button.Options(Self).ButtonType; 37 | 38 | pub fn create(allocator: Allocator, parent: Plane, event_handler: ?EventHandler, arg: ?[]const u8) @import("widget.zig").CreateError!Widget { 39 | const padding: ?usize, const leader: ?Leader, const style: ?DigitStyle = if (arg) |fmt| blk: { 40 | var it = std.mem.splitScalar(u8, fmt, ','); 41 | break :blk .{ 42 | if (it.next()) |size| std.fmt.parseInt(usize, size, 10) catch null else null, 43 | if (it.next()) |leader| std.meta.stringToEnum(Leader, leader) orelse null else null, 44 | if (it.next()) |style| std.meta.stringToEnum(DigitStyle, style) orelse null else null, 45 | }; 46 | } else .{ null, null, null }; 47 | 48 | return Button.create_widget(Self, allocator, parent, .{ 49 | .ctx = .{ 50 | .padding = padding, 51 | .leader = leader, 52 | .style = style, 53 | }, 54 | .label = "", 55 | .on_click = on_click, 56 | .on_layout = layout, 57 | .on_render = render, 58 | .on_receive = receive, 59 | .on_event = event_handler, 60 | }); 61 | } 62 | 63 | fn on_click(_: *Self, _: *ButtonType, _: Widget.Pos) void { 64 | command.executeName("goto", .{}) catch {}; 65 | } 66 | 67 | pub fn layout(self: *Self, btn: *ButtonType) Widget.Layout { 68 | const warn_len = if (self.utf8_sanitized) btn.plane.egc_chunk_width(utf8_sanitized_warning, 0, 1) else 0; 69 | const len = btn.plane.egc_chunk_width(self.rendered, 0, 1) + warn_len; 70 | return .{ .static = len }; 71 | } 72 | 73 | pub fn render(self: *Self, btn: *ButtonType, theme: *const Widget.Theme) bool { 74 | btn.plane.set_base_style(theme.editor); 75 | btn.plane.erase(); 76 | btn.plane.home(); 77 | btn.plane.set_style(if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar); 78 | btn.plane.fill(" "); 79 | btn.plane.home(); 80 | if (self.utf8_sanitized) { 81 | btn.plane.set_style(.{ .fg = theme.editor_error.fg.? }); 82 | _ = btn.plane.putstr(utf8_sanitized_warning) catch {}; 83 | } 84 | btn.plane.set_style(if (btn.active) theme.editor_cursor else if (btn.hover) theme.statusbar_hover else theme.statusbar); 85 | _ = btn.plane.putstr(self.rendered) catch {}; 86 | return false; 87 | } 88 | 89 | fn format(self: *Self) void { 90 | var fbs = std.io.fixedBufferStream(&self.buf); 91 | const writer = fbs.writer(); 92 | const eol_mode = switch (self.eol_mode) { 93 | .lf => "", 94 | .crlf => " ␍␊", 95 | }; 96 | const indent_mode = switch (self.indent_mode) { 97 | .spaces, .auto => "", 98 | .tabs => " ⭾ ", 99 | }; 100 | std.fmt.format(writer, "{s}{s} Ln ", .{ eol_mode, indent_mode }) catch {}; 101 | self.format_count(writer, self.line + 1, self.padding orelse 0) catch {}; 102 | std.fmt.format(writer, ", Col ", .{}) catch {}; 103 | self.format_count(writer, self.column + 1, self.padding orelse 0) catch {}; 104 | std.fmt.format(writer, " ", .{}) catch {}; 105 | self.rendered = @ptrCast(fbs.getWritten()); 106 | self.buf[self.rendered.len] = 0; 107 | } 108 | 109 | fn format_count(self: *Self, writer: anytype, value: usize, width: usize) !void { 110 | var buf: [64]u8 = undefined; 111 | var fbs = std.io.fixedBufferStream(&buf); 112 | const writer_ = fbs.writer(); 113 | try std.fmt.format(writer_, "{d}", .{value}); 114 | const value_str = fbs.getWritten(); 115 | 116 | const char: []const u8 = switch (self.leader orelse .space) { 117 | .space => " ", 118 | .zero => "0", 119 | }; 120 | for (0..(@max(value_str.len, width) - value_str.len)) |_| try writer.writeAll(fonts.get_digit_ascii(char, self.style orelse .ascii)); 121 | for (value_str, 0..) |_, i| try writer.writeAll(fonts.get_digit_ascii(value_str[i .. i + 1], self.style orelse .ascii)); 122 | } 123 | 124 | pub fn receive(self: *Self, _: *ButtonType, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 125 | if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) })) { 126 | self.format(); 127 | } else if (try m.match(.{ "E", "eol_mode", tp.extract(&self.eol_mode), tp.extract(&self.utf8_sanitized), tp.extract(&self.indent_mode) })) { 128 | self.format(); 129 | } else if (try m.match(.{ "E", "open", tp.more })) { 130 | self.eol_mode = .lf; 131 | } else if (try m.match(.{ "E", "close" })) { 132 | self.lines = 0; 133 | self.line = 0; 134 | self.column = 0; 135 | self.rendered = ""; 136 | self.eol_mode = .lf; 137 | self.utf8_sanitized = false; 138 | } 139 | return false; 140 | } 141 | -------------------------------------------------------------------------------- /contrib/make_nightly_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for arg in "$@"; do 5 | case "$arg" in 6 | --no-github) NO_GITHUB=1 ;; 7 | --no-codeberg) NO_CODEBERG=1 ;; 8 | --no-flowcontrol) NO_FLOWCONTROL=1 ;; 9 | --allow-dirty) ALLOW_DIRTY=1 ;; 10 | esac 11 | done 12 | 13 | builddir="nightly-build" 14 | 15 | DESTDIR="$(pwd)/$builddir" 16 | BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" 17 | APPNAME="$(basename "$BASEDIR")" 18 | title="$APPNAME nightly build" 19 | repo="neurocyte/$APPNAME-nightly" 20 | 21 | release_notes="$BASEDIR/$builddir-release-notes" 22 | 23 | cd "$BASEDIR" 24 | 25 | if [ -e "$DESTDIR" ]; then 26 | echo directory \"$builddir\" already exists 27 | exit 1 28 | fi 29 | 30 | if [ -e "$release_notes" ]; then 31 | echo file \""$release_notes"\" already exists 32 | exit 1 33 | fi 34 | 35 | DIFF="$(git diff --stat --patch HEAD)" 36 | 37 | if [ -z "$ALLOW_DIRTY" ]; then 38 | if [ -n "$DIFF" ]; then 39 | echo there are outstanding changes: 40 | echo "$DIFF" 41 | exit 1 42 | fi 43 | 44 | UNPUSHED="$(git log --pretty=oneline '@{u}...')" 45 | 46 | if [ -n "$UNPUSHED" ]; then 47 | echo there are unpushed commits: 48 | echo "$UNPUSHED" 49 | exit 1 50 | fi 51 | fi 52 | 53 | # get latest version tag 54 | 55 | if [ -z "$NO_FLOWCONTROL" ]; then 56 | last_nightly_version=$(curl -s https://git.flow-control.dev/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) 57 | elif [ -z "$NO_GITHUB" ]; then 58 | last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name) 59 | elif [ -z "$NO_CODEBERG" ]; then 60 | last_nightly_version=$(curl -s https://codeberg.org/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) 61 | fi 62 | [ -z "$last_nightly_version" ] && { 63 | echo "failed to fetch $title latest version" 64 | exit 1 65 | } 66 | 67 | local_version="$(git --git-dir "$BASEDIR/.git" describe)" 68 | if [ "$1" != "--no-github" ]; then 69 | if [ "$local_version" == "$last_nightly_version" ]; then 70 | echo "$title is already at version $last_nightly_version" 71 | exit 1 72 | fi 73 | fi 74 | 75 | echo 76 | echo "building $title version $local_version... (previous $last_nightly_version)" 77 | echo 78 | git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" 79 | echo 80 | echo running tests... 81 | 82 | zig build test 83 | 84 | echo building... 85 | 86 | zig build -Dall_targets --release --prefix "$DESTDIR/build" 87 | 88 | VERSION=$(/bin/cat "$DESTDIR/build/version") 89 | 90 | git archive --format=tar.gz --output="$DESTDIR/flow-$VERSION-source.tar.gz" HEAD 91 | git archive --format=zip --output="$DESTDIR/flow-$VERSION-source.zip" HEAD 92 | 93 | cd "$DESTDIR/build" 94 | 95 | TARGETS=$(/bin/ls) 96 | 97 | for target in $TARGETS; do 98 | if [ -d "$target" ]; then 99 | cd "$target" 100 | if [ "${target:0:8}" == "windows-" ]; then 101 | echo packing zip "$target"... 102 | zip -r "../../${APPNAME}-${VERSION}-${target}.zip" ./* 103 | cd .. 104 | else 105 | echo packing tar "$target"... 106 | tar -czf "../../${APPNAME}-${VERSION}-${target}.tar.gz" -- * 107 | cd .. 108 | fi 109 | fi 110 | done 111 | 112 | cd .. 113 | rm -r build 114 | 115 | TARFILES=$(/bin/ls) 116 | 117 | for tarfile in $TARFILES; do 118 | echo signing "$tarfile"... 119 | gpg --local-user 4E6CF7234FFC4E14531074F98EB1E1BB660E3FB9 --detach-sig "$tarfile" 120 | sha256sum -b "$tarfile" >"${tarfile}.sha256" 121 | done 122 | 123 | echo "done making $title $VERSION @ $DESTDIR" 124 | echo 125 | 126 | /bin/ls -lah 127 | 128 | cd .. 129 | 130 | { 131 | echo "## commits in this build" 132 | echo 133 | git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" 134 | echo 135 | echo 136 | 137 | echo "## contributors" 138 | git shortlog -s -n "${last_nightly_version}..HEAD" | cut -b 8- 139 | echo 140 | 141 | echo "## downloads" 142 | echo "[flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION) (source only)" 143 | echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" 144 | echo "[codeberg.org](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" 145 | } >"$release_notes" 146 | 147 | cat "$release_notes" 148 | 149 | ASSETS="" 150 | 151 | if [ -z "$NO_FLOWCONTROL" ]; then 152 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz" 153 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sig" 154 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sha256" 155 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip" 156 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sig" 157 | ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sha256" 158 | echo uploading to git.flow-control.dev 159 | tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ 160 | $ASSETS 161 | fi 162 | 163 | if [ -z "$NO_CODEBERG" ]; then 164 | for a in $DESTDIR/*; do 165 | ASSETS="$ASSETS --asset $a" 166 | done 167 | echo uploading to codeberg.org 168 | tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ 169 | $ASSETS 170 | fi 171 | 172 | if [ -z "$NO_GITHUB" ]; then 173 | echo uploading to github.com 174 | gh release create "$VERSION" --repo "$repo" --title "$title $VERSION" --notes-file "$release_notes" $DESTDIR/* 175 | fi 176 | -------------------------------------------------------------------------------- /src/tui/mode/overlay/command_palette.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor"); 3 | const tp = @import("thespian"); 4 | const root = @import("soft_root").root; 5 | const command = @import("command"); 6 | 7 | const tui = @import("../../tui.zig"); 8 | pub const Type = @import("palette.zig").Create(@This()); 9 | 10 | pub const label = "Search commands"; 11 | pub const name = "󱊒 command"; 12 | pub const description = "command"; 13 | 14 | pub const Entry = struct { 15 | label: []const u8, 16 | name: []const u8, 17 | hint: []const u8, 18 | id: command.ID, 19 | used_time: i64, 20 | }; 21 | 22 | pub fn load_entries(palette: *Type) !usize { 23 | const hints = if (tui.input_mode()) |m| m.keybind_hints else @panic("no keybind hints"); 24 | var longest_description: usize = 0; 25 | var longest_total: usize = 0; 26 | for (command.commands.items) |cmd_| if (cmd_) |p| { 27 | if (p.meta.description.len > 0) { 28 | const hint = hints.get(p.name) orelse ""; 29 | longest_description = @max(longest_description, p.meta.description.len); 30 | longest_total = @max(longest_total, p.meta.description.len + hint.len + 1); 31 | (try palette.entries.addOne(palette.allocator)).* = .{ 32 | .label = if (p.meta.description.len > 0) p.meta.description else p.name, 33 | .name = p.name, 34 | .hint = hint, 35 | .id = p.id, 36 | .used_time = 0, 37 | }; 38 | } 39 | }; 40 | return longest_total - longest_description; 41 | } 42 | 43 | pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { 44 | var value: std.Io.Writer.Allocating = .init(palette.allocator); 45 | defer value.deinit(); 46 | const writer = &value.writer; 47 | try cbor.writeValue(writer, entry.label); 48 | try cbor.writeValue(writer, entry.hint); 49 | try cbor.writeValue(writer, matches orelse &[_]usize{}); 50 | try cbor.writeValue(writer, entry.id); 51 | try palette.menu.add_item_with_handler(value.written(), select); 52 | palette.items += 1; 53 | } 54 | 55 | fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void { 56 | var unused: []const u8 = undefined; 57 | var command_id: command.ID = undefined; 58 | var iter = button.opts.label; 59 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 60 | if (!(cbor.matchString(&iter, &unused) catch false)) return; 61 | var len = cbor.decodeArrayHeader(&iter) catch return; 62 | while (len > 0) : (len -= 1) 63 | cbor.skipValue(&iter) catch break; 64 | if (!(cbor.matchValue(&iter, cbor.extract(&command_id)) catch false)) return; 65 | update_used_time(menu.*.opts.ctx, command_id); 66 | tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("command_palette", e); 67 | tp.self_pid().send(.{ "cmd", command_id, .{} }) catch |e| menu.*.opts.ctx.logger.err("command_palette", e); 68 | } 69 | 70 | fn sort_by_used_time(palette: *Type) void { 71 | const less_fn = struct { 72 | fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { 73 | return lhs.used_time > rhs.used_time; 74 | } 75 | }.less_fn; 76 | std.mem.sort(Entry, palette.entries.items, {}, less_fn); 77 | } 78 | 79 | fn update_used_time(palette: *Type, id: command.ID) void { 80 | set_used_time(palette, id, std.time.milliTimestamp()); 81 | write_state(palette) catch {}; 82 | } 83 | 84 | fn set_used_time(palette: *Type, id: command.ID, used_time: i64) void { 85 | for (palette.entries.items) |*cmd_| if (cmd_.id == id) { 86 | cmd_.used_time = used_time; 87 | return; 88 | }; 89 | } 90 | 91 | fn write_state(palette: *Type) !void { 92 | var state_file_buffer: [std.fs.max_path_bytes]u8 = undefined; 93 | const state_file = try std.fmt.bufPrint(&state_file_buffer, "{s}/{s}", .{ try root.get_state_dir(), "commands" }); 94 | var file = try std.fs.createFileAbsolute(state_file, .{ .truncate = true }); 95 | defer file.close(); 96 | var buf: [4096]u8 = undefined; 97 | var file_writer = file.writer(&buf); 98 | const writer = &file_writer.interface; 99 | defer writer.flush() catch {}; 100 | 101 | for (palette.entries.items) |cmd_| { 102 | if (cmd_.used_time == 0) continue; 103 | try cbor.writeArrayHeader(writer, 2); 104 | try cbor.writeValue(writer, cmd_.name); 105 | try cbor.writeValue(writer, cmd_.used_time); 106 | } 107 | } 108 | 109 | pub fn restore_state(palette: *Type) !void { 110 | var state_file_buffer: [std.fs.max_path_bytes]u8 = undefined; 111 | const state_file = try std.fmt.bufPrint(&state_file_buffer, "{s}/{s}", .{ try root.get_state_dir(), "commands" }); 112 | const a = std.heap.c_allocator; 113 | var file = std.fs.openFileAbsolute(state_file, .{ .mode = .read_only }) catch |e| switch (e) { 114 | error.FileNotFound => return, 115 | else => return e, 116 | }; 117 | defer file.close(); 118 | const stat = try file.stat(); 119 | var buffer = try a.alloc(u8, @intCast(stat.size)); 120 | defer a.free(buffer); 121 | const size = try file.readAll(buffer); 122 | const data = buffer[0..size]; 123 | 124 | defer sort_by_used_time(palette); 125 | var name_: []const u8 = undefined; 126 | var used_time: i64 = undefined; 127 | var iter: []const u8 = data; 128 | while (cbor.matchValue(&iter, .{ 129 | tp.extract(&name_), 130 | tp.extract(&used_time), 131 | }) catch |e| switch (e) { 132 | error.TooShort => return, 133 | else => return e, 134 | }) { 135 | const id = command.get_id(name_) orelse continue; 136 | set_used_time(palette, id, used_time); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/tui/mode/mini/find_in_files.zig: -------------------------------------------------------------------------------- 1 | const tp = @import("thespian"); 2 | 3 | const input = @import("input"); 4 | const keybind = @import("keybind"); 5 | const command = @import("command"); 6 | const EventHandler = @import("EventHandler"); 7 | 8 | const tui = @import("../../tui.zig"); 9 | 10 | const Allocator = @import("std").mem.Allocator; 11 | const eql = @import("std").mem.eql; 12 | 13 | const Self = @This(); 14 | const name = "󰥨 find"; 15 | 16 | const Commands = command.Collection(cmds); 17 | 18 | const max_query_size = 1024; 19 | 20 | allocator: Allocator, 21 | buf: [max_query_size]u8 = undefined, 22 | input_: []u8 = "", 23 | last_buf: [max_query_size]u8 = undefined, 24 | last_input: []u8 = "", 25 | commands: Commands = undefined, 26 | 27 | pub fn create(allocator: Allocator, _: command.Context) !struct { tui.Mode, tui.MiniMode } { 28 | const self = try allocator.create(Self); 29 | errdefer allocator.destroy(self); 30 | self.* = .{ .allocator = allocator }; 31 | try self.commands.init(self); 32 | if (tui.get_active_selection(self.allocator)) |text| { 33 | defer self.allocator.free(text); 34 | @memcpy(self.buf[0..text.len], text); 35 | self.input_ = self.buf[0..text.len]; 36 | } 37 | var mode = try keybind.mode("mini/find_in_files", allocator, .{ 38 | .insert_command = "mini_mode_insert_bytes", 39 | }); 40 | mode.event_handler = EventHandler.to_owned(self); 41 | return .{ mode, .{ .name = name } }; 42 | } 43 | 44 | pub fn deinit(self: *Self) void { 45 | self.commands.deinit(); 46 | self.allocator.destroy(self); 47 | } 48 | 49 | pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { 50 | var text: []const u8 = undefined; 51 | 52 | defer self.update_mini_mode_text(); 53 | 54 | if (try m.match(.{"F"})) { 55 | self.start_query() catch |e| return tp.exit_error(e, @errorReturnTrace()); 56 | } else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { 57 | self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace()); 58 | } 59 | return false; 60 | } 61 | 62 | fn insert_code_point(self: *Self, c: u32) !void { 63 | if (self.input_.len + 6 >= self.buf.len) 64 | return; 65 | const bytes = try input.ucs32_to_utf8(&[_]u32{c}, self.buf[self.input_.len..]); 66 | self.input_ = self.buf[0 .. self.input_.len + bytes]; 67 | } 68 | 69 | fn insert_bytes(self: *Self, bytes_: []const u8) !void { 70 | const bytes = bytes_[0..@min(self.buf.len - self.input_.len, bytes_.len)]; 71 | const newlen = self.input_.len + bytes.len; 72 | @memcpy(self.buf[self.input_.len..newlen], bytes); 73 | self.input_ = self.buf[0..newlen]; 74 | } 75 | 76 | fn start_query(self: *Self) !void { 77 | if (self.input_.len < 2 or eql(u8, self.input_, self.last_input)) 78 | return; 79 | @memcpy(self.last_buf[0..self.input_.len], self.input_); 80 | self.last_input = self.last_buf[0..self.input_.len]; 81 | try command.executeName("find_in_files_query", command.fmt(.{self.input_})); 82 | } 83 | 84 | fn update_mini_mode_text(self: *Self) void { 85 | if (tui.mini_mode()) |mini_mode| { 86 | mini_mode.text = self.input_; 87 | mini_mode.cursor = tui.egc_chunk_width(self.input_, 0, 1); 88 | } 89 | } 90 | 91 | const cmds = struct { 92 | pub const Target = Self; 93 | const Ctx = command.Context; 94 | const Meta = command.Metadata; 95 | const Result = command.Result; 96 | 97 | pub fn mini_mode_reset(self: *Self, _: Ctx) Result { 98 | self.input_ = ""; 99 | self.update_mini_mode_text(); 100 | } 101 | pub const mini_mode_reset_meta: Meta = .{ .description = "Clear input" }; 102 | 103 | pub fn mini_mode_cancel(_: *Self, _: Ctx) Result { 104 | command.executeName("close_find_in_files_results", .{}) catch {}; 105 | command.executeName("exit_mini_mode", .{}) catch {}; 106 | } 107 | pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; 108 | 109 | pub fn mini_mode_select(_: *Self, _: Ctx) Result { 110 | command.executeName("goto_selected_file", .{}) catch {}; 111 | return command.executeName("exit_mini_mode", .{}); 112 | } 113 | pub const mini_mode_select_meta: Meta = .{ .description = "Select" }; 114 | 115 | pub fn mini_mode_insert_code_point(self: *Self, ctx: Ctx) Result { 116 | var egc: u32 = 0; 117 | if (!try ctx.args.match(.{tp.extract(&egc)})) 118 | return error.InvalidFindInFilesInsertCodePointArgument; 119 | self.insert_code_point(egc) catch |e| return tp.exit_error(e, @errorReturnTrace()); 120 | self.update_mini_mode_text(); 121 | } 122 | pub const mini_mode_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} }; 123 | 124 | pub fn mini_mode_insert_bytes(self: *Self, ctx: Ctx) Result { 125 | var bytes: []const u8 = undefined; 126 | if (!try ctx.args.match(.{tp.extract(&bytes)})) 127 | return error.InvalidFindInFilesInsertBytesArgument; 128 | self.insert_bytes(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace()); 129 | self.update_mini_mode_text(); 130 | } 131 | pub const mini_mode_insert_bytes_meta: Meta = .{ .arguments = &.{.string} }; 132 | 133 | pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { 134 | self.input_ = self.input_[0 .. self.input_.len - tui.egc_last(self.input_).len]; 135 | self.update_mini_mode_text(); 136 | } 137 | pub const mini_mode_delete_backwards_meta: Meta = .{ .description = "Delete backwards" }; 138 | 139 | pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result { 140 | return mini_mode_insert_bytes(self, ctx); 141 | } 142 | pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} }; 143 | }; 144 | --------------------------------------------------------------------------------