Initial public release

This commit is contained in:
CJ van den Berg 2024-02-29 00:00:15 +01:00
parent 3c3f068914
commit 4ece4babad
63 changed files with 15101 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.cache/
/zig-out/
/zig-cache/

290
build.zig Normal file
View file

@ -0,0 +1,290 @@
const std = @import("std");
const CrossTarget = std.zig.CrossTarget;
const cppflags = [_][]const u8{
"-fcolor-diagnostics",
"-std=c++20",
"-Wall",
"-Wextra",
"-Werror",
"-Wno-unqualified-std-cast-call",
"-Wno-bitwise-instead-of-logical", //for notcurses
"-fno-sanitize=undefined",
"-gen-cdb-fragment-path",
".cache/cdb",
};
pub fn build(b: *std.Build) void {
const enable_tracy_option = b.option(bool, "enable_tracy", "Enable tracy client library (default: no)");
const optimize_deps_option = b.option(bool, "optimize_deps", "Enable optimization for dependecies (default: yes)");
const use_llvm_option = b.option(bool, "use_llvm", "Enable llvm backend (default: yes)");
const use_lld_option = b.option(bool, "use_lld", "Enable lld backend (default: yes)");
const use_system_notcurses = b.option(bool, "use_system_notcurses", "Build against system notcurses (default: no)") orelse false;
const tracy_enabled = if (enable_tracy_option) |enabled| enabled else false;
const optimize_deps_enabled = if (optimize_deps_option) |enabled| enabled else true;
const options = b.addOptions();
options.addOption(bool, "enable_tracy", tracy_enabled);
options.addOption(bool, "optimize_deps", optimize_deps_enabled);
options.addOption(bool, "use_llvm", use_llvm_option orelse false);
options.addOption(bool, "use_lld", use_lld_option orelse false);
options.addOption(bool, "use_system_notcurses", use_system_notcurses);
const options_mod = options.createModule();
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const dependency_optimize = if (optimize_deps_enabled) .ReleaseFast else optimize;
const notcurses_dep = b.dependency("notcurses", .{
.target = target,
.optimize = dependency_optimize,
.use_system_notcurses = use_system_notcurses,
});
const clap_dep = b.dependency("clap", .{
.target = target,
.optimize = dependency_optimize,
});
const dizzy_dep = b.dependency("dizzy", .{
.target = target,
.optimize = dependency_optimize,
});
const tracy_dep = if (tracy_enabled) b.dependency("tracy", .{
.target = target,
.optimize = dependency_optimize,
}) else undefined;
const tracy_mod = if (tracy_enabled) tracy_dep.module("tracy") else b.createModule(.{
.root_source_file = .{ .path = "src/tracy_noop.zig" },
});
const themes_dep = b.dependency("themes", .{});
const thespian_dep = b.dependency("thespian", .{
.target = target,
.optimize = dependency_optimize,
.enable_tracy = tracy_enabled,
});
const thespian_mod = thespian_dep.module("thespian");
const cbor_mod = thespian_dep.module("cbor");
const notcurses_mod = notcurses_dep.module("notcurses");
const help_mod = b.createModule(.{
.root_source_file = .{ .path = "help.md" },
});
const config_mod = b.createModule(.{
.root_source_file = .{ .path = "src/config.zig" },
.imports = &.{
.{ .name = "cbor", .module = cbor_mod },
},
});
const log_mod = b.createModule(.{
.root_source_file = .{ .path = "src/log.zig" },
.imports = &.{
.{ .name = "thespian", .module = thespian_mod },
},
});
const tree_sitter_dep = b.dependency("tree-sitter", .{
.target = target,
.optimize = dependency_optimize,
});
const color_mod = b.createModule(.{
.root_source_file = .{ .path = "src/color.zig" },
});
const Buffer_mod = b.createModule(.{
.root_source_file = .{ .path = "src/Buffer.zig" },
.imports = &.{
.{ .name = "notcurses", .module = notcurses_mod },
.{ .name = "cbor", .module = cbor_mod },
},
});
const ripgrep_mod = b.createModule(.{
.root_source_file = .{ .path = "src/ripgrep.zig" },
.imports = &.{
.{ .name = "thespian", .module = thespian_mod },
.{ .name = "cbor", .module = cbor_mod },
.{ .name = "log", .module = log_mod },
},
});
const location_history_mod = b.createModule(.{
.root_source_file = .{ .path = "src/location_history.zig" },
.imports = &.{
.{ .name = "thespian", .module = thespian_mod },
},
});
const syntax_mod = b.createModule(.{
.root_source_file = .{ .path = "src/syntax.zig" },
.imports = &.{
.{ .name = "Buffer", .module = Buffer_mod },
.{ .name = "tracy", .module = tracy_mod },
.{ .name = "treez", .module = tree_sitter_dep.module("treez") },
ts_queryfile(b, tree_sitter_dep, "tree-sitter-agda/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-bash/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-c-sharp/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-c/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-cpp/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-css/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-diff/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-dockerfile/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-git-rebase/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-go/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-fish/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-haskell/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-html/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-java/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-javascript/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-jsdoc/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-json/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-lua/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-make/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-nasm/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-ninja/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-nix/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-ocaml/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-openscad/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-org/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-php/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-python/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-regex/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-ruby/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-rust/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-ssh-config/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-scala/queries/scala/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-scheme/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-toml/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-typescript/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-xml/dtd/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-xml/xml/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-zig/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-ziggy/tree-sitter-ziggy/queries/highlights.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-cpp/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-html/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-javascript/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-lua/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-nasm/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-nix/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-openscad/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-php/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/vim_queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-rust/queries/injections.scm"),
ts_queryfile(b, tree_sitter_dep, "tree-sitter-zig/queries/injections.scm"),
},
});
const diff_mod = b.createModule(.{
.root_source_file = .{ .path = "src/diff.zig" },
.imports = &.{
.{ .name = "thespian", .module = thespian_mod },
.{ .name = "Buffer", .module = Buffer_mod },
.{ .name = "tracy", .module = tracy_mod },
.{ .name = "dizzy", .module = dizzy_dep.module("dizzy") },
.{ .name = "log", .module = log_mod },
.{ .name = "cbor", .module = cbor_mod },
},
});
const text_manip_mod = b.createModule(.{
.root_source_file = .{ .path = "src/text_manip.zig" },
.imports = &.{},
});
const tui_mod = b.createModule(.{
.root_source_file = .{ .path = "src/tui/tui.zig" },
.imports = &.{
.{ .name = "notcurses", .module = notcurses_mod },
.{ .name = "thespian", .module = thespian_mod },
.{ .name = "cbor", .module = cbor_mod },
.{ .name = "config", .module = config_mod },
.{ .name = "log", .module = log_mod },
.{ .name = "location_history", .module = location_history_mod },
.{ .name = "syntax", .module = syntax_mod },
.{ .name = "text_manip", .module = text_manip_mod },
.{ .name = "Buffer", .module = Buffer_mod },
.{ .name = "ripgrep", .module = ripgrep_mod },
.{ .name = "theme", .module = themes_dep.module("theme") },
.{ .name = "themes", .module = themes_dep.module("themes") },
.{ .name = "tracy", .module = tracy_mod },
.{ .name = "build_options", .module = options_mod },
.{ .name = "color", .module = color_mod },
.{ .name = "diff", .module = diff_mod },
.{ .name = "help.md", .module = help_mod },
},
});
const exe = b.addExecutable(.{
.name = "flow",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
if (use_llvm_option) |enabled| exe.use_llvm = enabled;
if (use_lld_option) |enabled| exe.use_lld = enabled;
exe.root_module.addImport("build_options", options_mod);
exe.root_module.addImport("clap", clap_dep.module("clap"));
exe.root_module.addImport("cbor", cbor_mod);
exe.root_module.addImport("config", config_mod);
exe.root_module.addImport("tui", tui_mod);
exe.root_module.addImport("thespian", thespian_mod);
exe.root_module.addImport("log", log_mod);
exe.root_module.addImport("tracy", tracy_mod);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const tests = b.addTest(.{
.root_source_file = .{ .path = "test/tests.zig" },
.target = target,
.optimize = optimize,
});
tests.root_module.addImport("build_options", options_mod);
tests.root_module.addImport("log", log_mod);
tests.root_module.addImport("Buffer", Buffer_mod);
tests.root_module.addImport("color", color_mod);
// b.installArtifact(tests);
const test_run_cmd = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&test_run_cmd.step);
}
fn ts_queryfile(b: *std.Build, dep: *std.Build.Dependency, comptime sub_path: []const u8) std.Build.Module.Import {
return .{
.name = sub_path,
.module = b.createModule(.{
.root_source_file = dep.path(sub_path),
}),
};
}

1
build.zig.version Normal file
View file

@ -0,0 +1 @@
0.12.0-dev.3059+27f589dea

42
build.zig.zon Normal file
View file

@ -0,0 +1,42 @@
.{
.name = "flow",
.version = "0.0.1",
.dependencies = .{
.notcurses = .{
.url = "https://github.com/neurocyte/notcurses-zig/archive/99dd02c42fc8b276fed0890369c9136316747ab9.tar.gz",
.hash = "1220be39bfc0c45e66f7e99cbddf3836dbe32b1fb36a8b5c616f4b13f97a275d36cf",
},
.clap = .{
.url = "https://github.com/Hejsil/zig-clap/archive/9c23bcb5aebe0c2542b4de4472f60959974e2222.tar.gz",
.hash = "12209e829da9d7d0bc089e4e0cbc07bb882f6192cd583277277da34df53cd05b8f2a",
},
.@"tree-sitter" = .{
.url = "https://github.com/neurocyte/tree-sitter/releases/download/master-87dba6ef605389dc2e61cf8ef498ea68e816f4b1/source.tar.gz",
.hash = "12208fe4c5317901bf05642d1740bccbe653e989693b5cc4ed5d7a3f7814e213cbc3",
},
.tracy = .{
.url = "https://github.com/neurocyte/zig-tracy/archive/d2113e7d778ebe7a063e95b5182ff145343aac38.tar.gz",
.hash = "122005c37f1324dcdd00600f266b64f0a66a73428550015ece51d52ae40a552608d1",
},
.dizzy = .{
.url = "https://github.com/SuperAuguste/dizzy/archive/d4aaf67d0f5ef69d0a0287ae472ddfece064d341.tar.gz",
.hash = "1220a7cf5f59b61257993bc5b02991ffc523d103f66842fa8d8ab5c9fdba52799340",
},
.thespian = .{
.url = "https://github.com/neurocyte/thespian/archive/522813dae1ef02eb1348a3eda5710b6626a65477.tar.gz",
.hash = "1220d1a80d5419e5a89df908d44d586b99f84c7dbf37e074f1123522b81d7473a700",
},
.themes = .{
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-9ee6d7bc28256202aa7b3b20555bf480715c4e5c/flow-themes.tar.gz",
.hash = "1220cd21ee1f3e194f1cca5d52175c20d2c663a53eadaa9057997e72aa828f5d3864",
},
},
.paths = .{
"include",
"src",
"test",
"build.zig",
"build.zig.zon",
},
}

293
help.md Normal file
View file

@ -0,0 +1,293 @@
# Flow Control: a programmer's text editor
## Searching
Press Ctrl-f to search this help file. Type a search term and press
Ctrl-n/Ctrl-p or F3/Shift-F3 to jump through the matches. Press Enter
to exit find mode at the current match or Escape to return to your
starting point.
## Input Modes
Flow Control supports multiple input modes that may be changed
interactively at runtime. The current input mode (and some other
settings) is persisted in the configuration file automatically.
- F2 => Cycle major input modes (flow, vim, ...)
The current input mode Input mode is displayed in the `modestatus`
widget at the left side of the statusbar.
## Flow mode
The default input mode, called just flow, is based on common GUI
programming editors. It most closely resembles Visual Studio Code, but
also takes some inspiration from Emacs and others. This mode focuses
on powerful multi cursor support with a find -> select -> modify
cycle style of editing.
### Navigation Commands
- Up, Down, Left, Right =>
Move the cursor
- Home, End =>
Move to the beginning/end of the line
- PageUp, PageDown =>
Move up/down one screen
- Ctrl-Left, Ctrl-Right, Alt-b, Alt-f =>
Move the cursor word wise
- Ctrl-Home, Ctrl-End =>
Move to the beginning/end of the file
- Alt-Left, Alt-Right =>
Jump to previous/next location in the location history
- Ctrl-f =>
Enter find mode
- Ctrl-g =>
Enter goto line mode
- Ctrl-t, Ctrl-b =>
Enter move to next/previous character mode
- Ctrl-n, Ctrl-p, F3, Shift-F3, Alt-n, Alt-p =>
Goto next/previous match
- Ctrl-l =>
Scroll cursor to center of screen, cycle cursor to
top/bottom of screen
- MouseLeft =>
Clear all cursors and selections and place cursor at mouse pointer
- MouseWheel =>
Scroll
- Ctrl-MouseWheel =>
Fast scroll
### Selection Commands
- Shift-Left, Shift-Right =>
Add next character to selection
- Ctrl-Shift-Left, Ctrl-Shift-Right =>
Add next word to selection
- Shift-Up, Shift-Down =>
Add next line to selection
- Ctrl-Shift-Up, Ctrl-Shift-Down =>
Add next line to selection and scroll
- Shift-Home, Shift-End =>
Add begging/end of line to selection
- Ctrl-Shift-Home, Ctrl-Shift-End =>
Add begging/end of file to selection
- Shift-PageUp, Shift-PageDown =>
Add next screen to selection
- Ctrl-a =>
Select entire file
- Ctrl-d =>
Select word under cursor, or add cursor at next match
(see Multi Cursor Commands)
- Ctrl-Space =>
Reverse selection direction
- Double-MouseLeft =>
Select word at mouse pointer
- Triple-MouseLeft =>
Select line at mouse pointer
- Drag-MouseLeft =>
Extend selection to mouse pointer
- MouseRight =>
Extend selection to mouse pointer
### Multi Cursor Commands
- Ctrl-d =>
Add cursor at next match (either find match, or auto match)
- Alt-Shift-Down, Alt-Shift-Up =>
Add cursor on the previous/next line
- Ctrl-MouseLeft =>
Add cursor at mouse click
- Ctrl-u =>
Remove last added cursor (pop)
- Ctrl-k -> Ctrl-d =>
Move primary cursor to next match (skip)
- Escape =>
Remove all cursors and selections
### Editing Commands
- Ctrl-Enter, Ctrl-Shift-Enter =>
Insert new line after/before current line
- Ctrl-Backspace, Ctrl-Delete =>
Delete word left/right
- Ctrl-k -> Ctrl-u =>
Delete to beginning of line
- Ctrl-k -> Ctrl-k =>
Delete to end of line
- Ctrl-Shift-d, Alt-Shift-d =>
Duplicate current line or selection down/up
- Alt-Down, Alt-Up =>
Pull current line or selection down/up
- Ctrl-c =>
Copy selected text
- Ctrl-x =>
Cut selected text, or line if there is no selection
- Ctrl-v =>
Paste previously copied/cut text
- Ctrl-z =>
Undo last change
- Ctrl-Shift-z, Ctrl-y =>
Redo last undone change
- Tab, Shift-Tab =>
Indent/Unindent line
- Ctrl-/ =>
Toggle comment prefix in line
- Alt-s =>
Sort file or selection
- Alt-Shift-f =>
Reformat file or selection
### File Commands
- Ctrl-s =>
Save file
- Ctrl-o =>
Open file
- Ctrl-q =>
Exit
- Ctrl-q =>
Exit
- Ctrl-q =>
Exit
- Ctrl-Shift-q =>
Force exit without saving
- Ctrl-Shift-r =>
Restart Flow Control/reload file
### Configuration Commands
- F9 =>
Select previous theme
- F10 =>
Select next theme
- Ctrl-F10 =>
Toggle visible whitespace mode
### Debugging Commands
- F5 =>
Toggle inspector view
- F6 =>
Dump buffer AST for current line to log view
- F7 =>
Dump current line to log view
- F11, Ctrl-J, Ctrl-Shift-l =>
Toggle log view
- F12, Ctrl-Shift-i =>
Toggle input view
- Ctrl-Shift-/ =>
Dump current widget tree to log view
## Vim mode
The vim mode, called NOR or INS, follows the basic modal editing
style of vim. Normal and insert mode basics follow vim closely,
but more advanced vim functions (e.g. macros and registers) are
not supported yet. Keybindings from flow mode that do not
conflict with common vim keybindings also work in vim mode.
The vim command prompt (:) is not available yet. To save/load/quit
you will have to use the flow mode keybindings.
(work in progress)
## Configuration
Configuration is stored in the standard location
`${XDG_CONFIG_HOME}/flow/config.json`. This is usually
`~/.config/flow/config.json`.
The default configuration will be written the first time
Flow Control is started and looks similar to this:
```
{
"frame_rate": 60,
"theme": "default",
"input_mode": "flow",
"modestate_show": true,
"selectionstate_show": true,
"modstate_show": false,
"keystate_show": false,
"gutter_line_numbers": true,
"gutter_line_numbers_relative": false,
"enable_terminal_cursor": false,
"highlight_current_line": true,
"highlight_current_line_gutter": true,
"show_whitespace": false,
"animation_min_lag": 0,
"animation_max_lag": 150
}
```
Most of these options are fairly self explanitory.
`theme`, `input_mode` and `show_whitespace` are automatically
persisted when changed interactively with keybindings.
`frame_rate` can be tuned to control the maximum number
of frames rendered.
`*state_show` toggle various parts of the statusbar.
`animation_max_lag` controls the maximum amount of time allowed
for rendering scrolling animations. Set to 0 to disable scrolling
animation altogether.

1158
src/Buffer.zig Normal file

File diff suppressed because it is too large Load diff

220
src/Cursor.zig Normal file
View file

@ -0,0 +1,220 @@
const std = @import("std");
const cbor = @import("cbor");
const Buffer = @import("Buffer.zig");
const View = @import("View.zig");
const Selection = @import("Selection.zig");
row: usize = 0,
col: usize = 0,
target: usize = 0,
const Self = @This();
pub inline fn invalid() Self {
return .{
.row = std.math.maxInt(u32),
.col = std.math.maxInt(u32),
.target = std.math.maxInt(u32),
};
}
pub inline fn eql(self: Self, other: Self) bool {
return self.row == other.row and self.col == other.col;
}
pub inline fn right_of(self: Self, other: Self) bool {
return if (self.row > other.row) true else if (self.row == other.row and self.col > other.col) true else false;
}
pub fn clamp_to_buffer(self: *Self, root: Buffer.Root) void {
self.row = @min(self.row, root.lines() - 1);
self.col = @min(self.col, root.line_width(self.row) catch 0);
}
fn follow_target(self: *Self, root: Buffer.Root) void {
self.col = @min(self.target, root.line_width(self.row) catch 0);
}
fn move_right_no_target(self: *Self, root: Buffer.Root) !void {
const lines = root.lines();
if (lines <= self.row) return error.Stop;
if (self.col < root.line_width(self.row) catch 0) {
_, const wcwidth, const offset = root.ecg_at(self.row, self.col) catch return error.Stop;
self.col += wcwidth - offset;
} else if (self.row < lines - 1) {
self.col = 0;
self.row += 1;
} else return error.Stop;
}
pub fn move_right(self: *Self, root: Buffer.Root) !void {
try self.move_right_no_target(root);
self.target = self.col;
}
fn move_left_no_target(self: *Self, root: Buffer.Root) !void {
if (self.col == 0) {
if (self.row == 0) return error.Stop;
self.row -= 1;
self.col = root.line_width(self.row) catch 0;
} else {
_, const wcwidth, _ = root.ecg_at(self.row, self.col - 1) catch return error.Stop;
if (self.col > wcwidth) self.col -= wcwidth else self.col = 0;
}
}
pub fn move_left(self: *Self, root: Buffer.Root) !void {
try self.move_left_no_target(root);
self.target = self.col;
}
pub fn move_up(self: *Self, root: Buffer.Root) !void {
if (self.row > 0) {
self.row -= 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
try self.move_right_no_target(root);
} else return error.Stop;
}
pub fn move_down(self: *Self, root: Buffer.Root) !void {
if (self.row < root.lines() - 1) {
self.row += 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
try self.move_right_no_target(root);
} else return error.Stop;
}
pub fn move_page_up(self: *Self, root: Buffer.Root, view: *const View) void {
self.row = if (self.row > view.rows) self.row - view.rows else 0;
self.follow_target(root);
self.move_left_no_target(root) catch return;
self.move_right_no_target(root) catch return;
}
pub fn move_page_down(self: *Self, root: Buffer.Root, view: *const View) void {
if (root.lines() < view.rows) {
self.move_buffer_last(root);
} else if (self.row < root.lines() - view.rows - 1) {
self.row += view.rows;
} else self.row = root.lines() - 1;
self.follow_target(root);
self.move_left_no_target(root) catch return;
self.move_right_no_target(root) catch return;
}
pub fn move_to(self: *Self, root: Buffer.Root, row: usize, col: usize) !void {
if (row < root.lines()) {
self.row = row;
self.col = @min(col, root.line_width(self.row) catch return error.Stop);
self.target = self.col;
} else return error.Stop;
}
pub fn move_abs(self: *Self, root: Buffer.Root, v: *View, y: usize, x: usize) !void {
self.row = v.row + y;
self.col = v.col + x;
self.clamp_to_buffer(root);
self.target = self.col;
}
pub fn move_begin(self: *Self) void {
self.col = 0;
self.target = self.col;
}
pub fn move_end(self: *Self, root: Buffer.Root) void {
if (self.row < root.lines()) self.col = root.line_width(self.row) catch 0;
self.target = std.math.maxInt(u32);
}
pub fn move_buffer_begin(self: *Self) void {
self.row = 0;
self.col = 0;
self.target = 0;
}
pub fn move_buffer_end(self: *Self, root: Buffer.Root) void {
self.row = root.lines() - 1;
self.move_end(root);
if (self.col == 0) self.target = 0;
}
fn move_buffer_first(self: *Self, root: Buffer.Root) void {
self.row = 0;
self.follow_target(root);
}
fn move_buffer_last(self: *Self, root: Buffer.Root) void {
self.row = root.lines() - 1;
self.follow_target(root);
}
fn is_at_begin(self: *const Self) bool {
return self.col == 0;
}
fn is_at_end(self: *const Self, root: Buffer.Root) bool {
return if (self.row < root.lines()) self.col == root.line_width(self.row) catch 0 else true;
}
pub fn test_at(self: *const Self, root: Buffer.Root, pred: *const fn (c: []const u8) bool) bool {
return root.test_at(pred, self.row, self.col);
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try cbor.writeValue(writer, .{
self.row,
self.col,
self.target,
});
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
return cbor.matchValue(iter, .{
cbor.extract(&self.row),
cbor.extract(&self.col),
cbor.extract(&self.target),
});
}
pub fn nudge_insert(self: *Self, nudge: Selection) void {
if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return;
const rows = nudge.end.row - nudge.begin.row;
if (self.row == nudge.begin.row) {
if (nudge.begin.row < nudge.end.row) {
self.row += rows;
self.col = self.col - nudge.begin.col + nudge.end.col;
} else {
self.col += nudge.end.col - nudge.begin.col;
}
} else {
self.row += rows;
}
}
pub fn nudge_delete(self: *Self, nudge: Selection) bool {
if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return true;
if (self.row == nudge.begin.row) {
if (nudge.begin.row < nudge.end.row) {
return false;
} else {
if (self.col < nudge.end.col) {
return false;
}
self.col -= nudge.end.col - nudge.begin.col;
return true;
}
}
if (self.row < nudge.end.row) return false;
if (self.row == nudge.end.row) {
if (self.col < nudge.end.col) return false;
self.row -= nudge.end.row - nudge.begin.row;
self.col -= nudge.end.col;
return true;
}
self.row -= nudge.end.row - nudge.begin.row;
return true;
}

61
src/Selection.zig Normal file
View file

@ -0,0 +1,61 @@
const std = @import("std");
const Buffer = @import("Buffer.zig");
const Cursor = @import("Cursor.zig");
begin: Cursor = Cursor{},
end: Cursor = Cursor{},
const Self = @This();
pub inline fn eql(self: Self, other: Self) bool {
return self.begin.eql(other.begin) and self.end.eql(other.end);
}
pub fn from_cursor(cursor: *const Cursor) Self {
return .{ .begin = cursor.*, .end = cursor.* };
}
pub fn line_from_cursor(cursor: Cursor, root: Buffer.Root) Self {
var begin = cursor;
var end = cursor;
begin.move_begin();
end.move_end(root);
end.move_right(root) catch {};
return .{ .begin = begin, .end = end };
}
pub fn empty(self: *const Self) bool {
return self.begin.eql(self.end);
}
pub fn reverse(self: *Self) void {
const tmp = self.begin;
self.begin = self.end;
self.end = tmp;
}
pub fn normalize(self: *Self) void {
if (self.begin.right_of(self.end))
self.reverse();
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try self.begin.write(writer);
try self.end.write(writer);
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
if (!try self.begin.extract(iter)) return false;
return self.end.extract(iter);
}
pub fn nudge_insert(self: *Self, nudge: Self) void {
self.begin.nudge_insert(nudge);
self.end.nudge_insert(nudge);
}
pub fn nudge_delete(self: *Self, nudge: Self) bool {
if (!self.begin.nudge_delete(nudge))
return false;
return self.end.nudge_delete(nudge);
}

142
src/View.zig Normal file
View file

@ -0,0 +1,142 @@
const std = @import("std");
const cbor = @import("cbor");
const Buffer = @import("Buffer.zig");
const Cursor = @import("Cursor.zig");
const Selection = @import("Selection.zig");
row: usize = 0,
col: usize = 0,
rows: usize = 0,
cols: usize = 0,
const scroll_cursor_min_border_distance = 5;
const scroll_cursor_min_border_distance_mouse = 1;
const Self = @This();
pub inline fn invalid() Self {
return .{
.row = std.math.maxInt(u32),
.col = std.math.maxInt(u32),
};
}
inline fn reset(self: *Self) void {
self.* = .{};
}
pub inline fn eql(self: Self, other: Self) bool {
return self.row == other.row and self.col == other.col and self.rows == other.rows and self.cols == other.cols;
}
pub fn move_left(self: *Self) !void {
if (self.col > 0) {
self.col -= 1;
} else return error.Stop;
}
pub fn move_right(self: *Self) !void {
self.col += 1;
}
pub fn move_up(self: *Self) !void {
if (!self.is_at_top()) {
self.row -= 1;
} else return error.Stop;
}
pub fn move_down(self: *Self, root: Buffer.Root) !void {
if (!self.is_at_bottom(root)) {
self.row += 1;
} else return error.Stop;
}
pub fn move_to(self: *Self, root: Buffer.Root, row: usize) !void {
if (row < root.lines() - self.rows - 1) {
self.row = row;
} else return error.Stop;
}
inline fn is_at_top(self: *const Self) bool {
return self.row == 0;
}
inline fn is_at_bottom(self: *const Self, root: Buffer.Root) bool {
if (root.lines() < self.rows) return true;
return self.row >= root.lines() - scroll_cursor_min_border_distance;
}
pub inline fn is_visible(self: *const Self, cursor: *const Cursor) bool {
const row_min = self.row;
const row_max = row_min + self.rows;
const col_min = self.col;
const col_max = col_min + self.cols;
return row_min <= cursor.row and cursor.row <= row_max and
col_min <= cursor.col and cursor.col < col_max;
}
inline fn is_visible_selection(self: *const Self, sel: *const Selection) bool {
const row_min = self.row;
const row_max = row_min + self.rows;
return self.is_visible(sel.begin) or is_visible(sel.end) or
(sel.begin.row < row_min and sel.end.row > row_max);
}
inline fn to_cursor_top(self: *const Self) Cursor {
return .{ .row = self.row, .col = 0 };
}
inline fn to_cursor_bottom(self: *const Self, root: Buffer.Root) Cursor {
const bottom = @min(root.lines(), self.row + self.rows + 1);
return .{ .row = bottom, .col = 0 };
}
fn clamp_row(self: *Self, cursor: *const Cursor, abs: bool) void {
const min_border_distance: usize = if (abs) scroll_cursor_min_border_distance_mouse else scroll_cursor_min_border_distance;
if (cursor.row < min_border_distance) {
self.row = 0;
return;
}
if (self.row > 0 and cursor.row >= min_border_distance) {
if (cursor.row < self.row + min_border_distance) {
self.row = cursor.row - min_border_distance;
return;
}
}
if (cursor.row < self.row) {
self.row = 0;
} else if (cursor.row > self.row + self.rows - min_border_distance) {
self.row = cursor.row + min_border_distance - self.rows;
}
}
fn clamp_col(self: *Self, cursor: *const Cursor, _: bool) void {
if (cursor.col < self.col) {
self.col = cursor.col;
} else if (cursor.col > self.col + self.cols - 1) {
self.col = cursor.col - self.cols + 1;
}
}
pub fn clamp(self: *Self, cursor: *const Cursor, abs: bool) void {
self.clamp_row(cursor, abs);
self.clamp_col(cursor, abs);
}
pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void {
try cbor.writeValue(writer, .{
self.row,
self.col,
self.rows,
self.cols,
});
}
pub fn extract(self: *Self, iter: *[]const u8) !bool {
return cbor.matchValue(iter, .{
cbor.extract(&self.row),
cbor.extract(&self.col),
cbor.extract(&self.rows),
cbor.extract(&self.cols),
});
}

62
src/color.zig Normal file
View file

@ -0,0 +1,62 @@
const pow = @import("std").math.pow;
pub const RGB = struct {
r: u8,
g: u8,
b: u8,
pub inline fn from_u24(v: u24) RGB {
const r = @as(u8, @intCast(v >> 16 & 0xFF));
const g = @as(u8, @intCast(v >> 8 & 0xFF));
const b = @as(u8, @intCast(v & 0xFF));
return .{ .r = r, .g = g, .b = b };
}
pub inline fn to_u24(v: RGB) u24 {
const r = @as(u24, @intCast(v.r)) << 16;
const g = @as(u24, @intCast(v.g)) << 8;
const b = @as(u24, @intCast(v.b));
return r | b | g;
}
pub fn contrast(a_: RGB, b_: RGB) f32 {
const a = RGBf.from_RGB(a_).luminance();
const b = RGBf.from_RGB(b_).luminance();
return (@max(a, b) + 0.05) / (@min(a, b) + 0.05);
}
pub fn max_contrast(v: RGB, a: RGB, b: RGB) RGB {
return if (contrast(v, a) > contrast(v, b)) a else b;
}
};
pub const RGBf = struct {
r: f32,
g: f32,
b: f32,
pub inline fn from_RGB(v: RGB) RGBf {
return .{ .r = tof(v.r), .g = tof(v.g), .b = tof(v.b) };
}
pub fn luminance(v: RGBf) f32 {
return linear(v.r) * RED + linear(v.g) * GREEN + linear(v.b) * BLUE;
}
inline fn tof(c: u8) f32 {
return @as(f32, @floatFromInt(c)) / 255.0;
}
inline fn linear(v: f32) f32 {
return if (v <= 0.03928) v / 12.92 else pow(f32, (v + 0.055) / 1.055, GAMMA);
}
const RED = 0.2126;
const GREEN = 0.7152;
const BLUE = 0.0722;
const GAMMA = 2.4;
};
pub fn max_contrast(v: u24, a: u24, b: u24) u24 {
return RGB.max_contrast(RGB.from_u24(v), RGB.from_u24(a), RGB.from_u24(b)).to_u24();
}

15
src/config.zig Normal file
View file

@ -0,0 +1,15 @@
frame_rate: usize = 60,
theme: []const u8 = "default",
input_mode: []const u8 = "flow",
modestate_show: bool = true,
selectionstate_show: bool = true,
modstate_show: bool = false,
keystate_show: bool = false,
gutter_line_numbers: bool = true,
gutter_line_numbers_relative: bool = false,
enable_terminal_cursor: bool = false,
highlight_current_line: bool = true,
highlight_current_line_gutter: bool = true,
show_whitespace: bool = true,
animation_min_lag: usize = 0, //milliseconds
animation_max_lag: usize = 150, //milliseconds

165
src/diff.zig Normal file
View file

@ -0,0 +1,165 @@
const std = @import("std");
const tp = @import("thespian");
const dizzy = @import("dizzy");
const Buffer = @import("Buffer");
const tracy = @import("tracy");
const cbor = @import("cbor");
const Self = @This();
const module_name = @typeName(Self);
pub const Error = error{ OutOfMemory, Exit };
pub const Kind = enum { insert, delete };
pub const Edit = struct {
kind: Kind,
line: usize,
offset: usize,
bytes: []const u8,
};
pid: ?tp.pid,
pub fn create() Error!Self {
return .{ .pid = try Process.create() };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"shutdown"}) catch {};
pid.deinit();
self.pid = null;
}
}
const Process = struct {
arena: std.heap.ArenaAllocator,
a: std.mem.Allocator,
receiver: Receiver,
const Receiver = tp.Receiver(*Process);
const outer_a = std.heap.page_allocator;
pub fn create() Error!tp.pid {
const self = try outer_a.create(Process);
self.* = .{
.arena = std.heap.ArenaAllocator.init(outer_a),
.a = self.arena.allocator(),
.receiver = Receiver.init(Process.receive, self),
};
return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e);
}
fn start(self: *Process) tp.result {
errdefer self.deinit();
tp.receive(&self.receiver);
}
fn deinit(self: *Process) void {
self.arena.deinit();
outer_a.destroy(self);
}
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var cb: usize = 0;
var root_dst: usize = 0;
var root_src: usize = 0;
return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&root_dst), tp.extract(&root_src) }))
self.diff(from, cb, root_dst, root_src) catch |e| tp.exit_error(e)
else if (try m.match(.{"shutdown"}))
tp.exit_normal();
}
fn diff(self: *Process, from: tp.pid_ref, cb_addr: usize, root_new_addr: usize, root_old_addr: usize) !void {
const frame = tracy.initZone(@src(), .{ .name = "diff" });
defer frame.deinit();
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
const root_dst: Buffer.Root = if (root_new_addr == 0) return else @ptrFromInt(root_new_addr);
const root_src: Buffer.Root = if (root_old_addr == 0) return else @ptrFromInt(root_old_addr);
var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){};
var dst = std.ArrayList(u8).init(self.a);
var src = std.ArrayList(u8).init(self.a);
var scratch = std.ArrayListUnmanaged(u32){};
var edits = std.ArrayList(Edit).init(self.a);
defer {
dst.deinit();
src.deinit();
scratch.deinit(self.a);
dizzy_edits.deinit(self.a);
}
try root_dst.store(dst.writer());
try root_src.store(src.writer());
const scratch_len = 4 * (dst.items.len + src.items.len) + 2;
try scratch.ensureTotalCapacity(self.a, scratch_len);
scratch.items.len = scratch_len;
try dizzy.PrimitiveSliceDiffer(u8).diff(self.a, &dizzy_edits, src.items, dst.items, scratch.items);
if (dizzy_edits.items.len > 2)
try edits.ensureTotalCapacity((dizzy_edits.items.len - 1) / 2);
var lines_dst: usize = 0;
var pos_src: usize = 0;
var pos_dst: usize = 0;
var last_offset: usize = 0;
for (dizzy_edits.items) |dizzy_edit| {
switch (dizzy_edit.kind) {
.equal => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += dist;
pos_dst += dist;
scan_char(src.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', &last_offset);
},
.insert => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += 0;
pos_dst += dist;
const line_start_dst: usize = lines_dst;
scan_char(dst.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null);
(try edits.addOne()).* = .{
.kind = .insert,
.line = line_start_dst,
.offset = last_offset,
.bytes = dst.items[dizzy_edit.range.start..dizzy_edit.range.end],
};
},
.delete => {
const dist = dizzy_edit.range.end - dizzy_edit.range.start;
pos_src += dist;
pos_dst += 0;
(try edits.addOne()).* = .{
.kind = .delete,
.line = lines_dst,
.offset = last_offset,
.bytes = src.items[dizzy_edit.range.start..dizzy_edit.range.end],
};
},
}
}
cb(from, edits.items);
}
fn scan_char(chars: []const u8, lines: *usize, char: u8, last_offset: ?*usize) void {
var pos = chars;
while (pos.len > 0) {
if (pos[0] == char) {
if (last_offset) |off| off.* = pos.len - 1;
lines.* += 1;
}
pos = pos[1..];
}
}
};
pub const CallBack = fn (from: tp.pid_ref, edits: []Edit) void;
pub fn diff(self: Self, cb: *const CallBack, root_dst: Buffer.Root, root_src: Buffer.Root) tp.result {
if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), @intFromPtr(root_dst), @intFromPtr(root_src) });
}

127
src/file_type.zig Normal file
View file

@ -0,0 +1,127 @@
const std = @import("std");
const treez = @import("treez");
pub const FileType = @This();
color: u24,
icon: []const u8,
name: []const u8,
lang_fn: LangFn,
extensions: []const []const u8,
highlights: [:0]const u8,
injections: ?[:0]const u8,
first_line_matches: ?FirstLineMatch = null,
comment: []const u8,
pub fn get_by_name(name: []const u8) ?*const FileType {
for (file_types) |*file_type|
if (std.mem.eql(u8, file_type.name, name))
return file_type;
return null;
}
pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType {
if (guess_first_line(content)) |ft| return ft;
for (file_types) |*file_type|
if (file_path) |fp| if (match_file_type(file_type, fp))
return file_type;
return null;
}
fn guess_first_line(content: []const u8) ?*const FileType {
const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content;
for (file_types) |*file_type|
if (file_type.first_line_matches) |match|
if (match_first_line(match, first_line))
return file_type;
return null;
}
fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool {
if (match.prefix) |prefix|
if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix))
return false;
if (match.content) |content|
if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false;
return true;
}
fn match_file_type(file_type: *const FileType, file_path: []const u8) bool {
const basename = std.fs.path.basename(file_path);
const extension = std.fs.path.extension(file_path);
return for (file_type.extensions) |ext| {
if (ext.len == basename.len and std.mem.eql(u8, ext, basename))
return true;
if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..]))
return true;
} else false;
}
pub fn Parser(comptime lang: []const u8) LangFn {
return get_parser(lang);
}
fn get_parser(comptime lang: []const u8) LangFn {
const language_name = ft_func_name(lang);
return @extern(?LangFn, .{ .name = "tree_sitter_" ++ language_name }) orelse @compileError(std.fmt.comptimePrint("Cannot find extern tree_sitter_{s}", .{language_name}));
}
fn ft_func_name(comptime lang: []const u8) []const u8 {
var func_name: [lang.len]u8 = undefined;
for (lang, 0..) |c, i|
func_name[i] = if (c == '-') '_' else c;
return &func_name;
}
const LangFn = *const fn () callconv(.C) ?*const treez.Language;
const FirstLineMatch = struct {
prefix: ?[]const u8 = null,
content: ?[]const u8 = null,
};
const FileTypeOptions = struct {
extensions: []const []const u8 = &[_][]const u8{},
comment: []const u8,
icon: ?[]const u8 = null,
color: ?u24 = null,
highlights: ?[:0]const u8 = null,
injections: ?[:0]const u8 = null,
first_line_matches: ?FirstLineMatch = null,
parser: ?LangFn = null,
};
fn DeclLang(comptime lang: []const u8, comptime args: FileTypeOptions) FileType {
return .{
.color = args.color orelse 0xffffff,
.icon = args.icon orelse "󱀫",
.name = lang,
.lang_fn = if (args.parser) |p| p else get_parser(lang),
.extensions = args.extensions,
.comment = args.comment,
.highlights = if (args.highlights) |h| h else @embedFile("tree-sitter-" ++ lang ++ "/queries/highlights.scm"),
.injections = args.injections,
.first_line_matches = args.first_line_matches,
};
}
pub const file_types = load_file_types(@import("file_types.zig"));
fn load_file_types(comptime Namespace: type) []FileType {
comptime switch (@typeInfo(Namespace)) {
.Struct => |info| {
var count = 0;
for (info.decls) |_| {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
count += 1;
}
var cmds: [count]FileType = undefined;
var i = 0;
for (info.decls) |decl| {
cmds[i] = DeclLang(decl.name, @field(Namespace, decl.name));
i += 1;
}
return &cmds;
},
else => @compileError("expected tuple or struct type"),
};
}

294
src/file_types.zig Normal file
View file

@ -0,0 +1,294 @@
pub const agda = .{
.extensions = &[_][]const u8{"agda"},
.comment = "--",
};
pub const bash = .{
.color = 0x3e474a,
.icon = "󱆃",
.extensions = &[_][]const u8{ "sh", "bash" },
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "sh" },
};
pub const c = .{
.icon = "󰙱",
.extensions = &[_][]const u8{ "c", "h" },
.comment = "//",
};
pub const @"c-sharp" = .{
.color = 0x68217a,
.icon = "󰌛",
.extensions = &[_][]const u8{"cs"},
.comment = "//",
};
pub const conf = .{
.color = 0x000000,
.icon = "",
.extensions = &[_][]const u8{ "conf", "config", ".gitconfig" },
.highlights = fish.highlights,
.comment = "#",
.parser = fish.parser,
};
pub const cpp = .{
.color = 0x9c033a,
.icon = "",
.extensions = &[_][]const u8{ "cc", "cpp", "cxx", "hpp", "hxx", "h", "ipp", "ixx" },
.comment = "//",
.injections = @embedFile("tree-sitter-cpp/queries/injections.scm"),
};
pub const css = .{
.color = 0x3d8fc6,
.icon = "󰌜",
.extensions = &[_][]const u8{"css"},
.comment = "//",
};
pub const diff = .{
.extensions = &[_][]const u8{ "diff", "patch" },
.comment = "#",
};
pub const dockerfile = .{
.color = 0x019bc6,
.icon = "",
.extensions = &[_][]const u8{ "Dockerfile", "dockerfile", "docker", "Containerfile", "container" },
.comment = "#",
};
pub const dtd = .{
.icon = "󰗀",
.extensions = &[_][]const u8{"dtd"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-xml/dtd/queries/highlights.scm"),
};
pub const fish = .{
.extensions = &[_][]const u8{"fish"},
.comment = "#",
.parser = @import("file_type.zig").Parser("fish"),
.highlights = @embedFile("tree-sitter-fish/queries/highlights.scm"),
};
pub const @"git-rebase" = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"git-rebase-todo"},
.comment = "#",
};
pub const gitcommit = .{
.color = 0xf34f29,
.icon = "",
.extensions = &[_][]const u8{"COMMIT_EDITMSG"},
.comment = "#",
.injections = @embedFile("tree-sitter-gitcommit/queries/injections.scm"),
};
pub const go = .{
.color = 0x00acd7,
.icon = "󰟓",
.extensions = &[_][]const u8{"go"},
.comment = "//",
};
pub const haskell = .{
.color = 0x5E5185,
.icon = "󰲒",
.extensions = &[_][]const u8{"hs"},
.comment = "--",
};
pub const html = .{
.color = 0xe54d26,
.icon = "󰌝",
.extensions = &[_][]const u8{"html"},
.comment = "<!--",
.injections = @embedFile("tree-sitter-html/queries/injections.scm"),
};
pub const java = .{
.color = 0xEA2D2E,
.icon = "",
.extensions = &[_][]const u8{"java"},
.comment = "//",
};
pub const javascript = .{
.color = 0xf0db4f,
.icon = "󰌞",
.extensions = &[_][]const u8{"js"},
.comment = "//",
.injections = @embedFile("tree-sitter-javascript/queries/injections.scm"),
};
pub const json = .{
.extensions = &[_][]const u8{"json"},
.comment = "//",
};
pub const lua = .{
.color = 0x000080,
.icon = "󰢱",
.extensions = &[_][]const u8{"lua"},
.comment = "--",
.injections = @embedFile("tree-sitter-lua/queries/injections.scm"),
.first_line_matches = .{ .prefix = "--", .content = "lua" },
};
pub const make = .{
.extensions = &[_][]const u8{ "makefile", "Makefile", "MAKEFILE", "GNUmakefile", "mk", "mak", "dsp" },
.comment = "#",
};
pub const markdown = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{"md"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"),
};
pub const @"markdown-inline" = .{
.color = 0x000000,
.icon = "󰍔",
.extensions = &[_][]const u8{},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"),
.injections = @embedFile("tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"),
};
pub const nasm = .{
.extensions = &[_][]const u8{ "asm", "nasm" },
.comment = "#",
.injections = @embedFile("tree-sitter-nasm/queries/injections.scm"),
};
pub const ninja = .{
.extensions = &[_][]const u8{"ninja"},
.comment = "#",
};
pub const nix = .{
.color = 0x5277C3,
.icon = "󱄅",
.extensions = &[_][]const u8{"nix"},
.comment = "#",
.injections = @embedFile("tree-sitter-nix/queries/injections.scm"),
};
pub const ocaml = .{
.color = 0xF18803,
.icon = "",
.extensions = &[_][]const u8{ "ml", "mli" },
.comment = "(*",
};
pub const openscad = .{
.color = 0x000000,
.icon = "󰻫",
.extensions = &[_][]const u8{"scad"},
.comment = "//",
.injections = @embedFile("tree-sitter-openscad/queries/injections.scm"),
};
pub const org = .{
.icon = "",
.extensions = &[_][]const u8{"org"},
.comment = "#",
};
pub const php = .{
.color = 0x6181b6,
.icon = "󰌟",
.extensions = &[_][]const u8{"php"},
.comment = "//",
.injections = @embedFile("tree-sitter-php/queries/injections.scm"),
};
pub const purescript = .{
.color = 0x14161a,
.icon = "",
.extensions = &[_][]const u8{"purs"},
.comment = "--",
.injections = @embedFile("tree-sitter-purescript/queries/injections.scm"),
};
pub const python = .{
.color = 0xffd845,
.icon = "󰌠",
.extensions = &[_][]const u8{"py"},
.comment = "#",
.first_line_matches = .{ .prefix = "#!", .content = "/bin/bash" },
};
pub const regex = .{
.extensions = &[_][]const u8{},
.comment = "#",
};
pub const ruby = .{
.color = 0xd91404,
.icon = "󰴭",
.extensions = &[_][]const u8{"rb"},
.comment = "#",
};
pub const rust = .{
.color = 0x000000,
.icon = "󱘗",
.extensions = &[_][]const u8{"rs"},
.comment = "//",
.injections = @embedFile("tree-sitter-rust/queries/injections.scm"),
};
pub const scheme = .{
.extensions = &[_][]const u8{ "scm", "ss", "el" },
.comment = ";",
};
pub const @"ssh-config" = .{
.extensions = &[_][]const u8{".ssh/config"},
.comment = "#",
};
pub const toml = .{
.extensions = &[_][]const u8{ "toml" },
.comment = "#",
};
pub const typescript = .{
.color = 0x007acc,
.icon = "󰛦",
.extensions = &[_][]const u8{ "ts", "tsx" },
.comment = "//",
};
pub const xml = .{
.icon = "󰗀",
.extensions = &[_][]const u8{"xml"},
.comment = "<!--",
.highlights = @embedFile("tree-sitter-xml/xml/queries/highlights.scm"),
.first_line_matches = .{ .prefix = "<?xml " },
};
pub const zig = .{
.color = 0xf7a41d,
.icon = "",
.extensions = &[_][]const u8{ "zig", "zon" },
.comment = "//",
.injections = @embedFile("tree-sitter-zig/queries/injections.scm"),
};
pub const ziggy = .{
.color = 0xf7a41d,
.icon = "",
.extensions = &[_][]const u8{ "ziggy" },
.comment = "//",
.highlights = @embedFile("tree-sitter-ziggy/tree-sitter-ziggy/queries/highlights.scm"),
};

147
src/location_history.zig Normal file
View file

@ -0,0 +1,147 @@
const std = @import("std");
const tp = @import("thespian");
const Self = @This();
const module_name = @typeName(Self);
pub const Error = error{ OutOfMemory, Exit };
pid: ?tp.pid,
pub const Cursor = struct {
row: usize = 0,
col: usize = 0,
};
pub const Selection = struct {
begin: Cursor = Cursor{},
end: Cursor = Cursor{},
};
pub fn create() Error!Self {
return .{ .pid = try Process.create() };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"shutdown"}) catch {};
pid.deinit();
self.pid = null;
}
}
const Process = struct {
arena: std.heap.ArenaAllocator,
a: std.mem.Allocator,
pos: usize = 0,
records: std.ArrayList(Entry),
receiver: Receiver,
const Receiver = tp.Receiver(*Process);
const outer_a = std.heap.page_allocator;
const Entry = struct {
cursor: Cursor,
selection: ?Selection = null,
};
pub fn create() Error!tp.pid {
const self = try outer_a.create(Process);
self.* = .{
.arena = std.heap.ArenaAllocator.init(outer_a),
.a = self.arena.allocator(),
.records = std.ArrayList(Entry).init(self.a),
.receiver = Receiver.init(Process.receive, self),
};
return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e);
}
fn start(self: *Process) tp.result {
errdefer self.deinit();
tp.receive(&self.receiver);
}
fn deinit(self: *Process) void {
self.records.deinit();
self.arena.deinit();
outer_a.destroy(self);
}
fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var c: Cursor = .{};
var s: Selection = .{};
var cb: usize = 0;
return if (try m.match(.{ "A", tp.extract(&c.col), tp.extract(&c.row) }))
self.add(.{ .cursor = c })
else if (try m.match(.{ "A", tp.extract(&c.col), tp.extract(&c.row), tp.extract(&s.begin.row), tp.extract(&s.begin.col), tp.extract(&s.end.row), tp.extract(&s.end.col) }))
self.add(.{ .cursor = c, .selection = s })
else if (try m.match(.{ "B", tp.extract(&cb) }))
self.back(from, cb)
else if (try m.match(.{ "F", tp.extract(&cb) }))
self.forward(from, cb)
else if (try m.match(.{"shutdown"}))
tp.exit_normal();
}
fn add(self: *Process, entry: Entry) tp.result {
if (self.records.items.len == 0)
return self.records.append(entry) catch |e| tp.exit_error(e);
if (entry.cursor.row == self.records.items[self.pos].cursor.row) {
self.records.items[self.pos] = entry;
return;
}
if (self.records.items.len > self.pos + 1) {
if (entry.cursor.row == self.records.items[self.pos + 1].cursor.row)
return;
}
if (self.pos > 0) {
if (entry.cursor.row == self.records.items[self.pos - 1].cursor.row)
return;
}
self.records.append(entry) catch |e| return tp.exit_error(e);
self.pos = self.records.items.len - 1;
}
fn back(self: *Process, from: tp.pid_ref, cb_addr: usize) void {
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
if (self.pos == 0)
return;
self.pos -= 1;
const entry = self.records.items[self.pos];
cb(from, entry.cursor, entry.selection);
}
fn forward(self: *Process, from: tp.pid_ref, cb_addr: usize) void {
const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr);
if (self.pos == self.records.items.len - 1)
return;
self.pos += 1;
const entry = self.records.items[self.pos];
cb(from, entry.cursor, entry.selection);
}
};
pub fn add(self: Self, cursor: Cursor, selection: ?Selection) void {
if (self.pid) |pid| {
if (selection) |sel|
pid.send(.{ "A", cursor.col, cursor.row, sel.begin.row, sel.begin.col, sel.end.row, sel.end.col }) catch {}
else
pid.send(.{ "A", cursor.col, cursor.row }) catch {};
}
}
pub const CallBack = fn (from: tp.pid_ref, cursor: Cursor, selection: ?Selection) void;
pub fn back(self: Self, cb: *const CallBack) tp.result {
if (self.pid) |pid| try pid.send(.{ "B", @intFromPtr(cb) });
}
pub fn forward(self: Self, cb: *const CallBack) tp.result {
if (self.pid) |pid| try pid.send(.{ "F", @intFromPtr(cb) });
}

158
src/log.zig Normal file
View file

@ -0,0 +1,158 @@
const std = @import("std");
const tp = @import("thespian");
const deque = std.TailQueue;
const fba = std.heap.FixedBufferAllocator;
const Self = @This();
pub const max_log_message = tp.max_message_size - 128;
a: std.mem.Allocator,
receiver: Receiver,
subscriber: ?tp.pid,
heap: [32 + 1024]u8,
fba: fba,
msg_store: MsgStoreT,
const MsgStoreT = std.DoublyLinkedList([]u8);
const Receiver = tp.Receiver(*Self);
const StartArgs = struct {
a: std.mem.Allocator,
};
pub fn spawn(ctx: *tp.context, a: std.mem.Allocator, env: ?*const tp.env) !tp.pid {
return try ctx.spawn_link(StartArgs{ .a = a }, Self.start, "log", null, env);
}
fn start(args: StartArgs) tp.result {
_ = tp.set_trap(true);
var this = Self.init(args) catch |e| return tp.exit_error(e);
errdefer this.deinit();
tp.receive(&this.receiver);
}
fn init(args: StartArgs) !*Self {
var p = try args.a.create(Self);
p.* = .{
.a = args.a,
.receiver = Receiver.init(Self.receive, p),
.subscriber = null,
.heap = undefined,
.fba = fba.init(&p.heap),
.msg_store = MsgStoreT{},
};
return p;
}
fn deinit(self: *const Self) void {
if (self.subscriber) |*s| s.deinit();
self.a.destroy(self);
}
fn log(msg: []const u8) void {
tp.self_pid().send(.{ "log", "log", msg }) catch {};
}
fn store(self: *Self, m: tp.message) void {
const a: std.mem.Allocator = self.fba.allocator();
const buf: []u8 = a.alloc(u8, m.len()) catch return;
var node: *MsgStoreT.Node = a.create(MsgStoreT.Node) catch return;
node.data = buf;
@memcpy(buf, m.buf);
self.msg_store.append(node);
}
fn store_send(self: *Self) void {
var node = self.msg_store.first;
if (self.subscriber) |sub| {
while (node) |node_| {
sub.send_raw(tp.message{ .buf = node_.data }) catch return;
node = node_.next;
}
}
self.store_reset();
}
fn store_reset(self: *Self) void {
self.msg_store = MsgStoreT{};
self.fba.reset();
}
fn receive(self: *Self, from: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
if (try m.match(.{ "log", tp.more })) {
if (self.subscriber) |subscriber| {
subscriber.send_raw(m) catch {};
} else {
self.store(m);
}
} else if (try m.match(.{"subscribe"})) {
// log("subscribed");
if (self.subscriber) |*s| s.deinit();
self.subscriber = from.clone();
self.store_send();
} else if (try m.match(.{"unsubscribe"})) {
// log("unsubscribed");
if (self.subscriber) |*s| s.deinit();
self.subscriber = null;
self.store_reset();
} else if (try m.match(.{"shutdown"})) {
return tp.exit_normal();
}
}
pub const Logger = struct {
proc: tp.pid_ref,
tag: []const u8,
const Self_ = @This();
pub fn write(self: Self_, value: anytype) void {
self.proc.send(.{ "log", self.tag } ++ value) catch {};
}
pub fn print(self: Self_, comptime fmt: anytype, args: anytype) void {
var buf: [max_log_message]u8 = undefined;
const output = std.fmt.bufPrint(&buf, fmt, args) catch "MESSAGE TOO LARGE";
self.proc.send(.{ "log", self.tag, output }) catch {};
}
pub fn err(self: Self_, context: []const u8, e: anyerror) void {
defer tp.reset_error();
var buf: [max_log_message]u8 = undefined;
var msg: []const u8 = "UNKNOWN";
switch (e) {
error.Exit => {
const msg_: tp.message = .{ .buf = tp.error_message() };
var msg__: []const u8 = undefined;
if (!(msg_.match(.{ "exit", tp.extract(&msg__) }) catch false))
msg__ = msg_.buf;
if (msg__.len > buf.len) {
self.proc.send(.{ "log", "error", self.tag, context, "->", "MESSAGE TOO LARGE" }) catch {};
return;
}
const msg___ = buf[0..msg__.len];
@memcpy(msg___, msg__);
msg = msg___;
},
else => {
msg = @errorName(e);
},
}
self.proc.send(.{ "log", "error", self.tag, context, "->", msg }) catch {};
}
};
pub fn logger(tag: []const u8) Logger {
return .{ .proc = tp.env.get().proc("log"), .tag = tag };
}
pub fn subscribe() tp.result {
return tp.env.get().proc("log").send(.{"subscribe"});
}
pub fn unsubscribe() tp.result {
return tp.env.get().proc("log").send(.{"unsubscribe"});
}

168
src/lsp_process.zig Normal file
View file

@ -0,0 +1,168 @@
const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
pid: ?tp.pid,
const Self = @This();
const module_name = @typeName(Self);
const sp_tag = "LSP";
pub const Error = error{ OutOfMemory, Exit };
pub fn open(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!Self {
return .{ .pid = try Process.create(a, cmd, tag) };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
pid.send(.{"close"}) catch {};
self.pid = null;
pid.deinit();
}
}
pub fn send(self: *Self, message: []const u8) tp.result {
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
try pid.send(.{ "M", message });
}
pub fn close(self: *Self) void {
self.deinit();
}
const Process = struct {
a: std.mem.Allocator,
cmd: tp.message,
receiver: Receiver,
sp: ?tp.subprocess = null,
recv_buf: std.ArrayList(u8),
parent: tp.pid,
tag: [:0]const u8,
logger: log.Logger,
const Receiver = tp.Receiver(*Process);
pub fn create(a: std.mem.Allocator, cmd: tp.message, tag: [:0]const u8) Error!tp.pid {
const self = try a.create(Process);
self.* = .{
.a = a,
.cmd = try cmd.clone(a),
.receiver = Receiver.init(receive, self),
.recv_buf = std.ArrayList(u8).init(a),
.parent = tp.self_pid().clone(),
.tag = try a.dupeZ(u8, tag),
.logger = log.logger(@typeName(Self)),
};
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
}
fn deinit(self: *Process) void {
self.recv_buf.deinit();
self.a.free(self.cmd.buf);
self.close() catch {};
}
fn close(self: *Process) tp.result {
if (self.sp) |*sp| {
defer self.sp = null;
try sp.close();
}
}
fn start(self: *Process) tp.result {
_ = tp.set_trap(true);
self.sp = tp.subprocess.init(self.a, self.cmd, sp_tag, self.stdin_behavior) catch |e| return tp.exit_error(e);
tp.receive(&self.receiver);
}
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var bytes: []u8 = "";
if (try m.match(.{ "S", tp.extract(&bytes) })) {
const sp = if (self.sp) |sp| sp else return tp.exit_error(error.Closed);
try sp.send(bytes);
} else if (try m.match(.{"close"})) {
try self.close();
} else if (try m.match(.{ sp_tag, "stdout", tp.extract(&bytes) })) {
self.handle_output(bytes) catch |e| return tp.exit_error(e);
} else if (try m.match(.{ sp_tag, "term", tp.more })) {
self.handle_terminated() catch |e| return tp.exit_error(e);
} else if (try m.match(.{ sp_tag, "stderr", tp.extract(&bytes) })) {
self.logger.print("ERR: {s}", .{bytes});
} else if (try m.match(.{ "exit", "normal" })) {
return tp.exit_normal();
} else {
self.logger.err("receive", tp.unexpected(m));
return tp.unexpected(m);
}
}
fn handle_output(self: *Process, bytes: []u8) !void {
try self.recv_buf.appendSlice(bytes);
@import("log").logger(module_name).print("{s}", .{bytes}) catch {};
const message = try self.frame_message() orelse return;
_ = message;
}
fn handle_terminated(self: *Process) !void {
const recv_buf = try self.recv_buf.toOwnedSlice();
var it = std.mem.splitScalar(u8, recv_buf, '\n');
while (it.next()) |json| {
if (json.len == 0) continue;
var msg_buf: [tp.max_message_size]u8 = undefined;
const msg: tp.message = .{ .buf = try cbor.fromJson(json, &msg_buf) };
try self.dispatch(msg);
// var buf: [tp.max_message_size]u8 = undefined;
// @import("log").logger(module_name).print("json: {s}", .{try msg.to_json(&buf)}) catch {};
}
@import("log").logger(module_name).print("done", .{}) catch {};
try self.parent.send(.{ self.tag, "done" });
}
fn frame_message(self: *Self) !?Message {
const end = std.mem.indexOf(u8, self.recv_buf, "\r\n\r\n") orelse return null;
const headers = try Headers.parse(self.recv_buf[0..end]);
const body = self.recv_buf[end + 2 ..];
if (body.len < headers.content_length) return null;
return .{ .body = body };
}
};
const Message = struct {
body: []const u8,
};
const Headers = struct {
content_length: usize = 0,
content_type: ?[]const u8 = null,
fn parse(buf_: []const u8) !Headers {
var buf = buf_;
var ret: Headers = .{};
while (true) {
const sep = std.mem.indexOf(u8, buf, ":") orelse return error.InvalidSyntax;
const name = buf[0..sep];
const end = std.mem.indexOf(u8, buf, "\r\n") orelse buf.len;
const vstart = if (buf.len > sep + 1)
if (buf[sep + 1] == ' ')
sep + 2
else
sep + 1
else
sep + 1;
const value = buf[vstart..end];
ret.parse_one(name, value);
buf = if (end < buf.len - 2) buf[end + 2 ..] else return ret;
}
}
fn parse_one(self: *Headers, name: []const u8, value: []const u8) void {
if (std.mem.eql(u8, "Content-Length", name)) {
self.content_length = std.fmt.parseInt(@TypeOf(self.content_length), value, 10);
} else if (std.mem.eql(u8, "Content-Type", name)) {
self.content_type = value;
}
}
};

319
src/main.zig Normal file
View file

@ -0,0 +1,319 @@
const std = @import("std");
const tui = @import("tui");
const thespian = @import("thespian");
const clap = @import("clap");
const builtin = @import("builtin");
const c = @cImport({
@cInclude("locale.h");
});
const build_options = @import("build_options");
const log = @import("log");
pub const application_name = "flow";
pub const application_logo = "󱞏 ";
pub fn main() anyerror!void {
const params = comptime clap.parseParamsComptime(
\\-h, --help Display this help and exit.
\\-f, --frame-rate <usize> Set target frame rate. (default: 60)
\\--debug-wait Wait for key press before starting UI.
\\--debug-dump-on-error Dump stack traces on errors.
\\--no-sleep Do not sleep the main loop when idle.
\\--no-alternate Do not use the alternate terminal screen.
\\--no-trace Do not enable internal tracing.
\\--restore-session Restore restart session.
\\<str>... File to open.
\\ Add +<LINE> to the command line or append
\\ :LINE or :LINE:COL to the file name to jump
\\ to a location in the file.
\\
);
if (builtin.os.tag == .linux) {
// drain stdin so we don't pickup junk from previous application/shell
_ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.os.STDIN_FILENO))), std.os.linux.T.CFLSH, 0);
}
const a = std.heap.c_allocator;
var diag = clap.Diagnostic{};
var res = clap.parse(clap.Help, &params, clap.parsers.default, .{
.diagnostic = &diag,
.allocator = a,
}) catch |err| {
diag.report(std.io.getStdErr().writer(), err) catch {};
clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{}) catch {};
exit(1);
return err;
};
defer res.deinit();
if (res.args.help != 0)
return clap.help(std.io.getStdErr().writer(), clap.Help, &params, .{});
if (std.os.getenv("JITDEBUG")) |_| thespian.install_debugger();
if (res.args.@"debug-wait" != 0) {
std.debug.print("press return to start", .{});
var buf: [10]u8 = undefined;
_ = std.c.read(0, &buf, @sizeOf(@TypeOf(buf)));
}
if (c.setlocale(c.LC_ALL, "") == null) {
return error.SetLocaleFailed;
}
var ctx = try thespian.context.init(a);
defer ctx.deinit();
const env = thespian.env.init();
defer env.deinit();
if (build_options.enable_tracy) {
if (res.args.@"no-trace" == 0) {
env.enable_all_channels();
env.on_trace(trace);
}
}
const log_proc = try log.spawn(&ctx, a, &env);
defer log_proc.deinit();
env.set("restore-session", (res.args.@"restore-session" != 0));
env.set("no-alternate", (res.args.@"no-alternate" != 0));
env.set("no-sleep", (res.args.@"no-sleep" != 0));
env.set("dump-stack-trace", (res.args.@"debug-dump-on-error" != 0));
if (res.args.@"frame-rate") |frame_rate|
env.num_set("frame-rate", @intCast(frame_rate));
env.proc_set("log", log_proc.ref());
var eh = thespian.make_exit_handler({}, print_exit_status);
const tui_proc = try tui.spawn(a, &ctx, &eh, &env);
defer tui_proc.deinit();
const Dest = struct {
file: []const u8 = "",
line: ?usize = null,
column: ?usize = null,
end_column: ?usize = null,
};
var dests = std.ArrayList(Dest).init(a);
defer dests.deinit();
var prev: ?*Dest = null;
var line_next: ?usize = null;
for (res.positionals) |arg| {
if (arg.len == 0) continue;
if (arg[0] == '+') {
const line = try std.fmt.parseInt(usize, arg[1..], 10);
if (prev) |p| {
p.line = line;
} else {
line_next = line;
}
continue;
}
const curr = try dests.addOne();
curr.* = .{};
prev = curr;
if (line_next) |line| {
curr.line = line;
line_next = null;
}
var it = std.mem.splitScalar(u8, arg, ':');
curr.file = it.first();
if (it.next()) |line_|
curr.line = std.fmt.parseInt(usize, line_, 10) catch null;
if (it.next()) |col_|
curr.column = std.fmt.parseInt(usize, col_, 10) catch null;
if (it.next()) |col_|
curr.end_column = std.fmt.parseInt(usize, col_, 10) catch null;
}
for (dests.items) |dest| {
if (dest.file.len == 0) continue;
if (dest.line) |l| {
if (dest.column) |col| {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file, .line = l, .column = col } });
if (dest.end_column) |end|
try tui_proc.send(.{ "A", l, col - 1, end - 1 });
} else {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file, .line = l } });
}
} else {
try tui_proc.send(.{ "cmd", "navigate", .{ .file = dest.file } });
}
} else {
try tui_proc.send(.{ "cmd", "show_home" });
}
ctx.run();
if (want_restart) restart();
exit(final_exit_status);
}
var final_exit_status: u8 = 0;
var want_restart: bool = false;
fn print_exit_status(_: void, msg: []const u8) void {
if (std.mem.eql(u8, msg, "normal")) {
return;
} else if (std.mem.eql(u8, msg, "restart")) {
want_restart = true;
} else {
std.io.getStdErr().writer().print("\n" ++ application_name ++ " ERROR: {s}\n", .{msg}) catch {};
final_exit_status = 1;
}
}
fn count_args() usize {
var args = std.process.args();
_ = args.next();
var count: usize = 0;
while (args.next()) |_| {
count += 1;
}
return count;
}
fn trace(m: thespian.message.c_buffer_type) callconv(.C) void {
thespian.message.from(m).to_json_cb(trace_json);
}
fn trace_json(json: thespian.message.json_string_view) callconv(.C) void {
const callstack_depth = 10;
___tracy_emit_message(json.base, json.len, callstack_depth);
}
extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void;
fn exit(status: u8) noreturn {
if (builtin.os.tag == .linux) {
// drain stdin so we don't leave junk at the next prompt
_ = std.os.linux.syscall3(.ioctl, @as(usize, @bitCast(@as(isize, std.os.STDIN_FILENO))), std.os.linux.T.CFLSH, 0);
}
std.os.exit(status);
}
const config = @import("config");
pub fn read_config(a: std.mem.Allocator, buf: *?[]const u8) config {
const file_name = get_app_config_file_name(application_name) catch return .{};
return read_json_config_file(a, file_name, buf) catch .{};
}
fn read_json_config_file(a: std.mem.Allocator, file_name: []const u8, buf: *?[]const u8) !config {
const cbor = @import("cbor");
var file = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch |e| switch (e) {
error.FileNotFound => return .{},
else => return e,
};
defer file.close();
const json = try file.readToEndAlloc(a, 64 * 1024);
defer a.free(json);
const cbor_buf: []u8 = try a.alloc(u8, json.len);
buf.* = cbor_buf;
const cb = try cbor.fromJson(json, cbor_buf);
var iter = cb;
var len = try cbor.decodeMapHeader(&iter);
var data: config = .{};
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidConfig;
inline for (@typeInfo(config).Struct.fields) |field_info| {
if (std.mem.eql(u8, field_name, field_info.name)) {
var value: field_info.type = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidConfig;
@field(data, field_info.name) = value;
}
}
}
return data;
}
pub fn write_config(conf: config, a: std.mem.Allocator) !void {
return write_json_file(config, conf, a, try get_app_config_file_name(application_name));
}
fn write_json_file(comptime T: type, data: T, a: std.mem.Allocator, file_name: []const u8) !void {
const cbor = @import("cbor");
var file = try std.fs.createFileAbsolute(file_name, .{ .truncate = true });
defer file.close();
var cb = std.ArrayList(u8).init(a);
defer cb.deinit();
try cbor.writeValue(cb.writer(), data);
var s = std.json.writeStream(file.writer(), .{ .whitespace = .indent_4 });
var iter: []const u8 = cb.items;
try cbor.JsonStream(std.fs.File).jsonWriteValue(&s, &iter);
}
pub fn get_config_dir() ![]const u8 {
return get_app_config_dir(application_name);
}
fn get_app_config_dir(appname: []const u8) ![]const u8 {
const local = struct {
var config_dir_buffer: [std.os.PATH_MAX]u8 = undefined;
var config_dir: ?[]const u8 = null;
};
const config_dir = if (local.config_dir) |dir|
dir
else if (std.os.getenv("XDG_CONFIG_HOME")) |xdg|
try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/{s}", .{ xdg, appname })
else if (std.os.getenv("HOME")) |home|
try std.fmt.bufPrint(&local.config_dir_buffer, "{s}/.config/{s}", .{ home, appname })
else
return error.AppConfigDirUnavailable;
local.config_dir = config_dir;
std.fs.makeDirAbsolute(config_dir) catch |e| switch (e) {
error.PathAlreadyExists => {},
else => return e,
};
return config_dir;
}
fn get_app_config_file_name(appname: []const u8) ![]const u8 {
const local = struct {
var config_file_buffer: [std.os.PATH_MAX]u8 = undefined;
var config_file: ?[]const u8 = null;
};
const config_file_name = "config.json";
const config_file = if (local.config_file) |file|
file
else
try std.fmt.bufPrint(&local.config_file_buffer, "{s}/{s}", .{ try get_app_config_dir(appname), config_file_name });
local.config_file = config_file;
return config_file;
}
pub fn get_config_file_name() ![]const u8 {
return get_app_config_file_name(application_name);
}
pub fn get_restore_file_name() ![]const u8 {
const local = struct {
var restore_file_buffer: [std.os.PATH_MAX]u8 = undefined;
var restore_file: ?[]const u8 = null;
};
const restore_file_name = "restore";
const restore_file = if (local.restore_file) |file|
file
else
try std.fmt.bufPrint(&local.restore_file_buffer, "{s}/{s}", .{ try get_app_config_dir(application_name), restore_file_name });
local.restore_file = restore_file;
return restore_file;
}
fn restart() noreturn {
const argv = [_]?[*:0]const u8{
std.os.argv[0],
"--restore-session",
null,
};
const ret = std.c.execve(std.os.argv[0], @ptrCast(&argv), @ptrCast(std.os.environ));
std.io.getStdErr().writer().print("\nrestart failed: {d}", .{ret}) catch {};
exit(234);
}

235
src/ripgrep.zig Normal file
View file

@ -0,0 +1,235 @@
const std = @import("std");
const tp = @import("thespian");
const cbor = @import("cbor");
const log = @import("log");
pub const ripgrep_binary = "rg";
pid: ?tp.pid,
stdin_behavior: std.ChildProcess.StdIo,
const Self = @This();
const module_name = @typeName(Self);
pub const max_chunk_size = tp.subprocess.max_chunk_size;
pub const Writer = std.io.Writer(*Self, error{Exit}, write);
pub const BufferedWriter = std.io.BufferedWriter(max_chunk_size, Writer);
pub const Error = error{ OutOfMemory, Exit };
pub const FindF = fn (a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self;
pub fn find_in_stdin(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self {
return create(a, query, tag, .Pipe);
}
pub fn find_in_files(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8) Error!Self {
return create(a, query, tag, .Close);
}
fn create(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8, stdin_behavior: std.ChildProcess.StdIo) Error!Self {
return .{ .pid = try Process.create(a, query, tag, stdin_behavior), .stdin_behavior = stdin_behavior };
}
pub fn deinit(self: *Self) void {
if (self.pid) |pid| {
if (self.stdin_behavior == .Pipe)
pid.send(.{"close"}) catch {};
self.pid = null;
pid.deinit();
}
}
pub fn write(self: *Self, bytes: []const u8) error{Exit}!usize {
try self.input(bytes);
return bytes.len;
}
pub fn input(self: *const Self, bytes: []const u8) tp.result {
const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed);
var remaining = bytes;
while (remaining.len > 0)
remaining = loop: {
if (remaining.len > max_chunk_size) {
try pid.send(.{ "input", remaining[0..max_chunk_size] });
break :loop remaining[max_chunk_size..];
} else {
try pid.send(.{ "input", remaining });
break :loop &[_]u8{};
}
};
}
pub fn close(self: *Self) void {
self.deinit();
}
pub fn writer(self: *Self) Writer {
return .{ .context = self };
}
pub fn bufferedWriter(self: *Self) BufferedWriter {
return .{ .unbuffered_writer = self.writer() };
}
const Process = struct {
a: std.mem.Allocator,
query: []const u8,
receiver: Receiver,
sp: ?tp.subprocess = null,
output: std.ArrayList(u8),
parent: tp.pid,
tag: [:0]const u8,
logger: log.Logger,
stdin_behavior: std.ChildProcess.StdIo,
const Receiver = tp.Receiver(*Process);
pub fn create(a: std.mem.Allocator, query: []const u8, tag: [:0]const u8, stdin_behavior: std.ChildProcess.StdIo) Error!tp.pid {
const self = try a.create(Process);
self.* = .{
.a = a,
.query = try a.dupe(u8, query),
.receiver = Receiver.init(receive, self),
.output = std.ArrayList(u8).init(a),
.parent = tp.self_pid().clone(),
.tag = try a.dupeZ(u8, tag),
.logger = log.logger(@typeName(Self)),
.stdin_behavior = stdin_behavior,
};
return tp.spawn_link(self.a, self, Process.start, tag) catch |e| tp.exit_error(e);
}
fn deinit(self: *Process) void {
self.output.deinit();
self.a.free(self.query);
self.close() catch {};
}
fn close(self: *Process) tp.result {
if (self.sp) |*sp| {
defer self.sp = null;
try sp.close();
}
}
fn start(self: *Process) tp.result {
_ = tp.set_trap(true);
const args = tp.message.fmt(.{
ripgrep_binary,
// "--line-buffered",
"--fixed-strings",
"--json",
self.query,
});
self.sp = tp.subprocess.init(self.a, args, module_name, self.stdin_behavior) catch |e| return tp.exit_error(e);
tp.receive(&self.receiver);
}
fn receive(self: *Process, _: tp.pid_ref, m: tp.message) tp.result {
errdefer self.deinit();
var bytes: []u8 = "";
if (try m.match(.{ "input", tp.extract(&bytes) })) {
const sp = if (self.sp) |sp| sp else return tp.exit_error(error.Closed);
try sp.send(bytes);
} else if (try m.match(.{"close"})) {
try self.close();
} else if (try m.match(.{ module_name, "stdout", tp.extract(&bytes) })) {
self.handle_output(bytes) catch |e| return tp.exit_error(e);
} else if (try m.match(.{ module_name, "term", tp.more })) {
self.handle_terminated() catch |e| return tp.exit_error(e);
} else if (try m.match(.{ module_name, "stderr", tp.extract(&bytes) })) {
self.logger.print("ERR: {s}", .{bytes});
} else if (try m.match(.{ "exit", "normal" })) {
return tp.exit_normal();
} else {
self.logger.err("receive", tp.unexpected(m));
return tp.unexpected(m);
}
}
fn handle_output(self: *Process, bytes: []u8) !void {
try self.output.appendSlice(bytes);
// @import("log").logger(module_name).print("{s}", .{bytes}) catch {};
}
fn handle_terminated(self: *Process) !void {
const output = try self.output.toOwnedSlice();
var it = std.mem.splitScalar(u8, output, '\n');
while (it.next()) |json| {
if (json.len == 0) continue;
var msg_buf: [tp.max_message_size]u8 = undefined;
const msg: tp.message = .{ .buf = try cbor.fromJson(json, &msg_buf) };
try self.dispatch(msg);
// var buf: [tp.max_message_size]u8 = undefined;
// @import("log").logger(module_name).print("json: {s}", .{try msg.to_json(&buf)}) catch {};
}
// @import("log").logger(module_name).print("done", .{}) catch {};
try self.parent.send(.{ self.tag, "done" });
}
fn dispatch(self: *Process, m: tp.message) !void {
var obj = std.json.ObjectMap.init(self.a);
defer obj.deinit();
if (try m.match(tp.extract(&obj))) {
if (obj.get("type")) |*val| {
if (std.mem.eql(u8, "match", val.string))
if (obj.get("data")) |*data| switch (data.*) {
.object => |*o| try self.dispatch_match(o),
else => {},
};
}
}
}
fn get_match_string(obj: *const std.json.ObjectMap, name: []const u8) ?[]const u8 {
return if (obj.get(name)) |*val| switch (val.*) {
.object => |*o| if (o.get("text")) |*val_| switch (val_.*) {
.string => |s| if (std.mem.eql(u8, "<stdin>", s)) null else s,
else => null,
} else null,
else => null,
} else null;
}
fn dispatch_match(self: *Process, obj: *const std.json.ObjectMap) !void {
const path: ?[]const u8 = get_match_string(obj, "path");
const lines: ?[]const u8 = get_match_string(obj, "lines");
const line = if (obj.get("line_number")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
if (obj.get("submatches")) |*val| switch (val.*) {
.array => |*a| try self.dispatch_submatches(path, line, a, lines),
else => return,
};
}
fn dispatch_submatches(self: *Process, path: ?[]const u8, line: i64, arr: *const std.json.Array, lines: ?[]const u8) !void {
for (arr.items) |*item| switch (item.*) {
.object => |*o| try self.dispatch_submatch(path, line, o, lines),
else => {},
};
}
fn dispatch_submatch(self: *Process, path: ?[]const u8, line: i64, obj: *const std.json.ObjectMap, lines: ?[]const u8) !void {
const begin = if (obj.get("start")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
const end = if (obj.get("end")) |*val| switch (val.*) {
.integer => |i| i,
else => return,
} else return;
if (path) |p| {
const match_text = if (lines) |l|
if (l[l.len - 1] == '\n') l[0 .. l.len - 2] else l
else
"";
try self.parent.send(.{ self.tag, p, line, begin, line, end, match_text });
} else {
try self.parent.send(.{ self.tag, line, begin, line, end });
}
}
};

112
src/syntax.zig Normal file
View file

@ -0,0 +1,112 @@
const std = @import("std");
const treez = @import("treez");
const Self = @This();
pub const Edit = treez.InputEdit;
pub const FileType = @import("file_type.zig");
pub const Range = treez.Range;
pub const Point = treez.Point;
const Language = treez.Language;
const Parser = treez.Parser;
const Query = treez.Query;
const Tree = treez.Tree;
a: std.mem.Allocator,
lang: *const Language,
file_type: *const FileType,
parser: *Parser,
query: *Query,
injections: *Query,
tree: ?*Tree = null,
pub fn create(file_type: *const FileType, a: std.mem.Allocator, content: []const u8) !*Self {
const self = try a.create(Self);
self.* = .{
.a = a,
.lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {d}", .{file_type.name}),
.file_type = file_type,
.parser = try Parser.create(),
.query = try Query.create(self.lang, file_type.highlights),
.injections = try Query.create(self.lang, file_type.highlights),
};
errdefer self.destroy();
try self.parser.setLanguage(self.lang);
try self.parse(content);
return self;
}
pub fn create_file_type(a: std.mem.Allocator, content: []const u8, lang_name: []const u8) !*Self {
const file_type = FileType.get_by_name(lang_name) orelse return error.NotFound;
return create(file_type, a, content);
}
pub fn create_guess_file_type(a: std.mem.Allocator, content: []const u8, file_path: ?[]const u8) !*Self {
const file_type = FileType.guess(file_path, content) orelse return error.NotFound;
return create(file_type, a, content);
}
pub fn destroy(self: *Self) void {
if (self.tree) |tree| tree.destroy();
self.query.destroy();
self.parser.destroy();
self.a.destroy(self);
}
fn parse(self: *Self, content: []const u8) !void {
if (self.tree) |tree| tree.destroy();
self.tree = try self.parser.parseString(null, content);
}
pub fn refresh_full(self: *Self, content: []const u8) !void {
return self.parse(content);
}
pub fn edit(self: *Self, ed: Edit) void {
if (self.tree) |tree| tree.edit(&ed);
}
pub fn refresh(self: *Self, content: []const u8) !void {
const old_tree = self.tree;
defer if (old_tree) |tree| tree.destroy();
self.tree = try self.parser.parseString(old_tree, content);
}
fn CallBack(comptime T: type) type {
return fn (ctx: T, sel: Range, scope: []const u8, id: u32, capture_idx: usize) error{Stop}!void;
}
pub fn render(self: *const Self, ctx: anytype, comptime cb: CallBack(@TypeOf(ctx)), range: ?Range) !void {
const cursor = try Query.Cursor.create();
defer cursor.destroy();
const tree = if (self.tree) |p| p else return;
cursor.execute(self.query, tree.getRootNode());
if (range) |r| cursor.setPointRange(r.start_point, r.end_point);
while (cursor.nextMatch()) |match| {
var idx: usize = 0;
for (match.captures()) |capture| {
try cb(ctx, capture.node.getRange(), self.query.getCaptureNameForId(capture.id), capture.id, idx);
idx += 1;
}
}
}
pub fn highlights_at_point(self: *const Self, ctx: anytype, comptime cb: CallBack(@TypeOf(ctx)), point: Point) void {
const cursor = Query.Cursor.create() catch return;
defer cursor.destroy();
const tree = if (self.tree) |p| p else return;
cursor.execute(self.query, tree.getRootNode());
cursor.setPointRange(.{ .row = point.row, .column = 0 }, .{ .row = point.row + 1, .column = 0 });
while (cursor.nextMatch()) |match| {
for (match.captures()) |capture| {
const range = capture.node.getRange();
const start = range.start_point;
const end = range.end_point;
const scope = self.query.getCaptureNameForId(capture.id);
if (start.row == point.row and start.row <= point.column and point.column < end.column)
cb(ctx, range, scope, capture.id, 0) catch return;
break;
}
}
return;
}

63
src/text_manip.zig Normal file
View file

@ -0,0 +1,63 @@
const std = @import("std");
const TextWriter = std.ArrayList(u8).Writer;
pub fn find_first_non_ws(text: []const u8) ?usize {
for (text, 0..) |c, i| if (c == ' ' or c == '\t') continue else return i;
return null;
}
pub fn find_prefix(prefix: []const u8, text: []const u8) ?usize {
var start: usize = 0;
var pos: usize = 0;
var in_prefix: bool = false;
for (text, 0..) |c, i| {
if (!in_prefix) {
if (c == ' ' or c == '\t')
continue
else {
in_prefix = true;
start = i;
}
}
if (in_prefix) {
if (c == prefix[pos]) {
pos += 1;
if (prefix.len > pos) continue else return start;
} else return null;
}
}
return null;
}
fn toggle_prefix_in_line(prefix: []const u8, text: []const u8, writer: TextWriter) !void {
if (find_prefix(prefix, text)) |pos| {
_ = try writer.write(text[0..pos]);
if (text.len > pos + prefix.len) {
_ = try if (text[pos + prefix.len] == ' ')
writer.write(text[pos + 1 + prefix.len ..])
else
writer.write(text[pos + prefix.len ..]);
}
} else if (find_first_non_ws(text)) |pos| {
_ = try writer.write(text[0..pos]);
_ = try writer.write(prefix);
_ = try writer.write(" ");
_ = try writer.write(text[pos..]);
} else {
_ = try writer.write(prefix);
_ = try writer.write(text);
}
}
pub fn toggle_prefix_in_text(prefix: []const u8, text: []const u8, a: std.mem.Allocator) ![]const u8 {
var result = try std.ArrayList(u8).initCapacity(a, prefix.len + text.len);
const writer = result.writer();
var pos: usize = 0;
while (std.mem.indexOfScalarPos(u8, text, pos, '\n')) |next| {
try toggle_prefix_in_line(prefix, text[pos..next], writer);
_ = try writer.write("\n");
pos = next + 1;
}
return result.toOwnedSlice();
}

9
src/tracy_noop.zig Normal file
View file

@ -0,0 +1,9 @@
pub fn initZone(_: anytype, _: anytype) Zone {
return .{};
}
pub const Zone = struct {
pub fn deinit(_: @This()) void {}
};
pub fn frameMark() void {}

40
src/tui/Box.zig Normal file
View file

@ -0,0 +1,40 @@
const Plane = @import("notcurses").Plane;
const Self = @This();
y: usize = 0,
x: usize = 0,
h: usize = 1,
w: usize = 1,
pub fn opts(self: Self, name_: [:0]const u8) Plane.Options {
return self.opts_flags(name_, 0);
}
pub fn opts_vscroll(self: Self, name_: [:0]const u8) Plane.Options {
return self.opts_flags(name_, Plane.option.VSCROLL);
}
fn opts_flags(self: Self, name_: [:0]const u8, flags: u64) Plane.Options {
return Plane.Options{
.y = @intCast(self.y),
.x = @intCast(self.x),
.rows = @intCast(self.h),
.cols = @intCast(self.w),
.name = name_,
.flags = flags,
};
}
pub fn from(n: Plane) Self {
return .{
.y = @intCast(n.abs_y()),
.x = @intCast(n.abs_x()),
.h = @intCast(n.dim_y()),
.w = @intCast(n.dim_x()),
};
}
pub fn is_abs_coord_inside(self: Self, y: usize, x: usize) bool {
return y >= self.y and y < self.y + self.h and x >= self.x and x < self.x + self.w;
}

175
src/tui/EventHandler.zig Normal file
View file

@ -0,0 +1,175 @@
const std = @import("std");
const tp = @import("thespian");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Self = @This();
const EventHandler = Self;
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) tp.result,
type_name: []const u8,
};
pub fn to_owned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.send = struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.receive,
},
};
}
pub fn to_unowned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.send = if (@hasDecl(child, "send")) struct {
pub fn send(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.send(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.send else struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
_ = try child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.receive,
},
};
}
pub fn bind(pimpl: anytype, comptime f: *const fn (ctx: @TypeOf(pimpl), from: tp.pid_ref, m: tp.message) tp.result) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.send = struct {
pub fn receive(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) tp.result {
return @call(.auto, f, .{ @as(*child, @ptrCast(@alignCast(ctx))), from_, m });
}
}.receive,
},
};
}
pub fn deinit(self: Self) void {
return self.vtable.deinit(self.ptr);
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn msg(self: Self, m: anytype) tp.result {
return self.vtable.send(self.ptr, tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: Self, from_: tp.pid_ref, m: tp.message) tp.result {
return self.vtable.send(self.ptr, from_, m);
}
pub fn empty(a: Allocator) !Self {
const child: type = struct {};
const widget = try a.create(child);
widget.* = .{};
return .{
.ptr = widget,
.plane = &widget.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a_: Allocator) void {
return a_.destroy(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.send = struct {
pub fn receive(_: *anyopaque, _: tp.pid_ref, _: tp.message) tp.result {
return false;
}
}.receive,
},
};
}
pub const List = struct {
a: Allocator,
list: ArrayList(EventHandler),
recursion_check: bool = false,
pub fn init(a: Allocator) List {
return .{
.a = a,
.list = ArrayList(EventHandler).init(a),
};
}
pub fn deinit(self: *List) void {
for (self.list.items) |*i|
i.deinit();
self.list.deinit();
}
pub fn add(self: *List, h: EventHandler) !void {
(try self.list.addOne()).* = h;
}
pub fn remove(self: *List, h: EventHandler) !void {
return self.remove_ptr(h.ptr);
}
pub fn remove_ptr(self: *List, p_: *anyopaque) void {
for (self.list.items, 0..) |*p, i|
if (p.ptr == p_)
self.list.orderedRemove(i).deinit();
}
pub fn msg(self: *const List, m: anytype) tp.result {
return self.send(tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: *const List, from: tp.pid_ref, m: tp.message) tp.result {
if (self.recursion_check)
unreachable;
const self_nonconst = @constCast(self);
self_nonconst.recursion_check = true;
defer self_nonconst.recursion_check = false;
tp.trace(tp.channel.event, m);
var buf: [tp.max_message_size]u8 = undefined;
@memcpy(buf[0..m.buf.len], m.buf);
const m_: tp.message = .{ .buf = buf[0..m.buf.len] };
var e: ?error{Exit} = null;
for (self.list.items) |*i|
i.send(from, m_) catch |e_| {
e = e_;
};
return if (e) |e_| e_;
}
};

138
src/tui/MessageFilter.zig Normal file
View file

@ -0,0 +1,138 @@
const std = @import("std");
const tp = @import("thespian");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Self = @This();
const MessageFilter = Self;
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque) void,
filter: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,
type_name: []const u8,
};
pub fn to_owned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.filter,
},
};
}
pub fn to_unowned(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.filter(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.filter,
},
};
}
pub fn bind(pimpl: anytype, comptime f: *const fn (ctx: @TypeOf(pimpl), from: tp.pid_ref, m: tp.message) error{Exit}!bool) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(_: *anyopaque) void {}
}.deinit,
.filter = struct {
pub fn filter(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return @call(.auto, f, .{ @as(*child, @ptrCast(@alignCast(ctx))), from_, m });
}
}.filter,
},
};
}
pub fn deinit(self: Self) void {
return self.vtable.deinit(self.ptr);
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn filter(self: Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return self.vtable.filter(self.ptr, from_, m);
}
pub const List = struct {
a: Allocator,
list: ArrayList(MessageFilter),
pub fn init(a: Allocator) List {
return .{
.a = a,
.list = ArrayList(MessageFilter).init(a),
};
}
pub fn deinit(self: *List) void {
for (self.list.items) |*i|
i.deinit();
self.list.deinit();
}
pub fn add(self: *List, h: MessageFilter) !void {
(try self.list.addOne()).* = h;
// @import("log").logger("MessageFilter").print("add: {d} {s}", .{ self.list.items.len, self.list.items[self.list.items.len - 1].vtable.type_name });
}
pub fn remove(self: *List, h: MessageFilter) !void {
return self.remove_ptr(h.ptr);
}
pub fn remove_ptr(self: *List, p_: *anyopaque) void {
for (self.list.items, 0..) |*p, i|
if (p.ptr == p_)
self.list.orderedRemove(i).deinit();
}
pub fn filter(self: *const List, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
var buf: [tp.max_message_size]u8 = undefined;
@memcpy(buf[0..m.buf.len], m.buf);
const m_: tp.message = .{ .buf = buf[0..m.buf.len] };
var e: ?error{Exit} = null;
for (self.list.items) |*i| {
const consume = i.filter(from, m_) catch |e_| ret: {
e = e_;
break :ret false;
};
if (consume)
return true;
}
return if (e) |e_| e_ else false;
}
};

273
src/tui/Widget.zig Normal file
View file

@ -0,0 +1,273 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
pub const Box = @import("Box.zig");
pub const EventHandler = @import("EventHandler.zig");
pub const Theme = @import("theme");
pub const themes = @import("themes").themes;
pub const scopes = @import("themes").scopes;
ptr: *anyopaque,
plane: *nc.Plane,
vtable: *const VTable,
const Self = @This();
pub const WalkFn = *const fn (ctx: *anyopaque, w: *Self) bool;
pub const Direction = enum { horizontal, vertical };
pub const Layout = union(enum) {
dynamic,
static: usize,
pub inline fn eql(self: Layout, other: Layout) bool {
return switch (self) {
.dynamic => switch (other) {
.dynamic => true,
.static => false,
},
.static => |s| switch (other) {
.dynamic => false,
.static => |o| s == o,
},
};
}
};
pub const VTable = struct {
deinit: *const fn (ctx: *anyopaque, a: Allocator) void,
send: *const fn (ctx: *anyopaque, from: tp.pid_ref, m: tp.message) error{Exit}!bool,
update: *const fn (ctx: *anyopaque) void,
render: *const fn (ctx: *anyopaque, theme: *const Theme) bool,
resize: *const fn (ctx: *anyopaque, pos: Box) void,
layout: *const fn (ctx: *anyopaque) Layout,
subscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
unsubscribe: *const fn (ctx: *anyopaque, h: EventHandler) error{NotSupported}!void,
get: *const fn (ctx: *anyopaque, name_: []const u8) ?*Self,
walk: *const fn (ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool,
type_name: []const u8,
};
pub fn to(pimpl: anytype) Self {
const impl = @typeInfo(@TypeOf(pimpl));
const child: type = impl.Pointer.child;
return .{
.ptr = pimpl,
.plane = &pimpl.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a: Allocator) void {
return child.deinit(@as(*child, @ptrCast(@alignCast(ctx))), a);
}
}.deinit,
.send = if (@hasDecl(child, "receive")) struct {
pub fn f(ctx: *anyopaque, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return child.receive(@as(*child, @ptrCast(@alignCast(ctx))), from_, m);
}
}.f else struct {
pub fn f(_: *anyopaque, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}
}.f,
.update = if (@hasDecl(child, "update")) struct {
pub fn f(ctx: *anyopaque) void {
return child.update(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.f else struct {
pub fn f(_: *anyopaque) void {}
}.f,
.render = if (@hasDecl(child, "render")) struct {
pub fn f(ctx: *anyopaque, theme: *const Theme) bool {
return child.render(@as(*child, @ptrCast(@alignCast(ctx))), theme);
}
}.f else struct {
pub fn f(_: *anyopaque, _: *const Theme) bool {
return false;
}
}.f,
.resize = if (@hasDecl(child, "handle_resize")) struct {
pub fn f(ctx: *anyopaque, pos: Box) void {
return child.handle_resize(@as(*child, @ptrCast(@alignCast(ctx))), pos);
}
}.f else struct {
pub fn f(ctx: *anyopaque, pos: Box) void {
const self: *child = @ptrCast(@alignCast(ctx));
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
}
}.f,
.layout = if (@hasDecl(child, "layout")) struct {
pub fn f(ctx: *anyopaque) Layout {
return child.layout(@as(*child, @ptrCast(@alignCast(ctx))));
}
}.f else struct {
pub fn f(_: *anyopaque) Layout {
return .dynamic;
}
}.f,
.subscribe = struct {
pub fn subscribe(ctx: *anyopaque, h: EventHandler) error{NotSupported}!void {
return if (comptime @hasDecl(child, "subscribe"))
child.subscribe(@as(*child, @ptrCast(@alignCast(ctx))), h)
else
error.NotSupported;
}
}.subscribe,
.unsubscribe = struct {
pub fn unsubscribe(ctx: *anyopaque, h: EventHandler) error{NotSupported}!void {
return if (comptime @hasDecl(child, "unsubscribe"))
child.unsubscribe(@as(*child, @ptrCast(@alignCast(ctx))), h)
else
error.NotSupported;
}
}.unsubscribe,
.get = struct {
pub fn get(ctx: *anyopaque, name_: []const u8) ?*Self {
return if (comptime @hasDecl(child, "get")) child.get(@as(*child, @ptrCast(@alignCast(ctx))), name_) else null;
}
}.get,
.walk = struct {
pub fn walk(ctx: *anyopaque, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (comptime @hasDecl(child, "walk")) child.walk(@as(*child, @ptrCast(@alignCast(ctx))), walk_ctx, f) else false;
}
}.walk,
},
};
}
pub fn dynamic_cast(self: Self, comptime T: type) ?*T {
return if (std.mem.eql(u8, self.vtable.type_name, @typeName(T)))
@as(*T, @ptrCast(@alignCast(self.ptr)))
else
null;
}
pub fn need_render() void {
tp.self_pid().send(.{"render"}) catch {};
}
pub fn need_reflow() void {
tp.self_pid().send(.{"reflow"}) catch {};
}
pub fn name(self: Self, buf: []u8) []u8 {
return self.plane.name(buf);
}
pub fn box(self: Self) Box {
return Box.from(self.plane.*);
}
pub fn deinit(self: Self, a: Allocator) void {
return self.vtable.deinit(self.ptr, a);
}
pub fn msg(self: *const Self, m: anytype) error{Exit}!bool {
return self.vtable.send(self.ptr, tp.self_pid(), tp.message.fmt(m));
}
pub fn send(self: *const Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
return self.vtable.send(self.ptr, from_, m);
}
pub fn update(self: Self) void {
return self.vtable.update(self.ptr);
}
pub fn render(self: Self, theme: *const Theme) bool {
return self.vtable.render(self.ptr, theme);
}
pub fn resize(self: Self, pos: Box) void {
return self.vtable.resize(self.ptr, pos);
}
pub fn layout(self: Self) Layout {
return self.vtable.layout(self.ptr);
}
pub fn subscribe(self: Self, h: EventHandler) !void {
return self.vtable.subscribe(self.ptr, h);
}
pub fn unsubscribe(self: Self, h: EventHandler) !void {
return self.vtable.unsubscribe(self.ptr, h);
}
pub fn get(self: *Self, name_: []const u8) ?*Self {
var buf: [256]u8 = undefined;
return if (std.mem.eql(u8, self.plane.name(&buf), name_))
self
else
self.vtable.get(self.ptr, name_);
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: WalkFn) bool {
return if (self.vtable.walk(self.ptr, walk_ctx, f)) true else f(walk_ctx, self);
}
pub fn empty(a: Allocator, parent: nc.Plane, layout_: Layout) !Self {
const child: type = struct { plane: nc.Plane, layout: Layout };
const widget = try a.create(child);
const n = try nc.Plane.init(&(Box{}).opts("empty"), parent);
widget.* = .{ .plane = n, .layout = layout_ };
return .{
.ptr = widget,
.plane = &widget.plane,
.vtable = comptime &.{
.type_name = @typeName(child),
.deinit = struct {
pub fn deinit(ctx: *anyopaque, a_: Allocator) void {
const self: *child = @ptrCast(@alignCast(ctx));
self.plane.deinit();
a_.destroy(self);
}
}.deinit,
.send = struct {
pub fn receive(_: *anyopaque, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}
}.receive,
.update = struct {
pub fn update(_: *anyopaque) void {}
}.update,
.render = struct {
pub fn render(_: *anyopaque, _: *const Theme) bool {
return false;
}
}.render,
.resize = struct {
pub fn resize(_: *anyopaque, _: Box) void {}
}.resize,
.layout = struct {
pub fn layout(ctx: *anyopaque) Layout {
const self: *child = @ptrCast(@alignCast(ctx));
return self.layout;
}
}.layout,
.subscribe = struct {
pub fn subscribe(_: *anyopaque, _: EventHandler) error{NotSupported}!void {
return error.NotSupported;
}
}.subscribe,
.unsubscribe = struct {
pub fn unsubscribe(_: *anyopaque, _: EventHandler) error{NotSupported}!void {
return error.NotSupported;
}
}.unsubscribe,
.get = struct {
pub fn get(_: *anyopaque, _: []const u8) ?*Self {
return null;
}
}.get,
.walk = struct {
pub fn walk(_: *anyopaque, _: *anyopaque, _: WalkFn) bool {
return false;
}
}.walk,
},
};
}

223
src/tui/WidgetList.zig Normal file
View file

@ -0,0 +1,223 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const Box = @import("Box.zig");
const Self = @This();
pub const Direction = Widget.Direction;
pub const Layout = Widget.Layout;
const WidgetState = struct {
widget: Widget,
layout: Layout = .{},
};
plane: nc.Plane,
parent: nc.Plane,
a: Allocator,
widgets: ArrayList(WidgetState),
layout: Layout,
direction: Direction,
box: ?Widget.Box = null,
pub fn createH(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self {
const self: *Self = try a.create(Self);
self.* = try init(a, parent, name, .horizontal, layout_);
return self;
}
pub fn createV(a: Allocator, parent: Widget, name: [:0]const u8, layout_: Layout) !*Self {
const self: *Self = try a.create(Self);
self.* = try init(a, parent, name, .vertical, layout_);
return self;
}
fn init(a: Allocator, parent: Widget, name: [:0]const u8, dir: Direction, layout_: Layout) !Self {
return .{
.plane = try nc.Plane.init(&(Box{}).opts(name), parent.plane.*),
.parent = parent.plane.*,
.a = a,
.widgets = ArrayList(WidgetState).init(a),
.layout = layout_,
.direction = dir,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn layout(self: *Self) Widget.Layout {
return self.layout;
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
for (self.widgets.items) |*w|
w.widget.deinit(self.a);
self.widgets.deinit();
self.plane.deinit();
a.destroy(self);
}
pub fn add(self: *Self, w_: Widget) !void {
_ = try self.addP(w_);
}
pub fn addP(self: *Self, w_: Widget) !*Widget {
var w: *WidgetState = try self.widgets.addOne();
w.widget = w_;
w.layout = w_.layout();
return &w.widget;
}
pub fn remove(self: *Self, w: Widget) void {
for (self.widgets.items, 0..) |p, i| if (p.widget.ptr == w.ptr)
self.widgets.orderedRemove(i).widget.deinit(self.a);
}
pub fn empty(self: *const Self) bool {
return self.widgets.items.len == 0;
}
pub fn swap(self: *Self, n: usize, w: Widget) Widget {
const old = self.widgets.items[n];
self.widgets.items[n].widget = w;
self.widgets.items[n].layout = w.layout();
return old.widget;
}
pub fn replace(self: *Self, n: usize, w: Widget) void {
const old = self.swap(n, w);
old.deinit(self.a);
}
pub fn send(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*w|
if (try w.widget.send(from, m))
return true;
return false;
}
pub fn update(self: *Self) void {
for (self.widgets.items) |*w|
w.widget.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
for (self.widgets.items) |*w| if (!w.layout.eql(w.widget.layout())) {
self.refresh_layout();
break;
};
var more = false;
for (self.widgets.items) |*w|
if (w.widget.render(theme)) {
more = true;
};
return more;
}
pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*w|
if (try w.widget.send(from_, m))
return true;
return false;
}
fn get_size_a(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.h,
.horizontal => &pos.w,
};
}
fn get_size_b(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.w,
.horizontal => &pos.h,
};
}
fn get_loc_a(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.y,
.horizontal => &pos.x,
};
}
fn get_loc_b(self: *Self, pos: *Widget.Box) *usize {
return switch (self.direction) {
.vertical => &pos.x,
.horizontal => &pos.y,
};
}
pub fn resize(self: *Self, pos: Widget.Box) void {
return self.handle_resize(pos);
}
fn refresh_layout(self: *Self) void {
return if (self.box) |box| self.handle_resize(box);
}
pub fn handle_resize(self: *Self, pos_: Widget.Box) void {
self.box = pos_;
var pos = pos_;
const total = self.get_size_a(&pos).*;
var avail = total;
var statics: usize = 0;
var dynamics: usize = 0;
for (self.widgets.items) |*w| {
w.layout = w.widget.layout();
switch (w.layout) {
.dynamic => {
dynamics += 1;
},
.static => |val| {
statics += 1;
avail = if (avail > val) avail - val else 0;
},
}
}
const dyn_size = avail / if (dynamics > 0) dynamics else 1;
const rounded: usize = if (dyn_size * dynamics < avail) avail - dyn_size * dynamics else 0;
var cur_loc: usize = self.get_loc_a(&pos).*;
var first = true;
for (self.widgets.items) |*w| {
var w_pos: Box = .{};
const size = switch (w.layout) {
.dynamic => if (first) val: {
first = false;
break :val dyn_size + rounded;
} else dyn_size,
.static => |val| val,
};
self.get_size_a(&w_pos).* = size;
self.get_loc_a(&w_pos).* = cur_loc;
cur_loc += size;
self.get_size_b(&w_pos).* = self.get_size_b(&pos).*;
self.get_loc_b(&w_pos).* = self.get_loc_b(&pos).*;
w.widget.resize(w_pos);
}
}
pub fn get(self: *Self, name_: []const u8) ?*Widget {
for (self.widgets.items) |*w|
if (w.widget.get(name_)) |p|
return p;
return null;
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.widgets.items) |*w|
if (w.widget.walk(walk_ctx, f)) return true;
return false;
}

94
src/tui/WidgetStack.zig Normal file
View file

@ -0,0 +1,94 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const eql = std.mem.eql;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const Self = @This();
a: Allocator,
widgets: ArrayList(Widget),
pub fn init(a_: Allocator) Self {
return .{
.a = a_,
.widgets = ArrayList(Widget).init(a_),
};
}
pub fn deinit(self: *Self) void {
for (self.widgets.items) |*widget|
widget.deinit(self.a);
self.widgets.deinit();
}
pub fn addWidget(self: *Self, widget: Widget) !void {
(try self.widgets.addOne()).* = widget;
}
pub fn swapWidget(self: *Self, n: usize, widget: Widget) Widget {
const old = self.widgets.items[n];
self.widgets.items[n] = widget;
return old;
}
pub fn replaceWidget(self: *Self, n: usize, widget: Widget) void {
const old = self.swapWidget(n, widget);
old.deinit(self.a);
}
pub fn deleteWidget(self: *Self, name: []const u8) bool {
for (self.widgets.items, 0..) |*widget, i| {
var buf: [64]u8 = undefined;
const wname = widget.name(&buf);
if (eql(u8, wname, name)) {
self.widgets.orderedRemove(i).deinit(self.a);
return true;
}
}
return false;
}
pub fn findWidget(self: *Self, name: []const u8) ?*Widget {
for (self.widgets.items) |*widget| {
var buf: [64]u8 = undefined;
const wname = widget.name(&buf);
if (eql(u8, wname, name))
return widget;
}
return null;
}
pub fn send(self: *Self, from: tp.pid_ref, m: tp.message) error{Exit}!bool {
for (self.widgets.items) |*widget|
if (try widget.send(from, m))
return true;
return false;
}
pub fn update(self: *Self) void {
for (self.widgets.items) |*widget| widget.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
var more = false;
for (self.widgets.items) |*widget|
if (widget.render(theme)) {
more = true;
};
return more;
}
pub fn resize(self: *Self, pos: Widget.Box) void {
for (self.widgets.items) |*widget|
widget.resize(pos);
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
for (self.widgets.items) |*w|
if (w.walk(walk_ctx, f)) return true;
return false;
}

194
src/tui/command.zig Normal file
View file

@ -0,0 +1,194 @@
const std = @import("std");
const tp = @import("thespian");
const log = @import("log");
const tui = @import("tui.zig");
pub const ID = usize;
pub const Context = struct {
args: tp.message = .{},
pub fn fmt(value: anytype) Context {
return .{ .args = tp.message.fmtbuf(&context_buffer, value) catch unreachable };
}
};
threadlocal var context_buffer: [tp.max_message_size]u8 = undefined;
pub const fmt = Context.fmt;
const Vtable = struct {
id: ID = 0,
name: []const u8,
run: *const fn (self: *Vtable, ctx: Context) tp.result,
};
pub fn Closure(comptime T: type) type {
return struct {
vtbl: Vtable,
f: FunT,
data: T,
const FunT: type = *const fn (T, ctx: Context) tp.result;
const Self = @This();
pub fn init(f: FunT, data: T, name: []const u8) Self {
return .{
.vtbl = .{
.run = run,
.name = name,
},
.f = f,
.data = data,
};
}
pub fn register(self: *Self) !void {
self.vtbl.id = try addCommand(&self.vtbl);
// try log.logger("cmd").print("addCommand({s}) => {d}", .{ self.vtbl.name, self.vtbl.id });
}
pub fn unregister(self: *Self) void {
removeCommand(self.vtbl.id);
}
fn run(vtbl: *Vtable, ctx: Context) tp.result {
const self: *Self = fromVtable(vtbl);
return self.f(self.data, ctx);
}
fn fromVtable(vtbl: *Vtable) *Self {
return @fieldParentPtr(Self, "vtbl", vtbl);
}
};
}
const CommandTable = std.ArrayList(?*Vtable);
var commands: CommandTable = CommandTable.init(std.heap.page_allocator);
fn addCommand(cmd: *Vtable) !ID {
try commands.append(cmd);
return commands.items.len - 1;
}
pub fn removeCommand(id: ID) void {
commands.items[id] = null;
}
pub fn execute(id: ID, ctx: Context) tp.result {
_ = tui.current(); // assert we are in tui thread scope
if (id >= commands.items.len)
return tp.exit_fmt("CommandNotFound: {d}", .{id});
const cmd = commands.items[id];
if (cmd) |p| {
// var buf: [tp.max_message_size]u8 = undefined;
// log.logger("cmd").print("execute({s}) {s}", .{ p.name, ctx.args.to_json(&buf) catch "" }) catch |e| return tp.exit_error(e);
return p.run(p, ctx);
} else {
return tp.exit_fmt("CommandNotAvailable: {d}", .{id});
}
}
pub fn getId(name: []const u8) ?ID {
for (commands.items) |cmd| {
if (cmd) |p|
if (std.mem.eql(u8, p.name, name))
return p.id;
}
return null;
}
pub fn get_id_cache(name: []const u8, id: *?ID) ?ID {
for (commands.items) |cmd| {
if (cmd) |p|
if (std.mem.eql(u8, p.name, name)) {
id.* = p.id;
return p.id;
};
}
return null;
}
pub fn executeName(name: []const u8, ctx: Context) tp.result {
return execute(getId(name) orelse return tp.exit_fmt("CommandNotFound: {s}", .{name}), ctx);
}
fn CmdDef(comptime T: type) type {
return struct {
const Fn = fn (T, Context) tp.result;
name: [:0]const u8,
f: *const Fn,
};
}
fn getTargetType(comptime Namespace: type) type {
return @field(Namespace, "Target");
}
fn getCommands(comptime Namespace: type) []CmdDef(*getTargetType(Namespace)) {
comptime switch (@typeInfo(Namespace)) {
.Struct => |info| {
var count = 0;
const Target = getTargetType(Namespace);
// @compileLog(Namespace, Target);
for (info.decls) |decl| {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
if (@TypeOf(@field(Namespace, decl.name)) == CmdDef(*Target).Fn)
count += 1;
}
var cmds: [count]CmdDef(*Target) = undefined;
var i = 0;
for (info.decls) |decl| {
if (@TypeOf(@field(Namespace, decl.name)) == CmdDef(*Target).Fn) {
cmds[i] = .{ .f = &@field(Namespace, decl.name), .name = decl.name };
i += 1;
}
}
return &cmds;
},
else => @compileError("expected tuple or struct type"),
};
}
pub fn Collection(comptime Namespace: type) type {
const cmds = comptime getCommands(Namespace);
const Target = getTargetType(Namespace);
const Clsr = Closure(*Target);
var fields: [cmds.len]std.builtin.Type.StructField = undefined;
inline for (cmds, 0..) |cmd, i| {
@setEvalBranchQuota(10_000);
fields[i] = .{
.name = cmd.name,
.type = Clsr,
.default_value = null,
.is_comptime = false,
.alignment = if (@sizeOf(Clsr) > 0) @alignOf(Clsr) else 0,
};
}
const Fields = @Type(.{
.Struct = .{
.is_tuple = false,
.layout = .Auto,
.decls = &.{},
.fields = &fields,
},
});
return struct {
fields: Fields,
const Self = @This();
pub fn init(self: *Self, targetPtr: *Target) !void {
if (cmds.len == 0)
@compileError("no commands found in type " ++ @typeName(Target) ++ " (did you mark them public?)");
inline for (cmds) |cmd| {
@field(self.fields, cmd.name) = Closure(*Target).init(cmd.f, targetPtr, cmd.name);
try @field(self.fields, cmd.name).register();
}
}
pub fn deinit(self: *Self) void {
inline for (cmds) |cmd|
Closure(*Target).unregister(&@field(self.fields, cmd.name));
}
};
}

3290
src/tui/editor.zig Normal file

File diff suppressed because it is too large Load diff

327
src/tui/editor_gutter.zig Normal file
View file

@ -0,0 +1,327 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const diff = @import("diff");
const cbor = @import("cbor");
const root = @import("root");
const Widget = @import("Widget.zig");
const WidgetList = @import("WidgetList.zig");
const EventHandler = @import("EventHandler.zig");
const MessageFilter = @import("MessageFilter.zig");
const tui = @import("tui.zig");
const command = @import("command.zig");
const ed = @import("editor.zig");
a: Allocator,
plane: nc.Plane,
parent: Widget,
lines: u32 = 0,
rows: u32 = 1,
row: u32 = 1,
line: usize = 0,
linenum: bool,
relative: bool,
highlight: bool,
width: usize = 4,
editor: *ed.Editor,
diff: diff,
diff_symbols: std.ArrayList(Symbol),
const Self = @This();
const Kind = enum { insert, modified, delete };
const Symbol = struct { kind: Kind, line: usize };
pub fn create(a: Allocator, parent: Widget, event_source: Widget, editor: *ed.Editor) !Widget {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent.plane.*),
.parent = parent,
.linenum = tui.current().config.gutter_line_numbers,
.relative = tui.current().config.gutter_line_numbers_relative,
.highlight = tui.current().config.highlight_current_line_gutter,
.editor = editor,
.diff = try diff.create(),
.diff_symbols = std.ArrayList(Symbol).init(a),
};
try tui.current().message_filters.add(MessageFilter.bind(self, filter_receive));
try event_source.subscribe(EventHandler.bind(self, handle_event));
return self.widget();
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
self.diff_symbols_clear();
self.diff_symbols.deinit();
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
fn diff_symbols_clear(self: *Self) void {
self.diff_symbols.clearRetainingCapacity();
}
pub fn handle_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
if (try m.match(.{ "E", "update", tp.more }))
return self.diff_update() catch |e| return tp.exit_error(e);
if (try m.match(.{ "E", "view", tp.extract(&self.lines), tp.extract(&self.rows), tp.extract(&self.row) }))
return self.update_width();
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.more }))
return self.update_width();
if (try m.match(.{ "E", "close" })) {
self.lines = 0;
self.line = 0;
}
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var y: i32 = undefined;
var ypx: i32 = undefined;
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
return self.primary_click(y);
if (try m.match(.{ "D", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
return self.primary_drag(y);
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON4, tp.more }))
return self.mouse_click_button4();
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON5, tp.more }))
return self.mouse_click_button5();
return false;
}
fn update_width(self: *Self) void {
if (!self.linenum) return;
var buf: [31]u8 = undefined;
const tmp = std.fmt.bufPrint(&buf, " {d} ", .{self.lines}) catch return;
self.width = if (self.relative and tmp.len > 6) 6 else @max(tmp.len, 4);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.get_width() };
}
inline fn get_width(self: *Self) usize {
return if (self.linenum) self.width else 1;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = "gutter render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.editor_gutter);
self.plane.erase();
if (self.linenum) {
const relative = self.relative or std.mem.eql(u8, tui.get_mode(), root.application_logo ++ "NOR"); // TODO: move to mode
if (relative)
self.render_relative(theme)
else
self.render_linear(theme);
}
return false;
}
pub fn render_linear(self: *Self, theme: *const Widget.Theme) void {
var pos: usize = 0;
var linenum = self.row + 1;
var rows = self.rows;
var diff_symbols = self.diff_symbols.items;
var buf: [31:0]u8 = undefined;
while (rows > 0) : (rows -= 1) {
if (linenum > self.lines) return;
if (linenum == self.line + 1) {
tui.set_base_style(&self.plane, " ", theme.editor_gutter_active);
self.plane.on_styles(nc.style.bold);
} else {
tui.set_base_style(&self.plane, " ", theme.editor_gutter);
self.plane.off_styles(nc.style.bold);
}
_ = self.plane.putstr_aligned(@intCast(pos), nc.Align.right, std.fmt.bufPrintZ(&buf, "{d} ", .{linenum}) catch return) catch {};
if (self.highlight and linenum == self.line + 1)
self.render_line_highlight(pos, theme);
self.render_diff_symbols(&diff_symbols, pos, linenum, theme);
pos += 1;
linenum += 1;
}
}
pub fn render_relative(self: *Self, theme: *const Widget.Theme) void {
const row: isize = @intCast(self.row + 1);
const line: isize = @intCast(self.line + 1);
var pos: usize = 0;
var linenum: isize = row - line;
var rows = self.rows;
var buf: [31:0]u8 = undefined;
while (rows > 0) : (rows -= 1) {
if (pos > self.lines - row) return;
tui.set_base_style(&self.plane, " ", if (linenum == 0) theme.editor_gutter_active else theme.editor_gutter);
const val = @abs(if (linenum == 0) line else linenum);
const fmt = std.fmt.bufPrintZ(&buf, "{d} ", .{val}) catch return;
_ = self.plane.putstr_aligned(@intCast(pos), nc.Align.right, if (fmt.len > 6) "==> " else fmt) catch {};
if (self.highlight and linenum == 0)
self.render_line_highlight(pos, theme);
pos += 1;
linenum += 1;
}
}
inline fn render_line_highlight(self: *Self, pos: usize, theme: *const Widget.Theme) void {
for (0..self.get_width()) |i| {
self.plane.cursor_move_yx(@intCast(pos), @intCast(i)) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style_bg(&cell, theme.editor_line_highlight);
_ = self.plane.putc(&cell) catch {};
}
}
inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Symbol, pos: usize, linenum_: usize, theme: *const Widget.Theme) void {
const linenum = linenum_ - 1;
if (diff_symbols.len == 0) return;
while ((diff_symbols.*)[0].line < linenum) {
diff_symbols.* = (diff_symbols.*)[1..];
if (diff_symbols.len == 0) return;
}
if ((diff_symbols.*)[0].line > linenum) return;
const sym = (diff_symbols.*)[0];
const char = switch (sym.kind) {
.insert => "",
.modified => "",
.delete => "",
};
self.plane.cursor_move_yx(@intCast(pos), @intCast(self.get_width() - 1)) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style_fg(&cell, switch (sym.kind) {
.insert => theme.editor_gutter_added,
.modified => theme.editor_gutter_modified,
.delete => theme.editor_gutter_deleted,
});
_ = self.plane.cell_load(&cell, char) catch {};
_ = self.plane.putc(&cell) catch {};
}
fn primary_click(self: *const Self, y: i32) error{Exit}!bool {
var line = self.row + 1;
line += @intCast(y);
try command.executeName("goto_line", command.fmt(.{line}));
try command.executeName("goto_column", command.fmt(.{1}));
try command.executeName("select_end", .{});
try command.executeName("select_right", .{});
return true;
}
fn primary_drag(_: *const Self, y: i32) error{Exit}!bool {
try command.executeName("drag_to", command.fmt(.{ y + 1, 0 }));
return true;
}
fn mouse_click_button4(_: *Self) error{Exit}!bool {
try command.executeName("scroll_up_pageup", .{});
return true;
}
fn mouse_click_button5(_: *Self) error{Exit}!bool {
try command.executeName("scroll_down_pagedown", .{});
return true;
}
fn diff_update(self: *Self) !void {
const editor = self.editor;
const new = if (editor.get_current_root()) |new| new else return;
const old = if (editor.buffer) |buffer| if (buffer.last_save) |old| old else return else return;
return self.diff.diff(diff_result, new, old);
}
fn diff_result(from: tp.pid_ref, edits: []diff.Edit) void {
diff_result_send(from, edits) catch |e| @import("log").logger(@typeName(Self)).err("diff", e);
}
fn diff_result_send(from: tp.pid_ref, edits: []diff.Edit) !void {
var buf: [tp.max_message_size]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
const writer = stream.writer();
try cbor.writeArrayHeader(writer, 2);
try cbor.writeValue(writer, "DIFF");
try cbor.writeArrayHeader(writer, edits.len);
for (edits) |edit| {
try cbor.writeArrayHeader(writer, 4);
try cbor.writeValue(writer, switch (edit.kind) {
.insert => "I",
.delete => "D",
});
try cbor.writeValue(writer, edit.line);
try cbor.writeValue(writer, edit.offset);
try cbor.writeValue(writer, edit.bytes);
}
from.send_raw(tp.message{ .buf = stream.getWritten() }) catch return;
}
pub fn process_diff(self: *Self, cb: []const u8) !void {
var iter = cb;
self.diff_symbols_clear();
var count = try cbor.decodeArrayHeader(&iter);
while (count > 0) : (count -= 1) {
var line: usize = undefined;
var offset: usize = undefined;
var bytes: []const u8 = undefined;
if (try cbor.matchValue(&iter, .{ "I", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
var pos: usize = 0;
var ln: usize = line;
while (std.mem.indexOfScalarPos(u8, bytes, pos, '\n')) |next| {
const end = if (next < bytes.len) next + 1 else next;
try self.process_edit(.insert, ln, offset, bytes[pos..end]);
pos = next + 1;
ln += 1;
offset = 0;
}
try self.process_edit(.insert, ln, offset, bytes[pos..]);
continue;
}
if (try cbor.matchValue(&iter, .{ "D", cbor.extract(&line), cbor.extract(&offset), cbor.extract(&bytes) })) {
try self.process_edit(.delete, line, offset, bytes);
continue;
}
}
}
fn process_edit(self: *Self, kind: Kind, line: usize, offset: usize, bytes: []const u8) !void {
const change = if (self.diff_symbols.items.len > 0) self.diff_symbols.items[self.diff_symbols.items.len - 1].line == line else false;
if (change) {
self.diff_symbols.items[self.diff_symbols.items.len - 1].kind = .modified;
return;
}
(try self.diff_symbols.addOne()).* = switch (kind) {
.insert => ret: {
if (offset > 0)
break :ret .{ .kind = .modified, .line = line };
if (bytes.len == 0)
return;
if (bytes[bytes.len - 1] == '\n')
break :ret .{ .kind = .insert, .line = line };
break :ret .{ .kind = .modified, .line = line };
},
.delete => .{ .kind = .delete, .line = line },
else => unreachable,
};
}
pub fn filter_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var cb: []const u8 = undefined;
if (try m.match(.{ "DIFF", tp.extract_cbor(&cb) })) {
self.process_diff(cb) catch |e| return tp.exit_error(e);
return true;
}
return false;
}

185
src/tui/fonts.zig Normal file
View file

@ -0,0 +1,185 @@
const nc = @import("notcurses");
pub fn print_string_large(n: nc.Plane, s: []const u8) !void {
for (s) |c|
print_char_large(n, c) catch break;
}
pub fn print_char_large(n: nc.Plane, char: u8) !void {
const bitmap = font8x8[char];
for (0..8) |y| {
for (0..8) |x| {
const set = bitmap[y] & @as(usize, 1) << @intCast(x);
if (set != 0) {
_ = try n.putstr("");
} else {
n.cursor_move_rel(0, 1) catch {};
}
}
n.cursor_move_rel(1, -8) catch {};
}
n.cursor_move_rel(-8, 8) catch {};
}
pub fn print_string_medium(n: nc.Plane, s: []const u8) !void {
for (s) |c|
print_char_medium(n, c) catch break;
}
const QUADBLOCKS = [_][:0]const u8{ " ", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" };
pub fn print_char_medium(n: nc.Plane, char: u8) !void {
const bitmap = font8x8[char];
for (0..4) |y| {
for (0..4) |x| {
const yt = 2 * y;
const yb = 2 * y + 1;
const xl = 2 * x;
const xr = 2 * x + 1;
const settl: u4 = if (bitmap[yt] & @as(usize, 1) << @intCast(xl) != 0) 1 else 0;
const settr: u4 = if (bitmap[yt] & @as(usize, 1) << @intCast(xr) != 0) 2 else 0;
const setbl: u4 = if (bitmap[yb] & @as(usize, 1) << @intCast(xl) != 0) 4 else 0;
const setbr: u4 = if (bitmap[yb] & @as(usize, 1) << @intCast(xr) != 0) 8 else 0;
const quadidx: u4 = setbr | setbl | settr | settl;
const c = QUADBLOCKS[quadidx];
if (quadidx != 0) {
_ = try n.putstr(c);
} else {
n.cursor_move_rel(0, 1) catch {};
}
}
n.cursor_move_rel(1, -4) catch {};
}
n.cursor_move_rel(-4, 4) catch {};
}
pub const font8x8: [128][8]u8 = [128][8]u8{
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
[8]u8{ 24, 60, 60, 24, 24, 0, 24, 0 },
[8]u8{ 54, 54, 0, 0, 0, 0, 0, 0 },
[8]u8{ 54, 54, 127, 54, 127, 54, 54, 0 },
[8]u8{ 12, 62, 3, 30, 48, 31, 12, 0 },
[8]u8{ 0, 99, 51, 24, 12, 102, 99, 0 },
[8]u8{ 28, 54, 28, 110, 59, 51, 110, 0 },
[8]u8{ 6, 6, 3, 0, 0, 0, 0, 0 },
[8]u8{ 24, 12, 6, 6, 6, 12, 24, 0 },
[8]u8{ 6, 12, 24, 24, 24, 12, 6, 0 },
[8]u8{ 0, 102, 60, 255, 60, 102, 0, 0 },
[8]u8{ 0, 12, 12, 63, 12, 12, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 12, 12, 6 },
[8]u8{ 0, 0, 0, 63, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 12, 12, 0 },
[8]u8{ 96, 48, 24, 12, 6, 3, 1, 0 },
[8]u8{ 62, 99, 115, 123, 111, 103, 62, 0 },
[8]u8{ 12, 14, 12, 12, 12, 12, 63, 0 },
[8]u8{ 30, 51, 48, 28, 6, 51, 63, 0 },
[8]u8{ 30, 51, 48, 28, 48, 51, 30, 0 },
[8]u8{ 56, 60, 54, 51, 127, 48, 120, 0 },
[8]u8{ 63, 3, 31, 48, 48, 51, 30, 0 },
[8]u8{ 28, 6, 3, 31, 51, 51, 30, 0 },
[8]u8{ 63, 51, 48, 24, 12, 12, 12, 0 },
[8]u8{ 30, 51, 51, 30, 51, 51, 30, 0 },
[8]u8{ 30, 51, 51, 62, 48, 24, 14, 0 },
[8]u8{ 0, 12, 12, 0, 0, 12, 12, 0 },
[8]u8{ 0, 12, 12, 0, 0, 12, 12, 6 },
[8]u8{ 24, 12, 6, 3, 6, 12, 24, 0 },
[8]u8{ 0, 0, 63, 0, 0, 63, 0, 0 },
[8]u8{ 6, 12, 24, 48, 24, 12, 6, 0 },
[8]u8{ 30, 51, 48, 24, 12, 0, 12, 0 },
[8]u8{ 62, 99, 123, 123, 123, 3, 30, 0 },
[8]u8{ 12, 30, 51, 51, 63, 51, 51, 0 },
[8]u8{ 63, 102, 102, 62, 102, 102, 63, 0 },
[8]u8{ 60, 102, 3, 3, 3, 102, 60, 0 },
[8]u8{ 31, 54, 102, 102, 102, 54, 31, 0 },
[8]u8{ 127, 70, 22, 30, 22, 70, 127, 0 },
[8]u8{ 127, 70, 22, 30, 22, 6, 15, 0 },
[8]u8{ 60, 102, 3, 3, 115, 102, 124, 0 },
[8]u8{ 51, 51, 51, 63, 51, 51, 51, 0 },
[8]u8{ 30, 12, 12, 12, 12, 12, 30, 0 },
[8]u8{ 120, 48, 48, 48, 51, 51, 30, 0 },
[8]u8{ 103, 102, 54, 30, 54, 102, 103, 0 },
[8]u8{ 15, 6, 6, 6, 70, 102, 127, 0 },
[8]u8{ 99, 119, 127, 127, 107, 99, 99, 0 },
[8]u8{ 99, 103, 111, 123, 115, 99, 99, 0 },
[8]u8{ 28, 54, 99, 99, 99, 54, 28, 0 },
[8]u8{ 63, 102, 102, 62, 6, 6, 15, 0 },
[8]u8{ 30, 51, 51, 51, 59, 30, 56, 0 },
[8]u8{ 63, 102, 102, 62, 54, 102, 103, 0 },
[8]u8{ 30, 51, 7, 14, 56, 51, 30, 0 },
[8]u8{ 63, 45, 12, 12, 12, 12, 30, 0 },
[8]u8{ 51, 51, 51, 51, 51, 51, 63, 0 },
[8]u8{ 51, 51, 51, 51, 51, 30, 12, 0 },
[8]u8{ 99, 99, 99, 107, 127, 119, 99, 0 },
[8]u8{ 99, 99, 54, 28, 28, 54, 99, 0 },
[8]u8{ 51, 51, 51, 30, 12, 12, 30, 0 },
[8]u8{ 127, 99, 49, 24, 76, 102, 127, 0 },
[8]u8{ 30, 6, 6, 6, 6, 6, 30, 0 },
[8]u8{ 3, 6, 12, 24, 48, 96, 64, 0 },
[8]u8{ 30, 24, 24, 24, 24, 24, 30, 0 },
[8]u8{ 8, 28, 54, 99, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 255 },
[8]u8{ 12, 12, 24, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 30, 48, 62, 51, 110, 0 },
[8]u8{ 7, 6, 6, 62, 102, 102, 59, 0 },
[8]u8{ 0, 0, 30, 51, 3, 51, 30, 0 },
[8]u8{ 56, 48, 48, 62, 51, 51, 110, 0 },
[8]u8{ 0, 0, 30, 51, 63, 3, 30, 0 },
[8]u8{ 28, 54, 6, 15, 6, 6, 15, 0 },
[8]u8{ 0, 0, 110, 51, 51, 62, 48, 31 },
[8]u8{ 7, 6, 54, 110, 102, 102, 103, 0 },
[8]u8{ 12, 0, 14, 12, 12, 12, 30, 0 },
[8]u8{ 48, 0, 48, 48, 48, 51, 51, 30 },
[8]u8{ 7, 6, 102, 54, 30, 54, 103, 0 },
[8]u8{ 14, 12, 12, 12, 12, 12, 30, 0 },
[8]u8{ 0, 0, 51, 127, 127, 107, 99, 0 },
[8]u8{ 0, 0, 31, 51, 51, 51, 51, 0 },
[8]u8{ 0, 0, 30, 51, 51, 51, 30, 0 },
[8]u8{ 0, 0, 59, 102, 102, 62, 6, 15 },
[8]u8{ 0, 0, 110, 51, 51, 62, 48, 120 },
[8]u8{ 0, 0, 59, 110, 102, 6, 15, 0 },
[8]u8{ 0, 0, 62, 3, 30, 48, 31, 0 },
[8]u8{ 8, 12, 62, 12, 12, 44, 24, 0 },
[8]u8{ 0, 0, 51, 51, 51, 51, 110, 0 },
[8]u8{ 0, 0, 51, 51, 51, 30, 12, 0 },
[8]u8{ 0, 0, 99, 107, 127, 127, 54, 0 },
[8]u8{ 0, 0, 99, 54, 28, 54, 99, 0 },
[8]u8{ 0, 0, 51, 51, 51, 62, 48, 31 },
[8]u8{ 0, 0, 63, 25, 12, 38, 63, 0 },
[8]u8{ 56, 12, 12, 7, 12, 12, 56, 0 },
[8]u8{ 24, 24, 24, 0, 24, 24, 24, 0 },
[8]u8{ 7, 12, 12, 56, 12, 12, 7, 0 },
[8]u8{ 110, 59, 0, 0, 0, 0, 0, 0 },
[8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 },
};

263
src/tui/home.zig Normal file
View file

@ -0,0 +1,263 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const tui = @import("tui.zig");
const command = @import("command.zig");
const fonts = @import("fonts.zig");
a: std.mem.Allocator,
plane: nc.Plane,
parent: nc.Plane,
fire: ?Fire = null,
commands: Commands = undefined,
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: Widget) !Widget {
const self: *Self = try a.create(Self);
var n = try nc.Plane.init(&(Widget.Box{}).opts("editor"), parent.plane.*);
errdefer n.deinit();
self.* = .{
.a = a,
.parent = parent.plane.*,
.plane = n,
};
try self.commands.init(self);
command.executeName("enter_mode", command.Context.fmt(.{"home"})) catch {};
return Widget.to(self);
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.commands.deinit();
self.plane.deinit();
if (self.fire) |*fire| fire.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.editor);
self.plane.erase();
self.plane.home();
if (self.fire) |*fire| fire.render() catch unreachable;
const style_title = if (tui.find_scope_style(theme, "function")) |sty| sty.style else theme.editor;
const style_subtext = if (tui.find_scope_style(theme, "comment")) |sty| sty.style else theme.editor;
const style_text = if (tui.find_scope_style(theme, "keyword")) |sty| sty.style else theme.editor;
const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else theme.editor;
const title = "Flow Control";
const subtext = "a programmer's text editor";
if (self.plane.dim_x() > 120 and self.plane.dim_y() > 22) {
self.set_style(style_title);
self.plane.cursor_move_yx(2, 4) catch return false;
fonts.print_string_large(self.plane, title) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(10, 8) catch return false;
fonts.print_string_medium(self.plane, subtext) catch return false;
self.plane.cursor_move_yx(15, 10) catch return false;
} else if (self.plane.dim_x() > 55 and self.plane.dim_y() > 16) {
self.set_style(style_title);
self.plane.cursor_move_yx(2, 4) catch return false;
fonts.print_string_medium(self.plane, title) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(7, 6) catch return false;
_ = self.plane.print(subtext, .{}) catch {};
self.plane.cursor_move_yx(9, 8) catch return false;
} else {
self.set_style(style_title);
self.plane.cursor_move_yx(1, 4) catch return false;
_ = self.plane.print(title, .{}) catch return false;
self.set_style(style_subtext);
self.plane.cursor_move_yx(3, 6) catch return false;
_ = self.plane.print(subtext, .{}) catch {};
self.plane.cursor_move_yx(5, 8) catch return false;
}
if (self.plane.dim_x() > 48 and self.plane.dim_y() > 12)
self.render_hints(style_subtext, style_text, style_keybind);
return true;
}
fn render_hints(self: *Self, style_base: Widget.Theme.Style, style_text: Widget.Theme.Style, style_keybind: Widget.Theme.Style) void {
const hint_text: [:0]const u8 =
\\Help ······················· :F1 / C-?
\\Open file ·················· :C-o
\\Open recent file ··········· :C-e / C-r
\\Show/Run commands ·········· :C-p / C-S-p
\\Open config file ··········· :F6
\\Quit/Close ················· :C-q, C-w
\\
;
const left: c_int = @intCast(self.plane.cursor_x());
var pos: usize = 0;
while (std.mem.indexOfScalarPos(u8, hint_text, pos, '\n')) |next| {
const line = hint_text[pos..next];
const sep = std.mem.indexOfScalar(u8, line, ':') orelse line.len;
self.set_style(style_base);
self.set_style(style_text);
_ = self.plane.print("{s}", .{line[0..sep]}) catch {};
self.set_style(style_keybind);
_ = self.plane.print("{s}", .{line[sep + 1 ..]}) catch {};
self.plane.cursor_move_rel(1, 0) catch {};
self.plane.cursor_move_yx(-1, left) catch {};
pos = next + 1;
}
}
fn set_style(self: *Self, style: Widget.Theme.Style) void {
tui.set_style(&self.plane, style);
}
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
if (self.fire) |*fire| {
fire.deinit();
self.fire = Fire.init(self.a, self.plane, pos) catch unreachable;
}
}
const Commands = command.Collection(cmds);
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
pub fn home_sheeran(self: *Self, _: Ctx) tp.result {
self.fire = if (self.fire) |*fire| ret: {
fire.deinit();
break :ret null;
} else Fire.init(self.a, self.plane, Widget.Box.from(self.plane)) catch |e| return tp.exit_error(e);
}
};
const Fire = struct {
const px = "";
allocator: std.mem.Allocator,
plane: nc.Plane,
prng: std.rand.DefaultPrng,
//scope cache - spread fire
spread_px: u8 = 0,
spread_rnd_idx: u8 = 0,
spread_dst: u16 = 0,
FIRE_H: u16,
FIRE_W: u16,
FIRE_SZ: u16,
FIRE_LAST_ROW: u16,
screen_buf: []u8,
const MAX_COLOR = 256;
const LAST_COLOR = MAX_COLOR - 1;
fn init(a: std.mem.Allocator, plane: nc.Plane, pos: Widget.Box) !Fire {
const FIRE_H = @as(u16, @intCast(pos.h)) * 2;
const FIRE_W = @as(u16, @intCast(pos.w));
var self: Fire = .{
.allocator = a,
.plane = plane,
.prng = std.rand.DefaultPrng.init(blk: {
var seed: u64 = undefined;
try std.os.getrandom(std.mem.asBytes(&seed));
break :blk seed;
}),
.FIRE_H = FIRE_H,
.FIRE_W = FIRE_W,
.FIRE_SZ = FIRE_H * FIRE_W,
.FIRE_LAST_ROW = (FIRE_H - 1) * FIRE_W,
.screen_buf = try a.alloc(u8, FIRE_H * FIRE_W),
};
var buf_idx: u16 = 0;
while (buf_idx < self.FIRE_SZ) : (buf_idx += 1) {
self.screen_buf[buf_idx] = fire_black;
}
// last row is white...white is "fire source"
buf_idx = 0;
while (buf_idx < self.FIRE_W) : (buf_idx += 1) {
self.screen_buf[self.FIRE_LAST_ROW + buf_idx] = fire_white;
}
return self;
}
fn deinit(self: *Fire) void {
self.allocator.free(self.screen_buf);
}
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 };
const fire_black: u8 = 0;
const fire_white: u8 = fire_palette.len - 1;
fn render(self: *Fire) !void {
var rand = self.prng.random();
//update fire buf
var doFire_x: u16 = 0;
while (doFire_x < self.FIRE_W) : (doFire_x += 1) {
var doFire_y: u16 = 0;
while (doFire_y < self.FIRE_H) : (doFire_y += 1) {
const doFire_idx: u16 = doFire_y * self.FIRE_W + doFire_x;
//spread fire
self.spread_px = self.screen_buf[doFire_idx];
//bounds checking
if ((self.spread_px == 0) and (doFire_idx >= self.FIRE_W)) {
self.screen_buf[doFire_idx - self.FIRE_W] = 0;
} else {
self.spread_rnd_idx = rand.intRangeAtMost(u8, 0, 3);
if (doFire_idx >= (self.spread_rnd_idx + 1)) {
self.spread_dst = doFire_idx - self.spread_rnd_idx + 1;
} else {
self.spread_dst = doFire_idx;
}
if (self.spread_dst >= self.FIRE_W) {
if (self.spread_px > (self.spread_rnd_idx & 1)) {
self.screen_buf[self.spread_dst - self.FIRE_W] = self.spread_px - (self.spread_rnd_idx & 1);
} else {
self.screen_buf[self.spread_dst - self.FIRE_W] = 0;
}
}
}
}
}
//scope cache - fire 2 screen buffer
var frame_x: u16 = 0;
var frame_y: u16 = 0;
// for each row
frame_y = 0;
while (frame_y < self.FIRE_H) : (frame_y += 2) { // 'paint' two rows at a time because of half height char
// for each col
frame_x = 0;
while (frame_x < self.FIRE_W) : (frame_x += 1) {
//each character rendered is actually to rows of 'pixels'
// - "hi" (current px row => fg char)
// - "low" (next row => bg color)
const px_hi = self.screen_buf[frame_y * self.FIRE_W + frame_x];
const px_lo = self.screen_buf[(frame_y + 1) * self.FIRE_W + frame_x];
try self.plane.set_fg_palindex(fire_palette[px_hi]);
try self.plane.set_bg_palindex(fire_palette[px_lo]);
_ = try self.plane.putstr(px);
}
self.plane.cursor_move_yx(-1, 0) catch {};
self.plane.cursor_move_rel(1, 0) catch {};
}
}
};

106
src/tui/inputview.zig Normal file
View file

@ -0,0 +1,106 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const Mutex = @import("std").Thread.Mutex;
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const A = nc.Align;
pub const name = "inputview";
parent: nc.Plane,
plane: nc.Plane,
lastbuf: [4096]u8 = undefined,
last: []u8 = "",
last_count: u64 = 0,
last_time: i64 = 0,
last_tdiff: i64 = 0,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
.last_time = time.microTimestamp(),
};
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.panel);
return false;
}
fn output_tdiff(self: *Self, tdiff: i64) !void {
const msi = @divFloor(tdiff, time.us_per_ms);
if (msi == 0) {
const d: f64 = @floatFromInt(tdiff);
const ms = d / time.us_per_ms;
_ = try self.plane.print("{d:6.2}▎", .{ms});
} else {
const ms: u64 = @intCast(msi);
_ = try self.plane.print("{d:6}▎", .{ms});
}
}
fn output_new(self: *Self, json: []const u8) !void {
if (self.plane.cursor_x() != 0)
_ = try self.plane.putstr("\n");
const ts = time.microTimestamp();
const tdiff = ts - self.last_time;
self.last_count = 0;
self.last = self.lastbuf[0..json.len];
@memcpy(self.last, json);
try self.output_tdiff(tdiff);
_ = try self.plane.print("{s}", .{json});
self.last_time = ts;
self.last_tdiff = tdiff;
}
fn output_repeat(self: *Self, json: []const u8) !void {
if (self.plane.cursor_x() != 0)
try self.plane.cursor_move_yx(-1, 0);
self.last_count += 1;
try self.output_tdiff(self.last_tdiff);
_ = try self.plane.print("{s} ({})", .{ json, self.last_count });
}
fn output(self: *Self, json: []const u8) !void {
return if (!eql(u8, json, self.last))
self.output_new(json)
else
self.output_repeat(json);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
if (try m.match(.{ "M", tp.more })) return;
var buf: [4096]u8 = undefined;
const json = m.to_json(&buf) catch |e| return tp.exit_error(e);
self.output(json) catch |e| return tp.exit_error(e);
}
pub fn receive(_: *Self, _: tp.pid_ref, _: tp.message) error{Exit}!bool {
return false;
}

181
src/tui/inspector_view.zig Normal file
View file

@ -0,0 +1,181 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const Buffer = @import("Buffer");
const color = @import("color");
const syntax = @import("syntax");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const mainview = @import("mainview.zig");
const ed = @import("editor.zig");
const A = nc.Align;
pub const name = @typeName(Self);
plane: nc.Plane,
editor: *ed.Editor,
need_render: bool = true,
need_clear: bool = false,
theme: ?*const Widget.Theme = null,
theme_name: []const u8 = "",
pos_cache: ed.PosToWidthCache,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
const self: *Self = try a.create(Self);
self.* = .{
.plane = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent),
.editor = editor,
.pos_cache = try ed.PosToWidthCache.init(a),
};
try editor.handlers.add(EventHandler.bind(self, ed_receive));
return Widget.to(self);
};
return error.NotFound;
}
pub fn deinit(self: *Self, a: Allocator) void {
self.editor.handlers.remove_ptr(self);
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
self.reset_style();
self.theme = theme;
if (self.theme_name.ptr != theme.name.ptr) {
self.theme_name = theme.name;
self.need_render = true;
}
if (self.need_render) {
self.need_render = false;
const cursor = self.editor.get_primary().cursor;
self.inspect_location(cursor.row, cursor.col);
}
return false;
}
pub fn handle_resize(self: *Self, pos: Widget.Box) void {
self.plane.move_yx(@intCast(pos.y), @intCast(pos.x)) catch return;
self.plane.resize_simple(@intCast(pos.h), @intCast(pos.w)) catch return;
self.need_render = true;
}
fn ed_receive(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var row: usize = 0;
var col: usize = 0;
if (try m.match(.{ "E", "pos", tp.any, tp.extract(&row), tp.extract(&col) }))
return self.inspect_location(row, col);
if (try m.match(.{ "E", "location", "modified", tp.extract(&row), tp.extract(&col), tp.more })) {
self.need_render = true;
return;
}
if (try m.match(.{ "E", "close" }))
return self.clear();
}
fn clear(self: *Self) void {
self.plane.erase();
self.plane.home();
}
fn inspect_location(self: *Self, row: usize, col: usize) void {
self.need_clear = true;
const syn = if (self.editor.syntax) |p| p else return;
syn.highlights_at_point(self, dump_highlight, .{ .row = @intCast(row), .column = @intCast(col) });
}
fn get_buffer_text(self: *Self, buf: []u8, sel: Buffer.Selection) ?[]const u8 {
const root = self.editor.get_current_root() orelse return null;
return root.get_range(sel, buf, null, null) catch return null;
}
fn dump_highlight(self: *Self, range: syntax.Range, scope: []const u8, id: u32, _: usize) error{Stop}!void {
const sel = self.pos_cache.range_to_selection(range, self.editor.get_current_root() orelse return) orelse return;
if (self.need_clear) {
self.need_clear = false;
self.clear();
}
if (self.editor.matches.items.len == 0) {
(self.editor.matches.addOne() catch return).* = ed.Match.from_selection(sel);
} else if (self.editor.matches.items.len == 1) {
self.editor.matches.items[0] = ed.Match.from_selection(sel);
}
var buf: [1024]u8 = undefined;
const text = self.get_buffer_text(&buf, sel) orelse "";
if (self.editor.style_lookup(self.theme, scope, id)) |token| {
if (text.len > 14) {
_ = self.plane.print("scope: {s} -> \"{s}...\" matched: {s}", .{
scope,
text[0..15],
Widget.scopes[token.id],
}) catch {};
} else {
_ = self.plane.print("scope: {s} -> \"{s}\" matched: {s}", .{
scope,
text,
Widget.scopes[token.id],
}) catch {};
}
self.show_color("fg", token.style.fg);
self.show_color("bg", token.style.bg);
self.show_font(token.style.fs);
_ = self.plane.print("\n", .{}) catch {};
return;
}
_ = self.plane.print("scope: {s} -> \"{s}\"\n", .{ scope, text }) catch return;
}
fn show_color(self: *Self, tag: []const u8, c_: ?Widget.Theme.Color) void {
const theme = self.theme orelse return;
if (c_) |c| {
_ = self.plane.print(" {s}:", .{tag}) catch return;
self.plane.set_bg_rgb(c) catch {};
self.plane.set_fg_rgb(color.max_contrast(c, theme.panel.fg orelse 0xFFFFFF, theme.panel.bg orelse 0x000000)) catch {};
_ = self.plane.print("#{x}", .{c}) catch return;
self.reset_style();
}
}
fn show_font(self: *Self, font: ?Widget.Theme.FontStyle) void {
if (font) |fs| switch (fs) {
.normal => {
self.plane.set_styles(nc.style.none);
_ = self.plane.print(" normal", .{}) catch return;
},
.bold => {
self.plane.set_styles(nc.style.bold);
_ = self.plane.print(" bold", .{}) catch return;
},
.italic => {
self.plane.set_styles(nc.style.italic);
_ = self.plane.print(" italic", .{}) catch return;
},
.underline => {
self.plane.set_styles(nc.style.underline);
_ = self.plane.print(" underline", .{}) catch return;
},
.strikethrough => {
self.plane.set_styles(nc.style.struck);
_ = self.plane.print(" strikethrough", .{}) catch return;
},
};
self.plane.set_styles(nc.style.none);
}
fn reset_style(self: *Self) void {
tui.set_base_style(&self.plane, " ", (self.theme orelse return).panel);
}

132
src/tui/logview.zig Normal file
View file

@ -0,0 +1,132 @@
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const time = @import("std").time;
const Allocator = @import("std").mem.Allocator;
const Mutex = @import("std").Thread.Mutex;
const nc = @import("notcurses");
const tp = @import("thespian");
const log = @import("log");
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const MessageFilter = @import("MessageFilter.zig");
const escape = fmt.fmtSliceEscapeLower;
const A = nc.Align;
pub const name = @typeName(Self);
plane: nc.Plane,
lastbuf_src: [128]u8 = undefined,
lastbuf_msg: [log.max_log_message]u8 = undefined,
last_src: []u8 = "",
last_msg: []u8 = "",
last_count: u64 = 0,
last_time: i64 = 0,
last_tdiff: i64 = 0,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = init(parent) catch |e| return tp.exit_error(e);
try tui.current().message_filters.add(MessageFilter.bind(self, log_receive));
try log.subscribe();
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent);
errdefer n.deinit();
return .{
.plane = n,
.last_time = time.microTimestamp(),
};
}
pub fn deinit(self: *Self, a: Allocator) void {
log.unsubscribe() catch {};
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.panel);
return false;
}
pub fn log_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "log", tp.more })) {
self.log_process(m) catch |e| return tp.exit_error(e);
return true;
}
return false;
}
pub fn log_process(self: *Self, m: tp.message) !void {
var src: []const u8 = undefined;
var context: []const u8 = undefined;
var msg: []const u8 = undefined;
if (try m.match(.{ "log", tp.extract(&src), tp.extract(&msg) })) {
try self.output(src, msg);
} else if (try m.match(.{ "log", "error", tp.extract(&src), tp.extract(&context), "->", tp.extract(&msg) })) {
try self.output_error(src, context, msg);
} else if (try m.match(.{ "log", tp.extract(&src), tp.more })) {
try self.output_json(src, m);
}
}
fn output_tdiff(self: *Self, tdiff: i64) !void {
const msi = @divFloor(tdiff, time.us_per_ms);
if (msi == 0) {
const d: f64 = @floatFromInt(tdiff);
const ms = d / time.us_per_ms;
_ = try self.plane.print("\n{d:6.2} ▏", .{ms});
} else {
const ms: u64 = @intCast(msi);
_ = try self.plane.print("\n{d:6} ▏", .{ms});
}
}
fn output_new(self: *Self, src: []const u8, msg: []const u8) !void {
const ts = time.microTimestamp();
const tdiff = ts - self.last_time;
self.last_count = 0;
self.last_src = self.lastbuf_src[0..src.len];
self.last_msg = self.lastbuf_msg[0..msg.len];
@memcpy(self.last_src, src);
@memcpy(self.last_msg, msg);
try self.output_tdiff(tdiff);
_ = try self.plane.print("{s}: {s}", .{ escape(src), escape(msg) });
self.last_time = ts;
self.last_tdiff = tdiff;
}
fn output_repeat(self: *Self, src: []const u8, msg: []const u8) !void {
_ = src;
self.last_count += 1;
try self.plane.cursor_move_rel(-1, 0);
try self.output_tdiff(self.last_tdiff);
_ = try self.plane.print("{s} ({})", .{ escape(msg), self.last_count });
}
fn output(self: *Self, src: []const u8, msg: []const u8) !void {
return if (eql(u8, msg, self.last_src) and eql(u8, msg, self.last_msg))
self.output_repeat(src, msg)
else
self.output_new(src, msg);
}
fn output_error(self: *Self, src: []const u8, context: []const u8, msg_: []const u8) !void {
var buf: [4096]u8 = undefined;
const msg = try fmt.bufPrint(&buf, "error in {s}: {s}", .{ context, msg_ });
try self.output(src, msg);
}
fn output_json(self: *Self, src: []const u8, m: tp.message) !void {
var buf: [4096]u8 = undefined;
const json = try m.to_json(&buf);
try self.output(src, json);
}

374
src/tui/mainview.zig Normal file
View file

@ -0,0 +1,374 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const location_history = @import("location_history");
const tui = @import("tui.zig");
const command = @import("command.zig");
const Box = @import("Box.zig");
const EventHandler = @import("EventHandler.zig");
const Widget = @import("Widget.zig");
const WidgetList = @import("WidgetList.zig");
const WidgetStack = @import("WidgetStack.zig");
const ed = @import("editor.zig");
const home = @import("home.zig");
const Self = @This();
const Commands = command.Collection(cmds);
a: std.mem.Allocator,
plane: nc.Plane,
widgets: *WidgetList,
floating_views: WidgetStack,
commands: Commands = undefined,
statusbar: *Widget,
editor: ?*ed.Editor = null,
panels: ?*WidgetList = null,
last_match_text: ?[]const u8 = null,
logview_enabled: bool = false,
location_history: location_history,
const NavState = struct {
time: i64 = 0,
lines: usize = 0,
rows: usize = 0,
row: usize = 0,
col: usize = 0,
matches: usize = 0,
};
pub fn create(a: std.mem.Allocator, n: nc.Plane) !Widget {
const self = try a.create(Self);
self.* = .{
.a = a,
.plane = n,
.widgets = undefined,
.floating_views = WidgetStack.init(a),
.statusbar = undefined,
.location_history = try location_history.create(),
};
try self.commands.init(self);
const w = Widget.to(self);
const widgets = try WidgetList.createV(a, w, @typeName(Self), .dynamic);
self.widgets = widgets;
try widgets.add(try Widget.empty(a, n, .dynamic));
self.statusbar = try widgets.addP(try @import("status/statusbar.zig").create(a, w));
self.resize();
return w;
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.close_all_panel_views();
self.commands.deinit();
self.widgets.deinit(a);
self.floating_views.deinit();
a.destroy(self);
}
pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{"write_restore_info"})) {
self.write_restore_info();
return true;
}
return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m);
}
pub fn update(self: *Self) void {
self.widgets.update();
self.floating_views.update();
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
var more = self.widgets.render(theme);
if (self.floating_views.render(theme))
more = true;
return more;
}
pub fn resize(self: *Self) void {
self.handle_resize(Box.from(self.plane));
}
pub fn handle_resize(self: *Self, pos: Box) void {
self.widgets.resize(pos);
self.floating_views.resize(pos);
}
pub fn box(self: *const Self) Box {
return Box.from(self.plane);
}
fn toggle_panel_view(self: *Self, view: anytype, enable_only: bool) error{Exit}!bool {
var enabled = true;
if (self.panels) |panels| {
if (panels.get(@typeName(view))) |w| {
if (!enable_only) {
panels.remove(w.*);
if (panels.empty()) {
self.widgets.remove(panels.widget());
self.panels = null;
}
enabled = false;
}
} else {
panels.add(view.create(self.a, self.widgets.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
}
} else {
const panels = WidgetList.createH(self.a, self.widgets.widget(), "panel", .{ .static = self.box().h / 5 }) catch |e| return tp.exit_error(e);
self.widgets.add(panels.widget()) catch |e| return tp.exit_error(e);
panels.add(view.create(self.a, self.widgets.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
self.panels = panels;
}
self.resize();
return enabled;
}
fn close_all_panel_views(self: *Self) void {
if (self.panels) |panels| {
self.widgets.remove(panels.widget());
self.panels = null;
}
self.resize();
}
fn toggle_view(self: *Self, view: anytype) tp.result {
if (self.widgets.get(@typeName(view))) |w| {
self.widgets.remove(w.*);
} else {
self.widgets.add(view.create(self.a, self.plane) catch |e| return tp.exit_error(e)) catch |e| return tp.exit_error(e);
}
self.resize();
}
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
pub fn quit(self: *Self, _: Ctx) tp.result {
if (self.editor) |editor| if (editor.is_dirty())
return tp.exit("unsaved changes");
try tp.self_pid().send("quit");
}
pub fn quit_without_saving(_: *Self, _: Ctx) tp.result {
try tp.self_pid().send("quit");
}
pub fn navigate(self: *Self, ctx: Ctx) tp.result {
const frame = tracy.initZone(@src(), .{ .name = "navigate" });
defer frame.deinit();
var file: ?[]const u8 = null;
var file_name: []const u8 = undefined;
var line: ?i64 = null;
var column: ?i64 = null;
var obj = std.json.ObjectMap.init(self.a);
defer obj.deinit();
if (ctx.args.match(tp.extract(&obj)) catch false) {
if (obj.get("line")) |v| switch (v) {
.integer => |line_| line = line_,
else => return tp.exit_error(error.InvalidArgument),
};
if (obj.get("column")) |v| switch (v) {
.integer => |column_| column = column_,
else => return tp.exit_error(error.InvalidArgument),
};
if (obj.get("file")) |v| switch (v) {
.string => |file_| file = file_,
else => return tp.exit_error(error.InvalidArgument),
};
} else if (ctx.args.match(tp.extract(&file_name)) catch false) {
file = file_name;
} else return tp.exit_error(error.InvalidArgument);
if (file) |f| {
try self.create_editor();
try command.executeName("open_file", command.fmt(.{f}));
if (line) |l| {
try command.executeName("goto_line", command.fmt(.{l}));
}
if (column) |col| {
try command.executeName("goto_column", command.fmt(.{col}));
}
try command.executeName("scroll_view_center", .{});
tui.need_render();
}
}
pub fn open_help(self: *Self, _: Ctx) tp.result {
try self.create_editor();
try command.executeName("open_scratch_buffer", command.fmt(.{ "help.md", @embedFile("help.md") }));
tui.need_render();
}
pub fn open_config(_: *Self, _: Ctx) tp.result {
const file_name = root.get_config_file_name() catch |e| return tp.exit_error(e);
try tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } });
}
pub fn restore_session(self: *Self, _: Ctx) tp.result {
try self.create_editor();
self.read_restore_info() catch |e| return tp.exit_error(e);
tui.need_render();
}
pub fn toggle_logview(self: *Self, _: Ctx) tp.result {
self.logview_enabled = try self.toggle_panel_view(@import("logview.zig"), false);
}
pub fn show_logview(self: *Self, _: Ctx) tp.result {
self.logview_enabled = try self.toggle_panel_view(@import("logview.zig"), true);
}
pub fn toggle_inputview(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inputview.zig"), false);
}
pub fn toggle_inspector_view(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inspector_view.zig"), false);
}
pub fn show_inspector_view(self: *Self, _: Ctx) tp.result {
_ = try self.toggle_panel_view(@import("inspector_view.zig"), true);
}
pub fn jump_back(self: *Self, _: Ctx) tp.result {
try self.location_history.back(location_jump);
}
pub fn jump_forward(self: *Self, _: Ctx) tp.result {
try self.location_history.forward(location_jump);
}
pub fn show_home(self: *Self, _: Ctx) tp.result {
return self.create_home();
}
};
pub fn handle_editor_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
const editor = if (self.editor) |editor_| editor_ else return;
var sel: ed.Selection = undefined;
if (try m.match(.{ "E", "location", tp.more }))
return self.location_update(m);
if (try m.match(.{ "E", "close" })) {
self.editor = null;
self.show_home_async();
return;
}
if (try m.match(.{ "E", "sel", tp.more })) {
if (try m.match(.{ tp.any, tp.any, "none" }))
return self.clear_auto_find(editor);
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) })) {
sel.normalize();
if (sel.end.row - sel.begin.row > ed.max_match_lines)
return self.clear_auto_find(editor);
const text = editor.get_selection(sel, self.a) catch return self.clear_auto_find(editor);
if (text.len == 0)
return self.clear_auto_find(editor);
if (!self.is_last_match_text(text)) {
editor.find_in_buffer(text) catch return;
}
}
return;
}
}
pub fn location_update(self: *Self, m: tp.message) tp.result {
var row: usize = 0;
var col: usize = 0;
if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col) }))
return self.location_history.add(.{ .row = row + 1, .col = col + 1 }, null);
var sel: location_history.Selection = .{};
if (try m.match(.{ tp.any, tp.any, tp.any, tp.extract(&row), tp.extract(&col), tp.extract(&sel.begin.row), tp.extract(&sel.begin.col), tp.extract(&sel.end.row), tp.extract(&sel.end.col) }))
return self.location_history.add(.{ .row = row + 1, .col = col + 1 }, sel);
}
fn location_jump(from: tp.pid_ref, cursor: location_history.Cursor, selection: ?location_history.Selection) void {
if (selection) |sel|
from.send(.{ "cmd", "goto", .{ cursor.row, cursor.col, sel.begin.row, sel.begin.col, sel.end.row, sel.end.col } }) catch return
else
from.send(.{ "cmd", "goto", .{ cursor.row, cursor.col } }) catch return;
}
fn clear_auto_find(self: *Self, editor: *ed.Editor) !void {
try editor.clear_matches();
self.store_last_match_text(null);
}
fn is_last_match_text(self: *Self, text: []const u8) bool {
const is = if (self.last_match_text) |old| std.mem.eql(u8, old, text) else false;
self.store_last_match_text(text);
return is;
}
fn store_last_match_text(self: *Self, text: ?[]const u8) void {
if (self.last_match_text) |old|
self.a.free(old);
self.last_match_text = text;
}
pub fn get_editor(self: *Self) ?*ed.Editor {
return self.editor;
}
pub fn walk(self: *Self, walk_ctx: *anyopaque, f: Widget.WalkFn) bool {
return if (self.floating_views.walk(walk_ctx, f)) true else self.widgets.walk(walk_ctx, f);
}
fn create_editor(self: *Self) tp.result {
command.executeName("enter_mode_default", .{}) catch {};
var editor_widget = ed.create(self.a, Widget.to(self)) catch |e| return tp.exit_error(e);
errdefer editor_widget.deinit(self.a);
if (editor_widget.get("editor")) |editor| {
editor.subscribe(EventHandler.to_unowned(self.statusbar)) catch unreachable;
editor.subscribe(EventHandler.bind(self, handle_editor_event)) catch unreachable;
self.editor = if (editor.dynamic_cast(ed.EditorWidget)) |p| &p.editor else null;
} else unreachable;
self.widgets.replace(0, editor_widget);
self.resize();
}
fn show_home_async(_: *Self) void {
tp.self_pid().send(.{ "cmd", "show_home" }) catch return;
}
fn create_home(self: *Self) tp.result {
if (self.editor) |_| return;
var home_widget = home.create(self.a, Widget.to(self)) catch |e| return tp.exit_error(e);
errdefer home_widget.deinit(self.a);
self.widgets.replace(0, home_widget);
self.resize();
}
fn write_restore_info(self: *Self) void {
if (self.editor) |editor| {
var sfa = std.heap.stackFallback(512, self.a);
const a = sfa.get();
var meta = std.ArrayList(u8).init(a);
editor.write_state(meta.writer()) catch return;
const file_name = root.get_restore_file_name() catch return;
var file = std.fs.createFileAbsolute(file_name, .{ .truncate = true }) catch return;
defer file.close();
file.writeAll(meta.items) catch return;
}
}
fn read_restore_info(self: *Self) !void {
if (self.editor) |editor| {
const file_name = try root.get_restore_file_name();
const file = try std.fs.cwd().openFile(file_name, .{ .mode = .read_only });
defer file.close();
const stat = try file.stat();
var buf = try self.a.alloc(u8, stat.size);
defer self.a.free(buf);
const size = try file.readAll(buf);
try editor.extract_state(buf[0..size]);
}
}

44
src/tui/message_box.zig Normal file
View file

@ -0,0 +1,44 @@
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const Widget = @import("Widget.zig");
const tui = @import("tui.zig");
pub const name = @typeName(Self);
const Self = @This();
plane: nc.Plane,
const y_pos = 10;
const y_pos_hidden = -15;
const x_pos = 10;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
pub fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts_vscroll(name), parent);
errdefer n.deinit();
return .{
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
_ = self;
_ = m;
return false;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.sidebar);
return false;
}

286
src/tui/mode/input/flow.zig Normal file
View file

@ -0,0 +1,286 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../tui.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ root.application_name,
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'O' => self.cmd("enter_open_file_mode", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'I' => self.insert_bytes("\t"),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
// 'B' => self.cmd("select_word_left", .{}),
// 'F' => self.cmd("select_word_right", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("cancel", .{}),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) tp.result {
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}

103
src/tui/mode/input/home.zig Normal file
View file

@ -0,0 +1,103 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../tui.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Self = @This();
a: std.mem.Allocator,
f: usize = 0,
pub fn create(a: std.mem.Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ root.application_name,
};
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.any, tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, modifiers);
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
nc.mod.CTRL => switch (keynormal) {
'F' => self.sheeran(),
'J' => self.cmd("toggle_logview", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("quit", .{}),
'O' => self.cmd("enter_open_file_mode", .{}),
'/' => self.cmd("open_help", .{}),
else => {},
},
nc.mod.CTRL | nc.mod.SHIFT => switch (keynormal) {
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("open_help", .{}),
else => {},
},
nc.mod.ALT => switch (keynormal) {
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
nc.key.LEFT => self.cmd("jump_back", .{}),
nc.key.RIGHT => self.cmd("jump_forward", .{}),
else => {},
},
0 => switch (keypress) {
nc.key.F01 => self.cmd("open_help", .{}),
nc.key.F06 => self.cmd("open_config", .{}),
nc.key.F09 => self.cmd("theme_prev", .{}),
nc.key.F10 => self.cmd("theme_next", .{}),
nc.key.F11 => self.cmd("toggle_logview", .{}),
nc.key.F12 => self.cmd("toggle_inputview", .{}),
else => {},
},
else => {},
};
}
fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result {
try command.executeName(name_, ctx);
}
fn cmd_async(_: *Self, name_: []const u8) tp.result {
return tp.self_pid().send(.{ "cmd", name_ });
}
fn sheeran(self: *Self) void {
self.f += 1;
if (self.f >= 5) {
self.f = 0;
self.cmd("home_sheeran", .{}) catch {};
}
}

View file

@ -0,0 +1,284 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "INSERT",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("enter_mode", command.fmt(.{"vim/normal"})),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'O' => self.cmd("run_ls", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'I' => self.insert_bytes("\t"),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("enter_mode", command.fmt(.{"vim/normal"})),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) tp.result {
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}

View file

@ -0,0 +1,497 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
count: usize = 0,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "NORMAL",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'R' => self.cmd("redo", .{}),
'O' => self.cmd("jump_back", .{}),
'I' => self.cmd("jump_forward", .{}),
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'D' => self.cmd("add_cursor_next_match", .{}),
'A' => self.cmd("select_all", .{}),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("move_buffer_end", .{}),
key.HOME => self.cmd("move_buffer_begin", .{}),
key.UP => self.cmd("move_scroll_up", .{}),
key.DOWN => self.cmd("move_scroll_down", .{}),
key.PGUP => self.cmd("move_scroll_page_up", .{}),
key.PGDOWN => self.cmd("move_scroll_page_down", .{}),
key.LEFT => self.cmd("move_word_left", .{}),
key.RIGHT => self.cmd("move_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("move_word_left", .{}),
'F' => self.cmd("move_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
// 'B' => self.cmd("select_word_left", .{}),
// 'F' => self.cmd("select_word_right", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
key.UP => self.cmd("add_cursor_up", .{}),
key.DOWN => self.cmd("add_cursor_down", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
'N' => self.cmd("goto_prev_match", .{}),
'A' => self.seq(.{ "move_end", "enter_mode" }, command.fmt(.{"vim/insert"})),
'4' => self.cmd("move_end", .{}),
'G' => if (self.count == 0)
self.cmd("move_buffer_end", .{})
else {
const count = self.count;
try self.cmd("move_buffer_begin", .{});
self.count = count - 1;
if (self.count > 0)
try self.cmd_count("move_down", .{});
},
'O' => self.seq(.{ "insert_line_before", "enter_mode" }, command.fmt(.{"vim/insert"})),
else => {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.cmd("cancel", .{}),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
'i' => self.cmd("enter_mode", command.fmt(.{"vim/insert"})),
'a' => self.seq(.{ "move_right", "enter_mode" }, command.fmt(.{"vim/insert"})),
'v' => self.cmd("enter_mode", command.fmt(.{"vim/visual"})),
'/' => self.cmd("enter_find_mode", .{}),
'n' => self.cmd("goto_next_match", .{}),
'h' => self.cmd_count("move_left", .{}),
'j' => self.cmd_count("move_down", .{}),
'k' => self.cmd_count("move_up", .{}),
'l' => self.cmd_count("move_right", .{}),
' ' => self.cmd_count("move_right", .{}),
'b' => self.cmd_count("move_word_left", .{}),
'w' => self.cmd_count("move_word_right_vim", .{}),
'e' => self.cmd_count("move_word_right", .{}),
'$' => self.cmd_count("move_end", .{}),
'0' => self.cmd_count("move_begin", .{}),
'1' => self.add_count(1),
'2' => self.add_count(2),
'3' => self.add_count(3),
'4' => self.add_count(4),
'5' => self.add_count(5),
'6' => self.add_count(6),
'7' => self.add_count(7),
'8' => self.add_count(8),
'9' => self.add_count(9),
'x' => self.cmd_count("delete_forward", .{}),
'u' => self.cmd("undo", .{}),
'd' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'r' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'c' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'z' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'g' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'y' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'p' => self.cmd("paste", .{}),
'o' => self.seq(.{ "insert_line_after", "enter_mode" }, command.fmt(.{"vim/insert"})),
key.LEFT => self.cmd("move_left", .{}),
key.RIGHT => self.cmd("move_right", .{}),
key.UP => self.cmd("move_up", .{}),
key.DOWN => self.cmd("move_down", .{}),
key.HOME => self.cmd("smart_move_begin", .{}),
key.END => self.cmd("move_end", .{}),
key.PGUP => self.cmd("move_page_up", .{}),
key.PGDOWN => self.cmd("move_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
if (keypress == key.LCTRL or
keypress == key.RCTRL or
keypress == key.LALT or
keypress == key.RALT or
keypress == key.LSHIFT or
keypress == key.RSHIFT or
keypress == key.LSUPER or
keypress == key.RSUPER) return;
switch (modifiers) {
0 => switch (keypress) {
'1' => {
self.add_count(1);
return;
},
'2' => {
self.add_count(2);
return;
},
'3' => {
self.add_count(3);
return;
},
'4' => {
self.add_count(4);
return;
},
'5' => {
self.add_count(5);
return;
},
'6' => {
self.add_count(6);
return;
},
'7' => {
self.add_count(7);
return;
},
'8' => {
self.add_count(8);
return;
},
'9' => {
self.add_count(9);
return;
},
else => {},
},
else => {},
}
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
0 => switch (ldr.keypress) {
'D', 'C' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.cmd("delete_to_end", .{}),
else => {},
},
0 => switch (keypress) {
'D' => self.seq_count(.{ "move_begin", "select_end", "select_right", "cut" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "cut" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "cut" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.cmd("delete_to_end", .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
'R' => switch (modifiers) {
mod.SHIFT, 0 => if (!key.synthesized_p(keypress)) {
var count = self.count;
try self.cmd_count("delete_forward", .{});
while (count > 0) : (count -= 1)
try self.insert_code_point(egc);
},
else => {},
},
'Z' => switch (modifiers) {
0 => switch (keypress) {
'Z' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
else => {},
},
else => {},
},
'G' => switch (modifiers) {
0 => switch (keypress) {
'G' => self.cmd("move_buffer_begin", .{}),
else => {},
},
else => {},
},
'Y' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.seq(.{ "select_to_end", "copy" }, .{}),
else => {},
},
0 => switch (keypress) {
'Y' => self.seq_count(.{ "move_begin", "select_end", "select_right", "copy" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "copy" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "copy" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.seq(.{ "select_to_end", "copy" }, .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn add_count(self: *Self, value: usize) void {
if (self.count > 0) self.count *= 10;
self.count += value;
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_count(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
while (count > 0) : (count -= 1)
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}
fn seq(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}
fn seq_count(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
while (count > 0) : (count -= 1)
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}

View file

@ -0,0 +1,472 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const root = @import("root");
const tui = @import("../../../tui.zig");
const command = @import("../../../command.zig");
const EventHandler = @import("../../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const ArrayList = @import("std").ArrayList;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
const input_buffer_size = 1024;
a: Allocator,
input: ArrayList(u8),
last_cmd: []const u8 = "",
leader: ?struct { keypress: u32, modifiers: u32 } = null,
count: usize = 0,
pub fn create(a: Allocator) !tui.Mode {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.input = try ArrayList(u8).initCapacity(a, input_buffer_size),
};
return .{
.handler = EventHandler.to_owned(self),
.name = root.application_logo ++ "VISUAL",
};
}
pub fn deinit(self: *Self) void {
self.input.deinit();
self.a.destroy(self);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
var text: []const u8 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
try self.flush_input();
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
try self.flush_input();
try self.insert_bytes(text);
try self.flush_input();
}
return false;
}
pub fn add_keybind() void {}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
return switch (evtype) {
nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => self.mapRelease(keypress, egc, modifiers),
else => {},
};
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
if (self.leader) |_| return self.mapFollower(keynormal, egc, modifiers);
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'R' => self.cmd("redo", .{}),
'O' => self.cmd("jump_back", .{}),
'I' => self.cmd("jump_forward", .{}),
'J' => self.cmd("toggle_logview", .{}),
'Z' => self.cmd("undo", .{}),
'Y' => self.cmd("redo", .{}),
'Q' => self.cmd("quit", .{}),
'W' => self.cmd("close_file", .{}),
'S' => self.cmd("save_file", .{}),
'L' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'B' => self.cmd("enter_move_to_char_mode", command.fmt(.{false})),
'T' => self.cmd("enter_move_to_char_mode", command.fmt(.{true})),
'X' => self.cmd("cut", .{}),
'C' => self.cmd("copy", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.cmd("pop_cursor", .{}),
'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'F' => self.cmd("enter_find_mode", .{}),
'G' => self.cmd("enter_goto_mode", .{}),
'A' => self.cmd("select_all", .{}),
'/' => self.cmd("toggle_comment", .{}),
key.ENTER => self.cmd("insert_line_after", .{}),
key.SPACE => self.cmd("selections_reverse", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.PGUP => self.cmd("select_scroll_page_up", .{}),
key.PGDOWN => self.cmd("select_scroll_page_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
key.BACKSPACE => self.cmd("delete_word_left", .{}),
key.DEL => self.cmd("delete_word_right", .{}),
else => {},
},
mod.CTRL | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_down", .{}),
'Z' => self.cmd("redo", .{}),
'Q' => self.cmd("quit_without_saving", .{}),
'R' => self.cmd("restart", .{}),
'F' => self.cmd("enter_find_in_files_mode", .{}),
'L' => self.cmd_async("toggle_logview"),
'I' => self.cmd_async("toggle_inputview"),
'/' => self.cmd("log_widgets", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.END => self.cmd("select_buffer_end", .{}),
key.HOME => self.cmd("select_buffer_begin", .{}),
key.UP => self.cmd("select_scroll_up", .{}),
key.DOWN => self.cmd("select_scroll_down", .{}),
key.LEFT => self.cmd("select_word_left", .{}),
key.RIGHT => self.cmd("select_word_right", .{}),
else => {},
},
mod.ALT => switch (keynormal) {
'J' => self.cmd("join_next_line", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'L' => self.cmd("toggle_logview", .{}),
'I' => self.cmd("toggle_inputview", .{}),
'B' => self.cmd("select_word_left", .{}),
'F' => self.cmd("select_word_right", .{}),
'S' => self.cmd("filter", command.fmt(.{"sort"})),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("jump_back", .{}),
key.RIGHT => self.cmd("jump_forward", .{}),
key.UP => self.cmd("pull_up", .{}),
key.DOWN => self.cmd("pull_down", .{}),
key.ENTER => self.cmd("insert_line", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'D' => self.cmd("dupe_up", .{}),
'F' => self.cmd("filter", command.fmt(.{ "zig", "fmt", "--stdin" })),
'S' => self.cmd("filter", command.fmt(.{ "sort", "-u" })),
'V' => self.cmd("paste", .{}),
key.LEFT => self.cmd("move_scroll_left", .{}),
key.RIGHT => self.cmd("move_scroll_right", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.F03 => self.cmd("goto_prev_match", .{}),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.ENTER => self.cmd("insert_line_before", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
key.TAB => self.cmd("unindent", .{}),
'N' => self.cmd("goto_prev_match", .{}),
'A' => self.seq(.{ "move_end", "enter_mode" }, command.fmt(.{"vim/insert"})),
'4' => self.cmd("select_end", .{}),
'G' => if (self.count == 0)
self.cmd("move_buffer_end", .{})
else {
const count = self.count;
try self.cmd("move_buffer_begin", .{});
self.count = count - 1;
if (self.count > 0)
try self.cmd_count("move_down", .{});
},
'O' => self.seq(.{ "insert_line_before", "enter_mode" }, command.fmt(.{"vim/insert"})),
else => {},
},
0 => switch (keypress) {
key.F02 => self.cmd("toggle_input_mode", .{}),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}), // S-F3
key.F05 => self.cmd("toggle_inspector_view", .{}), // C-F5
key.F06 => self.cmd("dump_current_line_tree", .{}),
key.F07 => self.cmd("dump_current_line", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.F11 => self.cmd("toggle_logview", .{}),
key.F12 => self.cmd("toggle_inputview", .{}),
key.F34 => self.cmd("toggle_whitespace", .{}), // C-F10
key.ESC => self.seq(.{ "cancel", "enter_mode" }, command.fmt(.{"vim/normal"})),
key.ENTER => self.cmd("smart_insert_line", .{}),
key.DEL => self.cmd("delete_forward", .{}),
key.BACKSPACE => self.cmd("delete_backward", .{}),
'i' => self.cmd("enter_mode", command.fmt(.{"vim/insert"})),
'a' => self.seq(.{ "move_right", "enter_mode" }, command.fmt(.{"vim/insert"})),
'v' => self.cmd("enter_mode", command.fmt(.{"vim/visual"})),
'/' => self.cmd("enter_find_mode", .{}),
'n' => self.cmd("goto_next_match", .{}),
'h' => self.cmd_count("select_left", .{}),
'j' => self.cmd_count("select_down", .{}),
'k' => self.cmd_count("select_up", .{}),
'l' => self.cmd_count("select_right", .{}),
' ' => self.cmd_count("select_right", .{}),
'b' => self.cmd_count("select_word_left", .{}),
'w' => self.cmd_count("select_word_right_vim", .{}),
'e' => self.cmd_count("select_word_right", .{}),
'$' => self.cmd_count("select_end", .{}),
'0' => self.cmd_count("select_begin", .{}),
'1' => self.add_count(1),
'2' => self.add_count(2),
'3' => self.add_count(3),
'4' => self.add_count(4),
'5' => self.add_count(5),
'6' => self.add_count(6),
'7' => self.add_count(7),
'8' => self.add_count(8),
'9' => self.add_count(9),
'u' => self.cmd("undo", .{}),
'd' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'r' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'c' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'z' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'g' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers },
'x' => self.cmd("cut", .{}),
'y' => self.cmd("copy", .{}),
'p' => self.cmd("paste", .{}),
'o' => self.seq(.{ "insert_line_after", "enter_mode" }, command.fmt(.{"vim/insert"})),
key.LEFT => self.cmd("select_left", .{}),
key.RIGHT => self.cmd("select_right", .{}),
key.UP => self.cmd("select_up", .{}),
key.DOWN => self.cmd("select_down", .{}),
key.HOME => self.cmd("smart_select_begin", .{}),
key.END => self.cmd("select_end", .{}),
key.PGUP => self.cmd("select_page_up", .{}),
key.PGDOWN => self.cmd("select_page_down", .{}),
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
key.TAB => self.cmd("indent", .{}),
else => {},
},
else => {},
};
}
fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
if (keypress == key.LCTRL or
keypress == key.RCTRL or
keypress == key.LALT or
keypress == key.RALT or
keypress == key.LSHIFT or
keypress == key.RSHIFT or
keypress == key.LSUPER or
keypress == key.RSUPER) return;
switch (modifiers) {
0 => switch (keypress) {
'1' => {
self.add_count(1);
return;
},
'2' => {
self.add_count(2);
return;
},
'3' => {
self.add_count(3);
return;
},
'4' => {
self.add_count(4);
return;
},
'5' => {
self.add_count(5);
return;
},
'6' => {
self.add_count(6);
return;
},
'7' => {
self.add_count(7);
return;
},
'8' => {
self.add_count(8);
return;
},
'9' => {
self.add_count(9);
return;
},
else => {},
},
else => {},
}
defer self.leader = null;
const ldr = if (self.leader) |leader| leader else return;
return switch (ldr.modifiers) {
mod.CTRL => switch (ldr.keypress) {
'K' => switch (modifiers) {
mod.CTRL => switch (keypress) {
'U' => self.cmd("delete_to_begin", .{}),
'K' => self.cmd("delete_to_end", .{}),
'D' => self.cmd("move_cursor_next_match", .{}),
else => {},
},
else => {},
},
else => {},
},
0 => switch (ldr.keypress) {
'D', 'C' => {
try switch (modifiers) {
mod.SHIFT => switch (keypress) {
'4' => self.cmd("delete_to_end", .{}),
else => {},
},
0 => switch (keypress) {
'D' => self.seq_count(.{ "move_begin", "select_end", "select_right", "cut" }, .{}),
'W' => self.seq_count(.{ "select_word_right", "select_word_right", "select_word_left", "cut" }, .{}),
'E' => self.seq_count(.{ "select_word_right", "cut" }, .{}),
else => {},
},
else => switch (egc) {
'$' => self.cmd("delete_to_end", .{}),
else => {},
},
};
if (ldr.keypress == 'C')
try self.cmd("enter_mode", command.fmt(.{"vim/insert"}));
},
'R' => switch (modifiers) {
mod.SHIFT, 0 => if (!key.synthesized_p(keypress)) {
var count = self.count;
try self.cmd_count("delete_forward", .{});
while (count > 0) : (count -= 1)
try self.insert_code_point(egc);
},
else => {},
},
'Z' => switch (modifiers) {
0 => switch (keypress) {
'Z' => self.cmd_cycle3("scroll_view_center", "scroll_view_top", "scroll_view_bottom", .{}),
else => {},
},
else => {},
},
'G' => switch (modifiers) {
0 => switch (keypress) {
'G' => self.cmd("move_buffer_begin", .{}),
else => {},
},
else => {},
},
else => {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn add_count(self: *Self, value: usize) void {
if (self.count > 0) self.count *= 10;
self.count += value;
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.input.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.items.len + 4 > input_buffer_size)
try self.flush_input();
self.input.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
var insert_chars_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.items.len > 0) {
defer self.input.clearRetainingCapacity();
const id = insert_chars_id orelse command.get_id_cache("insert_chars", &insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound);
};
try command.execute(id, command.fmt(.{self.input.items}));
self.last_cmd = "insert_chars";
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
try command.executeName(name_, ctx);
}
fn cmd_count(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
try self.flush_input();
self.last_cmd = name_;
while (count > 0) : (count -= 1)
try command.executeName(name_, ctx);
}
fn cmd_cycle3(self: *Self, name1: []const u8, name2: []const u8, name3: []const u8, ctx: command.Context) tp.result {
return if (eql(u8, self.last_cmd, name2))
self.cmd(name3, ctx)
else if (eql(u8, self.last_cmd, name1))
self.cmd(name2, ctx)
else
self.cmd(name1, ctx);
}
fn cmd_async(self: *Self, name_: []const u8) tp.result {
self.last_cmd = name_;
return tp.self_pid().send(.{ "cmd", name_ });
}
fn seq(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}
fn seq_count(self: *Self, cmds: anytype, ctx: command.Context) tp.result {
var count = if (self.count == 0) 1 else self.count;
self.count = 0;
const cmds_type_info = @typeInfo(@TypeOf(cmds));
if (cmds_type_info != .Struct) @compileError("expected tuple argument");
const fields_info = cmds_type_info.Struct.fields;
while (count > 0) : (count -= 1)
inline for (fields_info) |field_info|
try self.cmd(@field(cmds, field_info.name), ctx);
}

231
src/tui/mode/mini/find.zig Normal file
View file

@ -0,0 +1,231 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const ed = @import("../../editor.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [1024]u8 = undefined,
input: []u8 = "",
last_buf: [1024]u8 = undefined,
last_input: []u8 = "",
start_view: ed.View,
start_cursor: ed.Cursor,
editor: *ed.Editor,
history_pos: ?usize = null,
pub fn create(a: Allocator, _: command.Context) !*Self {
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.start_view = editor.view,
.start_cursor = editor.get_primary().cursor,
.editor = editor,
};
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
@memcpy(self.buf[0..text.len], text);
self.input = self.buf[0..text.len];
}
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "find";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.input;
mini_mode.cursor = self.input.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
self.flush_input() catch |e| return e;
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.input = "",
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'F' => self.cmd("goto_next_match", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'I' => self.insert_bytes("\t"),
key.SPACE => self.cancel(),
key.ENTER => self.insert_bytes("\n"),
key.BACKSPACE => self.input = "",
else => {},
},
mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.ENTER => self.cmd("goto_prev_match", .{}),
key.F03 => self.cmd("goto_prev_match", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.UP => self.find_history_prev(),
key.DOWN => self.find_history_next(),
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.ESC => self.cancel(),
key.ENTER => self.confirm(),
key.BACKSPACE => if (self.input.len > 0) {
self.input = self.input[0 .. self.input.len - 1];
},
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, self.buf[self.input.len..]) catch |e| return tp.exit_error(e);
self.input = self.buf[0 .. self.input.len + bytes];
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const newlen = self.input.len + bytes.len;
@memcpy(self.buf[self.input.len..newlen], bytes);
self.input = self.buf[0..newlen];
}
var find_cmd_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.len > 0) {
if (eql(u8, self.input, self.last_input))
return;
@memcpy(self.last_buf[0..self.input.len], self.input);
self.last_input = self.last_buf[0..self.input.len];
self.editor.find_operation = .goto_next_match;
self.editor.get_primary().cursor = self.start_cursor;
try self.editor.find_in_buffer(self.input);
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.flush_input() catch {};
return command.executeName(name_, ctx);
}
fn confirm(self: *Self) void {
self.editor.push_find_history(self.input);
self.cmd("exit_mini_mode", .{}) catch {};
}
fn cancel(self: *Self) void {
self.editor.get_primary().cursor = self.start_cursor;
self.editor.scroll_to(self.start_view.row);
command.executeName("exit_mini_mode", .{}) catch {};
}
fn find_history_prev(self: *Self) void {
if (self.editor.find_history) |*history| {
if (self.history_pos) |pos| {
if (pos > 0) self.history_pos = pos - 1;
} else {
self.history_pos = history.items.len - 1;
if (self.input.len > 0)
self.editor.push_find_history(self.editor.a.dupe(u8, self.input) catch return);
if (eql(u8, history.items[self.history_pos.?], self.input) and self.history_pos.? > 0)
self.history_pos = self.history_pos.? - 1;
}
self.load_history(self.history_pos.?);
}
}
fn find_history_next(self: *Self) void {
if (self.editor.find_history) |*history| if (self.history_pos) |pos| {
if (pos < history.items.len - 1) {
self.history_pos = pos + 1;
self.load_history(self.history_pos.?);
}
};
}
fn load_history(self: *Self, pos: usize) void {
if (self.editor.find_history) |*history| {
const new = history.items[pos];
@memcpy(self.buf[0..new.len], new);
self.input = self.buf[0..new.len];
}
}

View file

@ -0,0 +1,190 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const ed = @import("../../editor.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [1024]u8 = undefined,
input: []u8 = "",
last_buf: [1024]u8 = undefined,
last_input: []u8 = "",
start_view: ed.View,
start_cursor: ed.Cursor,
editor: *ed.Editor,
pub fn create(a: Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
self.* = .{
.a = a,
.start_view = editor.view,
.start_cursor = editor.get_primary().cursor,
.editor = editor,
};
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
@memcpy(self.buf[0..text.len], text);
self.input = self.buf[0..text.len];
}
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "find in files";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.input;
mini_mode.cursor = self.input.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
} else if (try m.match(.{"F"})) {
self.flush_input() catch |e| return e;
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.input = "",
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'F' => self.cmd("goto_next_match", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
'I' => self.insert_bytes("\t"),
key.SPACE => self.cancel(),
key.ENTER => self.insert_bytes("\n"),
key.BACKSPACE => self.input = "",
else => {},
},
mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
'N' => self.cmd("goto_next_match", .{}),
'P' => self.cmd("goto_prev_match", .{}),
else => {},
},
mod.ALT | mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
mod.SHIFT => switch (keypress) {
key.ENTER => self.cmd("goto_prev_match", .{}),
key.F03 => self.cmd("goto_prev_match", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
key.F03 => self.cmd("goto_next_match", .{}),
key.F15 => self.cmd("goto_prev_match", .{}),
key.F09 => self.cmd("theme_prev", .{}),
key.F10 => self.cmd("theme_next", .{}),
key.ESC => self.cancel(),
key.ENTER => self.cmd("exit_mini_mode", .{}),
key.BACKSPACE => if (self.input.len > 0) {
self.input = self.input[0 .. self.input.len - 1];
},
key.LCTRL, key.RCTRL => self.cmd("enable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("enable_fast_scroll", .{}),
else => if (!key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(self: *Self, keypress: u32, _: u32, _: u32) tp.result {
return switch (keypress) {
key.LCTRL, key.RCTRL => self.cmd("disable_fast_scroll", .{}),
key.LALT, key.RALT => self.cmd("disable_fast_scroll", .{}),
else => {},
};
}
fn insert_code_point(self: *Self, c: u32) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, self.buf[self.input.len..]) catch |e| return tp.exit_error(e);
self.input = self.buf[0 .. self.input.len + bytes];
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
if (self.input.len + 16 > self.buf.len)
try self.flush_input();
const newlen = self.input.len + bytes.len;
@memcpy(self.buf[self.input.len..newlen], bytes);
self.input = self.buf[0..newlen];
}
var find_cmd_id: ?command.ID = null;
fn flush_input(self: *Self) tp.result {
if (self.input.len > 0) {
if (eql(u8, self.input, self.last_input))
return;
@memcpy(self.last_buf[0..self.input.len], self.input);
self.last_input = self.last_buf[0..self.input.len];
command.executeName("show_logview", .{}) catch {};
try self.editor.find_in_files(self.input);
}
}
fn cmd(self: *Self, name_: []const u8, ctx: command.Context) tp.result {
self.flush_input() catch {};
return command.executeName(name_, ctx);
}
fn cancel(self: *Self) void {
self.editor.get_primary().cursor = self.start_cursor;
self.editor.scroll_to(self.start_view.row);
command.executeName("exit_mini_mode", .{}) catch {};
}

116
src/tui/mode/mini/goto.zig Normal file
View file

@ -0,0 +1,116 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
buf: [30]u8 = undefined,
input: ?usize = null,
start: usize,
pub fn create(a: Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
self.* = .{
.a = a,
.start = editor.get_primary().cursor.row,
};
return self;
};
return error.NotFound;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "goto";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = if (self.input) |linenum|
(fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "")
else
"";
mini_mode.cursor = mini_mode.text.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.any, tp.string, tp.extract(&modifiers) }))
try self.mapEvent(evtype, keypress, modifiers);
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
mod.CTRL => switch (keynormal) {
'Q' => command.executeName("quit", .{}),
'U' => self.input = null,
'G' => self.cancel(),
'C' => self.cancel(),
'L' => command.executeName("scroll_view_center", .{}),
key.SPACE => self.cancel(),
else => {},
},
0 => switch (keypress) {
key.ESC => self.cancel(),
key.ENTER => command.executeName("exit_mini_mode", .{}),
key.BACKSPACE => if (self.input) |linenum| {
const newval = if (linenum < 10) 0 else linenum / 10;
self.input = if (newval == 0) null else newval;
self.goto();
},
'0' => {
if (self.input) |linenum| self.input = linenum * 10;
self.goto();
},
'1'...'9' => {
const digit: usize = @intCast(keypress - '0');
self.input = if (self.input) |x| x * 10 + digit else digit;
self.goto();
},
else => {},
},
else => {},
};
}
fn goto(self: *Self) void {
command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {};
}
fn cancel(self: *Self) void {
self.input = null;
self.goto();
command.executeName("exit_mini_mode", .{}) catch {};
}

View file

@ -0,0 +1,122 @@
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Allocator = @import("std").mem.Allocator;
const json = @import("std").json;
const eql = @import("std").mem.eql;
const fmt = @import("std").fmt;
const mod = nc.mod;
const key = nc.key;
const Self = @This();
a: Allocator,
key: [6]u8 = undefined,
direction: Direction,
operation: Operation,
const Direction = enum {
left,
right,
};
const Operation = enum {
move,
select,
};
pub fn create(a: Allocator, ctx: command.Context) !*Self {
var right: bool = true;
const select = if (tui.current().mainview.dynamic_cast(mainview)) |mv| if (mv.get_editor()) |editor| if (editor.get_primary().selection) |_| true else false else false else false;
_ = ctx.args.match(.{tp.extract(&right)}) catch return error.NotFound;
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.direction = if (right) .right else .left,
.operation = if (select) .select else .move,
};
return self;
}
pub fn deinit(self: *Self) void {
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(self: *Self) []const u8 {
return switch (self.operation) {
.move => switch (self.direction) {
.left => "move left to char",
.right => "move right to char",
},
.select => switch (self.direction) {
.left => "select left to char",
.right => "select right to char",
},
};
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var modifiers: u32 = undefined;
var egc: u32 = undefined;
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) }))
try self.mapEvent(evtype, keypress, egc, modifiers);
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (keypress) {
key.LSUPER, key.RSUPER => return,
key.LSHIFT, key.RSHIFT => return,
key.LCTRL, key.RCTRL => return,
key.LALT, key.RALT => return,
else => {},
}
return switch (modifiers) {
mod.SHIFT => if (!key.synthesized_p(keypress)) self.execute_operation(egc) else self.cancel(),
0 => switch (keypress) {
key.ESC => self.cancel(),
key.ENTER => self.cancel(),
else => if (!key.synthesized_p(keypress)) self.execute_operation(egc) else self.cancel(),
},
else => self.cancel(),
};
}
fn execute_operation(self: *Self, c: u32) void {
const cmd = switch (self.direction) {
.left => switch (self.operation) {
.move => "move_to_char_left",
.select => "select_to_char_left",
},
.right => switch (self.operation) {
.move => "move_to_char_right",
.select => "select_to_char_right",
},
};
var buf: [6]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch return;
command.executeName(cmd, command.fmt(.{buf[0..bytes]})) catch {};
command.executeName("exit_mini_mode", .{}) catch {};
}
fn cancel(_: *Self) void {
command.executeName("exit_mini_mode", .{}) catch {};
}

View file

@ -0,0 +1,144 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const tui = @import("../../tui.zig");
const mainview = @import("../../mainview.zig");
const command = @import("../../command.zig");
const EventHandler = @import("../../EventHandler.zig");
const Self = @This();
a: std.mem.Allocator,
file_path: std.ArrayList(u8),
pub fn create(a: std.mem.Allocator, _: command.Context) !*Self {
const self: *Self = try a.create(Self);
self.* = .{
.a = a,
.file_path = std.ArrayList(u8).init(a),
};
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| {
if (editor.is_dirty()) return tp.exit("unsaved changes");
if (editor.file_path) |old_path|
if (std.mem.lastIndexOf(u8, old_path, "/")) |pos|
try self.file_path.appendSlice(old_path[0 .. pos + 1]);
if (editor.get_primary().selection) |sel| ret: {
const text = editor.get_selection(sel, self.a) catch break :ret;
defer self.a.free(text);
if (!(text.len > 2 and std.mem.eql(u8, text[0..2], "..")))
self.file_path.clearRetainingCapacity();
try self.file_path.appendSlice(text);
}
};
return self;
}
pub fn deinit(self: *Self) void {
self.file_path.deinit();
self.a.destroy(self);
}
pub fn handler(self: *Self) EventHandler {
return EventHandler.to_owned(self);
}
pub fn name(_: *Self) []const u8 {
return "open file";
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var evtype: u32 = undefined;
var keypress: u32 = undefined;
var egc: u32 = undefined;
var modifiers: u32 = undefined;
defer {
if (tui.current().mini_mode) |*mini_mode| {
mini_mode.text = self.file_path.items;
mini_mode.cursor = self.file_path.items.len;
}
}
if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) {
try self.mapEvent(evtype, keypress, egc, modifiers);
}
return false;
}
fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result {
switch (evtype) {
nc.event_type.PRESS => try self.mapPress(keypress, egc, modifiers),
nc.event_type.REPEAT => try self.mapPress(keypress, egc, modifiers),
nc.event_type.RELEASE => try self.mapRelease(keypress, egc, modifiers),
else => {},
}
}
fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result {
const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress;
return switch (modifiers) {
nc.mod.CTRL => switch (keynormal) {
'Q' => self.cmd("quit", .{}),
'V' => self.cmd("system_paste", .{}),
'U' => self.file_path.clearRetainingCapacity(),
'G' => self.cancel(),
'C' => self.cancel(),
'L' => self.cmd("scroll_view_center", .{}),
'I' => self.insert_bytes("\t"),
nc.key.SPACE => self.cancel(),
nc.key.BACKSPACE => self.file_path.clearRetainingCapacity(),
else => {},
},
nc.mod.ALT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
nc.mod.ALT | nc.mod.SHIFT => switch (keynormal) {
'V' => self.cmd("system_paste", .{}),
else => {},
},
nc.mod.SHIFT => switch (keypress) {
else => if (!nc.key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
0 => switch (keypress) {
nc.key.ESC => self.cancel(),
nc.key.ENTER => self.navigate(),
nc.key.BACKSPACE => if (self.file_path.items.len > 0) {
self.file_path.shrinkRetainingCapacity(self.file_path.items.len - 1);
},
else => if (!nc.key.synthesized_p(keypress))
self.insert_code_point(egc)
else {},
},
else => {},
};
}
fn mapRelease(_: *Self, _: u32, _: u32, _: u32) tp.result {}
fn insert_code_point(self: *Self, c: u32) tp.result {
var buf: [32]u8 = undefined;
const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e);
self.file_path.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e);
}
fn insert_bytes(self: *Self, bytes: []const u8) tp.result {
self.file_path.appendSlice(bytes) catch |e| return tp.exit_error(e);
}
fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result {
return command.executeName(name_, ctx);
}
fn cancel(_: *Self) void {
command.executeName("exit_mini_mode", .{}) catch {};
}
fn navigate(self: *Self) void {
if (self.file_path.items.len > 0)
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = self.file_path.items } }) catch {};
command.executeName("exit_mini_mode", .{}) catch {};
}

171
src/tui/scrollbar_v.zig Normal file
View file

@ -0,0 +1,171 @@
const Allocator = @import("std").mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("Widget.zig");
const EventHandler = @import("EventHandler.zig");
const tui = @import("tui.zig");
plane: nc.Plane,
pos_scrn: u32 = 0,
view_scrn: u32 = 8,
size_scrn: u32 = 8,
pos_virt: u32 = 0,
view_virt: u32 = 1,
size_virt: u32 = 1,
max_ypx: i32 = 8,
parent: Widget,
hover: bool = false,
active: bool = false,
const Self = @This();
pub fn create(a: Allocator, parent: Widget, event_source: Widget) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try event_source.subscribe(EventHandler.bind(self, handle_event));
return self.widget();
}
fn init(parent: Widget) !Self {
return .{
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent.plane.*),
.parent = parent,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = 1 };
}
pub fn handle_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var size: u32 = 0;
var view: u32 = 0;
var pos: u32 = 0;
if (try m.match(.{ "E", "view", tp.extract(&size), tp.extract(&view), tp.extract(&pos) }))
self.set(size, view, pos);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var y: i32 = undefined;
var ypx: i32 = undefined;
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true;
self.move_to(y, ypx);
return true;
} else if (try m.match(.{ "B", nc.event_type.RELEASE, tp.more })) {
self.active = false;
return true;
} else if (try m.match(.{ "D", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true;
self.move_to(y, ypx);
return true;
} else if (try m.match(.{ "B", nc.event_type.RELEASE, tp.more })) {
self.active = false;
return true;
} else if (try m.match(.{ "H", tp.extract(&self.hover) })) {
self.active = false;
return true;
}
return false;
}
fn move_to(self: *Self, y_: i32, ypx_: i32) void {
self.max_ypx = @max(self.max_ypx, ypx_);
const max_ypx: f64 = @floatFromInt(self.max_ypx);
const y: f64 = @floatFromInt(y_);
const ypx: f64 = @floatFromInt(ypx_);
const plane_y: f64 = @floatFromInt(self.plane.abs_y());
const size_scrn: f64 = @floatFromInt(self.size_scrn);
const view_scrn: f64 = @floatFromInt(self.view_scrn);
const ratio = max_ypx / eighths_c;
const pos_scrn: f64 = ((y - plane_y) * eighths_c) + (ypx / ratio) - (view_scrn / 2);
const max_pos_scrn = size_scrn - view_scrn;
const pos_scrn_clamped = @min(@max(0, pos_scrn), max_pos_scrn);
const pos_virt = self.pos_scrn_to_virt(@intFromFloat(pos_scrn_clamped));
self.set(self.size_virt, self.view_virt, pos_virt);
_ = self.parent.msg(.{ "scroll_to", pos_virt }) catch {};
}
fn pos_scrn_to_virt(self: Self, pos_scrn_: u32) u32 {
const size_virt: f64 = @floatFromInt(self.size_virt);
const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c);
const pos_scrn: f64 = @floatFromInt(pos_scrn_);
const ratio = size_virt / size_scrn;
return @intFromFloat(pos_scrn * ratio);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = "scrollbar_v render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.active) theme.scrollbar_active else if (self.hover) theme.scrollbar_hover else theme.scrollbar);
self.plane.erase();
smooth_bar_at(self.plane, @intCast(self.pos_scrn), @intCast(self.view_scrn)) catch {};
return false;
}
pub fn set(self: *Self, size_virt_: u32, view_virt_: u32, pos_virt_: u32) void {
self.pos_virt = pos_virt_;
self.view_virt = view_virt_;
self.size_virt = size_virt_;
var size_virt: f64 = @floatFromInt(size_virt_);
var view_virt: f64 = @floatFromInt(view_virt_);
const pos_virt: f64 = @floatFromInt(pos_virt_);
const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c);
if (size_virt == 0) size_virt = 1;
if (view_virt_ == 0) view_virt = 1;
if (view_virt > size_virt) view_virt = size_virt;
const ratio = size_virt / size_scrn;
self.pos_scrn = @intFromFloat(pos_virt / ratio);
self.view_scrn = @intFromFloat(view_virt / ratio);
self.size_scrn = @intFromFloat(size_scrn);
}
const eighths_b = [_][]const u8{ "", "", "", "", "", "", "", "" };
const eighths_t = [_][]const u8{ " ", "", "🮂", "🮃", "", "🮄", "🮅", "🮆" };
const eighths_c: i32 = @intCast(eighths_b.len);
fn smooth_bar_at(plane: nc.Plane, pos_: i32, size_: i32) !void {
const height: i32 = @intCast(plane.dim_y());
var size = @max(size_, 8);
const pos: i32 = @min(height * eighths_c - size, pos_);
var pos_y = @as(c_int, @intCast(@divFloor(pos, eighths_c)));
const blk = @mod(pos, eighths_c);
const b = eighths_b[@intCast(blk)];
plane.erase();
plane.cursor_move_yx(pos_y, 0) catch return;
_ = try plane.putstr(@ptrCast(b));
size -= @as(u16, @intCast(eighths_c)) - @as(u16, @intCast(blk));
while (size >= 8) {
pos_y += 1;
size -= 8;
plane.cursor_move_yx(pos_y, 0) catch return;
_ = try plane.putstr(@ptrCast(eighths_b[0]));
}
if (size > 0) {
pos_y += 1;
plane.cursor_move_yx(pos_y, 0) catch return;
const t = eighths_t[size];
_ = try plane.putstr(@ptrCast(t));
}
}

View file

@ -0,0 +1,218 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
a: Allocator,
parent: nc.Plane,
plane: nc.Plane,
name: []const u8,
name_buf: [512]u8 = undefined,
title: []const u8 = "",
title_buf: [512]u8 = undefined,
file_type: []const u8,
file_type_buf: [64]u8 = undefined,
file_icon: [:0]const u8 = "",
file_icon_buf: [6]u8 = undefined,
file_color: u24 = 0,
line: usize,
lines: usize,
column: usize,
file_exists: bool,
file_dirty: bool = false,
detailed: bool = false,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(a, parent);
self.show_cwd();
return Widget.to(self);
}
fn init(a: Allocator, parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.a = a,
.parent = parent,
.plane = n,
.name = "",
.file_type = "",
.lines = 0,
.line = 0,
.column = 0,
.file_exists = true,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
if (tui.current().mini_mode) |_|
self.render_mini_mode(theme)
else if (self.detailed)
self.render_detailed(theme)
else
self.render_normal(theme);
self.render_terminal_title();
return false;
}
fn render_mini_mode(self: *Self, theme: *const Widget.Theme) void {
self.plane.off_styles(nc.style.italic);
const mini_mode = if (tui.current().mini_mode) |m| m else return;
_ = self.plane.print(" {s}", .{mini_mode.text}) catch {};
if (mini_mode.cursor) |cursor| {
const pos: c_int = @intCast(cursor);
self.plane.cursor_move_yx(0, pos + 1) catch return;
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style(&cell, theme.editor_cursor);
_ = self.plane.putc(&cell) catch {};
}
return;
}
// 󰆓 Content save
// 󰽂 Content save alert
// 󰳻 Content save edit
// 󰘛 Content save settings
// 󱙃 Content save off
// 󱣪 Content save check
// 󱑛 Content save cog
// 󰆔 Content save all
fn render_normal(self: *Self, theme: *const Widget.Theme) void {
self.plane.on_styles(nc.style.italic);
_ = self.plane.putstr(" ") catch {};
if (self.file_icon.len > 0) {
self.render_file_icon(theme);
_ = self.plane.print(" ", .{}) catch {};
}
_ = self.plane.putstr(if (!self.file_exists) "󰽂 " else if (self.file_dirty) "󰆓 " else "") catch {};
_ = self.plane.print("{s}", .{self.name}) catch {};
return;
}
fn render_detailed(self: *Self, theme: *const Widget.Theme) void {
self.plane.on_styles(nc.style.italic);
_ = self.plane.putstr(" ") catch {};
if (self.file_icon.len > 0) {
self.render_file_icon(theme);
_ = self.plane.print(" ", .{}) catch {};
}
_ = self.plane.putstr(if (!self.file_exists) "󰽂" else if (self.file_dirty) "󰆓" else "󱣪") catch {};
_ = self.plane.print(" {s}:{d}:{d}", .{ self.name, self.line + 1, self.column + 1 }) catch {};
_ = self.plane.print(" of {d} lines", .{self.lines}) catch {};
if (self.file_type.len > 0)
_ = self.plane.print(" ({s})", .{self.file_type}) catch {};
return;
}
fn render_terminal_title(self: *Self) void {
const file_name = if (std.mem.lastIndexOfScalar(u8, self.name, '/')) |pos|
self.name[pos + 1 ..]
else if (self.name.len == 0)
root.application_name
else
self.name;
var new_title_buf: [512]u8 = undefined;
const new_title = std.fmt.bufPrint(&new_title_buf, "{s}{s}", .{ if (!self.file_exists) "" else if (self.file_dirty) "" else "", file_name }) catch return;
if (std.mem.eql(u8, self.title, new_title)) return;
@memcpy(self.title_buf[0..new_title.len], new_title);
self.title = self.title_buf[0..new_title.len];
tui.set_terminal_title(self.title);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var file_path: []const u8 = undefined;
var file_type: []const u8 = undefined;
var file_icon: []const u8 = undefined;
var file_dirty: bool = undefined;
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) }))
return false;
if (try m.match(.{ "E", "dirty", tp.extract(&file_dirty) })) {
self.file_dirty = file_dirty;
} else if (try m.match(.{ "E", "save", tp.extract(&file_path) })) {
@memcpy(self.name_buf[0..file_path.len], file_path);
self.name = self.name_buf[0..file_path.len];
self.file_exists = true;
self.file_dirty = false;
self.abbrv_home();
} else if (try m.match(.{ "E", "open", tp.extract(&file_path), tp.extract(&self.file_exists), tp.extract(&file_type), tp.extract(&file_icon), tp.extract(&self.file_color) })) {
@memcpy(self.name_buf[0..file_path.len], file_path);
self.name = self.name_buf[0..file_path.len];
@memcpy(self.file_type_buf[0..file_type.len], file_type);
self.file_type = self.file_type_buf[0..file_type.len];
@memcpy(self.file_icon_buf[0..file_icon.len], file_icon);
self.file_icon_buf[file_icon.len] = 0;
self.file_icon = self.file_icon_buf[0..file_icon.len :0];
self.file_dirty = false;
self.abbrv_home();
} else if (try m.match(.{ "E", "close" })) {
self.name = "";
self.lines = 0;
self.line = 0;
self.column = 0;
self.file_exists = true;
self.show_cwd();
}
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
self.detailed = !self.detailed;
return true;
}
return false;
}
fn render_file_icon(self: *Self, _: *const Widget.Theme) void {
var cell = self.plane.cell_init();
_ = self.plane.at_cursor_cell(&cell) catch return;
if (self.file_color != 0x000001) {
nc.channels_set_fg_rgb(&cell.channels, self.file_color) catch {};
nc.channels_set_fg_alpha(&cell.channels, nc.ALPHA_OPAQUE) catch {};
}
_ = self.plane.cell_load(&cell, self.file_icon) catch {};
_ = self.plane.putc(&cell) catch {};
self.plane.cursor_move_rel(0, 1) catch {};
}
fn show_cwd(self: *Self) void {
self.file_icon = "";
self.file_color = 0x000001;
self.name = std.fs.cwd().realpath(".", &self.name_buf) catch "(none)";
self.abbrv_home();
}
fn abbrv_home(self: *Self) void {
if (std.fs.path.isAbsolute(self.name)) {
if (std.os.getenv("HOME")) |homedir| {
const homerelpath = std.fs.path.relative(self.a, homedir, self.name) catch return;
if (homerelpath.len == 0) {
self.name = "~";
} else if (homerelpath.len > 3 and std.mem.eql(u8, homerelpath[0..3], "../")) {
return;
} else {
self.name_buf[0] = '~';
self.name_buf[1] = '/';
@memcpy(self.name_buf[2 .. homerelpath.len + 2], homerelpath);
self.name = self.name_buf[0 .. homerelpath.len + 2];
}
}
}
}

211
src/tui/status/keystate.zig Normal file
View file

@ -0,0 +1,211 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
const EventHandler = @import("../EventHandler.zig");
const history = 8;
parent: nc.Plane,
plane: nc.Plane,
frame: u64 = 0,
idle_frame: u64 = 0,
key_active_frame: u64 = 0,
wipe_after_frames: i64 = 60,
hover: bool = false,
keys: [history]Key = [_]Key{.{}} ** history,
const Key = struct { id: u32 = 0, mod: u32 = 0 };
const Self = @This();
const idle_msg = "🐶";
pub const width = idle_msg.len + 20;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return self.widget();
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
var frame_rate = tp.env.get().num("frame-rate");
if (frame_rate == 0) frame_rate = 60;
return .{
.parent = parent,
.plane = n,
.wipe_after_frames = @divTrunc(frame_rate, 2),
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = width };
}
fn render_active(self: *Self) bool {
var c: usize = 0;
for (self.keys) |k| {
if (k.id == 0)
return true;
if (c > 0)
_ = self.plane.putstr(" ") catch {};
if (nc.isSuper(k.mod))
_ = self.plane.putstr("H-") catch {};
if (nc.isCtrl(k.mod))
_ = self.plane.putstr("C-") catch {};
if (nc.isShift(k.mod))
_ = self.plane.putstr("S-") catch {};
if (nc.isAlt(k.mod))
_ = self.plane.putstr("A-") catch {};
_ = self.plane.print("{s}", .{nc.key_id_string(k.id)}) catch {};
c += 1;
}
return true;
}
const idle_spinner = [_][]const u8{ "🞻", "", "🞼", "🞽", "🞾", "🞿", "🞾", "🞽", "🞼", "" };
fn render_idle(self: *Self) bool {
self.idle_frame += 1;
if (self.idle_frame > 180) {
return self.animate();
} else {
const i = @mod(self.idle_frame / 8, idle_spinner.len);
_ = self.plane.print_aligned(0, .center, "{s} {s} {s}", .{ idle_spinner[i], idle_msg, idle_spinner[i] }) catch {};
}
return true;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.hover) theme.statusbar_hover else theme.statusbar);
self.frame += 1;
if (self.frame - self.key_active_frame > self.wipe_after_frames)
self.unset_key_all();
self.plane.erase();
self.plane.home();
return if (self.keys[0].id > 0) self.render_active() else self.render_idle();
}
fn set_nkey(self: *Self, key: Key) void {
for (self.keys, 0..) |k, i| {
if (k.id == 0) {
self.keys[i].id = key.id;
self.keys[i].mod = key.mod;
return;
}
}
for (self.keys, 0.., 1..) |_, i, j| {
if (j < self.keys.len)
self.keys[i] = self.keys[j];
}
self.keys[self.keys.len - 1].id = key.id;
self.keys[self.keys.len - 1].mod = key.mod;
}
fn unset_nkey_(self: *Self, key: u32) void {
for (self.keys, 0..) |k, i| {
if (k.id == key) {
for (i..self.keys.len, (i + 1)..) |i_, j| {
if (j < self.keys.len)
self.keys[i_] = self.keys[j];
}
self.keys[self.keys.len - 1].id = 0;
return;
}
}
}
const upper_offset: u32 = 'a' - 'A';
fn unset_nkey(self: *Self, key: Key) void {
self.unset_nkey_(key.id);
if (key.id >= 'a' and key.id <= 'z')
self.unset_nkey_(key.id - upper_offset);
if (key.id >= 'A' and key.id <= 'Z')
self.unset_nkey_(key.id + upper_offset);
}
fn unset_key_all(self: *Self) void {
for (0..self.keys.len) |i| {
self.keys[i].id = 0;
self.keys[i].mod = 0;
}
}
fn set_key(self: *Self, key: Key, val: bool) void {
self.idle_frame = 0;
self.key_active_frame = self.frame;
(if (val) &set_nkey else &unset_nkey)(self, key);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var key: u32 = 0;
var mod: u32 = 0;
if (try m.match(.{ "I", nc.event_type.PRESS, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more })) {
self.set_key(.{ .id = key, .mod = mod }, true);
} else if (try m.match(.{ "I", nc.event_type.RELEASE, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more })) {
self.set_key(.{ .id = key, .mod = mod }, false);
}
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
command.executeName("toggle_inputview", .{}) catch {};
return true;
}
if (try m.match(.{ "H", tp.extract(&self.hover) })) {
tui.current().request_mouse_cursor_pointer(self.hover);
return true;
}
return false;
}
fn animate(self: *Self) bool {
const positions = eighths_c * (width - 1);
const frame = @mod(self.frame, positions * 2);
const pos = if (frame > eighths_c * (width - 1))
positions * 2 - frame
else
frame;
smooth_block_at(self.plane, pos);
return false;
// return pos != 0;
}
const eighths_l = [_][]const u8{ "", "", "", "", "", "", "", "" };
const eighths_r = [_][]const u8{ " ", "", "🮇", "🮈", "", "🮉", "🮊", "🮋" };
const eighths_c = eighths_l.len;
fn smooth_block_at(plane: nc.Plane, pos: u64) void {
const blk = @mod(pos, eighths_c) + 1;
const l = eighths_l[eighths_c - blk];
const r = eighths_r[eighths_c - blk];
plane.erase();
plane.cursor_move_yx(0, @as(c_int, @intCast(@divFloor(pos, eighths_c)))) catch return;
_ = plane.putstr(@ptrCast(r)) catch return;
_ = plane.putstr(@ptrCast(l)) catch return;
}

View file

@ -0,0 +1,71 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
line: usize = 0,
lines: usize = 0,
column: usize = 0,
buf: [256]u8 = undefined,
rendered: [:0]const u8 = "",
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.rendered.len };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.putstr(self.rendered) catch {};
return false;
}
fn format(self: *Self) void {
var fbs = std.io.fixedBufferStream(&self.buf);
const writer = fbs.writer();
std.fmt.format(writer, " Ln {d}, Col {d} ", .{ self.line + 1, self.column + 1 }) catch {};
self.rendered = @ptrCast(fbs.getWritten());
self.buf[self.rendered.len] = 0;
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.extract(&self.column) })) {
self.format();
} else if (try m.match(.{ "E", "close" })) {
self.lines = 0;
self.line = 0;
self.column = 0;
self.rendered = "";
}
return false;
}

110
src/tui/status/minilog.zig Normal file
View file

@ -0,0 +1,110 @@
const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const log = @import("log");
const Widget = @import("../Widget.zig");
const MessageFilter = @import("../MessageFilter.zig");
const tui = @import("../tui.zig");
const mainview = @import("../mainview.zig");
parent: nc.Plane,
plane: nc.Plane,
msg: std.ArrayList(u8),
is_error: bool = false,
timer: ?tp.timeout = null,
const message_display_time_seconds = 2;
const error_display_time_seconds = 4;
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = .{
.parent = parent,
.plane = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent),
.msg = std.ArrayList(u8).init(a),
};
try tui.current().message_filters.add(MessageFilter.bind(self, log_receive));
try log.subscribe();
return Widget.to(self);
}
pub fn deinit(self: *Self, a: std.mem.Allocator) void {
self.cancel_timer();
self.msg.deinit();
log.unsubscribe() catch {};
tui.current().message_filters.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = if (self.msg.items.len > 0) self.msg.items.len + 2 else 1 };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
tui.set_base_style(&self.plane, " ", if (self.msg.items.len > 0) theme.sidebar else theme.statusbar);
self.plane.erase();
self.plane.home();
if (self.is_error)
tui.set_base_style(&self.plane, " ", theme.editor_error);
_ = self.plane.print(" {s} ", .{self.msg.items}) catch return false;
return false;
}
pub fn log_receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "log", tp.more })) {
self.log_process(m) catch |e| return tp.exit_error(e);
if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.logview_enabled)
return false; // pass on log messages to logview
return true;
} else if (try m.match(.{ "minilog", "clear" })) {
self.is_error = false;
self.cancel_timer();
self.msg.clearRetainingCapacity();
Widget.need_render();
return true;
}
return false;
}
pub fn log_process(self: *Self, m: tp.message) !void {
var src: []const u8 = undefined;
var context: []const u8 = undefined;
var msg: []const u8 = undefined;
if (try m.match(.{ "log", tp.extract(&src), tp.extract(&msg) })) {
if (self.is_error) return;
self.reset_timer();
self.msg.clearRetainingCapacity();
try self.msg.appendSlice(msg);
Widget.need_render();
} else if (try m.match(.{ "log", "error", tp.extract(&src), tp.extract(&context), "->", tp.extract(&msg) })) {
self.is_error = true;
self.reset_timer();
self.msg.clearRetainingCapacity();
try self.msg.appendSlice(msg);
Widget.need_render();
} else if (try m.match(.{ "log", tp.extract(&src), tp.more })) {
self.is_error = true;
self.reset_timer();
self.msg.clearRetainingCapacity();
var s = std.json.writeStream(self.msg.writer(), .{});
var iter: []const u8 = m.buf;
try @import("cbor").JsonStream(@TypeOf(self.msg)).jsonWriteValue(&s, &iter);
Widget.need_render();
}
}
fn reset_timer(self: *Self) void {
self.cancel_timer();
const delay: u64 = std.time.ms_per_s * @as(u64, if (self.is_error) error_display_time_seconds else message_display_time_seconds);
self.timer = tp.timeout.init_ms(delay, tp.message.fmt(.{ "minilog", "clear" })) catch null;
}
fn cancel_timer(self: *Self) void {
if (self.timer) |*timer| {
timer.deinit();
self.timer = null;
}
}

View file

@ -0,0 +1,74 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const root = @import("root");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const ed = @import("../editor.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = if (is_mini_mode()) tui.get_mode().len + 5 else tui.get_mode().len - 1 };
}
fn is_mini_mode() bool {
return if (tui.current().mini_mode) |_| true else false;
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
if (is_mini_mode())
self.render_mode(theme)
else
self.render_logo(theme);
return false;
}
fn render_mode(self: *Self, theme: *const Widget.Theme) void {
tui.set_base_style(&self.plane, " ", theme.statusbar_hover);
self.plane.on_styles(nc.style.bold);
self.plane.erase();
self.plane.home();
var buf: [31:0]u8 = undefined;
_ = self.plane.putstr(std.fmt.bufPrintZ(&buf, " {s} ", .{tui.get_mode()}) catch return) catch {};
if (theme.statusbar_hover.bg) |bg| self.plane.set_fg_rgb(bg) catch {};
if (theme.statusbar.bg) |bg| self.plane.set_bg_rgb(bg) catch {};
_ = self.plane.putstr("") catch {};
}
fn render_logo(self: *Self, theme: *const Widget.Theme) void {
tui.set_base_style(&self.plane, " ", theme.statusbar_hover);
self.plane.on_styles(nc.style.bold);
self.plane.erase();
self.plane.home();
var buf: [31:0]u8 = undefined;
_ = self.plane.putstr(std.fmt.bufPrintZ(&buf, " {s} ", .{tui.get_mode()}) catch return) catch {};
}

102
src/tui/status/modstate.zig Normal file
View file

@ -0,0 +1,102 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const command = @import("../command.zig");
const tui = @import("../tui.zig");
const EventHandler = @import("../EventHandler.zig");
parent: nc.Plane,
plane: nc.Plane,
ctrl: bool = false,
shift: bool = false,
alt: bool = false,
hover: bool = false,
const Self = @This();
pub const width = 5;
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
try tui.current().input_listeners.add(EventHandler.bind(self, listen));
return self.widget();
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn widget(self: *Self) Widget {
return Widget.to(self);
}
pub fn deinit(self: *Self, a: Allocator) void {
tui.current().input_listeners.remove_ptr(self);
self.plane.deinit();
a.destroy(self);
}
pub fn layout(_: *Self) Widget.Layout {
return .{ .static = width };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", if (self.hover) theme.statusbar_hover else theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.print("\u{2003}{s}{s}{s}\u{2003}", .{
mode(self.ctrl, "", "🅒"),
mode(self.shift, "", "🅢"),
mode(self.alt, "", "🅐"),
}) catch {};
return false;
}
inline fn mode(state: bool, off: [:0]const u8, on: [:0]const u8) [:0]const u8 {
return if (state) on else off;
}
fn render_modifier(self: *Self, state: bool, off: [:0]const u8, on: [:0]const u8) void {
_ = self.plane.putstr(if (state) on else off) catch {};
}
fn set_modifiers(self: *Self, key: u32, mods: u32) void {
const modifiers = switch (key) {
nc.key.LCTRL, nc.key.RCTRL => mods ^ nc.mod.CTRL,
nc.key.LSHIFT, nc.key.RSHIFT => mods ^ nc.mod.SHIFT,
nc.key.LALT, nc.key.RALT => mods ^ nc.mod.ALT,
else => mods,
};
self.ctrl = nc.isCtrl(modifiers);
self.shift = nc.isShift(modifiers);
self.alt = nc.isAlt(modifiers);
}
pub fn listen(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
var key: u32 = 0;
var mod: u32 = 0;
if (try m.match(.{ "I", tp.any, tp.extract(&key), tp.any, tp.any, tp.extract(&mod), tp.more }))
self.set_modifiers(key, mod);
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "B", nc.event_type.PRESS, nc.key.BUTTON1, tp.any, tp.any, tp.any, tp.any, tp.any })) {
command.executeName("toggle_inputview", .{}) catch {};
return true;
}
return try m.match(.{ "H", tp.extract(&self.hover) });
}

View file

@ -0,0 +1,105 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const nc = @import("notcurses");
const tp = @import("thespian");
const tracy = @import("tracy");
const Widget = @import("../Widget.zig");
const ed = @import("../editor.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
matches: usize = 0,
cursels: usize = 0,
selection: ?ed.Selection = null,
buf: [256]u8 = undefined,
rendered: [:0]const u8 = "",
const Self = @This();
pub fn create(a: Allocator, parent: nc.Plane) !Widget {
const self: *Self = try a.create(Self);
self.* = try init(parent);
return Widget.to(self);
}
fn init(parent: nc.Plane) !Self {
var n = try nc.Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent);
errdefer n.deinit();
return .{
.parent = parent,
.plane = n,
};
}
pub fn deinit(self: *Self, a: Allocator) void {
self.plane.deinit();
a.destroy(self);
}
pub fn layout(self: *Self) Widget.Layout {
return .{ .static = self.rendered.len };
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const frame = tracy.initZone(@src(), .{ .name = @typeName(@This()) ++ " render" });
defer frame.deinit();
tui.set_base_style(&self.plane, " ", theme.statusbar);
self.plane.erase();
self.plane.home();
_ = self.plane.putstr(self.rendered) catch {};
return false;
}
fn format(self: *Self) void {
var fbs = std.io.fixedBufferStream(&self.buf);
const writer = fbs.writer();
_ = writer.write(" ") catch {};
if (self.matches > 1) {
std.fmt.format(writer, "({d} matches)", .{self.matches}) catch {};
if (self.selection) |_|
_ = writer.write(" ") catch {};
}
if (self.cursels > 1) {
std.fmt.format(writer, "({d} cursels)", .{self.cursels}) catch {};
if (self.selection) |_|
_ = writer.write(" ") catch {};
}
if (self.selection) |sel_| {
var sel = sel_;
sel.normalize();
const lines = sel.end.row - sel.begin.row;
if (lines == 0) {
std.fmt.format(writer, "({d} selected)", .{sel.end.col - sel.begin.col}) catch {};
} else {
std.fmt.format(writer, "({d} lines selected)", .{if (sel.end.col == 0) lines else lines + 1}) catch {};
}
}
_ = writer.write(" ") catch {};
self.rendered = @ptrCast(fbs.getWritten());
self.buf[self.rendered.len] = 0;
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "E", "match", tp.extract(&self.matches) }))
self.format();
if (try m.match(.{ "E", "cursels", tp.extract(&self.cursels) }))
self.format();
if (try m.match(.{ "E", "close" })) {
self.matches = 0;
self.selection = null;
self.format();
} else if (try m.match(.{ "E", "sel", tp.more })) {
var sel: ed.Selection = undefined;
if (try m.match(.{ tp.any, tp.any, "none" })) {
self.matches = 0;
self.selection = null;
} 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) })) {
self.selection = sel;
}
self.format();
}
return false;
}

View file

@ -0,0 +1,23 @@
const std = @import("std");
const nc = @import("notcurses");
const Widget = @import("../Widget.zig");
const WidgetList = @import("../WidgetList.zig");
const tui = @import("../tui.zig");
parent: nc.Plane,
plane: nc.Plane,
const Self = @This();
pub fn create(a: std.mem.Allocator, parent: Widget) !Widget {
var w = try WidgetList.createH(a, parent, "statusbar", .{ .static = 1 });
if (tui.current().config.modestate_show) try w.add(try @import("modestate.zig").create(a, w.plane));
try w.add(try @import("filestate.zig").create(a, w.plane));
try w.add(try @import("minilog.zig").create(a, w.plane));
if (tui.current().config.selectionstate_show) try w.add(try @import("selectionstate.zig").create(a, w.plane));
try w.add(try @import("linenumstate.zig").create(a, w.plane));
if (tui.current().config.modstate_show) try w.add(try @import("modstate.zig").create(a, w.plane));
if (tui.current().config.keystate_show) try w.add(try @import("keystate.zig").create(a, w.plane));
return w.widget();
}

1021
src/tui/tui.zig Normal file

File diff suppressed because it is too large Load diff

39
src/unicode.zig Normal file
View file

@ -0,0 +1,39 @@
pub fn control_code_to_unicode(code: u8) [:0]const u8 {
return switch (code) {
'\x00' => "",
'\x01' => "",
'\x02' => "",
'\x03' => "",
'\x04' => "",
'\x05' => "",
'\x06' => "",
'\x07' => "",
'\x08' => "",
'\x09' => "",
'\x0A' => "",
'\x0B' => "",
'\x0C' => "",
'\x0D' => "",
'\x0E' => "",
'\x0F' => "",
'\x10' => "",
'\x11' => "",
'\x12' => "",
'\x13' => "",
'\x14' => "",
'\x15' => "",
'\x16' => "",
'\x17' => "",
'\x18' => "",
'\x19' => "",
'\x1A' => "",
'\x1B' => "",
'\x1C' => "",
'\x1D' => "",
'\x1E' => "",
'\x1F' => "",
'\x20' => "",
'\x7F' => "",
else => "",
};
}

7
test/tests.zig Normal file
View file

@ -0,0 +1,7 @@
const std = @import("std");
pub const buffer = @import("tests_buffer.zig");
pub const color = @import("tests_color.zig");
test {
std.testing.refAllDecls(@This());
}

279
test/tests_buffer.zig Normal file
View file

@ -0,0 +1,279 @@
const std = @import("std");
const Buffer = @import("Buffer");
const ArrayList = std.ArrayList;
const a = std.testing.allocator;
fn get_big_doc() !*Buffer {
const BigDocGen = struct {
line_num: usize = 0,
lines: usize = 10000,
buf: [128]u8 = undefined,
line_buf: []u8 = "",
read_count: usize = 0,
const Self = @This();
const Reader = std.io.Reader(*Self, Err, read);
const Err = error{NoSpaceLeft};
fn gen_line(self: *Self) Err!void {
var stream = std.io.fixedBufferStream(&self.buf);
const writer = stream.writer();
try writer.print("this is line {d}\n", .{self.line_num});
self.line_buf = stream.getWritten();
self.read_count = 0;
self.line_num += 1;
}
fn read(self: *Self, buffer: []u8) Err!usize {
if (self.line_num > self.lines)
return 0;
if (self.line_buf.len == 0 or self.line_buf.len - self.read_count == 0)
try self.gen_line();
const read_count = self.read_count;
const bytes_to_read = @min(self.line_buf.len - read_count, buffer.len);
@memcpy(buffer[0..bytes_to_read], self.line_buf[read_count .. read_count + bytes_to_read]);
self.read_count += bytes_to_read;
return bytes_to_read;
}
fn reader(self: *Self) Reader {
return .{ .context = self };
}
};
var gen: BigDocGen = .{};
var doc = ArrayList(u8).init(a);
defer doc.deinit();
try gen.reader().readAllArrayList(&doc, std.math.maxInt(usize));
var buf = try Buffer.create(a);
var fis = std.io.fixedBufferStream(doc.items);
buf.update(try buf.load(fis.reader(), doc.items.len));
return buf;
}
test "buffer" {
const doc: []const u8 =
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
;
const buffer = try Buffer.create(a);
defer buffer.deinit();
const root = try buffer.load_from_string(doc);
try std.testing.expect(root.is_balanced());
buffer.update(root);
const result: []const u8 = try buffer.store_to_string(a);
defer a.free(result);
try std.testing.expectEqualDeep(result, doc);
try std.testing.expectEqual(doc.len, result.len);
try std.testing.expectEqual(doc.len, buffer.root.length());
}
fn get_line(buf: *const Buffer, line: usize) ![]const u8 {
var result = ArrayList(u8).init(a);
try buf.root.get_line(line, &result);
return result.toOwnedSlice();
}
test "walk_from_line" {
const buffer = try get_big_doc();
defer buffer.deinit();
const lines = buffer.root.lines();
try std.testing.expectEqual(lines, 10002);
const line0 = try get_line(buffer, 0);
defer a.free(line0);
try std.testing.expect(std.mem.eql(u8, line0, "this is line 0"));
const line1 = try get_line(buffer, 1);
defer a.free(line1);
try std.testing.expect(std.mem.eql(u8, line1, "this is line 1"));
const line100 = try get_line(buffer, 100);
defer a.free(line100);
try std.testing.expect(std.mem.eql(u8, line100, "this is line 100"));
const line9999 = try get_line(buffer, 9999);
defer a.free(line9999);
try std.testing.expect(std.mem.eql(u8, line9999, "this is line 9999"));
}
test "line_len" {
const doc: []const u8 =
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
;
const buffer = try Buffer.create(a);
defer buffer.deinit();
buffer.update(try buffer.load_from_string(doc));
try std.testing.expectEqual(try buffer.root.line_width(0), 8);
try std.testing.expectEqual(try buffer.root.line_width(1), 5);
}
test "del_chars" {
const doc: []const u8 =
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
;
const buffer = try Buffer.create(a);
defer buffer.deinit();
buffer.update(try buffer.load_from_string(doc));
buffer.update(try buffer.root.del_chars(3, try buffer.root.line_width(3) - 1, 1, buffer.a));
const line3 = try get_line(buffer, 3);
defer a.free(line3);
try std.testing.expect(std.mem.eql(u8, line3, "us"));
buffer.update(try buffer.root.del_chars(3, 0, 7, buffer.a));
const line3_1 = try get_line(buffer, 3);
defer a.free(line3_1);
try std.testing.expect(std.mem.eql(u8, line3_1, "your"));
// try buffer.rebalance();
// try std.testing.expect(buffer.is_balanced());
buffer.update(try buffer.root.del_chars(0, try buffer.root.line_width(0) - 1, 2, buffer.a));
const line0 = try get_line(buffer, 0);
defer a.free(line0);
try std.testing.expect(std.mem.eql(u8, line0, "All youropes"));
}
fn check_line(buffer: *const Buffer, line_no: usize, expect: []const u8) !void {
const line = try get_line(buffer, line_no);
defer a.free(line);
try std.testing.expect(std.mem.eql(u8, line, expect));
}
test "del_chars2" {
const doc: []const u8 =
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
\\All your
\\ropes
\\are belong to
\\us!
;
const buffer = try Buffer.create(a);
defer buffer.deinit();
buffer.update(try buffer.load_from_string(doc));
buffer.update(try buffer.root.del_chars(2, try buffer.root.line_width(2) - 3, 6, buffer.a));
try check_line(buffer, 2, "are belong!");
try check_line(buffer, 3, "All your");
try check_line(buffer, 4, "ropes");
}
test "insert_chars" {
const doc: []const u8 =
\\B
;
const buffer = try Buffer.create(a);
defer buffer.deinit();
buffer.update(try buffer.load_from_string(doc));
const line0 = try get_line(buffer, 0);
defer a.free(line0);
try std.testing.expect(std.mem.eql(u8, line0, "B"));
_, _, const root = try buffer.root.insert_chars(0, 0, "1", buffer.a);
buffer.update(root);
const line1 = try get_line(buffer, 0);
defer a.free(line1);
try std.testing.expect(std.mem.eql(u8, line1, "1B"));
_, _, root = try root.insert_chars(0, 1, "2", buffer.a);
buffer.update(root);
const line2 = try get_line(buffer, 0);
defer a.free(line2);
try std.testing.expect(std.mem.eql(u8, line2, "12B"));
_, _, root = try root.insert_chars(0, 2, "3", buffer.a);
buffer.update(root);
const line3 = try get_line(buffer, 0);
defer a.free(line3);
try std.testing.expect(std.mem.eql(u8, line3, "123B"));
_, _, root = try root.insert_chars(0, 3, "4", buffer.a);
buffer.update(root);
const line4 = try get_line(buffer, 0);
defer a.free(line4);
try std.testing.expect(std.mem.eql(u8, line4, "1234B"));
_, _, root = try root.insert_chars(0, 4, "5", buffer.a);
buffer.update(root);
const line5 = try get_line(buffer, 0);
defer a.free(line5);
try std.testing.expect(std.mem.eql(u8, line5, "12345B"));
_, _, root = try root.insert_chars(0, 5, "6", buffer.a);
buffer.update(root);
const line6 = try get_line(buffer, 0);
defer a.free(line6);
try std.testing.expect(std.mem.eql(u8, line6, "123456B"));
_, _, root = try root.insert_chars(0, 6, "7", buffer.a);
buffer.update(root);
const line7 = try get_line(buffer, 0);
defer a.free(line7);
try std.testing.expect(std.mem.eql(u8, line7, "1234567B"));
const line, const col, root = try buffer.root.insert_chars(0, 7, "8\n9", buffer.a);
buffer.update(root);
const line8 = try get_line(buffer, 0);
defer a.free(line8);
const line9 = try get_line(buffer, 1);
defer a.free(line9);
try std.testing.expect(std.mem.eql(u8, line8, "12345678"));
try std.testing.expect(std.mem.eql(u8, line9, "9B"));
try std.testing.expectEqual(line, 1);
try std.testing.expectEqual(col, 1);
}

43
test/tests_color.zig Normal file
View file

@ -0,0 +1,43 @@
const std = @import("std");
const color = @import("color");
const RGB = color.RGB;
const rgb = RGB.from_u24;
test "contrast white/yellow" {
const a: u24 = 0xFFFFFF; // white
const b: u24 = 0x00FFFF; // yellow
const ratio = RGB.contrast(rgb(a), rgb(b));
try std.testing.expectApproxEqAbs(ratio, 1.25388109, 0.000001);
}
test "contrast white/blue" {
const a: u24 = 0xFFFFFF; // white
const b: u24 = 0x0000FF; // blue
const ratio = RGB.contrast(rgb(a), rgb(b));
try std.testing.expectApproxEqAbs(ratio, 8.59247135, 0.000001);
}
test "contrast black/yellow" {
const a: u24 = 0x000000; // black
const b: u24 = 0x00FFFF; // yellow
const ratio = RGB.contrast(rgb(a), rgb(b));
try std.testing.expectApproxEqAbs(ratio, 16.7479991, 0.000001);
}
test "contrast black/blue" {
const a: u24 = 0x000000; // black
const b: u24 = 0x0000FF; // blue
const ratio = RGB.contrast(rgb(a), rgb(b));
try std.testing.expectApproxEqAbs(ratio, 2.444, 0.000001);
}
test "best contrast black/white to yellow" {
const best = color.max_contrast(0x00FFFF, 0xFFFFFF, 0x000000);
try std.testing.expectEqual(best, 0x000000);
}
test "best contrast black/white to blue" {
const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000);
try std.testing.expectEqual(best, 0xFFFFFF);
}

59
zig Executable file
View file

@ -0,0 +1,59 @@
#!/bin/bash
set -e
ARCH=$(uname -m)
BASEDIR="$(cd "$(dirname "$0")" && pwd)"
ZIGDIR=$BASEDIR/.cache/zig
VERSION=$(< build.zig.version)
OS=$(uname)
if [ "$OS" == "Linux" ] ; then
OS=linux
elif [ "$OS" == "Darwin" ] ; then
OS=macos
fi
ZIGVER="zig-$OS-$ARCH-$VERSION"
ZIG=$ZIGDIR/$ZIGVER/zig
if [ "$1" == "update" ] ; then
curl -L --silent https://ziglang.org/download/index.json | jq -r '.master | .version' > build.zig.version
NEWVERSION=$(< build.zig.version)
if [ "$VERSION" != "$NEWVERSION" ] ; then
echo zig version updated from $VERSION to $NEWVERSION
echo rebuilding to update cdb...
$0 cdb
exit 0
fi
echo zig version $VERSION is up-to-date
exit 0
fi
get_zig() {
(
mkdir -p "$ZIGDIR"
cd "$ZIGDIR"
TARBALL="https://ziglang.org/builds/$ZIGVER.tar.xz"
if [ ! -d "$ZIGVER" ] ; then
curl "$TARBALL" | tar -xJ
fi
)
}
get_zig
if [ "$1" == "cdb" ] ; then
rm -rf zig-cache
rm -rf .cache/cdb
$ZIG build
(echo \[ ; cat .cache/cdb/* ; echo {}\]) | perl -0777 -pe 's/,\n\{\}//igs' | jq . | grep -v 'no-default-config' > compile_commands.json
exit 0
fi
exec $ZIG "$@"