summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-01 19:56:21 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitcc806b11ba880d1bd326e3f1f9e69f7539c22395 (patch)
tree3b200ed4e46e2c80d2762ae2efd45bf87413133d /main.zig
parentf3618a5a3151e02db2c1e2280ab42d48c35eeba9 (diff)
downloadpardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.tar.gz
pardes-cc806b11ba880d1bd326e3f1f9e69f7539c22395.zip
more treesitter grammars
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig244
1 files changed, 213 insertions, 31 deletions
diff --git a/main.zig b/main.zig
index bf064f66..33bc5345 100644
--- a/main.zig
+++ b/main.zig
@@ -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);