Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:34:32 +03:00
commit 53b4f60da1
158 changed files with 29442 additions and 0 deletions
@@ -0,0 +1,184 @@
import Foundation
/// Pure 4-state FSM for island open/close logic.
/// No AppKit / AppState dependencies — communicates via `onTransition`.
@MainActor
final class IslandStateMachine {
enum State: Equatable {
case hidden // island invisible (notch size)
case petit // compact island (notch + ears)
case home // expanded, overview
case coucou // expanded, greeting animation
}
private(set) var state: State = .hidden
/// Fired on every transition: (from, to)
var onTransition: ((State, State) -> Void)?
/// When non-nil and returns true, timers and mouse-leave never auto-collapse or hide the island.
var isHeldOpen: (() -> Bool)?
/// home → petit delay (seconds). Override for debug.
var homeToPetitDelay: TimeInterval = 15
/// petit → hidden delay (seconds). Override for debug.
var petitToHiddenDelay: TimeInterval = 60
/// coucou → petit delay after greeting animation ends (no hover). ~0.6s syncs with canvas collapse.
var greetAutoCollapseDelay: TimeInterval = 0.6
/// coucou → petit delay when mouse is hovering over the greeting.
var greetHoverCollapseDelay: TimeInterval = 10
private var petitHideWork: DispatchWorkItem?
private var homeCollapseWork: DispatchWorkItem?
private var greetCollapseWork: DispatchWorkItem?
// MARK: – Inputs
/// App launched or debug "launch greeting"
func launch() {
cancelTimers()
transition(to: .coucou)
}
/// Mouse entered the island notch area
func mouseEntered() {
switch state {
case .hidden:
if isHeldOpen?() == true {
// Island already expanded by an external call — sync FSM state without transition
state = .home
} else {
cancelTimers()
transition(to: .petit)
}
case .petit:
petitHideWork?.cancel()
petitHideWork = nil
case .home:
homeCollapseWork?.cancel()
homeCollapseWork = nil
case .coucou:
// Mouse hovering during greeting — cancel short auto-collapse, extend to hover delay
scheduleGreetCollapse(delay: greetHoverCollapseDelay)
}
}
/// Mouse left the island notch area
func mouseLeft() {
switch state {
case .hidden:
break
case .petit:
schedulePetitHide()
case .home:
if isHeldOpen?() != true { scheduleHomeCollapse() }
case .coucou:
if isHeldOpen?() != true {
// Interrupt greeting immediately → compact (overrides 10s auto-collapse)
greetCollapseWork?.cancel(); greetCollapseWork = nil
transition(to: .petit)
}
}
}
/// Compact island clicked.
/// Also accepts `.hidden`: after an alert the island can be on screen while the
/// FSM never saw the mouse enter (it was already there), and the click must still open it.
func click() {
guard state == .petit || state == .hidden else { return }
cancelTimers()
transition(to: .home)
}
/// The app hid the island on its own (e.g. `AppState.syncMode()` when the last
/// task ends). Mirror it without side effects, so the next hover peeks again
/// instead of being swallowed by a FSM that still thinks the island is `.petit`.
func hiddenExternally() {
guard state == .petit else { return }
cancelTimers()
state = .hidden
}
/// The app expanded the island externally (hookExpand for an alert).
/// Cancel timers and sync state to `.home` without firing `onTransition`, so the
/// next hover/mouseLeft behave correctly instead of collapsing the island.
func openedExternally() {
cancelTimers()
guard state != .home && state != .coucou else { return }
state = .home
}
/// The app folded the island itself (Escape, Settings, OK button, auto-close).
/// Move to `.petit` right away so hover and click keep working; waiting for the
/// 15 s home timer left the island compact on screen while the FSM still said `.home`.
func collapse() {
guard state == .home || state == .coucou else { return }
cancelTimers()
transition(to: .petit)
}
/// Greeting animation finished (called at T.end ≈ 4.60 s).
/// Schedules auto-collapse. Does not override a longer hover timer already running.
func greetComplete() {
guard state == .coucou else { return }
// If mouse entered before this fires (hover timer already running), don't override it
if greetCollapseWork == nil {
scheduleGreetCollapse(delay: greetAutoCollapseDelay)
}
}
private func scheduleGreetCollapse(delay: TimeInterval) {
greetCollapseWork?.cancel()
let item = DispatchWorkItem { [weak self] in
guard let self, self.state == .coucou else { return }
self.transition(to: .petit)
}
greetCollapseWork = item
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: item)
}
/// Non-alert work event: show compact from hidden (HookServer reveal)
func reveal() {
guard state == .hidden else { return }
cancelTimers()
transition(to: .petit)
schedulePetitHide()
}
// MARK: – Timers
private func schedulePetitHide() {
petitHideWork?.cancel()
let item = DispatchWorkItem { [weak self] in
guard let self, self.state == .petit, !(self.isHeldOpen?() ?? false) else { return }
self.transition(to: .hidden)
}
petitHideWork = item
DispatchQueue.main.asyncAfter(deadline: .now() + petitToHiddenDelay, execute: item)
}
private func scheduleHomeCollapse() {
homeCollapseWork?.cancel()
let item = DispatchWorkItem { [weak self] in
guard let self, self.state == .home, !(self.isHeldOpen?() ?? false) else { return }
self.transition(to: .petit)
}
homeCollapseWork = item
DispatchQueue.main.asyncAfter(deadline: .now() + homeToPetitDelay, execute: item)
}
func cancelTimers() {
petitHideWork?.cancel(); petitHideWork = nil
homeCollapseWork?.cancel(); homeCollapseWork = nil
greetCollapseWork?.cancel(); greetCollapseWork = nil
}
private func transition(to new: State) {
guard new != state else { return }
let old = state
state = new
onTransition?(old, new)
}
}