Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menuband: 0.7 — sharp Riso title shadow + light-mode LED visualizer

Voice title swaps to max-contrast text (white-on-dark / black-on-light)
with a hard 1-pixel family-color drop shadow — Risograph-style
misregister, no soft halo. The previous double-shadow (view-level blur
+ attributed-string shadow) read as fuzzy and dark in both modes.

LED visualizer gains a light mode: warm off-white substrate, hot zone
darkens at peak (instead of brightening to white), unlit segments fade
to the substrate so they don't read as always-on dots. Bezel substrate
also flips so the bars read as recessed.

Also: dev affordance — distributed notification handler that opens the
popover from the shell. Mirror infra: snapshot-mirror slab/menuband/
to github.com/whistlegraph/menuband, hooked into lith/deploy.fish so
the mirror's release pace tracks aesthetic.computer/menuband.

Landing page download link bumped to 0.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+299 -25
+12
lith/deploy.fish
··· 262 262 ssh -i $SSH_KEY $LITH_USER@$TARGET_HOST "systemctl restart lith" 263 263 264 264 echo -e "$GREEN-> Done. lith deployed to $TARGET_HOST$NC" 265 + 266 + # Mirror slab/menuband/ to its standalone GitHub repo. Runs after a 267 + # successful site deploy so the mirror's release pace matches what's 268 + # actually live on aesthetic.computer/menuband. Failure here doesn't 269 + # void the deploy — the mirror is a courtesy surface for external 270 + # contributors, not a critical path. 271 + set MIRROR_SYNC "$REPO_ROOT/slab/menuband/bin/mirror-sync.sh" 272 + if test -x "$MIRROR_SYNC" 273 + echo -e "$GREEN-> Syncing menuband mirror...$NC" 274 + bash "$MIRROR_SYNC" 2>&1 | tail -3 275 + or echo -e "$YELLOW menuband mirror sync failed (non-fatal)$NC" 276 + end
+2 -2
slab/menuband/Info.plist
··· 17 17 <key>CFBundlePackageType</key> 18 18 <string>APPL</string> 19 19 <key>CFBundleShortVersionString</key> 20 - <string>0.6</string> 20 + <string>0.7</string> 21 21 <key>CFBundleVersion</key> 22 - <string>6</string> 22 + <string>7</string> 23 23 <key>LSMinimumSystemVersion</key> 24 24 <string>11.0</string> 25 25 <key>LSUIElement</key>
+52
slab/menuband/MIRROR_README.md
··· 1 + # Menu Band 2 + 3 + A tiny piano in your macOS menu bar. Click keys with the mouse, or type letters 4 + to play. Sends notes out a virtual MIDI port for your DAW, and ships with a 5 + built-in General MIDI synth so it makes sound on its own. 6 + 7 + Free and open source. Part of [aesthetic.computer](https://aesthetic.computer). 8 + 9 + - **Download (notarized DMG):** <https://aesthetic.computer/menuband> 10 + - **Landing page + changelog:** <https://aesthetic.computer/menuband> 11 + - **Issue tracker / pull requests:** here on GitHub 12 + 13 + ## Building from source 14 + 15 + ```sh 16 + swift build -c release 17 + ./install.sh # builds, signs, installs ~/Applications/Menu Band.app, 18 + # and loads the launchd agent so it starts on login 19 + ``` 20 + 21 + `install.sh` will use a Developer ID Application certificate from your keychain 22 + if one is available, or generate a self-signed identity for local testing. 23 + 24 + ## Mirror notice 25 + 26 + This repository is a **read-only mirror** of `slab/menuband/` from the 27 + [aesthetic.computer monorepo](https://tangled.org/@aesthetic.computer/core). 28 + The monorepo is canonical; this mirror is force-pushed by a `git subtree split` 29 + hook every time the upstream Menu Band code changes. 30 + 31 + Force-push only affects `main`. Your forks and feature branches are not 32 + disturbed by upstream sync. 33 + 34 + ### Submitting changes 35 + 36 + Forks + pull requests work normally on GitHub. When the maintainer accepts 37 + your PR, the changes get applied back to the monorepo via `git format-patch` 38 + + `git am`, preserving your authorship in the commit log. The next mirror 39 + sync after that lands those commits here too — so your hash on the mirror 40 + will eventually match a hash in the upstream monorepo's history. 41 + 42 + ## Acknowledgements 43 + 44 + Performance improvements in 0.5 (visualizer pause-when-hidden, Metal vsync 45 + disable) were contributed by [Esteban Uribe](https://github.com/estebanuribe). 46 + Thanks Esteban! 47 + 48 + ## License 49 + 50 + The code in this repository is part of aesthetic.computer. See the 51 + [upstream LICENSE](https://tangled.org/@aesthetic.computer/core/blob/main/LICENSE) 52 + for terms.
+25
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 164 164 hotkey.register(keyCode: UInt32(kVK_ANSI_P), modifiers: modMask) 165 165 typeModeHotkey = hotkey 166 166 167 + // Dev affordance: post the 168 + // `computer.aestheticcomputer.menuband.showPopover` 169 + // distributed notification to toggle the popover from the 170 + // shell. Used during iteration to flash the popover open 171 + // after rebuild without hunting for the menubar item. 172 + DistributedNotificationCenter.default().addObserver( 173 + self, 174 + selector: #selector(handleShowPopoverNotification(_:)), 175 + name: NSNotification.Name("computer.aestheticcomputer.menuband.showPopover"), 176 + object: nil 177 + ) 178 + 167 179 // Local key capture wiring. Routes keys to the same note logic the 168 180 // global tap uses, with the ghost-label flash on every press so the 169 181 // user sees the layout dynamically appear while typing. ··· 717 729 } 718 730 if let m = clickAwayMonitor { NSEvent.removeMonitor(m); clickAwayMonitor = nil } 719 731 if let m = popoverEscMonitor { NSEvent.removeMonitor(m); popoverEscMonitor = nil } 732 + } 733 + 734 + /// Distributed-notification entry point for the dev affordance 735 + /// registered in `applicationDidFinishLaunching`. Always *opens* 736 + /// the popover (never closes) — repeated triggers from a shell 737 + /// rebuild loop should leave it visible, not flicker shut. 738 + @objc private func handleShowPopoverNotification(_ note: Notification) { 739 + DispatchQueue.main.async { [weak self] in 740 + guard let self = self else { return } 741 + if !self.popover.isShown { 742 + self.showPopover() 743 + } 744 + } 720 745 } 721 746 722 747 private func showPopover() {
+39 -14
slab/menuband/Sources/MenuBand/MenuBandPopover.swift
··· 344 344 waveformBezel = NSView() 345 345 waveformBezel.wantsLayer = true 346 346 waveformBezel.layer?.cornerRadius = 6 347 + // Bezel substrate color is set per-appearance in 348 + // `applyAppearanceToVisualizer` (called from syncFromController); 349 + // start dark so first-paint before sync isn't a flash. 347 350 waveformBezel.layer?.backgroundColor = NSColor(white: 0.06, alpha: 1.0).cgColor 348 351 waveformBezel.layer?.borderWidth = 1 349 352 // Border color is set in `updateInstrumentReadout` so the ··· 423 426 instrumentReadout.wantsLayer = true 424 427 instrumentReadout.lineBreakMode = .byTruncatingTail 425 428 instrumentReadout.alignment = .center 426 - let titleShadow = NSShadow() 427 - titleShadow.shadowColor = NSColor.black.withAlphaComponent(0.55) 428 - titleShadow.shadowBlurRadius = 3 429 - titleShadow.shadowOffset = NSSize(width: 0, height: -1) 430 - instrumentReadout.shadow = titleShadow 429 + // No view-level shadow — the per-glyph shadow lives in the 430 + // attributed string (set in updateInstrumentReadout) so it 431 + // stays crisp. A view shadow on top of that double-shadows 432 + // the text and looks like a soft halo. 431 433 // Center the chip in its row by flanking it with greedy 432 434 // spacers — without these, .fill distribution lets the 433 435 // (now-shrunken) text hug the leading edge. ··· 831 833 btn.state = (i == segIdx) ? .on : .off 832 834 } 833 835 instrumentList.selectedProgram = n.melodicProgram 836 + applyAppearanceToVisualizer() 834 837 updateInstrumentReadout() 835 838 // Keep the QWERTY layout's keymap + tint synced with the 836 839 // controller. Voice color picks up the family hue for the ··· 899 902 // always has presence. 900 903 let isDark = view.effectiveAppearance.bestMatch( 901 904 from: [.aqua, .darkAqua]) == .darkAqua 902 - let textColor: NSColor = isDark 903 - ? (famColor.highlight(withLevel: 0.35) ?? famColor) 904 - : (famColor.shadow(withLevel: 0.25) ?? famColor) 905 - // Soft drop shadow opposite the system appearance so the 906 - // glyph anchors on the bg without a chip backdrop. 905 + // Flip the foreground/shadow relationship: max-contrast text 906 + // (white in dark, black in light) with the family color as a 907 + // hard 1-px shadow. Reads like a Risograph misregister — the 908 + // hue still keys the voice but the title stays legible. 909 + let textColor: NSColor = isDark ? .white : .black 907 910 let shadow = NSShadow() 908 - shadow.shadowColor = (isDark ? NSColor.black : NSColor.white) 909 - .withAlphaComponent(0.55) 910 - shadow.shadowOffset = NSSize(width: 0, height: -1) 911 - shadow.shadowBlurRadius = 2 911 + // Light-tinted family color in both modes — a pastel offset 912 + // that reads as a soft Riso-style misregister rather than a 913 + // dark drop shadow weighing the title down. 914 + shadow.shadowColor = (famColor.highlight(withLevel: isDark ? 0.25 : 0.55) 915 + ?? famColor) 916 + shadow.shadowOffset = NSSize(width: 1, height: -1) 917 + shadow.shadowBlurRadius = 0 912 918 // Try YWFT Processing first (bundled in Resources/), fall 913 919 // back through the Processing-IDE family, then the system 914 920 // black-weight as a last resort. ··· 943 949 // open via syncFromController — viewDidChangeEffectiveAppearance 944 950 // isn't on NSViewController in macOS so we don't try to hook it 945 951 // mid-session. 952 + 953 + /// Flip the LED bezel + visualizer between dark-mode (LED-on-black 954 + /// glow) and light-mode (ink-on-paper) substrates so the meter 955 + /// doesn't look like a black slab pasted onto a white popover. 956 + private func applyAppearanceToVisualizer() { 957 + let isDark = view.effectiveAppearance.bestMatch( 958 + from: [.aqua, .darkAqua]) == .darkAqua 959 + waveformView.setLightMode(!isDark) 960 + if isDark { 961 + waveformBezel?.layer?.backgroundColor = 962 + NSColor(white: 0.06, alpha: 1.0).cgColor 963 + } else { 964 + // Slightly darker than the visualizer's own clear color so 965 + // the inset bars read as recessed into the bezel — same 966 + // recessed-housing effect as the dark mode 0.06 → 0.0 step. 967 + waveformBezel?.layer?.backgroundColor = 968 + NSColor(white: 0.82, alpha: 1.0).cgColor 969 + } 970 + } 946 971 947 972 /// Reflect the MIDI loopback self-test status as the inline "MIDI" 948 973 /// label's color. No textual chrome — the color is the indicator.
+41 -7
slab/menuband/Sources/MenuBand/WaveformView.swift
··· 230 230 1.0) 231 231 } 232 232 233 + /// Switch the meter substrate + lit-bar tonality between the 234 + /// glowing-on-black look (dark mode) and an ink-on-paper look 235 + /// (light mode). In light mode the clear color flips to a warm 236 + /// off-white and the shader's hot-zone mix darkens toward black 237 + /// instead of brightening to white, so peak still reads as 238 + /// "hotter" without washing out against the light substrate. 239 + func setLightMode(_ isLight: Bool) { 240 + if isLight { 241 + // Warm off-white — closer to a printed page than pure 242 + // white, so the colored bars don't vibrate against it. 243 + clearColor = MTLClearColor(red: 0.93, green: 0.92, blue: 0.90, alpha: 1.0) 244 + uniforms.isLight = 1 245 + } else { 246 + clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1.0) 247 + uniforms.isLight = 0 248 + } 249 + display() 250 + } 251 + 233 252 // MARK: - Per-frame audio analysis 234 253 235 254 private func updateLevels() { ··· 301 320 float minHeight; 302 321 float4 color; 303 322 float dotMatrix; 323 + float isLight; 304 324 }; 305 325 306 326 struct VertexOut { ··· 366 386 discard_fragment(); 367 387 } 368 388 // Bar color = the instrument's chosen base hue, passed in via 369 - // u.color. The top of the bar still brightens toward white so 370 - // there's a "peaking" cue even when the base is dim — VU 371 - // gradient feel without forcing green/amber/red. 389 + // u.color. In dark mode the top brightens toward white (LED 390 + // glow); in light mode it darkens toward black (ink saturation 391 + // at peak) — both read as "this bar is hotter at the top" 392 + // against their respective substrates. 372 393 float3 base = u.color.rgb; 373 394 float hot = max(0.0, (y01 - HOT_AT) / (1.0 - HOT_AT)); 374 - float3 tier = mix(base, float3(1.0, 1.0, 1.0), hot * 0.65); 395 + float3 hotTarget = (u.isLight > 0.5) ? float3(0.0, 0.0, 0.0) : float3(1.0, 1.0, 1.0); 396 + float3 tier = mix(base, hotTarget, hot * 0.65); 375 397 // Per-segment glow: brighter at the center of each LED cell, 376 398 // falling off toward the gap edges. Reads as a soft bloom on 377 399 // each lit segment without a real blur pass. ··· 387 409 bool maskLit = (u.dotMatrix > 0.5) && (((mask >> segIndex) & 1u) != 0u); 388 410 bool levelLit = y01 < in.level; 389 411 bool lit = maskLit || levelLit; 390 - float3 color = lit ? (tier + bloom * 0.40) : tier; 391 - float a = lit ? u.color.a : (u.color.a * UNLIT_ALPHA); 392 - return float4(min(color, 1.0), a); 412 + // Bloom direction also flips with the substrate so the 413 + // per-segment glow reinforces "hotter" instead of fighting it. 414 + float bloomSign = (u.isLight > 0.5) ? -1.0 : 1.0; 415 + float3 color = lit ? (tier + bloomSign * bloom * 0.40) : tier; 416 + // In light mode unlit segments fade toward the substrate (warm 417 + // off-white) instead of toward black — without this the 418 + // "off" rows show as faint colored dots, which reads as a row 419 + // of always-on LEDs rather than empty headroom. 420 + float unlitAlpha = (u.isLight > 0.5) ? 0.20 : UNLIT_ALPHA; 421 + float a = lit ? u.color.a : (u.color.a * unlitAlpha); 422 + return float4(clamp(color, float3(0.0), float3(1.0)), a); 393 423 } 394 424 """ 395 425 } ··· 405 435 /// (see `dotMasks`) instead of the continuous-level VU. Used in 406 436 /// MIDI mode to spell "MIDI" out of the LED segments. 407 437 var dotMatrix: Float = 0 438 + /// Set to 1 when the popover is in light appearance — flips the 439 + /// hot-zone mix target from white to black and the unlit fade 440 + /// target from black to the warm off-white substrate. 441 + var isLight: Float = 0 408 442 } 409 443 410 444 extension WaveformView: MTKViewDelegate {
+126
slab/menuband/bin/mirror-sync.sh
··· 1 + #!/usr/bin/env bash 2 + # mirror-sync.sh — Snapshot-mirror slab/menuband/ to its standalone 3 + # GitHub repo at github.com/whistlegraph/menuband. 4 + # 5 + # Why snapshot, not subtree split: 6 + # `git subtree split` walks every commit in the entire monorepo 7 + # history (50k+ commits, 1.3 GB pack) to figure out which ones 8 + # touched the prefix — takes 10+ minutes per sync. The mirror 9 + # doesn't actually need that history; contributors care about 10 + # "what's the current source code, and how do I PR a change." 11 + # So this script just snapshots the current tree as a single new 12 + # commit titled "Mirror of <mono-hash> — <mono-subject>". The 13 + # mirror's git log becomes the release log instead of a granular 14 + # per-file history. Authorship is preserved from the upstream 15 + # commit so contributors who land changes in the mono get the 16 + # commit credit on the mirror too. 17 + # 18 + # Idempotent. Safe to call repeatedly: if the snapshot tree matches 19 + # the mirror's current tip, no commit is created. 20 + # 21 + # Auth: uses HTTPS via the `gh` CLI's credential helper. Run once: 22 + # `gh auth setup-git` to install the helper if not already done. 23 + 24 + set -euo pipefail 25 + 26 + CYAN=$'\033[1;36m' 27 + GREEN=$'\033[1;32m' 28 + YELLOW=$'\033[1;33m' 29 + RED=$'\033[1;31m' 30 + RESET=$'\033[0m' 31 + say() { printf "%s• %s%s\n" "$CYAN" "$1" "$RESET"; } 32 + ok() { printf "%s✓ %s%s\n" "$GREEN" "$1" "$RESET"; } 33 + warn() { printf "%s! %s%s\n" "$YELLOW" "$1" "$RESET"; } 34 + err() { printf "%s✗ %s%s\n" "$RED" "$1" "$RESET" 1>&2; } 35 + 36 + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" 37 + if [[ -z "${REPO_ROOT}" ]]; then 38 + err "not inside a git repository" 39 + exit 1 40 + fi 41 + cd "${REPO_ROOT}" 42 + 43 + PREFIX="slab/menuband" 44 + MIRROR_URL="https://github.com/whistlegraph/menuband.git" 45 + MIRROR_BRANCH="main" 46 + 47 + if [[ ! -d "${PREFIX}" ]]; then 48 + err "${PREFIX}/ not found in ${REPO_ROOT}" 49 + exit 1 50 + fi 51 + 52 + # Identify the most recent mono commit that touched the prefix — 53 + # its hash + subject become the mirror's snapshot label, and its 54 + # author lands on the mirror commit so contributors see their 55 + # attribution preserved. 56 + MONO_HASH="$(git log -1 --format=%H -- "${PREFIX}")" 57 + MONO_SHORT="${MONO_HASH:0:7}" 58 + MONO_SUBJECT="$(git log -1 --format=%s -- "${PREFIX}")" 59 + MONO_AUTHOR="$(git log -1 --format='%aN <%aE>' -- "${PREFIX}")" 60 + MONO_DATE="$(git log -1 --format='%aI' -- "${PREFIX}")" 61 + 62 + # Working dir: clone the mirror if it has any commits, otherwise 63 + # init from scratch and set the remote up. Using HTTPS so gh's 64 + # credential helper can supply the token transparently. 65 + WORK="$(mktemp -d)" 66 + trap "rm -rf ${WORK}" EXIT 67 + 68 + if git ls-remote --heads "${MIRROR_URL}" "${MIRROR_BRANCH}" 2>/dev/null | grep -q "refs/heads/${MIRROR_BRANCH}"; then 69 + say "cloning mirror" 70 + git clone --depth=1 --branch="${MIRROR_BRANCH}" "${MIRROR_URL}" "${WORK}" >/dev/null 2>&1 71 + else 72 + say "initialising empty mirror" 73 + git init -q -b "${MIRROR_BRANCH}" "${WORK}" 74 + git -C "${WORK}" remote add origin "${MIRROR_URL}" 75 + fi 76 + 77 + # Replace the mirror's working tree with the current snapshot of 78 + # slab/menuband/, preserving the .git directory. `find -delete` in 79 + # two passes (files first, then empty dirs) handles deeply nested 80 + # layouts without rmdir errors. 81 + say "snapshotting ${PREFIX} → mirror tree" 82 + find "${WORK}" -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + 83 + # `cp -R prefix/.` copies *contents* of prefix into WORK without 84 + # nesting prefix as a subdir. The trailing `/.` is critical. 85 + cp -R "${REPO_ROOT}/${PREFIX}/." "${WORK}/" 86 + 87 + # Strip build artifacts, vendor caches, and signed/notarized 88 + # release DMGs that have no business in source. The mirror is 89 + # code only; release binaries live on assets.aesthetic.computer 90 + # and the GitHub Releases page. 91 + rm -rf "${WORK}/.build" 92 + rm -f "${WORK}"/Menu-Band-*.dmg "${WORK}"/Menu-Band-*.zip 93 + find "${WORK}" -name '.DS_Store' -delete 94 + 95 + # Drop in / refresh the README so contributors see the mirror 96 + # notice and contribution flow. The README lives in the mono so 97 + # it's editable from the canonical side. 98 + README_SRC="${REPO_ROOT}/${PREFIX}/MIRROR_README.md" 99 + if [[ -f "${README_SRC}" ]]; then 100 + cp "${README_SRC}" "${WORK}/README.md" 101 + fi 102 + 103 + cd "${WORK}" 104 + git add -A 105 + 106 + # No-op if the snapshot tree matches the previous commit's tree. 107 + if git diff --cached --quiet; then 108 + ok "mirror already up-to-date (${MONO_SHORT})" 109 + exit 0 110 + fi 111 + 112 + # Mirror commit message: human-readable header naming the upstream 113 + # hash, plus the body of the upstream commit subject. Preserves 114 + # upstream author so contributors who land changes via PR see their 115 + # attribution carry over to the mirror. 116 + git -c "user.name=${MONO_AUTHOR%% <*}" \ 117 + -c "user.email=$(printf '%s' "${MONO_AUTHOR}" | sed -nE 's/.*<([^>]+)>.*/\1/p')" \ 118 + commit -q \ 119 + --author="${MONO_AUTHOR}" \ 120 + --date="${MONO_DATE}" \ 121 + -m "Mirror of ${MONO_SHORT}: ${MONO_SUBJECT}" \ 122 + -m "Snapshot of slab/menuband/ from aesthetic.computer/core @ ${MONO_HASH}." >/dev/null 123 + 124 + say "pushing to ${MIRROR_URL}" 125 + git push -q origin "${MIRROR_BRANCH}" 126 + ok "mirror updated → github.com/whistlegraph/menuband (${MONO_SHORT})"
+2 -2
system/public/menuband/index.html
··· 571 571 <p class="tagline">Taking macOS' standard instruments out of the 🎸 Garage and kickin' it on the curb!</p> 572 572 573 573 <div class="button-row"> 574 - <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.6.dmg" download> 574 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.7.dmg" download> 575 575 Download 576 - <small>0.5 · Apple Silicon · 2.1 MB</small> 576 + <small>0.7 · Apple Silicon · 2.1 MB</small> 577 577 </a> 578 578 </div> 579 579 <p class="aux"><a href="https://tangled.org/@aesthetic.computer/core/tree/main/slab/menuband">view source</a> · by <a href="https://aesthetic.computer">aesthetic.computer</a></p>