Files
maksarsanjeev 52e1e8288b Security + upstream fixes
- Network core moves to a root-only folder; BroV talks to a narrow root helper (netctl.py) over a user-only socket; install script verifies the mihomo SHA256 and migrates keys (scripts/netcore)
- Hardened runtime, no get-task-allow; bypassPermissions removed from the chat; concealed clipboard items are not restored; DangerCheck knows core, LaunchAgents and hook paths; dropped-file copies expire after 7 days
- Ported from upstream Coucou: 1h crash fix (d05f22b), safe settings.json writes (918d30e), Escape/fold for pending approvals (6012900, 40e3ba8), auto-close delay + reopen (74984f2, ea244a7), full AskUserQuestion (52b1562)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:49:11 +03:00

191 lines
6.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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), kept in sync with the auto-close preference.
var homeToPetitDelay: TimeInterval = 15 {
didSet {
guard homeToPetitDelay != oldValue,
state == .home, homeCollapseWork != nil else { return }
scheduleHomeCollapse()
}
}
/// 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)
}
}