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 /main.zig | |
| parent | f3618a5a3151e02db2c1e2280ab42d48c35eeba9 (diff) | |
| download | pardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.tar.gz pardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.zip | |
more treesitter grammars
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 244 |
1 files changed, 213 insertions, 31 deletions
@@ -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); |
