├── .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 |
--------------------------------------------------------------------------------
|