1use std::fmt::Write as _;
9use std::path::Path;
10use std::time::SystemTime;
11
12use clap::Parser;
13use tokio::io::{AsyncWrite, AsyncWriteExt};
14
15use crate::error::Result;
16use crate::session::{self, Session, SessionStore};
17
18#[derive(Debug, Parser)]
19pub struct LsArgs {}
20
21pub async fn execute(
22 _args: &LsArgs,
23 session_root_flag: Option<&Path>,
24 repo_cfg_override: Option<&Path>,
25 global_cfg_path: &Path,
26 cwd: &Path,
27) -> Result<i32> {
28 let root = session::resolve_session_root_for_cli(
29 session_root_flag,
30 repo_cfg_override,
31 global_cfg_path,
32 cwd,
33 )?;
34 let store = SessionStore::new(root);
35 let mut stdout = tokio::io::stdout();
36 let mut stderr = tokio::io::stderr();
37 execute_with(&mut stdout, &mut stderr, &store).await
38}
39
40pub async fn execute_with<W, E>(stdout: &mut W, stderr: &mut E, store: &SessionStore) -> Result<i32>
41where
42 W: AsyncWrite + Unpin,
43 E: AsyncWrite + Unpin,
44{
45 let sessions = store.list()?;
46 if sessions.is_empty() {
47 stderr.write_all(b"[outrig] no sessions\n").await?;
48 return Ok(0);
49 }
50 let now = SystemTime::now();
51 let table = render_table(&sessions, now);
52 stdout.write_all(table.as_bytes()).await?;
53 Ok(0)
54}
55
56fn render_table(sessions: &[Session], now: SystemTime) -> String {
60 let rows: Vec<Row> = sessions.iter().map(|s| Row::from_session(s, now)).collect();
61
62 let id_w = max_width("ID", rows.iter().map(|r| r.id.as_str()));
63 let started_w = max_width("STARTED", rows.iter().map(|r| r.started.as_str()));
64 let duration_w = max_width("DURATION", rows.iter().map(|r| r.duration.as_str()));
65 let image_w = max_width("IMAGE", rows.iter().map(|r| r.image.as_str()));
66
67 let mut out = String::new();
68 let _ = writeln!(
69 out,
70 "{:<id_w$} {:<started_w$} {:<duration_w$} {:<image_w$} EXIT",
71 "ID",
72 "STARTED",
73 "DURATION",
74 "IMAGE",
75 id_w = id_w,
76 started_w = started_w,
77 duration_w = duration_w,
78 image_w = image_w,
79 );
80 for r in &rows {
81 let exit_cell = match &r.link_target {
82 Some(t) => format!("{:<3} -> {}", r.exit, t),
83 None => r.exit.clone(),
84 };
85 let _ = writeln!(
86 out,
87 "{:<id_w$} {:<started_w$} {:<duration_w$} {:<image_w$} {}",
88 r.id,
89 r.started,
90 r.duration,
91 r.image,
92 exit_cell,
93 id_w = id_w,
94 started_w = started_w,
95 duration_w = duration_w,
96 image_w = image_w,
97 );
98 }
99 out
100}
101
102struct Row {
103 id: String,
104 started: String,
105 duration: String,
106 image: String,
107 exit: String,
108 link_target: Option<String>,
109}
110
111impl Row {
112 fn from_session(s: &Session, now: SystemTime) -> Self {
113 let started = session::format_started_at(s.started_at);
114 let end = s.ended_at.unwrap_or(now);
115 let duration = end
116 .duration_since(s.started_at)
117 .map(session::format_duration)
118 .unwrap_or_else(|_| "?".to_string());
119 let image = s.image_config_name.clone();
120 let exit = match s.exit_code {
121 Some(c) => c.to_string(),
122 None => "-".to_string(),
123 };
124 let link_target = s.link_target.as_ref().map(|p| p.display().to_string());
125 Self {
126 id: s.id.as_str().to_string(),
127 started,
128 duration,
129 image,
130 exit,
131 link_target,
132 }
133 }
134}
135
136fn max_width<'a, I: IntoIterator<Item = &'a str>>(header: &str, values: I) -> usize {
137 let mut w = header.len();
138 for v in values {
139 if v.len() > w {
140 w = v.len();
141 }
142 }
143 w
144}