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 is_file = match pattern::is_pattern_file(&path) {
64            Ok(b) => b,
65            Err(e) => return Err(with_path(&path, &e)),
66        };
67        if !is_file {
68            continue;
69        }
70        let text = match std::fs::read_to_string(&path) {
71            Ok(text) => text,
72            // Not UTF-8 means it is not a pattern file; a file removed since listing is gone.
73            Err(e)
74                if matches!(
75                    e.kind(),
76                    io::ErrorKind::InvalidData | io::ErrorKind::NotFound
77                ) =>
78            {
79                continue;
80            }
81            Err(e) => return Err(with_path(&path, &e)),
82        };
83        let Ok(parsed) = toml::from_str::<toml::Value>(&text) else {
84            continue;
85        };
86        found = true;
87
88        let cmd_match = parsed
89            .get("command_match")
90            .and_then(|v| v.as_str())
91            .unwrap_or("(unknown)");
92        let has_success = parsed.get("success").is_some();
93        let has_failure = parsed.get("failure").is_some();
94
95        write_pattern_entry(out, cmd_match, has_success, has_failure)?;
96    }
97    Ok(found)
98}
99
100/// Print pattern entries from a single directory, returning true if any were found.
101///
102/// Each line is printed with a two-space indent so callers can add section headers.
103/// A read or write failure is printed to stderr and returns false, so callers cannot tell it
104/// apart from an empty directory.
105pub fn list_patterns_in(dir: &Path) -> bool {
106    match write_patterns_in(&mut io::stdout().lock(), dir) {
107        Ok(found) => found,
108        Err(e) => {
109            eprintln!("oo: {e}");
110            false
111        }
112    }
113}
114
115/// List learned pattern files from a single directory (legacy test helper).
116///
117/// Uses the same rendering as the `User` section of `oo patterns`: the empty-state
118/// line when nothing is listed. Returns 0 on success, and 1 with `oo: <path>: <error>`
119/// on stderr when the directory cannot be read.
120pub fn cmd_patterns_in(dir: &Path) -> i32 {
121    let result = write_patterns_in(&mut io::stdout().lock(), dir);
122    match result {
123        Ok(true) => 0,
124        Ok(false) => {
125            println!("{EMPTY_USER_PATTERNS_LINE}");
126            0
127        }
128        Err(e) => {
129            eprintln!("oo: {e}");
130            1
131        }
132    }
133}
134
135/// Write the `User (<dir>):` section, with the empty-state line when nothing is listed.
136fn write_user_section<W: Write>(out: &mut W, dir: &Path) -> io::Result<()> {
137    writeln!(out, "User ({}):", dir.display())?;
138    if !write_patterns_in(out, dir)? {
139        writeln!(out, "{EMPTY_USER_PATTERNS_LINE}")?;
140    }
141    Ok(())
142}
143
144/// Write the built-in patterns.
145fn write_builtins<W: Write>(out: &mut W) -> io::Result<()> {
146    for pat in pattern::builtins() {
147        let cmd_match = pat.command_match.as_str();
148        let has_success = pat.success.is_some();
149        let has_failure = pat.failure.is_some();
150
151        write_pattern_entry(out, cmd_match, has_success, has_failure)?;
152    }
153    Ok(())
154}
155
156/// Write the full `oo patterns` listing.
157fn write_all_patterns<W: Write>(out: &mut W) -> io::Result<()> {
158    let project_dir = std::env::current_dir()
159        .map(|cwd| init::project_patterns_dir(&cwd))
160        .ok();
161    let user_dir = learn::patterns_dir();
162
163    // Show built-in patterns first
164    writeln!(out, "Built-in ({} patterns):", pattern::builtins().len())?;
165    write_builtins(out)?;
166    writeln!(out)?;
167
168    if let Some(ref pdir) = project_dir {
169        if pdir.exists() {
170            writeln!(out, "Project ({}):", pdir.display())?;
171            write_patterns_in(out, pdir)?;
172            writeln!(out)?;
173        }
174    }
175
176    write_user_section(out, &user_dir)
177}
178
179/// List patterns from both project-local and user config directories.
180pub fn cmd_patterns() -> i32 {
181    match write_all_patterns(&mut io::stdout().lock()) {
182        Ok(()) => 0,
183        Err(e) => {
184            eprintln!("oo: {e}");
185            1
186        }
187    }
188}
189
190#[cfg(test)]
191#[path = "patterns_empty_tests.rs"]
192mod tests;