use std::ffi::OsString;
use std::fmt;
use std::marker::PhantomData;
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, 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))]
pub enum Scope {
Session,
Window,
Pane,
}
#[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 = ""))]
pub struct Slot<T> {
index: usize,
part: Part,
#[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))]
pub(crate) enum Part {
Created,
FirstWindow,
FirstPane,
}
impl<T> Slot<T> {
pub(crate) const fn new(index: usize, part: Part) -> Self {
Self {
index,
part,
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()
}
}
impl<T> PartialEq for Slot<T> {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && self.part == other.part
}
}
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)
}
#[must_use]
pub const fn pane(self) -> Slot<PaneSlot> {
Slot::new(self.index, Part::FirstPane)
}
}
impl Slot<WindowSlot> {
#[must_use]
pub const fn pane(self) -> Slot<PaneSlot> {
Slot::new(self.index, Part::FirstPane)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PaneTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
PaneId,
),
Slot(Slot<PaneSlot>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WindowTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
WindowId,
),
Slot(Slot<WindowSlot>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SessionTarget {
Id(
#[cfg_attr(
feature = "serde",
serde(serialize_with = "wire::id", deserialize_with = "wire::parse_id")
)]
SessionId,
),
Slot(Slot<SessionSlot>),
}
macro_rules! target_conversions {
($target:ty, $id:ty, $marker:ty) => {
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(crate) const fn slot(&self) -> Option<(usize, Part)> {
match self {
Self::Id(_) => None,
Self::Slot(slot) => Some((slot.index, slot.part)),
}
}
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);
target_conversions!(WindowTarget, WindowId, WindowSlot);
target_conversions!(SessionTarget, SessionId, SessionSlot);
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) -> Self;
}
impl FromStep for () {
fn from_step(_: usize) {}
}
impl<T> FromStep for Slot<T> {
fn from_step(index: usize) -> Self {
Self::new(index, Part::Created)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Op {
NewSession(NewSession),
NewWindow(NewWindow),
SplitWindow(SplitWindow),
SendKeys(SendKeys),
SelectPane(SelectPane),
SelectWindow(SelectWindow),
RenameWindow(RenameWindow),
SetOption(SetOption),
SetEnvironment(SetEnvironment),
SelectLayout(SelectLayout),
CapturePane(CapturePane),
KillPane(KillPane),
KillWindow(KillWindow),
}
impl Op {
#[must_use]
pub const fn effects(&self) -> Effects {
match self {
Self::NewSession(_) => NewSession::EFFECTS,
Self::NewWindow(_) => NewWindow::EFFECTS,
Self::SplitWindow(_) => SplitWindow::EFFECTS,
Self::SendKeys(_) => SendKeys::EFFECTS,
Self::SelectPane(_) => SelectPane::EFFECTS,
Self::SelectWindow(_) => SelectWindow::EFFECTS,
Self::RenameWindow(_) => RenameWindow::EFFECTS,
Self::SetOption(_) => SetOption::EFFECTS,
Self::SetEnvironment(_) => SetEnvironment::EFFECTS,
Self::SelectLayout(_) => SelectLayout::EFFECTS,
Self::CapturePane(_) => CapturePane::EFFECTS,
Self::KillPane(_) => KillPane::EFFECTS,
Self::KillWindow(_) => KillWindow::EFFECTS,
}
}
#[must_use]
pub const fn safety(&self) -> Safety {
match self {
Self::NewSession(_) => NewSession::SAFETY,
Self::NewWindow(_) => NewWindow::SAFETY,
Self::SplitWindow(_) => SplitWindow::SAFETY,
Self::SendKeys(_) => SendKeys::SAFETY,
Self::SelectPane(_) => SelectPane::SAFETY,
Self::SelectWindow(_) => SelectWindow::SAFETY,
Self::RenameWindow(_) => RenameWindow::SAFETY,
Self::SetOption(_) => SetOption::SAFETY,
Self::SetEnvironment(_) => SetEnvironment::SAFETY,
Self::SelectLayout(_) => SelectLayout::SAFETY,
Self::CapturePane(_) => CapturePane::SAFETY,
Self::KillPane(_) => KillPane::SAFETY,
Self::KillWindow(_) => KillWindow::SAFETY,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::NewSession(_) => "new-session",
Self::NewWindow(_) => "new-window",
Self::SplitWindow(_) => "split-window",
Self::SendKeys(_) => "send-keys",
Self::SelectPane(_) => "select-pane",
Self::SelectWindow(_) => "select-window",
Self::RenameWindow(_) => "rename-window",
Self::SetOption(_) => "set-option",
Self::SetEnvironment(_) => "set-environment",
Self::SelectLayout(_) => "select-layout",
Self::CapturePane(_) => "capture-pane",
Self::KillPane(_) => "kill-pane",
Self::KillWindow(_) => "kill-window",
}
}
#[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,
}
}
pub(crate) fn slots(&self) -> [Option<(usize, Part)>; 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],
}
}
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, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Plan {
steps: Vec<Op>,
}
impl Plan {
#[must_use]
pub const fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn add<O: Operation>(&mut self, operation: O) -> O::Creates {
let index = self.steps.len();
self.steps.push(operation.into());
O::Creates::from_step(index)
}
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()
}
#[must_use]
pub fn preview(&self) -> Vec<Option<Command>> {
self.steps
.iter()
.map(|op| op.render(&|_, _| None, ()))
.collect()
}
}