From 3bed2627fb78c3e963d2dfda5b09fab03721a587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agu=20Rodr=C3=ADguez?= Date: Fri, 27 Feb 2026 11:45:59 -0300 Subject: [PATCH 1/4] feat: add dev script, build prereq installer, and Getting Started docs - Add dev.ps1: one-command orchestrator that checks prerequisites, installs them if missing, builds, and launches CodexBar - Add scripts/setup-windows.ps1: automated installer for Rust (rustup + x86_64-pc-windows-gnu target) and MinGW-w64 (WinLibs) - Update README.md with a Getting Started section covering quick start, download, and manual build paths - Update .gitignore to allow tracked PowerShell scripts (dev.ps1, scripts/*.ps1) while keeping local .ps1 files ignored The GNU toolchain requires MinGW-w64 (for dlltool.exe and the linker), which was not documented. New contributors can now clone and run .\dev.ps1 to get a working build environment automatically. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + README.md | 39 +++++++++-- dev.ps1 | 110 +++++++++++++++++++++++++++++++ scripts/setup-windows.ps1 | 132 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 dev.ps1 create mode 100644 scripts/setup-windows.ps1 diff --git a/.gitignore b/.gitignore index f150be17a8..2ad2fbd106 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,6 @@ nul # Local scripts and env files *.ps1 +!dev.ps1 +!scripts/*.ps1 version.env diff --git a/README.md b/README.md index a238b45ef1..34acc427e5 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,44 @@ A Windows port of [CodexBar](https://github.com/steipete/CodexBar) - the tiny me ### Overview ![CodexBar Overview](docs/images/overview.png) -## Install +## Getting Started + +### Quick Start (from source) + +```powershell +# Clone and run — prerequisites are installed automatically +git clone https://github.com/Finesssee/Win-CodexBar.git +cd Win-CodexBar +.\dev.ps1 +``` + +This will: +1. Check for Rust and MinGW-w64, install them if missing +2. Build CodexBar in debug mode +3. Launch the system tray app + +Other options: +```powershell +.\dev.ps1 -Release # optimised build +.\dev.ps1 -Verbose # debug logging +.\dev.ps1 -SkipBuild # run last build without rebuilding +``` ### Download + Download the latest release from [GitHub Releases](https://github.com/Finesssee/Win-CodexBar/releases). -### Build from Source -```bash +### Manual Build + +Prerequisites: Rust 1.70+ with `x86_64-pc-windows-gnu` target, MinGW-w64. +Install them automatically with: + +```powershell +.\scripts\setup-windows.ps1 +``` + +Then build: +```powershell cd rust cargo build --release # Binary at: target/release/codexbar.exe @@ -39,7 +70,7 @@ cargo build --release ## Usage ### GUI (System Tray) -```bash +```powershell codexbar menubar ``` diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000000..64d28f3d3d --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,110 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Build and run CodexBar for Windows. + +.DESCRIPTION + Checks that build prerequisites are installed (Rust, MinGW-w64), + installs them if missing, then builds and launches CodexBar. + +.PARAMETER Release + Build in release mode (optimised). Default is debug. + +.PARAMETER SkipBuild + Skip the build step and run the last built binary. + +.PARAMETER Verbose + Pass -v to CodexBar for debug logging. + +.EXAMPLE + .\start.ps1 # debug build + run + .\start.ps1 -Release # release build + run + .\start.ps1 -SkipBuild # run last build + .\start.ps1 -Verbose # debug build + run with verbose logging +#> + +param( + [switch]$Release, + [switch]$SkipBuild, + [switch]$Verbose +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$RepoRoot = $PSScriptRoot +$RustDir = Join-Path $RepoRoot "rust" + +# ── Check prerequisites ───────────────────────────────────────────────────── + +$hasCargo = [bool](Get-Command cargo -ErrorAction SilentlyContinue) +$hasDlltool = [bool](Get-Command dlltool -ErrorAction SilentlyContinue) + +if (-not $hasCargo -or -not $hasDlltool) { + $missing = @() + if (-not $hasCargo) { $missing += "cargo (Rust)" } + if (-not $hasDlltool) { $missing += "dlltool (MinGW-w64)" } + Write-Host "Missing prerequisites: $($missing -join ', ')" -ForegroundColor Yellow + Write-Host "Running setup script..." -ForegroundColor Cyan + Write-Host "" + + $setupScript = Join-Path $RepoRoot "scripts\setup-windows.ps1" + if (-not (Test-Path $setupScript)) { + Write-Host "ERROR: Setup script not found at $setupScript" -ForegroundColor Red + exit 1 + } + + & $setupScript + + # Re-check after setup + $hasCargo = [bool](Get-Command cargo -ErrorAction SilentlyContinue) + $hasDlltool = [bool](Get-Command dlltool -ErrorAction SilentlyContinue) + if (-not $hasCargo -or -not $hasDlltool) { + Write-Host "" + Write-Host "ERROR: Prerequisites still missing after setup." -ForegroundColor Red + Write-Host "Please restart your terminal and try again." -ForegroundColor Yellow + exit 1 + } +} + +# ── Build ──────────────────────────────────────────────────────────────────── + +if (-not $SkipBuild) { + Push-Location $RustDir + try { + if ($Release) { + Write-Host "Building CodexBar (release)..." -ForegroundColor Cyan + cargo build --bin codexbar --release + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Host "Building CodexBar (debug)..." -ForegroundColor Cyan + cargo build --bin codexbar + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } + } finally { + Pop-Location + } +} + +# ── Run ────────────────────────────────────────────────────────────────────── + +if ($Release) { + $binary = Join-Path $RustDir "target\release\codexbar.exe" +} else { + $binary = Join-Path $RustDir "target\debug\codexbar.exe" +} + +if (-not (Test-Path $binary)) { + Write-Host "ERROR: Binary not found at $binary" -ForegroundColor Red + Write-Host "Run without -SkipBuild to build first." -ForegroundColor Yellow + exit 1 +} + +$args_ = @("menubar") +if ($Verbose) { + $args_ = @("-v") + $args_ +} + +Write-Host "" +Write-Host "Starting CodexBar..." -ForegroundColor Green +& $binary @args_ diff --git a/scripts/setup-windows.ps1 b/scripts/setup-windows.ps1 new file mode 100644 index 0000000000..4575c9abbc --- /dev/null +++ b/scripts/setup-windows.ps1 @@ -0,0 +1,132 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Installs build prerequisites for CodexBar on Windows. + +.DESCRIPTION + Sets up: + 1. Rust (via rustup) with the x86_64-pc-windows-gnu target + 2. MinGW-w64 toolchain (WinLibs) for the GNU linker and dlltool + 3. Adds both to the user PATH + + Run from an elevated (Admin) PowerShell if you want system-wide PATH changes, + otherwise user-level PATH is updated. + +.EXAMPLE + .\scripts\setup-windows.ps1 +#> + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$MinGWDir = "C:\mingw64" +$MinGWBin = "$MinGWDir\bin" +$CargoBin = "$env:USERPROFILE\.cargo\bin" +$WinLibsVersion = "15.2.0posix-13.0.0-ucrt-r6" +$WinLibsZip = "winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-13.0.0-r6.zip" +$WinLibsUrl = "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WinLibsVersion/$WinLibsZip" + +function Write-Step($msg) { + Write-Host "`n=> $msg" -ForegroundColor Cyan +} + +function Add-ToUserPath($dir) { + $current = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($current -notlike "*$dir*") { + [Environment]::SetEnvironmentVariable("PATH", "$dir;$current", "User") + Write-Host " Added $dir to user PATH (restart terminal to take effect)" -ForegroundColor Yellow + } else { + Write-Host " $dir already in user PATH" -ForegroundColor Green + } + # Also update current session + if ($env:PATH -notlike "*$dir*") { + $env:PATH = "$dir;$env:PATH" + } +} + +# ── 1. Rust ────────────────────────────────────────────────────────────────── + +Write-Step "Checking Rust installation" + +if (Get-Command rustup -ErrorAction SilentlyContinue) { + $rustVersion = rustc --version + Write-Host " Found: $rustVersion" -ForegroundColor Green +} else { + Write-Step "Installing Rust via rustup" + $rustupInit = "$env:TEMP\rustup-init.exe" + Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile $rustupInit + & $rustupInit -y --default-toolchain stable --default-host x86_64-pc-windows-gnu + Remove-Item $rustupInit -ErrorAction SilentlyContinue + Add-ToUserPath $CargoBin + Write-Host " Rust installed" -ForegroundColor Green +} + +# ── 2. GNU target ──────────────────────────────────────────────────────────── + +Write-Step "Ensuring x86_64-pc-windows-gnu target is installed" + +$targets = rustup target list --installed 2>&1 +if ($targets -match "x86_64-pc-windows-gnu") { + Write-Host " Target already installed" -ForegroundColor Green +} else { + rustup target add x86_64-pc-windows-gnu + Write-Host " Target added" -ForegroundColor Green +} + +# ── 3. MinGW-w64 (WinLibs) ────────────────────────────────────────────────── + +Write-Step "Checking MinGW-w64 (dlltool, gcc)" + +if (Get-Command dlltool -ErrorAction SilentlyContinue) { + $dlltoolPath = (Get-Command dlltool).Source + Write-Host " Found: $dlltoolPath" -ForegroundColor Green +} else { + if (Test-Path "$MinGWBin\dlltool.exe") { + Write-Host " MinGW exists at $MinGWDir but not in PATH" -ForegroundColor Yellow + } else { + Write-Step "Downloading MinGW-w64 (WinLibs) — ~250 MB" + $zipPath = "$env:TEMP\$WinLibsZip" + + if (-not (Test-Path $zipPath)) { + Invoke-WebRequest -Uri $WinLibsUrl -OutFile $zipPath + } + + Write-Step "Extracting to $MinGWDir" + Expand-Archive -Path $zipPath -DestinationPath "C:\" -Force + Remove-Item $zipPath -ErrorAction SilentlyContinue + Write-Host " Extracted" -ForegroundColor Green + } + + Add-ToUserPath $MinGWBin +} + +# ── 4. Verify ──────────────────────────────────────────────────────────────── + +Write-Step "Verifying toolchain" + +$checks = @( + @{ Name = "rustc"; Cmd = "rustc --version" }, + @{ Name = "cargo"; Cmd = "cargo --version" }, + @{ Name = "dlltool"; Cmd = "dlltool --version" }, + @{ Name = "gcc"; Cmd = "gcc --version" } +) + +$allOk = $true +foreach ($check in $checks) { + try { + $out = Invoke-Expression $check.Cmd 2>&1 | Select-Object -First 1 + Write-Host " [OK] $($check.Name): $out" -ForegroundColor Green + } catch { + Write-Host " [FAIL] $($check.Name) not found" -ForegroundColor Red + $allOk = $false + } +} + +Write-Host "" +if ($allOk) { + Write-Host "All prerequisites installed. You can now build:" -ForegroundColor Green + Write-Host " cd rust" -ForegroundColor White + Write-Host " cargo build" -ForegroundColor White +} else { + Write-Host "Some tools are missing. Restart your terminal and re-run this script." -ForegroundColor Yellow +} From f2f4c4f9160280562a0aec8430707ff5d9f97743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agu=20Rodr=C3=ADguez?= Date: Fri, 27 Feb 2026 13:00:24 -0300 Subject: [PATCH 2/4] fix: add missing test_server module that prevents compilation The test_server references were added in 73a3703 (upstream port) but the module file and declaration were never included, causing build failures. This adds the missing module that provides a TCP-based test input server for automated UI testing. Co-Authored-By: Claude Opus 4.6 --- rust/src/native_ui/mod.rs | 1 + rust/src/native_ui/test_server.rs | 104 ++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 rust/src/native_ui/test_server.rs diff --git a/rust/src/native_ui/mod.rs b/rust/src/native_ui/mod.rs index ecd1e361e2..367809b4a5 100644 --- a/rust/src/native_ui/mod.rs +++ b/rust/src/native_ui/mod.rs @@ -6,6 +6,7 @@ mod app; mod charts; mod preferences; mod provider_icons; +pub(crate) mod test_server; mod theme; pub use app::run; diff --git a/rust/src/native_ui/test_server.rs b/rust/src/native_ui/test_server.rs new file mode 100644 index 0000000000..7fc18870dc --- /dev/null +++ b/rust/src/native_ui/test_server.rs @@ -0,0 +1,104 @@ +//! Test input server for automated UI testing without moving the real cursor. +//! +//! Listens on a local TCP port and accepts JSON commands to inject synthetic +//! pointer events into the egui event loop. + +use std::io::Read; +use std::net::TcpListener; +use std::sync::{Arc, Mutex}; + +/// A synthetic input event to inject into the egui loop. +pub enum TestInput { + Click { x: f32, y: f32 }, + DoubleClick { x: f32, y: f32 }, + RightClick { x: f32, y: f32 }, +} + +/// Thread-safe queue of pending test inputs. +pub type TestInputQueue = Arc>>; + +/// Create a new empty test input queue. +pub fn create_queue() -> TestInputQueue { + Arc::new(Mutex::new(Vec::new())) +} + +/// Start a TCP server on `127.0.0.1:19400` that accepts JSON test commands. +/// +/// Each connection can send one JSON object per line: +/// ```json +/// {"type":"click","x":100,"y":200} +/// {"type":"double_click","x":100,"y":200} +/// {"type":"right_click","x":100,"y":200} +/// ``` +pub fn start_server(queue: TestInputQueue) { + std::thread::spawn(move || { + let listener = match TcpListener::bind("127.0.0.1:19400") { + Ok(l) => l, + Err(e) => { + tracing::warn!("Test server failed to bind: {e}"); + return; + } + }; + tracing::info!("Test input server listening on 127.0.0.1:19400"); + + for stream in listener.incoming() { + let mut stream = match stream { + Ok(s) => s, + Err(e) => { + tracing::warn!("Test server accept error: {e}"); + continue; + } + }; + + let mut buf = String::new(); + if let Err(e) = stream.read_to_string(&mut buf) { + tracing::warn!("Test server read error: {e}"); + continue; + } + + for line in buf.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + match parse_test_input(line) { + Some(input) => { + if let Ok(mut q) = queue.lock() { + q.push(input); + } + } + None => { + tracing::warn!("Test server: unrecognised input: {line}"); + } + } + } + } + }); +} + +fn parse_test_input(json: &str) -> Option { + let x = extract_f32(json, "x")?; + let y = extract_f32(json, "y")?; + + if json.contains("\"double_click\"") { + Some(TestInput::DoubleClick { x, y }) + } else if json.contains("\"right_click\"") { + Some(TestInput::RightClick { x, y }) + } else if json.contains("\"click\"") { + Some(TestInput::Click { x, y }) + } else { + None + } +} + +fn extract_f32(json: &str, key: &str) -> Option { + let pattern = format!("\"{key}\""); + let idx = json.find(&pattern)?; + let rest = &json[idx + pattern.len()..]; + let rest = rest.trim_start().strip_prefix(':')?; + let rest = rest.trim_start(); + let end = rest + .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-') + .unwrap_or(rest.len()); + rest[..end].parse().ok() +} From 3912b4d4342d9972cc21842d0fa26eb6081571cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agu=20Rodr=C3=ADguez?= Date: Fri, 27 Feb 2026 13:17:11 -0300 Subject: [PATCH 3/4] fix: ensure cargo and mingw PATH in current session before building dev.ps1 now prepends known tool paths (~/.cargo/bin, C:\mingw64\bin) to the current session PATH if the directories exist, so builds work without requiring a terminal restart after setup. Also fixes .EXAMPLE references from start.ps1 to dev.ps1. Co-Authored-By: Claude Opus 4.6 --- dev.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dev.ps1 b/dev.ps1 index 64d28f3d3d..809e269d92 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -17,10 +17,10 @@ Pass -v to CodexBar for debug logging. .EXAMPLE - .\start.ps1 # debug build + run - .\start.ps1 -Release # release build + run - .\start.ps1 -SkipBuild # run last build - .\start.ps1 -Verbose # debug build + run with verbose logging + .\dev.ps1 # debug build + run + .\dev.ps1 -Release # release build + run + .\dev.ps1 -SkipBuild # run last build + .\dev.ps1 -Verbose # debug build + run with verbose logging #> param( @@ -35,6 +35,15 @@ $ErrorActionPreference = 'Stop' $RepoRoot = $PSScriptRoot $RustDir = Join-Path $RepoRoot "rust" +# ── Ensure known tool paths are in current session PATH ───────────────────── + +$knownPaths = @("$env:USERPROFILE\.cargo\bin", "C:\mingw64\bin") +foreach ($p in $knownPaths) { + if ((Test-Path $p) -and ($env:PATH -notlike "*$p*")) { + $env:PATH = "$p;$env:PATH" + } +} + # ── Check prerequisites ───────────────────────────────────────────────────── $hasCargo = [bool](Get-Command cargo -ErrorAction SilentlyContinue) From 512cce3dc532d3c7c964df5556b831c050475877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agu=20Rodr=C3=ADguez?= Date: Fri, 27 Feb 2026 13:29:04 -0300 Subject: [PATCH 4/4] fix: find binary in GNU target output directory cargo with x86_64-pc-windows-gnu places binaries under target/x86_64-pc-windows-gnu/{debug,release}/ instead of target/{debug,release}/. Search both paths. Co-Authored-By: Claude Opus 4.6 --- dev.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dev.ps1 b/dev.ps1 index 809e269d92..b29e09cd6f 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -97,14 +97,17 @@ if (-not $SkipBuild) { # ── Run ────────────────────────────────────────────────────────────────────── -if ($Release) { - $binary = Join-Path $RustDir "target\release\codexbar.exe" -} else { - $binary = Join-Path $RustDir "target\debug\codexbar.exe" -} +# Binary may be under target/ or target// +$profile = if ($Release) { "release" } else { "debug" } +$candidates = @( + (Join-Path $RustDir "target\$profile\codexbar.exe"), + (Join-Path $RustDir "target\x86_64-pc-windows-gnu\$profile\codexbar.exe") +) +$binary = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1 -if (-not (Test-Path $binary)) { - Write-Host "ERROR: Binary not found at $binary" -ForegroundColor Red +if (-not $binary) { + Write-Host "ERROR: Binary not found. Searched:" -ForegroundColor Red + $candidates | ForEach-Object { Write-Host " $_" -ForegroundColor Red } Write-Host "Run without -SkipBuild to build first." -ForegroundColor Yellow exit 1 }