Skip to main content

par_term_config/
scripting.rs

1//! Configuration types for external observer scripts.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::automation::RestartPolicy;
7
8/// Configuration for an external observer script that receives terminal events via JSON protocol.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct ScriptConfig {
11    /// Human-readable name for this script
12    pub name: String,
13
14    /// Whether this script is enabled (default: true)
15    #[serde(default = "crate::defaults::bool_true")]
16    pub enabled: bool,
17
18    /// Path to the script executable
19    pub script_path: String,
20
21    /// Arguments to pass to the script
22    #[serde(default)]
23    pub args: Vec<String>,
24
25    /// Whether to start this script automatically when a tab opens
26    #[serde(default)]
27    pub auto_start: bool,
28
29    /// Policy for restarting the script when it exits
30    #[serde(default)]
31    pub restart_policy: RestartPolicy,
32
33    /// Delay in milliseconds before restarting (when restart_policy is not Never)
34    #[serde(default)]
35    pub restart_delay_ms: u64,
36
37    /// Event types to subscribe to (empty = all events)
38    #[serde(default)]
39    pub subscriptions: Vec<String>,
40
41    /// Additional environment variables to set for the script process
42    #[serde(default)]
43    pub env_vars: HashMap<String, String>,
44
45    /// Allow this script to inject text into the active PTY via `WriteText`.
46    ///
47    /// Defaults to `false`. Must be explicitly set to `true` to enable.
48    /// When enabled, VT/ANSI escape sequences are stripped before writing
49    /// and a rate limit is applied (see `write_text_rate_limit`).
50    #[serde(default)]
51    pub allow_write_text: bool,
52
53    /// Ask before each `WriteText` injection from this script.
54    ///
55    /// Defaults to `true`, mirroring `prompt_before_run` on triggers. Stripping
56    /// escape sequences does not make an injection safe — the payload that runs
57    /// a command is ordinary printable text plus a newline — so confirmation,
58    /// not filtering, is the control on this path. Set to `false` to write
59    /// immediately; the write is still sanitized and rate limited.
60    #[serde(default = "crate::defaults::bool_true")]
61    pub prompt_before_write_text: bool,
62
63    /// Allow this script to spawn external processes via `RunCommand`.
64    ///
65    /// Defaults to `false`. Must be explicitly set to `true` to enable.
66    /// When enabled, the command is checked against the denylist and a
67    /// rate limit is applied (see `run_command_rate_limit`).
68    #[serde(default)]
69    pub allow_run_command: bool,
70
71    /// Allow this script to modify runtime configuration via `ChangeConfig`.
72    ///
73    /// Defaults to `false`. Must be explicitly set to `true` to enable.
74    /// Only keys in the runtime allowlist may be changed.
75    #[serde(default)]
76    pub allow_change_config: bool,
77
78    /// Maximum `WriteText` writes per second (0 = use default of 10/s).
79    #[serde(default)]
80    pub write_text_rate_limit: u32,
81
82    /// Maximum `RunCommand` executions per second (0 = use default of 1/s).
83    #[serde(default)]
84    pub run_command_rate_limit: u32,
85}
86
87impl ScriptConfig {
88    /// Whether this script should be started automatically when a tab is created.
89    ///
90    /// A script auto-starts only when it is both enabled and opted in via
91    /// `auto_start`; `enabled: false` disables the script entirely, including
92    /// auto-start, matching the manual start path in the Settings UI.
93    pub fn should_auto_start(&self) -> bool {
94        self.enabled && self.auto_start
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::ScriptConfig;
101
102    const MINIMAL: &str = "name: observer\nscript_path: /tmp/observer.py\n";
103
104    fn parse(yaml: &str) -> ScriptConfig {
105        serde_yaml_ng::from_str(yaml).expect("deserialize ScriptConfig")
106    }
107
108    #[test]
109    fn write_text_confirmation_defaults_on_for_configs_that_predate_the_field() {
110        let script = parse(MINIMAL);
111        assert!(script.prompt_before_write_text);
112        // The capability itself stays opt-out by default; the prompt only
113        // matters once the user has granted it.
114        assert!(!script.allow_write_text);
115    }
116
117    #[test]
118    fn write_text_confirmation_can_be_switched_off_explicitly() {
119        let script = parse(&format!(
120            "{MINIMAL}allow_write_text: true\nprompt_before_write_text: false\n"
121        ));
122        assert!(script.allow_write_text);
123        assert!(!script.prompt_before_write_text);
124    }
125
126    #[test]
127    fn write_text_confirmation_survives_a_round_trip() {
128        let mut script = parse(MINIMAL);
129        script.prompt_before_write_text = false;
130        let yaml = serde_yaml_ng::to_string(&script).expect("serialize");
131        assert_eq!(parse(&yaml), script);
132    }
133}