Skip to main content

soroban_cli/
print.rs

1use std::io::{self, Write};
2use std::{env, fmt::Display};
3
4use crate::xdr::{Error as XdrError, Transaction};
5
6use crate::{
7    config::network::Network, utils::explorer_url_for_transaction, utils::transaction_hash,
8};
9
10#[derive(Clone)]
11pub struct Print {
12    pub quiet: bool,
13}
14
15impl Print {
16    pub fn new(quiet: bool) -> Print {
17        Print { quiet }
18    }
19
20    /// Print message to stderr if not in quiet mode
21    pub fn print<T: Display + Sized>(&self, message: T) {
22        if !self.quiet {
23            eprint!("{message}");
24        }
25    }
26
27    /// Print message with newline to stderr if not in quiet mode.
28    pub fn println<T: Display + Sized>(&self, message: T) {
29        if !self.quiet {
30            eprintln!("{message}");
31        }
32    }
33
34    pub fn clear_previous_line(&self) {
35        if !self.quiet {
36            if cfg!(windows) {
37                eprint!("\x1b[2A\r\x1b[2K");
38            } else {
39                eprint!("\x1b[1A\x1b[2K\r");
40            }
41
42            io::stderr().flush().unwrap();
43        }
44    }
45
46    // Some terminals like vscode's and macOS' default terminal will not render
47    // the subsequent space if the emoji codepoints size is 2; in this case,
48    // we need an additional space. We also need an additional space if `TERM_PROGRAM` is not
49    // defined (e.g. vhs running in a docker container).
50    pub fn compute_emoji<T: Display + Sized>(&self, emoji: T) -> String {
51        if should_add_additional_space()
52            && (emoji.to_string().chars().count() == 2 || format!("{emoji}") == " ")
53        {
54            return format!("{emoji} ");
55        }
56
57        emoji.to_string()
58    }
59
60    pub fn log_explorer_url(&self, network: &Network, tx_hash: &str) {
61        if let Some(url) = explorer_url_for_transaction(network, tx_hash) {
62            self.linkln(url);
63        }
64    }
65
66    /// # Errors
67    ///
68    /// Might return an error
69    pub fn log_transaction(
70        &self,
71        tx: &Transaction,
72        network: &Network,
73        show_link: bool,
74    ) -> Result<(), XdrError> {
75        let tx_hash = transaction_hash(tx, &network.network_passphrase)?;
76        let hash = hex::encode(tx_hash);
77
78        self.infoln(format!("Transaction hash is {hash}").as_str());
79
80        if show_link {
81            self.log_explorer_url(network, &hash);
82        }
83
84        Ok(())
85    }
86}
87
88macro_rules! create_print_functions {
89    ($name:ident, $nameln:ident, $icon:expr) => {
90        impl Print {
91            #[allow(dead_code)]
92            pub fn $name<T: Display + Sized>(&self, message: T) {
93                if !self.quiet {
94                    eprint!("{} {}", self.compute_emoji($icon), message);
95                }
96            }
97
98            #[allow(dead_code)]
99            pub fn $nameln<T: Display + Sized>(&self, message: T) {
100                if !self.quiet {
101                    eprintln!("{} {}", self.compute_emoji($icon), message);
102                }
103            }
104        }
105    };
106}
107
108/// Format a number with the appropriate number of decimals, trimming trailing zeros.
109///
110/// If `n` cannot be represented as an i128 value, returns "Err(number out of bounds)".
111pub fn format_number<T: TryInto<i128>>(n: T, decimals: u32) -> String {
112    let n: i128 = match n.try_into() {
113        Ok(value) => value,
114        Err(_) => return "Err(number out of bounds)".to_string(),
115    };
116    if decimals == 0 {
117        return n.to_string();
118    }
119    let divisor = 10i128.pow(decimals);
120    let integer_part = n / divisor;
121    let fractional_part = (n % divisor).abs();
122    // Pad with leading zeros to match decimals width, then trim trailing zeros
123    let frac_str = format!("{:0width$}", fractional_part, width = decimals as usize);
124    let frac_trimmed = frac_str.trim_end_matches('0');
125
126    if frac_trimmed.is_empty() {
127        format!("{integer_part}")
128    } else {
129        // If integer_part is 0, we still want to show the sign for negative numbers (e.g. -0.5)
130        let sign = if n < 0 && integer_part == 0 { "-" } else { "" };
131        format!("{sign}{integer_part}.{frac_trimmed}")
132    }
133}
134
135fn should_add_additional_space() -> bool {
136    const TERMS: &[&str] = &["Apple_Terminal", "vscode", "unknown"];
137    let term_program = env::var("TERM_PROGRAM").unwrap_or("unknown".to_string());
138
139    if TERMS.contains(&term_program.as_str()) {
140        return true;
141    }
142
143    false
144}
145
146create_print_functions!(bucket, bucketln, "đŸĒŖ");
147create_print_functions!(check, checkln, "✅");
148create_print_functions!(error, errorln, "❌");
149create_print_functions!(globe, globeln, "🌎");
150create_print_functions!(info, infoln, "â„šī¸");
151create_print_functions!(link, linkln, "🔗");
152create_print_functions!(plus, plusln, "➕");
153create_print_functions!(save, saveln, "💾");
154create_print_functions!(search, searchln, "🔎");
155create_print_functions!(warn, warnln, "âš ī¸");
156create_print_functions!(exclaim, exclaimln, "â—ī¸");
157create_print_functions!(arrow, arrowln, "âžĄī¸");
158create_print_functions!(log, logln, "📔");
159create_print_functions!(event, eventln, "📅");
160create_print_functions!(blank, blankln, "  ");
161create_print_functions!(gear, gearln, "âš™ī¸");
162create_print_functions!(dir, dirln, "📁");
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    #[allow(clippy::unreadable_literal)]
170    fn test_format_number() {
171        assert_eq!(format_number(0i128, 7), "0");
172        assert_eq!(format_number(1234567i128, 7), "0.1234567");
173        assert_eq!(format_number(12345000i128, 7), "1.2345");
174        assert_eq!(format_number(10000000i128, 7), "1");
175        assert_eq!(format_number(123456789012345i128, 7), "12345678.9012345");
176        assert_eq!(format_number(-1234567i128, 7), "-0.1234567");
177        assert_eq!(format_number(-12345000i128, 7), "-1.2345");
178        assert_eq!(format_number(12345i128, 0), "12345");
179        assert_eq!(format_number(12345i128, 1), "1234.5");
180        assert_eq!(format_number(1i128, 7), "0.0000001");
181
182        assert_eq!(format_number(1u32, 7), "0.0000001");
183        assert_eq!(format_number(1i32, 7), "0.0000001");
184        assert_eq!(format_number(1u64, 7), "0.0000001");
185        assert_eq!(format_number(1i64, 7), "0.0000001");
186        assert_eq!(format_number(1u128, 7), "0.0000001");
187
188        let err: u128 = u128::try_from(i128::MAX).unwrap() + 1;
189        let result = format_number(err, 0);
190        assert_eq!(result, "Err(number out of bounds)");
191
192        let min: i128 = i128::MIN;
193        let result = format_number(min, 18);
194        assert_eq!(result, "-170141183460469231731.687303715884105728");
195
196        let max: i128 = i128::MAX;
197        let result = format_number(max, 18);
198        assert_eq!(result, "170141183460469231731.687303715884105727");
199    }
200}