project-switch

A multi-plat app launcher written in Rust · source · devlog

  • v0.15.2 - Error logging and argument handling fixes

    I added error logging to a file and a GUI error dialog so that when something goes wrong in the launcher, there’s both a persistent log and a visible notification rather than a silent failure.

    I also fixed two command execution bugs: commands were being launched through wt.exe (Windows Terminal) when they should have gone through cmd, and the GUI launcher was dropping user-supplied arguments when opening URL commands. Both are now working correctly.

  • v0.15.1 - Monitor positioning and selection fix

    I fixed the launcher window appearing on the wrong monitor — it now reads monitor configuration and positions itself on the correct display. The hotkey config gained a monitor selection option so multi-monitor setups work predictably.

    I also fixed a frustrating bug where pressing Enter always opened the first item in the list regardless of arrow-key selection. The selected item now correctly responds to keyboard navigation.

  • v0.15 - History tracking and recent actions

    I added a history system to project-switch that records executed actions, calculator expressions, and file path navigations. When opening the launcher, recently used items now appear at the top of the list so frequently accessed projects and commands are just a keystroke away.

    I also fixed the half-second delay between pressing the hotkey and the GUI list appearing — the hotkey listener now communicates with the GUI process more directly. Calculator mode got an improvement too: incomplete expressions like =5+ no longer show an error, they just display nothing until the expression is valid.

  • v0.14.2 - Reliable focus stealing on hotkey

    I fixed the launcher window not reliably getting focus when activated via the hotkey on Windows. The window state machine now handles focus requests more aggressively to ensure the input field is ready for typing immediately after the hotkey press.

    Also tidied up the build script to clean individual binary files rather than entire directories, avoiding accidental deletion of other build artefacts.

  • v0.14.1 - macOS support and focus handling

    I spent the day getting the new native UI working on macOS. The hotkey service now launches the GUI directly on macOS instead of going through a terminal intermediary, and I configured both the hotkey and GUI processes to hide their Dock icons so they run as background-only apps.

    I also fixed the window hiding on focus loss — it was crashing in certain edge cases, so I added a safer approach. On the Windows side, I fixed URL opening from the UI when there’s no exact match in the project list, so typed-in URLs now open correctly in the browser.

  • v0.14 - Native UI launcher with egui

    Big day — I built out the native GUI launcher for project-switch using egui/eframe. The work was orchestrated through Conductor across three phases.

    Phase 1 evaluated UI frameworks (settling on egui over iced), defined the merged binary crate structure, and extracted a shared data model from the existing list command into a reusable launcher module. I also wrote state machine tests for the window lifecycle.

    Phase 2 implemented the actual launcher window — a filterable list of project commands that pops up on hotkey press. I added a calculator mode (prefix input with =), file path browsing with directory navigation, and action execution for opening URLs, running commands, and launching apps.

    Phase 3 polished the rough edges: fixing path entry behaviour, showing both “open” and “browse” options for directories, deduplicating path input, launching terminal commands through wt.exe, and tweaking the app colour scheme. Finally, I merged the hotkey daemon into the main binary so there’s now a single project-switch executable that handles both the CLI and the background hotkey service with system tray icon.

  • v0.13 - Hotkey trampoline and URL argument fix

    I added a trampoline mechanism to the hotkey service so that on startup it copies itself to LOCALAPPDATA and re-launches from there. This means the binary can be updated in-place without the running process holding a lock on the original file — useful for keeping the auto-start shortcut pointing at a stable path.

    I also fixed an issue where forward slashes in URL arguments were being stripped when launching commands. The list command now correctly preserves slashes so URLs with paths pass through intact.

    On the planning side, I set up Conductor — an AI agent orchestration framework — with code styleguides, product guidelines, and a spec for a native UI track that would merge the hotkey service and launcher into a single windowed process.

  • v0.12 - Terminal command field for project commands

    Added a command field to project commands as an alternative to url. Previously, if you wanted to run a terminal command like cargo build --release from the project switcher, you had to use the url field and hope no browser was configured — which was confusing and fragile. Now there’s explicit intent: command always runs in the terminal, url always opens in a browser.

    The config validation enforces mutual exclusivity — you can’t set both url and command on the same entry, and command can’t be paired with browser since that wouldn’t make sense. The args field works with both: for command entries it appends to the shell invocation, for url entries it gets URL-encoded and appended to the URL. Also extracted a merge_args helper to deduplicate the argument merging logic that was repeated in a few places.

  • v0.11.1 - Auto-encode user args in URLs

    Fixed URL encoding for user-supplied arguments on browser commands. Yesterday’s refactor removed the url_encode config field but didn’t replace it with automatic encoding. Now when a command opens in a browser and the user provides arguments (e.g. a search query), those args are automatically URL-encoded with urlencoding::encode before being appended to the URL. This means the search global command just works — type your query and spaces get encoded as %20 without needing any config flag.

  • v0.11 - Browser args rework and debug flag

    Reworked how command arguments are handled when launching browsers. Previously, args on a command were appended to the URL (with an optional url_encode flag), which was a confusing overload. Now args are treated as browser flags passed directly to the browser process. This is a cleaner model — if a command has a browser configured, the args go to the browser; if it’s a terminal command, the args go to the shell. Removed the url_encode field and the urlencoding dependency since they’re no longer needed.

    Added #[serde(deny_unknown_fields)] to the config structs (Config, Project, ProjectCommand) so typos or stale fields in YAML config files produce clear errors instead of being silently ignored.

    Also added a --debug flag to the list command that prints the full resolved command before executing it — handy for troubleshooting when a command isn’t launching the way you expect. And cleaned up main.rs to print errors with a “Press Enter to exit” prompt instead of returning Result from main, which gives better feedback when running in Windows Terminal where the window would otherwise close immediately.

  • v0.10.1 - URL space encoding fix for Windows

    Fixed a bug where URLs containing spaces would break when opened in a browser on Windows. PowerShell’s Start-Process was splitting the URL at space boundaries, so the browser would only receive a truncated URL. The fix encodes spaces as %20 before passing them through, and also switches the argument format to use PowerShell array syntax (@()) instead of a single quoted string — this ensures browser arguments and the URL are passed as distinct parameters rather than getting concatenated together.

  • v0.10 - Inline math calculator

    Added an inline math calculator to the list command. Typing = followed by a math expression (like =5+3*2) evaluates it live in the autocomplete suggestions using meval. Integer results display without decimal places, and invalid expressions show an error inline. It’s a small quality-of-life feature that saves opening a separate calculator.

    Also fixed a CRLF line ending issue that was breaking the Docker build on Linux, added a .gitattributes to enforce LF endings, and deduplicated shortcuts that appeared multiple times from overlapping scan directories.

  • v0.9 - Cross-platform hotkey and macOS support

    Made the hotkey listener cross-platform — it now works on both macOS and Windows. On macOS, it uses Core Graphics event taps to listen for the ALT+SPACE combination and launches project-switch list in a new Terminal window. The platform-specific code is cleanly separated into platform/macos.rs and platform/windows.rs modules behind conditional compilation.

    Added macOS .app bundle scanning to the shortcuts feature. It scans /Applications and ~/Applications for .app bundles and surfaces them alongside Windows .lnk shortcuts in the autocomplete, using open -a to launch them.

    Fixed the hotkey to fire only on key press (not release), cleaned up the Docker build pipeline by removing bash as a dependency, and added cargo registry volume caching for the lint container.

  • v0.8.1 - Cross-compilation fixes and config auto-create

    Fixed the Docker cross-compilation pipeline to work on ARM hosts (like Apple Silicon Macs). The Dockerfile now uses conditional platform targeting so it can build Windows and Linux x86_64 binaries regardless of the host architecture. Added a build.sh script for non-Windows environments that skips the lint Docker image.

    Also added auto-creation of the config file — if ~/.project-switch.yml doesn’t exist when the program starts, it creates one with sensible defaults instead of erroring. Added cargo registry caching in the Docker build to speed up repeated builds.

  • v0.8 - File path autocomplete and fixes

    Added file path autocomplete to the list command. When you start typing a Windows path (drive letter like C:\ or a UNC path), the autocomplete switches to browsing the filesystem instead of matching commands. It auto-expands when there’s a single directory match and displays directories and files with distinct styling. This makes project-switch usable as a quick file/folder opener too.

    Fixed multi-word shortcut selection — shortcuts with spaces in their names weren’t being matched properly in the autocomplete. Also added a fallback so the list command works even without a current project set, showing only global commands and shortcuts.

    On the hotkey side, added logic to kill any prior project-switch-hotkey.exe instances on startup so you don’t end up with duplicate listeners, and the hotkey now creates a default config file if one doesn’t exist.

  • v0.7 - Config includes, system tray, and shortcut indexing

    Packed day with three big features landing.

    First, added config file includes. The main ~/.project-switch.yml can now specify an include path pointing to another YAML file, and the two get merged at load time. Project-specific commands overlay base commands by key, and projects merge by name. This lets me keep a shared config in a git repo and machine-specific overrides locally.

    Building on that, the hotkey listener now auto-syncs the included config’s git repo in the background — it does a git pull on startup and periodically, then stages, commits, and pushes any local changes. All git operations run with CREATE_NO_WINDOW so nothing flashes on screen.

    Second, migrated the hotkey listener from a plain console app to a proper Windows system tray application using tray-icon and muda. It shows a notification area icon with a right-click “Exit” menu, and runs with #![windows_subsystem = "windows"] so there’s no console window. Generated a simple icon using a Python script with Pillow.

    Third, added Windows shortcut (.lnk) indexing. The list command now scans configured shortcut directories and surfaces .lnk files alongside regular commands in the autocomplete. This means Start Menu shortcuts and other Windows shortcuts are searchable from the same prompt.

    Also added cargo fmt, clippy, and cargo audit as verification checks, committed Cargo.lock for reproducible builds, and wired the build script to restart the hotkey listener after a successful build.

  • v0.6 - ALT+SPACE hotkey listener

    Built a companion binary, project-switch-hotkey, that runs in the background on Windows and listens for a global ALT+SPACE hotkey. When triggered, it kills any running project-switch.exe instances and launches project-switch list in a new Windows Terminal tab. The idea is to make project switching feel as instant as an app launcher — press the hotkey from anywhere and you’re immediately in the selection prompt.

    The hotkey registration uses the Windows RegisterHotKey API with MOD_ALT | MOD_NOREPEAT, and the message loop polls with PeekMessageW on a 50ms sleep. It handles graceful shutdown via ctrlc and wraps the launched command in cmd /c ... & exit /b 0 so the terminal tab closes cleanly even when taskkill force-kills a prior instance.

    It’s a standalone crate under hotkey/ (not a workspace member) and gets cross-compiled through the existing Docker pipeline alongside the main binary.

  • v0.5 - Direct URL opening support

    Added the ability to type a URL directly into the list prompt or pass one to open, without needing it to be a configured command. If the input doesn’t match any command key but looks like a URL (starts with http, www., or matches a domain.tld pattern), it gets opened in the project’s configured browser. Bare domains like example.com automatically get an https:// prefix.

    Also deprecated the open command in favour of list — it’s now hidden from help output. The open command still works but includes the same URL fallback behaviour for backwards compatibility.

  • v0.4.1 - Autocomplete fixes and YAML preservation

    Fixed a couple of annoying issues with the list command’s autocomplete. Selecting a suggestion was inserting the full formatted string including ANSI colour codes and the URL arrow, so I added an ANSI stripping function and taught the completion handler to extract just the command key from highlighted suggestions. Also fixed input parsing to handle cases where the formatted suggestion text leaked into the user’s input.

    The other fix was around YAML field ordering. Previously, saving the config would re-serialise the entire struct which shuffled fields into alphabetical order — not great when you’ve carefully organised your config file. Now the config manager preserves the raw YAML on load and only patches the changed fields (currentProject, projects) back into it on save, keeping everything else untouched.

    Added skip_serializing_if annotations across the config structs so None fields and false defaults don’t clutter the output.

  • v0.4 - Command args, global commands, and browser profiles

    Big batch of enhancements to project-switch today. Added support for command arguments — you can now type extra text after a command key and it gets appended to the URL (with optional URL encoding via a url_encode config flag). This is great for search URLs where you want to pass a query string directly from the prompt.

    Introduced global commands in the config file. These are available across all projects and show up alongside project-specific commands in the list view, with project commands taking priority over globals when there’s a key collision.

    Browser profiles are now supported too. The browser string in the config can include arguments like firefox -P work, and the launcher correctly parses and passes those through on Windows, macOS, and Linux.

    Also reworked the list command to use inquire’s autocomplete instead of a select menu, so you can type to filter commands and append arguments inline. Commands that aren’t URLs are now treated as terminal commands and spawned directly in the shell.

  • v0.3 - List command and build script

    Added a list subcommand that shows an interactive menu of all openable items from the current project. It displays each command’s key alongside a truncated URL, and opens the selected item in the configured browser. This makes it much quicker to jump to project resources compared to remembering exact command keys for open.

    Extracted browser-opening logic into a shared utils::browser module since both open and list need it. Also fixed the browser launch on Windows — switched from cmd /C start to PowerShell’s Start-Process, which handles URLs with special characters more reliably.

    Created a build.ps1 script that stops any running project-switch.exe processes, cleans the bin/ folder, and runs the Docker cross-compilation build. Cleaned up docker-compose.yml to remove unused profiles and volumes.

  • v0.2 - Rewriting project-switch in Rust

    Rewrote project-switch entirely from TypeScript to Rust. The Node.js version had noticeable startup latency (~100-200ms) which felt sluggish for something that should be instant. The Rust version starts in under 5ms.

    Ported all four subcommands (switch, add, current, open) to Rust using clap for argument parsing, serde_yaml for config file handling, and inquire for interactive prompts. The config format stays the same — ~/.project-switch.yml with projects, commands, and browser overrides — so it’s a drop-in replacement.

    Set up a Docker-based cross-compilation pipeline that builds both Windows and Linux binaries from a single docker-compose up build command, using gcc-mingw-w64 for the Windows target.

  • v0.1 - Project switch genesis

    Started project-switch today — a CLI tool for quickly switching between project directories. I’ve been wanting a way to jump between repos without hunting through nested folders, and this scratches that itch.

    Built it as a TypeScript project with three subcommands: switch for fuzzy-matching a project name and changing to its directory, add for registering new projects interactively, and open for launching a project directly in VS Code. Configuration lives in a YAML file at ~/.project-switch.yml where each project has a name, path, and optional tags.

    The switch command presents an interactive list using enquirer when there are multiple matches, and jumps straight there on an exact match. The add command walks through prompts for name, path, and tags, with the path defaulting to the current directory. Spent a bit of time fixing a path resolution bug for the config file location.