Skip to content
Merged
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
25 changes: 21 additions & 4 deletions Sources/removemacai/App/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ final class AppModel {

// What the person wants.
var wanted: Set<String> = []
/// Tweaks the person switched, so a partly applied one is only undone on request.
var touched: Set<String> = []
var aiOff = false
var aiKept: Set<String> = []

Expand Down Expand Up @@ -88,6 +90,7 @@ final class AppModel {
journal = Engine.loadJournal()
if resetChoices {
wanted = Set(states.filter { $0.value == .applied }.map(\.key))
touched = []
aiOff = currentAI != nil
aiKept = snapshot.profile.ai ? snapshot.profile.kept : []
}
Expand Down Expand Up @@ -127,9 +130,24 @@ final class AppModel {
func isOn(_ tweak: Tweak) -> Bool { wanted.contains(tweak.id) }

func toggle(_ tweak: Tweak, _ on: Bool) {
guard wanted.contains(tweak.id) != on else { return }
touched.insert(tweak.id)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if on { wanted.insert(tweak.id) } else { wanted.remove(tweak.id) }
}

/// Drops any choice made for the tweak, so it stays as it is on the Mac.
func leave(_ tweak: Tweak) {
wanted.remove(tweak.id)
touched.remove(tweak.id)
}

/// Whether leaving the tweak unwanted undoes it.
func undoes(_ tweak: Tweak) -> Bool {
let s = state(tweak)
guard !wanted.contains(tweak.id), s != .notApplied, s != .managed, s != .unsupported else { return false }
return s != .partial || touched.contains(tweak.id)
}

func choose(_ preset: Preset) {
wanted.formUnion(Tweaks.preset(preset).map(\.id))
aiOff = true
Expand All @@ -143,9 +161,8 @@ final class AppModel {
Tweaks.all.compactMap { t in
let s = state(t)
guard s != .managed && s != .unsupported else { return nil }
let want = wanted.contains(t.id)
if want && s != .applied { return (t, true) }
if !want && s != .notApplied { return (t, false) }
if wanted.contains(t.id) && s != .applied { return (t, true) }
if undoes(t) { return (t, false) }
return nil
}
}
Expand All @@ -157,7 +174,7 @@ final class AppModel {

func discard() { reload(resetChoices: true) }

func plan() -> Plan { Plan.make(wanted: wanted, ai: targetAI, snapshot: snapshot) }
func plan() -> Plan { Plan.make(wanted: wanted, ai: targetAI, undo: touched, snapshot: snapshot) }

// MARK: applying

Expand Down
17 changes: 12 additions & 5 deletions Sources/removemacai/App/Pages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,14 @@ struct TweakGroupView: View {
Menu {
Button("Select All") { for t in tweaks where t.supported { model.toggle(t, true) } }
Button("Select Recommended") {
for t in tweaks { model.toggle(t, t.presets.contains(.recommended) || model.state(t) == .applied) }
for t in tweaks {
let recommended = t.presets.contains(.recommended)
if model.state(t) == .partial && !recommended {
model.leave(t)
} else {
model.toggle(t, recommended || model.state(t) == .applied)
}
}
}
Button("Deselect All") { for t in tweaks { model.toggle(t, false) } }
} label: {
Expand All @@ -244,7 +251,7 @@ struct TweakRow: View {
Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.tertiary)
.help("Locked by the RemoveMacAI profile")
}
if let note = pendingNote(state: state, wanted: wanted) {
if let note = pendingNote(state: state, wanted: wanted, undoes: model.undoes(tweak)) {
Text(note).font(.caption.weight(.medium)).foregroundStyle(.tint)
}
}
Expand All @@ -257,7 +264,7 @@ struct TweakRow: View {
} else if state == .unsupported {
Text("Needs macOS \(tweak.since.0).\(tweak.since.1) or newer.").font(.caption).foregroundStyle(.secondary)
} else if state == .partial {
Text("Partly applied.").font(.caption).foregroundStyle(.secondary)
Text("Partly applied. It stays as it is unless you switch it.").font(.caption).foregroundStyle(.secondary)
}
}
.padding(.vertical, 2)
Expand All @@ -266,9 +273,9 @@ struct TweakRow: View {
.disabled(state == .managed || state == .unsupported)
}

func pendingNote(state: TweakState, wanted: Bool) -> String? {
func pendingNote(state: TweakState, wanted: Bool, undoes: Bool) -> String? {
if wanted && state != .applied { return "Applies on review" }
if !wanted && state != .notApplied && state != .managed && state != .unsupported { return "Undoes on review" }
if undoes { return "Undoes on review" }
return nil
}
}
Expand Down
18 changes: 14 additions & 4 deletions Sources/removemacai/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,28 @@ struct Plan {

/// From the tweaks that should end up applied and what Apple Intelligence
/// should look like (nil leaves it alone, a set turns it off except those).
static func make(wanted: Set<String>, ai: Set<String>?, snapshot s: Snapshot = Snapshot()) -> Plan {
/// A partly applied tweak is often the person's own setting, so it is only
/// undone when named in `undo`.
static func make(
wanted: Set<String>, ai: Set<String>?, undo: Set<String> = [], snapshot s: Snapshot = Snapshot(),
state read: ((Tweak) -> TweakState)? = nil
) -> Plan {
let stateOf = read ?? s.state
var plan = Plan()
func keepsPartial(_ tweak: Tweak) -> Bool { stateOf(tweak) == .partial && !undo.contains(tweak.id) }
for tweak in Tweaks.all {
let state = s.state(tweak)
let state = stateOf(tweak)
guard state != .managed && state != .unsupported else { continue }
if wanted.contains(tweak.id) && state != .applied {
plan.apply.append(tweak)
} else if !wanted.contains(tweak.id) && state != .notApplied {
} else if !wanted.contains(tweak.id) && state != .notApplied && !keepsPartial(tweak) {
plan.revert.append(tweak)
}
}
let profileTweaks = Set(Tweaks.all.filter { $0.inProfile && wanted.contains($0.id) && s.state($0) != .managed && $0.supported }.map(\.id))
let profileTweaks = Set(Tweaks.all.filter {
$0.inProfile && stateOf($0) != .managed && $0.supported
&& (wanted.contains($0.id) || (keepsPartial($0) && s.profile.tweaks.contains($0.id)))
}.map(\.id))
let target = Profile.Contents(ai: ai, tweaks: profileTweaks)
let current = s.profile.contents ?? Profile.Contents(ai: nil, tweaks: [])
if target != current { plan.profile = target }
Expand Down
34 changes: 34 additions & 0 deletions Sources/removemacai/SelfTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,40 @@ func selfTest() -> Bool {
program: "/Library/PrivilegedHelperTools/dev.orbstack.OrbStack.privhelper", system: true)
check(script.owner == "sync.sh" && helper.owner == "OrbStack", "background items are named after what they run")


// Partly applied tweaks are the person's own settings until they ask.
var s = Snapshot()
s.profile = Profile.Installed(on: true, ai: false, kept: [], tweaks: ["analytics"])
let states: [String: TweakState] = ["smart-punctuation": .partial, "autocorrect": .applied, "analytics": .partial]
let fake: (Tweak) -> TweakState = { states[$0.id] ?? .notApplied }
let applyOne = Plan.make(wanted: ["autocorrect", "file-extensions"], ai: nil, snapshot: s, state: fake)
check(applyOne.apply.map(\.id) == ["file-extensions"] && applyOne.revert.isEmpty && applyOne.profile == nil,
"applying a tweak leaves partly applied ones and the profile alone")
let undoOne = Plan.make(wanted: ["autocorrect"], ai: nil, undo: ["smart-punctuation"], snapshot: s, state: fake)
check(undoOne.revert.map(\.id) == ["smart-punctuation"] && undoOne.profile == nil,
"a partly applied tweak is undone when named")
let undoLocked = Plan.make(wanted: ["autocorrect"], ai: nil, undo: ["analytics"], snapshot: s, state: fake)
check(undoLocked.profile?.tweaks == [] && undoLocked.revert.map(\.id) == ["analytics"],
"a partly applied profile tweak leaves the profile when named")

MainActor.assumeIsolated {
let model = AppModel()
let partialTweak = Tweaks.tweak("smart-punctuation")!
model.states = Dictionary(uniqueKeysWithValues: Tweaks.all.map { ($0.id, TweakState.notApplied) })
model.states[partialTweak.id] = .partial
model.wanted = []
model.touched = []
model.toggle(partialTweak, false)
check(model.pendingTweaks.isEmpty, "switching off a partly applied tweak that is already off changes nothing")
model.toggle(partialTweak, true)
model.leave(partialTweak)
check(model.pendingTweaks.isEmpty, "leaving a partly applied tweak drops an earlier selection")
model.toggle(partialTweak, true)
model.toggle(partialTweak, false)
check(model.pendingTweaks.map(\.tweak.id) == [partialTweak.id] && model.pendingTweaks.first?.apply == false,
"switching a partly applied tweak on and off undoes it")
}

print(failed == 0 ? Term.green("all checks passed") : Term.red("\(failed) failed"))
return failed == 0
}
2 changes: 1 addition & 1 deletion Sources/removemacai/TweakCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ enum TweakCommands {
wanted.remove(id)
}
let ai: Set<String>? = s.profile.on && s.profile.ai ? s.profile.kept : nil
let plan = Plan.make(wanted: wanted, ai: ai, snapshot: s)
let plan = Plan.make(wanted: wanted, ai: ai, undo: Set(ids), snapshot: s)
return run(plan, dryRun: dryRun, yes: yes, verb: "Undo")
}

Expand Down