diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-02 21:41:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 81992cc92743a98066ee1a9025bc74f91cf0e967 (patch) | |
| tree | 12c07f68f8ad030f6c6f8a2b3415998d0a87eee7 /paint.zig | |
| parent | bead9f0b78380d1428f40fdab339c1c8f88fa626 (diff) | |
| download | pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.tar.gz pardes-81992cc92743a98066ee1a9025bc74f91cf0e967.zip | |
storage and replay
Diffstat (limited to 'paint.zig')
| -rw-r--r-- | paint.zig | 324 |
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; +} |
