Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Sources/Pullo/App/AppDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions Sources/Pullo/App/NotchPanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion Sources/Pullo/Features/Notch/NotchInteraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Duration>

Expand All @@ -40,6 +41,7 @@ final class NotchInteraction {
liveCI: LiveCIModel,
runs: RunStore,
accounts: AccountStore,
settings: AppSettings,
system: any SystemActions,
geometry: NotchGeometry,
clock: any Clock<Duration> = ContinuousClock()
Expand All @@ -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
Expand Down Expand Up @@ -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() }
}
Expand All @@ -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() }
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/Pullo/Features/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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)
}
}
5 changes: 5 additions & 0 deletions Sources/Pullo/Features/Settings/GeneralPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
32 changes: 32 additions & 0 deletions Sources/Pullo/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Sources/Pullo/Support/DefaultsKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions Tests/PulloTests/FirstRunSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct FirstRunSettingsTests {
#expect(settings.display == .builtIn)
#expect(settings.shortcut.shortcut == .togglePanel)
#expect(!settings.holdsAlertsInFullScreen)
#expect(settings.opensOnHover)
}

@Test func choicesAreRemembered() {
Expand All @@ -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() {
Expand Down
18 changes: 18 additions & 0 deletions Tests/PulloTests/NotchInteractionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading