feat(patterns): mobile-origin users + dual-boot supplement system

New SourceOs variants: Android, IpadOs — routed to android-to-* and
ipad-to-* pattern files respectively. Pattern bodies assume zero terminal
experience; every command explained from first principles with App Store /
iOS analogies.

Dual-boot supplement system: PatternFile::extend() + load_supplement()
in patterns.rs; lib.rs loads dualboot-{windows,macos}.toml on top of the
primary pattern file when migration.dual_boot_with is set. Supplement
covers NTFS dirty flag from Fast Startup, clock skew (RTC local vs UTC),
GRUB overwrite by Windows Update, BitLocker, APFS/HFS+ access, T2 Secure
Boot.

complete_onboarding() now accepts dual_boot_with: Option<String> and
normalises it to "windows"/"macos". Onboarding.vue becomes a 3-step flow:
source OS -> (Linux distro if linux) -> (dual-boot if windows/macos).
Mobile users skip the dual-boot step entirely.

10 new pattern files (8 mobile + 2 supplements), config.rs tests updated.
This commit is contained in:
pyr0ball 2026-05-19 09:32:18 -07:00
parent 19286e9860
commit e4a682be2f
15 changed files with 1272 additions and 30 deletions

View file

@ -0,0 +1,136 @@
[meta]
source_os = "android"
target_distro_family = "arch"
# Android user on their first Arch/CachyOS install.
# Assumes NO terminal experience (unless they used Termux).
# Every explanation starts from first principles.
# App Store analogy: pacman/AUR = Google Play + sideloading.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "App installer is busy"
body = "The package manager (Linux's equivalent of the Play Store) got interrupted and left a lock file. A lock file is a signal to other processes that says 'I'm running, don't start.' If nothing is installing right now, remove it: open a terminal, type exactly: sudo rm /var/lib/pacman/db.lck — then press Enter and try again. 'sudo' means 'run this as administrator.'"
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "App list is out of date — update everything"
body = "On Android, updates happen automatically. On Arch Linux, you need to run updates manually — and there's an important rule: always update ALL apps at the same time, never just the list. In a terminal, type: sudo pacman -Syu — then press Enter. Enter your password when asked. The -Syu means 'sync the list AND upgrade everything.'"
[[patterns]]
id = "pacman-dep-conflict"
sources = ["journald", "applog:pacman"]
match_text = "conflicting dependencies"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared — like two apps that both want to be the default music player. Read the message carefully. Usually one package replaces another. Remove the old one first: sudo pacman -R <old-package-name> — then try your install again."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "App build failed (AUR)"
body = "The AUR is like sideloading apps on Android — you're installing from source code that gets compiled on your machine, not a pre-built app. The build failed, usually because of a missing tool or broken code. Look at the error text above this message for the specific reason. The AUR package's comments page on aur.archlinux.org often has fixes."
# ── Terminal basics ───────────────────────────────────────────────────────────
[[patterns]]
id = "command-not-found"
sources = ["journald"]
match_text = "command not found"
severity = "info"
title = "Command not found — app may not be installed"
body = "You tried to run a program that isn't installed. On Android, apps are visible in the drawer; on Linux, they're invisible until you ask for them. To find and install the missing program, try: sudo pacman -Ss <name> — this searches for it. Then install with: sudo pacman -S <name>."
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "Linux has a permission system where files and folders are owned by specific users. This is more visible here than on Android. If you need admin access for a command, put 'sudo' before it — like: sudo <command>. For files you own but can't access, check ownership with: ls -la /path/to/file"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "On Android, drivers come pre-installed and invisible. On Linux, some hardware needs a separate firmware file — like a plugin for your Wi-Fi chip or graphics card. Install the main firmware package: sudo pacman -S linux-firmware — then restart. If a specific device still doesn't work, the error message above will name which firmware file is missing."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux ran out of RAM and had to close a program, similar to Android killing background apps. If this keeps happening, close programs you're not using, or add 'swap' (disk space used as overflow RAM): sudo pacman -S zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong reading or writing to the drive. This could be a hardware problem. Check drive health: sudo smartctl -a /dev/sda — first install the tool: sudo pacman -S smartmontools"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire is the audio manager — like the sound settings system inside Android. Restart it: open a terminal and type: systemctl --user restart pipewire pipewire-pulse wireplumber — if sound still doesn't work, log out and log back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "A software setting is blocking Bluetooth — like Airplane Mode on your phone. Run: rfkill unblock bluetooth — in a terminal. If it shows 'Hard blocked', there's a physical switch or BIOS setting to check."
[[patterns]]
id = "bluetooth-profile-unavailable"
sources = ["journald"]
match_text = "br-connection-profile-unavailable"
severity = "info"
title = "Bluetooth audio profile missing"
body = "Your Bluetooth device connected but the right audio mode isn't available. Install: sudo pacman -S pipewire-bluetooth — then restart Bluetooth: sudo systemctl restart bluetooth"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Linux couldn't connect to the network. Common causes: wrong password, or the Wi-Fi driver isn't loaded. Check connection status: nmcli device status — if the Wi-Fi adapter doesn't appear, the driver may need to be installed."
# ── GPU ───────────────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "Graphics card stopped responding"
body = "The graphics card froze and the driver recovered it — like a forced restart of the GPU. Games or video apps may have crashed. If this keeps happening, check that your graphics drivers are current: sudo pacman -Syu"

View file

@ -0,0 +1,137 @@
[meta]
source_os = "android"
target_distro_family = "debian"
# Android user on their first Debian/Ubuntu/Mint install.
# Assumes NO terminal experience. Ubuntu/Mint are the recommended starting points
# for Android migrants because of automatic updates and GUI app stores (GNOME Software/Discover).
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "App installer is busy"
body = "The software installer (Ubuntu/Mint calls it 'apt') is already running — probably doing automatic background updates, similar to how Android apps update silently. Wait a minute and try again. If it's stuck: open a terminal and type: sudo rm /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock — then: sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "App install was cut short"
body = "A previous install didn't finish cleanly — like pulling the charging cable out mid-update on your phone. Fix it: open a terminal and type: sudo dpkg --configure -a — then try your install again."
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "App needs another app first"
body = "The software you're trying to install needs something else installed first — similar to a game on Android requiring Google Play Services. Let the installer fix it automatically: sudo apt --fix-broken install"
# ── Terminal basics ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "Linux files and folders are protected by a permission system. If a command fails with this error, you may need to run it as admin — put 'sudo' before the command: sudo <command> — and enter your password. Your password won't show as you type, that's normal."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "Ubuntu/Debian includes a security layer called AppArmor — like Android's app permissions system, but for the whole operating system. An app tried to do something outside its allowed permissions. Usually this resolves itself; if an app keeps failing, check: sudo aa-status"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a 'firmware' file — a small program that tells Linux how to talk to a specific chip. Install the main firmware package: sudo apt install firmware-linux linux-firmware — restart after. Ubuntu usually handles this automatically; you may see this on Debian."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up memory — similar to Android killing background apps when RAM is full. If this keeps happening, try closing programs you're not using."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong reading or writing to the drive. This is a hardware-level issue. Install a diagnostic tool: sudo apt install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire is the audio manager — restart it: systemctl --user restart pipewire pipewire-pulse wireplumber — if that doesn't help, log out and back in."
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "Sound system not responding"
body = "The audio system (PulseAudio) stopped working. Restart it: pulseaudio --kill && pulseaudio --start — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal. Like turning Airplane Mode off on your phone."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "The printing service isn't running. Unlike Android where you'd use a manufacturer app, Linux uses a universal print system called CUPS. Start it: sudo systemctl start cups && sudo systemctl enable cups"
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Double-check the password, or try: nmcli device status — in a terminal to see your network devices."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Media format not supported"
body = "Linux doesn't include some video/audio formats by default for legal reasons — unlike Android which bundles them. Install them on Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — this adds MP3, MP4, and other common formats."

View file

@ -0,0 +1,102 @@
[meta]
source_os = "android"
target_distro_family = "fedora"
# Android user on their first Fedora install.
# Assumes NO terminal experience. All explanations from first principles.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "App installer is busy"
body = "Fedora's software installer (dnf) is already running — probably automatic background updates, similar to how Android apps update silently. Wait a minute. If it's stuck: open a terminal and type: sudo killall dnf — then try again."
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared. Let Fedora try to fix it: sudo dnf distro-sync — this brings everything back into a consistent state."
# ── SELinux ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "Security system blocked an action"
body = "Fedora includes SELinux — a security layer that controls what each program is allowed to do, more detailed than Android's app permissions. This is usually a normal event, not a problem. If an app keeps failing, check what's being blocked: ausearch -m AVC -ts recent"
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a firmware file — a program that tells Linux how to talk to a specific chip. Install it: sudo dnf install linux-firmware — restart after. For some hardware (especially older WiFi cards), you may also need: sudo dnf install https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm"
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM, like Android killing background apps. If this keeps happening, try closing some programs."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install a diagnostic tool: sudo dnf install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart the audio system: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check: nmcli device status — in a terminal. If the adapter doesn't appear, check: dmesg | grep firmware — a missing driver may be the cause."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Media format not supported"
body = "Linux doesn't include some video/audio formats by default. Install them from RPM Fusion: first enable it: sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm — then: sudo dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-ugly"

View file

@ -0,0 +1,103 @@
[meta]
source_os = "android"
target_distro_family = "opensuse"
# Android user on their first openSUSE install.
# Assumes NO terminal experience. openSUSE's YaST GUI tool is a good entry point
# for users unfamiliar with terminal-based administration.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "App installer is busy"
body = "The software installer (zypper) is already running. Wait a minute. If it's stuck: open a terminal and type: sudo zypper ps — to see what's using it."
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't be shared. Run a full update to resolve: sudo zypper dup — then try again."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "openSUSE includes AppArmor — a security layer like Android's app permissions but for the whole system. An app was blocked from doing something. Check: sudo aa-status — YaST also has an AppArmor section under Security."
# ── YaST ──────────────────────────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "Settings tool crashed"
body = "YaST is openSUSE's settings tool — like the Settings app on Android but for the whole system. It crashed. Try: sudo yast2 — in a terminal to run the text version, which is more stable."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Install the firmware package: sudo zypper install kernel-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM, like Android killing background apps. If this keeps happening, openSUSE's YaST -> System -> Partitioner can add or resize swap space."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install: sudo zypper install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart the audio system: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth blocked by software switch"
body = "Run: rfkill unblock bluetooth — in a terminal. YaST -> Network -> Bluetooth also has a toggle."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check: nmcli device status — or use YaST -> Network Settings to diagnose."

View file

@ -0,0 +1,65 @@
[meta]
source_os = "supplement"
target_distro_family = "any"
# Supplementary patterns for users dual-booting macOS alongside any Linux distro.
# These patterns cover coexistence-specific issues unique to Apple hardware.
# This file is merged on top of the primary migration pattern file.
[log_paths]
# ── Apple T2 / Secure Boot ────────────────────────────────────────────────────
[[patterns]]
id = "t2-secure-boot"
sources = ["kmsg", "journald"]
match_text = "Secure Boot"
severity = "warn"
title = "Apple T2 Secure Boot blocking Linux"
body = "Intel Macs with a T2 chip require Secure Boot to be disabled before Linux can boot. Boot into macOS Recovery (hold Cmd+R at startup) -> Utilities -> Startup Security Utility -> set Secure Boot to 'No Security' and allow booting from external media. Apple Silicon (M1/M2) Macs cannot dual-boot Linux at all — see Asahi Linux for the current state."
[[patterns]]
id = "apple-wifi-firmware"
sources = ["kmsg"]
match_text = "brcmfmac: brcmf_fw_alloc_request"
severity = "warn"
title = "Apple WiFi firmware not loading"
body = "Broadcom WiFi chips in Macs need proprietary firmware. Extract it from the macOS partition: mount your macOS partition and copy from /Volumes/Macintosh HD/usr/share/firmware/wifi/ — or install apple-firmware-wifi (check your distro's AUR or repos)."
# ── HFS+ / APFS ───────────────────────────────────────────────────────────────
[[patterns]]
id = "apfs-not-mounted"
sources = ["journald"]
match_text = "apfs: module not found"
severity = "info"
title = "macOS APFS partition not readable"
body = "Linux can't read APFS (macOS's filesystem) natively. To access files: sudo apt install apfs-fuse (Debian) or paru -S apfs-fuse-git (Arch). Mount: apfs-fuse /dev/sdXN /mnt/mac — read-only access only."
[[patterns]]
id = "hfsplus-not-mounted"
sources = ["journald"]
match_text = "hfsplus: Journal not clean"
severity = "warn"
title = "HFS+ partition not cleanly unmounted"
body = "The macOS HFS+ partition (older Macs) wasn't unmounted cleanly. Mount in macOS and run Disk Utility -> First Aid to fix it. Or force Linux mount: sudo mount -o force /dev/sdXN /mnt/mac"
# ── rEFInd / boot manager ────────────────────────────────────────────────────
[[patterns]]
id = "refind-missing"
sources = ["journald"]
match_text = "Boot0001"
severity = "info"
title = "EFI boot entry may be missing"
body = "macOS may have reset the EFI boot order after an update, removing the Linux entry. rEFInd is the recommended boot manager for Mac dual-boot: it auto-detects both macOS and Linux. Install: sudo refind-install — or reinstall GRUB EFI and re-add it with efibootmgr."
# ── Clock skew ────────────────────────────────────────────────────────────────
[[patterns]]
id = "rtc-time-wrong"
sources = ["journald"]
match_text = "RTC time"
severity = "info"
title = "System clock drifted after macOS boot"
body = "macOS stores the hardware clock in local time; Linux stores it in UTC. This causes clock drift in dual-boot. Fix in Linux: timedatectl set-local-rtc 0 — then set macOS to UTC by running in Terminal: sudo systemsetup -setusingnetworktime off && sudo systemsetup -settime $(date -u +%H:%M:%S)"

View file

@ -0,0 +1,75 @@
[meta]
source_os = "supplement"
target_distro_family = "any"
# Supplementary patterns for users dual-booting Windows alongside any Linux distro.
# These patterns cover coexistence-specific issues that only appear because both OSes
# share the same hardware. This file is merged on top of the primary migration pattern file.
[log_paths]
# ── NTFS / Fast Startup ───────────────────────────────────────────────────────
[[patterns]]
id = "ntfs-volume-dirty"
sources = ["kmsg"]
match_text = "volume is dirty"
severity = "warn"
title = "Windows drive needs a check (Fast Startup)"
body = "Windows didn't shut down cleanly — it probably used Fast Startup (hibernation). Linux mounted the NTFS partition read-only to protect your data. Fix in Windows: Start -> Power -> hold Shift and click Shut Down (real shutdown, not hibernate). Then turn Fast Startup off: Control Panel -> Power Options -> 'Choose what the power buttons do' -> uncheck 'Turn on fast startup'."
[[patterns]]
id = "ntfs-force-required"
sources = ["kmsg"]
match_text = "Dirty flag is set"
severity = "warn"
title = "NTFS drive mounted read-only (dirty flag)"
body = "Windows left the NTFS filesystem marked dirty. Boot into Windows and do a full shutdown (Shift+Shut Down), or force-fix on Linux: sudo ntfsfix /dev/sdXN — replace sdXN with the partition shown in the error."
[[patterns]]
id = "ntfs-hibernation"
sources = ["kmsg"]
match_text = "Windows is hibernated"
severity = "warn"
title = "Windows is hibernated — partition locked"
body = "Linux found a Windows hibernation file (hiberfil.sys) and can't write to the NTFS partition safely. You must resume and properly shut down Windows first. To remove the hibernation file permanently: in Windows as admin, run: powercfg /h off"
# ── Clock skew ────────────────────────────────────────────────────────────────
[[patterns]]
id = "rtc-time-wrong"
sources = ["journald"]
match_text = "RTC time"
severity = "info"
title = "System clock drifted after Windows boot"
body = "Windows stores the hardware clock in local time; Linux stores it in UTC. Dual-booting causes clock drift between sessions. Fix permanently in Linux: timedatectl set-local-rtc 0 (keep Linux correct and fix Windows instead). Or in Windows, add a registry key to use UTC: HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation, add DWORD RealTimeIsUniversal = 1."
# ── GRUB overwritten by Windows ───────────────────────────────────────────────
[[patterns]]
id = "grub-missing-windows-update"
sources = ["journald"]
match_text = "error: no such device"
severity = "warn"
title = "GRUB may have been overwritten"
body = "Windows Update sometimes overwrites the EFI boot entry. If Linux stopped booting after a Windows update: boot from your Linux USB installer -> rescue/chroot -> reinstall GRUB: grub-install /dev/sdX && update-grub (Debian/Ubuntu) or grub-install /dev/sdX && grub-mkconfig -o /boot/grub/grub.cfg (Arch)."
# ── BitLocker ─────────────────────────────────────────────────────────────────
[[patterns]]
id = "bitlocker-blocked"
sources = ["kmsg"]
match_text = "BitLocker"
severity = "info"
title = "BitLocker encrypted partition"
body = "This Windows partition is BitLocker-encrypted. Linux can mount it with dislocker: sudo apt install dislocker (Debian) or paru -S dislocker (Arch). You'll need the BitLocker recovery key from your Microsoft account."
# ── Shared NTFS partition permissions ────────────────────────────────────────
[[patterns]]
id = "ntfs-permission-error"
sources = ["journald"]
match_text = "ntfs-3g: Failed to open"
severity = "warn"
title = "NTFS permission error"
body = "ntfs-3g can't open the Windows partition. Check your /etc/fstab mount options — add uid=1000,gid=1000,umask=022 to give your Linux user access. Make sure Windows is fully shut down first."

View file

@ -0,0 +1,120 @@
[meta]
source_os = "ipad"
target_distro_family = "arch"
# iPad/iPhone user on their first Arch/CachyOS install.
# More sandboxed mental model than Android — no file manager, no sideloading concept,
# everything lived inside apps. Arch is a steep starting point; body text is encouraging
# but honest about the learning curve.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "pacman-db-lock"
sources = ["journald", "applog:pacman"]
match_text = "could not lock database: File exists"
severity = "warn"
title = "App installer is busy"
body = "The package manager (pacman — Linux's version of the App Store, but text-based) got interrupted and left a lock file behind. Think of it like an App Store update that got cut off. If nothing is currently installing, remove the lock: open a terminal (called 'Konsole' or 'Alacritty' on your system) and type: sudo rm /var/lib/pacman/db.lck — then press Enter. 'sudo' means run as administrator; your password won't show as you type."
[[patterns]]
id = "partial-upgrade-warning"
sources = ["applog:pacman"]
match_text = "warning: database file for"
severity = "info"
title = "App list out of date — update everything together"
body = "On iPad, updates happen automatically and silently. On Arch Linux, you run them manually. Important rule: always update everything at once. In a terminal, type: sudo pacman -Syu — press Enter, enter your password. This refreshes the app list AND updates all installed software."
[[patterns]]
id = "aur-build-failure"
sources = ["journald", "applog:pacman"]
match_text = "error: failed to build"
severity = "warn"
title = "App build failed')"
body = "The AUR (Arch User Repository) is software that gets compiled on your machine from source code — there's no real iOS equivalent since Apple controls all distribution. A build failed. Look at the error text above this notification for the specific cause. The AUR package's comment page on aur.archlinux.org often has workarounds."
# ── Files and paths ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "On iPad, every app lives in its own private sandbox — you never think about file permissions. On Linux, files are shared between users and programs, and access is controlled by permissions. If you need admin rights for a command, add 'sudo' before it: sudo <command> — and enter your password."
[[patterns]]
id = "no-such-file"
sources = ["journald"]
match_text = "No such file or directory"
severity = "info"
title = "File or folder not found"
body = "On iPad, files lived inside apps and you never typed paths. On Linux, files have locations like /home/username/Documents. Check that the path you typed is correct — Linux paths are case-sensitive ('Documents' and 'documents' are different). Use 'ls' to list files in the current folder."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "On iPad, Apple controls all hardware and drivers — you never see this. On Linux, some hardware components need a driver file installed separately. Install the main driver package: sudo pacman -S linux-firmware — then restart your computer."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory — closed an app"
body = "Linux had to close a program to free up RAM — similar to iPadOS refreshing apps in the background when RAM runs low. If this keeps happening, consider closing unused programs or adding 'zram' (uses storage as extra RAM): sudo pacman -S zram-generator"
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Something went wrong with the drive. Install a check tool: sudo pacman -S smartmontools — then run: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "PipeWire manages audio on this system — like the iOS audio system, but you can restart it. Type in a terminal: systemctl --user restart pipewire pipewire-pulse wireplumber — if sound still doesn't work, log out and log back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "A software setting is blocking Bluetooth — like enabling Airplane Mode. Turn it back on: rfkill unblock bluetooth — in a terminal. If it says 'Hard blocked', there's a physical switch on your computer or a setting in the BIOS."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Couldn't connect to the network. Check status: nmcli device status — in a terminal. If the Wi-Fi adapter doesn't appear in the list, the driver may not be loaded."
# ── GPU ───────────────────────────────────────────────────────────────────────
[[patterns]]
id = "gpu-hang"
sources = ["kmsg"]
match_text = "GPU HANG"
severity = "warn"
title = "Graphics card stopped responding"
body = "The graphics system crashed and recovered — similar to an app freezing on iPad, but at a lower level. If this keeps happening during games or video, update your graphics drivers: sudo pacman -Syu"

View file

@ -0,0 +1,137 @@
[meta]
source_os = "ipad"
target_distro_family = "debian"
# iPad/iPhone user on their first Debian/Ubuntu/Mint install.
# Ubuntu/Mint are the most recommended starting points for iPad migrants —
# automatic updates, GUI software center, familiar GNOME/Cinnamon interface.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "apt-lock"
sources = ["journald"]
match_text = "Could not get lock /var/lib/dpkg/lock"
severity = "warn"
title = "Software installer is busy"
body = "Ubuntu's software installer is already running in the background — probably doing automatic updates, similar to how iOS updates apps silently. Wait a minute and try again. You can also open 'Software Updater' from the app menu to see what's happening. If it's been stuck for a long time: open the Terminal app and type: sudo rm /var/lib/dpkg/lock-frontend && sudo dpkg --configure -a"
[[patterns]]
id = "dpkg-interrupted"
sources = ["journald"]
match_text = "dpkg was interrupted"
severity = "warn"
title = "App install was cut short"
body = "A previous software install didn't finish. Fix it: open Terminal and type: sudo dpkg --configure -a — then press Enter."
[[patterns]]
id = "apt-unmet-dependency"
sources = ["journald"]
match_text = "Unmet dependencies"
severity = "warn"
title = "App needs another app first"
body = "The software you're installing needs something else first. Let Ubuntu fix it: sudo apt --fix-broken install"
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "Ubuntu includes AppArmor — a security layer that restricts what each program can do, similar to how iOS tightly sandboxes every app. Something was blocked. This is usually routine security protection, not a problem."
# ── Files and paths ───────────────────────────────────────────────────────────
[[patterns]]
id = "permission-denied"
sources = ["journald"]
match_text = "Permission denied"
severity = "info"
title = "Permission denied"
body = "On iPad, files are hidden inside apps and permissions are invisible. On Linux, files are shared and controlled. If a command fails with this, add 'sudo' before it to run as admin: sudo <command> — your password won't show as you type, that's normal."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a driver file installed separately — unlike iPad where Apple controls everything. Ubuntu usually handles this, but if something isn't working: sudo apt install firmware-linux linux-firmware — then restart."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free up RAM — similar to iPadOS refreshing apps in the background. Try closing some programs you're not using."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "A hardware-level storage error. Install: sudo apt install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "pulseaudio-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to pulseaudio"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: pulseaudio --kill && pulseaudio --start — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — in a terminal."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "The printing service needs to be started: sudo systemctl start cups && sudo systemctl enable cups — then try printing again from your app."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check the network status icon in the top bar — or open Terminal and type: nmcli device status"
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Video or audio format not supported"
body = "Linux needs extra packages to play some media formats. On Ubuntu/Mint: sudo apt install ubuntu-restricted-extras — this adds support for MP3, MP4, and other common formats."

View file

@ -0,0 +1,102 @@
[meta]
source_os = "ipad"
target_distro_family = "fedora"
# iPad/iPhone user on their first Fedora install.
# Assumes NO terminal experience. Fedora GNOME is visually close to iPadOS.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "dnf-lock"
sources = ["journald"]
match_text = "Another app is currently holding the dnf lock"
severity = "warn"
title = "Software installer is busy"
body = "Fedora is doing automatic updates in the background — like iOS updating apps silently. Wait a minute and try again. You can see what's happening in GNOME Software (the App Store equivalent)."
[[patterns]]
id = "dnf-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict with each other"
body = "Two packages need something that can't coexist. Run: sudo dnf distro-sync — in a terminal to bring everything back into sync."
# ── SELinux ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "selinux-denial"
sources = ["journald"]
match_text = "type=AVC"
severity = "info"
title = "Security system blocked an action"
body = "Fedora uses SELinux — a detailed security system that controls what each program can access. Think of it as a much stricter version of iOS app permissions. This log entry is usually routine. If an app keeps failing, GNOME's 'Setroubleshoot' tool will pop up with an explanation and fix."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Some hardware needs a driver installed separately. Fedora usually handles this automatically, but if a device isn't working: sudo dnf install linux-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free RAM — like iPadOS refreshing background apps. Close unused programs."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Install: sudo dnf install smartmontools — then check: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — in a terminal."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check the network icon in the top bar, or: nmcli device status — in a terminal."
# ── Media ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "missing-codec"
sources = ["journald"]
match_text = "GStreamer: Failed to find plugin"
severity = "info"
title = "Video or audio format not supported"
body = "Fedora needs extra packages for some media formats. Enable RPM Fusion: sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm — then: sudo dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-ugly"

View file

@ -0,0 +1,111 @@
[meta]
source_os = "ipad"
target_distro_family = "opensuse"
# iPad/iPhone user on their first openSUSE install.
# YaST (openSUSE's graphical admin tool) is a good bridge for users
# unfamiliar with terminal-based system administration.
[log_paths]
steam = "~/.local/share/Steam/logs/content_log.txt"
proton = "~/.local/share/Steam/logs/proton_log.txt"
# ── Package management ────────────────────────────────────────────────────────
[[patterns]]
id = "zypper-lock"
sources = ["journald"]
match_text = "System management is locked"
severity = "warn"
title = "Software installer is busy"
body = "openSUSE's software manager is running — like iOS doing background updates. Wait a minute, or open YaST -> Software -> Software Management to see what's happening."
[[patterns]]
id = "zypper-dep-conflict"
sources = ["journald"]
match_text = "conflicts with"
severity = "warn"
title = "Two apps conflict"
body = "Run a full update: sudo zypper dup — this resolves most conflicts."
# ── AppArmor ──────────────────────────────────────────────────────────────────
[[patterns]]
id = "apparmor-denial"
sources = ["journald"]
match_text = "apparmor=\"DENIED\""
severity = "info"
title = "App blocked by security policy"
body = "openSUSE uses AppArmor for security — similar to how iOS isolates apps. Something was blocked. YaST -> Security -> AppArmor Configuration shows active profiles."
# ── YaST ──────────────────────────────────────────────────────────────────────
[[patterns]]
id = "yast-backend-fail"
sources = ["journald"]
match_text = "YaST got signal"
severity = "warn"
title = "Settings tool crashed"
body = "YaST (openSUSE's settings tool) crashed. Try running it from a terminal: sudo yast2 — the text mode is more stable than the graphical version."
# ── System ────────────────────────────────────────────────────────────────────
[[patterns]]
id = "kernel-driver-firmware"
sources = ["kmsg"]
match_text = "firmware: failed to load"
severity = "warn"
title = "Hardware driver file missing"
body = "Install the firmware package: sudo zypper install kernel-firmware — restart after."
[[patterns]]
id = "oom-killer"
sources = ["kmsg"]
match_text = "Out of memory: Kill process"
severity = "warn"
title = "System ran out of memory"
body = "Linux closed a program to free RAM. YaST -> System -> Partitioner can add or resize swap space (overflow RAM stored on disk)."
[[patterns]]
id = "disk-io-error"
sources = ["kmsg"]
match_text = "Buffer I/O error on device"
severity = "warn"
title = "Storage error"
body = "Install: sudo zypper install smartmontools — then: sudo smartctl -a /dev/sda"
# ── Audio ─────────────────────────────────────────────────────────────────────
[[patterns]]
id = "pipewire-connect-fail"
sources = ["journald"]
match_text = "Failed to connect to PipeWire"
severity = "warn"
title = "Sound system not responding"
body = "Restart audio: systemctl --user restart pipewire pipewire-pulse wireplumber — or log out and back in."
[[patterns]]
id = "bluetooth-rfkill-blocked"
sources = ["journald"]
match_text = "Blocked through rfkill"
severity = "warn"
title = "Bluetooth turned off by software"
body = "Run: rfkill unblock bluetooth — or use YaST -> Network -> Bluetooth."
[[patterns]]
id = "cups-server-error"
sources = ["journald"]
match_text = "Unable to connect to CUPS server"
severity = "info"
title = "Printer service not running"
body = "Start printing: sudo systemctl start cups && sudo systemctl enable cups — or use YaST -> Hardware -> Printer."
# ── Network ───────────────────────────────────────────────────────────────────
[[patterns]]
id = "networkmanager-activation-fail"
sources = ["journald"]
match_text = "Activation failed"
severity = "info"
title = "Wi-Fi connection failed"
body = "Check YaST -> Network Settings — or: nmcli device status — in a terminal."

View file

@ -29,12 +29,15 @@ pub fn complete_onboarding(
source_os: String, source_os: String,
distro: String, distro: String,
source_distro: Option<String>, source_distro: Option<String>,
dual_boot_with: Option<String>,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<(), String> { ) -> Result<(), String> {
let source = match source_os.to_lowercase().as_str() { let source = match source_os.to_lowercase().as_str() {
"macos" | "mac" => SourceOs::Macos, "macos" | "mac" => SourceOs::Macos,
"windows" => SourceOs::Windows, "windows" => SourceOs::Windows,
"linux" => SourceOs::Linux, "linux" => SourceOs::Linux,
"android" => SourceOs::Android,
"ipad" | "ios" | "ipados" => SourceOs::IpadOs,
_ => SourceOs::Unknown, _ => SourceOs::Unknown,
}; };
@ -44,18 +47,24 @@ pub fn complete_onboarding(
distro distro
}; };
// For Linux-to-Linux migrations, derive source family from the reported source distro. let source_distro_family = source_distro.as_deref().and_then(|sd| {
// If a source_distro was passed explicitly, use distro_family() on it; otherwise leave None.
let source_distro_family = source_distro.as_deref().map(|sd| {
let family = crate::distro::distro_family(sd); let family = crate::distro::distro_family(sd);
if family == "unknown" { None } else { Some(family.to_string()) } if family == "unknown" { None } else { Some(family.to_string()) }
}).flatten(); });
// Normalise dual_boot_with to a canonical name; reject unrecognised values.
let dual_boot_with = dual_boot_with.and_then(|s| match s.to_lowercase().as_str() {
"windows" => Some("windows".to_string()),
"macos" | "mac" => Some("macos".to_string()),
_ => None,
});
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.migration = Some(MigrationConfig { config.migration = Some(MigrationConfig {
source_os: source, source_os: source,
distro: detected, distro: detected,
source_distro_family, source_distro_family,
dual_boot_with,
fluency_level: 0, fluency_level: 0,
}); });
config.save().map_err(|e| e.to_string()) config.save().map_err(|e| e.to_string())

View file

@ -8,6 +8,8 @@ pub enum SourceOs {
Macos, Macos,
Windows, Windows,
Linux, Linux,
Android,
IpadOs,
Unknown, Unknown,
} }
@ -38,6 +40,11 @@ pub struct MigrationConfig {
/// Used to load a more specific pattern file (e.g. debian-to-arch) before the generic linux-to-arch fallback. /// Used to load a more specific pattern file (e.g. debian-to-arch) before the generic linux-to-arch fallback.
#[serde(default)] #[serde(default)]
pub source_distro_family: Option<String>, pub source_distro_family: Option<String>,
/// Co-installed OS for dual-boot setups, e.g. "windows", "macos".
/// When set, a supplementary pattern file (dualboot-windows.toml) is loaded on top of
/// the primary migration patterns to surface coexistence-specific issues.
#[serde(default)]
pub dual_boot_with: Option<String>,
/// 05: grows as user dismisses suggestions they already know /// 05: grows as user dismisses suggestions they already know
pub fluency_level: u8, pub fluency_level: u8,
} }
@ -143,6 +150,7 @@ mod tests {
source_os: SourceOs::Macos, source_os: SourceOs::Macos,
distro: "cachyos".into(), distro: "cachyos".into(),
source_distro_family: None, source_distro_family: None,
dual_boot_with: None,
fluency_level: 0, fluency_level: 0,
}); });
assert!(!config.needs_onboarding()); assert!(!config.needs_onboarding());
@ -155,6 +163,7 @@ mod tests {
source_os: SourceOs::Windows, source_os: SourceOs::Windows,
distro: "linuxmint".into(), distro: "linuxmint".into(),
source_distro_family: None, source_distro_family: None,
dual_boot_with: None,
fluency_level: 2, fluency_level: 2,
}), }),
..Default::default() ..Default::default()

View file

@ -40,9 +40,18 @@ pub fn run() {
config::SourceOs::Macos => "macos", config::SourceOs::Macos => "macos",
config::SourceOs::Windows => "windows", config::SourceOs::Windows => "windows",
config::SourceOs::Linux => "linux", config::SourceOs::Linux => "linux",
config::SourceOs::Android => "android",
config::SourceOs::IpadOs => "ipad",
config::SourceOs::Unknown => "unknown", config::SourceOs::Unknown => "unknown",
}; };
patterns::load(source, migration.source_distro_family.as_deref(), family).ok() let mut pf = patterns::load(source, migration.source_distro_family.as_deref(), family).ok();
// Layer dual-boot supplement patterns on top when a co-installed OS is configured.
if let (Some(ref mut primary), Some(ref dualboot)) = (&mut pf, &migration.dual_boot_with) {
if let Ok(supplement) = patterns::load_supplement(dualboot) {
primary.extend(supplement);
}
}
pf
} else { } else {
None None
}; };

View file

@ -87,6 +87,42 @@ pub fn load(
anyhow::bail!("pattern file not found for {source_os}-to-{distro_family}.toml") anyhow::bail!("pattern file not found for {source_os}-to-{distro_family}.toml")
} }
impl PatternFile {
/// Merge patterns and log_paths from `other` into this file.
/// Used to layer a dual-boot supplement on top of the primary pattern file.
pub fn extend(&mut self, other: PatternFile) {
self.patterns.extend(other.patterns);
for (k, v) in other.log_paths {
self.log_paths.entry(k).or_insert(v);
}
}
}
/// Load a supplementary pattern file by name (e.g. "windows" loads `dualboot-windows.toml`).
/// Supplement files cover coexistence-specific issues and are merged into the primary file.
pub fn load_supplement(name: &str) -> Result<PatternFile> {
let filename = format!("dualboot-{name}.toml");
let candidates = [
format!("patterns/{filename}"),
format!("src-tauri/patterns/{filename}"),
format!("/usr/share/robin/patterns/{filename}"),
];
for path in &candidates {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
match toml::from_str::<PatternFile>(&content) {
Ok(pf) => return Ok(pf),
Err(e) => {
log::warn!("patterns: failed to parse supplement {path}: {e}");
continue;
}
}
}
anyhow::bail!("supplement not found: {filename}")
}
#[must_use] #[must_use]
pub fn classify(event: &SystemEvent, pf: &PatternFile) -> Option<MatchedEvent> { pub fn classify(event: &SystemEvent, pf: &PatternFile) -> Option<MatchedEvent> {
for pattern in &pf.patterns { for pattern in &pf.patterns {

View file

@ -2,9 +2,11 @@
<div class="onboarding"> <div class="onboarding">
<div class="onboarding-card"> <div class="onboarding-card">
<div class="robin-logo">🐦</div> <div class="robin-logo">🐦</div>
<!-- Step 1: Source OS -->
<template v-if="step === 1">
<h1>Hi, I'm Robin.</h1> <h1>Hi, I'm Robin.</h1>
<p>I help you find your feet on Linux. Before we start what were you using before?</p> <p>I help you find your feet on Linux. Before we start what were you using before?</p>
<div class="os-choices"> <div class="os-choices">
<button <button
v-for="os in osOptions" v-for="os in osOptions"
@ -16,46 +18,134 @@
{{ os.label }} {{ os.label }}
</button> </button>
</div> </div>
<p class="hint">Robin uses this to explain things in terms you already know.</p> <p class="hint">Robin uses this to explain things in terms you already know.</p>
<button class="continue-btn" :disabled="!selectedOs" @click="advanceFromStep1">
Continue
</button>
</template>
<!-- Step 2: Source Linux distro (Linux-to-Linux only) -->
<template v-else-if="step === 2 && selectedOs === 'linux'">
<h1>Which distro were you on?</h1>
<p>Robin loads the right patterns for your background.</p>
<div class="os-choices">
<button <button
class="continue-btn" v-for="d in linuxDistroOptions"
:disabled="!selectedOs" :key="d.value"
@click="submit" class="os-btn"
:class="{ selected: selectedSourceDistro === d.value }"
@click="selectedSourceDistro = d.value"
> >
{{ d.label }}
</button>
</div>
<button class="continue-btn" @click="advanceFromLinuxDistro">
{{ selectedSourceDistro ? 'Continue' : 'Skip' }}
</button>
</template>
<!-- Step 3: Dual-boot question (Windows / macOS only) -->
<template v-else-if="step === 3">
<h1>Are you keeping {{ sourceOsLabel }} alongside Linux?</h1>
<p>Dual-boot setups have some extra quirks Robin can watch for like Windows locking shared drives.</p>
<div class="os-choices">
<button
class="os-btn"
:class="{ selected: dualBoot === true }"
@click="dualBoot = true"
>
Yes, dual-booting
</button>
<button
class="os-btn"
:class="{ selected: dualBoot === false }"
@click="dualBoot = false"
>
No, Linux only
</button>
</div>
<button class="continue-btn" :disabled="dualBoot === null" @click="submit">
Let's go Let's go
</button> </button>
</template>
<!-- Final step for OS types that skip step 3 -->
<template v-else-if="step === 'submit'">
<h1>All set.</h1>
<p>Robin is ready to help.</p>
</template>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
const emit = defineEmits<{ complete: [] }>() const emit = defineEmits<{ complete: [] }>()
type Step = 1 | 2 | 3 | 'submit'
const step = ref<Step>(1)
const selectedOs = ref('') const selectedOs = ref('')
const selectedSourceDistro = ref('')
const dualBoot = ref<boolean | null>(null)
const osOptions = [ const osOptions = [
{ value: 'windows', label: 'Windows' }, { value: 'windows', label: 'Windows' },
{ value: 'macos', label: 'macOS' }, { value: 'macos', label: 'macOS' },
{ value: 'linux', label: 'Another Linux distro' }, { value: 'linux', label: 'Another Linux distro' },
{ value: 'android', label: 'Android / ChromeOS' },
{ value: 'ipad', label: 'iPhone / iPad' },
] ]
async function submit() { const linuxDistroOptions = [
{ value: 'ubuntu', label: 'Debian / Ubuntu / Mint / Pop!_OS' },
{ value: 'fedora', label: 'Fedora / RHEL / CentOS' },
{ value: 'arch', label: 'Arch / Manjaro / EndeavourOS' },
{ value: 'opensuse', label: 'openSUSE' },
]
const sourceOsLabel = computed(() =>
osOptions.find(o => o.value === selectedOs.value)?.label ?? selectedOs.value
)
function advanceFromStep1() {
if (!selectedOs.value) return if (!selectedOs.value) return
if (selectedOs.value === 'linux') {
step.value = 2
} else if (selectedOs.value === 'windows' || selectedOs.value === 'macos') {
step.value = 3
} else {
// mobile users: no dual-boot question
submitOnboarding()
}
}
function advanceFromLinuxDistro() {
// Linux-to-Linux users don't get the dual-boot question
submitOnboarding()
}
async function submit() {
submitOnboarding()
}
async function submitOnboarding() {
const distro = await detectDistro() const distro = await detectDistro()
const sourceDistro = selectedSourceDistro.value || undefined
const dualBootWith = dualBoot.value === true ? selectedOs.value : undefined
await invoke('complete_onboarding', { await invoke('complete_onboarding', {
sourceOs: selectedOs.value, sourceOs: selectedOs.value,
distro, distro,
sourceDistro,
dualBootWith,
}) })
emit('complete') emit('complete')
} }
async function detectDistro(): Promise<string> { async function detectDistro(): Promise<string> {
// M1: read /etc/os-release via tauri-plugin-fs
// M0: return placeholder
return 'unknown' return 'unknown'
} }
</script> </script>
@ -70,7 +160,7 @@ async function detectDistro(): Promise<string> {
} }
.onboarding-card { .onboarding-card {
max-width: 320px; max-width: 340px;
text-align: center; text-align: center;
} }
@ -95,6 +185,7 @@ p { color: #aaa; line-height: 1.5; margin-bottom: 20px; }
color: #e0e0e0; color: #e0e0e0;
cursor: pointer; cursor: pointer;
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
text-align: left;
} }
.os-btn:hover { border-color: #6c8ebf; } .os-btn:hover { border-color: #6c8ebf; }