use crate::config_edit::FileIo;
use crate::ui_state::Clock;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
pub(crate) static SPAWN_LOCK: Mutex<()> = Mutex::new(());
thread_local! {
static DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
pub(crate) struct SpawnGuard {
lock: Option<std::sync::MutexGuard<'static, ()>>,
}
impl Drop for SpawnGuard {
fn drop(&mut self) {
DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
drop(self.lock.take());
}
}
pub(crate) fn spawn_guard() -> SpawnGuard {
if DEPTH.with(|d| d.replace(d.get() + 1)) > 0 {
return SpawnGuard { lock: None };
}
SpawnGuard {
lock: Some(SPAWN_LOCK.lock().unwrap_or_else(PoisonError::into_inner)),
}
}
#[derive(Clone)]
pub(crate) struct FakeClock {
at: Arc<Mutex<Instant>>,
lurch: Duration,
}
impl FakeClock {
pub(crate) fn new() -> Self {
Self {
at: Arc::new(Mutex::new(Instant::now())),
lurch: Duration::ZERO,
}
}
pub(crate) fn lurching(lurch: Duration) -> Self {
Self {
lurch,
..Self::new()
}
}
pub(crate) fn handle(&self) -> Self {
Self {
at: Arc::clone(&self.at),
lurch: self.lurch,
}
}
pub(crate) fn arc(&self) -> Arc<dyn Clock> {
Arc::new(self.handle())
}
pub(crate) fn advance(&self, delta: Duration) {
*self
.at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) += delta;
}
}
impl Clock for FakeClock {
fn now(&self) -> Instant {
let mut at = self
.at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let read = *at;
*at += self.lurch;
read
}
fn stamp(&self) -> String {
"TS".to_string()
}
}
#[derive(Default)]
pub(crate) struct FakeFs {
pub(crate) files: Mutex<HashMap<PathBuf, Vec<u8>>>,
pub(crate) fail_write: bool,
}
impl FakeFs {
pub(crate) fn map(&self) -> std::sync::MutexGuard<'_, HashMap<PathBuf, Vec<u8>>> {
self.files
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn seed(path: &Path, bytes: &[u8]) -> Self {
let me = Self::default();
me.map().insert(path.to_path_buf(), bytes.to_vec());
me
}
pub(crate) fn get(&self, path: &Path) -> Option<Vec<u8>> {
self.map().get(path).cloned()
}
}
impl FileIo for FakeFs {
fn read(&self, path: &Path) -> std::io::Result<Option<Vec<u8>>> {
Ok(self.get(path))
}
fn write(&self, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
if self.fail_write {
return Err(std::io::Error::other("boom"));
}
self.map().insert(path.to_path_buf(), bytes.to_vec());
Ok(())
}
fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
let bytes = self.map().remove(from).unwrap_or_default();
self.map().insert(to.to_path_buf(), bytes);
Ok(())
}
fn remove(&self, path: &Path) -> std::io::Result<()> {
self.map().remove(path);
Ok(())
}
fn exists(&self, path: &Path) -> bool {
self.map().contains_key(path)
}
fn list_dir(&self, dir: &Path) -> std::io::Result<Vec<PathBuf>> {
Ok(self
.map()
.keys()
.filter(|p| p.parent() == Some(dir))
.cloned()
.collect())
}
}
pub(crate) fn spawn_locked(
cmd: &mut std::process::Command,
) -> std::io::Result<std::process::Child> {
let _g = spawn_guard();
cmd.spawn()
}
pub(crate) const TEMPLATE_PROVIDERS: &str = "roles:\n worker:\n provider: anthropic\n \
model: claude-sonnet-5\n tools: [apply_patch, bash, cd, dispatch, load_skill, message, \
multi_tool, read_file]\n compactor:\n provider: anthropic\n model: claude-haiku-4-5\n";
pub(crate) fn authoring_new_arm() -> String {
format!(
"new)\n mkdir -p \"$2/repo.git\"\n \
git init -q --bare -b config/default \"$2/repo.git\"\n \
git -C \"$2/repo.git\" worktree add -q --orphan -b config/default \"$2/.author\"\n \
printf '%s' '{TEMPLATE_PROVIDERS}' > \"$2/.author/providers.yaml\"\n \
git -C \"$2/.author\" add -A\n \
git -C \"$2/.author\" -c user.email=t@t.local -c user.name=T \
-c commit.gpgsign=false commit -q -m 'config: init [config/default]'\n \
git -C \"$2/repo.git\" worktree remove \"$2/.author\"\n ;;\n"
)
}
pub(crate) fn replace_directory(path: &std::path::Path) {
let fresh = prepared_replacement(path);
std::fs::remove_dir_all(path).expect("the original is unlinked");
std::fs::rename(&fresh, path).expect("the replacement takes the path");
}
pub(crate) fn prepared_replacement(path: &std::path::Path) -> std::path::PathBuf {
let mut fresh = path.as_os_str().to_owned();
fresh.push(".replacement");
let fresh = std::path::PathBuf::from(fresh);
std::fs::create_dir_all(&fresh).expect("the replacement is created beside the original");
fresh
}
pub(crate) mod world;
pub(crate) use world::{fixture_workspace, no_wall, no_world, wall_paths, world_under};
pub(crate) mod workspace;