diff --git a/webterm/config.go b/webterm/config.go index 58bfbf3..926f7d4 100644 --- a/webterm/config.go +++ b/webterm/config.go @@ -61,7 +61,7 @@ func LoadLandingYAML(manifestPath string) ([]App, error) { Path: path, Color: asString(entry["color"]), Terminal: terminal, - Theme: asString(entry["theme"]), + Theme: firstNonEmpty(asString(entry["theme"]), asString(entry["webterm-theme"])), }) } return apps, nil @@ -144,3 +144,12 @@ func asString(value any) string { } return "" } + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if v != "" { + return v + } + } + return "" +} diff --git a/webterm/config_test.go b/webterm/config_test.go index ffaa4f9..747c516 100644 --- a/webterm/config_test.go +++ b/webterm/config_test.go @@ -30,6 +30,35 @@ func TestLoadLandingYAML(t *testing.T) { } } +func TestLoadLandingYAMLWebtermThemeKey(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "landing.yaml") + content := ` +- name: Pro + command: /bin/sh + webterm-theme: monokai-pro +- name: Classic + command: /bin/sh + theme: dracula +` + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + apps, err := LoadLandingYAML(path) + if err != nil { + t.Fatalf("LoadLandingYAML() error = %v", err) + } + if len(apps) != 2 { + t.Fatalf("expected 2 apps, got %d", len(apps)) + } + if apps[0].Theme != "monokai-pro" { + t.Fatalf("expected monokai-pro from webterm-theme key, got %q", apps[0].Theme) + } + if apps[1].Theme != "dracula" { + t.Fatalf("expected dracula from theme key, got %q", apps[1].Theme) + } +} + func TestLoadComposeManifestReadsLabels(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "compose.yaml") diff --git a/webterm/static/js/terminal.js b/webterm/static/js/terminal.js index f5c55cf..0e27a42 100644 --- a/webterm/static/js/terminal.js +++ b/webterm/static/js/terminal.js @@ -10,7 +10,7 @@ Example: For tests, pass a Ghostty instance directly: import { Ghostty, Terminal } from "ghostty-web"; const ghostty = await Ghostty.load(); - const term = new Terminal({ ghostty });`);return B}var k=1000,P0=30000,I=null;async function S0(){if(!I){let j=L0();console.log("[webterm] Loading shared Ghostty WASM:",j),I=await u.load(j)}return I}var K0='ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", "Fira Code", "Roboto Mono", Menlo, Monaco, Consolas, "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace',M={tango:{background:"#000000",foreground:"#d3d7cf",cursor:"#d3d7cf",cursorAccent:"#000000",selectionBackground:"#d3d7cf",selectionForeground:"#000000",black:"#2e3436",red:"#cc0000",green:"#4e9a06",yellow:"#c4a000",blue:"#3465a4",magenta:"#75507b",cyan:"#06989a",white:"#d3d7cf",brightBlack:"#555753",brightRed:"#ef2929",brightGreen:"#8ae234",brightYellow:"#fce94f",brightBlue:"#729fcf",brightMagenta:"#ad7fa8",brightCyan:"#34e2e2",brightWhite:"#eeeeec"},xterm:{background:"#000000",foreground:"#e5e5e5",cursor:"#e5e5e5",cursorAccent:"#000000",selectionBackground:"#e5e5e5",selectionForeground:"#000000",black:"#000000",red:"#cd0000",green:"#00cd00",yellow:"#cdcd00",blue:"#0000cd",magenta:"#cd00cd",cyan:"#00cdcd",white:"#e5e5e5",brightBlack:"#4d4d4d",brightRed:"#ff0000",brightGreen:"#00ff00",brightYellow:"#ffff00",brightBlue:"#0000ff",brightMagenta:"#ff00ff",brightCyan:"#00ffff",brightWhite:"#ffffff"},monokai:{background:"#272822",foreground:"#fdfff1",cursor:"#fdfff1",cursorAccent:"#272822",selectionBackground:"#fdfff1",selectionForeground:"#272822",black:"#272822",red:"#f92672",green:"#a6e22e",yellow:"#e6db74",blue:"#fd971f",magenta:"#ae81ff",cyan:"#66d9ef",white:"#fdfff1",brightBlack:"#6e7066",brightRed:"#f92672",brightGreen:"#a6e22e",brightYellow:"#e6db74",brightBlue:"#fd971f",brightMagenta:"#ae81ff",brightCyan:"#66d9ef",brightWhite:"#fdfff1"},"monokai-pro":{background:"#2d2a2e",foreground:"#fcfcfa",cursor:"#fcfcfa",cursorAccent:"#2d2a2e",selectionBackground:"#fcfcfa",selectionForeground:"#2d2a2e",black:"#403e41",red:"#ff6188",green:"#a9dc76",yellow:"#ffd866",blue:"#fc9867",magenta:"#ab9df2",cyan:"#78dce8",white:"#fcfcfa",brightBlack:"#727072",brightRed:"#ff6188",brightGreen:"#a9dc76",brightYellow:"#ffd866",brightBlue:"#fc9867",brightMagenta:"#ab9df2",brightCyan:"#78dce8",brightWhite:"#fcfcfa"},ristretto:{background:"#2d2525",foreground:"#fff1f3",cursor:"#fff1f3",cursorAccent:"#2d2525",selectionBackground:"#fff1f3",selectionForeground:"#2d2525",black:"#2c2525",red:"#fd6883",green:"#adda78",yellow:"#f9cc6c",blue:"#f38d70",magenta:"#a8a9eb",cyan:"#85dacc",white:"#f9f8f5",brightBlack:"#655761",brightRed:"#fd6883",brightGreen:"#adda78",brightYellow:"#f9cc6c",brightBlue:"#f38d70",brightMagenta:"#a8a9eb",brightCyan:"#85dacc",brightWhite:"#f9f8f5"},dark:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",cursorAccent:"#1e1e1e",selectionBackground:"#d4d4d4",selectionForeground:"#1e1e1e",black:"#000000",red:"#cd3131",green:"#0dbc79",yellow:"#e5e510",blue:"#2472c8",magenta:"#bc3fbc",cyan:"#11a8cd",white:"#e5e5e5",brightBlack:"#666666",brightRed:"#f14c4c",brightGreen:"#23d18b",brightYellow:"#f5f543",brightBlue:"#3b8eea",brightMagenta:"#d670d6",brightCyan:"#29b8db",brightWhite:"#ffffff"},light:{background:"#ffffff",foreground:"#383a42",cursor:"#526eff",cursorAccent:"#ffffff",selectionBackground:"#383a42",selectionForeground:"#ffffff",black:"#000000",red:"#e45649",green:"#50a14f",yellow:"#c18401",blue:"#4078f2",magenta:"#a626a4",cyan:"#0184bc",white:"#a0a1a7",brightBlack:"#5c6370",brightRed:"#e06c75",brightGreen:"#98c379",brightYellow:"#d19a66",brightBlue:"#61afef",brightMagenta:"#c678dd",brightCyan:"#56b6c2",brightWhite:"#ffffff"},dracula:{background:"#282a36",foreground:"#f8f8f2",cursor:"#f8f8f2",cursorAccent:"#282a36",selectionBackground:"#f8f8f2",selectionForeground:"#282a36",black:"#21222c",red:"#ff5555",green:"#50fa7b",yellow:"#f1fa8c",blue:"#bd93f9",magenta:"#ff79c6",cyan:"#8be9fd",white:"#f8f8f2",brightBlack:"#6272a4",brightRed:"#ff6e6e",brightGreen:"#69ff94",brightYellow:"#ffffa5",brightBlue:"#d6acff",brightMagenta:"#ff92df",brightCyan:"#a4ffff",brightWhite:"#ffffff"},catppuccin:{background:"#1e1e2e",foreground:"#cdd6f4",cursor:"#f5e0dc",cursorAccent:"#1e1e2e",selectionBackground:"#cdd6f4",selectionForeground:"#1e1e2e",black:"#45475a",red:"#f38ba8",green:"#a6e3a1",yellow:"#f9e2af",blue:"#89b4fa",magenta:"#f5c2e7",cyan:"#94e2d5",white:"#bac2de",brightBlack:"#585b70",brightRed:"#f38ba8",brightGreen:"#a6e3a1",brightYellow:"#f9e2af",brightBlue:"#89b4fa",brightMagenta:"#f5c2e7",brightCyan:"#94e2d5",brightWhite:"#a6adc8"},nord:{background:"#2e3440",foreground:"#d8dee9",cursor:"#d8dee9",cursorAccent:"#2e3440",selectionBackground:"#d8dee9",selectionForeground:"#2e3440",black:"#3b4252",red:"#bf616a",green:"#a3be8c",yellow:"#ebcb8b",blue:"#81a1c1",magenta:"#b48ead",cyan:"#88c0d0",white:"#e5e9f0",brightBlack:"#4c566a",brightRed:"#bf616a",brightGreen:"#a3be8c",brightYellow:"#ebcb8b",brightBlue:"#81a1c1",brightMagenta:"#b48ead",brightCyan:"#8fbcbb",brightWhite:"#eceff4"},gruvbox:{background:"#282828",foreground:"#ebdbb2",cursor:"#ebdbb2",cursorAccent:"#282828",selectionBackground:"#ebdbb2",selectionForeground:"#282828",black:"#282828",red:"#cc241d",green:"#98971a",yellow:"#d79921",blue:"#458588",magenta:"#b16286",cyan:"#689d6a",white:"#a89984",brightBlack:"#928374",brightRed:"#fb4934",brightGreen:"#b8bb26",brightYellow:"#fabd2f",brightBlue:"#83a598",brightMagenta:"#d3869b",brightCyan:"#8ec07c",brightWhite:"#ebdbb2"},solarized:{background:"#002b36",foreground:"#839496",cursor:"#839496",cursorAccent:"#002b36",selectionBackground:"#839496",selectionForeground:"#002b36",black:"#073642",red:"#dc322f",green:"#859900",yellow:"#b58900",blue:"#268bd2",magenta:"#d33682",cyan:"#2aa198",white:"#eee8d5",brightBlack:"#586e75",brightRed:"#cb4b16",brightGreen:"#586e75",brightYellow:"#657b83",brightBlue:"#839496",brightMagenta:"#6c71c4",brightCyan:"#93a1a1",brightWhite:"#fdf6e3"},tokyo:{background:"#1a1b26",foreground:"#a9b1d6",cursor:"#c0caf5",cursorAccent:"#1a1b26",selectionBackground:"#a9b1d6",selectionForeground:"#1a1b26",black:"#15161e",red:"#f7768e",green:"#9ece6a",yellow:"#e0af68",blue:"#7aa2f7",magenta:"#bb9af7",cyan:"#7dcfff",white:"#a9b1d6",brightBlack:"#414868",brightRed:"#f7768e",brightGreen:"#9ece6a",brightYellow:"#e0af68",brightBlue:"#7aa2f7",brightMagenta:"#bb9af7",brightCyan:"#7dcfff",brightWhite:"#c0caf5"}};function u0(j){console.log("[webterm:parseConfig] Parsing config from element");let V={};if(j.dataset.fontFamily){let $=j.dataset.fontFamily;if($.startsWith("var(")){let Z=$.match(/var\(([^)]+)\)/);if(Z){let J=Z[1].trim(),O=getComputedStyle(document.documentElement).getPropertyValue(J).trim();if(O)$=O,console.log(`[webterm:parseConfig] Resolved CSS variable ${J} to: "${$}"`);else console.warn(`[webterm:parseConfig] CSS variable ${J} not found, using default font`),$=K0}}V.fontFamily=$,console.log(`[webterm:parseConfig] fontFamily: "${V.fontFamily}"`)}if(j.dataset.fontSize)V.fontSize=parseInt(j.dataset.fontSize,10),console.log(`[webterm:parseConfig] fontSize: ${V.fontSize}`);if(j.dataset.scrollback)V.scrollback=parseInt(j.dataset.scrollback,10),console.log(`[webterm:parseConfig] scrollback: ${V.scrollback}`);if(j.dataset.theme){let $=j.dataset.theme.toLowerCase();if(console.log(`[webterm:parseConfig] theme attribute: "${j.dataset.theme}" -> normalized: "${$}"`),console.log(`[webterm:parseConfig] Available themes: ${Object.keys(M).join(", ")}`),console.log(`[webterm:parseConfig] Theme "${$}" in THEMES? ${$ in M}`),$ in M)V.theme=M[$],console.log(`[webterm:parseConfig] Using built-in theme "${$}":`,JSON.stringify(V.theme,null,2));else{console.log("[webterm:parseConfig] Theme not found in THEMES, trying JSON parse...");try{V.theme=JSON.parse(j.dataset.theme),console.log("[webterm:parseConfig] Parsed custom JSON theme:",V.theme)}catch(Z){console.warn(`[webterm:parseConfig] Unknown theme "${j.dataset.theme}", JSON parse failed:`,Z)}}}else console.log("[webterm:parseConfig] No theme attribute found on element");return console.log("[webterm:parseConfig] Final config:",V),V}function L0(){let j=document.querySelectorAll('script[src*="terminal.js"]');if(j.length>0){let V=j[0].src;return V.substring(0,V.lastIndexOf("/")+1)+"ghostty-vt.wasm"}return"/static/js/ghostty-vt.wasm"}function O0(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)||"ontouchstart"in window&&navigator.maxTouchPoints>0}var M0={"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"_","=":"+","[":"{","]":"}","\\":"|",";":":","'":'"',",":"<",".":">","/":"?"},k0={"2":"@","3":"[","4":"\\","5":"]","6":"^","7":"_","8":"?"},f0=["\x1BOP","\x1BOQ","\x1BOR","\x1BOS","\x1B[15~","\x1B[17~","\x1B[18~","\x1B[19~","\x1B[20~","\x1B[21~"],p0=["\x1B[23~","\x1B[24~","\x1B[25~","\x1B[26~","\x1B[28~","\x1B[29~","\x1B[31~","\x1B[32~","\x1B[33~","\x1B[34~"];function w0(j){if(j.length!==1)return j;if(j>="a"&&j<="z")return j.toUpperCase();return M0[j]??j}function R0(j){if(j.length!==1)return j;let V=k0[j]??j;if(V==="?")return"";let $=V.toUpperCase().charCodeAt(0);if($>=64&&$<=95)return String.fromCharCode($-64);return j}function r(j,V){if(j.length!==1)return null;let $="1234567890".indexOf(j);if($<0)return null;return V?p0[$]:f0[$]}function S(j){if(!j||j.startsWith("\x1B"))return j;return`\x1B${j}`}function l(j,V,$,Z,J){if(j.length!==1)return j;if(J){let O=r(j,V);if(O)return Z?S(O):O}if($){let O=R0(j);if(O!==j)return Z?S(O):O}if(V){let O=w0(j);return Z?S(O):O}return Z?S(j):j}class o{terminal;fitAddon;socket=null;textDecoder=new TextDecoder;element;wsUrl;reconnectAttempts=0;maxReconnectAttempts=5;reconnectDelay=1000;heartbeatIntervalMs=15000;stallTimeoutMs=45000;heartbeatTimer;lastMessageAt=0;lastPongAt=0;messageQueue=[];lastValidSize=null;mobileInput=null;mobileKeybar=null;ctrlActive=!1;altActive=!1;shiftActive=!1;fnActive=!1;pendingCtrl=!1;pendingAlt=!1;pendingShift=!1;pendingFn=!1;fontFamily;fontSize;cleanupTimer;resizeObserver=null;mobileKeybarStyle=null;boundHandlers=[];constructor(j,V,$,Z,J,O){this.element=j,this.wsUrl=V,this.terminal=$,this.fitAddon=Z,this.fontFamily=J,this.fontSize=O}addTrackedListener(j,V,$,Z){j.addEventListener(V,$,Z),this.boundHandlers.push({target:j,type:V,handler:$,options:Z})}static async create(j,V,$){console.log("[webterm:create] WebTerminal.create() called"),console.log("[webterm:create] Container:",j),console.log("[webterm:create] wsUrl:",V),console.log("[webterm:create] Config received:",JSON.stringify($,null,2));let Z=L0();console.log("[webterm:create] WASM path:",Z),console.log("[webterm:create] Loading shared Ghostty WASM...");let J=await S0();console.log("[webterm:create] Ghostty loaded:",J);let O=$.theme??M.tango;console.log("[webterm:create] Theme to use (config.theme ?? THEMES.xterm):",JSON.stringify(O,null,2));let X=$.fontFamily?.trim()||K0,P=$.fontSize??16,K={fontFamily:X,fontSize:P,scrollback:$.scrollback??1000,cursorBlink:!0,cursorStyle:"block",theme:O,ghostty:J};console.log("[webterm:create] Full ITerminalOptions:",JSON.stringify(K,null,2)),console.log("[webterm:create] Creating ghostty-web Terminal instance...");let L=new d(K);console.log("[webterm:create] Terminal created:",L),console.log("[webterm:create] Terminal.options:",L.options),console.log("[webterm:create] Creating FitAddon...");let z=new h;console.log("[webterm:create] Loading FitAddon into terminal..."),L.loadAddon(z),console.log("[webterm:create] Calling terminal.open(container)..."),L.open(j),console.log("[webterm:create] terminal.open() completed");let W=L;if(console.log("[webterm:create] Terminal internal keys:",Object.keys(W)),W.renderer){console.log("[webterm:create] Renderer exists:",W.renderer);let Y=W.renderer;if(console.log("[webterm:create] Renderer keys:",Object.keys(Y)),Y.theme)console.log("[webterm:create] Renderer.theme:",Y.theme);if(Y.palette)console.log("[webterm:create] Renderer.palette:",Y.palette)}let H=new o(j,V,L,z,X,P);return console.log("[webterm:create] WebTerminal instance created"),H.initialize(),console.log("[webterm:create] WebTerminal initialized"),H}initialize(){console.log("[webterm:init] initialize() called");let j=this.element.querySelector("canvas");if(console.log("[webterm:init] Canvas element:",j),j)console.log("[webterm:init] Canvas dimensions:",{width:j.width,height:j.height,clientWidth:j.clientWidth,clientHeight:j.clientHeight,style:j.style.cssText});if(console.log("[webterm:init] Container dimensions:",{clientWidth:this.element.clientWidth,clientHeight:this.element.clientHeight}),this.waitForFonts().then(()=>{if(console.log("[webterm:init] Fonts loaded, triggering font reload..."),typeof this.terminal.loadFonts==="function")this.terminal.loadFonts(),console.log("[webterm:init] terminal.loadFonts() called");this.fit(),console.log("[webterm:init] fit() completed");let $=this.element.querySelector("canvas");if($)console.log("[webterm:init] Canvas after fit:",{width:$.width,height:$.height,clientWidth:$.clientWidth,clientHeight:$.clientHeight})}),this.setupResizeObserver(),this.addTrackedListener(window,"resize",()=>{this.fit()}),this.terminal.onData(($)=>{this.send(["stdin",$])}),this.terminal.onResize(($)=>{if(this.isValidSize($.cols,$.rows))this.lastValidSize={cols:$.cols,rows:$.rows},this.send(["resize",{width:$.cols,height:$.rows}])}),this.setupMobileKeyboard(),this.setupTouchSelection(),O0())this.setupMobileKeybar();this.startResourceCleanup(),this.connect();let V=()=>{if(O0())this.focusMobileInput();else this.terminal.focus()};this.addTrackedListener(document,"visibilitychange",()=>{if(!document.hidden)V()}),this.addTrackedListener(window,"focus",()=>{V()}),this.addTrackedListener(window,"pageshow",()=>{V()})}setupMobileKeyboard(){let j=document.createElement("textarea");j.setAttribute("autocapitalize","off"),j.setAttribute("autocomplete","off"),j.setAttribute("autocorrect","off"),j.setAttribute("spellcheck","false"),j.setAttribute("inputmode","text"),j.setAttribute("enterkeyhint","send"),j.style.cssText=` + const term = new Terminal({ ghostty });`);return B}var k=1000,P0=30000,I=null;async function S0(){if(!I){let j=L0();console.log("[webterm] Loading shared Ghostty WASM:",j),I=await u.load(j)}return I}var K0='ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", "Fira Code", "Roboto Mono", Menlo, Monaco, Consolas, "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace',M={tango:{background:"#000000",foreground:"#d3d7cf",cursor:"#d3d7cf",cursorAccent:"#000000",selectionBackground:"#d3d7cf",selectionForeground:"#000000",black:"#2e3436",red:"#cc0000",green:"#4e9a06",yellow:"#c4a000",blue:"#3465a4",magenta:"#75507b",cyan:"#06989a",white:"#d3d7cf",brightBlack:"#555753",brightRed:"#ef2929",brightGreen:"#8ae234",brightYellow:"#fce94f",brightBlue:"#729fcf",brightMagenta:"#ad7fa8",brightCyan:"#34e2e2",brightWhite:"#eeeeec"},xterm:{background:"#000000",foreground:"#e5e5e5",cursor:"#e5e5e5",cursorAccent:"#000000",selectionBackground:"#e5e5e5",selectionForeground:"#000000",black:"#000000",red:"#cd0000",green:"#00cd00",yellow:"#cdcd00",blue:"#0000cd",magenta:"#cd00cd",cyan:"#00cdcd",white:"#e5e5e5",brightBlack:"#4d4d4d",brightRed:"#ff0000",brightGreen:"#00ff00",brightYellow:"#ffff00",brightBlue:"#0000ff",brightMagenta:"#ff00ff",brightCyan:"#00ffff",brightWhite:"#ffffff"},monokai:{background:"#272822",foreground:"#fdfff1",cursor:"#fdfff1",cursorAccent:"#272822",selectionBackground:"#fdfff1",selectionForeground:"#272822",black:"#272822",red:"#f92672",green:"#a6e22e",yellow:"#e6db74",blue:"#fd971f",magenta:"#ae81ff",cyan:"#66d9ef",white:"#fdfff1",brightBlack:"#6e7066",brightRed:"#f92672",brightGreen:"#a6e22e",brightYellow:"#e6db74",brightBlue:"#fd971f",brightMagenta:"#ae81ff",brightCyan:"#66d9ef",brightWhite:"#fdfff1"},"monokai-pro":{background:"#2d2a2e",foreground:"#fcfcfa",cursor:"#fcfcfa",cursorAccent:"#2d2a2e",selectionBackground:"#fcfcfa",selectionForeground:"#2d2a2e",black:"#403e41",red:"#ff6188",green:"#a9dc76",yellow:"#ffd866",blue:"#fc9867",magenta:"#ab9df2",cyan:"#78dce8",white:"#fcfcfa",brightBlack:"#727072",brightRed:"#ff6188",brightGreen:"#a9dc76",brightYellow:"#ffd866",brightBlue:"#fc9867",brightMagenta:"#ab9df2",brightCyan:"#78dce8",brightWhite:"#fcfcfa"},ristretto:{background:"#2d2525",foreground:"#fff1f3",cursor:"#fff1f3",cursorAccent:"#2d2525",selectionBackground:"#fff1f3",selectionForeground:"#2d2525",black:"#2c2525",red:"#fd6883",green:"#adda78",yellow:"#f9cc6c",blue:"#f38d70",magenta:"#a8a9eb",cyan:"#85dacc",white:"#f9f8f5",brightBlack:"#655761",brightRed:"#fd6883",brightGreen:"#adda78",brightYellow:"#f9cc6c",brightBlue:"#f38d70",brightMagenta:"#a8a9eb",brightCyan:"#85dacc",brightWhite:"#f9f8f5"},dark:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",cursorAccent:"#1e1e1e",selectionBackground:"#d4d4d4",selectionForeground:"#1e1e1e",black:"#000000",red:"#cd3131",green:"#0dbc79",yellow:"#e5e510",blue:"#2472c8",magenta:"#bc3fbc",cyan:"#11a8cd",white:"#e5e5e5",brightBlack:"#666666",brightRed:"#f14c4c",brightGreen:"#23d18b",brightYellow:"#f5f543",brightBlue:"#3b8eea",brightMagenta:"#d670d6",brightCyan:"#29b8db",brightWhite:"#ffffff"},light:{background:"#ffffff",foreground:"#383a42",cursor:"#526eff",cursorAccent:"#ffffff",selectionBackground:"#383a42",selectionForeground:"#ffffff",black:"#000000",red:"#e45649",green:"#50a14f",yellow:"#c18401",blue:"#4078f2",magenta:"#a626a4",cyan:"#0184bc",white:"#a0a1a7",brightBlack:"#5c6370",brightRed:"#e06c75",brightGreen:"#98c379",brightYellow:"#d19a66",brightBlue:"#61afef",brightMagenta:"#c678dd",brightCyan:"#56b6c2",brightWhite:"#ffffff"},dracula:{background:"#282a36",foreground:"#f8f8f2",cursor:"#f8f8f2",cursorAccent:"#282a36",selectionBackground:"#f8f8f2",selectionForeground:"#282a36",black:"#21222c",red:"#ff5555",green:"#50fa7b",yellow:"#f1fa8c",blue:"#bd93f9",magenta:"#ff79c6",cyan:"#8be9fd",white:"#f8f8f2",brightBlack:"#6272a4",brightRed:"#ff6e6e",brightGreen:"#69ff94",brightYellow:"#ffffa5",brightBlue:"#d6acff",brightMagenta:"#ff92df",brightCyan:"#a4ffff",brightWhite:"#ffffff"},catppuccin:{background:"#1e1e2e",foreground:"#cdd6f4",cursor:"#f5e0dc",cursorAccent:"#1e1e2e",selectionBackground:"#cdd6f4",selectionForeground:"#1e1e2e",black:"#45475a",red:"#f38ba8",green:"#a6e3a1",yellow:"#f9e2af",blue:"#89b4fa",magenta:"#f5c2e7",cyan:"#94e2d5",white:"#bac2de",brightBlack:"#585b70",brightRed:"#f38ba8",brightGreen:"#a6e3a1",brightYellow:"#f9e2af",brightBlue:"#89b4fa",brightMagenta:"#f5c2e7",brightCyan:"#94e2d5",brightWhite:"#a6adc8"},nord:{background:"#2e3440",foreground:"#d8dee9",cursor:"#d8dee9",cursorAccent:"#2e3440",selectionBackground:"#d8dee9",selectionForeground:"#2e3440",black:"#3b4252",red:"#bf616a",green:"#a3be8c",yellow:"#ebcb8b",blue:"#81a1c1",magenta:"#b48ead",cyan:"#88c0d0",white:"#e5e9f0",brightBlack:"#4c566a",brightRed:"#bf616a",brightGreen:"#a3be8c",brightYellow:"#ebcb8b",brightBlue:"#81a1c1",brightMagenta:"#b48ead",brightCyan:"#8fbcbb",brightWhite:"#eceff4"},gruvbox:{background:"#282828",foreground:"#ebdbb2",cursor:"#ebdbb2",cursorAccent:"#282828",selectionBackground:"#ebdbb2",selectionForeground:"#282828",black:"#282828",red:"#cc241d",green:"#98971a",yellow:"#d79921",blue:"#458588",magenta:"#b16286",cyan:"#689d6a",white:"#a89984",brightBlack:"#928374",brightRed:"#fb4934",brightGreen:"#b8bb26",brightYellow:"#fabd2f",brightBlue:"#83a598",brightMagenta:"#d3869b",brightCyan:"#8ec07c",brightWhite:"#ebdbb2"},solarized:{background:"#002b36",foreground:"#839496",cursor:"#839496",cursorAccent:"#002b36",selectionBackground:"#839496",selectionForeground:"#002b36",black:"#073642",red:"#dc322f",green:"#859900",yellow:"#b58900",blue:"#268bd2",magenta:"#d33682",cyan:"#2aa198",white:"#eee8d5",brightBlack:"#586e75",brightRed:"#cb4b16",brightGreen:"#586e75",brightYellow:"#657b83",brightBlue:"#839496",brightMagenta:"#6c71c4",brightCyan:"#93a1a1",brightWhite:"#fdf6e3"},tokyo:{background:"#1a1b26",foreground:"#a9b1d6",cursor:"#c0caf5",cursorAccent:"#1a1b26",selectionBackground:"#a9b1d6",selectionForeground:"#1a1b26",black:"#15161e",red:"#f7768e",green:"#9ece6a",yellow:"#e0af68",blue:"#7aa2f7",magenta:"#bb9af7",cyan:"#7dcfff",white:"#a9b1d6",brightBlack:"#414868",brightRed:"#f7768e",brightGreen:"#9ece6a",brightYellow:"#e0af68",brightBlue:"#7aa2f7",brightMagenta:"#bb9af7",brightCyan:"#7dcfff",brightWhite:"#c0caf5"},miasma:{background:"#222222",foreground:"#c2c2b0",cursor:"#c2c2b0",cursorAccent:"#222222",selectionBackground:"#c2c2b0",selectionForeground:"#222222",black:"#000000",red:"#685742",green:"#5f875f",yellow:"#b36d43",blue:"#78824b",magenta:"#bb7744",cyan:"#c9a554",white:"#d7c483",brightBlack:"#666666",brightRed:"#685742",brightGreen:"#5f875f",brightYellow:"#b36d43",brightBlue:"#78824b",brightMagenta:"#bb7744",brightCyan:"#c9a554",brightWhite:"#d7c483"},github:{background:"#1c2128",foreground:"#adbac7",cursor:"#adbac7",cursorAccent:"#1c2128",selectionBackground:"#adbac7",selectionForeground:"#1c2128",black:"#545d68",red:"#f47067",green:"#57ab5a",yellow:"#c69026",blue:"#539bf5",magenta:"#b083f0",cyan:"#39c5cf",white:"#909dab",brightBlack:"#636e7b",brightRed:"#ff938a",brightGreen:"#6bc46d",brightYellow:"#daaa3f",brightBlue:"#6cb6ff",brightMagenta:"#dcbdfb",brightCyan:"#56d4dd",brightWhite:"#cdd9e5"},gotham:{background:"#0c1014",foreground:"#99d1ce",cursor:"#99d1ce",cursorAccent:"#0c1014",selectionBackground:"#99d1ce",selectionForeground:"#0c1014",black:"#0c1014",red:"#c23127",green:"#2aa889",yellow:"#edb443",blue:"#195466",magenta:"#4e5166",cyan:"#33859e",white:"#99d1ce",brightBlack:"#0c1014",brightRed:"#c23127",brightGreen:"#2aa889",brightYellow:"#edb443",brightBlue:"#195466",brightMagenta:"#4e5166",brightCyan:"#33859e",brightWhite:"#99d1ce"}};function u0(j){console.log("[webterm:parseConfig] Parsing config from element");let V={};if(j.dataset.fontFamily){let $=j.dataset.fontFamily;if($.startsWith("var(")){let Z=$.match(/var\(([^)]+)\)/);if(Z){let J=Z[1].trim(),O=getComputedStyle(document.documentElement).getPropertyValue(J).trim();if(O)$=O,console.log(`[webterm:parseConfig] Resolved CSS variable ${J} to: "${$}"`);else console.warn(`[webterm:parseConfig] CSS variable ${J} not found, using default font`),$=K0}}V.fontFamily=$,console.log(`[webterm:parseConfig] fontFamily: "${V.fontFamily}"`)}if(j.dataset.fontSize)V.fontSize=parseInt(j.dataset.fontSize,10),console.log(`[webterm:parseConfig] fontSize: ${V.fontSize}`);if(j.dataset.scrollback)V.scrollback=parseInt(j.dataset.scrollback,10),console.log(`[webterm:parseConfig] scrollback: ${V.scrollback}`);if(j.dataset.theme){let $=j.dataset.theme.toLowerCase();if(console.log(`[webterm:parseConfig] theme attribute: "${j.dataset.theme}" -> normalized: "${$}"`),console.log(`[webterm:parseConfig] Available themes: ${Object.keys(M).join(", ")}`),console.log(`[webterm:parseConfig] Theme "${$}" in THEMES? ${$ in M}`),$ in M)V.theme=M[$],console.log(`[webterm:parseConfig] Using built-in theme "${$}":`,JSON.stringify(V.theme,null,2));else{console.log("[webterm:parseConfig] Theme not found in THEMES, trying JSON parse...");try{V.theme=JSON.parse(j.dataset.theme),console.log("[webterm:parseConfig] Parsed custom JSON theme:",V.theme)}catch(Z){console.warn(`[webterm:parseConfig] Unknown theme "${j.dataset.theme}", JSON parse failed:`,Z)}}}else console.log("[webterm:parseConfig] No theme attribute found on element");return console.log("[webterm:parseConfig] Final config:",V),V}function L0(){let j=document.querySelectorAll('script[src*="terminal.js"]');if(j.length>0){let V=j[0].src;return V.substring(0,V.lastIndexOf("/")+1)+"ghostty-vt.wasm"}return"/static/js/ghostty-vt.wasm"}function O0(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)||"ontouchstart"in window&&navigator.maxTouchPoints>0}var M0={"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"_","=":"+","[":"{","]":"}","\\":"|",";":":","'":'"',",":"<",".":">","/":"?"},k0={"2":"@","3":"[","4":"\\","5":"]","6":"^","7":"_","8":"?"},f0=["\x1BOP","\x1BOQ","\x1BOR","\x1BOS","\x1B[15~","\x1B[17~","\x1B[18~","\x1B[19~","\x1B[20~","\x1B[21~"],p0=["\x1B[23~","\x1B[24~","\x1B[25~","\x1B[26~","\x1B[28~","\x1B[29~","\x1B[31~","\x1B[32~","\x1B[33~","\x1B[34~"];function w0(j){if(j.length!==1)return j;if(j>="a"&&j<="z")return j.toUpperCase();return M0[j]??j}function R0(j){if(j.length!==1)return j;let V=k0[j]??j;if(V==="?")return"";let $=V.toUpperCase().charCodeAt(0);if($>=64&&$<=95)return String.fromCharCode($-64);return j}function r(j,V){if(j.length!==1)return null;let $="1234567890".indexOf(j);if($<0)return null;return V?p0[$]:f0[$]}function S(j){if(!j||j.startsWith("\x1B"))return j;return`\x1B${j}`}function l(j,V,$,Z,J){if(j.length!==1)return j;if(J){let O=r(j,V);if(O)return Z?S(O):O}if($){let O=R0(j);if(O!==j)return Z?S(O):O}if(V){let O=w0(j);return Z?S(O):O}return Z?S(j):j}class o{terminal;fitAddon;socket=null;textDecoder=new TextDecoder;element;wsUrl;reconnectAttempts=0;maxReconnectAttempts=5;reconnectDelay=1000;heartbeatIntervalMs=15000;stallTimeoutMs=45000;heartbeatTimer;lastMessageAt=0;lastPongAt=0;messageQueue=[];lastValidSize=null;mobileInput=null;mobileKeybar=null;ctrlActive=!1;altActive=!1;shiftActive=!1;fnActive=!1;pendingCtrl=!1;pendingAlt=!1;pendingShift=!1;pendingFn=!1;fontFamily;fontSize;cleanupTimer;resizeObserver=null;mobileKeybarStyle=null;boundHandlers=[];constructor(j,V,$,Z,J,O){this.element=j,this.wsUrl=V,this.terminal=$,this.fitAddon=Z,this.fontFamily=J,this.fontSize=O}addTrackedListener(j,V,$,Z){j.addEventListener(V,$,Z),this.boundHandlers.push({target:j,type:V,handler:$,options:Z})}static async create(j,V,$){console.log("[webterm:create] WebTerminal.create() called"),console.log("[webterm:create] Container:",j),console.log("[webterm:create] wsUrl:",V),console.log("[webterm:create] Config received:",JSON.stringify($,null,2));let Z=L0();console.log("[webterm:create] WASM path:",Z),console.log("[webterm:create] Loading shared Ghostty WASM...");let J=await S0();console.log("[webterm:create] Ghostty loaded:",J);let O=$.theme??M.tango;console.log("[webterm:create] Theme to use (config.theme ?? THEMES.xterm):",JSON.stringify(O,null,2));let X=$.fontFamily?.trim()||K0,P=$.fontSize??16,K={fontFamily:X,fontSize:P,scrollback:$.scrollback??1000,cursorBlink:!0,cursorStyle:"block",theme:O,ghostty:J};console.log("[webterm:create] Full ITerminalOptions:",JSON.stringify(K,null,2)),console.log("[webterm:create] Creating ghostty-web Terminal instance...");let L=new d(K);console.log("[webterm:create] Terminal created:",L),console.log("[webterm:create] Terminal.options:",L.options),console.log("[webterm:create] Creating FitAddon...");let z=new h;console.log("[webterm:create] Loading FitAddon into terminal..."),L.loadAddon(z),console.log("[webterm:create] Calling terminal.open(container)..."),L.open(j),console.log("[webterm:create] terminal.open() completed");let W=L;if(console.log("[webterm:create] Terminal internal keys:",Object.keys(W)),W.renderer){console.log("[webterm:create] Renderer exists:",W.renderer);let Y=W.renderer;if(console.log("[webterm:create] Renderer keys:",Object.keys(Y)),Y.theme)console.log("[webterm:create] Renderer.theme:",Y.theme);if(Y.palette)console.log("[webterm:create] Renderer.palette:",Y.palette)}let H=new o(j,V,L,z,X,P);return console.log("[webterm:create] WebTerminal instance created"),H.initialize(),console.log("[webterm:create] WebTerminal initialized"),H}initialize(){console.log("[webterm:init] initialize() called");let j=this.element.querySelector("canvas");if(console.log("[webterm:init] Canvas element:",j),j)console.log("[webterm:init] Canvas dimensions:",{width:j.width,height:j.height,clientWidth:j.clientWidth,clientHeight:j.clientHeight,style:j.style.cssText});if(console.log("[webterm:init] Container dimensions:",{clientWidth:this.element.clientWidth,clientHeight:this.element.clientHeight}),this.waitForFonts().then(()=>{if(console.log("[webterm:init] Fonts loaded, triggering font reload..."),typeof this.terminal.loadFonts==="function")this.terminal.loadFonts(),console.log("[webterm:init] terminal.loadFonts() called");this.fit(),console.log("[webterm:init] fit() completed");let $=this.element.querySelector("canvas");if($)console.log("[webterm:init] Canvas after fit:",{width:$.width,height:$.height,clientWidth:$.clientWidth,clientHeight:$.clientHeight})}),this.setupResizeObserver(),this.addTrackedListener(window,"resize",()=>{this.fit()}),this.terminal.onData(($)=>{this.send(["stdin",$])}),this.terminal.onResize(($)=>{if(this.isValidSize($.cols,$.rows))this.lastValidSize={cols:$.cols,rows:$.rows},this.send(["resize",{width:$.cols,height:$.rows}])}),this.setupMobileKeyboard(),this.setupTouchSelection(),O0())this.setupMobileKeybar();this.startResourceCleanup(),this.connect();let V=()=>{if(O0())this.focusMobileInput();else this.terminal.focus()};this.addTrackedListener(document,"visibilitychange",()=>{if(!document.hidden)V()}),this.addTrackedListener(window,"focus",()=>{V()}),this.addTrackedListener(window,"pageshow",()=>{V()})}setupMobileKeyboard(){let j=document.createElement("textarea");j.setAttribute("autocapitalize","off"),j.setAttribute("autocomplete","off"),j.setAttribute("autocorrect","off"),j.setAttribute("spellcheck","false"),j.setAttribute("inputmode","text"),j.setAttribute("enterkeyhint","send"),j.style.cssText=` position: absolute; left: 0; top: 0; diff --git a/webterm/static/js/terminal.ts b/webterm/static/js/terminal.ts index 8c03aeb..d9f37b5 100644 --- a/webterm/static/js/terminal.ts +++ b/webterm/static/js/terminal.ts @@ -352,6 +352,81 @@ const THEMES: Record = { brightCyan: "#7dcfff", brightWhite: "#c0caf5", }, + // Miasma - earthy warm tones + miasma: { + background: "#222222", + foreground: "#c2c2b0", + cursor: "#c2c2b0", + cursorAccent: "#222222", + selectionBackground: "#c2c2b0", + selectionForeground: "#222222", + black: "#000000", + red: "#685742", + green: "#5f875f", + yellow: "#b36d43", + blue: "#78824b", + magenta: "#bb7744", + cyan: "#c9a554", + white: "#d7c483", + brightBlack: "#666666", + brightRed: "#685742", + brightGreen: "#5f875f", + brightYellow: "#b36d43", + brightBlue: "#78824b", + brightMagenta: "#bb7744", + brightCyan: "#c9a554", + brightWhite: "#d7c483", + }, + // GitHub Dark Dimmed + github: { + background: "#1c2128", + foreground: "#adbac7", + cursor: "#adbac7", + cursorAccent: "#1c2128", + selectionBackground: "#adbac7", + selectionForeground: "#1c2128", + black: "#545d68", + red: "#f47067", + green: "#57ab5a", + yellow: "#c69026", + blue: "#539bf5", + magenta: "#b083f0", + cyan: "#39c5cf", + white: "#909dab", + brightBlack: "#636e7b", + brightRed: "#ff938a", + brightGreen: "#6bc46d", + brightYellow: "#daaa3f", + brightBlue: "#6cb6ff", + brightMagenta: "#dcbdfb", + brightCyan: "#56d4dd", + brightWhite: "#cdd9e5", + }, + // Gotham - dark blue-cyan + gotham: { + background: "#0c1014", + foreground: "#99d1ce", + cursor: "#99d1ce", + cursorAccent: "#0c1014", + selectionBackground: "#99d1ce", + selectionForeground: "#0c1014", + black: "#0c1014", + red: "#c23127", + green: "#2aa889", + yellow: "#edb443", + blue: "#195466", + magenta: "#4e5166", + cyan: "#33859e", + white: "#99d1ce", + brightBlack: "#0c1014", + brightRed: "#c23127", + brightGreen: "#2aa889", + brightYellow: "#edb443", + brightBlue: "#195466", + brightMagenta: "#4e5166", + brightCyan: "#33859e", + brightWhite: "#99d1ce", + }, };