Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save eduwass/177411a4ec4e22bfc0c13dd321ad15ee to your computer and use it in GitHub Desktop.

Select an option

Save eduwass/177411a4ec4e22bfc0c13dd321ad15ee to your computer and use it in GitHub Desktop.
tmux-palette Terminal theme tuner — preview across common terminal schemes
<!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 &lt; 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