Created
June 10, 2026 16:07
-
-
Save eduwass/177411a4ec4e22bfc0c13dd321ad15ee to your computer and use it in GitHub Desktop.
tmux-palette Terminal theme tuner — preview across common terminal schemes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>tmux-palette · Terminal theme tuner</title> | |
| <style> | |
| :root { color-scheme: dark; } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; background: #14151a; color: #d6d8df; | |
| font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| padding: 0 0 80px; | |
| } | |
| .wrap { padding: 0 clamp(16px, 4vw, 56px); } | |
| header.intro { padding: 36px clamp(16px, 4vw, 56px) 8px; } | |
| h1 { font-size: 22px; margin: 0 0 6px; letter-spacing: -0.01em; } | |
| .sub { color: #8a8d98; max-width: 820px; margin: 0; font-size: 14px; } | |
| .sub code { color: #b7bbc6; } | |
| /* control bar */ | |
| .controls { | |
| position: sticky; top: 0; z-index: 10; | |
| background: rgba(20,21,26,.92); backdrop-filter: blur(8px); | |
| border-bottom: 1px solid #292a33; | |
| padding: 14px clamp(16px, 4vw, 56px); | |
| display: flex; flex-wrap: wrap; gap: 16px 22px; align-items: flex-end; | |
| } | |
| .ctl { display: flex; flex-direction: column; gap: 5px; } | |
| .ctl label { font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: #8a8d98; font-weight: 600; } | |
| .ctl .pick { display: flex; align-items: center; gap: 7px; } | |
| .ctl select { | |
| appearance: none; background: #1c1d24; color: #e6e8ef; border: 1px solid #343642; | |
| border-radius: 7px; padding: 7px 26px 7px 10px; font: 13px ui-monospace, monospace; cursor: pointer; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238a8d98'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; background-position: right 9px center; | |
| } | |
| .sw { width: 16px; height: 16px; border-radius: 4px; border: 1px solid rgba(255,255,255,.18); flex: none; } | |
| .ctl-actions { display: flex; flex-direction: column; gap: 5px; margin-left: auto; } | |
| .json { | |
| font: 12px/1.4 ui-monospace, monospace; background: #1c1d24; border: 1px solid #343642; | |
| border-radius: 7px; padding: 8px 10px; color: #c7cbd6; white-space: pre; max-width: 420px; overflow:auto; | |
| } | |
| .row-btns { display: flex; gap: 8px; } | |
| button { | |
| background: #262833; color: #d6d8df; border: 1px solid #3a3d4a; border-radius: 7px; | |
| padding: 7px 12px; font: 12px/1 -apple-system, sans-serif; cursor: pointer; | |
| } | |
| button:hover { background: #2f323f; } | |
| .callout { | |
| margin: 18px 0 0; font-size: 13.5px; color: #c8b88a; | |
| background: #211f17; border: 1px solid #3a3320; border-radius: 10px; padding: 11px 16px; | |
| } | |
| .callout code { color: #e6d29a; } | |
| h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em; color: #7d808b; margin: 30px 0 14px; font-weight: 600; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px; } | |
| .card { border-radius: 12px; overflow: hidden; border: 1px solid rgba(255,255,255,.08); box-shadow: 0 6px 22px rgba(0,0,0,.35); } | |
| .card .name { display: flex; align-items: center; justify-content: space-between; padding: 9px 13px; | |
| font-size: 12.5px; font-weight: 600; background: #1c1d24; color: #cfd2da; border-bottom: 1px solid #292a33; } | |
| .badges { display: flex; gap: 6px; align-items:center; } | |
| .badge { font: 600 11px/1 ui-monospace, monospace; padding: 4px 6px; border-radius: 5px; color: #0c0d10; } | |
| .badge .lab { opacity: .65; margin-right: 3px; } | |
| .tag { font-size: 10.5px; font-weight: 700; letter-spacing: .06em; padding: 3px 7px; border-radius: 999px; } | |
| .tag.dark { background: #2a2c36; color: #9aa0b0; } | |
| .tag.light { background: #e8e3cf; color: #6a6552; } | |
| .popup { padding: 14px 16px 16px; font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } | |
| .prow { display: flex; align-items: baseline; white-space: pre; } | |
| .prow .l { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } | |
| .prow .r { padding-left: 18px; } | |
| .hdr { display: flex; justify-content: space-between; font-weight: 700; } | |
| .spacer { height: 10px; } | |
| .cat { font-weight: 700; margin-top: 2px; } | |
| .icon { display: inline-block; width: 1.4em; } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="intro"> | |
| <h1>tmux-palette — <em>Terminal</em> theme tuner</h1> | |
| <p class="sub">Change which ANSI color each role uses and watch every scheme re-render live. Backgrounds | |
| stay transparent (each card shows the scheme's own bg), so you're tuning the named-color roles only — | |
| exactly the fields in the bundled <code>terminal</code> theme. The JSON on the right is copy-paste ready. | |
| Representative 16-color palettes from the common iTerm2 / terminalcolors.com set.</p> | |
| </header> | |
| <div class="controls" id="controls"> | |
| <!-- selects injected --> | |
| <div class="ctl-actions"> | |
| <div class="row-btns"> | |
| <button id="reset">Reset to defaults</button> | |
| <button id="copy">Copy JSON</button> | |
| </div> | |
| <div class="json" id="json"></div> | |
| </div> | |
| </div> | |
| <div class="wrap"> | |
| <div class="callout"> | |
| Badges = <b>WCAG contrast vs background</b>. <code>active</code> = active-row color, <code>labels</code> | |
| = idle-text color. Green ≥ 4.5 (crisp), amber ≥ 3 (ok for bold/large), red < 3 (hard to read). | |
| The light schemes are where choices break. | |
| </div> | |
| <div id="dark"></div> | |
| <div id="light"></div> | |
| </div> | |
| <script> | |
| // name -> ANSI index; "default" uses the scheme foreground. | |
| const IDX = { black:0, red:1, green:2, yellow:3, blue:4, magenta:5, cyan:6, white:7, | |
| "bright-black":8, "bright-red":9, "bright-green":10, "bright-yellow":11, | |
| "bright-blue":12, "bright-magenta":13, "bright-cyan":14, "bright-white":15 }; | |
| const NAMES = [...Object.keys(IDX), "transparent"]; | |
| // canonical xterm-ish hints for the control swatches (not used for cards) | |
| const HINT = ["#000000","#cd0000","#00cd00","#cdcd00","#1f6fff","#cd00cd","#00cdcd","#e5e5e5", | |
| "#7f7f7f","#ff5555","#5fff5f","#ffff5f","#5c8cff","#ff5fff","#5fffff","#ffffff"]; | |
| // scheme = { name, dark, bg, fg, c:[16] } | |
| const SCHEMES = [ | |
| { name:"Dracula", dark:true, bg:"#282A36", fg:"#F8F8F2", c:["#21222C","#FF5555","#50FA7B","#F1FA8C","#BD93F9","#FF79C6","#8BE9FD","#F8F8F2","#6272A4","#FF6E6E","#69FF94","#FFFFA5","#D6ACFF","#FF92DF","#A4FFFF","#FFFFFF"] }, | |
| { name:"Tokyo Night", dark:true, bg:"#1A1B26", fg:"#C0CAF5", c:["#15161E","#F7768E","#9ECE6A","#E0AF68","#7AA2F7","#BB9AF7","#7DCFFF","#A9B1D6","#414868","#F7768E","#9ECE6A","#E0AF68","#7AA2F7","#BB9AF7","#7DCFFF","#C0CAF5"] }, | |
| { name:"Catppuccin Mocha", dark:true, bg:"#1E1E2E", fg:"#CDD6F4", c:["#45475A","#F38BA8","#A6E3A1","#F9E2AF","#89B4FA","#F5C2E7","#94E2D5","#BAC2DE","#585B70","#F38BA8","#A6E3A1","#F9E2AF","#89B4FA","#F5C2E7","#94E2D5","#A6ADC8"] }, | |
| { name:"Gruvbox Dark", dark:true, bg:"#282828", fg:"#EBDBB2", c:["#282828","#CC241D","#98971A","#D79921","#458588","#B16286","#689D6A","#A89984","#928374","#FB4934","#B8BB26","#FABD2F","#83A598","#D3869B","#8EC07C","#EBDBB2"] }, | |
| { name:"Nord", dark:true, bg:"#2E3440", fg:"#D8DEE9", c:["#3B4252","#BF616A","#A3BE8C","#EBCB8B","#81A1C1","#B48EAD","#88C0D0","#E5E9F0","#4C566A","#BF616A","#A3BE8C","#EBCB8B","#81A1C1","#B48EAD","#8FBCBB","#ECEFF4"] }, | |
| { name:"One Dark", dark:true, bg:"#282C34", fg:"#ABB2BF", c:["#282C34","#E06C75","#98C379","#E5C07B","#61AFEF","#C678DD","#56B6C2","#ABB2BF","#5C6370","#E06C75","#98C379","#E5C07B","#61AFEF","#C678DD","#56B6C2","#FFFFFF"] }, | |
| { name:"Solarized Dark", dark:true, bg:"#002B36", fg:"#839496", c:["#073642","#DC322F","#859900","#B58900","#268BD2","#D33682","#2AA198","#EEE8D5","#586E75","#CB4B16","#586E75","#657B83","#839496","#6C71C4","#93A1A1","#FDF6E3"] }, | |
| // light | |
| { name:"Builtin / Tango Light", dark:false, bg:"#FFFFFF", fg:"#000000", c:["#000000","#CC0000","#4E9A06","#C4A000","#3465A4","#75507B","#06989A","#D3D7CF","#555753","#EF2929","#8AE234","#FCE94F","#729FCF","#AD7FA8","#34E2E2","#EEEEEC"] }, | |
| { name:"GitHub Light", dark:false, bg:"#FFFFFF", fg:"#24292E", c:["#24292E","#D73A49","#28A745","#DBAB09","#0366D6","#5A32A3","#1B7C83","#6A737D","#959DA5","#CB2431","#22863A","#B08800","#005CC5","#5A32A3","#3192AA","#D1D5DA"] }, | |
| { name:"Solarized Light", dark:false, bg:"#FDF6E3", fg:"#657B83", c:["#073642","#DC322F","#859900","#B58900","#268BD2","#D33682","#2AA198","#EEE8D5","#93A1A1","#CB4B16","#586E75","#657B83","#839496","#6C71C4","#93A1A1","#FDF6E3"] }, | |
| { name:"One Light", dark:false, bg:"#FAFAFA", fg:"#383A42", c:["#383A42","#E45649","#50A14F","#C18401","#4078F2","#A626A4","#0184BC","#A0A1A7","#A0A1A7","#E45649","#50A14F","#C18401","#4078F2","#A626A4","#0184BC","#FFFFFF"] }, | |
| { name:"Gruvbox Light", dark:false, bg:"#FBF1C7", fg:"#3C3836", c:["#FBF1C7","#CC241D","#98971A","#D79921","#458588","#B16286","#689D6A","#7C6F64","#928374","#9D0006","#79740E","#B57614","#076678","#8F3F71","#427B58","#3C3836"] }, | |
| ]; | |
| const ROLES = [ | |
| { key:"title", label:"Title", def:"magenta" }, | |
| { key:"accent", label:"Accent / icons", def:"blue" }, | |
| { key:"active", label:"Active row", def:"yellow" }, | |
| { key:"labels", label:"Idle labels", def:"bright-black" }, | |
| ]; | |
| const state = Object.fromEntries(ROLES.map(r => [r.key, r.def])); | |
| function hexOf(scheme, name) { return name === "transparent" ? scheme.fg : scheme.c[IDX[name]]; } | |
| function lum(hex){ | |
| const c = hex.replace("#",""); | |
| const v = [0,2,4].map(i => parseInt(c.slice(i,i+2),16)/255) | |
| .map(x => x <= 0.03928 ? x/12.92 : Math.pow((x+0.055)/1.055, 2.4)); | |
| return 0.2126*v[0] + 0.7152*v[1] + 0.0722*v[2]; | |
| } | |
| function contrast(a,b){ const la=lum(a), lb=lum(b); return (Math.max(la,lb)+0.05)/(Math.min(la,lb)+0.05); } | |
| function badge(label, ratio){ | |
| const r = ratio.toFixed(1); | |
| let col = ratio < 3 ? "#ff453a" : ratio < 4.5 ? "#ffd60a" : "#34c759"; | |
| return `<span class="badge" style="background:${col}"><span class="lab">${label}</span>${r}</span>`; | |
| } | |
| const ROWS = [ | |
| { icon:"▤", title:"Split Horizontal", sc:"Cmd+D" }, | |
| { icon:"▥", title:"Split Vertical", sc:"Cmd+Shift+D" }, | |
| { icon:"✕", title:"Close Pane", sc:"Cmd+W" }, | |
| { icon:"→", title:"Next Pane", sc:"Cmd+Right" }, | |
| { icon:"↺", title:"Cycle Pane Layout", sc:"" }, | |
| ]; | |
| function card(s){ | |
| const c = (color, txt, w="") => `<span style="color:${color};${w}">${txt}</span>`; | |
| const title = hexOf(s, state.title), acc = hexOf(s, state.accent), | |
| hi = hexOf(s, state.active), idle = hexOf(s, state.labels); | |
| const idleRows = ROWS.map(r => ` | |
| <div class="prow"> | |
| <span class="l">${c(acc,`<span class="icon">${r.icon}</span>`)}${c(idle, r.title)}</span> | |
| ${r.sc ? `<span class="r">${c(idle, r.sc)}</span>` : ""} | |
| </div>`).join(""); | |
| return ` | |
| <div class="card"> | |
| <div class="name"> | |
| <span>${s.name}</span> | |
| <span class="badges"> | |
| ${badge("active", contrast(hi, s.bg))} | |
| ${badge("labels", contrast(idle, s.bg))} | |
| <span class="tag ${s.dark?'dark':'light'}">${s.dark?'DARK':'LIGHT'}</span> | |
| </span> | |
| </div> | |
| <div class="popup" style="background:${s.bg}"> | |
| <div class="prow hdr"><span>${c(title,"Commands","font-weight:700")}</span><span>${c(idle,"esc")}</span></div> | |
| <div class="prow">${c(acc,"▌")}${c(idle," Search")}</div> | |
| <div class="spacer"></div> | |
| <div class="cat">${c(acc,"Panes","font-weight:700")}</div> | |
| <div class="prow"> | |
| <span class="l">${c(hi,"▌","font-weight:700")}${c(hi,`<span class="icon" style="padding-left:.3em">⌕</span>`)}${c(hi,"Find Pane","font-weight:700")}</span> | |
| <span class="r">${c(hi,"Cmd+Shift+P")}</span> | |
| </div> | |
| ${idleRows} | |
| </div> | |
| </div>`; | |
| } | |
| function themeJSON(){ | |
| return `{ | |
| "bg": "transparent", | |
| "panel": "transparent", | |
| "selected": "transparent", | |
| "fg": "transparent", | |
| "muted": "${state.labels}", | |
| "accent": "${state.accent}", | |
| "selectedFg": "${state.active}", | |
| "titleFg": "${state.title}" | |
| }`; | |
| } | |
| function render(){ | |
| const D = SCHEMES.filter(s=>s.dark), L = SCHEMES.filter(s=>!s.dark); | |
| document.getElementById("dark").innerHTML = `<h2>Dark schemes</h2><div class="grid">${D.map(card).join("")}</div>`; | |
| document.getElementById("light").innerHTML = `<h2>Light schemes — the real test</h2><div class="grid">${L.map(card).join("")}</div>`; | |
| document.getElementById("json").textContent = themeJSON(); | |
| // refresh swatches | |
| ROLES.forEach(r => { | |
| const name = state[r.key]; | |
| const hex = name === "transparent" ? "#888" : HINT[IDX[name]]; | |
| document.querySelector(`#sw-${r.key}`).style.background = hex; | |
| }); | |
| } | |
| function buildControls(){ | |
| const host = document.getElementById("controls"); | |
| const frag = ROLES.map(r => { | |
| const opts = NAMES.map(n => `<option value="${n}" ${n===state[r.key]?"selected":""}>${n}</option>`).join(""); | |
| return `<div class="ctl"> | |
| <label>${r.label}</label> | |
| <div class="pick"><span class="sw" id="sw-${r.key}"></span> | |
| <select data-role="${r.key}">${opts}</select></div> | |
| </div>`; | |
| }).join(""); | |
| host.insertAdjacentHTML("afterbegin", frag); | |
| host.querySelectorAll("select").forEach(sel => | |
| sel.addEventListener("change", e => { state[e.target.dataset.role] = e.target.value; render(); })); | |
| document.getElementById("reset").addEventListener("click", () => { | |
| ROLES.forEach(r => state[r.key] = r.def); | |
| host.querySelectorAll("select").forEach(sel => sel.value = state[sel.dataset.role]); | |
| render(); | |
| }); | |
| document.getElementById("copy").addEventListener("click", () => | |
| navigator.clipboard?.writeText(themeJSON()).then(() => { | |
| const b = document.getElementById("copy"); const t = b.textContent; | |
| b.textContent = "Copied ✓"; setTimeout(() => b.textContent = t, 1200); | |
| })); | |
| } | |
| buildControls(); | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment