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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ nul

# Local scripts and env files
*.ps1
!dev.ps1
!scripts/*.ps1
version.env
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,7 +70,7 @@ cargo build --release
## Usage

### GUI (System Tray)
```bash
```powershell
codexbar menubar
```

Expand Down
122 changes: 122 additions & 0 deletions dev.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#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
.\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(
[switch]$Release,
[switch]$SkipBuild,
[switch]$Verbose
)

Set-StrictMode -Version Latest
$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)
$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 ──────────────────────────────────────────────────────────────────────

# Binary may be under target/<profile> or target/<triple>/<profile>
$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 $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
}

$args_ = @("menubar")
if ($Verbose) {
$args_ = @("-v") + $args_
}

Write-Host ""
Write-Host "Starting CodexBar..." -ForegroundColor Green
& $binary @args_
1 change: 1 addition & 0 deletions rust/src/native_ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod app;
mod charts;
mod preferences;
mod provider_icons;
pub(crate) mod test_server;
mod theme;

pub use app::run;
104 changes: 104 additions & 0 deletions rust/src/native_ui/test_server.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<Vec<TestInput>>>;

/// 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<TestInput> {
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<f32> {
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()
}
Loading