sway_command/
lib.rs

1#![warn(missing_docs)]
2//! Implements a builder for swaymsg.
3use std::vec;
4
5use commands::{CriterialessCommand, SubCommand};
6use criteria::{Criteria, CriteriaList};
7use derive_more::{AsRef, Display, From};
8
9/// Contains the types for command creation
10pub mod commands;
11/// Contains the types for criteria creation
12pub mod criteria;
13
14// TODO make AsRef a feature (maybe)
15// Without it you'd just call `.to_string()`
16// AsRef necessitates updating the `rep` string on every change... might be not
17// ideal performance wise, but maybe also doesn't matter as long as you don't
18// add criteria after the fact
19/// Create a command list able to be run via sway ipc
20#[derive(AsRef, Default)]
21pub struct CommandList {
22    // To be able to implement `AsRef<str>`
23    #[as_ref(forward)]
24    rep: String,
25    commands: Vec<Command>,
26}
27
28#[doc(hidden)]
29pub fn normalize_whitespace(value: impl AsRef<str>) -> String {
30    value
31        .as_ref()
32        .split_whitespace()
33        .collect::<Vec<_>>()
34        .join(" ")
35}
36impl CommandList {
37    /// Get the commands
38    pub fn get_commands(&self) -> &[Command] {
39        &self.commands
40    }
41    /// ```
42    /// # use sway_command::*;
43    /// # use sway_command::normalize_whitespace;
44    /// # use sway_command::commands::*;
45    /// # use sway_command::criteria::*;
46    /// let cmd = CommandList::default()
47    ///     .command("workspace 5")
48    ///     .command(SubCommand::Border(Border::None))
49    ///     .command(
50    ///         CriteriaCommand::default()
51    ///             .criteria(Criteria::Floating)
52    ///             .command(SubCommand::Floating(EnDisTog::Disable)),
53    ///     )
54    ///     .command(CriterialessCommand::Bindsym(
55    ///         Default::default(),
56    ///         SymKey::key("a"),
57    ///         SubCommand::Exit.into(),
58    ///     ));
59    /// let cmd: &str = cmd.as_ref();
60    /// assert_eq!(
61    ///     normalize_whitespace(cmd),
62    ///     "workspace 5;border none;[floating]floating disable;bindsym a exit"
63    /// );
64    /// ```
65    pub fn command(mut self, command: impl Into<Command>) -> Self {
66        let command = command.into();
67        if !self.commands.is_empty() {
68            self.rep.push(';');
69        }
70        self.rep.push_str(command.to_string().as_ref());
71        self.commands.push(command);
72        self
73    }
74}
75
76// TODO https://github.com/JelteF/derive_more/issues/219
77// #[derive(AsRef)]
78/// A Command that can be added to a [`CommandList`] or run directly
79#[derive(Display, From)]
80pub enum Command {
81    // #[as_ref(forward)]
82    /// A Command that contains criteria
83    #[from(types(SubCommand))]
84    Criteria(CriteriaCommand),
85    /// A Command without Criteria
86    #[from(types(CriterialessCommand))]
87    Criterialess(Box<CriterialessCommand>),
88    // #[from(types("&str"))]
89    /// Untyped Command
90    #[from(forward)]
91    Raw(String),
92}
93
94#[derive(AsRef, Display, Default, Clone)]
95#[display(fmt = "{rep}")]
96/// A command with an optional Criteria
97pub struct CriteriaCommand {
98    // To be able to implement `AsRef<str>`
99    #[as_ref(forward)]
100    rep: String,
101    criteria: Option<CriteriaList>,
102    commands: Vec<SubCommand>,
103}
104
105impl From<SubCommand> for CriteriaCommand {
106    fn from(cmd: SubCommand) -> Self {
107        Self {
108            rep: cmd.to_string(),
109            commands: vec![cmd],
110            criteria: Default::default(),
111        }
112    }
113}
114
115impl CriteriaCommand {
116    /// Get the commands in CriteriaCommand
117    pub fn get_commands(&self) -> &[SubCommand] {
118        &self.commands
119    }
120    /// At a new command
121    pub fn command(mut self, command: SubCommand) -> Self {
122        if !self.commands.is_empty() {
123            self.rep.push(',');
124        }
125        self.rep.push_str(&command.to_string());
126        self.commands.push(command);
127        self
128    }
129    /// Preformance note:
130    ///
131    /// When adding criteria after adding the first commands, the string
132    /// representation needs to be rebuild
133    pub fn criteria(mut self, criteria: Criteria) -> Self {
134        if self.commands.is_empty() && self.criteria.is_some() {
135            let Some(criterias) = &mut self.criteria else { unreachable!() };
136            criterias.criteria(criteria);
137            // TODO investigate if this could be replaced with `self.rep =
138            // criterias.to_string()`
139            assert_eq!(self.rep.pop(), Some(']'));
140            self.rep.push_str(" {criteria}]")
141        } else {
142            if let Some(criterias) = &mut self.criteria {
143                criterias.criteria(criteria);
144                self.rep = String::with_capacity(self.rep.len());
145                self.rep.push_str(criterias.as_ref());
146            } else {
147                self.criteria = Some(CriteriaList::new(criteria));
148                self.rep = self.criteria.as_ref().unwrap().to_string();
149            }
150            // TODO no need to rebuild, just copy the original string here, just need to
151            // remember where the commands start.
152            if !self.commands.is_empty() {
153                self.rep.push_str(&self.commands[0].to_string());
154                for command in &self.commands[1..] {
155                    self.rep.push(',');
156                    self.rep.push_str(&command.to_string());
157                }
158            }
159        }
160        self
161    }
162}