Skip to main content

omnyssh_core/config/
snippets.rs

1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3
4use crate::utils::platform;
5
6/// Scope of a command snippet.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "lowercase")]
9pub enum SnippetScope {
10    /// Available for any host.
11    Global,
12    /// Available only for a specific host (identified by name).
13    Host,
14}
15
16/// A saved command snippet stored in `~/.config/omnyssh/snippets.toml`.
17/// Mirrors the format defined in ยง7 of the technical specification.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Snippet {
20    pub name: String,
21    pub command: String,
22    pub scope: SnippetScope,
23    /// Required when `scope == Host`.
24    pub host: Option<String>,
25    pub tags: Option<Vec<String>>,
26    /// Named placeholder parameters, e.g. `["service_name"]`.
27    pub params: Option<Vec<String>>,
28}
29
30/// Root container that maps to the TOML array-of-tables format.
31#[derive(Debug, Default, Serialize, Deserialize)]
32pub struct SnippetsFile {
33    #[serde(default)]
34    pub snippets: Vec<Snippet>,
35}
36
37/// Loads snippets from `~/.config/omnyssh/snippets.toml`.
38///
39/// Returns an empty `Vec` if the file does not exist yet.
40///
41/// # Errors
42/// Returns an error if the file exists but cannot be read or parsed.
43pub fn load_snippets() -> anyhow::Result<Vec<Snippet>> {
44    let path = platform::snippets_config_path().context("Cannot determine snippets config path")?;
45
46    if !path.exists() {
47        return Ok(Vec::new());
48    }
49
50    let content = std::fs::read_to_string(&path)
51        .with_context(|| format!("Failed to read {}", path.display()))?;
52
53    let file: SnippetsFile =
54        toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
55
56    Ok(file.snippets)
57}
58
59/// Persists snippets to `~/.config/omnyssh/snippets.toml`.
60///
61/// # Errors
62/// Returns an error if the directory cannot be created or the file cannot
63/// be written.
64pub fn save_snippets(snippets: &[Snippet]) -> anyhow::Result<()> {
65    let dir = platform::app_config_dir().context("Cannot determine app config directory")?;
66
67    std::fs::create_dir_all(&dir)
68        .with_context(|| format!("Failed to create directory {}", dir.display()))?;
69
70    let path = dir.join("snippets.toml");
71
72    let file = SnippetsFile {
73        snippets: snippets.to_vec(),
74    };
75    let content = toml::to_string_pretty(&file).context("Failed to serialise snippets")?;
76
77    // Write to a temp file and rename for atomic replacement (avoids a corrupt
78    // snippets.toml if the process is interrupted mid-write).
79    let tmp_path = path.with_extension("toml.tmp");
80    std::fs::write(&tmp_path, &content)
81        .with_context(|| format!("Failed to write {}", tmp_path.display()))?;
82    if let Err(e) = std::fs::rename(&tmp_path, &path) {
83        let _ = std::fs::remove_file(&tmp_path);
84        return Err(e).with_context(|| {
85            format!(
86                "Failed to rename {} to {}",
87                tmp_path.display(),
88                path.display()
89            )
90        });
91    }
92    #[cfg(unix)]
93    {
94        use std::os::unix::fs::PermissionsExt;
95        let perms = std::fs::Permissions::from_mode(0o600);
96        let _ = std::fs::set_permissions(&path, perms);
97    }
98
99    Ok(())
100}