v0.2.12 : ligne de statut — temps dexecution de la derniere commande (⚡ ms/s) + ls parallele (tailles de dossiers reparties sur les coeurs, un seul appel de metadonnees par entree) — 130 entrees instantanees
This commit is contained in:
Generated
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-manager"
|
name = "agent-manager"
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
|
description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+74
-48
@@ -51,9 +51,8 @@ pub fn dir_size(path: &Path) -> Option<u64> {
|
|||||||
Some(total)
|
Some(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modified_str(path: &Path) -> String {
|
fn modified_text(meta: &std::fs::Metadata) -> String {
|
||||||
std::fs::metadata(path)
|
meta.modified()
|
||||||
.and_then(|m| m.modified())
|
|
||||||
.ok()
|
.ok()
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
let dt: chrono::DateTime<chrono::Local> = t.into();
|
let dt: chrono::DateTime<chrono::Local> = t.into();
|
||||||
@@ -94,59 +93,86 @@ fn is_executable(path: &Path, meta: &std::fs::Metadata) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One ls row: (sort key, cells). A single symlink_metadata call per entry
|
||||||
|
/// (the modification time comes from the same metadata).
|
||||||
|
fn row_for(p: &Path, color: bool) -> Option<(String, Vec<Cell>)> {
|
||||||
|
let meta = std::fs::symlink_metadata(p).ok()?;
|
||||||
|
let file_type = meta.file_type();
|
||||||
|
let name = p.file_name().and_then(|n| n.to_str())?.to_string();
|
||||||
|
let (kind, style) = if file_type.is_symlink() {
|
||||||
|
("symlink", if p.exists() { 3 } else { 4 })
|
||||||
|
} else if file_type.is_dir() {
|
||||||
|
("dir", 1)
|
||||||
|
} else if is_executable(p, &meta) {
|
||||||
|
("file", 2)
|
||||||
|
} else {
|
||||||
|
("file", 0)
|
||||||
|
};
|
||||||
|
let (size_text, size_bytes) = if file_type.is_dir() {
|
||||||
|
match dir_size(p) {
|
||||||
|
Some(s) => (format_size(s), Some(s)),
|
||||||
|
None => ("—".to_string(), None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(format_size(meta.len()), Some(meta.len()))
|
||||||
|
};
|
||||||
|
let mut display = name.clone();
|
||||||
|
if file_type.is_dir() {
|
||||||
|
display.push('/');
|
||||||
|
} else if file_type.is_symlink() {
|
||||||
|
if let Ok(target) = std::fs::read_link(p) {
|
||||||
|
display.push_str(&format!(" -> {}", target.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
name.to_lowercase(),
|
||||||
|
vec![
|
||||||
|
Cell::str(paint(&display, color, style)),
|
||||||
|
Cell::str(kind.to_string()),
|
||||||
|
Cell::bytes(size_bytes.unwrap_or(0), size_text),
|
||||||
|
Cell::str(modified_text(&meta)),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the ls table (columns: name, type, size, modified). Directories
|
/// Build the ls table (columns: name, type, size, modified). Directories
|
||||||
/// come first, then alphabetical order.
|
/// come first, then alphabetical order.
|
||||||
|
///
|
||||||
|
/// Rows are built in parallel: recursive directory sizes dominate the cost,
|
||||||
|
/// so a listing with many subdirectories stays fast (like Nushell, which
|
||||||
|
/// also parallelizes its listing).
|
||||||
pub fn ls_table(path: &Path, color: bool) -> Result<DataTable> {
|
pub fn ls_table(path: &Path, color: bool) -> Result<DataTable> {
|
||||||
let rd = std::fs::read_dir(path)
|
let rd = std::fs::read_dir(path)
|
||||||
.with_context(|| format!("cannot read {}", path.display()))?;
|
.with_context(|| format!("cannot read {}", path.display()))?;
|
||||||
let mut rows: Vec<(String, Vec<Cell>)> = Vec::new();
|
let entries: Vec<PathBuf> = rd.flatten().map(|e| e.path()).collect();
|
||||||
for entry in rd.flatten() {
|
|
||||||
let p = entry.path();
|
let threads = std::thread::available_parallelism()
|
||||||
let meta = match std::fs::symlink_metadata(&p) {
|
.map(|n| n.get())
|
||||||
Ok(m) => m,
|
.unwrap_or(4)
|
||||||
Err(_) => continue,
|
.min(8);
|
||||||
};
|
let chunk_size = entries.len().div_ceil(threads).max(1);
|
||||||
let file_type = meta.file_type();
|
let rows: Vec<(String, Vec<Cell>)> = std::thread::scope(|scope| {
|
||||||
let name = p
|
let mut handles = Vec::new();
|
||||||
.file_name()
|
for chunk in entries.chunks(chunk_size) {
|
||||||
.map(|n| n.to_string_lossy().to_string())
|
let chunk: Vec<PathBuf> = chunk.to_vec();
|
||||||
.unwrap_or_default();
|
handles.push(scope.spawn(move || {
|
||||||
let (kind, style) = if file_type.is_symlink() {
|
chunk
|
||||||
("symlink", if p.exists() { 3 } else { 4 })
|
.into_iter()
|
||||||
} else if file_type.is_dir() {
|
.filter_map(|p| row_for(&p, color))
|
||||||
("dir", 1)
|
.collect::<Vec<_>>()
|
||||||
} else if is_executable(&p, &meta) {
|
}));
|
||||||
("file", 2)
|
}
|
||||||
} else {
|
let mut out = Vec::new();
|
||||||
("file", 0)
|
for handle in handles {
|
||||||
};
|
if let Ok(mut rows) = handle.join() {
|
||||||
let (size_text, size_bytes) = if file_type.is_dir() {
|
out.append(&mut rows);
|
||||||
match dir_size(&p) {
|
|
||||||
Some(s) => (format_size(s), Some(s)),
|
|
||||||
None => ("—".to_string(), None),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(format_size(meta.len()), Some(meta.len()))
|
|
||||||
};
|
|
||||||
let mut display = name.clone();
|
|
||||||
if file_type.is_dir() {
|
|
||||||
display.push('/');
|
|
||||||
} else if file_type.is_symlink() {
|
|
||||||
if let Ok(target) = std::fs::read_link(&p) {
|
|
||||||
display.push_str(&format!(" -> {}", target.display()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.push((
|
out
|
||||||
name.to_lowercase(),
|
});
|
||||||
vec![
|
|
||||||
Cell::str(paint(&display, color, style)),
|
|
||||||
Cell::str(kind.to_string()),
|
|
||||||
Cell::bytes(size_bytes.unwrap_or(0), size_text),
|
|
||||||
Cell::str(modified_str(&p)),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// Directories first, then alphabetical.
|
// Directories first, then alphabetical.
|
||||||
|
let mut rows = rows;
|
||||||
rows.sort_by(|a, b| {
|
rows.sort_by(|a, b| {
|
||||||
let a_dir = a.1[1].text == "dir";
|
let a_dir = a.1[1].text == "dir";
|
||||||
let b_dir = b.1[1].text == "dir";
|
let b_dir = b.1[1].text == "dir";
|
||||||
|
|||||||
+23
-6
@@ -475,7 +475,9 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One-line status shown before every prompt (Hermes-style status bar).
|
/// One-line status shown before every prompt (Hermes-style status bar).
|
||||||
fn print_status(app: &App, session: &ShellSession, started: Instant) {
|
/// The second parameter is the duration of the previous command (None before
|
||||||
|
/// the first one).
|
||||||
|
fn print_status(app: &App, session: &ShellSession, started: Instant, last: Option<Duration>) {
|
||||||
let color = app.color();
|
let color = app.color();
|
||||||
let sep = dim(" │ ", color);
|
let sep = dim(" │ ", color);
|
||||||
let cwd = shorten_head(¤t_dir_string(), 26);
|
let cwd = shorten_head(¤t_dir_string(), 26);
|
||||||
@@ -493,9 +495,16 @@ fn print_status(app: &App, session: &ShellSession, started: Instant) {
|
|||||||
),
|
),
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
|
let last_text = match last {
|
||||||
|
Some(d) if d.as_secs() >= 1 => format_elapsed(d),
|
||||||
|
Some(d) if d.as_millis() >= 1 => format!("{}ms", d.as_millis()),
|
||||||
|
Some(d) => format!("{}µs", d.as_micros()),
|
||||||
|
None => "—".to_string(),
|
||||||
|
};
|
||||||
|
let last_part = dim(&format!("⚡ {last_text}"), color);
|
||||||
let elapsed = dim(&format_elapsed(started.elapsed()), color);
|
let elapsed = dim(&format_elapsed(started.elapsed()), color);
|
||||||
println!(
|
println!(
|
||||||
"{}am v{}{sep}{shell_part}{sep}{counts}{sep}⏲ {elapsed}",
|
"{}am v{}{sep}{shell_part}{sep}{counts}{sep}{last_part}{sep}⏲ {elapsed}",
|
||||||
dim(" ⚙ ", color),
|
dim(" ⚙ ", color),
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
);
|
);
|
||||||
@@ -694,8 +703,9 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
|
|||||||
}
|
}
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let mut last: Option<DataTable> = None;
|
let mut last: Option<DataTable> = None;
|
||||||
|
let mut last_duration: Option<Duration> = None;
|
||||||
loop {
|
loop {
|
||||||
print_status(app, session, started);
|
print_status(app, session, started, last_duration);
|
||||||
match rl.readline("❯ ") {
|
match rl.readline("❯ ") {
|
||||||
Ok(line) => {
|
Ok(line) => {
|
||||||
let trimmed = line.trim().to_string();
|
let trimmed = line.trim().to_string();
|
||||||
@@ -703,7 +713,10 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let _ = rl.add_history_entry(trimmed.as_str());
|
let _ = rl.add_history_entry(trimmed.as_str());
|
||||||
match handle_input(app, session, &mut last, &trimmed) {
|
let t = Instant::now();
|
||||||
|
let result = handle_input(app, session, &mut last, &trimmed);
|
||||||
|
last_duration = Some(t.elapsed());
|
||||||
|
match result {
|
||||||
Ok(true) => break,
|
Ok(true) => break,
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
Err(e) => app.log.error(&format!("{e:#}")),
|
Err(e) => app.log.error(&format!("{e:#}")),
|
||||||
@@ -725,8 +738,9 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
|
|||||||
fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
|
fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let mut last: Option<DataTable> = None;
|
let mut last: Option<DataTable> = None;
|
||||||
|
let mut last_duration: Option<Duration> = None;
|
||||||
loop {
|
loop {
|
||||||
print_status(app, session, started);
|
print_status(app, session, started, last_duration);
|
||||||
eprint!("❯ ");
|
eprint!("❯ ");
|
||||||
std::io::stderr().flush().ok();
|
std::io::stderr().flush().ok();
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
@@ -737,7 +751,10 @@ fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
|
|||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match handle_input(app, session, &mut last, &line) {
|
let t = Instant::now();
|
||||||
|
let result = handle_input(app, session, &mut last, &line);
|
||||||
|
last_duration = Some(t.elapsed());
|
||||||
|
match result {
|
||||||
Ok(true) => break,
|
Ok(true) => break,
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
Err(e) => app.log.error(&format!("{e:#}")),
|
Err(e) => app.log.error(&format!("{e:#}")),
|
||||||
|
|||||||
Reference in New Issue
Block a user