Skip to main content

trail/config/
terminal.rs

1//! The `[terminal]` config section: shell profiles, the panel's height, and
2//! when ending a shell needs a confirmation.
3//!
4//! Kept beside `schema.rs` rather than in it — the section's validation is
5//! its own, and `schema.rs` was already past the size a file should be.
6
7use serde::Deserialize;
8
9use super::schema::{invalid_value, SetConfigError};
10
11/// Terminal panel settings — see `docs/terminal_panel.md`.
12#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
13#[serde(deny_unknown_fields)]
14pub struct TerminalConfig {
15    /// Name of the profile a new shell uses. Blank means the first profile, or
16    /// the platform's own shell when there are none.
17    pub default_profile: String,
18    /// The panel's height, as a percentage of the area above the status bar.
19    pub height: u16,
20    /// When quitting Trail asks before ending the panel's shells:
21    /// `when_busy`, `always` or `never`.
22    ///
23    /// Parsed by [`crate::terminal::ConfirmPolicy::parse`].
24    pub confirm_quit: String,
25    /// When `:term close` asks before ending a shell; same values.
26    pub confirm_close: String,
27    /// Named shells a new panel shell can run.
28    pub profile: Vec<TerminalProfile>,
29}
30
31impl TerminalConfig {
32    /// Whether `name` is blank (the default) or names a configured profile.
33    #[must_use]
34    pub fn has_profile(&self, name: &str) -> bool {
35        name.trim().is_empty()
36            || self
37                .profile
38                .iter()
39                .any(|p| p.name.eq_ignore_ascii_case(name.trim()))
40    }
41
42    pub(super) fn validate(&self) -> Result<(), SetConfigError> {
43        if !TERMINAL_HEIGHT_RANGE.contains(&self.height) {
44            return Err(invalid_value(
45                "terminal.height",
46                &self.height.to_string(),
47                TERMINAL_HEIGHT_REASON,
48            ));
49        }
50        parse_confirm("terminal.confirm_quit", &self.confirm_quit)?;
51        parse_confirm("terminal.confirm_close", &self.confirm_close)?;
52        for (index, profile) in self.profile.iter().enumerate() {
53            let key = format!("terminal.profile[{}]", index + 1);
54            if profile.name.trim().is_empty() {
55                return Err(invalid_value(&key, "", "needs a name"));
56            }
57            if profile
58                .command
59                .first()
60                .map_or(true, |p| p.trim().is_empty())
61            {
62                return Err(invalid_value(
63                    &key,
64                    &profile.name,
65                    "needs a command, e.g. command = [\"pwsh.exe\", \"-NoLogo\"]",
66                ));
67            }
68            if self.profile[..index]
69                .iter()
70                .any(|p| p.name.eq_ignore_ascii_case(&profile.name))
71            {
72                return Err(invalid_value(&key, &profile.name, "is a duplicate name"));
73            }
74        }
75        if !self.has_profile(&self.default_profile) {
76            return Err(invalid_value(
77                "terminal.default_profile",
78                &self.default_profile,
79                "names no [[terminal.profile]]",
80            ));
81        }
82        Ok(())
83    }
84}
85
86/// One `[[terminal.profile]]`: a name and the program it runs.
87#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
88#[serde(deny_unknown_fields)]
89pub struct TerminalProfile {
90    /// Shown on the panel's tab, and what `:term new <name>` takes.
91    pub name: String,
92    /// The program followed by its arguments, run directly — not through a
93    /// shell, so nothing in it is expanded.
94    pub command: Vec<String>,
95}
96
97/// Accepted range for `[terminal] height`, in percent.
98pub(super) const TERMINAL_HEIGHT_RANGE: std::ops::RangeInclusive<u16> = 10..=90;
99pub(super) const TERMINAL_HEIGHT_REASON: &str = "must be a percentage between 10 and 90";
100
101pub(super) fn parse_confirm(key: &str, value: &str) -> Result<String, SetConfigError> {
102    crate::terminal::ConfirmPolicy::parse(value)
103        .ok_or_else(|| invalid_value(key, value, crate::terminal::CONFIRM_REASON))?;
104    Ok(value.trim().to_ascii_lowercase())
105}
106
107pub(super) fn parse_terminal_chord(key: &str, value: &str) -> Result<(), SetConfigError> {
108    crate::terminal::keys::KeyChord::parse(value)
109        .map(|_| ())
110        .ok_or_else(|| invalid_value(key, value, crate::terminal::keys::KEY_CHORD_REASON))
111}