project-switch

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

  • v0.20.1 - Restoring the macOS menu bar for the webview

    The macOS webview window had a subtle but irritating problem: because it runs from a bare binary that tao promotes to a foreground app at startup, it had no main menu — and a regular macOS app with no main menu leaves the left side of the system menu bar dead, so the Apple menu and the app menu didn’t respond to clicks. Worse, without an Edit menu the standard Cmd+X/C/V/A shortcuts didn’t work inside the page either.

    The fix installs a minimal main menu when NSApplication finishes launching (the same StartCause::Init moment I already use to set the dock icon, since anything done earlier is discarded by the promotion). It’s just two menus: an app menu with Quit, and an Edit menu wiring Cut/Copy/Paste/Select All to their standard selectors — which both revives the menu bar and restores clipboard shortcuts in the webview. This pulled in the NSMenu/NSMenuItem features from objc2-app-kit.

  • v0.20 - Webview polish and tighter webserver scoping

    A day of sanding down the webview window and the tray-managed webserver.

    The webview gained two conveniences. It now titles itself by the command that opened it on macOS — summon_or_open threads the command key through to a new --title flag, so a window opened from, say, a dashboard command reads “dashboard” instead of the generic constant. Windows deliberately ignores the title: the constant doubles as the handle that FindWindowW uses to find and reuse the single window, so renaming it would break window reuse. (A small follow-up fix renamed a shadowed local there so the title parameter wasn’t accidentally swallowed.) The webview also reloads on Cmd+R / Ctrl+R now, via an injected key handler — which neatly doubles as a retry button, since calling location.reload() on WKWebView’s or WebView2’s own error page re-requests the URL that failed.

    Then external links. On macOS, WKWebView doesn’t route target=_blank anchor clicks or window.open calls through wry’s navigation handlers, so links in the page — like the URLs the assist terminal prints — simply did nothing. An injected LINK_SCRIPT now captures both, forwards external http(s) URLs to the host over IPC, and lets same-origin links navigate normally; the host opens them in the default browser.

    On the webserver side, two scoping fixes. Stopping the server used to pkill -f on the command string, which would also catch unrelated assist/claude processes whose arguments happened to contain that substring. It now identifies the server by the port it listens on (lsof -ti tcp:3100) and waits, polling, until it’s actually gone. I also discovered the macOS server needed an interactive login shell (-ilc, not just -lc): version managers like fnm initialise in .zshrc, which a non-interactive shell never sources, so the server was resolving the wrong Node and sometimes couldn’t find assist on PATH at all. Finally, the hotkey service’s own process matching switched from pkill -f / pgrep -f to the exact-name -x form, so it stops matching unrelated command lines.

  • v0.19 - Real logo icons, and macOS parity for the webview and webserver

    The visible change today was getting rid of the hand-drawn “PS” square I’d been using as an icon. The tray and menu-bar icon now decode a bundled logo-tray.png (same artwork on both platforms), and I pulled all the icon handling into a shared src/icon.rs so the launcher window, the webview window, and the tray all draw from one place. On macOS the dock gets a squircle-masked variant of the full logo.png: the logo is inset on a transparent canvas at Apple’s icon grid ratio (~80.5% of the tile) with its corners rounded into the system squircle, premultiplied so the corner feathering reads correctly through AppKit’s bitmap initializer. The result sits at the same scale and shape as native dock icons instead of looking like a bare executable.

    Two follow-on fixes gave the windows their icons too: neither egui’s launcher window nor tao’s webview window adopts the exe’s embedded icon on its own, so the Windows taskbar was showing the default executable mark. Both now set the app logo explicitly via with_icon / with_window_icon.

    The larger piece of work was bringing macOS up to parity with Windows. The tray-managed assist webserver is now cross-platform: I collapsed the old Windows-only implementation and the macOS no-op stubs into a single hotkey/src/webserver.rs, with only the command construction varying by platform — Windows runs it inside WSL via wsl.exe -- bash -lc, while macOS runs it natively through the user’s login shell ($SHELL -lc), since there’s no WSL to go through. Logs land in ~/Library/Logs/project-switch on macOS and %LOCALAPPDATA% on Windows. The webview window got the same treatment, so the borderless in-app browser works on macOS as well.

    One macOS build gotcha worth recording: the cross-compiled (zigbuild) binaries carry a linker ad-hoc signature that the OS code-signing monitor rejects at launch with a SIGKILL, so build.sh now re-signs both binaries ad-hoc with the native codesign before restarting the hotkey service.

  • v0.18 - Pinned commands, external links, and a tray-managed webserver

    Three features today, all building on recent work.

    First, commands can now be pinned: true to force them to the top of the recent list when the launcher opens with empty input, regardless of when they were last used. The ordering logic lives in a shared order_recent_keys so the CLI list view and the GUI launcher stay identical: accessed pins first (by recency), then pins that have never been used (in config order, so they surface even with no history), then everything else by recency. Like the other per-command flags it merges across the project/client/global layers and is omitted from serialized YAML when false.

    Second, the webview window now behaves sensibly with links that point elsewhere. I capture the configured URL’s origin, and any navigation or target=_blank/window.open to a different http(s) origin is cancelled and handed to the system browser instead — same-origin navigation, and non-http(s) targets like about: or error pages, stay inside the webview. If handing the link to the browser fails, an injected, self-dismissing toast in the corner of the page says so rather than failing silently. This meant adding an origin_of helper to the URL utils.

    Third, the tray (the hotkey service) can now own the lifecycle of my WSL assist webserver. A new webserver: config section (enabled, command, distro) drives a “Webserver” submenu in the tray with Enabled / Restart / Open in browser / View logs. When enabled, the tray launches the command inside WSL via bash -lc "exec ..." at startup — first killing any orphan from a previous tray instance that didn’t exit cleanly, since that would still be holding port 3100 and cause an EADDRINUSE. Toggling the menu item flips the config and the tray manages start/stop accordingly.

  • v0.17 - A borderless in-app webview window

    Some of the things I open are local web apps that I’d rather see in a clean, chromeless window than in a browser tab. So commands can now carry webview: true, and instead of launching a browser they open the URL in a reusable, borderless WebView2 window. It’s mutually exclusive with command — config validation rejects setting both — and it merges across the project/client/global layers like any other command field.

    The window runs as its own process via a hidden webview <url> subcommand, deliberately, so its tao event loop never collides with the egui launcher’s. There’s only ever one of these windows: summon_or_open uses FindWindowW on a fixed window title to spot an existing instance and just foregrounds it (restoring it first if minimised), otherwise it spawns a fresh process and grants it AllowSetForegroundWindow so it can raise itself.

    The fiddly part was that a borderless window with a WebView2 child has no chrome and no way to be dragged — the child window swallows every mouse message, so the host never gets a WM_NCHITTEST over those pixels. The fix is an injected JavaScript shim: it treats the top strip as a drag handle and the window edges as resize handles, sets the right resize cursors, skips interactive elements so the app’s own header buttons still work, and forwards gestures over wry’s IPC channel. The host turns those into drag_window / drag_resize_window calls. The window also remembers where it was: its physical-pixel rect is saved to ~/.project-switch-webview.yml and reapplied with SetWindowPos on next open (which sidesteps tao’s cross-DPI mis-placement), unless the monitor it lived on has since been disconnected, in which case it falls back to the default placement.

    One build wrinkle: the GNU cross-compile target can’t statically link the MSVC WebView2 loader, so docker-build.sh now finds and ships WebView2Loader.dll next to the exe.

  • v0.16.2 - Finding MSIX and Store apps on Windows

    Store-packaged and MSIX apps were missing from the Windows launcher. The reason is that they don’t drop a .lnk into the Start Menu like classic win32 apps do — they register through an app manifest instead, so my directory-scanning approach never saw them.

    The fix reaches into shell:AppsFolder via COM. A new collect_packaged_apps opens FOLDERID_AppsFolder, enumerates it with IEnumShellItems, and pulls each item’s AUMID and display name through IShellItem::GetDisplayName (carefully freeing the COM-allocated strings with CoTaskMemFree). Packaged apps are identifiable by their PackageFamilyName!AppId AUMID format, so I filter on the ! and skip anything without it — those are plain win32 apps already covered by the existing .lnk/.url scan. Matches get stored with a shell:AppsFolder\<aumid> path so they launch correctly. The packaged apps are merged into the scanned entries, deduplicated by lowercased name against what the directory scan already found, and the combined list is sorted alphabetically. This pulled in two new windows crate features, Win32_UI_Shell and Win32_System_Com.

  • v0.16.1 - Two macOS launcher fixes

    A pair of macOS-specific fixes. The first was a self-inflicted one: to stop the launcher flashing on the primary display before jumping to a target monitor, I’d been spawning the window off-screen at (-32000, -32000) and repositioning it on the first frame. That repositioning relies on monitor_physical_rect, which is a no-op outside Windows — so on macOS the window started off-screen and simply stayed there, invisible forever. I gated the off-screen trick behind #[cfg(windows)] so other platforms just show the window where it lands.

    The second fix taught the app shortcut scanner about /System/Applications. macOS moved a lot of its bundled apps — Find My, Music, TV and friends — out of /Applications and into the system directory, so they were missing from the launcher entirely. Adding it as a non-recursive scan path alongside /Applications, /Applications/Utilities, and ~/Applications brings those back.

  • v0.16 - Clients, with projects nested underneath

    The big change today was rethinking the core model. What I used to call a “project” is now a “client”, and each client can contain its own projects: array. The motivation was real-world structure: I work with a client who has several distinct apps, and I wanted to switch into “client X / web” or “client X / api” and get the right command set for each, without duplicating all the shared client-level links.

    That meant a precedence chain for commands: project > client > global. When a project is active its commands win, falling back to the client’s, then to global. The same layering applies to the browser choice, so a command can override the project, which overrides the client, which overrides the global default. I reworked resolve_current to return the client and the optional nested project together, and threaded that through list, current, and switch. switch now presents clients first; if the chosen client has nested projects, a second prompt lets me pick the client itself (shown as <name> (client)) or one of its projects. current and the launcher header both display the active selection as client / project.

    To avoid breaking every existing config, old files using projects: / currentProject: are migrated in place to clients: / currentClient: on first load, and included configs get the same treatment. The merge rules for shared configs were extended to cover nested projects (matched by name, and a project can’t itself nest further). I also dropped the add command entirely — it never kept pace with the config schema, and editing ~/.project-switch.yml directly is simpler and more honest about what’s going on. The README and example configs were rewritten throughout to reflect the new vocabulary.

  • v0.15.3 - Enter opens the item you actually highlighted

    Fixed an annoying bug in the launcher: arrowing down to an item and pressing Enter would open the first item in the list instead of the one I’d highlighted. The GUI was handing the raw input text to resolve_item rather than looking at the selected entry, so navigation effectively didn’t count once you committed.

    The fix is a new selected_action_input on WindowState that resolves the action string from the currently selected entry. If the first word of my input matches the item’s key exactly, it keeps the full input so typed args survive (e.g. g some text still searches for “some text”); otherwise it falls back to the item’s key, so a partial filter like ji correctly launches jira. It returns None for empty or out-of-bounds selections and for non-Item entries like expressions and paths, which the caller already handles separately. The Enter handler in render_launcher now routes through it. I backfilled a batch of regression tests covering navigation, preserved args, partial filters, recents, and the empty-list case.

  • 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.