diff options
18 files changed, 2186 insertions, 204 deletions
diff --git a/assets/2026-07-03-instrument-console-panels-build-summary.org b/assets/2026-07-03-instrument-console-panels-build-summary.org index a7a3768..0602570 100644 --- a/assets/2026-07-03-instrument-console-panels-build-summary.org +++ b/assets/2026-07-03-instrument-console-panels-build-summary.org @@ -4,8 +4,8 @@ Findings summary for the no-approvals speedrun that rebuilt the net and bluetooth waybar panels as single-screen instrument consoles. Spec: -[[file:../docs/design/2026-07-03-instrument-console-panels-spec.org][2026-07-03-instrument-console-panels-spec.org]] (ID e73877f5, IMPLEMENTED). -Normative design: [[file:2026-07-03-instrument-console-panels-prototype.html][2026-07-03-instrument-console-panels-prototype.html]] +[[file:../docs/specs/2026-07-03-instrument-console-panels-spec.org][2026-07-03-instrument-console-panels-spec.org]] (ID e73877f5, IMPLEMENTED). +Normative design: [[file:../docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] (Craig approved through five prototype iterations). * What shipped diff --git a/docs/design/2026-07-02-timer-panel-spec.org b/docs/design/2026-07-02-timer-panel-spec.org deleted file mode 100644 index 2c9f7d4..0000000 --- a/docs/design/2026-07-02-timer-panel-spec.org +++ /dev/null @@ -1,113 +0,0 @@ -#+TITLE: Timer GTK Panel -#+AUTHOR: Craig Jennings -#+DATE: 2026-07-02 -#+TODO: TODO | DONE -#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED - -* DRAFT Status -:PROPERTIES: -:ID: 1770af2e-b093-4024-a512-ae4324a2869f -:END: -- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture "give the - timer a gtk UI/UX like the network panel. spec this out." - -* Metadata - -| Field | Value | -|--------+---------------------------------------------------| -| Status | draft | -|--------+---------------------------------------------------| -| Owner | Craig Jennings | -|--------+---------------------------------------------------| -| Repo | dotfiles | -|--------+---------------------------------------------------| -| Kin | net panel (architecture donor), wtimer (backing), | -| | desktop-settings panel spec (sibling) | -|--------+---------------------------------------------------| - -* Problem - -The timer's whole UI is a chain of three fuzzel prompts (type, value, label) -plus a fourth for cancel. That flow can't show what's already running while -you create, can't offer one-tap presets, gives no feedback on a typo until -the add silently fails, and pomodoro state (phase, cycle) is only visible in -a tooltip. The 2026-07-02 styling pass made the dialogs presentable, but the -shape is still four blind modals for what is really one small control -surface. - -* Goals - -1. One panel, opened from the bar's timer module, that shows everything - running (live countdowns, pomodoro phase/cycle, paused state) and creates - new items without leaving it. -2. One-tap presets for the common cases (tea, pomodoro, quick alarm) next to - freeform entry, with inline validation before the add. -3. Per-item controls: pause/resume, cancel, promote to primary (the bar - glyph slot). -4. wtimer stays the single owner of timer state and the notification path; - the panel is a view over it, never a second engine. - -* Design sketch - -** Architecture — clone the net panel's proven stack - -- GTK4 + gtk4-layer-shell dropdown anchored under the timer module, Blueprint - .blp compiled to committed .ui (=make ui=; compiler is dev-only). -- Humble-object split: GTK-free PanelModel presenter, unit-tested to 100%, - with thin widget bindings; one gated AT-SPI smoke via the - run-panel-smoke.sh pattern. -- Backing: shell out to the existing wtimer CLI (=add=, =toggle=, =cancel=, - =cycle=, =render=). =render= already emits a JSON payload; the panel polls - it (or subscribes to the same RTMIN+14 refresh signal) for live state. - wtimer's 89-case suite keeps owning the logic; panel tests fake the CLI - like every dotfiles suite fakes binaries. -- Dupre WIP palette CSS shared with the net panel (same factoring the - desktop-settings spec calls for — one palette asset, three panels). - -** Layout sketch - -- Header row: running-item count + a Clear All button (maps to cancel-all). -- Item list: one row per item — type glyph, label, live countdown / clock - time / phase+cycle for pomodoro, pause and cancel buttons, click-to-promote. -- Create strip: four type buttons (the wtimer glyphs), preset chips per type - (e.g. 5m / 15m / 25m / 60m for timers), a freeform entry validated with - wtimer's own parsers, an optional label field. -- Empty state: the create strip alone, centered. - -** What happens to the fuzzel flow - -The keybind/fuzzel path stays as the keyboard-fast lane (it's now styled and -tested); the panel replaces the click-driven path on the bar module. Whether -the fuzzel chain eventually retires is a decision below. - -* Decisions (Craig) - -** TODO Panel scope: standalone timer panel, or a page in the desktop-settings panel? -The desktop-settings spec (sibling DRAFT) could host timers as a page. -Standalone matches the net panel's one-domain-one-panel shape and keeps the -timer dropdown small; folding in means one panel binary fewer. Recommend -standalone, sharing the palette/css asset. - -** TODO Fuzzel flow: keep as keyboard fast lane, or retire once the panel lands? -Keeping both costs two creation paths to maintain (though the fuzzel chain is -small and freshly tested). Recommend keep until the panel proves itself, then -revisit. - -** TODO Presets: which chips per type? -Strawman: timer 5m/15m/25m/60m; alarm +30m/top-of-hour/07:00; pomodoro -default cycle only; stopwatch needs none. Adjust to taste. - -** TODO Live updates: poll render (1s, like the bar) or a wtimer "watch" mode? -Polling reuses what exists and matches the bar's cadence; a watch/subscribe -mode is cleaner but grows wtimer. Recommend polling first. - -* Implementation phases - -1. PanelModel presenter + CLI-backing seam (TDD, GTK-free, 100% like the net - PanelModel). -2. Blueprint UI: item list + create strip, wired to the presenter; palette - css factored to the shared asset. -3. Bar integration: timer module left-click opens the panel (replacing the - fuzzel menu binding there), RTMIN+14 refresh keeps bar and panel in step. -4. AT-SPI smoke + manual-testing checklist; decide the fuzzel flow's future - after a week of real use. diff --git a/assets/2026-07-03-instrument-console-panels-prototype.html b/docs/prototypes/2026-07-03-instrument-console-panels-prototype.html index 0258f20..0258f20 100644 --- a/assets/2026-07-03-instrument-console-panels-prototype.html +++ b/docs/prototypes/2026-07-03-instrument-console-panels-prototype.html diff --git a/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html new file mode 100644 index 0000000..3329cdb --- /dev/null +++ b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html @@ -0,0 +1,251 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Net panel — rescan affordance</title> +<style> +:root{ + --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; + --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; + --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; + --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; +} +*{box-sizing:border-box;margin:0;padding:0} +html{background:var(--ground)} +body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45; + background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} +.masthead{max-width:1200px;margin:0 auto 1.8rem} +.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} +h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} +.masthead p{color:var(--dim);font-size:.86rem;max-width:82ch} +.masthead p b{color:var(--silver)} + +.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1200px;margin:0 auto;align-items:flex-start} +.slot{width:400px} +.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem} +.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px; + box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px} + +.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto;box-shadow:0 0 6px 1px rgba(116,147,47,.55)} +.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.55)} +.lamp.off{background:var(--wash);box-shadow:none} +.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite} +@keyframes pulse{50%{opacity:.25}} + +.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px} +.b-id{display:flex;align-items:center;gap:9px} +.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em} +.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto} + +.chan{margin-top:12px} +.chan .line1{display:flex;align-items:baseline;gap:9px} +.chan .ssid{color:var(--cream);font-weight:700;font-size:14.5px} +.chan .line2{color:var(--dim);font-size:11.5px;margin-top:2px} +.ladder{display:inline-flex;gap:2px;align-items:flex-end;height:12px} +.ladder i{width:4px;background:var(--wash);border-radius:1px} +.ladder i:nth-child(1){height:4px}.ladder i:nth-child(2){height:7px} +.ladder i:nth-child(3){height:10px}.ladder i:nth-child(4){height:12px} +.ladder.l1 i:nth-child(-n+1){background:var(--gold)}.ladder.l2 i:nth-child(-n+2){background:var(--gold)} +.ladder.l3 i:nth-child(-n+3){background:var(--gold)}.ladder.l4 i{background:var(--gold)} + +/* engrave header with the rescan action */ +.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.24em;text-transform:uppercase; + display:flex;align-items:center;gap:8px;margin:14px 0 6px} +.engrave::before{content:"";height:1px;background:var(--wash);width:10px;flex:0 0 auto} +.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;cursor:pointer; + border-bottom:1px dotted transparent} +.engrave .cnt:hover{color:var(--gold);border-bottom-color:var(--wash)} +.engrave .cnt.scanning{color:var(--gold);animation:breathe 1.1s ease-in-out infinite;cursor:default;border-bottom-color:transparent} +.engrave .spacer{flex:1;height:1px;background:var(--wash)} +/* compact rescan glyph — sits right after the count, spins while scanning */ +.engrave .ricon{color:var(--dim);cursor:pointer;font-size:.82rem;display:inline-flex;line-height:1} +.engrave .ricon:hover{color:var(--gold)} +.engrave .ricon.spin{color:var(--gold);cursor:default;animation:spin .9s linear infinite} +.engrave .act{color:var(--dim);letter-spacing:.06em;text-transform:none;font-size:.72rem;cursor:pointer} +.engrave .act:hover{color:var(--gold)} +@keyframes spin{to{transform:rotate(360deg)}} +@keyframes breathe{0%,100%{opacity:1}50%{opacity:.35}} + +/* the list; section-breathe busy style pulses the whole well */ +#networks{border-radius:8px;transition:background .3s} +#networks.breathe{animation:sectionbreathe 1.4s ease-in-out infinite} +@keyframes sectionbreathe{0%,100%{background:transparent}50%{background:rgba(218,181,61,.06)}} + +.lamp-row{display:flex;align-items:center;gap:9px;padding:5px 6px;border-radius:7px;font-size:12.5px;cursor:pointer} +.lamp-row:hover{background:var(--wash)} +.lamp-row .who{color:var(--silver);white-space:nowrap} +.lamp-row .who b{color:var(--cream)} +.lamp-row .what{margin-left:auto;color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.lamp-row.fresh{animation:fadein .5s ease} +@keyframes fadein{from{opacity:0;transform:translateY(-3px);background:rgba(218,181,61,.12)}to{opacity:1}} + +.toast{margin-top:10px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px; + padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em} +.toast.show{opacity:1} + +.aside{flex:1 1 320px;min-width:290px} +.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:1.1rem 0 .5rem} +.aside h3:first-child{margin-top:.2rem} +.aside ul{list-style:none} +.aside li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative} +.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem} +.aside li b{color:var(--cream);font-weight:700} +.aside li em{color:var(--dim);font-style:normal} +.demo{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:1rem} +.demo .lbl{color:var(--steel);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;margin-bottom:.5rem} +.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden;margin-bottom:.7rem} +.seg button{flex:1;font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 6px;cursor:pointer} +.seg button:last-child{border-right:0} +.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700} +.demo .go{font:inherit;font-size:.8rem;color:var(--silver);background:transparent;border:1px solid var(--gold); + border-radius:8px;padding:.45rem 1rem;cursor:pointer} +.demo .go:hover{background:rgba(218,181,61,.12)} +.rec{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:.9rem;font-size:.82rem} +.rec b{color:var(--gold)} +@media (prefers-reduced-motion:reduce){*{animation:none!important}} +</style> +</head> +<body> + +<header class="masthead"> + <div class="eyebrow">archsetup · dupre panel family · net·01</div> + <h1>Networks — the rescan affordance</h1> + <p>Where does a WiFi rescan live, and how does it show it's working? The count "· N in range" is + really a live status field — the natural home for "scanning…". My lean: an explicit <b>⟳ rescan</b> + action in the engrave line (discoverable, same slot as "+ hidden"), with a <b>flash-and-fade</b> + busy state — the glyph spins, the count breathes, new rows fade in as found. The count is also + click-to-rescan as a shortcut. Use the selector on the right to feel each busy treatment.</p> +</header> + +<div class="stage"> + <div class="slot"> + <div class="slot-label">net·01 — networks section</div> + <div class="panel"> + <div class="b-face"> + <div class="b-id"> + <span class="lamp"></span> + <span class="state-word">ONLINE</span> + <span class="unit">NET·01</span> + </div> + </div> + + <div class="engrave">channel</div> + <div class="chan"> + <div class="line1"><span class="ssid">@Hyatt_WiFi</span> + <span class="ladder l3"><i></i><i></i><i></i><i></i></span> + <span class="dim" style="font-size:11px">-59 dBm · 44 ms</span></div> + <div class="line2">172.20.2.108/20 · gw 172.20.0.1 · route wlp170s0</div> + </div> + + <div class="engrave">networks<span class="cnt" id="cnt" onclick="rescan()">· 5 in range</span> + <span class="ricon" id="rescan" onclick="rescan()" title="Rescan for networks">⟳</span> + <span class="spacer"></span> + <span class="act" onclick="toast('would open the hidden-SSID dialog')">+ hidden</span> + </div> + <div id="networks"></div> + + <div class="toast" id="toast"></div> + </div> + </div> + + <div class="aside"> + <div class="demo"> + <div class="lbl">busy feedback style</div> + <div class="seg" id="styleSeg"> + <button class="on" data-s="all">all (rec)</button> + <button data-s="spin">spinner</button> + <button data-s="count">count</button> + <button data-s="section">section</button> + </div> + <button class="go" onclick="rescan()">▶ run a rescan</button> + </div> + + <h3>The three busy signals</h3> + <ul> + <li><b>Spinner</b> — the ⟳ glyph rotates while scanning. The clearest "working" cue; universal.</li> + <li><b>Count breathe</b> — "· 5 in range" becomes "scanning…" and slow-pulses. Your idea: the status field animates in place, no extra chrome.</li> + <li><b>Section breathe</b> — the whole list gives a faint gold breath while the scan runs; found rows fade in gold. Ambient, ties the animation to what's changing.</li> + <li><b>All (recommended)</b> — spinner + count + fade-in together. The section breathe is optional; it can read as busy on a small panel, so it's off in "all" by default and its own option to try.</li> + </ul> + + <h3>Why an explicit action, not only the count</h3> + <div class="rec"> + Overloading the count as the sole trigger is elegant but a first look doesn't know it's clickable. + An explicit <b>⟳ rescan</b> in the engrave line is discoverable, sits in the same slot as "+ hidden" + (consistent), and doesn't cost a heavy console key. Keeping the count clickable too gives power users + the shortcut without hiding the affordance. A dedicated <b>RESCAN console key</b> (next to DOCTOR / + SPEED TEST) is the third option — heavier, and rescan is a list action, not a diagnostic, so it fits + the engrave line better. + </div> + </div> +</div> + +<script> +const $=id=>document.getElementById(id); +let busy=false, style='all'; +let NETS=[ + {ssid:'@Hyatt_WiFi', sig:3, sec:'WPA2', stored:true, active:true}, + {ssid:'Hyatt_Meeting', sig:3, sec:'WPA2', stored:false, active:false}, + {ssid:'DIRECT-roku-882',sig:2, sec:'WPA2', stored:false, active:false}, + {ssid:'xfinitywifi', sig:1, sec:null, stored:false, active:false}, + {ssid:'HomeNet', sig:0, sec:'WPA2', stored:true, active:false, oor:true}, +]; +const NEWFOUND=[ + {ssid:'Hyatt_Guest', sig:2, sec:null, stored:false, active:false}, + {ssid:'Marriott_CONF', sig:1, sec:'WPA2', stored:false, active:false}, +]; +const pctFor=[null,'22%','44%','61%','78%']; + +$('styleSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return; + style=b.dataset.s;[...$('styleSeg').children].forEach(x=>x.classList.toggle('on',x===b));}); + +function rowEl(n,fresh){ + const r=document.createElement('div'); r.className='lamp-row'+(fresh?' fresh':''); + const lamp=n.active?'lamp':(n.oor?'lamp off':'lamp gold'); + const what=n.active?'active · '+(n.sec||'open') + :n.oor?'stored · out of range' + :(n.stored?'stored · ':'')+(n.sec||'open')+' · '+pctFor[n.sig]; + r.innerHTML=`<span class="${lamp}"></span><span class="who">${n.active?'<b>'+n.ssid+'</b>':n.ssid}</span>`+ + (!n.active&&!n.oor?`<span class="ladder l${n.sig}" style="margin-left:6px"><i></i><i></i><i></i><i></i></span>`:'')+ + `<span class="what">${what}</span>`; + r.onclick=()=>{ if(busy)return; toast(n.active?'already on '+n.ssid:'would join '+n.ssid); }; + return r; +} +function render(freshSet){ + const host=$('networks'); host.innerHTML=''; + const inRange=NETS.filter(n=>!n.oor).sort((a,b)=>(b.active-a.active)||(b.sig-a.sig)); + const oor=NETS.filter(n=>n.oor); + [...inRange,...oor].forEach(n=>host.appendChild(rowEl(n,freshSet&&freshSet.has(n.ssid)))); + if(!busy) $('cnt').textContent='· '+inRange.length+' in range'; +} +function rescan(){ + if(busy) return; busy=true; + const useSpin = style==='all'||style==='spin'; + const useCount= style==='all'||style==='count'; + const useSec = style==='section'; + if(useSpin) $('rescan').classList.add('spin'); + if(useCount){ $('cnt').classList.add('scanning'); $('cnt').textContent='scanning…'; } + if(useSec) $('networks').classList.add('breathe'); + toast('scanning for networks…'); + // networks trickle in as "found" + const fresh=new Set(); + setTimeout(()=>{ NETS.splice(1,0,NEWFOUND[0]); fresh.add(NEWFOUND[0].ssid); render(fresh); },900); + setTimeout(()=>{ NETS.splice(3,0,NEWFOUND[1]); fresh.add(NEWFOUND[1].ssid); render(fresh); },1700); + setTimeout(()=>{ + busy=false; + $('rescan').classList.remove('spin'); + $('cnt').classList.remove('scanning'); + $('networks').classList.remove('breathe'); + const n=NETS.filter(x=>!x.oor).length; + $('cnt').textContent='· '+n+' in range'; + toast('scan complete — '+n+' networks in range'); + // reset for the next run so the demo is repeatable + NETS=NETS.filter(x=>x.ssid!=='Hyatt_Guest'&&x.ssid!=='Marriott_CONF'); + },2600); +} +render(); +</script> +</body> +</html> diff --git a/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html new file mode 100644 index 0000000..8e642f4 --- /dev/null +++ b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html @@ -0,0 +1,338 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Panel widget gallery — dupre instrument console</title> +<style> +:root{ + --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; + --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; + --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; + --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; +} +*{box-sizing:border-box;margin:0;padding:0} +html{background:var(--ground)} +body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-height:1.45; + background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} +.wrap{max-width:1320px;margin:0 auto} +.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} +h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} +.masthead p{color:var(--dim);font-size:.86rem;max-width:86ch} +.masthead p b{color:var(--silver)} +h2{color:var(--steel);font-size:.74rem;letter-spacing:.24em;text-transform:uppercase; + margin:2.2rem 0 .2rem;display:flex;align-items:center;gap:12px} +h2::after{content:"";height:1px;background:var(--wash);flex:1} + +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:14px;margin-top:1rem} +.card{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:12px; + padding:13px 14px 12px;display:flex;flex-direction:column;gap:9px; + box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 6px 14px rgba(0,0,0,.4)} +.wname{color:var(--cream);font-size:.82rem;font-weight:700;display:flex;align-items:center;gap:8px} +.wname .no{color:var(--panel);background:var(--gold);border-radius:4px;font-size:.6rem;padding:0 5px;font-weight:400} +.stagew{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:14px 12px; + min-height:78px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap} +.wnote{color:var(--dim);font-size:.72rem;line-height:1.4} +.wnote b{color:var(--steel);font-weight:400} + +/* ---- shared primitives ---- */ +.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)} +.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)} +.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} +.lamp.off{background:var(--wash);box-shadow:none} +.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite} +@keyframes pulse{50%{opacity:.25}} + +.switch{width:40px;height:21px;border-radius:11px;background:var(--wash);border:1px solid var(--slate); + position:relative;cursor:pointer} +.switch::after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%; + background:var(--dim);transition:left .15s} +.switch.on{background:var(--slate);border-color:var(--gold)} +.switch.on::after{left:21px;background:var(--gold)} +.switch.red{background:rgba(203,107,77,.2);border-color:var(--fail)} +.switch.red::after{background:var(--fail);left:2px} + +.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer; + background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a; + border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} +.key:hover{color:var(--gold);border-color:var(--gold)} +.key:active{transform:translateY(1px)} +.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700} +.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} +.key.off{opacity:.4} + +.chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px} +.chip.on{color:var(--gold);border-color:var(--gold)} + +.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px} +.badge.red{background:var(--fail);color:var(--cream)} +.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)} + +/* fader */ +.fader{width:150px;height:16px;position:relative;cursor:pointer} +.fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} +.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold))} +.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px; + background:linear-gradient(180deg,#f0d879,#caa233);border:1px solid #7a6414;box-shadow:0 1px 2px rgba(0,0,0,.5)} +/* vertical fader */ +.vfader{width:16px;height:64px;position:relative;cursor:pointer} +.vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden} +.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#8a7524,var(--gold))} +.vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px; + background:linear-gradient(90deg,#caa233,#f0d879);border:1px solid #7a6414} + +/* rotary knob */ +.knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:pointer; + background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c; + box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)} +.knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi); + margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(255,215,95,.6)} +.knob-scale{position:relative;width:64px;height:40px} +.knob-scale .arc{position:absolute;inset:0 0 -24px 0;border:1.5px solid var(--wash);border-top-color:var(--gold);border-radius:50%;opacity:.5} + +/* needle gauge */ +.gauge{width:96px} +.gauge .dial{position:relative;height:48px;overflow:hidden} +.gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%} +.gauge .tk{position:absolute;left:50%;bottom:0;width:1.5px;height:8px;background:var(--steel);transform-origin:50% 48px} +.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi); + transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(255,215,95,.5); + transition:transform .5s cubic-bezier(.3,1.3,.5,1)} +.gauge .hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)} +.gauge .gv{color:var(--cream);text-align:center;font-size:12px;font-weight:700;margin-top:5px;font-variant-numeric:tabular-nums} + +/* segmented VU / LED bar */ +.vu{width:170px;display:flex;flex-direction:column;gap:5px} +.vurow{display:flex;align-items:center;gap:7px} +.vurow .ch{color:var(--steel);font-size:.6rem;width:8px} +.vubar{flex:1;display:flex;gap:2px;height:9px} +.vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3} +.vubar i.on{opacity:1;background:var(--pass)}.vubar i.hot{opacity:1;background:var(--gold)} +.vubar i.clip{opacity:1;background:var(--fail)}.vubar i.peak{outline:1px solid var(--gold-hi);outline-offset:-1px} + +/* mini 4-bar signal */ +.sig{display:flex;align-items:flex-end;gap:2px;height:18px} +.sig i{width:4px;background:var(--wash);border-radius:1px} +.sig i:nth-child(1){height:5px}.sig i:nth-child(2){height:9px}.sig i:nth-child(3){height:13px}.sig i:nth-child(4){height:17px} +.sig i.on{background:var(--pass)}.sig i.hot{background:var(--gold)}.sig i.clip{background:var(--fail)} + +/* signal ladder (wifi bars) */ +.ladder{display:inline-flex;gap:3px;align-items:flex-end;height:18px} +.ladder i{width:5px;background:var(--wash);border-radius:1px} +.ladder i:nth-child(1){height:6px}.ladder i:nth-child(2){height:10px} +.ladder i:nth-child(3){height:14px}.ladder i:nth-child(4){height:18px} +.ladder.l3 i:nth-child(-n+3){background:var(--gold)} + +/* linear progress / fuel bar */ +.bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative} +.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold));border-radius:6px} +.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))} + +/* radial ring */ +.ring{width:60px;height:60px;border-radius:50%; + background:conic-gradient(var(--gold) calc(var(--p)*1%),var(--wash) 0); + display:grid;place-items:center;position:relative} +.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)} +.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums} + +/* tabular readout */ +.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em} +.readout small{color:var(--dim);font-size:12px;font-weight:400} +.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px} + +/* sparkline */ +.spark{width:170px;height:44px} +.spark svg{display:block;width:100%;height:100%} + +/* lamp row (list item) */ +.lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px} +.lrow:hover{background:var(--wash)} +.lrow .who{color:var(--silver)}.lrow .who b{color:var(--cream)} +.lrow .what{margin-left:auto;color:var(--dim);font-size:11px} + +/* arm-to-fire */ +.arm{font:inherit;font-size:11.5px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b; + border-radius:8px;padding:7px 12px} +.arm.armed{background:rgba(203,107,77,.12);border-color:var(--fail);color:var(--fail)} + +/* stepper / segmented selector */ +.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden} +.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer} +.seg button:last-child{border-right:0} +.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700} + +/* engraved section label */ +.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase; + display:flex;align-items:center;gap:9px} +.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1} +.engrave::before{max-width:10px} +.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none} + +/* waveform strip */ +.wave{width:170px;height:38px} +.wave svg{width:100%;height:100%;display:block} + +/* toast */ +.toastw{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:5px 10px} + +/* output well (log step) */ +.owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px} +.ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0} +.ostep .lamp{margin-top:3px;width:7px;height:7px} +.ostep b{color:var(--cream);font-weight:700}.ostep .ev{color:var(--steel);display:block;font-size:10.5px} + +@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} +</style> +</head> +<body> +<div class="wrap"> +<header class="masthead"> + <div class="eyebrow">archsetup · dupre panel family</div> + <h1>Widget gallery — the instrument-console kit</h1> + <p>Every control + display idiom we can build in the dupre faceplate language, all rendering from + the same tokens the net / bt / sound panels use. <b>Controls</b> take input; <b>meters & + gauges</b> show a live analog value; <b>indicators & readouts</b> show state or a number. + Live ones animate. Pick what fits each job — most cost pure CSS; the two that need a real + drawing surface (needle gauge, waveform) are flagged in their notes.</p> +</header> + +<h2>Controls — take input</h2> +<div class="grid" id="controls"></div> + +<h2>Meters & gauges — live analog value</h2> +<div class="grid" id="meters"></div> + +<h2>Indicators & readouts — state or number</h2> +<div class="grid" id="indicators"></div> + +</div> +<script> +const $ = id => document.getElementById(id); +const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; +function card(host, no, name, html, note){ + const c=document.createElement('div'); c.className='card'; + c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}</div>`+ + `<div class="stagew">${html}</div><div class="wnote">${note}</div>`; + host.appendChild(c); return c; +} +function buildBars(el,n){el.innerHTML='';for(let k=0;k<n;k++)el.appendChild(document.createElement('i'));} + +/* ============ CONTROLS ============ */ +const C=$('controls'); +card(C,'01','Toggle switch', + `<span class="switch on" onclick="this.classList.toggle('on')"></span> + <span class="switch red"></span>`, + '<b>on / off / muted.</b> The faceplate master control — wifi radio, bt power, master-mute. Click to flip.'); +card(C,'02','Console key', + `<button class="key on">LIVE</button><button class="key">SCAN</button><button class="key red">MUTED</button>`, + '<b>physical push button.</b> DOCTOR / SPEED TEST / mic mode. Gold = engaged, terracotta = off.'); +card(C,'03','Horizontal fader', + `<div class="fader" id="f1"><div class="slot"><div class="fill" style="width:68%"></div></div><div class="cap" style="left:68%"></div></div>`, + '<b>continuous 0-100.</b> Per-device volume, brightness, kbd backlight. Drag; the gold cap tracks.'); +card(C,'04','Vertical fader', + `<div class="vfader"><div class="slot"><div class="fill" style="height:60%"></div></div><div class="cap" style="bottom:60%"></div></div> + <div class="vfader"><div class="slot"><div class="fill" style="height:35%"></div></div><div class="cap" style="bottom:35%"></div></div>`, + '<b>channel-strip style.</b> A mixer column per device if you want the classic board look.'); +card(C,'05','Rotary knob', + `<span class="knob" id="knob" onclick="bumpKnob()"><span class="ind" id="kind"></span></span>`, + '<b>dial in a value.</b> Volume/gain the analog way. Click to turn; drag in the real build. Pairs with a scale arc.'); +card(C,'06','Segmented selector', + `<div class="seg"><button class="on">TIMER</button><button>ALARM</button><button>POMO</button></div>`, + '<b>pick one of a few.</b> Timer type, layout mode, theme. One press-lit segment.'); +card(C,'07','Chip toggle', + `<span class="chip on" onclick="this.classList.toggle('on')">discoverable on</span>`, + '<b>inline binary.</b> A soft toggle inside a line of text — discoverable, auto-dim, DND. Gold when on.'); +card(C,'08','Arm-to-fire', + `<button class="arm" id="arm" onclick="armFire()">forget</button>`, + '<b>two-stage confirm.</b> Destructive/disruptive actions — forget network, disconnect. First click arms (red), second fires.'); +card(C,'09','Lamp row', + `<div class="lrow"><span class="lamp gold"></span><span class="who"><b>WH-1000XM4</b></span><span class="what">tap to connect</span></div>`, + '<b>actionable list item.</b> The net/bt/sound row: lamp + name + status, click acts. The workhorse.'); + +/* ============ METERS & GAUGES ============ */ +const M=$('meters'); +card(M,'10','Needle gauge', + `<div class="gauge"><div class="dial"><div class="arc"></div> + <div class="tk" style="transform:rotate(-60deg)"></div><div class="tk" style="transform:rotate(0)"></div><div class="tk" style="transform:rotate(60deg)"></div> + <div class="ndl" id="g1"></div><div class="hub"></div></div><div class="gv"><span id="g1v">0</span>%</div></div>`, + '<b>analog dial.</b> Throughput, battery, volume level. <b>Needs a Cairo/GTK drawing area</b> — CSS can fake a fixed angle but not a smooth sweep in waybar.'); +card(M,'11','Stereo VU (LED bar)', + `<div class="vu"><div class="vurow"><span class="ch">L</span><span class="vubar" id="vuL"></span></div> + <div class="vurow"><span class="ch">R</span><span class="vubar" id="vuR"></span></div></div>`, + '<b>live signal level.</b> The sound panel\'s second meter row. Peak-hold outline. Pure CSS — pango/box segments.'); +card(M,'12','Mini signal (4-bar)', + `<span class="sig" id="mini"></span>`, + '<b>compact activity.</b> Per-row "is this device playing" indicator. Cheap enough to sit in every list row.'); +card(M,'13','Signal ladder', + `<span class="ladder l3"><i></i><i></i><i></i><i></i></span>`, + '<b>discrete strength.</b> Wifi bars, bt RSSI — a stepped 0-4. Already in the net panel.'); +card(M,'14','Linear fuel bar', + `<div class="bar"><span style="width:72%"></span></div><div class="bar warn"><span style="width:12%"></span></div>`, + '<b>a single 0-100.</b> Battery, disk, download progress. Warn tint under threshold. Trivial in CSS.'); +card(M,'15','Radial ring', + `<span class="ring" style="--p:68"><b>68</b></span>`, + '<b>percentage as a donut.</b> CPU, battery, a single meter where a needle is overkill. conic-gradient, pure CSS.'); +card(M,'16','Sparkline', + `<span class="spark" id="spark"><svg viewBox="0 0 170 44" preserveAspectRatio="none"><polyline id="sparkp" fill="none" stroke="var(--gold-hi)" stroke-width="1.5"/></svg></span>`, + '<b>recent history.</b> Throughput/CPU over the last minute. SVG here; a drawing area in GTK.'); +card(M,'17','Waveform strip', + `<span class="wave" id="wave"><svg viewBox="0 0 170 38" preserveAspectRatio="none"><path id="wavep" fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg></span>`, + '<b>audio waveform / scope.</b> A richer signal view for the sound panel. <b>Needs a drawing surface.</b>'); + +/* ============ INDICATORS & READOUTS ============ */ +const I=$('indicators'); +card(I,'18','Status lamp', + `<span class="lamp"></span><span class="lamp gold"></span><span class="lamp red"></span><span class="lamp off"></span><span class="lamp busy"></span>`, + '<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. The family signature.'); +card(I,'19','Badge / tag', + `<span class="badge">TUNNEL</span> <span class="badge red">LOW BATT</span> <span class="badge ghost">2.4G</span>`, + '<b>a labelled flag.</b> On the faceplate or a row — MUTED, AIRPLANE, DEF, a band tag.'); +card(I,'20','Tabular readout', + `<div style="text-align:center"><div class="readout">24:10</div><span class="u">timer</span></div> + <div style="text-align:center"><div class="readout">68<small>%</small></div></div>`, + '<b>a precise number.</b> Clock, countdown, volume %. BerkeleyMono tabular-nums so digits don\'t jitter.'); +card(I,'21','Engraved label', + `<span class="engrave">outputs<span class="cnt">· 3</span></span>`, + '<b>section divider.</b> The hairline-flanked caps label with a count. Groups a panel into readable blocks.'); +card(I,'22','Output well', + `<div class="owell"><div class="ostep"><span class="lamp"></span><span><b>Link</b><span class="ev">wlp170s0 · @Hyatt</span></span></div> + <div class="ostep"><span class="lamp gold"></span><span><b>DNS</b><span class="ev">resolving…</span></span></div></div>`, + '<b>streaming step log.</b> The doctor/scan output — lamp-per-step with evidence. For any run-and-report action.'); +card(I,'23','Toast / status line', + `<span class="toastw">joined @Hyatt_WiFi — saved</span>`, + '<b>transient confirmation.</b> The one-line result after an action. Auto-dismiss; red variant for errors.'); + +/* ---- live animation ---- */ +let ph=0, kang=140; +function bumpKnob(){ kang=(kang+35)%300-0; $('kind').style.transform=`rotate(${kang-150}deg)`; } +function armFire(){ const a=$('arm'); if(a.classList.contains('armed')){a.classList.remove('armed');a.textContent='forget';} + else{a.classList.add('armed');a.textContent='forget? again';} } +buildBars($('vuL'),16); buildBars($('vuR'),16); buildBars($('mini'),0); +$('mini').innerHTML='<i></i><i></i><i></i><i></i>'; +$('kind').style.transform=`rotate(${kang-150}deg)`; +const hist=Array.from({length:40},()=>0.5); +function paintVU(el,l,pk){const b=el.children,n=b.length,lit=Math.round(l*n); + pk.v=Math.max(lit,(pk.v||0)-0.4);const p=Math.round(pk.v); + for(let k=0;k<n;k++){let c=k<lit?(k>=n-2?'clip':k>=n-4?'hot':'on'):'';if(p>0&&k===p-1)c=(c?c+' ':'')+'peak';b[k].className=c;}} +const pkL={v:0},pkR={v:0}; +function paintMini(el,l){const b=el.children,lit=Math.round(l*4);for(let k=0;k<4;k++)b[k].className=k<lit?(k>=3?'clip':k>=2?'hot':'on'):'';} +function lvl(){return Math.max(0,Math.min(1,0.5+0.4*Math.sin(ph*1.3)+ (Math.random()<0.15?Math.random()*0.4:0) - Math.random()*0.08));} +function tick(){ + ph+=0.09; + const a=lvl(), b=lvl(); + paintVU($('vuL'),a,pkL); paintVU($('vuR'),b,pkR); paintMini($('mini'),a); + // needle sweeps 0..100 + const gv=Math.round(50+45*Math.sin(ph*0.7)); + $('g1').style.transform=`rotate(${-60+gv/100*120}deg)`; $('g1v').textContent=gv; + // sparkline + hist.push(0.5+0.42*Math.sin(ph*0.9)+ (Math.random()-0.5)*0.25); hist.shift(); + $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-Math.max(0,Math.min(1,v))*40-2}`).join(' ')); + // waveform + let d='M0 19'; for(let x=0;x<=170;x+=3){const y=19+Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)*14; d+=` L${x} ${y.toFixed(1)}`;} + $('wavep').setAttribute('d',d); +} +if(!reduced) setInterval(tick,80); else tick(); +</script> +</body> +</html> diff --git a/docs/prototypes/2026-07-03-sound-panel-prototype.html b/docs/prototypes/2026-07-03-sound-panel-prototype.html new file mode 100644 index 0000000..d75f566 --- /dev/null +++ b/docs/prototypes/2026-07-03-sound-panel-prototype.html @@ -0,0 +1,417 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Sound — instrument console (pulsemixer)</title> +<style> +:root{ + --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; + --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; + --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; + --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; +} +*{box-sizing:border-box;margin:0;padding:0} +html{background:var(--ground)} +body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45; + background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} +.masthead{max-width:1280px;margin:0 auto 1.8rem} +.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} +h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} +.masthead p{color:var(--dim);font-size:.86rem;max-width:82ch} +.masthead p b{color:var(--silver);font-weight:700} + +.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1280px;margin:0 auto;align-items:flex-start} +.slot{width:400px} +.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem} + +.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px; + box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px;position:relative;overflow:hidden} + +.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto; + box-shadow:0 0 6px 1px rgba(116,147,47,.55)} +.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} + +.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px} +.b-id{display:flex;align-items:center;gap:9px} +.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em} +.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto} +.b-id .g{font-size:17px;color:var(--cream)} +.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold); + border-radius:4px;padding:1px 6px;display:none} +.badge.show{display:inline-block} +.badge.red{background:var(--fail);color:var(--cream)} +.x-btn{margin-left:6px;color:var(--dim);border:0;background:transparent;font:inherit;font-size:1rem; + cursor:pointer;border-radius:50%;width:26px;height:26px;line-height:1;flex:0 0 auto} +.x-btn:hover{background:var(--wash);color:var(--silver)} +/* faceplate master quick-mute — same switch idiom as net wifi / bt power */ +.switch{width:38px;height:20px;border-radius:10px;background:var(--wash); + border:1px solid var(--slate);position:relative;flex:0 0 auto;cursor:pointer} +.switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px; + border-radius:50%;background:var(--dim);transition:left .15s} +.switch.on{background:var(--slate);border-color:var(--gold)} +.switch.on::after{left:19px;background:var(--gold)} +.switch.muted{background:rgba(203,107,77,.2);border-color:var(--fail)} +.switch.muted::after{background:var(--fail);left:2px} + +.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.32em;text-transform:uppercase; + display:flex;align-items:center;gap:10px;margin:13px 0 6px} +.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1} +.engrave::before{max-width:12px} +.engrave .cnt{color:var(--dim);letter-spacing:.12em;margin-left:2px;text-transform:none;font-size:.62rem} + +/* device row — a FIXED grid so nothing can overflow the plate: + [signal] [name] [fader] [pct] [mute]. minmax(0,1fr) lets the name shrink + and ellipsis instead of forcing the row wider than the panel. */ +.dev{display:grid;grid-template-columns:15px minmax(0,1fr) 84px 32px 20px; + align-items:center;gap:8px;padding:6px 5px;border-radius:8px;cursor:default} +.dev:hover{background:var(--wash)} +.dev .who{min-width:0;display:flex;align-items:center;gap:6px;color:var(--silver)} +.dev .who .g{font-size:14px;color:var(--dim);flex:0 0 auto} +.dev.active .who .g{color:var(--gold)} +.dev .who .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.dev.active .who .nm{color:var(--cream);font-weight:700} +.dev.muted .who .nm{color:var(--dim)} +.dev .def{font-size:.5rem;letter-spacing:.14em;color:var(--panel);background:var(--gold); + border-radius:3px;padding:0 4px;flex:0 0 auto;display:none} +.dev.active .def{display:inline-block} +/* fader — machined slot + gold cap; width-based fill, bounded to its cell */ +.fader{height:16px;position:relative;cursor:pointer} +.fader .trk{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:var(--well); + border:1px solid #231f18;overflow:hidden} +.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold))} +.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px; + background:linear-gradient(180deg,#f0d879,#caa233);border:1px solid #7a6414;box-shadow:0 1px 2px rgba(0,0,0,.5)} +.dev.muted .fill{background:var(--wash)} +.dev.muted .cap{background:linear-gradient(180deg,#6a6a6a,#3f3f3f);border-color:#2a2a2a} +.pct{color:var(--cream);font-size:11.5px;font-variant-numeric:tabular-nums;text-align:right} +.dev.muted .pct{color:var(--fail)} +.mute-b{color:var(--dim);border:0;background:transparent;font:inherit;font-size:.9rem;cursor:pointer; + border-radius:5px;padding:0;justify-self:center;line-height:1} +.mute-b:hover{color:var(--silver)} +.dev.muted .mute-b{color:var(--fail)} +/* per-row signal mini-meter — 4 bars that light when THIS device has a live + stream, so the sink/source actually playing is visible before you pick it */ +.sig{display:flex;align-items:flex-end;gap:1.5px;height:14px;justify-self:center} +.sig i{width:2.5px;background:var(--wash);border-radius:1px} +.sig i:nth-child(1){height:4px}.sig i:nth-child(2){height:7px} +.sig i:nth-child(3){height:10px}.sig i:nth-child(4){height:13px} +.sig i.on{background:var(--pass)}.sig i.hot{background:var(--gold)}.sig i.clip{background:var(--fail)} + +/* mic mode — three console keys */ +.modes{display:flex;gap:8px;margin-top:2px} +.mode{flex:1;text-align:center;cursor:pointer;font:inherit;font-size:11px;letter-spacing:.06em; + background:linear-gradient(180deg,#23211e,#191715);color:var(--silver); + border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:8px;padding:8px 4px; + box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} +.mode:hover{color:var(--gold);border-color:var(--gold)} +.mode.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700} +.mode.on.mute{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream)} +.mode .k{display:block;font-size:.54rem;letter-spacing:.14em;color:var(--dim);margin-top:2px} +.mode.on .k{color:rgba(16,15,15,.7)} +.ptt-hint{color:var(--dim);font-size:10.5px;text-align:center;margin-top:6px;min-height:1.2em} +.ptt-hint.live{color:var(--gold)} + +/* meter row 1 — the volume dials you set (needles), OUT + IN */ +.meters{display:flex;gap:12px;margin-top:10px} +.meter{flex:1;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:9px 10px 7px;position:relative} +.meter .mode-tag{position:absolute;top:6px;left:8px;font-size:.56rem;letter-spacing:.2em;color:var(--pass)} +.meter .mode-tag.mut{color:var(--fail)} +.meter .dial{position:relative;height:50px;overflow:hidden;margin-top:13px} +.meter .arc{position:absolute;inset:0 0 -50px 0;border:2px solid var(--wash);border-radius:50%} +.meter .tick{position:absolute;left:50%;bottom:0;width:1.5px;height:9px;background:var(--steel);transform-origin:50% 50px} +.meter .needle{position:absolute;left:50%;bottom:0;width:2px;height:42px;background:var(--gold-hi); + transform-origin:50% 100%;transform:rotate(20deg);border-radius:2px; + box-shadow:0 0 6px rgba(255,215,95,.5);transition:transform .3s cubic-bezier(.3,1.3,.5,1)} +.meter .needle.mut{background:var(--fail);box-shadow:0 0 6px rgba(203,107,77,.5)} +.meter .hub{position:absolute;left:50%;bottom:-4px;width:9px;height:9px;margin-left:-4.5px;border-radius:50%;background:var(--gold)} +.meter .m-value{color:var(--cream);font-size:13px;text-align:center;font-weight:700;margin-top:6px;font-variant-numeric:tabular-nums} +.meter .m-value small{color:var(--dim);font-weight:400} +.meter .m-label{color:var(--steel);font-size:.6rem;letter-spacing:.2em;text-align:center;margin-top:2px; + white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + +/* meter row 2 — the stereo VU pair: the live SIGNAL through the selected + output. Confirms which device is actually carrying the audio. */ +.vupair{margin-top:10px;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:9px 11px 8px} +.vuhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:7px} +.vuhead .t{color:var(--steel);font-size:.56rem;letter-spacing:.2em;text-transform:uppercase} +.vuhead .src{color:var(--cream);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%} +.vurow{display:flex;align-items:center;gap:8px;margin:4px 0} +.vurow .ch{color:var(--steel);font-size:.62rem;width:9px;flex:0 0 auto} +.vubar{flex:1;display:flex;gap:2px;height:9px} +.vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3} +.vubar i.on{opacity:1;background:var(--pass)} +.vubar i.hot{opacity:1;background:var(--gold)} +.vubar i.clip{opacity:1;background:var(--fail)} +.vubar i.peak{outline:1px solid var(--gold-hi);outline-offset:-1px} +.vupair.mute .vubar i{background:var(--wash);opacity:.22} +.vupair.mute .vuhead .src{color:var(--fail)} + +.toast{margin-top:10px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px; + padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em} +.toast.show{opacity:1} +.toast.err{background:transparent;border:1px solid var(--fail);color:var(--fail)} + +.aside{flex:1 1 320px;min-width:300px} +.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:1.1rem 0 .5rem} +.aside h3:first-child{margin-top:.2rem} +.aside ul{list-style:none} +.aside li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative} +.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem} +.aside li b{color:var(--cream);font-weight:700} +.aside li em{color:var(--dim);font-style:normal} +.barbits{display:flex;gap:14px;margin:.4rem 0 .2rem;align-items:center;flex-wrap:wrap} +.barbits span{display:flex;align-items:center;gap:7px;color:var(--silver);font-size:.82rem} +.barbits .g{font-size:19px;color:var(--cream)} +.barbits .g.mut{color:var(--fail)} +.rec{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:.9rem;font-size:.82rem;color:var(--silver)} +.rec b{color:var(--gold)} +*{scrollbar-width:thin;scrollbar-color:var(--slate) transparent} +::-webkit-scrollbar{width:6px;height:6px} +::-webkit-scrollbar-thumb{background:var(--slate);border-radius:4px} +@media (prefers-reduced-motion:reduce){.needle{transition:none}} +</style> +</head> +<body> + +<header class="masthead"> + <div class="eyebrow">archsetup · dupre panel family · instrument consoles</div> + <h1>Sound — the pulsemixer console</h1> + <p>Same faceplate as net + bluetooth. The bar's <b>sound glyph</b> opens this. Every sink and + source is a row: <b>click the row body to make it default</b>, drag the fader for its volume, + hit the glyph to mute just it. Each row has a <b>live-signal meter</b> — the device actually + carrying audio dances even when it isn't the default, so you can <b>find the one playing the + meeting</b> and click it. The faceplate switch is the <b>master quick-mute</b>; the mic carries + <b>live · muted · push-to-talk</b> (hold Space). Row 1 of gauges is the volume you set; row 2 is + the <b>stereo VU</b> of the selected output's live signal.</p> +</header> + +<div class="stage"> + <div class="slot"> + <div class="slot-label">snd·01 — pulsemixer in console form</div> + <div class="panel"> + + <div class="b-face"> + <div class="b-id"> + <span class="lamp" id="lamp"></span> + <span class="g" id="face-g"></span> + <span class="state-word" id="state">PLAYBACK</span> + <span class="badge red" id="mute-badge">MUTED</span> + <span class="unit">SND·01</span> + <span class="switch on" id="master" onclick="masterMute()" title="Master quick-mute (Super+Shift+M)"></span> + <button class="x-btn" title="Close (Esc)">✕</button> + </div> + </div> + + <div class="engrave">outputs<span class="cnt" id="out-cnt"></span></div> + <div id="outputs"></div> + + <div class="engrave">inputs<span class="cnt" id="in-cnt"></span></div> + <div id="inputs"></div> + + <div class="engrave">mic mode</div> + <div class="modes"> + <button class="mode" id="md-toggle" onclick="micToggle()">LIVE<span class="k">Super+Shift+A</span></button> + <button class="mode" id="md-ptt" onclick="micPtt()">PUSH·TALK<span class="k">hold Space</span></button> + </div> + <div class="ptt-hint" id="ptt-hint"></div> + + <!-- meter row 1 — volume you set --> + <div class="meters"> + <div class="meter"> + <span class="mode-tag" id="vt-out">OUT VOL</span> + <div class="dial"><div class="arc"></div> + <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> + <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> + <div class="tick" style="transform:rotate(60deg)"></div> + <div class="needle" id="n-out"></div><div class="hub"></div></div> + <div class="m-value"><span id="v-out">68</span> <small>%</small></div> + <div class="m-label" id="l-out">SPEAKERS</div> + </div> + <div class="meter"> + <span class="mode-tag" id="vt-in">IN VOL</span> + <div class="dial"><div class="arc"></div> + <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div> + <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div> + <div class="tick" style="transform:rotate(60deg)"></div> + <div class="needle" id="n-in"></div><div class="hub"></div></div> + <div class="m-value"><span id="v-in">54</span> <small>%</small></div> + <div class="m-label" id="l-in">BUILT-IN MIC</div> + </div> + </div> + + <!-- meter row 2 — stereo VU of the selected output's live signal --> + <div class="vupair" id="vupair"> + <div class="vuhead"><span class="t">signal · VU peak</span><span class="src" id="vu-src">SPEAKERS</span></div> + <div class="vurow"><span class="ch">L</span><span class="vubar" id="vu-l"></span></div> + <div class="vurow"><span class="ch">R</span><span class="vubar" id="vu-r"></span></div> + </div> + + <div class="toast" id="toast"></div> + </div> + </div> + + <div class="aside"> + <h3>The bar glyph</h3> + <div class="barbits"> + <span><span class="g"></span> normal — speaker + arcs</span> + <span><span class="g mut"></span> muted — speaker ✕</span> + <span><span class="g" style="color:var(--gold)"></span> ptt armed</span> + </div> + <h3>Idiom map (same as net / bt)</h3> + <ul> + <li><b>Faceplate switch = master quick-mute</b> — the net wifi / bt power switch, here muting all output. Flip: state → MUTED, lamp red, bar glyph → speaker-✕.</li> + <li><b>Rows are devices</b> — every sink + source. <b>Click the row body</b> to set default (gold DEF moves); the fader sets that device's volume; the trailing glyph mutes just it.</li> + <li><b>Per-row signal meter</b> — the 4 bars at the left of each row read that device's <b>live</b> level. A sink can carry a stream without being default, so the one playing lights up — <em>demo: the music is on WH-1000XM4 while Speakers is still default. Click WH-1000XM4 to move to it.</em></li> + <li><b>Two meter rows</b> — row 1 = the volume you set (OUT + IN needles); row 2 = the <b>stereo VU</b> (L/R) of the selected output's live signal, red when muted.</li> + <li><b>Mic = two console keys</b> — one toggles LIVE↔MUTED (the label flips to show the state), the other is PUSH·TALK: the mic sits muted and un-mutes only while Space is held.</li> + <li><b>Verify-everything</b> — every action re-reads pactl/wpctl state after firing, like net/bt.</li> + </ul> + <h3>Push-to-talk — the one hard part</h3> + <div class="rec"> + Hold-to-talk needs a global key grab under Wayland. Two routes to spec: <b>(a)</b> a Hyprland + <b>bind pair</b> — <em>bindp</em> Space press → unmute, release → re-mute, armed only while PTT + mode is active (so it doesn't steal Space everywhere); or <b>(b)</b> an <b>evdev/libinput + listener</b> reading the key directly. (a) is lighter; (b) survives focus changes but needs + input-group permissions. Feasibility research is phase 1 of the spec. + </div> + </div> +</div> + +<script> +const $ = id => document.getElementById(id); +const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; + +/* sig = base live level 0..1 (what stream is on this device right now). + Note the demo: music is on WH-1000XM4 (sig .72) while Speakers is default. */ +let OUT = [ + {id:'spk', name:'Built-in Speakers', g:'', vol:68, mute:false, def:true, sig:0, ph:0.0}, + {id:'xm4', name:'WH-1000XM4', g:'', vol:80, mute:false, def:false, sig:0.72,ph:1.7}, + {id:'hdmi',name:'HDMI · Dell U2720', g:'', vol:100,mute:true, def:false, sig:0, ph:2.9}, +]; +let IN = [ + {id:'bmic', name:'Built-in Mic', g:'', vol:54, mute:false, def:true, sig:0.16,ph:0.6}, + {id:'yeti', name:'Blue Yeti USB', g:'', vol:70, mute:false, def:false, sig:0, ph:2.1}, + {id:'xm4m', name:'WH-1000XM4 Headset',g:'', vol:60, mute:true, def:false, sig:0, ph:3.3}, +]; +let master = false, micMode = 'live', pttHeld = false; +const pkL={v:0}, pkR={v:0}; + +function outMutedOf(d){ return master || d.mute; } +function inMutedOf(d){ return d.mute || (d.def && (micMode==='mute' || (micMode==='ptt' && !pttHeld))); } + +function fader(dev){ + const w=document.createElement('div'); w.className='fader'; + w.innerHTML=`<div class="trk"><div class="fill" style="width:${dev.vol}%"></div></div>`+ + `<div class="cap" style="left:${dev.vol}%"></div>`; + w.onclick=(e)=>{e.stopPropagation(); + const r=w.getBoundingClientRect(); + dev.vol=Math.max(0,Math.min(100,Math.round((e.clientX-r.left)/r.width*100))); + if(dev.vol>0) dev.mute=false; + render(); toast(`${dev.name} → ${dev.vol}%`); + }; + return w; +} +function row(dev,kind){ + const muted = kind==='out'?outMutedOf(dev):inMutedOf(dev); + const r=document.createElement('div'); + r.className='dev'+(dev.def?' active':'')+(muted?' muted':''); + r.title=dev.name; + const sig=document.createElement('span'); sig.className='sig'; sig.id='sig-'+dev.id; + sig.innerHTML='<i></i><i></i><i></i><i></i>'; + const who=document.createElement('div'); who.className='who'; + who.innerHTML=`<span class="g">${dev.g}</span><span class="nm">${dev.name}</span><span class="def">DEF</span>`; + const pct=document.createElement('span'); pct.className='pct'; pct.textContent=dev.mute?'mute':dev.vol+'%'; + const mb=document.createElement('button'); mb.className='mute-b'; mb.textContent=dev.mute?'':''; + mb.title='mute '+dev.name; + mb.onclick=(e)=>{e.stopPropagation();dev.mute=!dev.mute;render();toast(`${dev.name} ${dev.mute?'muted':'unmuted'}`);}; + r.append(sig,who,fader(dev),pct,mb); + r.onclick=()=>{ if(dev.def) return; + (kind==='out'?OUT:IN).forEach(d=>d.def=false); dev.def=true; render(); + toast(`default ${kind==='out'?'output':'input'} → ${dev.name}`); + }; + return r; +} +function render(){ + const o=$('outputs'); o.innerHTML=''; OUT.forEach(d=>o.appendChild(row(d,'out'))); + const i=$('inputs'); i.innerHTML=''; IN.forEach(d=>i.appendChild(row(d,'in'))); + $('out-cnt').textContent='· '+OUT.length; + $('in-cnt').textContent='· '+IN.length; + const od=OUT.find(d=>d.def), id=IN.find(d=>d.def); + const oM=outMutedOf(od), iM=inMutedOf(id); + $('master').className='switch'+(master?' muted':' on'); + $('state').textContent=oM?'MUTED':'PLAYBACK'; + $('lamp').className='lamp'+(oM?' red':''); + $('face-g').textContent=oM?'':''; + $('mute-badge').classList.toggle('show',oM); + setNeedle('out',oM?0:od.vol,oM); $('v-out').textContent=oM?0:od.vol; $('l-out').textContent=od.name.toUpperCase(); + setNeedle('in',iM?0:id.vol,iM); $('v-in').textContent=iM?0:id.vol; $('l-in').textContent=id.name.toUpperCase(); + $('vt-out').textContent=oM?'OUT·MUTE':'OUT VOL'; $('vt-out').className='mode-tag'+(oM?' mut':''); + $('vt-in').textContent=iM?'IN·MUTE':'IN VOL'; $('vt-in').className='mode-tag'+(iM?' mut':''); + $('vu-src').textContent=od.name.toUpperCase(); + $('vupair').classList.toggle('mute',oM); + // mic controls: one live/muted toggle + one push-to-talk key + const tg=$('md-toggle'); + tg.textContent = micMode==='mute' ? 'MUTED' : 'LIVE'; + tg.appendChild(Object.assign(document.createElement('span'),{className:'k',textContent:'Super+Shift+A'})); + tg.className = 'mode' + (micMode==='mute' ? ' on mute' : micMode==='live' ? ' on' : ''); + $('md-ptt').className='mode'+(micMode==='ptt'?' on':''); + $('ptt-hint').textContent = micMode==='ptt' + ? (pttHeld?'▸ transmitting — Space held':'mic muted — hold Space to talk') + : micMode==='mute' ? 'mic muted' : ''; + $('ptt-hint').className='ptt-hint'+(micMode==='ptt'&&pttHeld?' live':''); +} +function setNeedle(side,val,muted){ + const deg=-60+Math.max(0,Math.min(1,val/100))*120; + const n=$('n-'+side); n.style.transform=`rotate(${deg}deg)`; + n.className='needle'+(muted?' mut':''); +} + +/* live-signal animation — the per-row minis + the stereo VU pair */ +let phase=0; +function level(dev,muted){ + if(muted||!dev.sig) return 0; + const env=0.5+0.5*Math.abs(Math.sin(phase*1.25+dev.ph)); + const trans=Math.random()<0.14?Math.random()*0.4:0; + return Math.max(0,Math.min(1,dev.sig*env+trans-Math.random()*0.07)); +} +function paintMini(el,lvl){ + const b=el.children, lit=Math.round(lvl*4); + for(let k=0;k<4;k++){ b[k].className = k<lit ? (k>=3?'clip':k>=2?'hot':'on') : ''; } +} +function paintVU(el,lvl,pk){ + const b=el.children, n=b.length, lit=Math.round(lvl*n); + pk.v=Math.max(lit,pk.v-0.35); const p=Math.round(pk.v); + for(let k=0;k<n;k++){ let c = k<lit ? (k>=n-2?'clip':k>=n-4?'hot':'on') : ''; + if(p>0 && k===p-1) c=(c?c+' ':'')+'peak'; b[k].className=c; } +} +function buildVU(el,n){ el.innerHTML=''; for(let k=0;k<n;k++) el.appendChild(document.createElement('i')); } +function tick(){ + phase+=0.09; + OUT.forEach(d=>{const el=$('sig-'+d.id); if(el) paintMini(el, d._l=level(d,outMutedOf(d)));}); + IN.forEach(d=>{const el=$('sig-'+d.id); if(el) paintMini(el, d._l=level(d,inMutedOf(d)));}); + const od=OUT.find(d=>d.def), oM=outMutedOf(od); + const base=oM?0:level(od,false); + paintVU($('vu-l'),Math.min(1,base*(0.92+Math.random()*0.16)),pkL); + paintVU($('vu-r'),Math.min(1,base*(0.92+Math.random()*0.16)),pkR); +} + +function masterMute(){ master=!master; render(); toast(master?'ALL OUTPUT MUTED':'output unmuted'); } +/* two mic buttons: toggle flips live<->muted (and leaves ptt); ptt arms/disarms */ +function micToggle(){ micMode = micMode==='mute' ? 'live' : 'mute'; pttHeld=false; render(); + toast(micMode==='mute'?'mic muted':'mic live'); } +function micPtt(){ micMode = micMode==='ptt' ? 'live' : 'ptt'; pttHeld=false; render(); + toast(micMode==='ptt'?'push-to-talk armed — hold Space':'mic live'); } +let tT; +function toast(msg,err){ const t=$('toast'); t.textContent=msg; + t.className='toast show'+(err?' err':''); clearTimeout(tT); tT=setTimeout(()=>t.className='toast',2200); } +addEventListener('keydown',e=>{ if(e.code==='Space'&&micMode==='ptt'&&!e.repeat){e.preventDefault();pttHeld=true;render();}}); +addEventListener('keyup', e=>{ if(e.code==='Space'&&micMode==='ptt'){e.preventDefault();pttHeld=false;render();}}); + +buildVU($('vu-l'),16); buildVU($('vu-r'),16); +render(); +if(!reduced) setInterval(tick,70); else tick(); +</script> +</body> +</html> diff --git a/docs/prototypes/2026-07-03-waybar-redesign-prototype.html b/docs/prototypes/2026-07-03-waybar-redesign-prototype.html new file mode 100644 index 0000000..3f3e7c1 --- /dev/null +++ b/docs/prototypes/2026-07-03-waybar-redesign-prototype.html @@ -0,0 +1,321 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Waybar redesign — dupre instrument console</title> +<style> +:root{ + --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; + --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; + --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; + --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; +} +*{box-sizing:border-box;margin:0;padding:0} +html{background:var(--ground)} +body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-height:1.45; + background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} +.wrap{max-width:1400px;margin:0 auto} +.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} +h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} +.masthead p{color:var(--dim);font-size:.86rem;max-width:88ch} +.masthead p b{color:var(--silver);font-weight:700} + +/* each variation sits on a "desk" — a strip of desktop so the bar reads as a + real top bar floating over a window, exactly how it looks on-screen */ +.desk{margin:1.9rem 0 0;border-radius:14px;overflow:hidden; + border:1px solid #211e1a; + background: + linear-gradient(180deg,#0e0d0c 0 62px,transparent 62px), + repeating-linear-gradient(135deg,#191613 0 14px,#171512 14px 28px); + position:relative} +.desk-label{display:flex;align-items:baseline;gap:.7rem;padding:.5rem .2rem .1rem} +.desk-label .n{color:var(--gold);font-size:.82rem;letter-spacing:.08em} +.desk-label .d{color:var(--dim);font-size:.76rem} +.desk-window{position:absolute;inset:70px 26px 22px;border:1px solid #262320;border-radius:10px; + background:linear-gradient(180deg,#131110,#0e0d0c);opacity:.6} +.desk-window::before{content:"emacs — instrument-console.el";position:absolute;top:8px;left:14px; + color:#3a3630;font-size:.72rem} + +/* the bar frame: matches waybar's -54 top strip. two clusters, gold-bordered, + floating with a gap between them (modules-center is empty in the real config) */ +.bar{position:relative;z-index:2;display:flex;justify-content:space-between;align-items:flex-start; + gap:1rem;padding:10px 12px;height:132px} +.cluster{display:flex;align-items:center;gap:2px; + background:var(--panel);border:1.4px solid var(--gold);border-radius:15px; + padding:2px 9px;box-shadow:0 4px 9px rgba(0,0,0,.5)} +.mod{display:flex;align-items:center;gap:7px;color:var(--silver); + padding:7px 9px;border-radius:11px;font-size:14px;cursor:default;white-space:nowrap;position:relative} +.mod .g{font-size:16px;line-height:1} +.mod .g.xl{font-size:19px} +.mod:hover{background:var(--wash)} +.val{font-variant-numeric:tabular-nums} +.cream{color:var(--cream)}.gold{color:var(--gold)}.dim{color:var(--dim)} +.fail{color:var(--fail)}.pass{color:var(--pass)}.steel{color:var(--steel)} + +/* workspaces — circular tokens like the real ws-icons */ +.ws{display:flex;gap:5px;padding:0 3px} +.ws b{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-size:13px; + color:var(--silver);border:1.4px solid var(--slate);background:#141210} +.ws b.on{color:var(--panel);background:var(--gold);border-color:var(--gold);font-weight:700; + box-shadow:0 0 8px 1px rgba(218,181,61,.4)} +.ws b.busy{border-color:var(--steel);color:var(--cream)} +.menu{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;color:var(--gold); + font-size:17px;background:linear-gradient(180deg,#211e19,#151210);border:1px solid #33302b} +.title{color:var(--dim);font-size:13px;max-width:230px;overflow:hidden;text-overflow:ellipsis} + +/* collapse arrows — recessed dim wells, per the current design */ +.arrow{color:var(--dim);font-size:12px;background:rgba(0,0,0,.35); + box-shadow:inset 0 1px 2px rgba(0,0,0,.7);border-radius:7px;padding:8px 7px} + +/* lamp — the panel's signature status dot, glow and all */ +.lamp{width:8px;height:8px;border-radius:50%;background:var(--pass);flex:0 0 auto; + box-shadow:0 0 6px 1px rgba(116,147,47,.55)} +.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)} +.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)} +.lamp.off{background:var(--wash);box-shadow:none} + +/* engraved hairline divider between functional groups (echoes .engrave rules) */ +.sep{width:1px;align-self:stretch;margin:6px 3px; + background:linear-gradient(180deg,transparent,var(--wash) 22%,var(--wash) 78%,transparent)} + +.notes{margin-top:2.4rem;display:flex;gap:2rem;flex-wrap:wrap} +.note{flex:1 1 300px;min-width:280px} +.note h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .5rem} +.note ul{list-style:none} +.note li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative;color:var(--silver)} +.note li::before{content:"·";color:var(--gold);position:absolute;left:.25rem} +.note li b{color:var(--cream);font-weight:700} +.note li em{color:var(--dim);font-style:normal} +.rec{border:1px dashed var(--wash);border-radius:10px;padding:.9rem 1.1rem;margin-top:1.1rem; + font-size:.83rem;color:var(--silver)} +.rec b{color:var(--gold)} + +/* ============ V1 · FACEPLATE ============ */ +/* machined faceplate: vertical gradient + a 1px top highlight + deeper shadow. + otherwise the current layout — lowest-risk, faithful to GTK CSS. */ +.v1 .cluster{background:linear-gradient(180deg,var(--raise),var(--panel)); + box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.55)} +.v1 .val.clock{color:var(--cream)} + +/* ============ V2 · INSTRUMENT SEGMENTS ============ */ +/* each functional group is its own recessed sub-faceplate with an engraved unit + label underneath, lamps on status modules, gold-hi active values */ +.v2 .cluster{background:linear-gradient(180deg,var(--raise),var(--panel));padding:3px 6px; + box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.55)} +.v2 .seg{display:flex;flex-direction:column;align-items:center;gap:2px; + background:var(--well);border:1px solid #232019;border-radius:10px;padding:3px 5px 2px;margin:0 2px} +.v2 .seg .row{display:flex;align-items:center;gap:6px} +.v2 .seg .unit{color:var(--steel);font-size:.52rem;letter-spacing:.24em;text-transform:uppercase} +.v2 .mod{padding:5px 7px} +.v2 .mod:hover{background:var(--wash)} +.v2 .menu{background:linear-gradient(180deg,#2a251d,#161310);border-color:var(--gold)} +/* mini gauge for sysmon — a squat needle echoing the panel meters */ +.gauge{width:26px;height:15px;position:relative;overflow:hidden} +.gauge .arc{position:absolute;inset:0 0 -26px 0;border:1.5px solid var(--wash);border-radius:50%} +.gauge .ndl{position:absolute;left:50%;bottom:0;width:1.5px;height:13px;background:var(--gold-hi); + transform-origin:50% 100%;transform:rotate(18deg);border-radius:1px; + box-shadow:0 0 4px rgba(255,215,95,.5)} +.gauge .hub{position:absolute;left:50%;bottom:-2px;width:5px;height:5px;margin-left:-2.5px; + border-radius:50%;background:var(--gold)} + +/* ============ V3 · FULL CONSOLE ============ */ +/* every module a recessed well with a lamp; console-key toggles with full + physical-key gradient + inset; sysmon as twin analog gauges; cream clock */ +.v3 .cluster{background:linear-gradient(180deg,#0d0c0b,#080807);border-width:1.8px; + box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 8px 18px rgba(0,0,0,.6);padding:3px 8px} +.v3 .mod{background:var(--well);border:1px solid #201d17;border-radius:9px;margin:0 2px;padding:6px 9px} +.v3 .mod:hover{background:#141210;border-color:var(--slate)} +/* console-key toggles — the physical key from the panels' .c-btn */ +.v3 .key{background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b; + border-bottom-color:#0c0b0a;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} +.v3 .key.engaged{color:var(--gold);border-color:var(--gold)} +.v3 .key.off{color:var(--fail)} +.v3 .menu{background:linear-gradient(180deg,#2a251d,#141110);border:1px solid var(--gold); + box-shadow:0 0 8px rgba(218,181,61,.25)} +.v3 .ws b{background:var(--well);border-color:#201d17} +.v3 .ws b.on{background:var(--gold);border-color:var(--gold)} +.v3 .val.clock{color:var(--cream);font-weight:700} +.v3 .tz{color:var(--steel);font-size:.55rem;letter-spacing:.2em} +.v3 .twin{display:flex;gap:6px} +.v3 .gauge2{width:22px;height:14px;position:relative;overflow:hidden} +.v3 .gauge2 .arc{position:absolute;inset:0 0 -22px 0;border:1.5px solid var(--wash);border-radius:50%} +.v3 .gauge2 .ndl{position:absolute;left:50%;bottom:0;width:1.5px;height:12px;transform-origin:50% 100%; + border-radius:1px;background:var(--gold-hi);box-shadow:0 0 4px rgba(255,215,95,.5)} +.v3 .gauge2 .ndl.warn{background:var(--gold)} +.v3 .gauge2 .hub{position:absolute;left:50%;bottom:-2px;width:4px;height:4px;margin-left:-2px; + border-radius:50%;background:var(--gold)} +</style> +</head> +<body> +<div class="wrap"> + +<header class="masthead"> + <div class="eyebrow">archsetup · dupre panel family · waybar</div> + <h1>Waybar — three ways to spruce it</h1> + <p>The bar already runs the dupre palette and a gold border. These three push it toward the + <b>instrument-console faceplate</b> language of the net + bluetooth panels — machined + gradient plates, engraved unit labels, glowing status lamps, physical console keys, analog + gauges — dialing the intensity up from left-touch to full console. Same real module set in + each so you're comparing <b>treatment, not content</b>. All three stay inside what GTK3 CSS + (waybar's engine) can actually render.</p> +</header> + +<!-- ============ CURRENT (reference) ============ --> +<div class="desk"> + <div class="bar" style="padding-top:14px"> + <div class="cluster"> + <span class="menu"></span> + <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span> + <span class="mod"><span class="g"></span></span> + <span class="title">instrument-console.el</span> + <span class="mod arrow"></span> + </div> + <div class="cluster"> + <span class="mod arrow"></span> + <span class="mod"><span class="g gold"></span></span> + <span class="mod"><span class="g xl"></span></span> + <span class="mod"><span class="g xl"></span> <span class="val">62%</span></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g"></span> <span class="val">8%</span></span> + <span class="mod"><span class="g"></span> <span class="val">24:10</span></span> + <span class="mod"><span class="g"></span> <span class="val">Fri Jul 3</span></span> + </div> + </div> + <div class="desk-window"></div> +</div> +<div class="desk-label"><span class="n">current</span><span class="d">— flat pills, colour-only states. the baseline these three build on.</span></div> + +<!-- ============ V1 · FACEPLATE ============ --> +<div class="desk v1"> + <div class="bar" style="padding-top:14px"> + <div class="cluster"> + <span class="menu"></span> + <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span> + <span class="sep"></span> + <span class="mod"><span class="g"></span></span> + <span class="title">instrument-console.el</span> + <span class="mod arrow"></span> + </div> + <div class="cluster"> + <span class="mod arrow"></span> + <span class="mod"><span class="lamp gold"></span><span class="g gold"></span></span> + <span class="sep"></span> + <span class="mod"><span class="g xl"></span></span> + <span class="mod"><span class="g xl"></span> <span class="val cream">62%</span></span> + <span class="sep"></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g"></span></span> + <span class="mod"><span class="g gold"></span></span> + <span class="mod"><span class="lamp"></span><span class="g"></span></span> + <span class="sep"></span> + <span class="mod"><span class="g"></span> <span class="val">8%</span></span> + <span class="mod"><span class="g gold"></span> <span class="val gold">24:10</span></span> + <span class="sep"></span> + <span class="mod"><span class="g dim"></span> <span class="val clock">Fri Jul 3</span></span> + </div> + </div> + <div class="desk-window"></div> +</div> +<div class="desk-label"><span class="n">variation 1 · faceplate</span><span class="d">— machined gradient + top highlight, engraved hairline dividers, status lamps on net/bt, cream clock. Nearest to today; drop-in GTK CSS.</span></div> + +<!-- ============ V2 · INSTRUMENT SEGMENTS ============ --> +<div class="desk v2"> + <div class="bar" style="padding-top:12px"> + <div class="cluster"> + <span class="menu"></span> + <span class="seg"><span class="row ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span><span class="unit">wksp</span></span> + <span class="seg"><span class="row"><span class="g"></span><span class="title" style="max-width:180px">instrument-console.el</span></span><span class="unit">layout · window</span></span> + <span class="mod arrow"></span> + </div> + <div class="cluster"> + <span class="mod arrow"></span> + <span class="seg"><span class="row"><span class="lamp gold"></span><span class="g gold"></span><span class="gold">CAPTIVE</span></span><span class="unit">net</span></span> + <span class="seg"><span class="row"><span class="g xl"></span><span class="g xl"></span><span class="val cream">62%</span></span><span class="unit">sound</span></span> + <span class="seg"><span class="row"><span class="g"></span><span class="g"></span><span class="g gold"></span></span><span class="unit">toggles</span></span> + <span class="seg"><span class="row"><span class="lamp"></span><span class="g"></span><span class="dim">M650</span></span><span class="unit">bt</span></span> + <span class="seg"><span class="row"><span class="gauge"><span class="arc"></span><span class="ndl"></span><span class="hub"></span></span><span class="val">8%</span></span><span class="unit">cpu</span></span> + <span class="seg"><span class="row"><span class="g gold"></span><span class="val gold">24:10</span></span><span class="unit">timer</span></span> + <span class="seg"><span class="row"><span class="val cream">Fri Jul 3</span><span class="val">11:23</span></span><span class="unit">clock</span></span> + </div> + </div> + <div class="desk-window"></div> +</div> +<div class="desk-label"><span class="n">variation 2 · instrument segments</span><span class="d">— each group a recessed sub-plate with an engraved unit label; a squat needle gauge for cpu. Reads like a row of instruments. Taller; label row costs a few px.</span></div> + +<!-- ============ V3 · FULL CONSOLE ============ --> +<div class="desk v3"> + <div class="bar" style="padding-top:12px"> + <div class="cluster"> + <span class="menu"></span> + <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span> + <span class="sep"></span> + <span class="mod key"><span class="g"></span></span> + <span class="title">instrument-console.el</span> + <span class="mod arrow"></span> + </div> + <div class="cluster"> + <span class="mod arrow"></span> + <span class="mod"><span class="lamp gold"></span><span class="g gold"></span> <span class="gold">CAPTIVE</span></span> + <span class="sep"></span> + <span class="mod key"><span class="g xl"></span></span> + <span class="mod"><span class="g xl"></span> <span class="val cream">62%</span></span> + <span class="sep"></span> + <span class="mod key engaged"><span class="g"></span></span> + <span class="mod key"><span class="g"></span></span> + <span class="mod key engaged"><span class="g"></span></span> + <span class="mod"><span class="lamp"></span><span class="g"></span> <span class="dim val">72%</span></span> + <span class="sep"></span> + <span class="mod"><span class="twin"> + <span class="gauge2"><span class="arc"></span><span class="ndl"></span><span class="hub"></span></span> + <span class="gauge2"><span class="arc"></span><span class="ndl warn" style="transform:rotate(38deg)"></span><span class="hub"></span></span> + </span></span> + <span class="mod key"><span class="g gold"></span> <span class="val gold">24:10</span></span> + <span class="sep"></span> + <span class="mod"><span class="g dim"></span> <span class="val clock">Fri Jul 3</span> <span class="val clock">11:23</span><span class="tz"> EDT</span></span> + </div> + </div> + <div class="desk-window"></div> +</div> +<div class="desk-label"><span class="n">variation 3 · full console</span><span class="d">— every module a recessed well, physical console keys for toggles (gold when engaged, terracotta when off), twin cpu/mem gauges, cream tabular clock with engraved TZ. Furthest from today; closest to the panels.</span></div> + +<!-- ============ NOTES ============ --> +<div class="notes"> + <div class="note"> + <h3>What carries over from the panels</h3> + <ul> + <li><b>Lamps</b> — the glowing status dot lands on net + bt so health reads at a glance, not just by glyph colour <em>(gold = captive/engaged, green = ok, red = fail, dim = off)</em>.</li> + <li><b>Machined faceplate</b> — the cluster gets the b-face vertical gradient + 1px top highlight + deeper shadow, so it looks milled rather than printed.</li> + <li><b>Engraved dividers</b> — hairline separators group the right cluster into net · sound · toggles · system · clock, echoing the panels' engraved section rules.</li> + <li><b>Console keys</b> — the toggles (touchpad, dim, caffeine) borrow .c-btn: gradient fill, inset highlight, gold border when engaged.</li> + <li><b>Gauges</b> — sysmon becomes a squat needle (or twin needles for cpu/mem), the same instrument the panels use for throughput and battery.</li> + <li><b>Cream + tabular</b> — the clock and live values shift to cream with tabular-nums, matching the panels' readouts.</li> + </ul> + </div> + <div class="note"> + <h3>GTK3 translation caveats</h3> + <ul> + <li><b>Dividers</b> need real separator modules or per-module borders — waybar can't inject <em>::before</em> content between modules the way this HTML does.</li> + <li><b>Lamps</b> render as a small pango glyph (● with colour + text-shadow glow) prepended in each script, or a tiny bordered box widget — both are GTK-safe.</li> + <li><b>Gauges</b> are the real work: GTK CSS can't draw a rotating needle. Options — a Cairo/GTK drawing area in a custom module, or fake it with a unicode gauge glyph that steps by load band. V2's single gauge is cheaper than V3's twin.</li> + <li><b>V2's unit labels</b> raise the bar height (the label row). Fine at 54px reserved, but worth eyeballing against the -54 margin strip.</li> + <li>Gradients, inset box-shadow, border colour states, tabular-nums — all already proven in the current stylesheet.</li> + </ul> + </div> + <div class="note"> + <h3>My read</h3> + <div class="rec"> + <b>Variation 1 (faceplate)</b> is the one I'd ship first: it lands ~80% of the instrument-console feel — lamps, milled plates, engraved grouping, cream clock — for pure CSS plus a lamp glyph in the net/bt scripts. No custom drawing, no height risk. + <br><br> + <b>Variation 3</b> is the aspirational target once a gauge-drawing module exists (it'd also upgrade the sysmon popup). <b>Variation 2</b> is the middle path if you want the unit labels' legibility but not the full recessed-well density. They're not exclusive — 1 can grow into 3. + </div> + </div> +</div> + +</div> +</body> +</html> diff --git a/docs/prototypes/README.org b/docs/prototypes/README.org new file mode 100644 index 0000000..9df85cb --- /dev/null +++ b/docs/prototypes/README.org @@ -0,0 +1,21 @@ +#+TITLE: Panel & Waybar Design Prototypes +#+AUTHOR: Craig Jennings + +Self-contained HTML/CSS design prototypes for the instrument-console panel +family and the waybar redesign. Each opens standalone in a browser (no external +assets). These are the normative visual references the specs in [[file:../specs/][docs/specs/]] +point at. + +* Prototypes + +- [[file:2026-07-03-instrument-console-panels-prototype.html][2026-07-03-instrument-console-panels-prototype.html]] — the net + bluetooth + pair; the approved faceplate design that shipped. Normative reference for + [[file:../specs/2026-07-03-instrument-console-panels-spec.org][the instrument-console spec]]. +- [[file:2026-07-03-net-panel-rescan-prototype.html][2026-07-03-net-panel-rescan-prototype.html]] — the manual rescan/scan ⟳ + affordance for the NETWORKS/NEARBY headers (busy-style throbber + list fade). +- [[file:2026-07-03-sound-panel-prototype.html][2026-07-03-sound-panel-prototype.html]] — the audio/pulsemixer console; layout + reference for [[file:../specs/2026-07-03-audio-panel-spec.org][the audio-panel spec]]. +- [[file:2026-07-03-panel-widget-gallery-prototype.html][2026-07-03-panel-widget-gallery-prototype.html]] — the shared instrument-console + widget kit (lamps, engraved sections, console keys, needle gauges). +- [[file:2026-07-03-waybar-redesign-prototype.html][2026-07-03-waybar-redesign-prototype.html]] — three directions for sprucing up + waybar in the dupre instrument-console aesthetic (future work). diff --git a/docs/design/2026-07-02-bluetooth-panel-spec.org b/docs/specs/2026-07-02-bluetooth-panel-spec.org index 121197a..f1b3ac1 100644 --- a/docs/design/2026-07-02-bluetooth-panel-spec.org +++ b/docs/specs/2026-07-02-bluetooth-panel-spec.org @@ -4,6 +4,12 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED +* IMPLEMENTED Bluetooth Panel — CLI-Driven, Net-Panel Kin +:PROPERTIES: +:ID: 8af6a76a-5665-4d20-9efd-ffdf7460c981 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped through phase 3; build task DONE and manual tests filed.) + * IMPLEMENTED Status :PROPERTIES: :ID: 1271a845-4463-4831-9902-990eda6b2265 diff --git a/docs/design/2026-07-02-desktop-settings-panel-spec.org b/docs/specs/2026-07-02-desktop-settings-panel-spec.org index 8becf71..50853f3 100644 --- a/docs/design/2026-07-02-desktop-settings-panel-spec.org +++ b/docs/specs/2026-07-02-desktop-settings-panel-spec.org @@ -4,10 +4,23 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED +* DRAFT Desktop-Settings Dropdown Panel +:PROPERTIES: +:ID: d6bb1e73-ec90-4327-85ee-bfa762da5bce +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed) + * DRAFT Status :PROPERTIES: :ID: fb7eec22-a214-4568-82c4-903612f4832f :END: +- [2026-07-04 Sat] DRAFT — all four decisions resolved by Craig (dim + airplane collapse into the panel, touchpad + caffeine stay on the bar; Super+Shift+G keybind; code in dotfiles settings/ beside net/; 5% brightness floor). The four collapse/keybind/location/floor decisions are closed; one open scoping question remains ("few other things" — see Decisions) before a spec-review can flip it READY. +- [2026-07-03 Fri] DRAFT update — from the waybar/panel-family design + discussion: adopt the instrument-console faceplate aesthetic net + bt + shipped (lamps, engraved sections, console keys, machined plate), not just + the raw palette; add the audio panel as a sibling in the panel family; + cross-reference the shared faceplate CSS. Toggle-consolidation scope (the + "few other things") still open — see the Decisions section. - [2026-07-02 Thu] DRAFT — initial spec from the todo.org task "Desktop-settings dropdown panel" (2026-06-24 review), updated for the Blueprint/GTK4 pipeline the net panel stood up 2026-07-01. @@ -23,6 +36,7 @@ | Repo | dotfiles | |--------+----------------------------------------------| | Kin | net panel (architecture donor), theme studio | +| | audio panel (sibling), bt panel (aesthetic) | |--------+----------------------------------------------| * Problem @@ -61,9 +75,12 @@ gathers them. suite. - One gated AT-SPI smoke (the run-panel-smoke.sh pattern), no bespoke headless widget suite. -- Dupre WIP palette CSS, shared with the net panel — factor the palette - block into a common css asset both panels load rather than duplicating - (feeds the theme-studio task later). +- Instrument-console faceplate aesthetic, consistent with net + bt + audio: + the machined gradient plate, glowing status lamps, engraved section labels, + physical console keys for the toggles, and (where a level applies) needle + gauges. Load the shared instrument-console palette/faceplate CSS asset all + the family panels use — factor it once, don't duplicate (feeds the + theme-studio task later). ** Controls and their backings @@ -93,28 +110,43 @@ signals so both surfaces agree. Gear glyph module on the bar right cluster; click toggles the panel (layer-shell anchored under the bar, right-aligned). Focus-out auto-hide + -Close button, matching the net panel. Keybind decision below. +Close button, matching the net panel. Keybind: Super+Shift+G (decision B). * Decisions (Craig) -** TODO Which standalone bar indicators collapse into the panel? -Options per module (dim, touchpad, caffeine): keep on bar + mirrored in -panel; or panel-only (frees bar width). Recommendation: keep touchpad and -caffeine visible on the bar (state you glance at), move dim into the panel -(you set it rarely), keep airplane where it is. - -** TODO Keybind for the panel? -Super+Shift+G (gear) is free. Or no keybind — mouse-only surface. - -** TODO Where does the code live? -Recommendation: dotfiles =settings/= sibling to =net/= (same src-layout, -tests in tests/settings/), sharing the palette css. In-tree pocketbook-style -was the old note; the net panel is the better donor now. - -** TODO Slider granularity and floor -brightnessctl exposes 0-100%; a 5% floor stops "screen went black in a dark -room" lockouts. Confirm the floor (or allow 0 with a long-press escape -hatch). +** DONE Which standalone bar indicators collapse into the panel? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): touchpad and caffeine stay on the bar (glanceable state); auto-dim and airplane move into the panel (panel-only, freeing bar width). The airplane Super+Shift+A toggle keybind stays as the quick lane — only its bar indicator collapses in. + +** DONE Keybind for the panel? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): Super+Shift+G (gear), for parity with the other panels' fast path. + +** DONE Where does the code live? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): dotfiles =settings/= sibling to =net/= (same src-layout, tests in tests/settings/), sharing the palette css. The net panel is the architecture donor; the old in-tree pocketbook-style note is out. + +** DONE Slider granularity and floor +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): 5% floor on the brightness slider, so a dark-room drag can't black the screen out and lock you out. brightnessctl's 0-100% range clamps to a 5% minimum. + +** TODO What are the "few other things" beyond the toggles? +The 2026-07-03 discussion named consolidating the toggle buttons "and a few +other things" into this panel, but the extras weren't enumerated. Current +control list (above): auto-dim, idle/caffeine, touchpad, mouse, airplane, +screen brightness, keyboard backlight. Candidates raised or adjacent — +confirm which belong here vs the audio panel vs the bar: night-light / color +temperature, a theme/dupre-vs-hudson switch (theme-studio kin), volume or a +master-mute mirror (or leave all audio to the audio panel), a +notifications/do-not-disturb toggle (dunst), lock/suspend actions. Craig to +name the set. + +*** 2026-07-04 Sat — Craig's input (roam capture): the set includes a wallpaper manager +Confirmed the panel gathers the mouse/trackpad toggle, a no-sleep (idle-inhibit) toggle, and the auto-dim toggle, and adds a *wallpaper manager* (this is where the displaced waypaper functionality lands — see the media/keybind change that freed Super+Shift+P). The wallpaper manager needs its own depth: +- take a number of directories to look in; +- switch the wallpaper with the change persisting across sessions; +- switch between two pictures at sunup / sundown (a day/night pair). +That last one implies a sun-time source (a lat/long or a sunrise/sunset lookup). The wallpaper manager is sizable enough it may want its own sub-spec rather than a single panel row; decide during the spec-review whether it's a row that opens a sub-view or a separate panel. Remaining "few other things" candidates above (night-light, theme switch, DND, lock/suspend) still await Craig's yes/no. * Implementation phases @@ -124,5 +156,6 @@ hatch). semantics) — unit-tested, no GTK. 3. Blueprint UI + gear bar module + open/close wiring; palette css factored to a shared asset; AT-SPI smoke. -4. Bar-module consolidation per the decision above (drop/keep indicators, - refresh-signal wiring, keybind). +4. Bar-module consolidation per decision A: drop the dim and airplane bar + modules (now panel-only), keep touchpad and caffeine on the bar, wire the + refresh signals so bar and panel agree, and bind Super+Shift+G. diff --git a/docs/design/2026-07-02-file-manager-swallow-spec.org b/docs/specs/2026-07-02-file-manager-swallow-spec.org index 4c61be1..b898f11 100644 --- a/docs/design/2026-07-02-file-manager-swallow-spec.org +++ b/docs/specs/2026-07-02-file-manager-swallow-spec.org @@ -4,6 +4,12 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED +* CANCELLED File-Manager Swallow Pattern +:PROPERTIES: +:ID: 179a1cd2-7a02-4c44-a09d-685c5a154895 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to CANCELLED (reason: Native swallow ruled out by test; reassigned to .emacs.d dirvish handling.) + * CANCELLED Status :PROPERTIES: :ID: d92e0074-f594-4e83-81a0-faf282e15ed0 diff --git a/docs/design/2026-07-02-net-panel-other-interfaces-spec.org b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org index 6b0a72d..0d63feb 100644 --- a/docs/design/2026-07-02-net-panel-other-interfaces-spec.org +++ b/docs/specs/2026-07-02-net-panel-other-interfaces-spec.org @@ -4,6 +4,12 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED +* IMPLEMENTED Net Panel — Tailscale, VPN, and WireGuard Interfaces +:PROPERTIES: +:ID: 09f4cd40-f391-4eba-a4ff-c22bad00ad7f +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Tunnels track shipped detection, diagnose, and panel bring-up; build task DONE.) + * IMPLEMENTED Status :PROPERTIES: :ID: 79a1075a-4b56-4f25-a861-b69f120a636a diff --git a/docs/specs/2026-07-02-timer-panel-spec.org b/docs/specs/2026-07-02-timer-panel-spec.org new file mode 100644 index 0000000..ceb6e03 --- /dev/null +++ b/docs/specs/2026-07-02-timer-panel-spec.org @@ -0,0 +1,145 @@ +#+TITLE: Timer GTK Panel +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-02 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Timer GTK Panel +:PROPERTIES: +:ID: 25ed5321-f035-42b3-b115-69364d775f41 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: 1770af2e-b093-4024-a512-ae4324a2869f +:END: +- [2026-07-05 Sun] IMPLEMENTED — built and shipped to dotfiles in a no-approvals speedrun (4 commits 1f4f270..78d3cbb): wtimer watch/lap/save; a new timer/ package with a GTK-free PanelModel (62 tests) + the GTK instrument-console panel; bar integration (custom/timer opens the panel, the fuzzel creation flow retired, Hyprland float rule added). Code-complete; live GTK behavior awaits Craig's manual pass (filed under "Manual testing and validation" in todo.org) — a failing check promotes to a bug. +- [2026-07-05 Sun] DOING — Craig directed the build (no-approvals speedrun). Folded in the cj input from the sibling waybar-timer-module spec (GTK app styled like the panels; a queue/output-wall auto-sorted by fire time; stopwatch lap/stop + saveable runs; notify integration; 5/25-min configurable+deletable defaults; up to 10 timers; widget-gallery elements) — see Build scope below. Bypassed the READY spec-review step at Craig's direction; the four decisions were already resolved. +- [2026-07-04 Sat] DRAFT — all four decisions resolved by Craig (standalone; retire fuzzel once the panel lands; timer chips gain 10m/30m/2h; wtimer watch mode over polling). Decision-complete; ready for a spec-review to flip it READY before build. +- [2026-07-02 Thu] DRAFT — initial spec from Craig's roam capture "give the + timer a gtk UI/UX like the network panel. spec this out." + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------| +| Status | implemented | +|--------+---------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------| +| Repo | dotfiles | +|--------+---------------------------------------------------| +| Kin | net panel (architecture donor), wtimer (backing), | +| | desktop-settings panel spec (sibling) | +|--------+---------------------------------------------------| + +* Problem + +The timer's whole UI is a chain of three fuzzel prompts (type, value, label) +plus a fourth for cancel. That flow can't show what's already running while +you create, can't offer one-tap presets, gives no feedback on a typo until +the add silently fails, and pomodoro state (phase, cycle) is only visible in +a tooltip. The 2026-07-02 styling pass made the dialogs presentable, but the +shape is still four blind modals for what is really one small control +surface. + +* Goals + +1. One panel, opened from the bar's timer module, that shows everything + running (live countdowns, pomodoro phase/cycle, paused state) and creates + new items without leaving it. +2. One-tap presets for the common cases (tea, pomodoro, quick alarm) next to + freeform entry, with inline validation before the add. +3. Per-item controls: pause/resume, cancel, promote to primary (the bar + glyph slot). +4. wtimer stays the single owner of timer state and the notification path; + the panel is a view over it, never a second engine. + +* Design sketch + +** Architecture — clone the net panel's proven stack + +- GTK4 + gtk4-layer-shell dropdown anchored under the timer module, Blueprint + .blp compiled to committed .ui (=make ui=; compiler is dev-only). +- Humble-object split: GTK-free PanelModel presenter, unit-tested to 100%, + with thin widget bindings; one gated AT-SPI smoke via the + run-panel-smoke.sh pattern. +- Backing: shell out to the existing wtimer CLI (=add=, =toggle=, =cancel=, + =cycle=, =render=). =render= already emits a JSON payload. Live state comes + from a new wtimer watch/subscribe mode (decision D), which the panel + subscribes to for push updates instead of polling =render= on a timer. + wtimer's 89-case suite keeps owning the logic; panel tests fake the CLI + like every dotfiles suite fakes binaries. +- Dupre WIP palette CSS shared with the net panel (same factoring the + desktop-settings spec calls for — one palette asset, three panels). + +** Layout sketch + +- Header row: running-item count + a Clear All button (maps to cancel-all). +- Item list: one row per item — type glyph, label, live countdown / clock + time / phase+cycle for pomodoro, pause and cancel buttons, click-to-promote. +- Create strip: four type buttons (the wtimer glyphs), preset chips per type + (timer 5m / 10m / 15m / 25m / 30m / 60m / 2h; alarm +30m / top-of-hour / + 07:00; pomodoro default cycle; stopwatch none — decision C), a freeform + entry validated with wtimer's own parsers, an optional label field. +- Empty state: the create strip alone, centered. + +** What happens to the fuzzel flow + +Decision B (below) resolved this: the fuzzel chain retires once the panel +lands. The panel becomes the single creation surface, replacing both the +click-driven bar path and the keybind/fuzzel path. Until the panel ships the +fuzzel flow stays (it's styled and tested); phase 4 removes it after the +panel proves out. + +* Build scope (consolidated 2026-07-05 — the four decisions plus Craig's cj input) + +The panel is a new =timer/= dotfiles package mirroring =net/= and =audio/= (src-layout, GTK4 + gtk4-layer-shell, humble-object PanelModel, instrument-console faceplate aesthetic — machined plate, engraved section labels, status lamps, console keys). wtimer stays the state engine; the panel is a view over it. + +Create + queue: +- A configure strip (top): pick timer / alarm / stopwatch / pomodoro, set the value (preset chips per decision C + a freeform entry validated by wtimer's parsers, optional label). A =+= adds the configured item to the queue. +- The queue is an output-wall-style list (the instrument-console output well), *auto-sorted by soonest fire time* (the item that notifies next is on top). One row per item: type glyph, label, live countdown / clock time / pomodoro phase+cycle, pause/resume, cancel, click-to-promote (bar glyph slot). +- Up to 10 timers; the two starting timer presets default to 5 min and 25 min, and the preset set is configurable and deletable. + +Stopwatch: +- A running stopwatch row has a Lap button and a Stop button. Lap records the elapsed time at the press; unlimited laps; a lap can optionally be named (non-interruptive — naming never blocks further laps). On stop, the full run (splits + optional names) can be saved to review later. Save target: an org file (default =~/org/stopwatch-runs.org=, override via a config key) — one heading per run with a table of laps. + +Live updates + notifications: +- A new =wtimer watch= subcommand emits state on every change (state-file watch → JSON lines on stdout); the panel subscribes for push updates instead of polling. The bar may adopt it later. +- Notifications for alarms and timers go through the =notify= script (wtimer already fires notify on completion; keep that path the single notification owner). + +UI elements: draw from the panel widget gallery prototype (=docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html= in the archsetup repo) for the console keys, lamps, output-well rows, and chips, matching the shipped net/bt/audio look. + +Retire the old timer: the bar's =custom/timer= on-click drives =wtimer new= (the fuzzel chain). Rewire the bar module's on-click to open this panel, and retire the =wtimer new= fuzzel creation flow (decision B). Keep =wtimer render= as the bar indicator and the wtimer engine as the state source. + +* Decisions (Craig) + +** DONE Panel scope: standalone timer panel, or a page in the desktop-settings panel? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): standalone, sharing the palette/css asset. Matches the net panel's one-domain-one-panel shape and keeps the timer dropdown small. + +** DONE Fuzzel flow: keep as keyboard fast lane, or retire once the panel lands? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): retire the fuzzel flow once the panel lands. The panel becomes the single creation surface; the keybind chain goes away rather than staying as a parallel path. (Implementation phase 4's "decide the fuzzel flow's future" is now decided — retire, don't keep.) + +** DONE Presets: which chips per type? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): timer chips are 5m / 10m / 15m / 25m / 30m / 60m / 2h (the strawman plus 10m, 30m, 2h). Alarm +30m / top-of-hour / 07:00, pomodoro default cycle only, stopwatch none — as the strawman. + +** DONE Live updates: poll render (1s, like the bar) or a wtimer "watch" mode? +CLOSED: [2026-07-04 Sat] +Resolved (Craig, 2026-07-04): a wtimer watch/subscribe mode, not 1s polling. This grows wtimer with a new watch capability that the panel (and potentially the bar) subscribes to for live state, rather than reusing the poll cadence — cleaner at the cost of a wtimer addition. Fold the watch mode into the phase 1 CLI-backing seam. + +* Implementation phases + +1. PanelModel presenter + CLI-backing seam (TDD, GTK-free, 100% like the net + PanelModel), plus the wtimer watch/subscribe mode (decision D) the presenter + subscribes to for live state. +2. Blueprint UI: item list + create strip, wired to the presenter; palette + css factored to the shared asset. +3. Bar integration: timer module left-click opens the panel (replacing the + fuzzel menu binding there); the panel and bar both track state via the + wtimer watch subscription. +4. AT-SPI smoke + manual-testing checklist; retire the fuzzel flow (decision B) + after the panel proves out over a week of real use. diff --git a/docs/specs/2026-07-03-audio-panel-spec.org b/docs/specs/2026-07-03-audio-panel-spec.org new file mode 100644 index 0000000..5b678a8 --- /dev/null +++ b/docs/specs/2026-07-03-audio-panel-spec.org @@ -0,0 +1,166 @@ +#+TITLE: Audio Panel — the pulsemixer console +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-03 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED Audio Panel — the pulsemixer console +:PROPERTIES: +:ID: 9175e017-46ad-4887-ae45-887e9551c005 +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Shipped; build task DONE, manual tests filed.) + +* IMPLEMENTED Status +:PROPERTIES: +:ID: 71f556c6-ee02-47cc-a3be-68c8289380f3 +:END: +- [2026-07-03 Fri] IMPLEMENTED — built in a no-approvals speedrun in the + dotfiles repo (branch panel-bugfixing): engine (pactl), presenter, GTK + panel, PTT arming, bar indicator, and the bar/keybind wiring, across four + commits 65e5bb0..9601420. 102 unit tests + a passing AT-SPI smoke on velox. + All five Decisions below resolved. Live-eyeball validation (visual polish, + PTT-in-a-meeting, fader feel, the master-mute hardware key) is the one open + follow-up, tracked as a manual-testing task in todo.org. +- [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec" + (roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion. + Written to iterate alongside the prototype + (=docs/prototypes/2026-07-03-sound-panel-prototype.html=). Spine is present; the + Decisions and Design detail get filled in as we go. + +* Metadata + +| Field | Value | +|--------+---------------------------------------------------| +| Status | implemented | +|--------+---------------------------------------------------| +| Owner | Craig Jennings | +|--------+---------------------------------------------------| +| Repo | dotfiles | +|--------+---------------------------------------------------| +| Kin | net panel + bt panel (architecture + aesthetic | +| | donors), desktop-settings panel (sibling) | +|--------+---------------------------------------------------| + +* Problem + +Audio control today is the pyprland audio scratchpad (Super+A) — a floating +pulsemixer TUI — plus scattered bar affordances: =pulseaudio= (volume, click +to mute sink), =pulseaudio#mic= (mic glyph + mic-toggle), Super+M audio-cycle +ring, Super+Shift+A mic-toggle. There's no single glanceable surface that +shows every sink and source, lets you set the default output/input, and +carries the meeting-grade mic controls Craig wants (a clean muted mode and a +hold-to-talk mode). The net + bluetooth panels set the pattern for exactly +this shape; audio is the third instrument in the family. + +* Goals + +1. One panel, opened from the bar's sound glyph, exposing the full pulsemixer + surface: every sink and source, per-device volume, per-device mute, and + switching the default output and input. +2. Replace the pyprland audio scratchpad (Super+A) as the primary audio UI. +3. Mic modes for meetings: *live*, *muted*, and *push-to-talk* (mic stays + muted except while Space is held, releasing re-mutes). +4. A *master quick-mute* — one action mutes all output — reachable from the + faceplate and a keybind. +5. Instrument-console aesthetic and architecture consistent with net + bt: + same faceplate, lamps, engraved sections, console keys, needle gauges, + verify-everything contract. +6. The bar glyph reflects live state: speaker + three arcs normally, a + speaker-with-✕ when muted (Craig's called glyphs). + +* Design sketch + +Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html= (the reference for +layout + idioms below). + +** Surface (from the prototype) + +- *Faceplate* — status lamp, sound glyph, state word (PLAYBACK / MUTED), a + MUTED badge, the SND·01 unit label, and the *master quick-mute switch* + (same switch idiom as net wifi / bt power), plus the close ✕. +- *OUTPUTS section* — one row per sink. Row body click = set default (gold + DEF tag). A machined fader sets that sink's volume; the trailing glyph + mutes just that device. Active/default row is emphasized (cream name, gold + lamp/glyph). +- *INPUTS section* — one row per source, same idioms. +- *Mic mode* — three console keys: LIVE / MUTED / PUSH·TALK. Push-to-talk + keeps the mic muted (red IN needle) and un-mutes only while Space is held. +- *Twin VU needles* — output level + input level, the sound analog of net + throughput and bt battery gauges. Needle goes red when its side is muted. + +** Architecture — clone the net/bt panel stack + +- GTK4 + gtk4-layer-shell, Blueprint =.blp= → committed =.ui= (=make ui=, + dev-only build dep). +- Humble-object split: a GTK-free PanelModel presenter (unit-tested like the + net/bt PanelModels) + thin composite-widget pages. Backing actions in a + GTK-free =audio.py= that shells to the audio control layer (pactl / + wpctl / pulsemixer — pick below), TDD'd with fake binaries. +- One gated AT-SPI smoke (=run-panel-smoke.sh= pattern). +- Shared instrument-console palette CSS asset (the one net/bt/settings all + load) — do not duplicate the palette block. +- Code lives in dotfiles =audio/= sibling to =net/= (src-layout, tests in + =tests/audio/=). + +* Decisions (Craig) + +** DONE Audio control backend — pactl vs wpctl vs pulsemixer +CLOSED: [2026-07-03 Fri] +Resolved: =pactl= (the engine module is =pactl.py=). Both ratio and velox run +PipeWire with the pipewire-pulse compat layer and no PulseAudio daemon, so +pactl and wpctl hit the same graph — but =pactl -f json= gives structured, +name-addressable output where wpctl offers only a volatile-id tree. Reads go +through =pactl -f json list sinks|sources= + =get-default-*=; writes target +devices by stable name behind an argv-charset guard. + +** DONE Push-to-talk mechanism under Wayland (feasibility — phase 1) +CLOSED: [2026-07-03 Fri] +Resolved: route (a), Hyprland dynamic binds. The phase-1 spike confirmed all +three primitives on velox (Hyprland 0.55.4): =hyprctl keyword bind/unbind= +adds and removes a bind live, =bindr= fires on release, and =pactl +set-source-mute @DEFAULT_SOURCE@ 0|1= toggles the mic cleanly. =ptt.py= arms a +press bind (un-mute) + a bindr (re-mute) on entering PTT mode and unbinds on +leaving, so the talk key isn't grabbed globally otherwise. No evdev needed. +Documented behavior: while PTT is armed, the talk key is the talk key. + +** DONE Quick-mute keybind + scope +CLOSED: [2026-07-03 Fri] +Resolved: the XF86AudioMute hardware key (Super+Shift+M turned out to be taken +by the monocle-layout bind, so the spec's assumption was wrong). The mute key +now runs =audio quick-mute=, which mutes every output (master), not just the +default sink — identical on a single-sink machine, correct on a multi-sink +one. Also reachable from the faceplate master switch and the panel. Scope: +master mute of all sinks, with verify-after-apply per sink. + +** DONE Bar glyph click map +CLOSED: [2026-07-03 Fri] +Resolved with the low-regret wiring: kept the existing =pulseaudio= waybar +module (left-click mute, scroll volume — no regression) and repointed its +right-click from the retired pulsemixer scratchpad to =audio-panel=. So: left += mute, right = open panel, scroll = volume. A fuller =custom/audio= indicator +(state-following speaker glyph + its own click map) is built and tested +(=indicator.py= + =waybar-audio=) but stays unwired until the new bar glyph +gets a live eyeball — the swap is a one-line waybar edit when Craig's ready. + +** DONE Fate of the existing audio affordances +CLOSED: [2026-07-03 Fri] +Resolved: Super+A repurposed from =pypr toggle audio= (the pulsemixer +scratchpad) to =audio-panel= — the panel is the primary audio UI now, so the +scratchpad is retired. Its definition still sits in the machine-local +=pyprland.toml= (not stowed) and can be deleted by hand. Kept: =pulseaudio= + +=pulseaudio#mic= waybar modules (glance + scroll + the mic-mute glance), +Super+M cycle, Super+Shift+A + XF86AudioMicMute mic-toggle. Changed: +XF86AudioMute → master quick-mute (see the quick-mute decision above). + +* Implementation phases + +1. Push-to-talk feasibility spike (decision above) — the one unknown; settle + the mechanism before committing the mic-mode design. +2. =audio.py= backings (list/get/set/mute/default for sinks + sources) — + pure engine, TDD with a fake audio backend. +3. PanelModel presenter (rows, default tracking, mic modes, master mute, + verify-after-apply) — unit-tested, no GTK. +4. Blueprint UI + sound bar glyph (normal / muted / ptt states) + open/close + wiring; shared palette css; AT-SPI smoke. +5. Bar-affordance consolidation per the decision above; retire the Super+A + scratchpad; keybinds. diff --git a/docs/design/2026-07-03-instrument-console-panels-spec.org b/docs/specs/2026-07-03-instrument-console-panels-spec.org index 315e0b4..2c80aa9 100644 --- a/docs/design/2026-07-03-instrument-console-panels-spec.org +++ b/docs/specs/2026-07-03-instrument-console-panels-spec.org @@ -3,10 +3,26 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED +* IMPLEMENTED Instrument-console rebuild — net + bluetooth panels +:PROPERTIES: +:ID: ac23e996-a51a-466b-ad80-2faff46447bf +:END: +- 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to IMPLEMENTED (reason: Panel rebuild shipped (dotfiles e993c3f); build task DONE.) + * IMPLEMENTED Status :PROPERTIES: :ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 :END: +- 2026-07-03 Fri @ 17:35 -0400 :: Post-impl increment (stays IMPLEMENTED): added + a manual rescan/scan ⟳ affordance to both panels — net NETWORKS header + (drives manage.rescan) and bt NEARBY header (drives on_scan / pair-mode + discovery), with the approved "all" busy style (Gtk.Spinner throbber + a + GLib breathe on "scanning…" + a one-shot list fade; no CSS keyframes exist in + this GTK setup). Prototype: archsetup docs/prototypes/2026-07-03-net-panel-rescan-prototype.html. Code shipped + UNCOMMITTED into the dotfiles repo from an archsetup session (cross-project); + handoff at ~/.dotfiles/inbox/2026-07-03-1733-from-archsetup-rescan-handoff.org. + Verified: net 584 + bt 223 unit OK, both AT-SPI smokes green (⟳ present); + live busy-animation feel pending Craig's eyeball. - 2026-07-03 Fri @ 06:49 -0400 :: DOING → IMPLEMENTED: all six phases shipped (net GTK-free layer 81ec9c3, net view 800ef60; bt GTK-free layer 5318b34, bt view 66f03d9; phase-6 dead-code removal f4e688e). Both panels are single-screen @@ -30,7 +46,7 @@ |---------------------+------------------------------------------------------------| | Repos | dotfiles (net/, bluetooth/, themes), archsetup | |---------------------+------------------------------------------------------------| -| Normative reference | [[file:../../assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] | +| Normative reference | [[file:../prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] | |---------------------+------------------------------------------------------------| * Summary diff --git a/scripts/testing/lib/network-diagnostics.sh b/scripts/testing/lib/network-diagnostics.sh index 38788e5..dc54334 100644 --- a/scripts/testing/lib/network-diagnostics.sh +++ b/scripts/testing/lib/network-diagnostics.sh @@ -6,58 +6,110 @@ # Note: logging.sh and vm-utils.sh should already be sourced by the calling script # Uses globals: ROOT_PASSWORD, SSH_PORT, SSH_OPTS, VM_IP (from vm-utils.sh or calling script) +# Optional global: TEST_RESULTS_DIR (raw command outputs are saved there when set) -# Run quick network diagnostics +# Gather one read-only fact from the VM, print it, and save the raw output. +# Facts are collected regardless of pass/fail so a failing install still leaves +# the IP/route/resolver evidence in the log and the results dir. +# $1 label human-readable label for the fact +# $2 slug filename slug for the saved raw output +# $3 cmd remote command to run over the shared ssh_base +# Uses the caller's locals ssh_base and results_dir (dynamic scope). +_netdiag_fact() { + local label="$1" slug="$2" cmd="$3" out + out="$($ssh_base "$cmd" 2>&1)" + info "${label}:" + printf '%s\n' "$out" | while IFS= read -r line; do + info " $line" + done + if [ -n "$results_dir" ]; then + printf '%s\n' "$out" > "$results_dir/netdiag-${slug}.txt" 2>/dev/null || true + fi +} + +# Run quick network diagnostics. +# +# Evidence first: collect read-only facts (interfaces, route, resolver) +# unconditionally, then run every reachability check and report all failures at +# the end. A DNS failure is named as a DNS failure, not masked as a generic "no +# internet" or misattributed to the Arch mirror. Returns 0 when all checks pass, +# non-zero when any check fails, so callers keep their success/failure contract. run_network_diagnostics() { local password="${ROOT_PASSWORD:-archsetup}" local port="${SSH_PORT:-22}" local host="${VM_IP:-localhost}" local ssh_base="sshpass -p $password ssh $SSH_OPTS -p $port root@$host" + local results_dir="${TEST_RESULTS_DIR:-}" + local failures=() section "Pre-flight Network Diagnostics" - # Test 1: Basic connectivity (use curl instead of ping - SLIRP may not handle ICMP) - step "Testing internet connectivity" - if $ssh_base "curl -s --connect-timeout 5 -o /dev/null http://archlinux.org" 2>/dev/null; then - success "Internet connectivity OK" - else - error "No internet connectivity" - return 1 - fi + # --- Phase 1: collect read-only facts, unconditionally --- + # These never gate the outcome; they exist so a failed install still has + # the interface/route/resolver evidence to diagnose from. + step "Collecting interface addresses" + _netdiag_fact "Interface addresses (ip -brief addr)" "ip-addr" "ip -brief addr" + + step "Collecting default route" + _netdiag_fact "Default route (ip route show default)" "ip-route" "ip route show default" - # Test 2: DNS resolution (use getent which is always available, unlike nslookup/dig) + step "Reading resolver configuration" + _netdiag_fact "Resolver (/etc/resolv.conf)" "resolv-conf" "cat /etc/resolv.conf" + + # --- Phase 2: generic connectivity checks (run all, don't short-circuit) --- + # DNS, egress, and TLS are independent failure modes. Keeping them separate + # means a resolver problem reads as DNS, not as a downstream mirror failure. step "Testing DNS resolution" if $ssh_base "getent hosts archlinux.org >/dev/null 2>&1" 2>/dev/null; then success "DNS resolution OK" else error "DNS resolution failed" - return 1 + failures+=("DNS resolution (getent hosts archlinux.org)") fi - # Test 3: Arch mirror accessibility + step "Testing HTTP egress" + if $ssh_base "curl -s --connect-timeout 5 -o /dev/null http://archlinux.org" 2>/dev/null; then + success "HTTP egress OK" + else + error "HTTP egress failed" + failures+=("HTTP egress (http://archlinux.org)") + fi + + step "Testing TLS/HTTPS egress" + if $ssh_base "curl -s --connect-timeout 5 -o /dev/null https://archlinux.org" 2>/dev/null; then + success "TLS/HTTPS egress OK" + else + error "TLS/HTTPS egress failed" + failures+=("TLS/HTTPS egress (https://archlinux.org)") + fi + + # --- Phase 3: Arch-specific checks (run all, don't short-circuit) --- step "Testing Arch mirror access" if $ssh_base "curl -s -I https://geo.mirror.pkgbuild.com/ | head -1 | grep -qE '(200|301|302)'" 2>/dev/null; then success "Arch mirrors accessible" else error "Cannot reach Arch mirrors" - return 1 + failures+=("Arch mirror (https://geo.mirror.pkgbuild.com/)") fi - # Test 4: AUR accessibility step "Testing AUR access" if $ssh_base "curl -s -I https://aur.archlinux.org/ | head -1 | grep -qE '(200|405)'" 2>/dev/null; then success "AUR accessible" else error "Cannot reach AUR" - return 1 + failures+=("AUR (https://aur.archlinux.org/)") fi - # Show network info - info "Network configuration:" - $ssh_base "ip addr show | grep 'inet ' | grep -v '127.0.0.1'" 2>/dev/null | while IFS= read -r line; do - info " $line" - done + # --- Summary: report every failure, not just the first --- + if [ ${#failures[@]} -eq 0 ]; then + success "Network diagnostics complete - all checks passed" + return 0 + fi - success "Network diagnostics complete" - return 0 + error "Network diagnostics found ${#failures[@]} failure(s):" + local f + for f in "${failures[@]}"; do + error " - $f" + done + return 1 } diff --git a/tests/network-diagnostics/test_network_diagnostics.py b/tests/network-diagnostics/test_network_diagnostics.py new file mode 100644 index 0000000..1a8073f --- /dev/null +++ b/tests/network-diagnostics/test_network_diagnostics.py @@ -0,0 +1,215 @@ +"""Tests for run_network_diagnostics in the VM testing harness. + +run_network_diagnostics is the VM install pre-flight network check. It +collects read-only facts (interfaces, default route, resolver) first and +unconditionally, then runs every reachability check -- DNS, HTTP egress, +TLS egress, Arch mirror, AUR -- accumulating failures and reporting them all +at the end. Facts are printed regardless of pass/fail, so a failed install +still leaves the evidence. Generic checks (DNS/egress/TLS) are kept separate +from Arch-specific checks (mirror/AUR) so a DNS failure is named as DNS, not +misattributed to the mirror. Returns 0 when all checks pass, non-zero +otherwise, preserving the caller's success/failure contract. + +These tests exercise the REAL function body (sourced out of +network-diagnostics.sh, not a copy) with: + - stub logging functions (section/step/info/success/error/warn) that just + echo, so output is assertable; + - a fake `sshpass` on PATH that dispatches on the remote command string and + returns canned exit codes driven by FAKE_*_FAIL env vars. This is the + system boundary -- the real function shells out through + `sshpass ... ssh ... "<remote cmd>"`, and the fake stands in for the VM. + +Run from repo root: + python3 -m unittest tests.network-diagnostics.test_network_diagnostics +""" + +import os +import shutil +import subprocess +import tempfile +import unittest + + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +NETDIAG = os.path.join( + REPO_ROOT, "scripts", "testing", "lib", "network-diagnostics.sh" +) + +# A fake sshpass. The real invocation is: +# sshpass -p <pw> ssh <opts> -p <port> root@<host> "<remote cmd>" +# so the remote command is always the last argument. This stub inspects it and +# returns a canned exit code per check, driven by FAKE_*_FAIL env vars. Fact +# commands (ip/route/resolv) always succeed and print sample output so the +# evidence-collection path is exercised. +FAKE_SSHPASS = r"""#!/bin/bash +cmd="${@: -1}" +case "$cmd" in + *"ip -brief addr"*) + echo "lo UNKNOWN 127.0.0.1/8" + echo "eth0 UP 10.0.2.15/24" + exit 0 ;; + *"ip route show default"*) + echo "default via 10.0.2.2 dev eth0" + exit 0 ;; + *"resolv.conf"*) + echo "nameserver 10.0.2.3" + exit 0 ;; + *"getent hosts"*) + [ "${FAKE_DNS_FAIL:-0}" = "1" ] && exit 2 + exit 0 ;; + *"https://archlinux.org"*) + [ "${FAKE_TLS_FAIL:-0}" = "1" ] && exit 7 + exit 0 ;; + *"http://archlinux.org"*) + [ "${FAKE_HTTP_FAIL:-0}" = "1" ] && exit 7 + exit 0 ;; + *"geo.mirror.pkgbuild.com"*) + [ "${FAKE_MIRROR_FAIL:-0}" = "1" ] && exit 1 + exit 0 ;; + *"aur.archlinux.org"*) + [ "${FAKE_AUR_FAIL:-0}" = "1" ] && exit 1 + exit 0 ;; + *) + exit 0 ;; +esac +""" + +# Stub logging functions plus the sourced real file, then call the function. +WRAPPER = r"""#!/bin/bash +section() { echo "=== $1 ==="; } +step() { echo " -> $1"; } +info() { echo "[i] $1"; } +success() { echo "[OK] $1"; } +warn() { echo "[!] $1" >&2; } +error() { echo "[X] $1" >&2; } +source "$1" +run_network_diagnostics +""" + + +class NetworkDiagnosticsHarness(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="netdiag-test-") + self.fakebin = os.path.join(self.tmp, "bin") + os.makedirs(self.fakebin) + sshpass = os.path.join(self.fakebin, "sshpass") + with open(sshpass, "w") as f: + f.write(FAKE_SSHPASS) + os.chmod(sshpass, 0o755) + self.wrapper = os.path.join(self.tmp, "run.sh") + with open(self.wrapper, "w") as f: + f.write(WRAPPER) + os.chmod(self.wrapper, 0o755) + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def run_diag(self, results_dir=None, **fail_flags): + env = dict(os.environ) + env["PATH"] = self.fakebin + os.pathsep + env["PATH"] + # Keep the harness deterministic regardless of the host's SSH config. + env["SSH_OPTS"] = "-o StrictHostKeyChecking=no" + env["ROOT_PASSWORD"] = "archsetup" + env["SSH_PORT"] = "22" + env["VM_IP"] = "localhost" + if results_dir is not None: + env["TEST_RESULTS_DIR"] = results_dir + for k, v in fail_flags.items(): + env[k] = v + return subprocess.run( + ["bash", self.wrapper, NETDIAG], + capture_output=True, text=True, timeout=20, env=env, + ) + + # --- Normal case: everything reachable ----------------------------- + + def test_all_checks_pass_returns_zero(self): + r = self.run_diag() + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertIn("all checks passed", r.stdout) + + def test_facts_collected_on_success(self): + r = self.run_diag() + out = r.stdout + r.stderr + self.assertIn("10.0.2.15/24", out) # interface fact + self.assertIn("default via 10.0.2.2", out) # route fact + self.assertIn("nameserver 10.0.2.3", out) # resolver fact + + # --- DNS-failure case ---------------------------------------------- + + def test_dns_failure_returns_nonzero(self): + r = self.run_diag(FAKE_DNS_FAIL="1") + self.assertNotEqual(r.returncode, 0) + + def test_dns_failure_names_dns_not_mirror(self): + r = self.run_diag(FAKE_DNS_FAIL="1") + out = r.stdout + r.stderr + self.assertIn("DNS resolution failed", out) + # A DNS failure must not be misreported as a mirror failure. With only + # DNS failing, the mirror check still runs and passes. + self.assertNotIn("Cannot reach Arch mirrors", out) + + def test_dns_failure_still_collects_evidence(self): + # The whole point of the change: evidence is gathered before any check + # can bail, so a DNS failure still leaves the facts in the output. + r = self.run_diag(FAKE_DNS_FAIL="1") + out = r.stdout + r.stderr + self.assertIn("10.0.2.15/24", out) + self.assertIn("default via 10.0.2.2", out) + self.assertIn("nameserver 10.0.2.3", out) + + def test_dns_failure_summary_lists_the_failure(self): + r = self.run_diag(FAKE_DNS_FAIL="1") + out = r.stdout + r.stderr + self.assertIn("found 1 failure", out) + self.assertIn("getent hosts archlinux.org", out) + + # --- Mirror-only-failure case -------------------------------------- + + def test_mirror_only_failure_returns_nonzero(self): + r = self.run_diag(FAKE_MIRROR_FAIL="1") + self.assertNotEqual(r.returncode, 0) + + def test_mirror_only_failure_generic_checks_pass(self): + r = self.run_diag(FAKE_MIRROR_FAIL="1") + out = r.stdout + r.stderr + # Generic checks are healthy; only the Arch-specific mirror check fails. + self.assertIn("DNS resolution OK", out) + self.assertIn("HTTP egress OK", out) + self.assertIn("TLS/HTTPS egress OK", out) + self.assertIn("Cannot reach Arch mirrors", out) + self.assertNotIn("DNS resolution failed", out) + + def test_mirror_only_failure_summary_names_mirror(self): + r = self.run_diag(FAKE_MIRROR_FAIL="1") + out = r.stdout + r.stderr + self.assertIn("geo.mirror.pkgbuild.com", out) + + # --- All checks run: multiple failures are all reported ------------ + + def test_multiple_failures_all_reported(self): + r = self.run_diag(FAKE_DNS_FAIL="1", FAKE_AUR_FAIL="1") + out = r.stdout + r.stderr + self.assertIn("found 2 failure", out) + self.assertIn("getent hosts archlinux.org", out) + self.assertIn("aur.archlinux.org", out) + + # --- Raw outputs saved to the results dir -------------------------- + + def test_raw_facts_saved_to_results_dir(self): + results = os.path.join(self.tmp, "results") + os.makedirs(results) + self.run_diag(results_dir=results) + for slug, needle in ( + ("ip-addr", "10.0.2.15/24"), + ("ip-route", "default via 10.0.2.2"), + ("resolv-conf", "nameserver 10.0.2.3"), + ): + path = os.path.join(results, "netdiag-%s.txt" % slug) + self.assertTrue(os.path.exists(path), "missing " + path) + with open(path) as f: + self.assertIn(needle, f.read()) + + +if __name__ == "__main__": + unittest.main() @@ -21,22 +21,25 @@ The vocabulary is open — topic tags are coined as needed — so these are conv - *Effort / autonomy*: =:quick:= a spare-moment fix (minutes, not a sitting); =:solo:= Claude can carry it end to end — there's a build path, a test path, and no upfront decision needed (a leftover manual spot-check doesn't disqualify it). - *Topic / area* (open): the subsystem a task touches — e.g. =:hyprland:= =:waybar:= =:mpd:= =:music:= =:network:= =:tooling:= =:llm:= =:eask:= =:pocketbook:= =:cmail:=. Coin a new one when it aids filtering. * Archsetup Open Work -** TODO [#B] Panels moveable + resizable by drag :feature:waybar:network:bluetooth: +** DONE [#B] Panels moveable + resizable by drag :feature:waybar:network:bluetooth: +CLOSED: [2026-07-04 Sat] +Resolved by the 2026-07-03 instrument-console rebuild (dotfiles e993c3f): both net + bt panels switched from anchored gtk4-layer-shell overlays to normal floating windows (set_decorated(False), positioned by the net.cjennings.netpanel window rule), so Hyprland moves them on drag and resizes on corner-drag natively. That was exactly the "switch to a normal floating window" approach the design note flagged as the required decision. + Both the net and bluetooth instrument-console panels should be repositionable and resizable at runtime: click-drag to move the panel anywhere on screen, drag the corners to resize. Raised from roam capture 2026-07-03. Design note: the panels are gtk4-layer-shell overlays anchored TOP+RIGHT with fixed margins — layer-shell surfaces are compositor-positioned, so free drag-move/resize needs either dynamic margin updates on pointer motion or a switch to a normal floating window (Hyprland moves/resizes those natively). Approach decision required before build. -** TODO [#B] Net panel wider initial width :waybar:network:quick: +** CANCELLED [#B] Net panel wider initial width :waybar:network:quick: +CLOSED: [2026-07-04 Sat] +Superseded by the 2026-07-03 instrument-console rebuild (dotfiles e993c3f): the panel is now a floating, user-resizable window (set_default_size(420, 560)), no longer a right-anchored layer-shell surface. The task's mechanic ("keep the right edge fixed, extend the left border leftward") assumed the old anchored surface, which no longer exists — the width is now drag-adjustable. Cancelled per the 2026-07-04 audit (Craig's call to close rather than re-file a "bump the 420px default" task). + Start the network panel a bit wider — keep the right edge fixed (it's right-anchored), extend the left border leftward. Raised from roam capture 2026-07-03. -** TODO [#B] Net panel doctor results can't display :bug:waybar:network: -The doctor diagnostic output is unreadable — the results well is too constrained to show the multi-line result. It should open a results box tall enough for several lines with a copy-results button; closing it via an X in the box's upper-right collapses the space back to what it occupied before. Raised from roam capture 2026-07-03. +** DONE [#B] Net panel doctor results can't display :bug:waybar:network: +CLOSED: [2026-07-04 Sat] +Resolved by the 2026-07-03 instrument-console rebuild (dotfiles e993c3f): the panel gained a streaming output well (gui.py) with a "Copy results" button (via wl-copy) and a dismiss control that collapses the well back to the panel's pre-open height (_shrink_to_compact asks Hyprland to resize back). Doctor/speed-test output streams into it as appended lines — matching the task's ask for a tall results box, copy button, and collapse-back. This capture (filed 2026-07-03 morning) predates the same-day 22:06 redesign that addressed it. -** TODO [#B] Audio panel spec :feature:waybar:audio:solo: -:PROPERTIES: -:LAST_REVIEWED: 2026-07-02 -:END: -Work Craig's ask (roam inbox, 2026-07-02) into a spec, net/bt-panel kin: an audio panel replacing the pypr audio scratchpad (Super+A) with the same functionality — change the default/active output (speaker) and input (mic), volume control for both. The one new capability: a push-to-talk mic mode for meetings — mic stays muted except while the space bar is held, releasing re-mutes. (Hold-to-talk under Wayland needs a global key grab — likely a hyprland bind pair on press/release or an evdev listener; feasibility research belongs in the spec.) Related current bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. +The doctor diagnostic output is unreadable — the results well is too constrained to show the multi-line result. It should open a results box tall enough for several lines with a copy-results button; closing it via an X in the box's upper-right collapses the space back to what it occupied before. Raised from roam capture 2026-07-03. ** TODO [#B] Scrolling/Carousel layout: frame fit + wrap-around :hyprland: :PROPERTIES: @@ -72,7 +75,7 @@ Refiled from the archsetup task audit (2026-06-28), landed via ~/.dotfiles/inbox ** TODO [#B] Waybar network module — custom/net :feature:waybar:network: :PROPERTIES: -:LAST_REVIEWED: 2026-06-29 +:LAST_REVIEWED: 2026-07-04 :END: Unifies the old wifi-no-internet indicator (was =[#C]=) and the network-manager dropdown (was =[#B]=) into one =custom/net= module: a tested Python =net= engine @@ -178,25 +181,34 @@ troubleshooting from the failure table, rollback); archsetup Hyprland dep instal stow step. Verify: =net --help= and each subcommand complete; user-guide covers every command + the recovery targets. +Build handed off to the dotfiles project 2026-07-04 (=~/.dotfiles/inbox/2026-07-04-1305-from-archsetup-phase4-handoff.md=): archsetup deps confirmed installed, the remaining help/user-guide/rollout-doc work is in the net package. dotfiles pings back when it lands. -*** TODO Phase 5 — VPN / WireGuard (vNext) :network: -Fold the existing archsetup wireguard tooling into the panel + CLI (=net vpn ...=). -Out of the v1 milestone; spec separately when picked up. (v1 only detects + -classifies a VPN-routed failure, it doesn't repair it.) +*** TODO Phase 5 — VPN / WireGuard CLI fold (vNext) :network: +Rescoped 2026-07-04 (audit): the tunnels track already shipped most of the original Phase 5. Panel tunnel bring-up/down and detection landed (dotfiles 2d9d060 probes tailscale/NM-wireguard/Proton; 21db05a brings overlays up/down from the panel's Tunnels sub-view; 31ba056 diagnose/doctor understand tunnel routes; archsetup 2e40781 wireguard config import; the net-panel-other-interfaces spec is IMPLEMENTED). What remains for Phase 5 is only the =net vpn ...= CLI subcommand — cli.py still has no vpn/tunnel parser. Fold the panel's existing tunnel operations into a CLI surface; spec separately when picked up. -** TODO [#B] Timer GTK panel :feature:waybar: +** TODO [#C] Waybar collapse control: replace the triangle glyph :feature:waybar: :PROPERTIES: -:LAST_REVIEWED: 2026-07-02 +:LAST_REVIEWED: 2026-07-04 +:END: +From the 2026-07-04 roam capture. The waybar collapse mechanism (click the triangle, the bar sections redisplay shortened) works, but the triangle glyph doesn't match the instrument-console aesthetic the panels now use. Replace it with something in keeping with the console look. Aesthetic decision — bring Craig two or three concrete glyph/style options (a machined chevron, a console-key style expander, an engraved caret) before wiring. Dotfiles waybar config (handled per the archsetup-owns-dotfiles rule). Raised alongside the net-panel/audio speedrun; deferred from it because the glyph choice is a taste call. + +** TODO [#C] Net panel: driver-health diagnostic tier :feature:network: +:PROPERTIES: +:LAST_REVIEWED: 2026-07-04 :END: -Initial spec written 2026-07-02: [[file:docs/design/2026-07-02-timer-panel-spec.org]] (DRAFT — four decisions await Craig's review before build; net-panel Blueprint/GTK4 stack, wtimer stays the state owner). +Follow-up from the 2026-07-04 net-panel hardening speedrun (Craig's cj question on the no-WiFi item). The shipped no-wifi-hardware verdict covers "no adapter at all." This tier covers "adapter present but the driver is wedged": read-only health signals — =ip link= (device present but no-carrier / down), =dmesg= / =journalctl -k= for firmware-load failures, =rfkill= for a hard block, =modinfo= / =lsmod= for the driver module — classified before a generic reset. Remedy actions: a privileged =modprobe -r <mod> && modprobe <mod>= reload of the wifi driver, and a firmware-package pointer when the failure is a missing/failed firmware load. Dotfiles net-package work (handled per the archsetup-owns-dotfiles rule). Design pass first to decide whether it's worth a repair tier vs a needs-user-action pointer. + +** DONE [#B] Timer GTK panel :feature:waybar: +CLOSED: [2026-07-05 Sun] +Built and shipped to dotfiles 2026-07-05 in a no-approvals speedrun (4 commits =1f4f270=..=78d3cbb=): wtimer gained watch/lap/save; a new =timer/= package holds a GTK-free PanelModel (62 tests) and the GTK instrument-console panel; the bar's =custom/timer= now opens the panel and the fuzzel creation flow retired. Spec: [[file:docs/specs/2026-07-02-timer-panel-spec.org]] (IMPLEMENTED). Code-complete; live GTK verification filed under Manual testing and validation below. -From Craig's roam capture 2026-07-02: give the timer a GTK UI/UX like the network panel. +From Craig's roam capture 2026-07-02: give the timer a GTK UI/UX like the network panel. Scope expanded via a later cj comment (queue/output-wall auto-sorted by fire time, stopwatch lap/stop + saveable runs, 5/25 configurable defaults, up to 10 timers, widget-gallery elements) — folded into the spec's Build scope and shipped. ** TODO [#B] Desktop-settings dropdown panel :waybar: :PROPERTIES: :LAST_REVIEWED: 2026-06-24 :END: -Initial spec written 2026-07-02: [[file:docs/design/2026-07-02-desktop-settings-panel-spec.org]] (DRAFT — four decisions await Craig's review before build; architecture updated to the net panel's Blueprint/GTK4 stack). +Initial spec written 2026-07-02: [[file:docs/specs/2026-07-02-desktop-settings-panel-spec.org]] (DRAFT — four decisions await Craig's review before build; architecture updated to the net panel's Blueprint/GTK4 stack). One waybar dropdown gathering the desktop toggles and sliders into a single settings panel, opened from a gear/settings glyph on the bar. Incorporate: - *Auto-dim* toggle (the =custom/dim= feature just shipped — fold in here, or keep the standalone indicator and mirror it). @@ -253,9 +265,14 @@ Boot the configured endpoint and send a short prompt; surface success/failure + Acceptance: fresh VM install of the ratio profile reaches an endpoint on =:8081= that answers a smoke prompt; velox profile gets Q4_K_M + 8B and answers a prompt within reasonable laptop latency; network-down install completes successfully with the pending-models warning surfaced. +** TODO [#C] Voice dictation / speech-to-text input :feature:tooling: +Push-to-talk dictation that types transcribed speech into the focused Wayland window — usable at any text field, including the Claude Code terminal prompt and Emacs buffers. Claude Code has no built-in voice input; dictation has to happen at the OS level and inject text. Raised 2026-07-03. + +Tool choice is the open decision (needs Craig): =nerd-dictation= (Vosk, lighter, lower accuracy) vs a =whisper.cpp=-based daemon (heavier, higher accuracy, optional GPU). Wayland typing backend is =wtype= or =ydotool=. Scope once chosen: install + model download, a push-to-talk keybind (Hyprland), and an autostart entry; fold into archsetup so it lands on both daily drivers. Consider an Emacs-native path (=whisper.el=) as a complement for in-buffer dictation. + ** TODO [#B] Review post-archsetup laptop setup steps (velox 2026-04-10) :PROPERTIES: -:LAST_REVIEWED: 2026-06-09 +:LAST_REVIEWED: 2026-07-04 :END: Items discovered during velox setup that needed manual intervention after archsetup. Decide which should be automated in archsetup vs documented as post-install steps. @@ -265,10 +282,8 @@ Both bluetooth and wifi were soft-blocked by rfkill. Fix was ~rfkill unblock blu ~systemd-rfkill~ persists state, so unblocking once should stick, but new installs may default to blocked. Consider: add ~rfkill unblock all~ to archsetup post-install or a firstboot script. -*** TODO Review: /efi mount permissions world-accessible (security) -Default vfat mount had ~fmask=0022,dmask=0022~. Fixed to ~fmask=0077,dmask=0077~ in fstab. -~bootctl~ warned about world-accessible random seed file. -Consider: set restrictive fmask/dmask in archsetup's fstab generation. +*** 2026-07-04 Sat @ 11:48:24 -0500 Automated /efi restrictive mount permissions in fstab generation +archsetup:2827-2836 now rewrites the /efi fstab line to =fmask=0177,dmask=0077= (idempotent), so fresh installs no longer land the world-accessible =fmask=0022,dmask=0022= default. Confirmed via the 2026-07-04 task audit. (Original velox note: default vfat mount had =fmask=0022,dmask=0022=, hand-fixed to restrictive; bootctl warned about a world-accessible random-seed file.) *** TODO Review: tmpfs layered over ZFS /tmp causing systemd-tmpfiles failures ~systemd-tmpfiles-clean.service~ failed repeatedly with "Protocol driver not attached". @@ -279,9 +294,8 @@ Fix: ~systemctl mask tmp.mount~. Consider: mask tmp.mount in archsetup when ZFS CPU running old microcode. Installed ~intel-ucode~ and rebuilt initramfs. Consider: add intel-ucode (or amd-ucode) to archsetup package list based on CPU vendor. -*** TODO Review: syncthing installed but not enabled -Package was installed but service was not enabled. Fixed with ~systemctl enable --now syncthing@cjennings~. -Consider: enable syncthing service in archsetup post-install. +*** 2026-07-04 Sat @ 11:48:24 -0500 Automated syncthing user-service enable in archsetup +archsetup:2263-2271 now installs syncthing and enables the user service (via symlink), so fresh installs no longer leave it installed-but-disabled. Confirmed via the 2026-07-04 task audit. (Original velox note: package installed but service not enabled; hand-fixed with =systemctl enable --now syncthing@cjennings=.) *** TODO Review: awww-daemon crashes at boot (coredump) Wallpaper daemon crashed with abort() shortly after boot. Hyprland also coredumped at same time. @@ -509,17 +523,15 @@ Read recommended resources to make informed security decisions (see metrics for :END: Practical guidelines for working in public spaces -** TODO [#B] Test each modernization thoroughly before replacing -:PROPERTIES: -:LAST_REVIEWED: 2026-06-28 -:END: -Ensure new tools integrate with the Hyprland environment and don't break workflow (the fleet is all Hyprland now; archsetup still supports DWM/X11 but no current machine uses it) +** CANCELLED [#B] Test each modernization thoroughly before replacing +CLOSED: [2026-07-04 Sat] +Retired in the 2026-07-04 audit (Craig's call): a standing-judgment umbrella with no completion criterion. The fleet is Hyprland-only now, and per-change test discipline is already carried by the actual work (TDD + the VM harness), so this adds nothing to track. Original intent: ensure new tools integrate with the Hyprland environment and don't break workflow (archsetup still supports DWM/X11 but no current machine uses it). -** TODO [#C] Window focus lost when unhiding stashed windows :bug:hyprland: -:PROPERTIES: -:LAST_REVIEWED: 2026-06-24 -:END: -From the roam inbox: hiding a window (e.g. the org-capture popup) then unhiding it should leave the unhidden window focused, but another window typically takes focus. Also =ctrl+j/k= (layout-navigate) can't reach the unhidden window afterward — it should always reach any visible window except the waybar. Involves stash-restore + layout-navigate; needs interactive reproduction with Craig. +** DONE [#C] Window focus lost when unhiding stashed windows :bug:hyprland: +CLOSED: [2026-07-04 Sat] +Verified fixed live on ratio 2026-07-04 (Craig at the machine). Stash (Super+O) → restore (Super+Shift+O) left the restored window focused, and Super+J/K (layout-navigate) cycled focus normally afterward — both original symptoms gone. Resolved by two fixes that postdated the filing: dotfiles 5619342 (raise window on focus nav, stop float focus-follow, 2026-06-28) and 09815f3 (cycle focus by address so j/k works in monocle, 2026-06-29). Both confirmed present in ratio's HEAD and in the live layout-navigate script at test time. + +From the roam inbox: hiding a window (e.g. the org-capture popup) then unhiding it should leave the unhidden window focused, but another window typically takes focus. Also =ctrl+j/k= (layout-navigate) can't reach the unhidden window afterward — it should always reach any visible window except the waybar. Involves stash-restore + layout-navigate; needs interactive reproduction with Craig. (Note: the actual bind is Super+J/K, not ctrl+j/k as the capture said.) ** TODO [#C] Ensure sleep/suspend works on laptops :PROPERTIES: @@ -595,6 +607,84 @@ Parse yay errors and provide specific, actionable fixes instead of generic error Enhance existing indicators to show what's happening in real-time ** TODO Manual testing and validation +*** Timer panel: apply the new package + configs, then reboot (precondition for the timer tests) +What we're verifying: the new =timer/= package, the =timer= / =timer-panel= bin shims, and the waybar + hyprland edits are live. New files need a restow (a plain =git pull= doesn't symlink them). Quit Hyprland or run from a TTY. +#+begin_src sh :results output +cd ~/.dotfiles && git pull && make restow hyprland +#+end_src +Then reboot (picks up the float rule, the 2px border, the Super+P/Shift+P + Control_R keybinds, and the bar rewire). +*** Timer panel opens from the bar, fuzzel retired +What we're verifying: the bar module opens the GTK panel, not the old fuzzel menu. +- Click the timer module on the bar. +Expected: the instrument-console timer panel opens floating, top-right; the old fuzzel type-menu does not appear. +- Press q or Escape. +Expected: the panel closes. +*** Create a timer (preset + freeform validation) +- Open the panel, pick the timer type, click the 5m chip, click +. +Expected: a 5:00 timer appears in the queue and counts down live. +- Type a bad value (e.g. "xyz") in the freeform entry. +Expected: an inline reject reason shows and + is blocked; a valid value (e.g. "25m") adds on +. +*** Queue auto-sorts by fire time +- Add a 25m timer, then a 5m timer, then an alarm a few minutes out. +Expected: the queue orders soonest-first (5m above 25m; the alarm placed by its fire time). +*** Per-item controls: pause / cancel / promote +- On a running timer: pause then resume; cancel; click the row to promote. +Expected: pause freezes the value and resume continues; cancel removes it; promote makes it the bar's primary glyph. +*** Stopwatch lap / stop / save +- Add a stopwatch, hit Lap two or three times (name one lap via the popover), then Stop. +Expected: laps record; Stop saves the run. Then: +#+begin_src sh :results output +cat ~/org/stopwatch-runs.org +#+end_src +Expected: an org heading for the run with a lap table (split / cumulative / optional name). +*** 10-item cap + live countdown +- Queue items until 10 exist. +Expected: + disables with a reason at 10; running countdowns advance live in the panel (the wtimer watch subscription). +*** Audio panel: apply the new shims + configs (precondition for the tests below) +What we're verifying: the new =audio=/=audio-panel=/=waybar-audio= bin shims and the hyprland.conf + waybar config edits are live. New files need a restow (a plain =git pull= doesn't symlink them). Quit Hyprland or run from a TTY — restowing live Hyprland writes a stub hyprland.conf. +#+begin_src sh :results output +cd ~/.dotfiles && git pull +cd ~/.dotfiles && make restow hyprland +#+end_src +- Reload waybar: mod+B (or =killall -SIGUSR2 waybar=). +- Reload the Hyprland config: mod+Shift+R (or =hyprctl reload=). +Expected: =which audio audio-panel waybar-audio= resolves all three to ~/.local/bin symlinks; no stow conflicts reported. + +*** Audio panel: opens and reads the live graph +What we're verifying: Super+A opens the instrument-console panel (not the old pulsemixer scratchpad) and it shows the real devices. +- Press Super+A. +Expected: the audio panel opens top-right. OUTPUTS lists every sink, INPUTS every mic (no .monitor entries), the default device in each is emphasized (cream name, gold glyph), each row shows its volume percent, and the twin OUT·PLAY / IN·MIC needles deflect. Esc closes it. + +*** Audio panel: set default, volume fader, per-device mute +What we're verifying: the three row interactions drive the real graph and the panel re-reads to confirm. +- With two or more outputs present, click a non-default output row's body. +Expected: that device becomes default (gold DEF emphasis moves to it) and playback follows. +- Drag a device's fader. +Expected: the volume percent tracks the fader and the actual device volume changes (no lag, no jump to the border). +- Click a device's trailing mute glyph. +Expected: that one device mutes/unmutes; the glyph and caption reflect it; other devices unaffected. + +*** Audio panel: master quick-mute (faceplate switch + mute key) +What we're verifying: the master switch and the XF86AudioMute key both mute every output at once (not just the default sink). +- Flip the faceplate master switch. +Expected: every output mutes, the state word reads MUTED, the badge shows, the faceplate glyph goes to the slashed speaker. Flip back to restore. +- With two outputs audible, press the hardware mute key (XF86AudioMute). +Expected: both outputs mute (master), not just the default. Press again to unmute all. + +*** Audio panel: mic modes + push-to-talk (the meeting case) +What we're verifying: LIVE/MUTED work, and PUSH·TALK holds the mic muted except while the talk key is held — the one genuinely new capability, and the one only a live test can confirm. +- Click LIVE, then MUTED, watching the default mic row. +Expected: LIVE unmutes the mic (needle lifts), MUTED mutes it (needle red); the active mode key shows a gold lamp. +- Click PUSH·TALK. In a call app (or =audio status= in a loop), watch the mic while you hold Space, speak, then release. +Expected: mic is muted at rest, un-mutes for exactly as long as Space is held, re-mutes on release. Switching back to LIVE or MUTED disarms the hold (Space types normally again). + +*** Audio panel: visual polish eyeball + bar entry point +What we're verifying: the panel looks right in the dupre instrument-console language, and the bar opens it. +- Look over the whole panel: faceplate spacing, engraved section labels, fader styling, VU needle centering, the muted-vs-audible glyph/color states. +Expected: it reads as a sibling of the net and bt panels; nothing overflows the gold border; the faders and gauges look machined, not stock GTK. +- Right-click the waybar sound module. +Expected: the panel opens (left-click still mutes, scroll still changes volume). + *** Timer dialog: Escape cancels at every step What we're verifying: Escape in the real fuzzel dialogs aborts the whole flow (the fix rides fuzzel's abort exit code; the unit tests fake it, this is the live confirmation). - Left-click the timer module on the bar, pick "timer", then hit Escape at the Duration prompt. @@ -612,6 +702,7 @@ Expected: a fail notification names the bad input; nothing is created. *** Speed test streams in the panel What we're verifying: the panel's speed test fills in as phases complete instead of dumping everything at the end (the CLI path is live-verified; this is the GTK rendering). +NOTE (2026-07-04 audit): these steps describe the OLD four-tab Blueprint panel ("Diagnostics → Network Performance"). The net panel was rebuilt as a single-screen instrument console (dotfiles 800ef60, f4e688e dropped the Blueprint pages), so the navigation below no longer matches the shipped UI — reword to the console layout before running. Don't delete; the streaming-render behavior is still worth verifying. - Open the net panel, Diagnostics → Network Performance → Run Speedtest. Expected: a Ping/Download/Upload checklist appears under the running row; ping lands within seconds, download mid-run, upload near the end; then the final rows (with jitter on Ping, a Packet loss row) replace it. A Tip row appears only if a rule fired, and it names the numbers that triggered it. @@ -812,6 +903,7 @@ What we're verifying: the custom/net clicks and the airplane keybind. Clicks (se - Check =airplane-mode= is still present (=ls ~/.local/bin/airplane-mode=), and =waybar-airplane= / =waybar-netspeed= / =custom/airplane= are gone. *** Network module Phase 3 — panel Diagnose / Repair / Speed test tabs What we're verifying: the four-tab panel works end to end. Left-click =custom/net= to open it. +NOTE (2026-07-04 audit): the "four-tab panel" framing predates the instrument-console rebuild (dotfiles 800ef60, f4e688e dropped the Blueprint pages). Diagnose/Repair/Speed-test now live in the single-screen console, not tabs — reword the steps to the console layout before running. Don't delete; the underlying behaviors still need verification. - Diagnose tab → "Run diagnose". - Expected: a list of steps (link, DHCP, gateway, DNS config, DNS resolution, internet) each with a ✓/✗/… glyph + evidence; on a captive network an "Open portal" button appears. - Repair tab → click Reset (or Bounce, or DNS override test). @@ -822,7 +914,7 @@ What we're verifying: the four-tab panel works end to end. Left-click =custom/ne ** DOING [#B] Prepare for GitHub open-source release :PROPERTIES: -:LAST_REVIEWED: 2026-06-28 +:LAST_REVIEWED: 2026-07-04 :END: Remove personal info, credentials, and code quality issues before publishing. *** 2026-06-16 Tue @ 00:55:39 -0500 Six dotfiles-scoped sub-tasks moved to the ~/.dotfiles project @@ -869,6 +961,8 @@ Options: =git filter-repo= to rewrite history, or start a fresh repo for the Git Recommend: fresh repo for GitHub (keep cjennings.net remote with full history). **** 2026-06-28 Sun @ 13:29:29 -0400 Reconciled: 589 commits, 5 credential files still in history History is now 589 commits (the 2026-05-11 note's "275" is stale). Only the calendar-feed file has been filter-repo'd so far (2026-05-20). The five credential files remain in history at their pre-=b10cba5= paths: =.tidal-dl.token.json= (5 commits), =calibre/smtp.py.json= (6), =transmission/settings.json= (5), =.msmtprc= (8), =.mbsyncrc= (9). None are tracked in the current tree. The scrub-or-fresh-repo decision still stands. +***** 2026-07-04 Sat @ 11:48:24 -0500 Count refresh — history now 565 commits; re-verify the 5-file claim before scrubbing +The 2026-07-04 audit found the history is now 565 commits, down from the 589 recorded above. Because the count dropped, re-verify that the five credential files are still present in history (re-run the per-file =git log --all -- <path>= check) before relying on the scrub scope — the earlier count is stale and the file set may have moved. *** 2026-06-24 Wed @ 19:41:56 -0400 Gated device-specific udev rules behind a flag The Logitech BRIO udev rule is now wrapped in =if [ "$install_device_udev_rules" = "true" ]=, fed by a new =INSTALL_DEVICE_UDEV_RULES= key (default yes, opt-out — still mainly a personal project). Added the var default, the config read, a =validate_config= check, and an =archsetup.conf.example= entry. Verified: default/yes writes the rule, no skips it, bogus is rejected; =bash -n= clean. @@ -1234,7 +1328,7 @@ CLOSED: [2026-07-02 Thu] :PROPERTIES: :SPEC_ID: 1271a845-4463-4831-9902-990eda6b2265 :END: -Spec: [[file:docs/design/2026-07-02-bluetooth-panel-spec.org]] (IMPLEMENTED 2026-07-02 — all five phases shipped same day: engine eb2230f, panel 76b2c05, bar module e372de3, bt-priv + blueman retirement 2a026b1/d8d8c53, install wiring proven by VM assertions). Residual: the phase 4-5 VM assertions run on the next VM pass; ratio picks up the package removal + hand-links on its trip list. +Spec: [[file:docs/specs/2026-07-02-bluetooth-panel-spec.org]] (IMPLEMENTED 2026-07-02 — all five phases shipped same day: engine eb2230f, panel 76b2c05, bar module e372de3, bt-priv + blueman retirement 2a026b1/d8d8c53, install wiring proven by VM assertions). Residual: the phase 4-5 VM assertions run on the next VM pass; ratio picks up the package removal + hand-links on its trip list. A bluetooth panel driving a CLI underneath (bluetoothctl one-shot verbs), consistent in look and feel with the net panel (GTK4 + layer-shell + Blueprint, humble-object presenter, verify-everything). Minimalistic interface, full functionality, plus a diagnostics/troubleshooting section mirroring the net panel's Diagnostics tab. Bar module glyph opens it. Craig's ask (2026-07-02): follow UX/UI best practices; where the net panel's patterns conflict with best practices, file a net-panel bug task rather than clone the flaw. @@ -1357,7 +1451,7 @@ CLOSED: [2026-07-03 Fri] :PROPERTIES: :SPEC_ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5 :END: -The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/design/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context. +The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/specs/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context. *** 2026-07-03 Fri @ 03:20:00 -0400 Phase 2 shipped: net GTK-free console layer + engine verbs Dotfiles =81ec9c3= (TDD, 52 new tests, 581 net green). Pure presenter logic for the single-screen console, no view code touched: =viewmodel.net_faceplate= (state word + lamp + TUNNEL/AIRPLANE badges, wired-link-wins precedence), =network_console_rows= (ethernet pinned, radio-off note, active-then-signal sort, per-row lamp/caption/ladder/forget), =channel_headline= (wired device+speed / SSID+ladder+dBm / not-connected placeholder), =tunnel_console_rows=, dial-meter geometry (=meter_needle_deg= + =meter_scale= 100→1000 auto-relabel), =signal_bars=/=mbps_label=, and =panel.ArmState= (two-click arm-to-fire for forget/disconnect). Engine verbs: =manage.wifi_radio= (nmcli radio wifi on|off), =manage.device_up= (ethernet take-the-route), =sysio.link_speed_mbps= (/sys wired speed), =connections.ethernet_devices=, hidden flag on =manage.add=. @@ -1439,7 +1533,7 @@ CLOSED: [2026-07-02 Thu] :PROPERTIES: :SPEC_ID: 79a1075a-4b56-4f25-a861-b69f120a636a :END: -Spec: [[file:docs/design/2026-07-02-net-panel-other-interfaces-spec.org]] (DOING — reviewed READY and decomposed 2026-07-02 evening; all four decisions were resolved same morning, claims re-verified live at review: protonvpn binary, tailscale JSON shape, seven importable wireguard configs). +Spec: [[file:docs/specs/2026-07-02-net-panel-other-interfaces-spec.org]] (DOING — reviewed READY and decomposed 2026-07-02 evening; all four decisions were resolved same morning, claims re-verified live at review: protonvpn binary, tailscale JSON shape, seven importable wireguard configs). Tunnels visible and controllable in the net panel: tailscale + NM wireguard + proton-vpn-cli probes, a Tunnels group in Connections, diagnose/doctor route-ownership awareness, a bar badge when a tunnel owns the default route, archsetup operator flag + package swap, and the one-time NM import of the seven Proton configs. Origin: roam inbox capture 2026-07-02. @@ -1468,7 +1562,7 @@ CLOSED: [2026-07-02 Thu] :PROPERTIES: :LAST_REVIEWED: 2026-07-02 :END: -Reassigned to .emacs.d 2026-07-02 (handoff: =~/.emacs.d/inbox/2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff.org=). The "file manager" is the dirvish popup (Super+F, an Emacs frame), not nautilus — so the fix is elisp in dirvish's external-open path (=cj/xdg-open=): spawn the handler directly with =start-process=, hide the popup frame, restore it from the process sentinel, notify on non-zero exit. The spec drafted here first ([[file:docs/design/2026-07-02-file-manager-swallow-spec.org]], now CANCELLED) records the feasibility finding that stays useful: gio/xdg-open launches double-fork, so no PID-ancestry approach (Hyprland native swallow included) can ever connect viewer to launcher. +Reassigned to .emacs.d 2026-07-02 (handoff: =~/.emacs.d/inbox/2026-07-02-2231-from-archsetup-dirvish-popup-swallow-handoff.org=). The "file manager" is the dirvish popup (Super+F, an Emacs frame), not nautilus — so the fix is elisp in dirvish's external-open path (=cj/xdg-open=): spawn the handler directly with =start-process=, hide the popup frame, restore it from the process sentinel, notify on non-zero exit. The spec drafted here first ([[file:docs/specs/2026-07-02-file-manager-swallow-spec.org]], now CANCELLED) records the feasibility finding that stays useful: gio/xdg-open launches double-fork, so no PID-ancestry approach (Hyprland native swallow included) can ever connect viewer to launcher. When the file manager launches another app, it should hide to a special workspace (the "swallow" pattern) and return when that process ends, rather than vanishing. Today it disappears with no signal of whether it's coming back, so the user can't tell success from failure — they should quit explicitly instead. Origin: roam inbox capture. @@ -1649,3 +1743,11 @@ GUI. That's a terminal reporting to the user, but only because there's no panel as an explicit carve-out (recovery-only, not terminal-as-UI), or replace it with something else (a TTY text UI still counts as a terminal)? Your call settles whether the Makefile/CLI recovery targets stay in the spec. +** DONE [#B] Audio panel spec :feature:waybar:audio:solo: +CLOSED: [2026-07-03 Fri] +:PROPERTIES: +:LAST_REVIEWED: 2026-07-02 +:END: +Went past the spec to a full build in a no-approvals speedrun. Spec is now IMPLEMENTED ([[file:docs/specs/2026-07-03-audio-panel-spec.org]], all 5 Decisions resolved). The panel shipped in the dotfiles repo (branch panel-bugfixing, commits 65e5bb0..9601420): pactl engine, GTK-free presenter, GTK instrument-console panel (OUTPUTS/INPUTS device rows with faders + per-device mute, LIVE/MUTED/PUSH·TALK mic keys, twin VU gauges, master quick-mute), Hyprland-bind push-to-talk, bar indicator, and the bar/keybind wiring (Super+A → panel, XF86AudioMute → master quick-mute). 102 unit tests + a passing AT-SPI smoke on velox. Live-eyeball validation filed under Manual testing and validation. Apply steps + follow-ups handed to the dotfiles project inbox. + +Original ask (roam inbox, 2026-07-02): net/bt-panel kin — change default output/input, volume for both, push-to-talk mic mode for meetings, master quick-mute, bar sound-glyph state. Related bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html=. |
