diff --git a/CHANGELOG.md b/CHANGELOG.md index 88eead9..9ba028c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to Perch. Versions follow [semver](https://semver.org), and each heading below corresponds to a published [release](https://github.com/ishanmalu/Perch/releases). +## [1.9.2] — 2026-09-04 + +### Added +- **Brightness on a shortcut.** `⌃⌥=` and `⌃⌥-` move every display a + sixteenth at a time — the same step the Mac's own brightness keys use, but + those only ever reach the built-in panel. External monitors move too: where + there is no backlight to control, Perch dims with an overlay. A brief readout + shows the new level, and both shortcuts are rebindable in Settings. + + The code behind this had been sitting in the app unreachable since brightness + control was added. `nudgeAll()` was written to be driven by a shortcut, the + shortcut was never registered, and nothing called it. It is wired up now + rather than deleted. + +### Fixed +- Stepping down can no longer land on zero. A display taken to full black has + no visible slider to bring it back, so the floor is one step above it, and a + display already at zero comes back up rather than sticking. + ## [1.9.1] — 2026-09-04 Homepage only — the app binary is unchanged from 1.9.0. It is tagged as a diff --git a/Sources/Perch/AppDelegate.swift b/Sources/Perch/AppDelegate.swift index 0eaafd3..99c8dc8 100644 --- a/Sources/Perch/AppDelegate.swift +++ b/Sources/Perch/AppDelegate.swift @@ -240,5 +240,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { hk.register("win.restore") { WindowManager.shared.restore() } hk.register("win.nextScreen") { WindowManager.shared.moveToScreen(next: true) } hk.register("win.prevScreen") { WindowManager.shared.moveToScreen(next: false) } + let step = BrightnessController.stepSize + hk.register("brightness.up") { BrightnessController.shared.nudgeAll(by: step) } + hk.register("brightness.down") { BrightnessController.shared.nudgeAll(by: -step) } } } diff --git a/Sources/Perch/Core/Hotkeys.swift b/Sources/Perch/Core/Hotkeys.swift index d214570..9659cbf 100644 --- a/Sources/Perch/Core/Hotkeys.swift +++ b/Sources/Perch/Core/Hotkeys.swift @@ -56,6 +56,11 @@ struct HotkeySpec: Codable, Equatable { "win.thirdL": .init(keyCode: UInt32(kVK_ANSI_D), modifiers: NSEvent.ModifierFlags([.control, .option]).rawValue), "win.thirdC": .init(keyCode: UInt32(kVK_ANSI_F), modifiers: NSEvent.ModifierFlags([.control, .option]).rawValue), "win.thirdR": .init(keyCode: UInt32(kVK_ANSI_G), modifiers: NSEvent.ModifierFlags([.control, .option]).rawValue), + // The Mac's own brightness keys only reach the built-in panel. These + // move every display, external ones included, which is the whole point + // of having them. + "brightness.up": .init(keyCode: UInt32(kVK_ANSI_Equal), modifiers: NSEvent.ModifierFlags([.control, .option]).rawValue), + "brightness.down": .init(keyCode: UInt32(kVK_ANSI_Minus), modifiers: NSEvent.ModifierFlags([.control, .option]).rawValue), ] } diff --git a/Sources/Perch/Screen/Brightness.swift b/Sources/Perch/Screen/Brightness.swift index 35f2fd0..9ab283a 100644 --- a/Sources/Perch/Screen/Brightness.swift +++ b/Sources/Perch/Screen/Brightness.swift @@ -78,8 +78,22 @@ final class BrightnessController: ObservableObject { for d in displays { setLevel(value, for: d.id) } } + /// One press of a brightness key. macOS moves in sixteenths, so matching + /// that keeps these keys feeling like the ones next to them. + static let stepSize = 1.0 / 16 + + /// Extracted so the clamping can be checked without a display attached. + /// Landing exactly on 0 would black a screen out with no way back, so the + /// floor is one step up from it. + static func stepped(_ level: Double, by delta: Double) -> Double { + min(1, max(stepSize, level + delta)) + } + func nudgeAll(by delta: Double) { - for d in displays { setLevel(level(for: d.id) + delta, for: d.id) } + guard !displays.isEmpty else { return } + for d in displays { + setLevel(Self.stepped(level(for: d.id), by: delta), for: d.id) + } Notifier.show("Brightness \(Int(level(for: displays.first?.id ?? 0) * 100))%", duration: 1) } diff --git a/Sources/Perch/SelfTest.swift b/Sources/Perch/SelfTest.swift index db54843..cc04c92 100644 --- a/Sources/Perch/SelfTest.swift +++ b/Sources/Perch/SelfTest.swift @@ -137,6 +137,17 @@ enum SelfTest { expect(!spec.cocoaFlags.isEmpty, "\(name) requires a modifier") } + print("\nBrightness steps stay in range") + let step = BrightnessController.stepSize + expect(BrightnessController.stepped(0.5, by: step) == 0.5625, + "one press moves a sixteenth") + expect(BrightnessController.stepped(1.0, by: step) == 1.0, + "stepping up from full stays at full") + expect(BrightnessController.stepped(0.05, by: -step) == step, + "stepping down floors at one step, never black") + expect(BrightnessController.stepped(0.0, by: -step) == step, + "a display already at zero comes back rather than sticking") + // OCR runs over whatever was captured, so it can be checked without // the screen: render known text, read it back. let sample = NSImage(size: NSSize(width: 640, height: 90)) diff --git a/Sources/Perch/UI/SettingsWindow.swift b/Sources/Perch/UI/SettingsWindow.swift index d471e88..ef6f8c0 100644 --- a/Sources/Perch/UI/SettingsWindow.swift +++ b/Sources/Perch/UI/SettingsWindow.swift @@ -517,6 +517,8 @@ private struct ShortcutSettings: View { ("win.bottomLeft", "Bottom Left Quarter"), ("win.bottomRight", "Bottom Right Quarter"), ("nightMode", "Night mode"), + ("brightness.up", "Brightness up"), + ("brightness.down", "Brightness down"), ("screenClean", "Screen cleaning"), ("keyboardClean", "Keyboard cleaning"), ("trackpadClean", "Trackpad cleaning"), @@ -568,6 +570,8 @@ private struct ShortcutSettings: View { case "screenshot": return "camera.viewfinder" case "switcher.altTab": return "arrow.left.arrow.right" case "nightMode": return "moon.fill" + case "brightness.up": return "sun.max.fill" + case "brightness.down": return "sun.min" default: return "macwindow" } } @@ -584,6 +588,7 @@ private struct ShortcutSettings: View { case "screenshot": return .pink case "switcher.altTab": return .blue case "nightMode": return .orange + case "brightness.up", "brightness.down": return .yellow default: return .gray } } diff --git a/docs/index.html b/docs/index.html index 1490057..77a8bab 100644 --- a/docs/index.html +++ b/docs/index.html @@ -796,7 +796,7 @@