Initial public release
This commit is contained in:
parent
3c3f068914
commit
4ece4babad
63 changed files with 15101 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/.cache/
|
||||
/zig-out/
|
||||
/zig-cache/
|
290
build.zig
Normal file
290
build.zig
Normal 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
1
build.zig.version
Normal file
|
@ -0,0 +1 @@
|
|||
0.12.0-dev.3059+27f589dea
|
42
build.zig.zon
Normal file
42
build.zig.zon
Normal 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
293
help.md
Normal 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
1158
src/Buffer.zig
Normal file
File diff suppressed because it is too large
Load diff
220
src/Cursor.zig
Normal file
220
src/Cursor.zig
Normal 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
61
src/Selection.zig
Normal 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
142
src/View.zig
Normal 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
62
src/color.zig
Normal 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
15
src/config.zig
Normal 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
165
src/diff.zig
Normal 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
127
src/file_type.zig
Normal 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
294
src/file_types.zig
Normal 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
147
src/location_history.zig
Normal 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
158
src/log.zig
Normal 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
168
src/lsp_process.zig
Normal 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
319
src/main.zig
Normal 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, ¶ms, 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, ¶ms, .{}) catch {};
|
||||
exit(1);
|
||||
return err;
|
||||
};
|
||||
defer res.deinit();
|
||||
|
||||
if (res.args.help != 0)
|
||||
return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{});
|
||||
|
||||
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
235
src/ripgrep.zig
Normal 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
112
src/syntax.zig
Normal 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
63
src/text_manip.zig
Normal 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
9
src/tracy_noop.zig
Normal 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
40
src/tui/Box.zig
Normal 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
175
src/tui/EventHandler.zig
Normal 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
138
src/tui/MessageFilter.zig
Normal 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
273
src/tui/Widget.zig
Normal 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
223
src/tui/WidgetList.zig
Normal 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
94
src/tui/WidgetStack.zig
Normal 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
194
src/tui/command.zig
Normal 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
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
327
src/tui/editor_gutter.zig
Normal 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
185
src/tui/fonts.zig
Normal 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
263
src/tui/home.zig
Normal 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
106
src/tui/inputview.zig
Normal 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
181
src/tui/inspector_view.zig
Normal 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
132
src/tui/logview.zig
Normal 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
374
src/tui/mainview.zig
Normal 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
44
src/tui/message_box.zig
Normal 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
286
src/tui/mode/input/flow.zig
Normal 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
103
src/tui/mode/input/home.zig
Normal 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 {};
|
||||
}
|
||||
}
|
284
src/tui/mode/input/vim/insert.zig
Normal file
284
src/tui/mode/input/vim/insert.zig
Normal 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_ });
|
||||
}
|
497
src/tui/mode/input/vim/normal.zig
Normal file
497
src/tui/mode/input/vim/normal.zig
Normal 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);
|
||||
}
|
472
src/tui/mode/input/vim/visual.zig
Normal file
472
src/tui/mode/input/vim/visual.zig
Normal 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
231
src/tui/mode/mini/find.zig
Normal 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];
|
||||
}
|
||||
}
|
190
src/tui/mode/mini/find_in_files.zig
Normal file
190
src/tui/mode/mini/find_in_files.zig
Normal 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
116
src/tui/mode/mini/goto.zig
Normal 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 {};
|
||||
}
|
122
src/tui/mode/mini/move_to_char.zig
Normal file
122
src/tui/mode/mini/move_to_char.zig
Normal 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 {};
|
||||
}
|
144
src/tui/mode/mini/open_file.zig
Normal file
144
src/tui/mode/mini/open_file.zig
Normal 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
171
src/tui/scrollbar_v.zig
Normal 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));
|
||||
}
|
||||
}
|
218
src/tui/status/filestate.zig
Normal file
218
src/tui/status/filestate.zig
Normal 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
211
src/tui/status/keystate.zig
Normal 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;
|
||||
}
|
71
src/tui/status/linenumstate.zig
Normal file
71
src/tui/status/linenumstate.zig
Normal 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
110
src/tui/status/minilog.zig
Normal 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;
|
||||
}
|
||||
}
|
74
src/tui/status/modestate.zig
Normal file
74
src/tui/status/modestate.zig
Normal 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
102
src/tui/status/modstate.zig
Normal 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) });
|
||||
}
|
105
src/tui/status/selectionstate.zig
Normal file
105
src/tui/status/selectionstate.zig
Normal 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;
|
||||
}
|
23
src/tui/status/statusbar.zig
Normal file
23
src/tui/status/statusbar.zig
Normal 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
1021
src/tui/tui.zig
Normal file
File diff suppressed because it is too large
Load diff
39
src/unicode.zig
Normal file
39
src/unicode.zig
Normal 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
7
test/tests.zig
Normal 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
279
test/tests_buffer.zig
Normal 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
43
test/tests_color.zig
Normal 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
59
zig
Executable 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 "$@"
|
Loading…
Add table
Reference in a new issue