Skip to main content

double_o/
commands_patterns.rs

1//! Pattern listing functionality for `oo patterns` commands.
2
3use std::io::{self, Write};
4use std::path::Path;
5
6use crate::{init, learn, pattern};
7
8/// Line printed under the `User (<dir>):` header when no user pattern is listable.
9const EMPTY_USER_PATTERNS_LINE: &str = "  no learned patterns yet";
10
11/// Write a single pattern entry with flags.
12///
13/// Helper function to reduce duplication in pattern listing code.
14/// Writes the command_match with optional success/failure flags.
15fn write_pattern_entry<W: Write>(
16    out: &mut W,
17    cmd_match: &str,
18    has_success: bool,
19    has_failure: bool,
20) -> io::Result<()> {
21    if has_success || has_failure {
22        let mut flags = Vec::new();
23        if has_success {
24            flags.push("success");
25        }
26        if has_failure {
27            flags.push("failure");
28        }
29        writeln!(out, "  {cmd_match}  [{}]", flags.join("] ["))
30    } else {
31        writeln!(out, "  {cmd_match}")
32    }
33}
34
35/// Prefix an I/O error with the path that failed, as `oo: <path>: <error>` reads.
36fn with_path(path: &Path, e: &io::Error) -> io::Error {
37    io::Error::new(e.kind(), format!("{}: {e}", path.display()))
38}
39
40/// Write pattern entries from a single directory, returning true if any were found.
41///
42/// Each line is written with a two-space indent so callers can add section headers.
43/// A missing directory, or one with no parseable `.toml` files, writes nothing and
44/// returns false. Any other read failure (permission denied, I/O error) is returned
45/// as an error naming the path, so an unreadable directory is never reported as empty.
46fn write_patterns_in<W: Write>(out: &mut W, dir: &Path) -> io::Result<bool> {
47    let entries = match std::fs::read_dir(dir) {
48        Ok(e) => e,
49        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
50        Err(e) => return Err(with_path(dir, &e)),
51    };
52
53    let mut found = false;
54    for entry in entries {
55        let path = entry.map_err(|e| with_path(dir, &e))?.path();
56        if path.extension().and_then(|e| e.to_str()) != Some("toml") {
57            continue;
58        }
59        // Only regular files are pattern files. Reading a directory fails and a FIFO would
60        // block forever, so both are skipped. Metadata follows symlinks, so a dangling one is
61        // NotFound and skipped; any other stat failure (e.g. EACCES on an unsearchable dir)
62        // must surface rather than look like an empty directory.
63        let meta = match std::fs::metadata(&path) {
64            Ok(m) => m,
65            Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
66            Err(e) => return Err(with_path(&path, &e)),
67        };
68        if !meta.is_file() {
69            continue;
70        }
71        let text = match std::fs::read_to_string(&path) {
72            Ok(text) => text,
73            // Not UTF-8 means it is not a pattern file; a file removed since listing is gone.
74            Err(e)
75                if matches!(
76                    e.kind(),
77                    io::ErrorKind::InvalidData | io::ErrorKind::NotFound
78                ) =>
79            {
80                continue;
81            }
82            Err(e) => return Err(with_path(&path, &e)),
83        };
84        let Ok(parsed) = toml::from_str::<toml::Value>(&text) else {
85            continue;
86        };
87        found = true;
88
89        let cmd_match = parsed
90            .get("command_match")
91            .and_then(|v| v.as_str())
92            .unwrap_or("(unknown)");
93        let has_success = parsed.get("success").is_some();
94        let has_failure = parsed.get("failure").is_some();
95
96        write_pattern_entry(out, cmd_match, has_success, has_failure)?;
97    }
98    Ok(found)
99}
100
101/// Print pattern entries from a single directory, returning true if any were found.
102///
103/// Each line is printed with a two-space indent so callers can add section headers.
104/// A read or write failure is printed to stderr and returns false, so callers cannot tell it
105/// apart from an empty directory.
106pub fn list_patterns_in(dir: &Path) -> bool {
107    match write_patterns_in(&mut io::stdout().lock(), dir) {
108        Ok(found) => found,
109        Err(e) => {
110            eprintln!("oo: {e}");
111            false
112        }
113    }
114}
115
116/// List learned pattern files from a single directory (legacy test helper).
117///
118/// Uses the same rendering as the `User` section of `oo patterns`: the empty-state
119/// line when nothing is listed. Returns 0 on success, and 1 with `oo: <path>: <error>`
120/// on stderr when the directory cannot be read.
121pub fn cmd_patterns_in(dir: &Path) -> i32 {
122    let result = write_patterns_in(&mut io::stdout().lock(), dir);
123    match result {
124        Ok(true) => 0,
125        Ok(false) => {
126            println!("{EMPTY_USER_PATTERNS_LINE}");
127            0
128        }
129        Err(e) => {
130            eprintln!("oo: {e}");
131            1
132        }
133    }
134}
135
136/// Write the `User (<dir>):` section, with the empty-state line when nothing is listed.
137fn write_user_section<W: Write>(out: &mut W, dir: &Path) -> io::Result<()> {
138    writeln!(out, "User ({}):", dir.display())?;
139    if !write_patterns_in(out, dir)? {
140        writeln!(out, "{EMPTY_USER_PATTERNS_LINE}")?;
141    }
142    Ok(())
143}
144
145/// Write the built-in patterns.
146fn write_builtins<W: Write>(out: &mut W) -> io::Result<()> {
147    for pat in pattern::builtins() {
148        let cmd_match = pat.command_match.as_str();
149        let has_success = pat.success.is_some();
150        let has_failure = pat.failure.is_some();
151
152        write_pattern_entry(out, cmd_match, has_success, has_failure)?;
153    }
154    Ok(())
155}
156
157/// Write the full `oo patterns` listing.
158fn write_all_patterns<W: Write>(out: &mut W) -> io::Result<()> {
159    let project_dir = std::env::current_dir()
160        .map(|cwd| init::project_patterns_dir(&cwd))
161        .ok();
162    let user_dir = learn::patterns_dir();
163
164    // Show built-in patterns first
165    writeln!(out, "Built-in ({} patterns):", pattern::builtins().len())?;
166    write_builtins(out)?;
167    writeln!(out)?;
168
169    if let Some(ref pdir) = project_dir {
170        if pdir.exists() {
171            writeln!(out, "Project ({}):", pdir.display())?;
172            write_patterns_in(out, pdir)?;
173            writeln!(out)?;
174        }
175    }
176
177    write_user_section(out, &user_dir)
178}
179
180/// List patterns from both project-local and user config directories.
181pub fn cmd_patterns() -> i32 {
182    match write_all_patterns(&mut io::stdout().lock()) {
183        Ok(()) => 0,
184        Err(e) => {
185            eprintln!("oo: {e}");
186            1
187        }
188    }
189}
190
191#[cfg(test)]
192#[path = "patterns_empty_tests.rs"]
193mod tests;