summaryrefslogtreecommitdiff
path: root/paint.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-02 21:41:13 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit81992cc92743a98066ee1a9025bc74f91cf0e967 (patch)
tree12c07f68f8ad030f6c6f8a2b3415998d0a87eee7 /paint.zig
parentbead9f0b78380d1428f40fdab339c1c8f88fa626 (diff)
downloadpardes-81992cc92743a98066ee1a9025bc74f91cf0e967.tar.gz
pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.zip
storage and replay
Diffstat (limited to 'paint.zig')
-rw-r--r--paint.zig324
1 files changed, 324 insertions, 0 deletions
diff --git a/paint.zig b/paint.zig
new file mode 100644
index 00000000..d8526cb4
--- /dev/null
+++ b/paint.zig
@@ -0,0 +1,324 @@
+const std = @import("std");
+const ui = @import("ui");
+const app_config = @import("app_config");
+const ghostty_vt = @import("ghostty-vt");
+const ts = @import("tree-sitter");
+const ts_queries = @import("ts_queries");
+
+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;
+
+pub const accent = if (app_config.app_kind == .replay) [3]u8{ 0xc8, 0x58, 0x58 } else [3]u8{ 0x6e, 0x4f, 0xd0 };
+pub const accent_dim = if (app_config.app_kind == .replay) [3]u8{ 0x54, 0x38, 0x38 } else [3]u8{ 0x38, 0x3c, 0x54 };
+const dark_tag_bg = if (app_config.app_kind == .replay) [3]u8{ 0x3e, 0x2a, 0x2a } else [3]u8{ 0x2c, 0x2a, 0x3e };
+
+pub const Theme = struct {
+ name: []const u8,
+ bg: ?[3]u8,
+ fg: ?[3]u8,
+ tag_bg: [3]u8,
+ tag_fg: [3]u8,
+ kw: [3]u8,
+ str: [3]u8,
+ num: [3]u8,
+ comment: [3]u8,
+ lineno: [3]u8,
+ scroll_track: [3]u8,
+ scroll_thumb: [3]u8,
+ palette: ?[16][3]u8,
+};
+
+pub const themes = [_]Theme{
+ .{
+ .name = "dark",
+ .bg = null,
+ .fg = null,
+ .tag_bg = dark_tag_bg,
+ .tag_fg = .{ 0x96, 0x96, 0x96 },
+ .kw = .{ 0xcc, 0x70, 0xd8 },
+ .str = .{ 0x8a, 0xb8, 0x7a },
+ .num = .{ 0xd0, 0xc0, 0x70 },
+ .comment = .{ 0xf0, 0xf0, 0xf0 },
+ .lineno = .{ 0x5a, 0x5a, 0x5a },
+ .scroll_track = .{ 0x2a, 0x2a, 0x2a },
+ .scroll_thumb = .{ 0x52, 0x52, 0x52 },
+ .palette = null,
+ },
+ .{
+ .name = "acme",
+ .bg = .{ 0xff, 0xff, 0xea },
+ .fg = .{ 0x00, 0x00, 0x00 },
+ .tag_bg = .{ 0xea, 0xff, 0xff },
+ .tag_fg = .{ 0x00, 0x00, 0x00 },
+ .kw = .{ 0x88, 0x22, 0x99 },
+ .str = .{ 0x00, 0x77, 0x33 },
+ .num = .{ 0xa0, 0x52, 0x00 },
+ .comment = .{ 0x55, 0x55, 0x55 },
+ .lineno = .{ 0x99, 0x99, 0x80 },
+ .scroll_track = .{ 0x99, 0x99, 0x4c },
+ .scroll_thumb = .{ 0xff, 0xff, 0xea },
+ .palette = .{
+ .{ 0x00, 0x00, 0x00 }, .{ 0xaa, 0x22, 0x22 }, .{ 0x22, 0x80, 0x22 }, .{ 0x88, 0x66, 0x00 },
+ .{ 0x22, 0x44, 0xbb }, .{ 0x99, 0x33, 0x99 }, .{ 0x11, 0x88, 0x88 }, .{ 0x55, 0x55, 0x55 },
+ .{ 0x88, 0x88, 0x88 }, .{ 0xcc, 0x33, 0x33 }, .{ 0x33, 0xaa, 0x33 }, .{ 0xaa, 0x77, 0x00 },
+ .{ 0x33, 0x55, 0xdd }, .{ 0xbb, 0x44, 0xbb }, .{ 0x22, 0xaa, 0xaa }, .{ 0x00, 0x00, 0x00 },
+ },
+ },
+};
+
+var theme_idx: usize = 0;
+
+pub fn T() *const Theme {
+ return &themes[theme_idx];
+}
+
+pub fn nextTheme() void {
+ theme_idx = (theme_idx + 1) % themes.len;
+}
+
+pub fn currentThemeName() []const u8 {
+ return T().name;
+}
+
+pub fn setThemeByName(name: []const u8) bool {
+ for (themes, 0..) |theme, i| {
+ if (std.mem.eql(u8, theme.name, name)) {
+ theme_idx = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+pub fn rgbColor(c: [3]u8) ui.Color {
+ return .{ .rgb = c };
+}
+
+pub fn colorOpt(c: ?[3]u8) ui.Color {
+ return if (c) |x| .{ .rgb = x } else .default;
+}
+
+pub const Syn = enum(u8) { none, keyword, string, number, comment };
+
+pub const SynStyle = struct {
+ fg: [3]u8,
+ bold: bool,
+};
+
+pub fn synStyle(s: Syn) ?SynStyle {
+ return switch (s) {
+ .none => null,
+ .keyword => .{ .fg = T().kw, .bold = true },
+ .string => .{ .fg = T().str, .bold = false },
+ .number => .{ .fg = T().num, .bold = false },
+ .comment => .{ .fg = T().comment, .bold = true },
+ };
+}
+
+fn synFor(name: []const u8) Syn {
+ for ([_]struct { []const u8, Syn }{
+ .{ "comment", .comment },
+ .{ "string", .string },
+ .{ "character", .string },
+ .{ "number", .number },
+ .{ "float", .number },
+ .{ "boolean", .number },
+ .{ "keyword", .keyword },
+ .{ "include", .keyword },
+ .{ "operator", .keyword },
+ .{ "conditional", .keyword },
+ .{ "repeat", .keyword },
+ }) |m| {
+ if (std.mem.indexOf(u8, name, m[0]) != null) return m[1];
+ }
+ 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;
+}
+
+pub fn highlightFile(gpa: std.mem.Allocator, path: []const u8, content: []const u8) ![]u8 {
+ const ext = std.fs.path.extension(path);
+ var lang: *const ts.Language = undefined;
+ var query_src: []const u8 = undefined;
+ 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 (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 (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 &.{};
+
+ const styles = try gpa.alloc(u8, content.len);
+ errdefer gpa.free(styles);
+ @memset(styles, 0);
+
+ const parser = ts.Parser.create();
+ defer parser.destroy();
+ parser.setLanguage(lang) catch return styles;
+ const tree = parser.parseString(content, null) orelse return styles;
+ defer tree.destroy();
+
+ var error_offset: u32 = 0;
+ const query = ts.Query.create(lang, query_src, &error_offset) catch return styles;
+ defer query.destroy();
+ const cursor = ts.QueryCursor.create();
+ defer cursor.destroy();
+ cursor.exec(query, tree.rootNode());
+
+ while (cursor.nextMatch()) |match| {
+ for (match.captures) |cap| {
+ const syn = synFor(query.captureNameForId(cap.index) orelse "");
+ if (syn == .none) continue;
+ var b: usize = cap.node.startByte();
+ const end = @min(@as(usize, cap.node.endByte()), styles.len);
+ while (b < end) : (b += 1) styles[b] = @intFromEnum(syn);
+ }
+ }
+ return styles;
+}
+
+fn palColor(idx: u8) ui.Color {
+ if (T().palette) |p| if (idx < 16) return rgbColor(p[idx]);
+ return .{ .index = idx };
+}
+
+fn ghostColor(color: ghostty_vt.Style.Color, is_bg: bool) ui.Color {
+ return switch (color) {
+ .none => colorOpt(if (is_bg) T().bg else T().fg),
+ .palette => |idx| palColor(idx),
+ .rgb => |rgb| .{ .rgb = .{ rgb.r, rgb.g, rgb.b } },
+ };
+}
+
+pub fn cellStyle(ci: ghostty_vt.PageList.Cell) ui.Style {
+ const style = ci.style();
+ var vs: ui.Style = .{
+ .fg = ghostColor(style.fg_color, false),
+ .bg = ghostColor(style.bg_color, true),
+ .bold = style.flags.bold,
+ .dim = style.flags.faint,
+ .italic = style.flags.italic,
+ .blink = style.flags.blink,
+ .reverse = style.flags.inverse,
+ .invisible = style.flags.invisible,
+ .strikethrough = style.flags.strikethrough,
+ .ul_style = switch (style.flags.underline) {
+ .none => .off,
+ .single => .single,
+ .double => .double,
+ .curly => .curly,
+ .dotted => .dotted,
+ .dashed => .dashed,
+ },
+ };
+ switch (ci.cell.content_tag) {
+ .bg_color_palette => vs.bg = palColor(ci.cell.content.color_palette.data),
+ .bg_color_rgb => {
+ const rgb = ci.cell.content.color_rgb;
+ vs.bg = .{ .rgb = .{ rgb.r, rgb.g, rgb.b } };
+ },
+ else => {},
+ }
+ return vs;
+}