diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-01 19:56:21 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | cc806b11ba880d1bd326e3f1f9e69f7539c22395 (patch) | |
| tree | 3b200ed4e46e2c80d2762ae2efd45bf87413133d | |
| parent | f3618a5a3151e02db2c1e2280ab42d48c35eeba9 (diff) | |
| download | pardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.tar.gz pardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.zip | |
more treesitter grammars
| -rw-r--r-- | build.zig | 54 | ||||
| -rw-r--r-- | build.zig.zon | 94 | ||||
| -rw-r--r-- | main.zig | 244 | ||||
| -rw-r--r-- | term.zig | 4 | ||||
| -rw-r--r-- | tests.zig | 50 | ||||
| -rw-r--r-- | tutor.txt | 49 |
6 files changed, 429 insertions, 66 deletions
@@ -101,25 +101,57 @@ pub fn build(b: *std.Build) void { const tree_sitter = b.dependency("tree_sitter", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("tree-sitter", tree_sitter.module("tree_sitter")); - const grammars = .{ - .{ "c", "ts_c", false }, - .{ "rust", "ts_rust", true }, - .{ "zig", "ts_zig", false }, + const Grammar = struct { + name: []const u8, + dep: []const u8, + src: []const u8 = "src", + scanner: bool = false, + query: []const u8 = "queries/highlights.scm", + local_query: ?[]const u8 = null, + }; + const grammars = [_]Grammar{ + .{ .name = "ada", .dep = "ts_ada" }, + .{ .name = "bash", .dep = "ts_bash", .scanner = true }, + .{ .name = "c", .dep = "ts_c" }, + .{ .name = "c_sharp", .dep = "ts_c_sharp", .scanner = true }, + .{ .name = "clojure", .dep = "ts_clojure" }, + .{ .name = "cpp", .dep = "ts_cpp", .scanner = true }, + .{ .name = "css", .dep = "ts_css", .scanner = true }, + .{ .name = "elixir", .dep = "ts_elixir", .scanner = true }, + .{ .name = "erlang", .dep = "ts_erlang", .scanner = true }, + .{ .name = "fortran", .dep = "ts_fortran", .scanner = true }, + .{ .name = "go", .dep = "ts_go" }, + .{ .name = "haskell", .dep = "ts_haskell", .scanner = true }, + .{ .name = "html", .dep = "ts_html", .scanner = true }, + .{ .name = "java", .dep = "ts_java" }, + .{ .name = "javascript", .dep = "ts_javascript", .scanner = true }, + .{ .name = "json", .dep = "ts_json" }, + .{ .name = "kotlin", .dep = "ts_kotlin", .scanner = true }, + .{ .name = "ocaml", .dep = "ts_ocaml", .src = "grammars/ocaml/src", .scanner = true }, + .{ .name = "pascal", .dep = "ts_pascal" }, + .{ .name = "php", .dep = "ts_php", .src = "php/src", .scanner = true }, + .{ .name = "powershell", .dep = "ts_powershell", .scanner = true }, + .{ .name = "python", .dep = "ts_python", .scanner = true }, + .{ .name = "ruby", .dep = "ts_ruby", .scanner = true }, + .{ .name = "rust", .dep = "ts_rust", .scanner = true }, + .{ .name = "scala", .dep = "ts_scala", .scanner = true }, + .{ .name = "zig", .dep = "ts_zig" }, }; const ts_queries = b.addOptions(); inline for (grammars) |g| { - const dep = b.dependency(g[1], .{}); - const query = std.Io.Dir.cwd().readFileAlloc(io, dep.path("queries/highlights.scm").getPath(b), b.allocator, .limited(0x100000)) catch @panic("read " ++ g[0] ++ " highlights"); - ts_queries.addOption([]const u8, g[0] ++ "_highlights", query); + const dep = b.dependency(g.dep, .{}); + const query_path = if (g.local_query) |p| b.path(p) else dep.path(g.query); + const query = std.Io.Dir.cwd().readFileAlloc(io, query_path.getPath(b), b.allocator, .limited(0x100000)) catch @panic("read " ++ g.name ++ " highlights"); + ts_queries.addOption([]const u8, g.name ++ "_highlights", query); const lib = b.addLibrary(.{ - .name = "tree-sitter-" ++ g[0], + .name = "tree-sitter-" ++ g.name, .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true }), .linkage = .static, }); - lib.root_module.addCSourceFile(.{ .file = dep.path("src/parser.c") }); - if (g[2]) lib.root_module.addCSourceFile(.{ .file = dep.path("src/scanner.c") }); - lib.root_module.addIncludePath(dep.path("src")); + lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/parser.c") }); + if (g.scanner) lib.root_module.addCSourceFile(.{ .file = dep.path(g.src ++ "/scanner.c") }); + lib.root_module.addIncludePath(dep.path(g.src)); exe.root_module.linkLibrary(lib); } exe.root_module.addOptions("ts_queries", ts_queries); diff --git a/build.zig.zon b/build.zig.zon index 3c72c8a4..2d5a2b3a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -24,7 +24,7 @@ .url = "git+https://github.com/zig-gamedev/zstbi#3813f5f113b26f644cfa01a30155cc76b3526e91", .hash = "zstbi-0.11.0-dev-L0Ea_3GWBwAh7m54XX055vwLKPTPij5hyqyeq92MXPE2", }, - // tree-sitter: Zig bindings + the C runtime, plus three grammars (parser.c + // tree-sitter: Zig bindings + the C runtime, plus grammars (parser.c // we link, highlights.scm we read at build time into the ts_queries module). .tree_sitter = .{ .url = "git+https://github.com/tree-sitter/zig-tree-sitter#b55d4977d61330390fc733944da06106ff972660", @@ -42,6 +42,98 @@ .url = "git+https://github.com/maxxnino/tree-sitter-zig#a80a6e9be81b33b182ce6305ae4ea28e29211bd5", .hash = "N-V-__8AADLAWgBszUZPv5YTGyO5ZNyxNcuuR8MB3mIMJrZf", }, + .ts_python = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-python#26855eabccb19c6abf499fbc5b8dc7cc9ab8bc64", + .hash = "N-V-__8AAOiqPgC9tH5tYSAYNsMWLoPiSxcmkHvIhMnezomx", + }, + .ts_javascript = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-javascript#58404d8cf191d69f2674a8fd507bd5776f46cb11", + .hash = "N-V-__8AANlfOQDpOAP-6YMQeJ2nQetX1OpUshQr903bnIHU", + }, + .ts_go = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-go#2346a3ab1bb3857b48b29d779a1ef9799a248cd7", + .hash = "N-V-__8AAM9cIgDttbrvbwLWFB0JPplu_2KtCWu7__79EjIT", + }, + .ts_json = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-json#001c28d7a29832b06b0e831ec77845553c89b56d", + .hash = "N-V-__8AAHNaAgD6kjK3HX6lAf89chPqS5zIm0tLpvBQLOtX", + }, + .ts_html = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-html#73a3947324f6efddf9e17c0ea58d454843590cc0", + .hash = "N-V-__8AAEmOAwDdgRRlet5t1CEdpX1YMCbfhFiIbj_vsBH9", + }, + .ts_css = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-css#dda5cfc5722c429eaba1c910ca32c2c0c5bb1a3f", + .hash = "N-V-__8AAKwvEACP3YMpMDoRyyZPOdDN1n-zdTZRWTc3uufz", + }, + .ts_bash = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-bash#a06c2e4415e9bc0346c6b86d401879ffb44058f7", + .hash = "N-V-__8AANjenwCy_cXlIbU8nqBzGxMlEHVokLZJYrmIXtoz", + }, + .ts_cpp = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-cpp#8b5b49eb196bec7040441bee33b2c9a4838d6967", + .hash = "N-V-__8AAMePmwGeojPhBaBC10aGwaQIOU4vg1H17IWakWs-", + }, + .ts_java = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-java#e10607b45ff745f5f876bfa3e94fbcc6b44bdc11", + .hash = "N-V-__8AAGrhLgBgxGFnDv-tHxMarGDRT4VTig1IIX1rcBdc", + }, + .ts_c_sharp = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-c-sharp#af29416d729b7a6603101b513604392d8f675e3b", + .hash = "N-V-__8AALpc-AFDJNotrh7YgiXce1oXC9ub9SuD-Enwt7rN", + }, + .ts_php = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-php#38216983c07bf9e1b56e16acde53b25adaeab61c", + .hash = "N-V-__8AABgQ6QCTufRtmuKAuAvX1ty8ZouX1jZ17wGFcx6I", + }, + .ts_ruby = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-ruby#ad907a69da0c8a4f7a943a7fe012712208da6dee", + .hash = "N-V-__8AAB_S8QDTw_FIWZfx_eKtcAvrPJuTP12QEMVN2LDS", + }, + .ts_scala = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-scala#4d081d98670ff6e98ca42c085294fc75eec15e1d", + .hash = "N-V-__8AAJlN0gFxPUfpXqmIgQLEXkmXsrvo3caVWINVJdTb", + }, + .ts_kotlin = .{ + .url = "git+https://github.com/fwcd/tree-sitter-kotlin#c8ac3d2627240160b999a2c100de3babbdb8f419", + .hash = "N-V-__8AAKSpGALvICLMOMcl4S_ypQBib_euVyf_B_9DP08A", + }, + .ts_haskell = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-haskell#0975ef72fc3c47b530309ca93937d7d143523628", + .hash = "N-V-__8AALWgQgGf5l7eg-Dwi24SJszCffzN4_mWEtv3e3oI", + }, + .ts_ocaml = .{ + .url = "git+https://github.com/tree-sitter/tree-sitter-ocaml#527d62ef0f24ce0d97fbedf004921d75d0a7e086", + .hash = "N-V-__8AAGaGxwK7r2vy00a8b4MDEm0eudoNzkzu1WM01h7Q", + }, + .ts_erlang = .{ + .url = "git+https://github.com/WhatsApp/tree-sitter-erlang#836aa2b6c3af2c7cef3f84049b0ed6d44485a870", + .hash = "N-V-__8AAAVWKgC44vrL1kvl8SLUnqNkFU4guLAaXK73RmYK", + }, + .ts_elixir = .{ + .url = "git+https://github.com/elixir-lang/tree-sitter-elixir#7937d3b4d65fa574163cfa59394515d3c1cf16f4", + .hash = "N-V-__8AAHFBzAB5XFJyJvMulqqafphEQOoizic5m6WFfy6j", + }, + .ts_clojure = .{ + .url = "git+https://github.com/sogaiu/tree-sitter-clojure#e43eff80d17cf34852dcd92ca5e6986d23a7040f", + .hash = "N-V-__8AAAnoDwDKLr63cSQ_IHEojhLIanmbGIkrZNQSUM5_", + }, + .ts_fortran = .{ + .url = "git+https://github.com/stadelmanma/tree-sitter-fortran#7edacd2b21aa80057d9725384a1304a1c758e0f8", + .hash = "N-V-__8AAJ0zQgKKtzb0-wDQjY1pewE7RXmpAHVsKuwLT0e3", + }, + .ts_ada = .{ + .url = "git+https://github.com/briot/tree-sitter-ada#6b58259a08b1a22ba0247a7ce30be384db618da6", + .hash = "N-V-__8AAHNJMgAgTa051RgoAs48ohsOgCTsM34tTxoj80CE", + }, + .ts_pascal = .{ + .url = "git+https://github.com/Isopod/tree-sitter-pascal#042119eca2e18a60e56317fb06ee3ba5c32cb447", + .hash = "N-V-__8AAMmnUgA4hx9Tv_Vl5uDuzQ8r4gSH6J1CuwlZ5YOm", + }, + .ts_powershell = .{ + .url = "git+https://github.com/airbus-cert/tree-sitter-powershell#d398441825243b00e317e87e1829b9d6a3e54ce0", + .hash = "N-V-__8AAKzlUACji5nMCtCC_dgDtF1rEg1qoduDm3MT5B5H", + }, }, .minimum_zig_version = "0.16.0", .paths = .{""}, @@ -9,8 +9,31 @@ const ts_queries = @import("ts_queries"); // grammars whose parser.c we link (see build.zig). The query strings ride along // in the ts_queries module. +extern fn tree_sitter_ada() callconv(.c) *const ts.Language; +extern fn tree_sitter_bash() callconv(.c) *const ts.Language; extern fn tree_sitter_c() callconv(.c) *const ts.Language; +extern fn tree_sitter_c_sharp() callconv(.c) *const ts.Language; +extern fn tree_sitter_clojure() callconv(.c) *const ts.Language; +extern fn tree_sitter_cpp() callconv(.c) *const ts.Language; +extern fn tree_sitter_css() callconv(.c) *const ts.Language; +extern fn tree_sitter_elixir() callconv(.c) *const ts.Language; +extern fn tree_sitter_erlang() callconv(.c) *const ts.Language; +extern fn tree_sitter_fortran() callconv(.c) *const ts.Language; +extern fn tree_sitter_go() callconv(.c) *const ts.Language; +extern fn tree_sitter_haskell() callconv(.c) *const ts.Language; +extern fn tree_sitter_html() callconv(.c) *const ts.Language; +extern fn tree_sitter_java() callconv(.c) *const ts.Language; +extern fn tree_sitter_javascript() callconv(.c) *const ts.Language; +extern fn tree_sitter_json() callconv(.c) *const ts.Language; +extern fn tree_sitter_kotlin() callconv(.c) *const ts.Language; +extern fn tree_sitter_ocaml() callconv(.c) *const ts.Language; +extern fn tree_sitter_pascal() callconv(.c) *const ts.Language; +extern fn tree_sitter_php() callconv(.c) *const ts.Language; +extern fn tree_sitter_powershell() callconv(.c) *const ts.Language; +extern fn tree_sitter_python() callconv(.c) *const ts.Language; +extern fn tree_sitter_ruby() callconv(.c) *const ts.Language; extern fn tree_sitter_rust() callconv(.c) *const ts.Language; +extern fn tree_sitter_scala() callconv(.c) *const ts.Language; extern fn tree_sitter_zig() callconv(.c) *const ts.Language; // A TUI can't let std.log hit stderr — it's the same tty as our alt-screen, so the @@ -98,6 +121,66 @@ fn setYank(gpa: std.mem.Allocator, text: []const u8) void { yank_dirty = true; } +const default_tty_toggle: u21 = 'b'; + +const Cli = struct { + tty_only: bool = false, + tty_toggle: u21 = default_tty_toggle, + help: bool = false, +}; + +const help_text = + \\Usage: pardes [options] + \\ + \\Options: + \\ --tty start as one shell pane already in tty mode + \\ --tty-toggle <key> use Ctrl-<key> to enter/leave tty mode + \\ --tty-toggle=<key> same as above; accepts b, Ctrl-b, or ^b + \\ -h, --help show this help and exit + \\ +; + +fn printHelp(io: std.Io) !void { + try std.Io.File.stdout().writeStreamingAll(io, help_text); +} + +fn parseCtrlKey(raw: []const u8) ?u21 { + var value = std.mem.trim(u8, raw, " \t\r\n"); + if (value.len >= 5 and std.ascii.eqlIgnoreCase(value[0..5], "ctrl-")) { + value = value[5..]; + } else if (value.len >= 5 and std.ascii.eqlIgnoreCase(value[0..5], "ctrl+")) { + value = value[5..]; + } else if (value.len == 2 and value[0] == '^') { + value = value[1..]; + } + if (value.len != 1) return null; + const c = std.ascii.toLower(value[0]); + if (c < 'a' or c > 'z' or c == 'c') return null; + return c; +} + +fn parseCli(args: []const [:0]const u8) !Cli { + var cli: Cli = .{}; + var i: usize = 1; + while (i < args.len) : (i += 1) { + const a = args[i]; + if (std.mem.eql(u8, a, "--tty")) { + cli.tty_only = true; + } else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) { + cli.help = true; + } else if (std.mem.startsWith(u8, a, "--tty-toggle=")) { + cli.tty_toggle = parseCtrlKey(a["--tty-toggle=".len..]) orelse return error.BadArgs; + } else if (std.mem.eql(u8, a, "--tty-toggle")) { + i += 1; + if (i >= args.len) return error.BadArgs; + cli.tty_toggle = parseCtrlKey(args[i]) orelse return error.BadArgs; + } else { + return error.BadArgs; + } + } + return cli; +} + const Loop = vaxis.Loop(@TypeOf(Command.value)); // layout constants @@ -251,9 +334,10 @@ fn synStyle(s: Syn) ?struct { fg: [3]u8, bold: bool } { // Builtins. Executing the name (middle-click / Tab) runs the action through the ONE // dispatcher (onSelect + actOnSelection), no matter where the name is shown — a pane // body, a file, the tagbar. onSelect matches against the whole list, so a name works -// wherever it appears. Window-specific ones (Del, Delcol) live in each pane's tag; -// window-agnostic ones (Kill, Newcol, Tutor, Debug, Colors, NextColor) in the tagbar. -const SelectAction = enum { none, send, newcol, delcol, del, tutor, kill, debug, colors, nextcolor }; +// wherever it appears. Window-specific ones (Save, Del, Delcol) live in each +// pane's tag or can be typed/executed; window-agnostic ones (Kill, Newcol, Tutor, +// Debug, Colors, NextColor) in the tagbar. +const SelectAction = enum { none, send, newcol, delcol, del, save, tutor, kill, debug, colors, nextcolor }; const Builtin = struct { name: []const u8, action: SelectAction }; const builtins = [_]Builtin{ .{ .name = "Kill", .action = .kill }, @@ -262,13 +346,17 @@ const builtins = [_]Builtin{ .{ .name = "Debug", .action = .debug }, .{ .name = "Colors", .action = .colors }, .{ .name = "NextColor", .action = .nextcolor }, + .{ .name = "Save", .action = .save }, .{ .name = "Del", .action = .del }, .{ .name = "Delcol", .action = .delcol }, }; -// per-pane tag commands (window-specific). -const pane_builtins_str = "Del Delcol"; -// default editable tail (builtins with the leading gap after the cwd/path). +// Default per-pane tag commands. Delcol remains a builtin command, but it is no +// longer advertised in the default tag text. +const pane_builtins_str = "Del"; +const file_pane_builtins_str = "Save Del"; +// default editable tails (builtins with the leading gap after the cwd/path). const pane_tail = " " ++ pane_builtins_str; +const file_pane_tail = " " ++ file_pane_builtins_str; // the global tagbar: window-agnostic commands. Kill closes everything; Debug toggles // the stats overlay, Colors the ansi/syntax recolor, NextColor cycles the theme. const topbar_str = "Kill Newcol Tutor Debug Colors NextColor"; @@ -305,6 +393,12 @@ fn writeFile(path: [*:0]const u8, data: []const u8) void { } } +fn saveFile(io: std.Io, f: *Term.File) !void { + var file = try std.Io.Dir.createFileAbsolute(io, f.path, .{}); + defer file.close(io); + try file.writeStreamingAll(io, f.content); +} + fn fork_pty(argv: []const ?[*:0]const u8, cwd: ?[*:0]const u8, ws: std.posix.winsize) struct { file: std.Io.File, pid: std.posix.pid_t } { var master: c_int = undefined; const pid = forkpty(&master, null, null, &ws); @@ -431,9 +525,13 @@ fn tagPrefix(alloc: std.mem.Allocator, t: *Term) ![]u8 { // The editable tail: the user's edited buffer once touched, else the default // builtins. Empty for image panes (whole tag is live). Borrowed, not owned. +fn defaultTail(t: *Term) []const u8 { + return if (t.file != null) file_pane_tail else pane_tail; +} + fn curTail(t: *Term) []const u8 { if (t.image != null) return ""; - return if (t.tag_init) t.tag_tail.items else pane_tail; + return if (t.tag_init) t.tag_tail.items else defaultTail(t); } // prefix + tail: the full rendered/selectable tag line. Caller frees with `alloc`. @@ -453,7 +551,7 @@ fn enterTagEdit(gpa: std.mem.Allocator, alloc: std.mem.Allocator, t: *Term, clic if (t.image != null) return; // images keep a live toggle tag, no editable tail const pfx = tagPrefix(alloc, t) catch return; if (!t.tag_init) { - t.tag_tail.appendSlice(gpa, pane_tail) catch return; + t.tag_tail.appendSlice(gpa, defaultTail(t)) catch return; t.tag_init = true; } t.tag_edit = true; @@ -798,6 +896,13 @@ fn synFor(name: []const u8) Syn { return .none; } +fn extIs(ext: []const u8, comptime choices: anytype) bool { + inline for (choices) |choice| { + if (std.ascii.eqlIgnoreCase(ext, choice)) return true; + } + return false; +} + // Syntax-highlight `content` with tree-sitter: returns a gpa-owned byte per content // byte (a `Syn` id; 0 = none), or empty when the extension has no grammar. Caller // frees (stored in File.highlights; see term.zig). Best-effort — any parse/query @@ -806,13 +911,82 @@ fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) const ext = std.fs.path.extension(path); var lang: *const ts.Language = undefined; var query_src: []const u8 = undefined; - if (std.mem.eql(u8, ext, ".c") or std.mem.eql(u8, ext, ".h")) { + if (extIs(ext, .{ ".adb", ".ads", ".ada" })) { + lang = tree_sitter_ada(); + query_src = ts_queries.ada_highlights; + } else if (extIs(ext, .{ ".sh", ".bash", ".zsh" })) { + lang = tree_sitter_bash(); + query_src = ts_queries.bash_highlights; + } else if (extIs(ext, .{ ".c", ".h" })) { lang = tree_sitter_c(); query_src = ts_queries.c_highlights; - } else if (std.mem.eql(u8, ext, ".rs")) { + } else if (extIs(ext, .{ ".cs", ".csx" })) { + lang = tree_sitter_c_sharp(); + query_src = ts_queries.c_sharp_highlights; + } else if (extIs(ext, .{ ".clj", ".cljs", ".cljc", ".edn" })) { + lang = tree_sitter_clojure(); + query_src = ts_queries.clojure_highlights; + } else if (extIs(ext, .{ ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx" })) { + lang = tree_sitter_cpp(); + query_src = ts_queries.cpp_highlights; + } else if (extIs(ext, .{".css"})) { + lang = tree_sitter_css(); + query_src = ts_queries.css_highlights; + } else if (extIs(ext, .{ ".ex", ".exs" })) { + lang = tree_sitter_elixir(); + query_src = ts_queries.elixir_highlights; + } else if (extIs(ext, .{ ".erl", ".hrl" })) { + lang = tree_sitter_erlang(); + query_src = ts_queries.erlang_highlights; + } else if (extIs(ext, .{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" })) { + lang = tree_sitter_fortran(); + query_src = ts_queries.fortran_highlights; + } else if (extIs(ext, .{".go"})) { + lang = tree_sitter_go(); + query_src = ts_queries.go_highlights; + } else if (extIs(ext, .{ ".hs", ".lhs" })) { + lang = tree_sitter_haskell(); + query_src = ts_queries.haskell_highlights; + } else if (extIs(ext, .{ ".html", ".htm" })) { + lang = tree_sitter_html(); + query_src = ts_queries.html_highlights; + } else if (extIs(ext, .{".java"})) { + lang = tree_sitter_java(); + query_src = ts_queries.java_highlights; + } else if (extIs(ext, .{ ".js", ".jsx", ".mjs", ".cjs" })) { + lang = tree_sitter_javascript(); + query_src = ts_queries.javascript_highlights; + } else if (extIs(ext, .{".json"})) { + lang = tree_sitter_json(); + query_src = ts_queries.json_highlights; + } else if (extIs(ext, .{ ".kt", ".kts" })) { + lang = tree_sitter_kotlin(); + query_src = ts_queries.kotlin_highlights; + } else if (extIs(ext, .{ ".ml", ".mli" })) { + lang = tree_sitter_ocaml(); + query_src = ts_queries.ocaml_highlights; + } else if (extIs(ext, .{ ".pas", ".pp", ".p" })) { + lang = tree_sitter_pascal(); + query_src = ts_queries.pascal_highlights; + } else if (extIs(ext, .{ ".php", ".phtml", ".php3", ".php4", ".php5" })) { + lang = tree_sitter_php(); + query_src = ts_queries.php_highlights; + } else if (extIs(ext, .{ ".ps1", ".psm1", ".psd1" })) { + lang = tree_sitter_powershell(); + query_src = ts_queries.powershell_highlights; + } else if (extIs(ext, .{ ".py", ".pyw" })) { + lang = tree_sitter_python(); + query_src = ts_queries.python_highlights; + } else if (extIs(ext, .{ ".rb", ".rake" })) { + lang = tree_sitter_ruby(); + query_src = ts_queries.ruby_highlights; + } else if (extIs(ext, .{".rs"})) { lang = tree_sitter_rust(); query_src = ts_queries.rust_highlights; - } else if (std.mem.eql(u8, ext, ".zig") or std.mem.eql(u8, ext, ".zon")) { + } else if (extIs(ext, .{ ".scala", ".sc" })) { + lang = tree_sitter_scala(); + query_src = ts_queries.scala_highlights; + } else if (extIs(ext, .{ ".zig", ".zon" })) { lang = tree_sitter_zig(); query_src = ts_queries.zig_highlights; } else return &.{}; @@ -2173,12 +2347,12 @@ fn insertRunText(t: *Term, text: []const u8) void { // edit delete d change c yank y paste p undo u redo U // chord Enter -> acme look (open file/dir) Tab -> acme execute (run it) // (act on the selection if one is active, else the word under cursor) -// mode Esc collapses the selection; insert Esc -> normal; Ctrl-b -> raw tty +// mode Esc collapses the selection; insert Esc -> normal; configured Ctrl key -> raw tty // window Ctrl-w h/j/k/l focus neighbor pane (Alt-n new term, Alt-c new col) // File panes mutate their content (real edits) with whole-content undo/redo; // terminals yank rendered text and paste it as an insertion run (shell output can't // be deleted, so d/c there only clear pasted runs in the selected rows + yank). -// Enter raw tty mode (the Ctrl-b toggle; terminals only). tty-native: if the shell is at a +// Enter raw tty mode (the configured Ctrl-key toggle; terminals only). tty-native: if the shell is at a // prompt and the modal cursor sits on the current input line, move the shell's // REAL cursor there via ghostty's promptClickMove (sends left/right arrows the // shell understands) — so dropping into tty lands where you navigated: at the @@ -2222,7 +2396,7 @@ fn handleNormal(gpa: std.mem.Allocator, talloc: std.mem.Allocator, t: *Term, key const half: usize = @max(1, t.rows / 2); // Esc collapses any selection / cancels a pending prefix (helix). It does NOT - // drop terminals to tty — that's the deliberate Ctrl-b toggle. + // drop terminals to tty — that's the deliberate configured Ctrl-key toggle. if (key.matches(vaxis.Key.escape, .{})) { t.vsel.active = false; t.msel.active = false; @@ -2637,6 +2811,11 @@ fn actOnSelection( .colors => colors_on = !colors_on, .nextcolor => theme_idx = (theme_idx + 1) % themes.len, .kill => return true, // close everything: signal the main loop to quit + .save => { + if (t.file) |*f| saveFile(io, f) catch |err| { + std.log.warn("save {s}: {}", .{ f.path, err }); + }; + }, .send => { // terminal: run in itself. file pane: run in a shell sitting in the // file's directory (found by cwd, else a fresh tty in the right column). @@ -2737,14 +2916,17 @@ pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; - // --tty: start tmux-style — a single shell pane already in tty mode — instead - // of the default multi-pane editor layout. It only changes the startup layout - // and mode; everything else behaves identically. - const tty_only = blk: { - const args = init.minimal.args.toSlice(init.arena.allocator()) catch break :blk false; - for (args) |a| if (std.mem.eql(u8, a, "--tty")) break :blk true; - break :blk false; + const args = try init.minimal.args.toSlice(init.arena.allocator()); + const cli = parseCli(args) catch |err| { + try printHelp(io); + return err; }; + if (cli.help) { + try printHelp(io); + return; + } + const tty_only = cli.tty_only; + const tty_toggle = cli.tty_toggle; var screen_w: u16 = 80; var screen_h: u16 = 24; @@ -3132,16 +3314,15 @@ pub fn main(init: std.process.Init) !void { break :kpress; } - // Ctrl-b: hidden toggle in/out of raw tty mode (terminals only). + // Configured Ctrl key: hidden toggle in/out of raw tty mode + // (terminals only). // tty forwards every key straight to the pty, so it's deliberately // off the normal editing path — this toggle is the only way in/out. - if (key.matches('b', .{ .ctrl = true })) { - if (at.isTerminal()) { - if (at.mode == .tty) { - at.mode = .normal; - at.pending = 0; - } else enterTty(at); - } + if (at.isTerminal() and key.matches(tty_toggle, .{ .ctrl = true })) { + if (at.mode == .tty) { + at.mode = .normal; + at.pending = 0; + } else enterTty(at); break :kpress; } @@ -3191,7 +3372,8 @@ pub fn main(init: std.process.Init) !void { } // per-mode dispatch. Esc: insert -> normal (normal Esc is a - // no-op, like helix). Ctrl-b toggles raw tty on terminals (above). + // no-op, like helix). The configured Ctrl key toggles raw tty + // on terminals (above). // tty forwards every key to the pty. switch (at.mode) { .normal => handleNormal(gpa, frame_arena.allocator(), at, key), @@ -3562,7 +3744,7 @@ fn bodyText(alloc: std.mem.Allocator, t: *Term) ![]u8 { const body = try t.term.plainString(alloc); defer alloc.free(body); // text mode (normal AND insert) hides shell prompt rows: a clean acme-style - // page to navigate and type on. Only raw tty mode (the Ctrl-b toggle) shows + // page to navigate and type on. Only raw tty mode (the configured Ctrl-key toggle) shows // the real terminal with its prompts + typed input. var flagbuf: [512]bool = undefined; const nrows: usize = @min(t.rows, flagbuf.len); @@ -15,10 +15,10 @@ pub const Sel = struct { /// Modal input mode. normal/insert are the helix-style text-editing modes; tty /// forwards keys to the shell (terminal panes only — file panes never use it). /// normal -> helix NORMAL: hjkl/w/b/e/... move a block cursor; i/a/o -> insert; -/// v/x select; Esc collapses the selection; Ctrl-b -> tty (terminals). +/// v/x select; Esc collapses the selection; configured Ctrl key -> tty (terminals). /// insert -> keys insert text at the cursor (acme click-and-type); Esc -> normal. /// tty -> keys -> pty as terminal sequences. The mouse stays usable here (it -/// selects/copies without dropping you back to normal); Ctrl-b exits. +/// selects/copies without dropping you back to normal); the configured Ctrl key exits. pub const Mode = enum { normal, insert, tty }; /// A modal line selection (helix `x` extends the selection down line by line). @@ -38,6 +38,7 @@ fn colOf(text: []const u8, needle: []const u8) ?usize { pub fn main(init: std.process.Init) !void { const gpa = init.gpa; + const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); if (args.len != 2) { std.debug.print("usage: {s} <pardes-exe>\n", .{args[0]}); @@ -299,6 +300,8 @@ pub fn main(init: std.process.Init) !void { try hs.pump(800); // build the run try hs.send("\x1b[<2;70;19M\x1b[<2;70;19m"); // right-click on the typed path (+1 row: global tagbar) try hs.expectWaitFor("ZZDEEPMARK", 12000, "right-click 'path:150' did not open the file scrolled to line 150"); + try hs.expectContains("Save Del", "file pane tag did not include the default Save command"); + try hs.expectNotContains("Delcol", "Delcol is still shown in the default pane tag"); // 9. Modal modes on the active FILE pane (left open & focused by step 8): // `i` -> INSERT (tag shows "in"), Esc -> NORMAL ("nm"). Validates the @@ -359,6 +362,25 @@ pub fn main(init: std.process.Init) !void { try hs.pump(800); try hs.expectNotContains("ZZVSELMARK", "U (redo) did not re-apply the delete"); + // 10c. Save writes the current file pane back to disk. Add a durable marker, + // middle-click the default "Save" command in the file tag, then read the + // real file from /tmp and confirm the edit landed there. + try hs.send("o"); // open a new line below -> insert + try hs.pump(500); + try hs.send("ZZSAVEDISK"); + try hs.pump(500); + try hs.send("\x1b"); // -> normal + try hs.pump(500); + try hs.expectContains("ZZSAVEDISK", "save setup: marker not visible in the file pane"); + try hs.send("\x1b[<1;31;2M\x1b[<1;31;2m"); // middle-click "Save" in the file tag + try hs.pump(1000); + const saved = try std.Io.Dir.cwd().readFileAlloc(io, "/tmp/pardesE2Efile.txt", gpa, .limited(1 << 20)); + defer gpa.free(saved); + if (std.mem.indexOf(u8, saved, "ZZSAVEDISK") == null) { + hs.dump("Save command did not write the edited file content to disk"); + return error.SaveDidNotWriteDisk; + } + // 11. The Tutor builtin now lives in the GLOBAL tagbar (top row, full width): // "Kill Newcol Tutor Debug". Clicking "Tutor" there spawns the tutor as a // file pane. The tutor is embedded in the binary (@embedFile), so its @@ -382,6 +404,34 @@ pub fn main(init: std.process.Init) !void { try hs2.expectWaitFor("ZZTTYFLAG", 8000, "--tty single shell did not forward input to the shell"); } + // 12b. -h/--help print usage and exit before entering the TUI. + { + var hsh = try Harness.initArgs(gpa, exe.ptr, 12, 80, "-h"); + defer hsh.deinit(); + try hsh.expectWaitFor("Usage: pardes", 4000, "-h did not print usage"); + try hsh.expectContains("--tty-toggle", "-h help did not mention --tty-toggle"); + } + { + var hsh2 = try Harness.initArgs(gpa, exe.ptr, 12, 80, "--help"); + defer hsh2.deinit(); + try hsh2.expectWaitFor("Usage: pardes", 4000, "--help did not print usage"); + } + + // 12c. The tty toggle can be moved off Ctrl-b. With --tty-toggle=g, Ctrl-b + // should no longer enter tty; Ctrl-g should. + { + var hsg = try Harness.initArgs(gpa, exe.ptr, 24, 80, "--tty-toggle=g"); + defer hsg.deinit(); + try hsg.expectWaitFor("nm /", 6000, "--tty-toggle=g app did not start in normal mode"); + try hsg.send("\x02"); // Ctrl-b: now just a normal-mode key, not the tty toggle + try hsg.pump(600); + try hsg.expectNotContains("sy /", "Ctrl-b still toggled tty after --tty-toggle=g"); + try hsg.send("\x07"); // Ctrl-g: configured tty toggle + try hsg.expectWaitFor("sy /", 6000, "Ctrl-g did not toggle tty after --tty-toggle=g"); + try hsg.send("\x07"); + try hsg.expectWaitFor("nm /", 6000, "Ctrl-g did not toggle tty back to normal"); + } + // 13. Terminal char-mode editing end to end (fresh instance for a clean pane): // insert an overlay run, backspace part of it, Ctrl-b to tty (overlay hidden) // and back (overlay returns), then u (undo the whole insert) / U (redo). @@ -25,7 +25,8 @@ This tutor is in THREE parts, ordered by what's most different from editors you may know: PART 1 — the MOUSE. Acme's three buttons; nothing like Vim/Helix. - PART 2 — the TTY. A terminal is just a pane; Ctrl-b drops into it. + PART 2 — the TTY. A terminal is just a pane; Ctrl-b drops into it + by default. PART 3 — the KEYS. Helix-style modal (with the Pardes differences). PRACTICE BLOCKS (part 3): the "# keys:" line lists keystrokes @@ -71,9 +72,11 @@ Tab to execute or Enter to look — the acme chords on the keyboard. THE TAG: each pane has a one-line tag: its MODE (nm/in/sy) + directory - or file path + the builtins "Del Delcol". Middle-click "Del" to close - the window, "Delcol" to close its column. A SEPARATE bar across the top - of the screen holds the window-agnostic builtins: + or file path + builtins. File panes show "Save Del" by default; Save + writes the current file to disk, Del closes the window. Terminal and + image panes show "Del". "Delcol" still exists as a command: type it in + a tag or body and execute it to close the whole column. A SEPARATE bar + across the top of the screen holds the window-agnostic builtins: Kill Newcol Tutor Debug Colors NextColor Middle-click "Newcol" for a new column, "Tutor" to spawn this tutor, "Kill" to quit; Debug/Colors/NextColor toggle the stats overlay, the @@ -115,10 +118,11 @@ go straight to the pty as terminal input. ESC: insert -> normal. (Esc never reaches tty.) - Ctrl-b: toggles TTY on a terminal — the ONLY way in or out. Entering - is tty-native: if the shell is at a prompt and your modal - cursor sits on the input line, it first moves the shell's REAL - cursor to that spot — + Ctrl-b: toggles TTY on a terminal by default — the ONLY way in or out. + Use `pardes --tty-toggle=g` (or another letter) to make Ctrl-g + the toggle instead. Entering is tty-native: if the shell is at + a prompt and your modal cursor sits on the input line, it first + moves the shell's REAL cursor to that spot — - empty input -> shell cursor at the prompt START - typed text -> shell cursor at/after the char you were on via the shell's OSC 133 semantic prompt (ghostty promptClickMove; @@ -129,9 +133,9 @@ chords on the keyboard: Enter = "look" (open the path under the cursor), Tab = "execute". Both act on the selection if one is active. - So the flow is: navigate the hidden-prompt page in normal, hit Ctrl-b - where you want to keep typing, and you're IN the live shell at that - spot. No separate "terminal mode cursor" to reconcile. + So the flow is: navigate the hidden-prompt page in normal, hit the tty + toggle where you want to keep typing, and you're IN the live shell at + that spot. No separate "terminal mode cursor" to reconcile. (The cursor positioning is enterTty's promptClickMove; it needs a live shell at a prompt. Mouse/tty behavior is exercised by the e2e suite.) @@ -150,7 +154,8 @@ - NO verb+noun (Vim's dw, cw). Motions only MOVE. To delete a word, select it (`v` then motions, or `x` for whole lines) then `d`. - ESC never reaches tty; it only does insert -> normal. Dropping a - terminal into the live shell is Ctrl-b (see Part 2). + terminal into the live shell is the tty toggle (Ctrl-b by default; + see Part 2). - Enter / Tab in normal mirror the mouse: Enter = look (open the path under the cursor), Tab = execute. `u` undo, `U` redo. - Editing a file pane MUTATES real content; a terminal pane yanks @@ -302,8 +307,9 @@ = 3.5 LINE SELECTION + EDIT: x d c y p = ----------------------------------------------------------------- - The Pardes difference. No `v` select-mode: `x` grows a line selection - downward; d/c/y act on it. + The Pardes difference is LINE-first selection: `x` grows a line + selection downward; d/c/y act on it. `v` is still available for a + character range when you need one. `x` start/extend a line selection `d` delete the selected lines (yanked) @@ -367,8 +373,9 @@ zt / zz / zb scroll so the cursor is at top/center/bottom Ctrl-d / Ctrl-u half-page down / up - Ctrl-f full page down. (Ctrl-b pages up on a file pane, - but on a terminal it's the tty toggle — see Part 2.) + Ctrl-f full page down. (Ctrl-b pages up on a file pane; + on a terminal it is the default tty toggle. If you + start with --tty-toggle=g, Ctrl-g takes that role.) Ctrl-w h/j/k/l focus the pane left/down/up/right Alt-n new terminal below the active one Alt-c move the active terminal into a fresh column @@ -382,9 +389,9 @@ MOUSE (acme): L select/focus+pin M execute R look(open) select-then-act: middle-drag, or v/x then Tab - tag = mode + dir + "Del Delcol"; top bar = Kill Newcol - Tutor Debug Colors NextColor - TTY: a terminal IS a pane; Ctrl-b toggles the live shell, + file tag = mode + path + "Save Del"; other tags show Del + top bar = Kill Newcol Tutor Debug Colors NextColor + TTY: a terminal IS a pane; Ctrl-b toggles the live shell by default, landing its cursor where you navigated (prompt start if the input is empty, mid-text otherwise) KEYS (helix): h j k l w b e 0 $ ^ gg G v x d c y p i a I A o O @@ -394,8 +401,8 @@ vs Helix: no multi-cursor; selection is LINE-first (x), plus v chars. vs Vim: no verb+noun (dw); motions only move; Esc never reaches tty. vs both: a terminal is just a pane; the same keys edit text and - navigate the shell screen, and Ctrl-b reuses the shell's - own prompt to position you precisely. + navigate the shell screen, and the tty toggle reuses the + shell's own prompt to position you precisely. To spawn THIS tutor again from anywhere: middle-click "Tutor" in the top bar (next to Kill / Newcol). |
