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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Integrity/hardening notes: a v6 segment's plaintext is verified against its inde

- Standard WPF MVVM. `App.xaml.cs` orchestrates startup/shutdown; specific concerns delegated to `Services/` classes (`TrayIconController`, `TrayTooltipController`, `DiskNotificationService`, `TempDirCompatChecker`, `SessionEndingSaveHandler`, `UpdateCheckService`, `GlobalMountCoordinator`, `ShellContextMenuManager`).
- Disk operations (`Mount`/`Unmount`/`Save`/`Dispose`) dispatched via `Task.Run` to keep UI responsive.
- Startup splash: `App_Startup` checks WinFsp, then shows `SplashWindow` before `StartUiAsync` (also with `StartMinimized`; `ShowActivated=False` so it does not take focus; on top only for `SplashTopmostState.StayOnTopFor`, never over a dialog from the tray (`ShowMainWindow` releases it); it has a taskbar button so a buried splash can be brought back, except when starting minimized) and keeps the main window hidden while `AutoMountDisksAsync` loads the disks (progress and "Loading disk x of y" go to the splash instead of the busy overlay). `CloseSplashAndShowMainWindowAsync` (ordering in the pure, tested `SplashLifecycle`: at least `SplashPolicy.MinimumDisplay`, then close) closes it and shows the main window unless `StartMinimized`; it also runs early when a disk needs a password, so the prompt isn't hidden behind the splash, and the busy overlay takes over. The splash also has random background circles (`SplashAnimationPlanner`) and a scrolling feature list (`Splash.Feature{n}` strings, `SplashTicker`; add a feature by adding the next number in both language files). `ShowSplashAsync` switches `ShutdownMode` to `OnExplicitShutdown` only while the splash is the sole window (`CloseSplashAndShowMainWindowAsync` restores `OnLastWindowClose`), and `StartUi` sets `MainWindow` explicitly (the splash is the first window created) — keep both.
- Disk presets: `DiskPreset`/`BuiltInPresets`/`PresetComposer`/`EnvRedirectPolicy` (`Core/DiskCreation`, pure) describe combinable templates; a disk card's context-menu "Presets" submenu (filled in `MainWindow.DiskContextMenu_Opened`) ticks/unticks presets live through `MainViewModel.GetActivePresetIds`/`SetPresetAsync`; the edit dialog ticks the presets a disk already has (`PresetSelection.Detect`), where for variable-redirecting presets the options are first reconciled with what the user's environment really points at (`MainViewModel.WithEnvironmentPresets` -> `PresetSelection.Reconcile`/`UserEnvironmentRedirector.PointsInto`) and ticking/unticking only changes folders and variables, keeping what no preset owns (`PresetSelection.Apply`); a live (no-remount) edit syncs the mounted disk via `DiskEffectsDiff` and `MainViewModel.SyncDiskEffectsAsync` (new variables applied, dropped ones restored, dropped folders left alone); a preset that redirects variables (`PresetSelection.IsExclusive`; not the folder-only browser one) lives on one disk only: creating or editing a disk to have it takes it from the others first (`MainViewModel.ReleaseClaimedPresetsAsync` -> `PresetSelection.Release`, variables restored, folders kept, unmounted profiles trimmed too), whereas auto-mount never takes over and still rejects a variable another disk owns; `DiskProfile` saves a disk's variable-redirecting presets as ids only (`DiskProfile.PresetIds`, via `PresetSelection.Split`/`Expand` in `ToProfile`/`ProfileToOptions`; older expanded profiles still load and are re-saved as ids), while the runtime `DiskOptions` keeps the expanded `Folders`/`EnvRedirects`; a disk keeps the result in `DiskOptions.Folders`/`EnvRedirects` (mirrored in `DiskProfile`, and `MainViewModel.ToProfile`/`ProfileToOptions` must carry any new field). A disk without an image is empty on every mount, so `MainViewModel.AddDiskSorted` runs `UserEnvironmentRedirector.ApplyDiskEffects` after each mount (creates the folders, points the per-user environment variables into the disk). The redirector stores each variable's previous registry value and kind in `EnvRedirectBackupStore` (`env-redirects.json` next to `settings.json`, so a damaged settings file cannot lose it) *before* writing, restores it when the disk leaves `Disks`, on exit (`RestoreAllEnvRedirects` in `ShutdownAsync`, and on Windows logoff/shutdown via `SessionEndingSaveHandler`'s `beforeSave` hook, which also resets TEMP and skips the broadcast) and for leftovers after a crash (`RestoreDanglingEnvRedirects` at startup, before the auto-mount); a variable is only restored while it still holds the value we wrote, and a variable already owned by another disk is refused. TEMP/TMP are ordinary redirects of the temp preset (`EnvRedirectPolicy` no longer reserves them), so they get the same backup/restore, and the context-menu "set as temp" action applies the same redirects; `MainViewModel.RestoreUserTemp` restores them from the backup and falls back to `TempDirResetService.Reset` (Windows defaults) only when nothing was recorded, e.g. a TEMP set by an older version. Ticking a temp-redirecting preset in the create dialog asks the one-time compatibility warning (`ConfirmTempDirWarning`). `IUserEnvironment` is the registry seam for tests. The `WM_SETTINGCHANGE` broadcast waits on every top-level window, so `TempDirResetService` sends it through `CoalescingBroadcaster` on a background thread (merged requests, `WaitForPendingBroadcasts` at exit) instead of on the caller's thread. Browsers have no cache environment variable, so that preset only creates a folder. The user-triggered restore (toolbar menu and tray submenu, `MainViewModel.GetEnvRestoreGroups`/`RestoreEnvAsync`) is a second path on the same backups: `EnvRestoreGroups` (Core, pure) groups the redirected variables by preset, `EnvRestoreService` restores one group (TEMP/TMP through `RestoreRecordedTemp`, which also covers a legacy TEMP without backup), and `DiskRestoreCoordinator` plus `DiskRestorePlanner` (Core, pure) make the mounted disks agree afterwards: before the restore it notes which presets/custom redirects point into each disk (`Plan`), afterwards it keeps only what really stopped pointing there (`Settle`, so a partly failed restore never leaves a disk claiming less than the environment backs) and drops it from the disk's options (`Apply`, same release as unticking the preset, folders on the disk stay). Call `DiskRestoreCoordinator.RunAsync`, not `Plan`/`TrimAsync` directly: it plans, runs the restore, trims even when the restore throws midway (some variables may already be back) and then rethrows (as an `AggregateException` with the report failure if the after-trim callback fails too). It runs on the UI thread under the busy overlay; `EnvRestoreMenuEntry` lays out the menu rows for both the window and the tray (`EnvRestoreTrayAction`). Keep these in step with the preset and backup rules above.
- Notifications: `DiskNotificationService` turns `DiskViewModel` events into status-bar text and tray balloons (balloons only while the main window is hidden). Besides high usage and save failures it reports refused writes: `MemoryFileSystem` raises `WriteRejected` (`WriteRejectionReason.LowMemory`/`DiskFull`, only on the refusal paths, at most once a second per reason) which `RamDisk` and `DiskViewModel` forward to the UI thread; the balloon is limited by `NotificationCooldown` (10 minutes per disk and reason, spent only when a balloon is actually shown). A 10 s poll feeds `LowMemoryMonitor` (pure; warns under 512 MB, recovers above 768 MB, 10-minute warning cooldown) so the user hears about low memory before the write guard starts refusing. `DiskViewModel.SaveRecovered` clears the sticky save-failure status once a save succeeds again. The status-bar file name is throttled to 500 ms (`DiskViewModel.ActivityThrottleWindow`).
- Self-update: `UpdateCheckService` (check) -> `UpdateDialog` (prompt/progress) -> `UpdateInstaller` (download via GitHubReleaseUpdater into `%LOCALAPPDATA%\ManagedDrive\updates`, never `%TEMP%`, then start the Inno Setup installer with `/SILENT`). The app does not exit itself: `installer/ManagedDrive.iss` asks the running app to exit via `mdrive.exe exit` (saves disks) and restarts it afterwards. A TEMP on a RAM disk is reset first because the installer aborts silently in that case. Generic download/verify/launch logic lives in the separate GitHubReleaseUpdater library; keep only ManagedDrive-specific policy here.
Expand Down
Loading
Loading