diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a90a61..300c1ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Pullo are listed here, newest first. The format follows [ ## [Unreleased] +### Added +- You can turn off opening Pullo when the pointer reaches the notch, so it doesn't compete with other notch apps that open on hover. Click the notch or press your shortcut to open it instead. Thanks to [@ZUBERKHAN034](https://github.com/ZUBERKHAN034) for the idea. +- Clicking the notch opens Pullo. + ## [0.11.4] - 2026-10-25 ### Changed diff --git a/Sources/Pullo/App/AppDependencies.swift b/Sources/Pullo/App/AppDependencies.swift index 907ac36..110680d 100644 --- a/Sources/Pullo/App/AppDependencies.swift +++ b/Sources/Pullo/App/AppDependencies.swift @@ -68,6 +68,7 @@ final class AppDependencies { liveCI: LiveCIModel(prs: store, runs: runs, accounts: accounts, settings: settings, peeks: peeks), runs: runs, accounts: accounts, + settings: settings, system: system, geometry: geometry ?? settings.display.screen().map { NotchGeometry(screen: $0) } ?? .placeholder, clock: clock diff --git a/Sources/Pullo/App/NotchPanelController.swift b/Sources/Pullo/App/NotchPanelController.swift index 174af31..e3715a3 100644 --- a/Sources/Pullo/App/NotchPanelController.swift +++ b/Sources/Pullo/App/NotchPanelController.swift @@ -142,8 +142,14 @@ final class NotchPanelController { }) { monitors.append(global) } - if let local = NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp, handler: { [weak self] event in - MainActor.assumeIsolated { self?.pointerChanged() } + if let local = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseUp], handler: { [weak self, panel] event in + MainActor.assumeIsolated { + if event.type == .leftMouseDown { + if event.window === panel { self?.notch.clickedNotch() } + } else { + self?.pointerChanged() + } + } return event }) { monitors.append(local) diff --git a/Sources/Pullo/Features/Notch/NotchInteraction.swift b/Sources/Pullo/Features/Notch/NotchInteraction.swift index 7408166..550b8d7 100644 --- a/Sources/Pullo/Features/Notch/NotchInteraction.swift +++ b/Sources/Pullo/Features/Notch/NotchInteraction.swift @@ -21,6 +21,7 @@ final class NotchInteraction { private let liveCIModel: LiveCIModel private let runs: RunStore private let accounts: AccountStore + private let settings: AppSettings private let system: any SystemActions private let clock: any Clock @@ -40,6 +41,7 @@ final class NotchInteraction { liveCI: LiveCIModel, runs: RunStore, accounts: AccountStore, + settings: AppSettings, system: any SystemActions, geometry: NotchGeometry, clock: any Clock = ContinuousClock() @@ -50,6 +52,7 @@ final class NotchInteraction { liveCIModel = liveCI self.runs = runs self.accounts = accounts + self.settings = settings self.system = system self.geometry = geometry self.clock = clock @@ -79,7 +82,7 @@ final class NotchInteraction { case .peek: peeks.pointerMoved(over: inside) case .closed: - guard inside else { return cancelHover() } + guard inside, settings.opensOnHover else { return cancelHover() } scheduleHover(after: .milliseconds(80)) { notch in if notch.isPointerOver(notch.state) { notch.open() } } @@ -95,6 +98,10 @@ final class NotchInteraction { } } + func clickedNotch() { + if state == .closed { open() } + } + func mouseDown(at point: NSPoint) { if state == .open, !holdsOpenForSetup, !layout.hitRect(for: .open).contains(point) { close() } } diff --git a/Sources/Pullo/Features/Settings/AppSettings.swift b/Sources/Pullo/Features/Settings/AppSettings.swift index 1870149..f9d4ad5 100644 --- a/Sources/Pullo/Features/Settings/AppSettings.swift +++ b/Sources/Pullo/Features/Settings/AppSettings.swift @@ -17,6 +17,7 @@ final class AppSettings { var display: DisplayChoice { didSet { save() } } var shortcut: ShortcutChoice { didSet { save() } } var holdsAlertsInFullScreen: Bool { didSet { save() } } + var opensOnHover: Bool { didSet { save() } } var shortcutUnavailable = false private(set) var launchAtLogin: Bool private(set) var launchAtLoginFailure: String? @@ -41,6 +42,7 @@ final class AppSettings { display = (defaults.object(for: .display) as? String).flatMap(DisplayChoice.init(rawValue:)) ?? .builtIn shortcut = (defaults.object(for: .shortcut) as? String).flatMap(ShortcutChoice.init(rawValue:)) ?? .optionCommandP holdsAlertsInFullScreen = defaults.object(for: .holdInFullScreen) as? Bool ?? false + opensOnHover = defaults.object(for: .openOnHover) as? Bool ?? true launchAtLogin = SMAppService.mainApp.status == .enabled } @@ -77,6 +79,7 @@ final class AppSettings { display = (defaults.object(for: .display) as? String).flatMap(DisplayChoice.init(rawValue:)) ?? .builtIn shortcut = (defaults.object(for: .shortcut) as? String).flatMap(ShortcutChoice.init(rawValue:)) ?? .optionCommandP holdsAlertsInFullScreen = defaults.object(for: .holdInFullScreen) as? Bool ?? false + opensOnHover = defaults.object(for: .openOnHover) as? Bool ?? true launchAtLogin = SMAppService.mainApp.status == .enabled } @@ -92,5 +95,6 @@ final class AppSettings { defaults.set(display.rawValue, for: .display) defaults.set(shortcut.rawValue, for: .shortcut) defaults.set(holdsAlertsInFullScreen, for: .holdInFullScreen) + defaults.set(opensOnHover, for: .openOnHover) } } diff --git a/Sources/Pullo/Features/Settings/GeneralPage.swift b/Sources/Pullo/Features/Settings/GeneralPage.swift index 0685187..b537a89 100644 --- a/Sources/Pullo/Features/Settings/GeneralPage.swift +++ b/Sources/Pullo/Features/Settings/GeneralPage.swift @@ -17,6 +17,11 @@ struct GeneralPage: View { "Built-in display uses the notch when there is one. Display with the pointer moves Pullo to the screen you're working on." ) } + Section { + Toggle("Open when the pointer reaches the notch", isOn: $settings.opensOnHover) + } footer: { + Text("Turn this off if another notch app opens on hover too. Click the notch or press your shortcut to open Pullo.") + } Section { Toggle("Open at login", isOn: Binding( get: { settings.launchAtLogin }, diff --git a/Sources/Pullo/Resources/Localizable.xcstrings b/Sources/Pullo/Resources/Localizable.xcstrings index 3cd69ee..412038a 100644 --- a/Sources/Pullo/Resources/Localizable.xcstrings +++ b/Sources/Pullo/Resources/Localizable.xcstrings @@ -5797,6 +5797,38 @@ } } } + }, + "Open when the pointer reaches the notch": { + "localizations": { + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir cuando el puntero llega al notch" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir quando o ponteiro chega no notch" + } + } + } + }, + "Turn this off if another notch app opens on hover too. Click the notch or press your shortcut to open Pullo.": { + "localizations": { + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactívalo si otra app del notch también se abre al pasar el puntero. Haz clic en el notch o usa tu atajo para abrir Pullo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desative se outro app de notch também abre ao passar o ponteiro. Clique no notch ou use seu atalho para abrir o Pullo." + } + } + } } }, "version": "1.0" diff --git a/Sources/Pullo/Support/DefaultsKey.swift b/Sources/Pullo/Support/DefaultsKey.swift index 056a575..052c0ba 100644 --- a/Sources/Pullo/Support/DefaultsKey.swift +++ b/Sources/Pullo/Support/DefaultsKey.swift @@ -3,7 +3,7 @@ import Foundation enum DefaultsKey: String { case accounts, tabs, runTabs, unreadableTabs, unreadableRunTabs case mutedCategories, mutedAccounts, haptics, sound, categorySounds, panelStyle, liveCI, liveCILabel - case display, shortcut, holdInFullScreen + case display, shortcut, holdInFullScreen, openOnHover case readBaselines, readKnownAccounts case betaUpdates } diff --git a/Tests/PulloTests/FirstRunSettingsTests.swift b/Tests/PulloTests/FirstRunSettingsTests.swift index c2d6733..71880c1 100644 --- a/Tests/PulloTests/FirstRunSettingsTests.swift +++ b/Tests/PulloTests/FirstRunSettingsTests.swift @@ -40,6 +40,7 @@ struct FirstRunSettingsTests { #expect(settings.display == .builtIn) #expect(settings.shortcut.shortcut == .togglePanel) #expect(!settings.holdsAlertsInFullScreen) + #expect(settings.opensOnHover) } @Test func choicesAreRemembered() { @@ -48,10 +49,12 @@ struct FirstRunSettingsTests { settings.display = .pointer settings.shortcut = .controlOptionP settings.holdsAlertsInFullScreen = true + settings.opensOnHover = false let reloaded = AppSettings(defaults: scratch.defaults) #expect(reloaded.display == .pointer) #expect(reloaded.shortcut.shortcut?.symbols == "⌃⌥P") #expect(reloaded.holdsAlertsInFullScreen) + #expect(!reloaded.opensOnHover) } @Test func theShortcutCanBeTurnedOff() { diff --git a/Tests/PulloTests/NotchInteractionTests.swift b/Tests/PulloTests/NotchInteractionTests.swift index 8b81746..56d512d 100644 --- a/Tests/PulloTests/NotchInteractionTests.swift +++ b/Tests/PulloTests/NotchInteractionTests.swift @@ -14,6 +14,24 @@ extension NotchBehavior { #expect(h.notch.state == .open) } + @Test func hoveringCanBeTurnedOff() async { + let h = await NotchHarness() + h.settings.opensOnHover = false + h.notch.mouseMoved(to: h.inside(.closed)) + await h.clock.advance(by: .seconds(1)) + #expect(h.notch.state == .closed) + h.notch.clickedNotch() + #expect(h.notch.state == .open) + } + + @Test func clickingTheNotchOpensIt() async { + let h = await NotchHarness() + h.notch.clickedNotch() + #expect(h.notch.state == .open) + h.notch.clickedNotch() + #expect(h.notch.state == .open) + } + @Test func passingOverTheNotchDoesNotOpenIt() async { let h = await NotchHarness() h.notch.mouseMoved(to: h.inside(.closed))