diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index 71a4b6bf..d191e040 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -11,6 +11,149 @@ import { Badge } from "./ui/badge" import { getNetworkUnit } from "../lib/format-network" import { fetchApi } from "../lib/api-config" +// GitHub Dark color palette for bash syntax highlighting +const BASH_KEYWORDS = new Set([ + 'if','then','else','elif','fi','for','while','until','do','done','case','esac', + 'function','return','local','readonly','export','declare','typeset','unset', + 'source','alias','exit','break','continue','in','select','time','trap', +]) +const BASH_BUILTINS = new Set([ + 'echo','printf','read','cd','pwd','ls','cat','grep','sed','awk','cut','sort','uniq','tee','wc', + 'head','tail','find','xargs','chmod','chown','chgrp','mkdir','rmdir','rm','cp','mv','ln','touch', + 'ps','kill','killall','pkill','pgrep','top','htop','df','du','free','uptime','uname','hostname', + 'systemctl','journalctl','service','apt','apt-get','dpkg','dnf','yum','zypper','pacman', + 'curl','wget','ssh','scp','rsync','tar','gzip','gunzip','bzip2','zip','unzip', + 'mount','umount','lsblk','blkid','fdisk','parted','mkfs','fsck','swapon','swapoff', + 'ip','ifconfig','iptables','netstat','ss','ping','traceroute','dig','nslookup','nc', + 'sudo','su','whoami','id','groups','passwd','useradd','userdel','usermod','groupadd', + 'test','true','false','sleep','wait','eval','exec','command','type','which','hash', + 'set','getopts','shift','let','expr','jq','sed','grep','awk','tr', + 'modprobe','lsmod','rmmod','insmod','dmesg','sysctl','ulimit','nohup','disown','bg','fg', + 'zpool','zfs','qm','pct','pvesh','pvesm','pvenode','pveam','pveversion','vzdump', + 'smartctl','nvme','ipmitool','sensors','upsc','dkms','modinfo','lspci','lsusb','lscpu', +]) + +function escapeHtml(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') +} + +function highlightBash(code: string): string { + // Token-based highlighter — processes line by line to avoid cross-line state issues + const lines = code.split('\n') + const out: string[] = [] + + for (const line of lines) { + let i = 0 + let result = '' + + while (i < line.length) { + const ch = line[i] + + // Comments (# to end of line, but not inside strings — simple heuristic) + if (ch === '#' && (i === 0 || /\s/.test(line[i - 1]))) { + result += `${escapeHtml(line.slice(i))}` + i = line.length + continue + } + + // Strings: double-quoted (may contain $variables) + if (ch === '"') { + let j = i + 1 + let content = '' + while (j < line.length && line[j] !== '"') { + if (line[j] === '\\' && j + 1 < line.length) { + content += line[j] + line[j + 1] + j += 2 + } else { + content += line[j] + j++ + } + } + const str = '"' + content + (line[j] === '"' ? '"' : '') + // Highlight $vars inside strings + const strHtml = escapeHtml(str).replace( + /(\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*|\$[0-9@#?*$!-])/g, + '$1' + ) + result += `${strHtml}` + i = j + 1 + continue + } + + // Strings: single-quoted (literal, no interpolation) + if (ch === "'") { + let j = i + 1 + while (j < line.length && line[j] !== "'") j++ + const str = line.slice(i, j + 1) + result += `${escapeHtml(str)}` + i = j + 1 + continue + } + + // Variables outside strings + if (ch === '$') { + const rest = line.slice(i) + let m = rest.match(/^\$\{[^}]+\}/) + if (!m) m = rest.match(/^\$[A-Za-z_][A-Za-z0-9_]*/) + if (!m) m = rest.match(/^\$[0-9@#?*$!-]/) + if (m) { + result += `${escapeHtml(m[0])}` + i += m[0].length + continue + } + } + + // Numbers + if (/[0-9]/.test(ch) && (i === 0 || /[\s=(\[,:;+\-*/]/.test(line[i - 1]))) { + const rest = line.slice(i) + const m = rest.match(/^[0-9]+/) + if (m) { + result += `${m[0]}` + i += m[0].length + continue + } + } + + // Identifiers — check if keyword, builtin, or function definition + if (/[A-Za-z_]/.test(ch)) { + const rest = line.slice(i) + const m = rest.match(/^[A-Za-z_][A-Za-z0-9_-]*/) + if (m) { + const word = m[0] + const after = line.slice(i + word.length) + if (BASH_KEYWORDS.has(word)) { + result += `${word}` + } else if (/^\s*\(\)\s*\{?/.test(after)) { + // function definition: name() { ... } + result += `${word}` + } else if (BASH_BUILTINS.has(word) && (i === 0 || /[\s|;&(]/.test(line[i - 1]))) { + result += `${word}` + } else { + result += escapeHtml(word) + } + i += word.length + continue + } + } + + // Operators and special chars + if (/[|&;<>(){}[\]=!+*\/%~^]/.test(ch)) { + result += `${escapeHtml(ch)}` + i++ + continue + } + + // Default: escape and append + result += escapeHtml(ch) + i++ + } + + out.push(result) + } + + return out.join('\n') +} + interface SuppressionCategory { key: string label: string @@ -874,27 +1017,23 @@ export function Settings() { {proxmenuxTools.length} active
- {proxmenuxTools.map((tool) => ( -
-
-
- {tool.name} + {proxmenuxTools.map((tool) => { + const clickable = !!tool.has_source + return ( +
viewToolSource(tool) : undefined} + className={`flex items-center justify-between gap-2 p-3 bg-muted/50 rounded-lg border border-border transition-colors ${clickable ? 'hover:bg-muted hover:border-orange-500/40 cursor-pointer' : ''}`} + title={clickable ? 'Click to view source code' : undefined} + > +
+
+ {tool.name} +
v{tool.version || '1.0'}
- {tool.has_source && ( - - )} -
- ))} + ) + })}
)} @@ -953,7 +1092,11 @@ export function Settings() {

{codeModal.error}

) : ( -
{codeModal.source}
+
${highlightBash(codeModal.source)}` }}
+                />
               )}