Files
Interlap ffd3a13d34 refactor: split into an importable library and a cmd/simslim CLI (#14)
The repo root becomes `package simslim`, an importable library holding every
piece of slimming logic; the CLI moves to `cmd/simslim`. Previously everything
was `package main`, so nothing here could be imported at all.

The library carries no external dependencies — urfave/cli stays on the CLI side,
along with all terminal-facing code: the interactive profile wizard, writeJSON,
truncate/humanBytes, fatal and usage. The library never writes to stdout; it
returns values and reports progress through the Reporter callback.

Renames forced by collisions with existing types: status() -> ReadStatus,
statusForDevice() -> ReadStatusForDevice, diskCleanupPlan() -> PlanDiskCleanup.
The shutdown/boot timeouts move to simctl.go as ShutdownTimeout/BootTimeout.

`go install` gains a path element: github.com/mobai-app/simslim/cmd/simslim.
The installed binary is still named simslim, and -X main.version still applies.
packaging/homebrew/simslim.rb.in is updated to match, since std_go_args passes
no package path.

Verified live against a real simulator: list, profiles, status, doctor, size,
disk-plan, measure, boot, shutdown, on, off, on --except, on --profile, clone,
rename, delete and the interactive wizard all behave identically to the previous
build. JSON output, human output, help text and exit codes are unchanged.
2026-07-23 12:39:20 +02:00

121 lines
3.9 KiB
Go

package simslim
import (
"context"
"fmt"
"sort"
)
// Reporter receives human-readable progress lines. A nil Reporter is a no-op,
// so non-interactive callers can ignore progress entirely.
type Reporter func(string)
func (r Reporter) report(msg string) {
if r != nil {
r(msg)
}
}
// ensure brings the device to exactly the desired disabled state and boots it.
// The disabled overrides persist in the device's launchd DB, so once set a slim
// device comes up slim in a single boot; a reboot only happens when the state
// actually changes. A running device is required to read/mutate launchctl, so
// the device is booted first regardless. Each slow phase reports progress so the
// caller can show the user that a multi-minute reconfigure is still working.
func ensure(ctx context.Context, set, udid string, desired map[string]bool, report Reporter) (changed bool, err error) {
report.report("Booting the simulator (a first boot can take up to a minute)...")
if err := BootAndWait(ctx, set, udid); err != nil {
return false, err
}
current, err := readDisabled(ctx, set, udid)
if err != nil {
return false, err
}
toDisable, toEnable := delta(current, desired, managedSet())
if len(toDisable) == 0 && len(toEnable) == 0 {
return false, nil
}
if len(toDisable) > 0 {
report.report(fmt.Sprintf("Disabling %d background services...", len(toDisable)))
}
if len(toEnable) > 0 {
report.report(fmt.Sprintf("Re-enabling %d background services...", len(toEnable)))
}
if err := applyDelta(ctx, set, udid, toDisable, toEnable, report); err != nil {
return true, err
}
report.report("Rebooting the simulator to apply the changes...")
if err := Shutdown(ctx, set, udid); err != nil {
return true, fmt.Errorf("shutdown before reboot: %w", err)
}
if err := WaitShutdown(ctx, set, udid, ShutdownTimeout); err != nil {
return true, err
}
return true, BootAndWait(ctx, set, udid)
}
// enableSlim disables the profile's daemons and boots the device slim.
func EnableSlim(ctx context.Context, set, udid string, p Profile, report Reporter) (bool, error) {
return ensure(ctx, set, udid, p.Desired(), report)
}
// disableSlim re-enables every managed daemon, returning the device to stock.
func DisableSlim(ctx context.Context, set, udid string, report Reporter) (bool, error) {
return ensure(ctx, set, udid, map[string]bool{}, report)
}
// Status describes how slim a device currently is.
type Status struct {
ManagedDisabled int `json:"managedDisabled"` // managed labels currently disabled
ManagedTotal int `json:"managedTotal"` // size of the managed universe
Booted bool `json:"booted"`
}
// status reports how slim a device is and returns the labels it currently has
// disabled (nil when the device is not booted).
func ReadStatus(ctx context.Context, udid string) (Status, map[string]bool, error) {
d, err := FindDevice(ctx, udid, "")
if err != nil {
return Status{}, nil, err
}
return ReadStatusForDevice(ctx, d)
}
func ReadStatusForDevice(ctx context.Context, d Device) (Status, map[string]bool, error) {
managed := SlimmableSet()
st := Status{ManagedTotal: len(managed), Booted: d.State == "Booted"}
if !st.Booted {
return st, nil, fmt.Errorf("simulator must be booted to read its state (it is %s)", d.State)
}
disabled, err := readDisabled(ctx, d.Set, d.UDID)
if err != nil {
return st, nil, err
}
for l := range disabled {
if managed[l] {
st.ManagedDisabled++
}
}
return st, disabled, nil
}
// droppedCategories groups the disabled managed daemons by category, in category
// order, omitting categories with nothing disabled.
func DroppedCategories(disabled map[string]bool) []DroppedCategory {
var out []DroppedCategory
for _, c := range Categories {
var labels []string
for _, l := range c.Labels {
if disabled[l] {
labels = append(labels, l)
}
}
if len(labels) == 0 {
continue
}
sort.Strings(labels)
out = append(out, DroppedCategory{ID: c.ID, Name: c.Name, Downside: c.Downside, Labels: labels})
}
return out
}