use std::collections::HashMap;
use std::ffi::OsString;
use super::planner::Planner;
use super::{Op, Part, Plan, Step};
use crate::{Command, CommandChain, Error, Server};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Outcome {
Complete,
Failed,
Skipped,
Unknown,
}
impl Outcome {
#[must_use]
pub const fn is_complete(self) -> bool {
matches!(self, Self::Complete)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Attribution {
PerCommand,
Merged,
}
#[derive(Clone, Debug)]
pub struct StepOutcome {
step: Step,
outcomes: Vec<Outcome>,
attribution: Attribution,
command: &'static str,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl StepOutcome {
#[must_use]
pub const fn step(&self) -> &Step {
&self.step
}
#[must_use]
pub fn outcomes(&self) -> &[Outcome] {
&self.outcomes
}
#[must_use]
pub const fn attribution(&self) -> Attribution {
self.attribution
}
#[must_use]
pub fn stdout(&self) -> &[u8] {
&self.stdout
}
#[must_use]
pub fn stderr(&self) -> &[u8] {
&self.stderr
}
#[must_use]
pub fn refusal(&self) -> Option<Error> {
if self.outcomes.iter().copied().all(Outcome::is_complete) {
return None;
}
Some(Error::refused(
self.command,
None,
String::from_utf8_lossy(&self.stderr).into_owned(),
None,
))
}
}
#[derive(Clone, Debug)]
pub struct PlanResult {
outcomes: Vec<Outcome>,
steps: Vec<StepOutcome>,
bound: HashMap<(usize, Part), OsString>,
dispatches: usize,
}
impl PlanResult {
#[must_use]
pub fn outcomes(&self) -> &[Outcome] {
&self.outcomes
}
#[must_use]
pub fn steps(&self) -> &[StepOutcome] {
&self.steps
}
#[must_use]
pub const fn dispatches(&self) -> usize {
self.dispatches
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.outcomes.iter().copied().all(Outcome::is_complete)
}
#[must_use]
pub fn created(&self, step: usize) -> Option<&OsString> {
self.bound.get(&(step, Part::Created))
}
}
impl Plan {
pub async fn run(&self, server: &Server, planner: Planner) -> Result<PlanResult, Error> {
let steps = planner.steps(self);
let mut bound: HashMap<(usize, Part), OsString> = HashMap::new();
let mut outcomes = vec![Outcome::Skipped; self.len()];
let mut reported = Vec::with_capacity(steps.len());
let mut dispatches = 0;
for step in steps {
let (result, marked_creation) = self.dispatch(server, &step, &bound).await?;
dispatches += 1;
let succeeded = result.success();
let step_outcomes = attribute(step.indices().len(), succeeded);
for (position, index) in step.indices().iter().enumerate() {
outcomes[*index] = step_outcomes[position];
}
if succeeded || marked_creation {
bind(&mut bound, self.steps(), &step, result.stdout());
}
reported.push(StepOutcome {
attribution: if step.indices().len() == 1 {
Attribution::PerCommand
} else {
Attribution::Merged
},
command: step
.indices()
.first()
.and_then(|index| self.steps().get(*index))
.map_or("plan", Op::name),
step,
outcomes: step_outcomes,
stdout: result.stdout().to_vec(),
stderr: result.stderr().to_vec(),
});
if !succeeded {
break;
}
}
Ok(PlanResult {
outcomes,
steps: reported,
bound,
dispatches,
})
}
async fn dispatch(
&self,
server: &Server,
step: &Step,
bound: &HashMap<(usize, Part), OsString>,
) -> Result<(crate::CommandResult, bool), Error> {
let commands = self.render_step(step, bound)?;
let marked = step.is_marked();
let mut commands = commands.into_iter();
let Some(first) = commands.next() else {
return Err(Error::CommandFailed {
command: "plan",
exit_code: None,
stderr: String::from("a plan step carried no commands"),
});
};
let result = match commands.next() {
None => server.cmd(first).await?,
Some(second) => {
let mut chain = CommandChain::new(first).then(second);
for command in commands {
chain = chain.then(command);
}
server.chain(chain).await?
}
};
Ok((result, marked))
}
fn render_step(
&self,
step: &Step,
bound: &HashMap<(usize, Part), OsString>,
) -> Result<Vec<Command>, Error> {
let marked = step
.is_marked()
.then(|| step.indices()[0])
.and_then(|index| {
self.steps()
.get(index)
.and_then(Op::focused_pane)
.map(|part| (index, part))
});
let resolve = |slot: usize, part: Part| -> Option<OsString> {
if marked == Some((slot, part)) {
return Some(OsString::from("{marked}"));
}
bound.get(&(slot, part)).cloned()
};
let mut commands = Vec::with_capacity(step.len() + 2);
for (position, index) in step.indices().iter().enumerate() {
let op = &self.steps()[*index];
let command = op
.render(&resolve, ())
.ok_or_else(|| Error::CommandFailed {
command: op.name(),
exit_code: None,
stderr: format!(
"step {index} targets an object no earlier step created; \
a plan cannot address what it has not made"
),
})?;
commands.push(command);
if step.is_marked() && position == 0 {
commands.push(Command::new("select-pane").arg("-m"));
}
}
if step.is_marked() {
commands.push(Command::new("select-pane").arg("-M"));
}
Ok(commands)
}
}
fn attribute(members: usize, succeeded: bool) -> Vec<Outcome> {
if succeeded {
return vec![Outcome::Complete; members];
}
if members == 1 {
return vec![Outcome::Failed];
}
vec![Outcome::Unknown; members]
}
fn bind(bound: &mut HashMap<(usize, Part), OsString>, ops: &[Op], step: &Step, stdout: &[u8]) {
let Some(index) = step.indices().first().copied() else {
return;
};
let Some(op) = ops.get(index) else {
return;
};
if op.effects().creates.is_none() {
return;
}
let Some(line) = String::from_utf8_lossy(stdout)
.lines()
.next()
.map(str::to_owned)
else {
return;
};
let ids: Vec<&str> = line.split_whitespace().collect();
let parts: &[Part] = match ids.len() {
3 => &[Part::Created, Part::FirstWindow, Part::FirstPane],
2 => &[Part::Created, Part::FirstPane],
1 => &[Part::Created],
_ => return,
};
for (id, part) in ids.iter().zip(parts) {
bound.insert((index, *part), OsString::from(*id));
}
}
#[cfg(feature = "control-mode")]
impl Plan {
pub async fn run_over_control_mode(
&self,
sender: &crate::control::ControlSender,
) -> Result<PlanResult, Error> {
let mut bound: HashMap<(usize, Part), OsString> = HashMap::new();
let mut outcomes = vec![Outcome::Skipped; self.len()];
let mut reported = Vec::with_capacity(self.len());
let mut dispatches = 0;
for (index, op) in self.steps().iter().enumerate() {
let resolve = |slot: usize, part: Part| bound.get(&(slot, part)).cloned();
let command = op
.render(&resolve, ())
.ok_or_else(|| Error::CommandFailed {
command: op.name(),
exit_code: None,
stderr: format!(
"step {index} targets an object no earlier step created; \
a plan cannot address what it has not made"
),
})?;
let block = sender.send(command).await?;
dispatches += 1;
let outcome = if block.succeeded() {
Outcome::Complete
} else {
Outcome::Failed
};
outcomes[index] = outcome;
let stdout = block
.output()
.iter()
.flat_map(|line| {
let mut bytes = line.as_bytes().to_vec();
bytes.push(b'\n');
bytes
})
.collect::<Vec<u8>>();
if block.succeeded() {
let step = Step::single(index);
bind(&mut bound, self.steps(), &step, &stdout);
}
reported.push(StepOutcome {
step: Step::single(index),
command: op.name(),
outcomes: vec![outcome],
attribution: Attribution::PerCommand,
stdout,
stderr: Vec::new(),
});
if !block.succeeded() {
break;
}
}
Ok(PlanResult {
outcomes,
steps: reported,
bound,
dispatches,
})
}
}