use super::cli::absolute_cwd;
use super::session::TernBackend;
use crate::backend::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct TernRef {
pub(super) session_id: String,
pub(super) tab_id: String,
pub(super) pane_id: String,
pub(super) session_label: String,
pub(super) base_pane: String,
pub(super) split_direction: String,
}
impl TernRef {
pub(super) fn from_session(session: &SessionRef) -> Result<Self> {
let tern = session
.backend_ref
.get("tern")
.ok_or_else(|| anyhow::anyhow!("tern session ref missing tern object"))?;
let field = |key: &str| -> Result<String> {
tern.get(key)
.and_then(id_of)
.ok_or_else(|| anyhow::anyhow!("tern session ref missing {key}"))
};
let optional = |key: &str| -> String {
tern.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
};
Ok(Self {
session_id: field("session_id")?,
tab_id: field("tab_id")?,
pane_id: field("pane_id")?,
session_label: optional("session_label"),
base_pane: optional("base_pane"),
split_direction: optional("split_direction"),
})
}
pub(super) fn to_value(&self) -> Value {
serde_json::json!({
"tern": {
"session_id": self.session_id,
"tab_id": self.tab_id,
"pane_id": self.pane_id,
"session_label": self.session_label,
"base_pane": self.base_pane,
"split_direction": self.split_direction,
}
})
}
}
pub(super) fn id_of(value: &Value) -> Option<String> {
match value {
Value::Number(number) => Some(number.to_string()),
Value::String(text) => Some(text.clone()),
_ => None,
}
}
pub(super) fn created_id(value: &Value, key: &str) -> Option<String> {
let raw = value.get(key)?;
id_of(raw).or_else(|| raw.get("id").and_then(id_of))
}
pub(super) fn session_label(spec: &SpawnSpec) -> String {
let cluster = spec
.env
.get("ONLYNE_CLUSTER")
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.unwrap_or("default");
format!("onlyne:{cluster}")
}
pub(super) fn role(spec: &SpawnSpec) -> String {
spec.env
.get("ONLYNE_ROLE")
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.unwrap_or("role")
.to_string()
}
pub(super) fn split_word(direction: SplitDirection) -> &'static str {
match direction {
SplitDirection::Right => "right",
SplitDirection::Down => "down",
}
}
pub(super) struct Listing {
pub(super) sessions: Vec<SessionRow>,
}
pub(super) struct SessionRow {
pub(super) id: String,
pub(super) name: Option<String>,
pub(super) tabs: Vec<TabRow>,
}
pub(super) struct TabRow {
pub(super) id: String,
pub(super) name: Option<String>,
pub(super) blocks: Vec<BlockRow>,
}
pub(super) struct BlockRow {
pub(super) id: String,
pub(super) title: Option<String>,
pub(super) cwd: Option<String>,
pub(super) program: Option<String>,
pub(super) command: Option<String>,
pub(super) exited: Option<Value>,
pub(super) keep_open: bool,
pub(super) focused: bool,
pub(super) live: bool,
}
fn text(row: &Value, key: &str) -> Option<String> {
row.get(key).and_then(Value::as_str).map(str::to_string)
}
pub(super) fn decode_listing(value: &Value) -> Listing {
let sessions = value
.get("sessions")
.and_then(Value::as_array)
.map(|rows| {
rows.iter()
.filter_map(|row| {
let id = row.get("id").and_then(id_of)?;
let tabs = row
.get("tabs")
.and_then(Value::as_array)
.map(|tabs| tabs.iter().filter_map(decode_tab).collect())
.unwrap_or_default();
Some(SessionRow {
id,
name: text(row, "name"),
tabs,
})
})
.collect()
})
.unwrap_or_default();
Listing { sessions }
}
fn decode_tab(row: &Value) -> Option<TabRow> {
let id = row.get("id").and_then(id_of)?;
let blocks = row
.get("blocks")
.and_then(Value::as_array)
.map(|blocks| blocks.iter().filter_map(decode_block).collect())
.unwrap_or_default();
Some(TabRow {
id,
name: text(row, "name"),
blocks,
})
}
fn decode_block(row: &Value) -> Option<BlockRow> {
Some(BlockRow {
id: row.get("id").and_then(id_of)?,
title: text(row, "title"),
cwd: text(row, "cwd"),
program: text(row, "program"),
command: text(row, "command"),
exited: row.get("exited").filter(|value| !value.is_null()).cloned(),
keep_open: row
.get("keep_open")
.and_then(Value::as_bool)
.unwrap_or(false),
focused: row.get("focused").and_then(Value::as_bool).unwrap_or(false),
live: row.get("live").and_then(Value::as_bool).unwrap_or(false),
})
}
impl TernBackend {
pub(super) fn find_or_create_session(&self, spec: &SpawnSpec) -> Result<String> {
let label = session_label(spec);
let listing = self.listing()?;
if let Some(session) = listing
.sessions
.iter()
.find(|session| session.name.as_deref() == Some(label.as_str()))
{
return Ok(session.id.clone());
}
let created = self.json(vec![
"new".into(),
"session".into(),
label.clone(),
"--cwd".into(),
absolute_cwd(&spec.cwd),
"--keep-open".into(),
"--json".into(),
])?;
let session_id = created_id(&created, "session")
.ok_or_else(|| anyhow::anyhow!("tern new session returned no session id"))?;
tracing::warn!(
label = label.as_str(),
session_id = session_id.as_str(),
"tern created a session for {label}; rename it with \
`tern rename {session_id} {label}` before spawning again"
);
Ok(session_id)
}
pub(super) fn find_or_create_tab(&self, session_id: &str, spec: &SpawnSpec) -> Result<TabSlot> {
let role = role(spec);
let listing = self.listing()?;
if let Some(session) = listing
.sessions
.iter()
.find(|session| session.id == session_id)
{
if let Some(tab) = session
.tabs
.iter()
.find(|tab| tab.name.as_deref() == Some(role.as_str()))
{
let base = base_block(tab)?;
return Ok(TabSlot {
tab_id: tab.id.clone(),
base_pane: base,
pane_count: tab.blocks.len(),
});
}
}
let created = self.json(vec![
"new".into(),
"tab".into(),
session_id.to_string(),
"--cwd".into(),
absolute_cwd(&spec.cwd),
"--keep-open".into(),
"--json".into(),
])?;
let tab_id = created_id(&created, "tab")
.ok_or_else(|| anyhow::anyhow!("tern new tab returned no tab id"))?;
let base = match created_id(&created, "block") {
Some(block) => block,
None => self
.listing()?
.tab(&tab_id)?
.blocks
.first()
.map(|block| block.id.clone())
.ok_or_else(|| {
anyhow::anyhow!("tern new tab {tab_id} made a tab with no block to split")
})?,
};
self.rename_tab(&base, &role)?;
Ok(TabSlot {
tab_id,
base_pane: base,
pane_count: 1,
})
}
pub(super) fn listing(&self) -> Result<Listing> {
Ok(decode_listing(
&self.json(vec!["ls".into(), "--json".into()])?,
))
}
fn rename_tab(&self, block_id: &str, role: &str) -> Result<()> {
self.json(vec![
"rename".into(),
block_id.into(),
role.into(),
"--json".into(),
])
.map(|_| ())
}
}
pub(super) struct TabSlot {
pub(super) tab_id: String,
pub(super) base_pane: String,
pub(super) pane_count: usize,
}
fn base_block(tab: &TabRow) -> Result<String> {
tab.blocks
.iter()
.find(|block| block.focused)
.or_else(|| tab.blocks.first())
.map(|block| block.id.clone())
.ok_or_else(|| anyhow::anyhow!("tern tab {} has no block to split", tab.id))
}
impl Listing {
pub(super) fn tab(&self, tab_id: &str) -> Result<&TabRow> {
self.sessions
.iter()
.flat_map(|session| session.tabs.iter())
.find(|tab| tab.id == tab_id)
.ok_or_else(|| anyhow::anyhow!("tern tab {tab_id} is in no session"))
}
pub(super) fn block(&self, pane_id: &str) -> Option<(&BlockRow, &TabRow, &SessionRow)> {
self.sessions.iter().find_map(|session| {
session.tabs.iter().find_map(|tab| {
tab.blocks
.iter()
.find(|block| block.id == pane_id)
.map(|block| (block, tab, session))
})
})
}
}