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
6 changes: 4 additions & 2 deletions Sources/SketchyBarToggle/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let controller = SketchyBarController()
let stateMachine = BarStateMachine(
controller: controller,
triggerZone: config.triggerZone,
untriggerZone: config.untriggerZone,
menuBarHeight: config.menuBarHeight,
debounceInterval: config.debounce
)
Expand Down Expand Up @@ -72,15 +73,15 @@ if config.debug || ProcessInfo.processInfo.environment["SKETCHYBAR_TOGGLE_DEBUG"
let line = "\(ISO8601DateFormatter().string(from: Date())) \(msg)\n"
debugLogFile?.write(line.data(using: .utf8) ?? Data())
}
debugLog?("started with trigger=\(Int(config.triggerZone)) menuBar=\(Int(config.menuBarHeight)) debounce=\(Int(config.debounce * 1000))ms")
debugLog?("started with trigger=\(Int(config.triggerZone)) untrigger=\(Int(config.untriggerZone)) menuBar=\(Int(config.menuBarHeight)) debounce=\(Int(config.debounce * 1000))ms")
} else {
debugLog = nil
}

let monitor = EventTapMonitor(stateMachine: stateMachine, debugLog: debugLog)
monitor.start()

print("sketchybar-toggle v\(version) running (trigger: \(Int(config.triggerZone))px, menu bar: \(Int(config.menuBarHeight))px, debounce: \(Int(config.debounce * 1000))ms)")
print("sketchybar-toggle v\(version) running (trigger: \(Int(config.triggerZone))px, untrigger: \(Int(config.untriggerZone))px, menu bar: \(Int(config.menuBarHeight))px, debounce: \(Int(config.debounce * 1000))ms)")
if config.debug { print("Debug logging to /tmp/sketchybar-toggle-debug.log") }
print("Press Ctrl+C to stop.")

Expand Down Expand Up @@ -110,6 +111,7 @@ func printUsage() {

Options:
--trigger-zone <px> Pixels from top of screen to trigger hide (default: 10)
--untrigger-zone <px> Pixels from top a click must clear to restore the bar (default: 30)
--menu-bar-height <px> Pixels from top defining menu bar zone (default: 50)
--debounce <ms> Debounce delay in milliseconds (default: 150)
--check-permissions Check permissions (no longer needed in v0.4.0+)
Expand Down
6 changes: 6 additions & 0 deletions Sources/SketchyBarToggleCore/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

public struct Config: Equatable {
public var triggerZone: CGFloat = 10
public var untriggerZone: CGFloat = 30
public var menuBarHeight: CGFloat = 50
public var debounce: TimeInterval = 0.15
public var checkPermissions = false
Expand Down Expand Up @@ -29,6 +30,11 @@ public func parseArguments(_ args: [String]) throws -> Config {
if i < args.count, let val = Double(args[i]) {
config.triggerZone = CGFloat(val)
}
case "--untrigger-zone":
i += 1
if i < args.count, let val = Double(args[i]) {
config.untriggerZone = CGFloat(val)
}
case "--menu-bar-height":
i += 1
if i < args.count, let val = Double(args[i]) {
Expand Down
5 changes: 4 additions & 1 deletion Sources/SketchyBarToggleCore/StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public final class BarStateMachine {
public private(set) var state: BarState = .visible

public let triggerZone: CGFloat
public let untriggerZone: CGFloat
public let menuBarHeight: CGFloat
public let debounceInterval: TimeInterval

Expand All @@ -21,12 +22,14 @@ public final class BarStateMachine {
public init(
controller: BarController,
triggerZone: CGFloat = 10,
untriggerZone: CGFloat = 30,
menuBarHeight: CGFloat = 50,
debounceInterval: TimeInterval = 0.15,
timerQueue: DispatchQueue = .main
) {
self.controller = controller
self.triggerZone = triggerZone
self.untriggerZone = untriggerZone
self.menuBarHeight = menuBarHeight
self.debounceInterval = debounceInterval
self.timerQueue = timerQueue
Expand Down Expand Up @@ -56,7 +59,7 @@ public final class BarStateMachine {
/// If the click is outside the trigger zone, immediately restore SketchyBar
/// so that window title bars near the top of the screen remain interactive.
public func handleMouseClick(distanceFromTop: CGFloat) {
guard state == .hidden, distanceFromTop >= triggerZone else { return }
guard state == .hidden, distanceFromTop >= untriggerZone else { return }
cancelDebounce()
state = .visible
controller.show()
Expand Down
6 changes: 6 additions & 0 deletions Tests/SketchyBarToggleTests/ConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ final class ConfigTests: XCTestCase {
func testDefaultConfig() throws {
let config = try parseArguments([])
XCTAssertEqual(config.triggerZone, 10)
XCTAssertEqual(config.untriggerZone, 30)
XCTAssertEqual(config.menuBarHeight, 50)
XCTAssertEqual(config.debounce, 0.15)
XCTAssertFalse(config.checkPermissions)
Expand All @@ -24,6 +25,11 @@ final class ConfigTests: XCTestCase {
XCTAssertEqual(config.menuBarHeight, 44)
}

func testUntriggerZone() throws {
let config = try parseArguments(["--untrigger-zone", "25"])
XCTAssertEqual(config.untriggerZone, 25)
}

func testDebounceConvertsMillisecondsToSeconds() throws {
let config = try parseArguments(["--debounce", "300"])
XCTAssertEqual(config.debounce, 0.3)
Expand Down
28 changes: 21 additions & 7 deletions Tests/SketchyBarToggleTests/StateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,40 @@ final class StateMachineTests: XCTestCase {

// MARK: - Click-to-restore behavior

func testClickInHiddenStateOutsideTriggerZoneRestoresBar() {
func testClickInHiddenStateOutsideUntriggerZoneRestoresBar() {
let mock = MockBarController()
let sm = BarStateMachine(controller: mock, triggerZone: 10, menuBarHeight: 50)
let sm = BarStateMachine(controller: mock, triggerZone: 10, untriggerZone: 30, menuBarHeight: 50)

sm.handleMousePosition(distanceFromTop: 5) // hide
XCTAssertEqual(sm.state, .hidden)
mock.reset()

sm.handleMouseClick(distanceFromTop: 30) // click in menu bar zone but outside trigger
sm.handleMouseClick(distanceFromTop: 40) // click below untrigger zone
XCTAssertEqual(sm.state, .visible)
XCTAssertEqual(mock.showCallCount, 1)
}

func testClickInHiddenStateInsideTriggerZoneDoesNotRestore() {
func testClickInHiddenStateInsideUntriggerZoneDoesNotRestore() {
let mock = MockBarController()
let sm = BarStateMachine(controller: mock, triggerZone: 10, menuBarHeight: 50)
let sm = BarStateMachine(controller: mock, triggerZone: 10, untriggerZone: 30, menuBarHeight: 50)

sm.handleMousePosition(distanceFromTop: 5) // hide
XCTAssertEqual(sm.state, .hidden)

sm.handleMouseClick(distanceFromTop: 5) // click still in trigger zone (using menu bar)
sm.handleMouseClick(distanceFromTop: 20) // click inside untrigger zone (menu bar area)
XCTAssertEqual(sm.state, .hidden)
}

func testClickBetweenTriggerAndUntriggerZoneDoesNotRestore() {
// Key split-threshold test: triggerZone=10, untriggerZone=30.
// A click at 15px is outside triggerZone but inside untriggerZone — should NOT restore.
let mock = MockBarController()
let sm = BarStateMachine(controller: mock, triggerZone: 10, untriggerZone: 30, menuBarHeight: 50)

sm.handleMousePosition(distanceFromTop: 5) // hide
XCTAssertEqual(sm.state, .hidden)

sm.handleMouseClick(distanceFromTop: 15) // between the two thresholds
XCTAssertEqual(sm.state, .hidden)
}

Expand All @@ -285,6 +298,7 @@ final class StateMachineTests: XCTestCase {
let sm = BarStateMachine(
controller: mock,
triggerZone: 10,
untriggerZone: 30,
menuBarHeight: 50,
debounceInterval: 0.1
)
Expand All @@ -300,7 +314,7 @@ final class StateMachineTests: XCTestCase {

func testClickBelowMenuBarZoneRestoresBar() {
let mock = MockBarController()
let sm = BarStateMachine(controller: mock, triggerZone: 10, menuBarHeight: 50)
let sm = BarStateMachine(controller: mock, triggerZone: 10, untriggerZone: 30, menuBarHeight: 50)

sm.handleMousePosition(distanceFromTop: 5) // hide
mock.reset()
Expand Down