use std::ffi::OsString;
use std::fmt;
use std::marker::PhantomData;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::{Command, PaneId, SessionId, WindowId};
mod ops;
mod planner;
mod run;
#[cfg(feature = "serde")]
mod wire;
pub use ops::{
CapturePane, KillPane, KillWindow, NewSession, NewWindow, RenameWindow, SelectLayout,
SelectPane, SelectWindow, SendKeys, SetEnvironment, SetOption, SplitWindow,
};
pub use planner::{Planner, Step, StepReason};
pub use run::{Attribution, OperationReport, OperationValue, Outcome, PlanResult, StepOutcome};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(
clippy::struct_excessive_bools,
reason = "a descriptor whose named fields are the API a caller reads"
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Effects {
pub read_only: bool,
pub destructive: bool,
pub idempotent: bool,
pub creates: Option<Scope>,
pub reads_output: bool,
}
impl Effects {
const MUTATING: Self = Self {
read_only: false,
destructive: false,
idempotent: false,
creates: None,
reads_output: false,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Scope {
Session,
Window,
Pane,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PlanValidationErrorKind {
SourceMissing,
SourceNotEarlier,
SourceOutputMissing,
SourceScopeMismatch,
SourceProvenanceMismatch,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PlanValidationError {
step: usize,
source_step: usize,
kind: PlanValidationErrorKind,
expected_scope: Scope,
source_scope: Option<Scope>,
}
impl PlanValidationError {
const fn new(
step: usize,
source_step: usize,
kind: PlanValidationErrorKind,
expected_scope: Scope,
source_scope: Option<Scope>,
) -> Self {
Self {
step,
source_step,
kind,
expected_scope,
source_scope,
}
}
#[must_use]
pub const fn step(&self) -> usize {
self.step
}
#[must_use]
pub const fn source_step(&self) -> usize {
self.source_step
}
#[must_use]
pub const fn kind(&self) -> PlanValidationErrorKind {
self.kind
}
#[must_use]
pub const fn expected_scope(&self) -> Scope {
self.expected_scope
}
#[must_use]
pub const fn source_scope(&self) -> Option<Scope> {
self.source_scope
}
}
impl fmt::Display for PlanValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"plan step {} has an invalid dependency on step {}: ",
self.step, self.source_step
)?;
match self.kind {
PlanValidationErrorKind::SourceMissing => formatter.write_str("the source is absent"),
PlanValidationErrorKind::SourceNotEarlier => {
formatter.write_str("the source is not earlier")
}
PlanValidationErrorKind::SourceOutputMissing => write!(
formatter,
"the source does not produce the requested {:?} output",
self.expected_scope
),
PlanValidationErrorKind::SourceScopeMismatch => write!(
formatter,
"the source output is {:?}, not {:?}",
self.source_scope, self.expected_scope
),
PlanValidationErrorKind::SourceProvenanceMismatch => {
formatter.write_str("the referenced output belongs to another plan")
}
}
}
}
impl std::error::Error for PlanValidationError {}
#[derive(Clone, Copy)]
pub(in crate::plan) struct SlotUse {
pub(in crate::plan) source_step: usize,
pub(in crate::plan) part: Part,
expected_scope: Scope,
producer: ProducerIdentity,
}
#[derive(Clone, Copy)]
pub(in crate::plan) struct ProducerIdentity(u64);
static NEXT_PRODUCER_ID: AtomicU64 = AtomicU64::new(1);
impl ProducerIdentity {
const fn unbound() -> Self {
Self(0)
}
fn fresh() -> Self {
let Ok(id) = NEXT_PRODUCER_ID
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
else {
#[allow(clippy::disallowed_methods)]
std::process::abort();
};
Self(id)
}
const fn matches(self, other: Self) -> bool {
self.0 == other.0
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Safety {
ReadOnly,
Mutating,
Destructive,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound = ""))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(bound = ""))]
pub struct Slot<T> {
index: usize,
part: Part,
#[cfg_attr(feature = "serde", serde(skip, default = "ProducerIdentity::unbound"))]
producer: ProducerIdentity,
#[cfg_attr(feature = "serde", serde(skip))]
scope: PhantomData<fn() -> T>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub(crate) enum Part {
Created,
FirstWindow,
FirstPane,
}
impl<T> Slot<T> {
const fn new(index: usize, part: Part, producer: ProducerIdentity) -> Self {
Self {
index,
part,
producer,
scope: PhantomData,
}
}
#[must_use]
pub const fn step(&self) -> usize {
self.index
}
}
impl<T> Clone for Slot<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Slot<T> {}
impl<T> fmt::Debug for Slot<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Slot")
.field("step", &self.index)
.field("part", &self.part)
.finish_non_exhaustive()
}
}
impl<T> PartialEq for Slot<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
&& self.part == other.part
&& self.producer.matches(other.producer)
}
}
impl<T> Eq for Slot<T> {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SessionSlot;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WindowSlot;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PaneSlot;
impl Slot<SessionSlot> {
#[must_use]
pub const fn window(self) -> Slot<WindowSlot> {
Slot::new(self.index, Part::FirstWindow, self.producer)
}
#[must_use]
pub const fn pane(self) -> Slot<PaneSlot> {
Slot::new(self.index, Part::FirstPane, self.producer)
}
}
impl Slot<WindowSlot> {
#[must_use]
pub const fn pane(self) -> Slot<PaneSlot> {
Slot::new(self.index, Part::FirstPane, self.producer)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PaneTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
#[cfg_attr(feature = "schema", schemars(schema_with = "wire::pane_id_schema"))]
PaneId,
),
Slot(Slot<PaneSlot>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum WindowTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
#[cfg_attr(feature = "schema", schemars(schema_with = "wire::window_id_schema"))]
WindowId,
),
Slot(Slot<WindowSlot>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SessionTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
#[cfg_attr(feature = "schema", schemars(schema_with = "wire::session_id_schema"))]
SessionId,
),
Slot(Slot<SessionSlot>),
}
macro_rules! target_conversions {
($target:ty, $id:ty, $marker:ty, $scope:expr) => {
impl From<$id> for $target {
fn from(id: $id) -> Self {
Self::Id(id)
}
}
impl From<Slot<$marker>> for $target {
fn from(slot: Slot<$marker>) -> Self {
Self::Slot(slot)
}
}
impl $target {
pub(in crate::plan) fn slot(&self) -> Option<SlotUse> {
match self {
Self::Id(_) => None,
Self::Slot(slot) => Some(SlotUse {
source_step: slot.index,
part: slot.part,
expected_scope: $scope,
producer: slot.producer,
}),
}
}
#[cfg(feature = "serde")]
pub(in crate::plan) fn rebind(&mut self, producers: &[Option<ProducerIdentity>]) {
if let Self::Slot(slot) = self {
if let Some(Some(producer)) = producers.get(slot.index) {
slot.producer = *producer;
}
}
}
pub(crate) fn token(
&self,
resolve: &dyn Fn(usize, Part) -> Option<OsString>,
) -> Option<OsString> {
match self {
Self::Id(id) => Some(OsString::from(id.to_string())),
Self::Slot(slot) => resolve(slot.index, slot.part),
}
}
}
};
}
target_conversions!(PaneTarget, PaneId, PaneSlot, Scope::Pane);
target_conversions!(WindowTarget, WindowId, WindowSlot, Scope::Window);
target_conversions!(SessionTarget, SessionId, SessionSlot, Scope::Session);
pub trait Operation: Into<Op> {
type Creates: FromStep;
const EFFECTS: Effects;
const SAFETY: Safety;
const MIN_VERSION: Option<(u32, u32)> = None;
}
pub trait Chainable: Operation {}
pub trait FromStep {
fn from_step(index: usize, plan: &Plan) -> Self;
}
impl FromStep for () {
fn from_step(_: usize, _: &Plan) {}
}
impl<T> FromStep for Slot<T> {
fn from_step(index: usize, plan: &Plan) -> Self {
let producer = plan.producers[index].unwrap_or_else(ProducerIdentity::unbound);
Self::new(index, Part::Created, producer)
}
}
macro_rules! operation_set {
($( $(#[$meta:meta])* $variant:ident($operation:ty) => $name:literal ),+ $(,)?) => {
#[derive(Clone, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Op {
$(
$(#[$meta])*
$variant($operation),
)+
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationKind {
$(
$(#[$meta])*
$variant,
)+
}
impl OperationKind {
pub const ALL: &'static [Self] = &[$(Self::$variant),+];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
$(Self::$variant => $name,)+
}
}
#[must_use]
pub const fn safety(self) -> Safety {
match self {
$(Self::$variant => <$operation as Operation>::SAFETY,)+
}
}
#[cfg(feature = "serde")]
#[must_use]
pub fn from_wire_name(name: &str) -> Option<Self> {
match name {
$(stringify!($variant) => Some(Self::$variant),)+
_ => None,
}
}
const fn effects(self) -> Effects {
match self {
$(Self::$variant => <$operation as Operation>::EFFECTS,)+
}
}
}
impl Op {
#[must_use]
pub const fn kind(&self) -> OperationKind {
match self {
$(Self::$variant(_) => OperationKind::$variant,)+
}
}
#[must_use]
pub const fn effects(&self) -> Effects {
self.kind().effects()
}
#[must_use]
pub const fn safety(&self) -> Safety {
self.kind().safety()
}
}
};
}
operation_set! {
NewSession(NewSession) => "new-session",
NewWindow(NewWindow) => "new-window",
SplitWindow(SplitWindow) => "split-window",
SendKeys(SendKeys) => "send-keys",
SelectPane(SelectPane) => "select-pane",
SelectWindow(SelectWindow) => "select-window",
RenameWindow(RenameWindow) => "rename-window",
SetOption(SetOption) => "set-option",
SetEnvironment(SetEnvironment) => "set-environment",
SelectLayout(SelectLayout) => "select-layout",
CapturePane(CapturePane) => "capture-pane",
KillPane(KillPane) => "kill-pane",
KillWindow(KillWindow) => "kill-window",
}
impl Op {
#[must_use]
pub const fn name(&self) -> &'static str {
self.kind().name()
}
#[must_use]
pub const fn is_chainable(&self) -> bool {
!self.effects().reads_output && self.effects().creates.is_none()
}
pub(crate) const fn focused_pane(&self) -> Option<Part> {
match self {
Self::SplitWindow(op) if op.focuses() => Some(Part::Created),
Self::NewWindow(op) if op.focuses() => Some(Part::FirstPane),
_ => None,
}
}
fn slots(&self) -> [Option<SlotUse>; 2] {
match self {
Self::NewSession(_) => [None, None],
Self::NewWindow(op) => [op.target.slot(), None],
Self::SplitWindow(op) => [op.target.slot(), None],
Self::SendKeys(op) => [op.target.slot(), None],
Self::SelectPane(op) => [op.target.slot(), None],
Self::SelectWindow(op) => [op.target.slot(), None],
Self::RenameWindow(op) => [op.target.slot(), None],
Self::SetOption(op) => [op.target(), None],
Self::SetEnvironment(op) => [op.target.slot(), None],
Self::SelectLayout(op) => [op.target.slot(), None],
Self::CapturePane(op) => [op.target.slot(), None],
Self::KillPane(op) => [op.target.slot(), None],
Self::KillWindow(op) => [op.target.slot(), None],
}
}
#[cfg(feature = "serde")]
fn rebind_slots(&mut self, producers: &[Option<ProducerIdentity>]) {
match self {
Self::NewSession(_) => {}
Self::NewWindow(op) => op.target.rebind(producers),
Self::SplitWindow(op) => op.target.rebind(producers),
Self::SendKeys(op) => op.target.rebind(producers),
Self::SelectPane(op) => op.target.rebind(producers),
Self::SelectWindow(op) => op.target.rebind(producers),
Self::RenameWindow(op) => op.target.rebind(producers),
Self::SetOption(op) => op.rebind_slots(producers),
Self::SetEnvironment(op) => op.target.rebind(producers),
Self::SelectLayout(op) => op.target.rebind(producers),
Self::CapturePane(op) => op.target.rebind(producers),
Self::KillPane(op) => op.target.rebind(producers),
Self::KillWindow(op) => op.target.rebind(producers),
}
}
fn output_scope(&self, part: Part) -> Option<Scope> {
let created = self.effects().creates?;
match (part, created) {
(Part::Created, scope) => Some(scope),
(Part::FirstWindow, Scope::Session) => Some(Scope::Window),
(Part::FirstPane, Scope::Session | Scope::Window) => Some(Scope::Pane),
_ => None,
}
}
pub(crate) fn declared_option_scope(&self) -> Option<(&std::ffi::OsStr, crate::OptionScope)> {
match self {
Self::SetOption(op) => op.declared_scope(),
_ => None,
}
}
pub(crate) fn render(
&self,
resolve: &dyn Fn(usize, Part) -> Option<OsString>,
_reserved: (),
) -> Option<Command> {
match self {
Self::NewSession(op) => Some(op.render()),
Self::NewWindow(op) => op.render(resolve),
Self::SplitWindow(op) => op.render(resolve),
Self::SendKeys(op) => op.render(resolve),
Self::SelectPane(op) => op.render(resolve),
Self::SelectWindow(op) => op.render(resolve),
Self::RenameWindow(op) => op.render(resolve),
Self::SetOption(op) => op.render(resolve),
Self::SetEnvironment(op) => op.render(resolve),
Self::SelectLayout(op) => op.render(resolve),
Self::CapturePane(op) => op.render(resolve),
Self::KillPane(op) => op.render(resolve),
Self::KillWindow(op) => op.render(resolve),
}
}
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(with = "Vec<Op>"))]
pub struct Plan {
steps: Vec<Op>,
producers: Vec<Option<ProducerIdentity>>,
}
impl fmt::Debug for Plan {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Plan")
.field("steps", &self.steps)
.finish_non_exhaustive()
}
}
impl Plan {
#[must_use]
pub const fn new() -> Self {
Self {
steps: Vec::new(),
producers: Vec::new(),
}
}
pub fn add<O: Operation>(&mut self, operation: O) -> O::Creates {
let index = self.steps.len();
self.steps.push(operation.into());
self.producers
.push(O::EFFECTS.creates.map(|_| ProducerIdentity::fresh()));
O::Creates::from_step(index, self)
}
pub fn chain<O: Chainable>(&mut self, operation: O) -> O::Creates {
self.add(operation)
}
#[must_use]
pub fn steps(&self) -> &[Op] {
&self.steps
}
#[must_use]
pub fn len(&self) -> usize {
self.steps.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.steps.is_empty()
}
pub fn validate(&self) -> Result<(), PlanValidationError> {
for (step, operation) in self.steps.iter().enumerate() {
for slot_use in operation.slots().into_iter().flatten() {
let Some(source) = self.steps.get(slot_use.source_step) else {
return Err(PlanValidationError::new(
step,
slot_use.source_step,
PlanValidationErrorKind::SourceMissing,
slot_use.expected_scope,
None,
));
};
if slot_use.source_step >= step {
return Err(PlanValidationError::new(
step,
slot_use.source_step,
PlanValidationErrorKind::SourceNotEarlier,
slot_use.expected_scope,
source.output_scope(slot_use.part),
));
}
let Some(source_scope) = source.output_scope(slot_use.part) else {
return Err(PlanValidationError::new(
step,
slot_use.source_step,
PlanValidationErrorKind::SourceOutputMissing,
slot_use.expected_scope,
None,
));
};
if source_scope != slot_use.expected_scope {
return Err(PlanValidationError::new(
step,
slot_use.source_step,
PlanValidationErrorKind::SourceScopeMismatch,
slot_use.expected_scope,
Some(source_scope),
));
}
let provenance_matches = self
.producers
.get(slot_use.source_step)
.and_then(Option::as_ref)
.is_some_and(|producer| producer.matches(slot_use.producer));
if !provenance_matches {
return Err(PlanValidationError::new(
step,
slot_use.source_step,
PlanValidationErrorKind::SourceProvenanceMismatch,
slot_use.expected_scope,
Some(source_scope),
));
}
}
}
Ok(())
}
#[must_use]
pub fn preview(&self) -> Vec<Option<Command>> {
self.steps
.iter()
.map(|op| op.render(&|_, _| None, ()))
.collect()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Plan {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::Error as _;
self.validate().map_err(S::Error::custom)?;
serde::Serialize::serialize(&self.steps, serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Plan {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
let plan = Self {
steps: Vec::<Op>::deserialize(deserializer)?,
producers: Vec::new(),
};
let mut plan = plan;
plan.producers = plan
.steps
.iter()
.map(|operation| {
operation
.effects()
.creates
.map(|_| ProducerIdentity::fresh())
})
.collect();
for operation in &mut plan.steps {
operation.rebind_slots(&plan.producers);
}
plan.validate().map_err(D::Error::custom)?;
Ok(plan)
}
}