use crate::daemon_id::DaemonId;
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::{PitchforkToml, PitchforkTomlAuto};
use crate::ui::table::print_table;
use crate::{Result, env};
use miette::IntoDiagnostic;
use std::collections::HashSet;
use std::path::PathBuf;
use comfy_table::{Cell, Color, ContentArrangement, Table};
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment)]
pub struct Project {
#[clap(subcommand)]
command: ProjectCommands,
}
#[derive(Debug, clap::Subcommand)]
enum ProjectCommands {
Enter(Enter),
Leave(Leave),
List(List),
}
#[derive(Debug, clap::Args)]
pub struct Enter {
#[clap(long)]
pid: u32,
#[clap(long)]
directory: Option<PathBuf>,
}
#[derive(Debug, clap::Args)]
pub struct Leave {
#[clap(long)]
pid: u32,
#[clap(long)]
directory: Option<PathBuf>,
}
#[derive(Debug, clap::Args)]
#[clap(
verbatim_doc_comment,
long_about = "\
List tracked project sessions
Displays a table of active project sessions with their host PID, directory,
liveness status, and recorded process title.
Example:
pitchfork project list
pitchfork project list --json"
)]
pub struct List {
#[clap(long)]
json: bool,
}
impl Project {
pub async fn run(&self) -> Result<()> {
match &self.command {
ProjectCommands::Enter(enter) => enter.run().await,
ProjectCommands::Leave(leave) => leave.run().await,
ProjectCommands::List(list) => list.run().await,
}
}
}
fn resolve_directory(directory: &Option<PathBuf>) -> Result<PathBuf> {
let canonical = match directory {
Some(d) => d.canonicalize().into_diagnostic()?,
None => env::CWD.canonicalize().into_diagnostic()?,
};
Ok(dunce::simplified(&canonical).to_path_buf())
}
impl Enter {
pub async fn run(&self) -> Result<()> {
let target_dir = resolve_directory(&self.directory)?;
let pt = PitchforkToml::all_merged_from(&target_dir)?;
let ipc = IpcClient::connect(true).await?;
ipc.project_enter(self.pid, target_dir.clone()).await?;
let to_start: Vec<DaemonId> = pt
.daemons
.into_iter()
.filter(|(_, d)| d.auto.contains(&PitchforkTomlAuto::Start))
.map(|(id, _)| id)
.collect();
if !to_start.is_empty() {
let active_daemons: HashSet<DaemonId> = ipc
.active_daemons()
.await?
.into_iter()
.map(|d| d.id)
.collect();
let mut args = vec!["start".to_string()];
for id in &to_start {
if active_daemons.contains(id) {
continue;
}
args.push(id.qualified());
}
if args.len() > 1 {
let status = tokio::process::Command::new(&*env::PITCHFORK_BIN)
.args(&args)
.current_dir(&target_dir)
.status()
.await
.into_diagnostic()?;
if !status.success() {
let _ = ipc.project_leave(self.pid, target_dir).await;
miette::bail!(
"pitchfork start {} exited with {status}",
args[1..].join(" ")
);
}
}
}
super::drain_notifications(&ipc).await;
Ok(())
}
}
impl Leave {
pub async fn run(&self) -> Result<()> {
let target_dir = resolve_directory(&self.directory)?;
let ipc = IpcClient::connect(true).await?;
ipc.project_leave(self.pid, target_dir).await?;
super::drain_notifications(&ipc).await;
Ok(())
}
}
impl List {
pub async fn run(&self) -> Result<()> {
let ipc = IpcClient::connect(true).await?;
let mut sessions = ipc.get_project_sessions().await?;
sessions.sort_by(|a, b| a.pid.cmp(&b.pid).then(a.directory.cmp(&b.directory)));
if self.json {
return crate::cli::json_output::print_json(&sessions);
}
let mut table = Table::new();
table
.load_preset(comfy_table::presets::NOTHING)
.set_content_arrangement(ContentArrangement::Disabled);
if console::user_attended() {
table.set_header(vec!["PID", "DIRECTORY", "STATUS", "TITLE"]);
}
for s in &sessions {
let pid = s.pid.to_string();
let directory = s.directory.display().to_string();
let (status_text, status_color) = if s.alive {
("alive".to_string(), Color::Green)
} else {
("dead".to_string(), Color::Red)
};
let title = s
.liveness_title
.clone()
.or_else(|| s.current_title.clone())
.unwrap_or_default();
table.add_row(vec![
Cell::new(&pid),
Cell::new(&directory),
Cell::new(&status_text).fg(status_color),
Cell::new(&title),
]);
}
print_table(table)
}
}