From 116dc232a89158d9ff910b8fb99ac2db7dcc3cf9 Mon Sep 17 00:00:00 2001 From: soup Date: Fri, 17 Jan 2025 11:41:35 -0500 Subject: [PATCH] [pritty] Initial commit --- .gitignore | 1 + pritty/colors/flexoki.toml | 245 +++++++++++++++++++++++++ pritty/deno.jsonc | 24 +++ pritty/deno.lock | 23 +++ pritty/scripts/colortest-16.sh | 32 ++++ pritty/src/main.ts | 326 +++++++++++++++++++++++++++++++++ 6 files changed, 651 insertions(+) create mode 100644 pritty/colors/flexoki.toml create mode 100644 pritty/deno.jsonc create mode 100644 pritty/deno.lock create mode 100755 pritty/scripts/colortest-16.sh diff --git a/.gitignore b/.gitignore index f2449a5..ab62a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/build/* **/target/* +pritty/outputs/* diff --git a/pritty/colors/flexoki.toml b/pritty/colors/flexoki.toml new file mode 100644 index 0000000..581c9dd --- /dev/null +++ b/pritty/colors/flexoki.toml @@ -0,0 +1,245 @@ +name = "flexoki" + +[palette] +black = "#100F0F" +paper = "#FFFCF0" + +base-50 = "#F2F0E5" +base-100 = "#E6E4D9" +base-150 = "#DAD8CE" +base-200 = "#CECDC3" +base-300 = "#B7B5AC" +base-400 = "#9F9D96" +base-500 = "#878580" +base-600 = "#6F6E69" +base-700 = "#575653" +base-800 = "#403E3C" +base-850 = "#343331" +base-900 = "#282726" +base-950 = "#1C1B1A" + +red-50 = "#FFE1D5" +red-100 = "#FFCABB" +red-150 = "#FDB2A2" +red-200 = "#F89A8A" +red-300 = "#E8705F" +red-400 = "#D14D41" +red-500 = "#C03E35" +red-600 = "#AF3029" +red-700 = "#942822" +red-800 = "#6C201C" +red-850 = "#551B18" +red-900 = "#3E1715" +red-950 = "#261312" + +orange-50 = "#FFE7CE" +orange-100 = "#FED3AF" +orange-150 = "#FCC192" +orange-200 = "#F9AE77" +orange-300 = "#EC8B49" +orange-400 = "#DA702C" +orange-500 = "#CB6120" +orange-600 = "#BC5215" +orange-700 = "#9D4310" +orange-800 = "#71320D" +orange-850 = "#59290D" +orange-900 = "#40200D" +orange-950 = "#27180E" + +yellow-50 = "#FAEEC6" +yellow-100 = "#F6E2A0" +yellow-150 = "#F1D67E" +yellow-200 = "#ECCB60" +yellow-300 = "#DFB431" +yellow-400 = "#D0A215" +yellow-500 = "#BE9207" +yellow-600 = "#AD8301" +yellow-700 = "#8E6B01" +yellow-800 = "#664D01" +yellow-850 = "#503D02" +yellow-900 = "#3A2D04" +yellow-950 = "#241E08" + +green-50 = "#EDEECF" +green-100 = "#DDE2B2" +green-150 = "#CDD597" +green-200 = "#BEC97E" +green-300 = "#A0AF54" +green-400 = "#879A39" +green-500 = "#768D21" +green-600 = "#66800B" +green-700 = "#536907" +green-800 = "#3D4C07" +green-850 = "#313D07" +green-900 = "#252D09" +green-950 = "#1A1E0C" + +cyan-50 = "#DDF1E4" +cyan-100 = "#BFE8D9" +cyan-150 = "#A2DECE" +cyan-200 = "#87D3C3" +cyan-300 = "#5ABDAC" +cyan-400 = "#3AA99F" +cyan-500 = "#2F968D" +cyan-600 = "#24837B" +cyan-700 = "#1C6C66" +cyan-800 = "#164F4A" +cyan-850 = "#143F3C" +cyan-900 = "#122F2C" +cyan-950 = "#101F1D" + +blue-50 = "#E1ECEB" +blue-100 = "#C6DDE8" +blue-150 = "#ABCFE2" +blue-200 = "#92BFDB" +blue-300 = "#66A0C8" +blue-400 = "#4385BE" +blue-500 = "#3171B2" +blue-600 = "#205EA6" +blue-700 = "#1A4F8C" +blue-800 = "#163B66" +blue-850 = "#133051" +blue-900 = "#12253B" +blue-950 = "#101A24" + +purple-50 = "#F0EAEC" +purple-100 = "#E2D9E9" +purple-150 = "#D3CAE6" +purple-200 = "#C4B9E0" +purple-300 = "#A699D0" +purple-400 = "#8B7EC8" +purple-500 = "#735EB5" +purple-600 = "#5E409D" +purple-700 = "#4F3685" +purple-800 = "#3C2A62" +purple-850 = "#31234E" +purple-900 = "#261C39" +purple-950 = "#1A1623" + +magenta-50 = "#FEE4E5" +magenta-100 = "#FCCFDA" +magenta-150 = "#F9B9CF" +magenta-200 = "#F4A4C2" +magenta-300 = "#E47DA8" +magenta-400 = "#CE5D97" +magenta-500 = "#B74583" +magenta-600 = "#A02F6F" +magenta-700 = "#87285E" +magenta-800 = "#641F46" +magenta-850 = "#4F1B39" +magenta-900 = "#39172B" +magenta-950 = "#24131D" + +[dark.backgrounds] +primary = "@black" +secondary = "@base-950" + +selection = "@base-800" +status-bar-active = "@base-900" +status-bar-inactive = "@black" +cursor = "@base-200" + +[dark.borders] +default = "@base-900" +hovered = "@base-850" +active = "@base-800" + +[dark.text] +primary = "@base-200" +muted = "@base-500" +faint = "@base-700" +error = "@red-400" +warning = "@orange-400" +success = "@green-400" +link = "@cyan-400" + +selection = "@base-200" +cursor = "@black" + +[dark.syntax] +operator = "@base-500" +import = "@red-400" +function = "@orange-400" +constant = "@yellow-400" +keyword = "@green-400" +string = "@cyan-400" +identifier = "@blue-400" +number = "@purple-400" +comment = "@base-700" +macro = "@magenta-400" +other = "@magenta-400" + +[dark.terminal-16] +bright-black = "@base-600" +bright-red = "@red-400" +bright-green = "@green-400" +bright-yellow = "@yellow-400" +bright-blue = "@blue-400" +bright-magenta = "@magenta-400" +bright-cyan = "@cyan-400" +bright-white = "@base-200" +black = "@black" +red = "@red-600" +green = "@green-600" +yellow = "@yellow-600" +blue = "@blue-600" +magenta = "@magenta-600" +cyan = "@cyan-600" +white = "@base-500" + +[light.backgrounds] +primary = "@paper" +secondary = "@base-50" + +selection = "@base-200" +status-bar-active = "@base-100" +status-bar-inactive = "@paper" +cursor = "@black" + +[light.borders] +default = "@base-900" +hovered = "@base-850" +active = "@base-800" + +[light.text] +primary = "@black" +muted = "@base-600" +faint = "@base-300" +error = "@red-600" +warning = "@orange-600" +success = "@green-600" +link = "@cyan-600" + +selection = "@black" +cursor = "@black" + +[light.syntax] +operator = "@base-500" +import = "@red-600" +function = "@orange-600" +constant = "@yellow-600" +keyword = "@green-600" +string = "@cyan-600" +identifier = "@blue-600" +number = "@purple-600" +comment = "@base-300" +macro = "@magenta-600" +other = "@magenta-600" + +[light.terminal-16] +bright-black = "@base-600" +red = "@red-400" +green = "@green-400" +yellow = "@yellow-400" +blue = "@blue-400" +magenta = "@magenta-400" +cyan = "@cyan-400" +bright-white = "@base-200" +black = "@black" +bright-red = "@red-600" +bright-green = "@green-600" +bright-yellow = "@yellow-600" +bright-blue = "@blue-600" +bright-magenta = "@magenta-600" +bright-cyan = "@cyan-600" +white = "@base-500" diff --git a/pritty/deno.jsonc b/pritty/deno.jsonc new file mode 100644 index 0000000..45a964d --- /dev/null +++ b/pritty/deno.jsonc @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "checkJs": true, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "deno.window", + "deno.unstable" + ] + }, + "tasks": { + "check": "deno check src/main.ts" + }, + "imports": { + "@std/toml": "jsr:@std/toml@^1.0.2" + }, + "fmt": { + "semiColons": true, + "singleQuote": true, + "useTabs": true, + "lineWidth": 90 + } +} diff --git a/pritty/deno.lock b/pritty/deno.lock new file mode 100644 index 0000000..1b8f912 --- /dev/null +++ b/pritty/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/collections@^1.0.9": "1.0.9", + "jsr:@std/toml@^1.0.2": "1.0.2" + }, + "jsr": { + "@std/collections@1.0.9": { + "integrity": "4f58104ead08a04a2199374247f07befe50ba01d9cca8cbb23ab9a0419921e71" + }, + "@std/toml@1.0.2": { + "integrity": "5892ba489c5b512265a384238a8fe8dddbbb9498b4b210ef1b9f0336a423a39b", + "dependencies": [ + "jsr:@std/collections" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/toml@^1.0.2" + ] + } +} diff --git a/pritty/scripts/colortest-16.sh b/pritty/scripts/colortest-16.sh new file mode 100755 index 0000000..2e3a2d8 --- /dev/null +++ b/pritty/scripts/colortest-16.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Default 16 ANSI color names +color_names=( + "Black" + "Red" + "Green" + "Yellow" + "Blue" + "Magenta" + "Cyan" + "White" + "Bright Black" + "Bright Red" + "Bright Green" + "Bright Yellow" + "Bright Blue" + "Bright Magenta" + "Bright Cyan" + "Bright White" +) + +echo "16-color palette with indices & names:" +for c in {0..15}; do + # Print the color block + printf "\033[38;5;%sm%2d: %-13s\033[0m " "$c" "$c" "${color_names[$c]}" + # New line every 4 colors + if (( (c+1) % 4 == 0 )); then + echo + fi +done +echo diff --git a/pritty/src/main.ts b/pritty/src/main.ts index e69de29..692720c 100644 --- a/pritty/src/main.ts +++ b/pritty/src/main.ts @@ -0,0 +1,326 @@ +import * as toml from '@std/toml'; + +type OutputType = 'ghostty' | 'emacs'; +type Output = { + outputType: OutputType; + dir: string; +}; + +const USAGE = ` +pritty ... +`; + +function printUsageAndDie() { + console.log(USAGE); + Deno.exit(1); +} + +type RawPalette = { + [k: string]: string | RawPalette; +}; +type Palette = Record; +function flattenPalette( + input: RawPalette, + prefix: string = '', + output: Record = {}, +) { + for (const key in input) { + const value = input[key]; + if (typeof value === 'object') { + flattenPalette(value, `${prefix}${key}.`, output); + } else { + output[`${prefix}${key}`] = value; + } + } + + return output; +} + +type TableEntryColor = `#${string}`; +type TableEntryRef = `@${string}`; +type TableEntry = TableEntryColor | TableEntryRef; +type Tables = Partial<{ + backgrounds: Partial<{ + primary: TableEntry; + secondary: TableEntry; + 'status-bar-active': TableEntry; + 'status-bar-inactive': TableEntry; + }>; + borders: Partial<{ + default: TableEntry; + hovered: TableEntry; + active: TableEntry; + }>; + text: Partial<{ + primary: TableEntry; + muted: TableEntry; + faint: TableEntry; + error: TableEntry; + warning: TableEntry; + success: TableEntry; + link: TableEntry; + }>; + syntax: Partial<{ + operator: TableEntry; + import: TableEntry; + function: TableEntry; + constant: TableEntry; + keyword: TableEntry; + string: TableEntry; + identifier: TableEntry; + number: TableEntry; + macro: TableEntry; + other: TableEntry; + }>; + 'terminal-16': Partial<{ + black: TableEntry; + red: TableEntry; + green: TableEntry; + yellow: TableEntry; + blue: TableEntry; + magenta: TableEntry; + cyan: TableEntry; + white: TableEntry; + 'bright-black': TableEntry; + 'bright-red': TableEntry; + 'bright-green': TableEntry; + 'bright-yellow': TableEntry; + 'bright-blue': TableEntry; + 'bright-magenta': TableEntry; + 'bright-cyan': TableEntry; + 'bright-white': TableEntry; + }>; +}>; + +function get(entry: TableEntry | undefined, palette: Palette) { + if (entry === undefined) { + return undefined; + } + + if (entry.startsWith('@')) { + const c = palette[entry.substring(1)]; + if (c === undefined) { + throw new Error(`Undefined reference ${entry}`); + } + + return c; + } + + return entry; +} + +function bindGet(palette: Palette) { + return (entry: TableEntry | undefined) => get(entry, palette); +} + +async function generateGhostty( + dir: string, + name: string, + variantName: string, + tables: Tables, + palette: Palette, +) { + let output = ''; + const get = bindGet(palette); + const w = (prop: string, v: string | undefined) => { + if (v !== undefined) { + output += `${prop} = ${v}\n`; + } + }; + + const p = (index: number, v: string | undefined) => { + if (v !== undefined) { + output += `palette = ${index}=${v}\n`; + } + }; + + w('background', get(tables['backgrounds']?.['primary'])); + w('foreground', get(tables['text']?.['primary'])); + w('selection-background', get(tables['backgrounds']?.['selection'])); + w('selection-foreground', get(tables['text']?.['selection'])); + + p(0, get(tables['terminal-16']?.['black'])); + p(1, get(tables['terminal-16']?.['red'])); + p(2, get(tables['terminal-16']?.['green'])); + p(3, get(tables['terminal-16']?.['yellow'])); + p(4, get(tables['terminal-16']?.['blue'])); + p(5, get(tables['terminal-16']?.['magenta'])); + p(6, get(tables['terminal-16']?.['cyan'])); + p(7, get(tables['terminal-16']?.['white'])); + p(8, get(tables['terminal-16']?.['bright-black'])); + p(9, get(tables['terminal-16']?.['bright-red'])); + p(10, get(tables['terminal-16']?.['bright-green'])); + p(11, get(tables['terminal-16']?.['bright-yellow'])); + p(12, get(tables['terminal-16']?.['bright-blue'])); + p(13, get(tables['terminal-16']?.['bright-magenta'])); + p(14, get(tables['terminal-16']?.['bright-cyan'])); + p(15, get(tables['terminal-16']?.['bright-white'])); + + const path = `${dir}/${name}-${variantName}.ghostty`; + await Deno.writeTextFile(path, output); +} + +async function generateEmacs( + dir: string, + name: string, + variantName: string, + tables: Tables, + palette: Palette, +) { + let output = `(deftheme ${name}-${variantName})\n`; + output += `(custom-theme-set-faces '${name}-${variantName}\n`; + + const get = bindGet(palette); + const f = (faceName: string, props: Record) => { + let out = ''; + for (const [prop, value] of Object.entries(props)) { + if (value !== undefined) { + if (out === '') { + out += ` \`(${faceName} ((t (`; + } + + if (typeof value === 'string') { + out += `:${prop} "${value}" `; + } else if (typeof value === 'boolean') { + const v = value ? 't' : 'nil'; + out += `:${prop} ${v} `; + } + } + } + + if (out !== '') { + out += '))))\n'; + } + + output += out; + }; + + f('default', { + foreground: get(tables['text']?.['primary']), + background: get(tables['backgrounds']?.['primary']), + }); + f('mode-line-active', { + foreground: get(tables['text']?.['primary']), + background: get(tables['backgrounds']?.['status-bar-active']), + box: true, + }); + f('mode-line-inactive', { + foreground: get(tables['text']?.['primary']), + background: get(tables['backgrounds']?.['status-bar-inactive']), + box: true, + }); + f('cursor', { + foreground: get(tables['text']?.['cursor']), + background: get(tables['backgrounds']?.['cursor']), + }); + f('region', { + foreground: get(tables['text']?.['selection']), + background: get(tables['backgrounds']?.['selection']), + }); + f('vertico-current', { + foreground: get(tables['text']?.['selection']), + background: get(tables['backgrounds']?.['selection']), + }); + f('whitespace-tab', { foreground: get(tables['text']?.['faint']) }); + + const s = (faceName: string, syntaxEntry: string) => { + const v = get(tables['syntax']?.[syntaxEntry]) ?? get(tables['text']?.[syntaxEntry]); + + f(faceName, { foreground: v }); + }; + const syntaxMappings = [ + ['font-lock-type-face', 'identifier'], + ['font-lock-constant-face', 'constant'], + ['font-lock-builtin-face', 'other'], + ['font-lock-preprocessor-face', 'macro'], + ['font-lock-doc-face', 'comment'], + ['font-lock-function-name-face', 'function'], + ['font-lock-keyword-face', 'keyword'], + ['font-lock-variable-name-face', 'identifier'], + ['font-lock-string-face', 'string'], + ['font-lock-variable-use-face', 'identifier'], + ['font-lock-negation-char-face', 'operator'], + ['font-lock-property-use-face', 'identifier'], + ['font-lock-punctuation-face', 'operator'], + ['font-lock-doc-markup-face', 'comment'], + ['font-lock-comment-delimiter-face', 'comment'], + ['font-lock-delimiter-face', 'operator'], + ['font-lock-number-face', 'constant'], + ['font-lock-function-call-face', 'function'], + ['font-lock-comment-face', 'comment'], + ['font-lock-operator-face', 'operator'], + ['font-lock-property-name-face', 'identifier'], + ['font-lock-misc-punctuation-face', 'operator'], + ['font-lock-escape-face', 'other'], + + ['font-lock-warning-face', 'warning'], + ['font-lock-error-face', 'error'], + ] as const; + + for (const [faceName, syntaxEntry] of syntaxMappings) { + s(faceName, syntaxEntry); + } + + output += `)\n`; + + const path = `${dir}/${name}-${variantName}-theme.el`; + await Deno.writeTextFile(path, output); +} + +async function generateOutput( + output: Output, + name: string, + variants: Record, + palette: Palette, +) { + const { outputType, dir } = output; + await Deno.mkdir(dir, { recursive: true }); + + const genFunctions = { + 'ghostty': generateGhostty, + 'emacs': generateEmacs, + }; + const genFunction = genFunctions[outputType]; + if (genFunction === undefined) { + throw new Error(`Unsupported output type "${outputType}"`); + } + + for (const [variantName, tables] of Object.entries(variants)) { + await genFunction(dir, name, variantName, tables, palette); + } +} + +async function main() { + const inputPath = Deno.args[1]; + if (inputPath === undefined) { + printUsageAndDie(); + } + + const outputs_ = Deno.args.slice(2); + if (outputs_.length === 0) { + printUsageAndDie(); + } + + const outputs: Output[] = outputs_.map((o) => { + const [outputType, dir] = o.split(':'); + + return { outputType: outputType as OutputType, dir }; + }); + + const input = await Deno.readTextFile(inputPath); + const parsed = toml.parse(input); + + const { name, palette: palette_, ...variants } = parsed; + const palette = flattenPalette(palette_ as RawPalette); + + for (const output of outputs) { + await generateOutput( + output, + name as string, + variants as Record, + palette, + ); + } +} + +await main();