Skip to main content

tmux_workspace/config/
mod.rs

1//! Parsing tmuxp-style workspace YAML.
2
3mod emit;
4mod expand;
5mod keys;
6mod locate;
7mod merge;
8mod parse;
9mod values;
10
11use std::path::PathBuf;
12use std::time::Duration;
13
14/// A workspace configuration failure.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ConfigError {
18    /// The workspace file could not be read.
19    #[error("cannot read workspace file {}", path.display())]
20    Read {
21        /// The file that was asked for.
22        path: PathBuf,
23        /// Why it could not be read.
24        source: std::io::Error,
25    },
26
27    /// The document was not valid YAML.
28    #[error("workspace configuration is not valid YAML at line {line}, column {column}: {reason}")]
29    Yaml {
30        /// The line the parser stopped on, counting from 1.
31        line: usize,
32        /// The column the parser stopped on, counting from 1.
33        column: usize,
34        /// What the parser expected and did not find.
35        reason: String,
36    },
37
38    /// The document was empty, or held more than one workspace.
39    #[error("expected exactly one workspace document, found {found}")]
40    DocumentCount {
41        /// How many documents the file held.
42        found: usize,
43    },
44
45    /// A required key was absent or the wrong shape.
46    #[error("workspace configuration is invalid at line {line}, column {column}: {path} {reason}")]
47    Invalid {
48        /// Where, as a key path such as `windows[0].panes[1]`.
49        path: String,
50        /// The line of the offending value, counting from 1. A missing key
51        /// is placed at the mapping that should have held it.
52        line: usize,
53        /// The column of the offending value, counting from 1.
54        column: usize,
55        /// What was wrong, in terms of the configuration's own vocabulary.
56        reason: String,
57    },
58}
59
60impl From<yaml_rust2::ScanError> for ConfigError {
61    fn from(error: yaml_rust2::ScanError) -> Self {
62        let mark = error.marker();
63        Self::Yaml {
64            line: mark.line(),
65            // `Marker::col` counts from zero; `ScanError`'s own `Display` adds one.
66            column: mark.col() + 1,
67            reason: error.info().to_owned(),
68        }
69    }
70}
71
72/// A key path and what is wrong there, before the source is scanned for
73/// where that path sits.
74#[derive(Debug)]
75struct Problem {
76    path: String,
77    reason: String,
78}
79
80impl Problem {
81    fn new(path: impl Into<String>, reason: impl Into<String>) -> Self {
82        Self {
83            path: path.into(),
84            reason: reason.into(),
85        }
86    }
87
88    fn locate(self, source: &str) -> ConfigError {
89        let (line, column) = locate::locate(source, &self.path);
90        ConfigError::Invalid {
91            path: self.path,
92            line,
93            column,
94            reason: self.reason,
95        }
96    }
97}
98
99/// One workspace: a session and the windows it should contain.
100#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct Workspace {
102    /// The session name to create.
103    pub session_name: String,
104    /// A working directory inherited by windows that do not set their own.
105    pub start_directory: Option<PathBuf>,
106    /// Environment variables to set on the session.
107    pub environment: Vec<(String, String)>,
108    /// Session options to apply once the session exists.
109    pub options: Vec<(String, String)>,
110    /// Global options to apply once the session exists.
111    pub global_options: Vec<(String, String)>,
112    /// Commands run in every pane before its own, in order.
113    pub shell_command_before: Vec<ShellCommand>,
114    /// Whether to keep pane commands out of the shell's history.
115    ///
116    /// A file that does not say reads as `true`, as in tmuxp.
117    pub suppress_history: bool,
118    /// The windows to create, in order.
119    pub windows: Vec<WindowConfig>,
120    /// Keys this parser recognized but does not act on.
121    ///
122    /// Reported rather than dropped, so a caller can say what part of a file
123    /// was ignored instead of leaving the difference to be discovered later.
124    pub unsupported_keys: Vec<String>,
125}
126
127/// One window and the panes it should contain.
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub struct WindowConfig {
130    /// The window name, or `None` to let tmux choose.
131    pub window_name: Option<String>,
132    /// The window index to create at, or `None` for the first free one.
133    pub window_index: Option<i32>,
134    /// A command that replaces the shell in every pane of this window that
135    /// does not set its own [`PaneConfig::shell`].
136    pub window_shell: Option<String>,
137    /// Environment variables set for the processes this window starts.
138    pub environment: Vec<(String, String)>,
139    /// A tmux layout name or specification applied after the panes exist.
140    pub layout: Option<String>,
141    /// A working directory inherited by panes that do not set their own.
142    pub start_directory: Option<PathBuf>,
143    /// Whether this window should end up selected.
144    pub focus: bool,
145    /// Window options to apply once the window exists.
146    pub options: Vec<(String, String)>,
147    /// Commands run in this window's panes before their own, in order.
148    pub shell_command_before: Vec<ShellCommand>,
149    /// Whether this window's commands stay out of the shell's history.
150    ///
151    /// `None` inherits the workspace setting.
152    pub suppress_history: Option<bool>,
153    /// The panes to create, in order. The first is the window's own pane.
154    pub panes: Vec<PaneConfig>,
155    /// Keys this parser recognized on the window but does not act on.
156    pub unsupported_keys: Vec<String>,
157}
158
159/// One pane.
160///
161/// The default is a pane that runs nothing and would press Enter after
162/// anything it were given, which is what tmuxp's `- pane`, `- blank` and an
163/// empty `-` all mean.
164#[derive(Clone, Debug, Eq, PartialEq)]
165pub struct PaneConfig {
166    /// Commands run in this pane before [`Self::shell_commands`], in order,
167    /// after the workspace's and the window's own
168    /// [`WindowConfig::shell_command_before`].
169    pub shell_command_before: Vec<ShellCommand>,
170    /// Commands to run in the pane once it exists.
171    pub shell_commands: Vec<ShellCommand>,
172    /// Environment variables set for the process this pane starts.
173    ///
174    /// `None` inherits the window's [`WindowConfig::environment`] wholesale;
175    /// `Some`, even an empty one, replaces it rather than adding to it, as
176    /// tmuxp does.
177    pub environment: Option<Vec<(String, String)>>,
178    /// The pane's working directory.
179    pub start_directory: Option<PathBuf>,
180    /// Whether this pane should end up selected.
181    pub focus: bool,
182    /// A command that replaces this pane's shell, overriding
183    /// [`WindowConfig::window_shell`] when both are given.
184    pub shell: Option<String>,
185    /// Whether to press Enter after each command, until a command sets its
186    /// own [`ShellCommand::enter`].
187    ///
188    /// tmuxp's `enter: false` types a command without running it, which is
189    /// how a file leaves something ready for the user to review. It covers
190    /// the `shell_command_before` commands typed into this pane too.
191    pub enter: bool,
192    /// How long to wait before each command, until a command sets its own.
193    pub sleep_before: Option<Duration>,
194    /// How long to wait after each command, until a command sets its own.
195    pub sleep_after: Option<Duration>,
196    /// Whether this pane's commands stay out of the shell's history.
197    ///
198    /// `None` inherits the window, then the workspace.
199    pub suppress_history: Option<bool>,
200    /// Keys this parser recognized on the pane but does not act on.
201    pub unsupported_keys: Vec<String>,
202}
203
204impl Default for PaneConfig {
205    fn default() -> Self {
206        Self {
207            shell_command_before: Vec::new(),
208            shell_commands: Vec::new(),
209            environment: None,
210            start_directory: None,
211            focus: false,
212            shell: None,
213            enter: true,
214            sleep_before: None,
215            sleep_after: None,
216            suppress_history: None,
217            unsupported_keys: Vec::new(),
218        }
219    }
220}
221
222/// One command typed into a pane, with tmuxp's per-command settings.
223///
224/// A file writes it as a string, or as a mapping with `cmd` and any of
225/// `enter`, `sleep_before` and `sleep_after`. A setting given here holds for
226/// the commands after it in the same pane until one of them sets its own,
227/// because that is what tmuxp does: `enter: false` on one command leaves the
228/// next one unentered too.
229///
230/// # Examples
231///
232/// ```
233/// use tmux_workspace::{ShellCommand, Workspace};
234///
235/// let workspace = Workspace::from_yaml(
236///     "
237/// session_name: demo
238/// windows:
239///   - panes:
240///       - shell_command:
241///           - cd src
242///           - cmd: cargo test
243///             enter: false
244/// ",
245/// )?;
246///
247/// let commands = &workspace.windows[0].panes[0].shell_commands;
248/// assert_eq!(commands[0], ShellCommand::new("cd src"));
249/// assert_eq!(commands[1].cmd, "cargo test");
250/// assert_eq!(commands[1].enter, Some(false));
251/// # Ok::<(), tmux_workspace::ConfigError>(())
252/// ```
253#[derive(Clone, Debug, Default, Eq, PartialEq)]
254pub struct ShellCommand {
255    /// The text typed into the pane.
256    pub cmd: String,
257    /// Whether to press Enter after it. `None` keeps whatever is in force.
258    pub enter: Option<bool>,
259    /// How long to wait before typing it. `None` keeps whatever is in force.
260    ///
261    /// The wait is a [`libtmux::plan::Pause`], so it happens in tmux and
262    /// holds the steps after it. tmux keeps typed input until the pane reads
263    /// it, so a sleep that only waited for a shell to start is not needed.
264    pub sleep_before: Option<Duration>,
265    /// How long to wait after typing it. `None` keeps whatever is in force.
266    pub sleep_after: Option<Duration>,
267}
268
269impl ShellCommand {
270    /// A command with no settings of its own.
271    #[must_use]
272    pub fn new(cmd: impl Into<String>) -> Self {
273        Self {
274            cmd: cmd.into(),
275            ..Self::default()
276        }
277    }
278
279    /// Whether this is the bare string form, with no settings of its own.
280    const fn is_plain(&self) -> bool {
281        self.enter.is_none() && self.sleep_before.is_none() && self.sleep_after.is_none()
282    }
283}