Phase 1: project scaffold, clap CLI, self-re-exec daemon, NDJSON IPC

- Cargo.toml with clap, tokio, serde, anyhow dependencies
- Entry point with env-var routing to daemon or CLI mode
- Core protocol types (Request/Response NDJSON wire format)
- Session detection (X11 check with DISPLAY/XDG_SESSION_TYPE)
- RefMap with @wN selector resolution (direct, prefix, substring)
- Snapshot/WindowInfo shared types with Display impl
- clap derive CLI with all subcommands (snapshot, click, type, etc.)
- Client connection: socket path resolution, daemon auto-start via
  self-re-exec, NDJSON send/receive with retry backoff
- Tokio async daemon: Unix socket listener, accept loop, graceful
  shutdown via notify
- DaemonState holding session info and ref map
- Placeholder handler returning hardcoded snapshot response
This commit is contained in:
Harivansh Rathi 2026-03-24 21:19:18 -04:00
parent 17d4a1edeb
commit dfaa339594
13 changed files with 1735 additions and 0 deletions

75
src/core/refs.rs Normal file
View file

@ -0,0 +1,75 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct RefEntry {
pub xcb_id: u32,
pub app_class: String,
pub title: String,
pub pid: u32,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub focused: bool,
pub minimized: bool,
}
#[derive(Debug, Default)]
#[allow(dead_code)]
pub struct RefMap {
map: HashMap<String, RefEntry>,
next_ref: usize,
}
#[allow(dead_code)]
impl RefMap {
pub fn new() -> Self {
Self { map: HashMap::new(), next_ref: 1 }
}
pub fn clear(&mut self) {
self.map.clear();
self.next_ref = 1;
}
pub fn insert(&mut self, entry: RefEntry) -> String {
let ref_id = format!("w{}", self.next_ref);
self.next_ref += 1;
self.map.insert(ref_id.clone(), entry);
ref_id
}
/// Resolve a selector to a RefEntry.
/// Accepts: "@w1", "w1", "ref=w1", or a substring match on app_class/title.
pub fn resolve(&self, selector: &str) -> Option<&RefEntry> {
let normalized = selector
.strip_prefix('@')
.or_else(|| selector.strip_prefix("ref="))
.unwrap_or(selector);
// Try direct ref lookup
if let Some(entry) = self.map.get(normalized) {
return Some(entry);
}
// Try substring match on app_class or title (case-insensitive)
let lower = selector.to_lowercase();
self.map.values().find(|e| {
e.app_class.to_lowercase().contains(&lower)
|| e.title.to_lowercase().contains(&lower)
})
}
/// Resolve a selector to the center coordinates of the window.
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
self.resolve(selector).map(|e| {
(e.x + e.width as i32 / 2, e.y + e.height as i32 / 2)
})
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
self.map.iter()
}
}