mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
An interrupted `tty7 run` leaves its pane running with nothing referencing it: no workspace holds it, every listing walks the tree, and the orphan sweep only logs. `pane ls --all` reads the server's registry instead and marks what nothing holds; `pane close` falls back to hanging a pane up directly when no workspace can route a PaneClose, so an orphan is stoppable. --quiet silenced failures as well as successes, leaving a bare exit code and nothing to debug; it now suppresses only output on success, and covers --json too. `run` exited through a path that skipped the report entirely, so `run --json` printed nothing at all; it now carries its report, with exit_code_known distinguishing the command's own 1 from the stand-in 1. The server lifecycle verbs can only drive the default endpoint — spawn::stop dials transport::connect() — while every other verb follows $TTY7_SOCKET. They now refuse when that names a different endpoint rather than acting on a different server than `tty7 status` reports on. Also: tables pad by display width, so a CJK name or path no longer skews every column after it; --h/--v become --horizontal/--vertical with the short forms kept as aliases; the verbs that are not implemented say so in --help instead of only at runtime; capture's help admits it decodes as lossy UTF-8.
184 lines
5.9 KiB
Rust
184 lines
5.9 KiB
Rust
use anyhow::{Result, bail};
|
|
use tty7_core::core::machine::{Machine, TabId, Workspace};
|
|
use tty7_core::core::session::WorkspaceId;
|
|
|
|
use crate::address::{TabAddress, WorkspaceAddress};
|
|
|
|
pub struct TabEntry {
|
|
pub ordinal: u64,
|
|
pub workspace: WorkspaceId,
|
|
pub tab: TabId,
|
|
}
|
|
|
|
pub fn tab_index(machine: &Machine) -> Vec<TabEntry> {
|
|
let mut entries = Vec::new();
|
|
let mut ordinal = 0;
|
|
for ws in &machine.workspaces {
|
|
for tab in &ws.tabs {
|
|
ordinal += 1;
|
|
entries.push(TabEntry {
|
|
ordinal,
|
|
workspace: ws.id,
|
|
tab: tab.id,
|
|
});
|
|
}
|
|
}
|
|
entries
|
|
}
|
|
|
|
pub fn ordinal_of(machine: &Machine, tab: TabId) -> Option<u64> {
|
|
tab_index(machine)
|
|
.into_iter()
|
|
.find(|e| e.tab == tab)
|
|
.map(|e| e.ordinal)
|
|
}
|
|
|
|
pub fn workspace<'m>(machine: &'m Machine, addr: &WorkspaceAddress) -> Result<&'m Workspace> {
|
|
match addr {
|
|
WorkspaceAddress::Id(id) => machine
|
|
.workspaces
|
|
.iter()
|
|
.find(|ws| ws.id == *id)
|
|
.ok_or_else(|| anyhow::anyhow!("no workspace with id {id} on this machine")),
|
|
WorkspaceAddress::Named(text) => {
|
|
let by_name: Vec<_> = machine
|
|
.workspaces
|
|
.iter()
|
|
.filter(|ws| ws.name.as_deref() == Some(text))
|
|
.collect();
|
|
match by_name.as_slice() {
|
|
[one] => return Ok(one),
|
|
[] => {}
|
|
many => bail!(
|
|
"'{text}' names {} workspaces — address one by id ({})",
|
|
many.len(),
|
|
many.iter()
|
|
.map(|ws| short_id(&ws.id))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
),
|
|
}
|
|
let by_prefix: Vec<_> = machine
|
|
.workspaces
|
|
.iter()
|
|
.filter(|ws| ws.id.to_string().starts_with(text.as_str()))
|
|
.collect();
|
|
match by_prefix.as_slice() {
|
|
[one] => Ok(one),
|
|
[] => bail!("no workspace named '{text}' — `tty7 ls` lists them"),
|
|
many => bail!(
|
|
"id prefix '{text}' matches {} workspaces — use more characters",
|
|
many.len()
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn tab(machine: &Machine, addr: &TabAddress) -> Result<(WorkspaceId, TabId)> {
|
|
match addr {
|
|
TabAddress::Ordinal(n) => tab_index(machine)
|
|
.into_iter()
|
|
.find(|e| e.ordinal == *n)
|
|
.map(|e| (e.workspace, e.tab))
|
|
.ok_or_else(|| anyhow::anyhow!("no tab @{n} — `tty7 ls` shows the @ numbers")),
|
|
TabAddress::Id(text) => {
|
|
for ws in &machine.workspaces {
|
|
for tab in &ws.tabs {
|
|
if tab.id.to_string() == *text {
|
|
return Ok((ws.id, tab.id));
|
|
}
|
|
}
|
|
}
|
|
bail!("no tab with id {text} on this machine")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn workspace_of_pane(machine: &Machine, pane: u64) -> Result<&Workspace> {
|
|
machine
|
|
.workspaces
|
|
.iter()
|
|
.find(|ws| ws.tabs.iter().any(|tab| tab.root.contains(pane)))
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!("no pane %{pane} on this machine — `tty7 pane ls` lists them")
|
|
})
|
|
}
|
|
|
|
pub fn short_id(id: &WorkspaceId) -> String {
|
|
id.to_string().chars().take(8).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::address::parse_workspace;
|
|
use crate::testbed::two_workspace_machine;
|
|
|
|
#[test]
|
|
fn tab_ordinals_number_the_machine_in_tree_order() {
|
|
let m = two_workspace_machine();
|
|
let index = tab_index(&m);
|
|
assert_eq!(index.len(), 3, "two tabs in api plus one in web");
|
|
assert_eq!(
|
|
index.iter().map(|e| e.ordinal).collect::<Vec<_>>(),
|
|
vec![1, 2, 3],
|
|
"ordinals are dense and start at @1"
|
|
);
|
|
assert_eq!(index[0].workspace, m.workspaces[0].id);
|
|
assert_eq!(index[2].workspace, m.workspaces[1].id);
|
|
assert_eq!(index[2].tab, m.workspaces[1].tabs[0].id);
|
|
}
|
|
|
|
#[test]
|
|
fn a_workspace_resolves_by_name_id_or_unique_id_prefix() {
|
|
let m = two_workspace_machine();
|
|
let api = &m.workspaces[0];
|
|
|
|
let by_name = workspace(&m, &parse_workspace("api")).unwrap();
|
|
assert_eq!(by_name.id, api.id);
|
|
|
|
let by_id = workspace(&m, &parse_workspace(&api.id.to_string())).unwrap();
|
|
assert_eq!(by_id.id, api.id);
|
|
|
|
let prefix: String = api.id.to_string().chars().take(8).collect();
|
|
let by_prefix = workspace(&m, &parse_workspace(&prefix)).unwrap();
|
|
assert_eq!(by_prefix.id, api.id);
|
|
|
|
let err = workspace(&m, &parse_workspace("nope"))
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert!(
|
|
err.contains("tty7 ls"),
|
|
"the error points at the listing: {err}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_tab_resolves_by_ordinal_or_full_id() {
|
|
let m = two_workspace_machine();
|
|
let (ws, tab) = super::tab(&m, &TabAddress::Ordinal(3)).unwrap();
|
|
assert_eq!(ws, m.workspaces[1].id);
|
|
assert_eq!(tab, m.workspaces[1].tabs[0].id);
|
|
|
|
let wanted = m.workspaces[0].tabs[1].id;
|
|
let (ws, tab) = super::tab(&m, &TabAddress::Id(wanted.to_string())).unwrap();
|
|
assert_eq!(ws, m.workspaces[0].id);
|
|
assert_eq!(tab, wanted);
|
|
|
|
let err = super::tab(&m, &TabAddress::Ordinal(9))
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert!(err.contains("@9"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_pane_is_traced_to_the_workspace_holding_it() {
|
|
let m = two_workspace_machine();
|
|
assert_eq!(workspace_of_pane(&m, 3).unwrap().id, m.workspaces[0].id);
|
|
assert_eq!(workspace_of_pane(&m, 5).unwrap().id, m.workspaces[1].id);
|
|
let err = workspace_of_pane(&m, 99).unwrap_err().to_string();
|
|
assert!(err.contains("%99"), "{err}");
|
|
}
|
|
}
|