Versi is a native GUI application for managing Node.js versions. It currently uses fnm (Fast Node Manager) as its backend, but the architecture is backend-agnostic — adding a new backend (e.g., nvm, volta) requires only implementing the BackendProvider and VersionManager traits.
- Language: Rust (2021 edition)
- GUI Framework: Iced 0.13 (Elm architecture)
- Async Runtime: Tokio
- Build System: Cargo workspace
versi/
├── Cargo.toml # Workspace root
├── crates/
│ ├── versi/ # Main GUI application
│ │ └── src/
│ │ ├── main.rs # Entry point
│ │ ├── app/ # Iced Application implementation (mod.rs + handler modules)
│ │ │ ├── mod.rs # Main update loop, Versi struct, message dispatch
│ │ │ ├── init.rs # Initialization and backend detection
│ │ │ ├── environment.rs # Environment switching and loading
│ │ │ ├── onboarding.rs # Onboarding flow handlers
│ │ │ ├── operations.rs # Install/uninstall/set-default operations
│ │ │ ├── shell.rs # Shell configuration handlers
│ │ │ ├── versions.rs # Remote version fetching and update checks
│ │ │ ├── tray_handlers.rs # System tray event handlers
│ │ │ └── platform.rs # Platform-specific helpers
│ │ ├── message.rs # Message enum (Elm-style)
│ │ ├── state.rs # Application state structs
│ │ ├── theme.rs # Light/dark themes and styles
│ │ ├── settings.rs # User settings persistence
│ │ ├── logging.rs # Debug log file management
│ │ ├── tray.rs # System tray integration
│ │ ├── single_instance.rs # Single-instance enforcement
│ │ ├── views/ # UI views (main_view, settings_view, onboarding, loading, about)
│ │ └── widgets/ # Custom widgets (version_list, toast_container)
│ ├── versi-backend/ # Abstract backend traits and types
│ │ └── src/
│ │ ├── traits.rs # BackendProvider, VersionManager, BackendDetection, BackendUpdate
│ │ ├── types.rs # Shared types (NodeVersion, InstalledVersion, RemoteVersion, etc.)
│ │ ├── error.rs # BackendError type
│ │ └── lib.rs # Re-exports
│ ├── versi-core/ # Shared utilities (release schedule, app updates, HideWindow)
│ │ └── src/
│ │ ├── schedule.rs # Node.js release schedule fetching
│ │ ├── update.rs # App update checking, GitHubRelease, version comparison
│ │ └── commands/mod.rs # HideWindow trait + impls
│ ├── versi-fnm/ # fnm backend implementation
│ │ └── src/
│ │ ├── provider.rs # FnmProvider - implements BackendProvider
│ │ ├── backend.rs # FnmBackend - implements VersionManager
│ │ ├── client.rs # FnmClient - CLI command execution
│ │ ├── version.rs # Version parsing
│ │ ├── progress.rs # Install progress tracking
│ │ ├── detection.rs # fnm binary detection
│ │ ├── update.rs # fnm update checking
│ │ └── error.rs # Error types
│ ├── versi-shell/ # Shell detection & configuration (backend-agnostic)
│ │ └── src/
│ │ ├── detect.rs # Shell detection
│ │ ├── config.rs # Config file editing (parameterized on marker/label)
│ │ ├── shells/ # Shell-specific implementations
│ │ └── verify.rs # Configuration verification (parameterized on marker/backend_binary)
│ └── versi-platform/ # Platform abstractions
│ └── src/
│ ├── paths.rs # Platform-native paths
│ ├── environment.rs # Environment abstraction
│ └── wsl.rs # WSL distro detection (Windows)
The application follows Iced's Elm-style architecture:
- State (
state.rs): Immutable application state - Message (
message.rs): Events that can modify state - Update (
app.rs): Handles messages and returns new state + tasks - View (
views/): Pure functions that render state to UI
- Tasks: Async operations return
Task<Message>for side effects - Subscriptions: Time-based events (tick for toast timeouts)
- Theming: Dynamic light/dark themes based on system preference
- Operation Queue: Installs run concurrently (
active_installs: Vec<Operation>), while uninstall and set-default are exclusive (exclusive_op: Option<Operation>). Pending operations are queued and drained when capacity is available. - System Tray: Optional background tray icon with version switching support
- Backend Abstraction: The
Versistruct holds anArc<dyn BackendProvider>which provides all backend-specific behavior. The GUI, shell, and platform crates have no direct dependency on any concrete backend.
# Build the project
cargo build
# Run the application
cargo run
# Run with release optimizations
cargo build --release
# Check for errors without building
cargo check
# Run tests
cargo test
# Format code
cargo fmt
# Lint code
cargo clippy- Follow standard Rust conventions (rustfmt)
- Use
thiserrorfor error types - Prefer
async/awaitover callbacks - Keep view functions pure (no side effects)
- Use meaningful message names that describe the event
- Group related functionality into separate crates
- Toasts are only for background errors. Never use toasts (
Toast::error) for feedback that can be shown reactively in the UI (disabled buttons, inline text, tooltips, etc.). Toasts are reserved for errors from async background operations where no other UI surface exists to report the failure (e.g., install failed, uninstall failed, set-default failed). - Prefer disabled states with tooltips, inline status text, or view changes over transient notifications.
crates/versi/src/app/mod.rs- Main application logic and message dispatchcrates/versi/src/state.rs- All state types and their relationshipscrates/versi/src/message.rs- All possible application eventscrates/versi-backend/src/traits.rs-BackendProviderandVersionManagertrait definitionscrates/versi-fnm/src/provider.rs-FnmProvider(concrete backend implementation)
- Add new message variant(s) to
message.rs - Add state fields to
state.rsif needed - Handle message in
app.rsupdate function - Update view in appropriate
views/file
- Create a new crate (e.g.,
versi-volta), followingversi-fnmas a reference - Implement
BackendProvidertrait (detection, installation, update checking) - Implement
VersionManagertrait (list installed/remote, install, uninstall, set default) - Wire the new provider into
Versi::new()inapp/mod.rs
- Add method to
FnmBackendinversi-fnm/src/backend.rs - Add any new types to
versi-backend/src/types.rsif they're shared, orversi-fnm/src/version.rsif fnm-specific - Expose via the
VersionManagertrait if applicable - Create corresponding message and handler in versi
- All styles are in
crates/versi/src/theme.rs - Light/dark palettes defined at the top
- Button and container styles as functions
- Unit tests should be in the same file as the code
- Integration tests in
tests/directory - Test backend interactions with mock or real backend installation
Key external crates:
iced- GUI frameworktokio- Async runtimereqwest- HTTP client (for release schedule)serde- Serializationopen- Opening URLs in browserdirs- Platform directorieswhich- Finding executables
Settings Location:
- macOS:
~/Library/Application Support/versi/ - Windows:
%APPDATA%/versi/ - Linux:
~/.config/versi/(XDG-compliant)
Cached Data:
- Available Node versions list (fetched from nodejs.org)
- Node.js release schedule (from GitHub)
The GUI interacts with backends exclusively through the BackendProvider and VersionManager traits defined in versi-backend. The current backend (fnm) executes CLI commands as subprocesses via FnmClient.
- All operations run as async tasks, keeping the UI responsive via Iced's
Tasksystem - Parse stdout/stderr for status and results
- Multiple installs can run concurrently; uninstall and set-default wait for all installs to finish
Key fnm commands used (in versi-fnm):
fnm list- Get installed versionsfnm list-remote- Get available versionsfnm install <version>- Install a versionfnm uninstall <version>- Remove a versionfnm default <version>- Set default versionfnm current- Get currently active version
- Primary development target
- Native ARM64 and x64 binaries
- Uses
dark-lightcrate for system theme detection
- Native Windows binary
- Support for PowerShell shell configuration
- WSL integration via
wsl.exefor multi-environment support
- Accessed via Windows app's multi-environment support
- Lists all WSL distros via
wsl.exe --list --verbose - Separately checks which distros are running via
wsl.exe --list --running --quiet - Only checks for the backend in running distros (avoids booting non-running distros)
- Detects backend binary path by checking common locations (
~/.local/share/fnm/fnm,~/.cargo/bin/fnm, etc.) - Shows all distros as tabs; non-running or backend-less distros appear disabled with reason
- Commands executed directly via
wsl.exe -d <distro> /path/to/backend ...(no shell needed) - Shell detection in settings is environment-aware: shows Linux shells (bash/zsh/fish) for WSL environments
- Native x64 and ARM64 binaries
- XDG-compliant paths
- Support for bash, zsh, fish shells