modcli/output/
print.rs

1use crate::output::style::build;
2use crate::output::themes::current_theme;
3use std::{
4    fs::File,
5    io::{self, BufRead},
6    path::Path,
7    thread,
8    time::Duration,
9};
10
11/// Prints a single line with optional delay (ms)
12#[inline(always)]
13pub fn line(text: &str) {
14    println!("{text}");
15}
16
17/// Prints a clickable hyperlink using OSC 8 sequences when enabled.
18///
19/// By default, this function falls back to printing `text (url)` to ensure
20/// compatibility with terminals that do not support OSC 8. To enable OSC 8
21/// output, set the environment variable `ENABLE_OSC8=true`.
22///
23/// Example:
24/// ```rust
25/// use modcli::output::print;
26/// print::link("mod-cli docs", "https://docs.rs/mod-cli");
27/// ```
28pub fn link(text: &str, url: &str) {
29    let osc8_enabled = osc8_supported();
30    if osc8_enabled {
31        // OSC 8: ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL
32        // Use \x1b (ESC) and \x07 (BEL)
33        print!("\x1b]8;;{url}\x07{text}\x1b]8;;\x07");
34        println!();
35    } else {
36        println!("{text} ({url})");
37    }
38}
39
40/// Detect whether OSC 8 hyperlinks should be enabled.
41///
42/// Priority:
43/// - If ENABLE_OSC8 is explicitly set to true/false, honor it.
44/// - Otherwise auto-enable for common terminals that support OSC 8.
45fn osc8_supported() -> bool {
46    if let Ok(val) = std::env::var("ENABLE_OSC8") {
47        let v = val.to_ascii_lowercase();
48        if v == "true" || v == "1" {
49            return true;
50        }
51        if v == "false" || v == "0" {
52            return false;
53        }
54    }
55
56    // Auto-detect common terminals with OSC 8 support
57    let has = |k: &str| std::env::var_os(k).is_some();
58    let term_program = std::env::var("TERM_PROGRAM")
59        .unwrap_or_default()
60        .to_ascii_lowercase();
61    let term = std::env::var("TERM")
62        .unwrap_or_default()
63        .to_ascii_lowercase();
64
65    // WezTerm
66    if has("WEZTERM_EXECUTABLE") || term_program.contains("wezterm") {
67        return true;
68    }
69    // iTerm2
70    if term_program.contains("iterm") {
71        return true;
72    }
73    // Kitty
74    if has("KITTY_WINDOW_ID") || term.contains("kitty") {
75        return true;
76    }
77    // VTE-based (many Linux terminals)
78    if has("VTE_VERSION") {
79        return true;
80    }
81    // Windows Terminal
82    if has("WT_SESSION") {
83        return true;
84    }
85
86    false
87}
88
89/// Prints text without newline
90#[inline(always)]
91pub fn write(text: &str) {
92    print!("{text}");
93}
94
95/// Prints just a newline
96#[inline(always)]
97pub fn newline() {
98    println!();
99}
100
101/// Prints just a newline
102#[inline(always)]
103pub fn end() {
104    println!();
105}
106
107/// Scrolls through a multi-line string with optional delay
108pub fn scroll(multiline: &[&str], delay_ms: u64) {
109    let delay = if delay_ms > 0 {
110        Some(Duration::from_millis(delay_ms))
111    } else {
112        None
113    };
114    for text_line in multiline {
115        line(text_line);
116        if let Some(d) = delay {
117            thread::sleep(d);
118        }
119    }
120}
121
122/// Prints each line from a file with optional delay.
123///
124/// Behavior:
125/// - On open/read failure, logs a themed error via `print::error` and returns (no panic).
126/// - `delay_ms` controls a fixed delay between lines.
127///
128/// Example (ignore in doctest):
129/// ```ignore
130/// use modcli::output::print;
131/// print::file("./examples/banner.txt", 0);
132/// ```
133pub fn file(path: &str, delay_ms: u64) {
134    if let Ok(lines) = read_lines(path) {
135        for text_line in lines.map_while(Result::ok) {
136            line(&text_line);
137            if delay_ms > 0 {
138                thread::sleep(Duration::from_millis(delay_ms));
139            }
140        }
141    } else {
142        error("Failed to open file");
143    }
144}
145
146// Reads lines from a file, returns an iterator
147fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
148where
149    P: AsRef<Path>,
150{
151    let file = File::open(filename)?;
152    Ok(io::BufReader::new(file).lines())
153}
154
155// --- Message Shortcodes ---
156
157pub fn debug(msg: &str) {
158    let theme = current_theme();
159    let styled = build()
160        .part("Debug:")
161        .color(theme.get_log_color("debug"))
162        .space()
163        .part(msg)
164        .get();
165    line(&styled);
166}
167
168pub fn info(msg: &str) {
169    let theme = current_theme();
170    let styled = build()
171        .part("Info:")
172        .color(theme.get_log_color("info"))
173        .bold()
174        .space()
175        .part(msg)
176        .get();
177    line(&styled);
178}
179
180pub fn warn(msg: &str) {
181    let theme = current_theme();
182    let styled = build()
183        .part("Warning:")
184        .color(theme.get_log_color("warn"))
185        .bold()
186        .space()
187        .part(msg)
188        .get();
189    line(&styled);
190}
191
192pub fn error(msg: &str) {
193    let theme = current_theme();
194    let styled = build()
195        .part("Error:")
196        .color(theme.get_log_color("error"))
197        .bold()
198        .space()
199        .part(msg)
200        .get();
201    line(&styled);
202}
203
204pub fn success(msg: &str) {
205    let theme = current_theme();
206    let styled = build()
207        .part("Success:")
208        .color(theme.get_log_color("success"))
209        .bold()
210        .space()
211        .part(msg)
212        .get();
213    line(&styled);
214}
215
216pub fn status(msg: &str) {
217    let theme = current_theme();
218    let styled = build()
219        .part("Status:")
220        .color(theme.get_log_color("status"))
221        .bold()
222        .space()
223        .part(msg)
224        .get();
225    line(&styled);
226}
227
228pub fn deprecated(msg: &str) {
229    let theme = current_theme();
230    let styled = build()
231        .part("Deprecated:")
232        .color(theme.get_log_color("notice"))
233        .bold()
234        .space()
235        .part(msg)
236        .get();
237    line(&styled);
238}
239
240pub fn unknown(msg: &str) {
241    let theme = current_theme();
242    let styled = build()
243        .part("Unknown Command:")
244        .color(theme.get_log_color("notice"))
245        .bold()
246        .space()
247        .part(msg)
248        .get();
249    line(&styled);
250}