From 19cfadfe6684a6c105ac0c4b3e4ba168ef02f123 Mon Sep 17 00:00:00 2001 From: Ishan Malu Date: Fri, 4 Sep 2026 21:27:45 +0300 Subject: [PATCH 1/2] Make the brightness slider and Night Mode act on the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Screen tab demo had a brightness slider drawn at 100 with a knob painted at zero, and a Night Mode toggle that only wrote a caption. Both are now live: dragging the slider dims the page, and Night Mode warms it. Brightness reuses the app's own mechanism rather than approximating it. Where Perch has no backlight to move it floats a black overlay above everything, click-through, clamped at 0.85 so the slider never disappears (Brightness.swift). The page does the same with a fixed scrim. Night Mode needs two layers. Multiply takes the blue out of anything light, but against a near-black backdrop it multiplies to near-black, so the dark theme would not shift at all. A screen layer lifts red and green in the shadows, and the pair reads as one warm ramp in either theme. The slider is a real slider: pointer drag with capture, arrow and page keys, Home and End, and the ARIA a span with a click handler was missing. It is captioned "in the panel" because brightness has no shortcut — nudgeAll() in Brightness.swift is unreferenced. Also fixes the scrim being declared after the script that looks for it, which left the whole block skipped. --- docs/index.html | 125 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 5 deletions(-) diff --git a/docs/index.html b/docs/index.html index 0052d74..d82f6a3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -364,6 +364,38 @@ position: absolute; top: 50%; width: 11px; height: 11px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.35); transform: translate(-50%,-50%); } + /* The brightness slider is the one control on the page that acts on the page. + Widened hit area: a 3px track is honest to the app but hopeless to grab. */ + #brightSlider { cursor: grab; touch-action: none; } + #brightSlider::before { content: ''; position: absolute; inset: -9px 0; } + #brightSlider.drag { cursor: grabbing; } + #brightSlider:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; border-radius: 2px; } + + /* Perch dims a non-built-in display with a black overlay window pinned above + everything, click-through, never past 0.85 so the slider stays findable + (Brightness.swift). This is the same trick, scoped to the page. */ + #dimmer { + position: fixed; inset: 0; z-index: 9999; pointer-events: none; + background: #000; opacity: 0; + } + /* Night Mode shifts the gamma ramps warm. Multiply against an amber pane is + the closest a web page gets: it pulls the blues down instead of laying a + haze over everything, which is what a plain translucent overlay does. */ + #warmer, #warmLift { + position: fixed; inset: 0; pointer-events: none; opacity: 0; + transition: opacity .45s ease; + } + /* Multiply pulls the blues out of everything light. On its own that is all a + warm overlay can do — against a near-black background it multiplies to + near-black, so a dark page would not shift at all. */ + #warmer { z-index: 9998; background: #ff9526; mix-blend-mode: multiply; } + /* So the shadows get the other half of a gamma shift: screen lifts red and + green where there is nothing to take blue away from. Together the two read + as one warm ramp in either theme. */ + #warmLift { z-index: 9997; background: #2b1600; mix-blend-mode: screen; } + @media (prefers-reduced-motion: reduce) { + #warmer, #warmLift { transition: none; } + } /* ---------- layout tiles ---------- The app draws a 26pt tile glyph for each window action, then a divider and @@ -567,6 +599,10 @@ + + + +

Put windows where you want them

- - 100 + + 100
Night Mode - +
@@ -1315,11 +1353,18 @@

Privacy

var night = document.getElementById('nightToggle'); if (night) { + var warmLayers = [document.getElementById('warmer'), + document.getElementById('warmLift')]; function flip(){ night.classList.toggle('on'); + var on = night.classList.contains('on'); + night.setAttribute('aria-checked', String(on)); + warmLayers.forEach(function (el) { + if (el) el.style.opacity = on ? '0.34' : '0'; + }); wName.innerHTML = 'Night Mode⌃⌥N'; - wDesc.textContent = night.classList.contains('on') - ? 'Warming the display by shifting the gamma ramps, the way f.lux does. No overlay window, so screenshots are unaffected.' + wDesc.textContent = on + ? 'On — and warming this page, so you can see it. In the app it shifts the gamma ramps the way f.lux does, with no overlay window, so screenshots are unaffected.' : 'Off. Night Mode warms the display by shifting the gamma ramps rather than covering the screen.'; } night.addEventListener('click', flip); @@ -1327,6 +1372,76 @@

Privacy

if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); flip(); } }); } + + /* ---- Brightness: the slider dims the page it is sitting on ---- + Perch dims a display it has no backlight control over by floating a + black overlay above everything, click-through, never past 0.85 so the + slider stays visible (Brightness.swift). Same mechanism here. */ + var slider = document.getElementById('brightSlider'); + var numEl = document.getElementById('brightNum'); + var dimmer = document.getElementById('dimmer'); + if (slider && numEl && dimmer) { + var MAX_DIM = 0.85; + var fillEl = slider.querySelector('i'), knobEl = slider.querySelector('b'); + var level = 1; + + function paintBright() { + var pct = Math.round(level * 100); + fillEl.style.width = (level * 100) + '%'; + knobEl.style.left = (level * 100) + '%'; + numEl.textContent = pct; + slider.setAttribute('aria-valuenow', pct); + slider.setAttribute('aria-valuetext', pct + '%'); + dimmer.style.opacity = ((1 - level) * MAX_DIM).toFixed(3); + } + + // No global shortcut reaches brightness, so it is tagged like the other + // panel-only actions rather than given a key it does not have. + function describeBright() { + wTiles.forEach(function (t) { t.classList.remove('on'); }); + wName.innerHTML = 'Brightness' + keyTag('panel'); + wDesc.textContent = level === 1 + ? 'Every display on one slider, external monitors included. Drag it — this one really does dim the page.' + : 'At ' + Math.round(level * 100) + '%. A built-in display moves its actual backlight; anything else is dimmed with an overlay, which is what you are looking at.'; + } + + function fromPointer(e) { + var r = slider.getBoundingClientRect(); + if (r.width <= 0) return; + level = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)); + paintBright(); describeBright(); + } + + slider.addEventListener('pointerdown', function (e) { + e.preventDefault(); + slider.classList.add('drag'); + slider.focus(); + try { slider.setPointerCapture(e.pointerId); } catch (err) {} + fromPointer(e); + }); + slider.addEventListener('pointermove', function (e) { + // Only track once captured, otherwise a plain hover moves the slider. + if (slider.classList.contains('drag')) fromPointer(e); + }); + // pointerup and pointercancel both end a drag; a lost capture would + // otherwise leave the track following the cursor forever. + ['pointerup', 'pointercancel', 'lostpointercapture'].forEach(function (ev) { + slider.addEventListener(ev, function () { slider.classList.remove('drag'); }); + }); + + var STEP = { ArrowLeft: -0.05, ArrowDown: -0.05, ArrowRight: 0.05, ArrowUp: 0.05, + PageDown: -0.2, PageUp: 0.2 }; + slider.addEventListener('keydown', function (e) { + var d = STEP[e.key]; + if (d === undefined && e.key !== 'Home' && e.key !== 'End') return; + e.preventDefault(); + level = e.key === 'Home' ? 0 : e.key === 'End' ? 1 + : Math.min(1, Math.max(0, level + d)); + paintBright(); describeBright(); + }); + + paintBright(); + } } /* ---- Tools tab ---- */ From e32412dfaa5bc4fd8970320d9f2d835e0325a15e Mon Sep 17 00:00:00 2001 From: Ishan Malu Date: Fri, 4 Sep 2026 21:36:40 +0300 Subject: [PATCH 2/2] Make the System panel show what the app shows, and add the Sound tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The demo panel had drifted from the app it claims to be a picture of. Sound was missing entirely — the headline feature of 1.9.0 was nowhere on the homepage. It is now the sixth tab, with no trace drawn, because volume is a setting rather than a measurement and the app omits the graph there too. The capability grid says plainly why there are no per-app sliders. GPU showed a ranked list of processes. Perch has no per-process GPU breakdown and never has; gpuDetail is the renderer and its thermal state. The page's own caption already said so, so the widget was contradicting the words beside it. Disk and network had the same problem in a milder form: both are value rows in the app, not bars, and network is two right-aligned columns. Only CPU and memory rank anything, so only they keep bars. Memory was tinted purple against .green in SystemMetric.tint. Also in this pass: - The panel footer read 1.8.0 in three places, through two releases. The release workflow now refuses to publish if the homepage does not name the version being tagged. - Switching metric kept the previous one's headline formatter until the next 1.5s text tick, so memory briefly rendered as a percentage. - Reduced motion drew no trace at all: the path only ever got a `d` from the animation frame, so the graph was blank for anyone who asked for less movement. - The metric tabs were divs with a click handler, unreachable by keyboard. - The Privacy section never closed its .wrap. - "4 MB" was neither the 2.5 MB download nor the 7 MB install. - `cd perch` after cloning Perch fails on a case-sensitive volume. - Privacy said two network features; downloading an update is a third. - Dropped two screenshots byte-identical to the pair the README uses. --- .github/workflows/release.yml | 11 + docs/index.html | 311 ++++++++++++++++++-------- docs/shots/panel-system-cpu-dark.png | Bin 324859 -> 0 bytes docs/shots/panel-system-cpu-light.png | Bin 312639 -> 0 bytes 4 files changed, 234 insertions(+), 88 deletions(-) delete mode 100644 docs/shots/panel-system-cpu-dark.png delete mode 100644 docs/shots/panel-system-cpu-light.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d0b561..f703437 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,17 @@ jobs: - name: Derive version id: version run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + # The homepage draws the panel footer, version string included, and it + # sat on 1.8.0 through two releases because nothing checked. Fail here + # rather than publish a site that misreports what is current. + - name: Homepage states this version + run: | + want="${GITHUB_REF_NAME#v}" + if ! grep -q "Perch $want — up to date" docs/index.html; then + echo "docs/index.html does not mention $want:" >&2 + grep -o 'Perch [0-9][^<]*— up to date' docs/index.html | sort -u >&2 + exit 1 + fi # Imported before the build so Scripts/build-app.sh finds the identity # in the keychain and signs with it instead of falling back to ad-hoc. # A stable identity is what stops macOS dropping the Accessibility grant diff --git a/docs/index.html b/docs/index.html index d82f6a3..1490057 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Perch — a dozen Mac utilities in one place - + @@ -230,7 +230,7 @@ .live-tabs { display: flex; gap: 2px; padding: 9px 9px 4px; } .live-tab { flex: 1; text-align: center; padding: 5px 0; border-radius: 7px; - font-size: 10.5px; font-weight: 600; color: var(--muted); + font-size: 9.5px; font-weight: 600; color: var(--muted); transition: background .3s ease, color .3s ease; } .live-tab.on { background: var(--mtint); color: #fff; } @@ -278,6 +278,27 @@ font-size: 10.5px; color: var(--muted); width: 54px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; } + /* Value rows (GPU, disk), interface columns (network) and the audible list + (sound). Only CPU and memory rank things with bars, so only they get one. */ + .live-row .val { + font-size: 10.5px; color: var(--muted); margin-left: auto; + font-variant-numeric: tabular-nums; white-space: nowrap; + } + .live-row .val.good { color: var(--free-fg); } + .live-row .col { + font-size: 9.5px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + color: var(--muted); width: 54px; text-align: right; + font-variant-numeric: tabular-nums; flex: none; + } + .live-row .col.up { color: var(--faint); } + .live-row .dot { color: var(--mtint); font-size: 9px; flex: none; } + .live-colhead { justify-content: flex-end; font-size: 8.5px; color: var(--faint); } + .live-colhead span { width: 54px; text-align: right; } + /* Volume is a setting, not a measurement, so the Sound tab has no trace — + the same reason the app leaves the graph off that tab. */ + .live.no-spark .live-spark { display: none; } + .live.no-spark .live-gauge { padding-bottom: 11px; } + .live-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } @media (max-width: 900px) { .live { width: 100%; max-width: 320px; } } @@ -613,6 +634,7 @@ + @@ -708,7 +730,7 @@

A dozen Mac utilities. Meet Perch.

macOS 14+ Apple silicon & Intel No telemetry - 4 MB + 2.5 MB download

@@ -774,7 +796,7 @@

Put windows where you want them

- Perch 1.8.0 — up to date + Perch 1.9.1 — up to date Check
@@ -787,9 +809,11 @@

Put windows where you want them

02 — SYSTEM

Know what your Mac is doing

Click CPU and you get per-core load and the processes driving it, - helpers grouped under the app that owns them. Click GPU, memory, disk or - network and you get the same treatment for each. End a runaway process - without opening Activity Monitor, and pin any figure next to the clock.

+ helpers grouped under the app that owns them. GPU, memory, disk, network + and sound each get the treatment that suits them — a ranked list where + ranking means something, plain figures where it does not. End a runaway + process without opening Activity Monitor, and pin any figure next to the + clock.

⌃⌥Space
@@ -820,11 +844,11 @@

Know what your Mac is doing

stroke-linejoin="round" vector-effect="non-scaling-stroke"/>
-

PER CORE

+

BY CPU

- Perch 1.8.0 — up to date + Perch 1.9.1 — up to date Check
@@ -866,7 +890,7 @@

The small stuff, one click away

- Perch 1.8.0 — up to date + Perch 1.9.1 — up to date Check
@@ -886,7 +910,7 @@

Everything in the box

Custom layoutsFive presets that tile your frontmost windows at once.
Alt-Tab switcherLive window previews, type to filter, close or quit inline.
Clipboard historySearchable, pinnable, with image previews and source apps.
-
System monitoringCPU, memory, disk, network, battery health, thermals.
+
System monitoringCPU, GPU, memory, disk, network, battery health, thermals.
Disk cleaningSixteen cache targets plus your own. Always to the Trash.
Screen cleaningFlat colours that double as a dead-pixel test.
Keyboard cleaningInput locked on a countdown so you can wipe the keys.
@@ -895,6 +919,7 @@

Everything in the box

Updates install themselvesPerch replaces itself and restarts. The new build must be signed by the same key as the running one, or it is refused before anything moves.
Night mode6500K to 2400K via display gamma. Manual or scheduled.
BrightnessPer display, including external monitors over any cable.
+
SoundOutput level and mute, switch between outputs, and see which apps are actually making noise. No per-app sliders: macOS exposes no volume on a process, and the only way round it is shipping an audio driver.
Keep awakeHold it open, or for 15 minutes to 8 hours with the time left on the row. It can start itself while plugged in, while an app runs, or while the CPU is busy, and let the display sleep while the machine works.
@@ -1020,7 +1045,7 @@

Press ⌃⌥Space

Needs Swift 6 and the macOS SDK. Command Line Tools are enough — full Xcode is not required.

git clone https://github.com/ishanmalu/Perch.git
-cd perch
+cd Perch
 Scripts/install.sh

Builds, runs the self-test, installs to /Applications and launches it. @@ -1037,7 +1062,7 @@

Privacy

Keystrokes are counted, never recordedThe cleaning modes discard every event they swallow. Key codes are never stored or logged.
Clipboard history stays localOwner-only permissions. Password managers and credential-shaped strings are skipped.
-
Two network features, both manualThe update check talks to GitHub. The speed test fetches a block of bytes from Cloudflare when you press Run. Nothing else opens a socket.
+
Every network call is one you asked forChecking for an update, and downloading it, talk to GitHub. The speed test fetches a block of bytes from Cloudflare when you press Run. Nothing else opens a socket.
Disk cleaning cannot go rogueA path guard rejects system and home directories. Removal is always to the Trash.
Accessibility reads geometry, not contentWindow positions, sizes, titles and app names. Never what is inside a window.
Window previews are optionalCaptured only while the switcher is open, held in memory, never written to disk.
@@ -1047,7 +1072,8 @@

Privacy

Every release ships a checksum, and you can run the built-in checks yourself with Perch --selftest.

- +
+