1use std::io::{self, BufRead, Write};
38use std::process::{Command as ProcessCommand, Stdio};
39use std::time::Instant;
40
41use crate::error::{ClickError, Result};
42
43#[macro_export]
73macro_rules! echo {
74 ($msg:expr) => {
76 $crate::termui::echo($msg, true, false, None)
77 };
78 ($fmt:expr, $($arg:tt)*) => {{
80 echo!(@parse $fmt, $($arg)*)
82 }};
83 (@parse $fmt:expr, nl = $nl:expr) => {
85 $crate::termui::echo($fmt, $nl, false, None)
86 };
87 (@parse $fmt:expr, err = $err:expr) => {
88 $crate::termui::echo($fmt, true, $err, None)
89 };
90 (@parse $fmt:expr, nl = $nl:expr, err = $err:expr) => {
91 $crate::termui::echo($fmt, $nl, $err, None)
92 };
93 (@parse $fmt:expr, err = $err:expr, nl = $nl:expr) => {
94 $crate::termui::echo($fmt, $nl, $err, None)
95 };
96 (@parse $fmt:expr, color = $color:expr) => {
97 $crate::termui::echo($fmt, true, false, $color)
98 };
99 (@parse $fmt:expr, nl = $nl:expr, color = $color:expr) => {
100 $crate::termui::echo($fmt, $nl, false, $color)
101 };
102 (@parse $fmt:expr, err = $err:expr, color = $color:expr) => {
103 $crate::termui::echo($fmt, true, $err, $color)
104 };
105 (@parse $fmt:expr, nl = $nl:expr, err = $err:expr, color = $color:expr) => {
106 $crate::termui::echo($fmt, $nl, $err, $color)
107 };
108 (@parse $fmt:expr, $($arg:tt)*) => {
110 $crate::termui::echo(&format!($fmt, $($arg)*), true, false, None)
111 };
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Color {
123 Black,
125 Red,
127 Green,
129 Yellow,
131 Blue,
133 Magenta,
135 Cyan,
137 White,
139 BrightBlack,
141 BrightRed,
143 BrightGreen,
145 BrightYellow,
147 BrightBlue,
149 BrightMagenta,
151 BrightCyan,
153 BrightWhite,
155 Reset,
157}
158
159impl Color {
160 pub fn fg_code(self) -> u8 {
162 match self {
163 Color::Black => 30,
164 Color::Red => 31,
165 Color::Green => 32,
166 Color::Yellow => 33,
167 Color::Blue => 34,
168 Color::Magenta => 35,
169 Color::Cyan => 36,
170 Color::White => 37,
171 Color::BrightBlack => 90,
172 Color::BrightRed => 91,
173 Color::BrightGreen => 92,
174 Color::BrightYellow => 93,
175 Color::BrightBlue => 94,
176 Color::BrightMagenta => 95,
177 Color::BrightCyan => 96,
178 Color::BrightWhite => 97,
179 Color::Reset => 39,
180 }
181 }
182
183 pub fn bg_code(self) -> u8 {
185 match self {
186 Color::Black => 40,
187 Color::Red => 41,
188 Color::Green => 42,
189 Color::Yellow => 43,
190 Color::Blue => 44,
191 Color::Magenta => 45,
192 Color::Cyan => 46,
193 Color::White => 47,
194 Color::BrightBlack => 100,
195 Color::BrightRed => 101,
196 Color::BrightGreen => 102,
197 Color::BrightYellow => 103,
198 Color::BrightBlue => 104,
199 Color::BrightMagenta => 105,
200 Color::BrightCyan => 106,
201 Color::BrightWhite => 107,
202 Color::Reset => 49,
203 }
204 }
205}
206
207pub const BLACK: Color = Color::Black;
209pub const RED: Color = Color::Red;
210pub const GREEN: Color = Color::Green;
211pub const YELLOW: Color = Color::Yellow;
212pub const BLUE: Color = Color::Blue;
213pub const MAGENTA: Color = Color::Magenta;
214pub const CYAN: Color = Color::Cyan;
215pub const WHITE: Color = Color::White;
216pub const BRIGHT_BLACK: Color = Color::BrightBlack;
217pub const BRIGHT_RED: Color = Color::BrightRed;
218pub const BRIGHT_GREEN: Color = Color::BrightGreen;
219pub const BRIGHT_YELLOW: Color = Color::BrightYellow;
220pub const BRIGHT_BLUE: Color = Color::BrightBlue;
221pub const BRIGHT_MAGENTA: Color = Color::BrightMagenta;
222pub const BRIGHT_CYAN: Color = Color::BrightCyan;
223pub const BRIGHT_WHITE: Color = Color::BrightWhite;
224pub const RESET: Color = Color::Reset;
225
226pub fn isatty(stream: &str) -> bool {
246 use std::io::IsTerminal;
247 match stream {
248 "stdin" => std::io::stdin().is_terminal(),
249 "stdout" => std::io::stdout().is_terminal(),
250 "stderr" => std::io::stderr().is_terminal(),
251 _ => false,
252 }
253}
254
255pub fn stdout_isatty() -> bool {
257 isatty("stdout")
258}
259
260pub fn stderr_isatty() -> bool {
262 isatty("stderr")
263}
264
265pub fn stdin_isatty() -> bool {
267 isatty("stdin")
268}
269
270pub fn get_terminal_size() -> (usize, usize) {
278 if let (Ok(cols), Ok(rows)) = (std::env::var("COLUMNS"), std::env::var("LINES")) {
280 if let (Ok(w), Ok(h)) = (cols.parse::<usize>(), rows.parse::<usize>()) {
281 if w > 0 && h > 0 {
282 return (w, h);
283 }
284 }
285 }
286
287 match crossterm::terminal::size() {
289 Ok((cols, rows)) if cols > 0 && rows > 0 => (cols as usize, rows as usize),
290 _ => (80, 24),
291 }
292}
293
294pub fn clear() {
299 if stdout_isatty() {
300 print!("\x1b[2J\x1b[H");
302 let _ = io::stdout().flush();
303 }
304}
305
306fn should_use_color(color: Option<bool>, err: bool) -> bool {
316 match color {
317 Some(true) => true,
318 Some(false) => false,
319 None => {
320 if err {
321 stderr_isatty()
322 } else {
323 stdout_isatty()
324 }
325 }
326 }
327}
328
329#[allow(clippy::too_many_arguments)]
352pub fn style(
353 text: &str,
354 fg: Option<Color>,
355 bg: Option<Color>,
356 bold: bool,
357 dim: bool,
358 underline: bool,
359 overline: bool,
360 italic: bool,
361 blink: bool,
362 strikethrough: bool,
363 reset: bool,
364) -> String {
365 let mut codes = Vec::new();
366
367 if bold {
369 codes.push("1".to_string());
370 }
371 if dim {
372 codes.push("2".to_string());
373 }
374 if italic {
375 codes.push("3".to_string());
376 }
377 if underline {
378 codes.push("4".to_string());
379 }
380 if blink {
381 codes.push("5".to_string());
382 }
383 if overline {
384 codes.push("53".to_string()); }
386 if strikethrough {
387 codes.push("9".to_string());
388 }
389
390 if let Some(color) = fg {
392 codes.push(color.fg_code().to_string());
393 }
394 if let Some(color) = bg {
395 codes.push(color.bg_code().to_string());
396 }
397
398 if codes.is_empty() {
399 return text.to_string();
400 }
401
402 let style_start = format!("\x1b[{}m", codes.join(";"));
403 let style_end = if reset { "\x1b[0m" } else { "" };
404
405 format!("{}{}{}", style_start, text, style_end)
406}
407
408pub fn echo(message: &str, nl: bool, err: bool, color: Option<bool>) {
417 let output = if should_use_color(color, err) {
418 message.to_string()
419 } else {
420 strip_ansi_codes(message)
422 };
423
424 if err {
425 if nl {
426 eprintln!("{}", output);
427 let _ = io::stderr().flush();
428 } else {
429 eprint!("{}", output);
430 let _ = io::stderr().flush();
431 }
432 } else if nl {
433 println!("{}", output);
434 let _ = io::stdout().flush();
435 } else {
436 print!("{}", output);
437 let _ = io::stdout().flush();
438 }
439}
440
441#[allow(clippy::too_many_arguments)]
462pub fn secho(
463 message: &str,
464 fg: Option<Color>,
465 bg: Option<Color>,
466 bold: bool,
467 dim: bool,
468 underline: bool,
469 overline: bool,
470 italic: bool,
471 blink: bool,
472 strikethrough: bool,
473 reset: bool,
474 nl: bool,
475 err: bool,
476 color: Option<bool>,
477) {
478 let styled = if should_use_color(color, err) {
479 style(
480 message,
481 fg,
482 bg,
483 bold,
484 dim,
485 underline,
486 overline,
487 italic,
488 blink,
489 strikethrough,
490 reset,
491 )
492 } else {
493 message.to_string()
494 };
495
496 if err {
497 if nl {
498 eprintln!("{}", styled);
499 } else {
500 eprint!("{}", styled);
501 let _ = io::stderr().flush();
502 }
503 } else if nl {
504 println!("{}", styled);
505 } else {
506 print!("{}", styled);
507 let _ = io::stdout().flush();
508 }
509}
510
511pub fn echo_via_pager(text: &str, color: Option<bool>) {
534 if !stdin_isatty() || !stdout_isatty() {
536 echo(text, true, false, color);
537 return;
538 }
539
540 let pager = std::env::var("PAGER")
542 .ok()
543 .filter(|p| !p.is_empty())
544 .unwrap_or_else(|| {
545 if which_pager("less").is_some() {
547 "less".to_string()
548 } else if which_pager("more").is_some() {
549 "more".to_string()
550 } else {
551 String::new()
552 }
553 });
554
555 if pager.is_empty() {
556 echo(text, true, false, color);
558 return;
559 }
560
561 let output_text = if color == Some(false) {
563 strip_ansi_codes(text)
564 } else {
565 text.to_string()
566 };
567
568 let mut parts = pager.split_whitespace();
570 let cmd_name = match parts.next() {
571 Some(name) => name,
572 None => {
573 echo(&output_text, true, false, color);
574 return;
575 }
576 };
577
578 let mut cmd = ProcessCommand::new(cmd_name);
579
580 for arg in parts {
582 cmd.arg(arg);
583 }
584
585 if cmd_name == "less" && color != Some(false) {
587 cmd.arg("-R");
588 }
589
590 match cmd.stdin(Stdio::piped()).spawn() {
592 Ok(mut child) => {
593 if let Some(mut stdin) = child.stdin.take() {
594 let _ = stdin.write_all(output_text.as_bytes());
595 }
596 let _ = child.wait();
598 }
599 Err(_) => {
600 echo(&output_text, true, false, color);
602 }
603 }
604}
605
606fn which_pager(name: &str) -> Option<String> {
608 if let Ok(path) = std::env::var("PATH") {
609 for dir in path.split(':') {
610 let full_path = std::path::Path::new(dir).join(name);
611 if full_path.exists() {
612 return Some(full_path.to_string_lossy().into_owned());
613 }
614 }
615 }
616 None
617}
618
619pub fn launch(url: &str, wait: bool, locate: bool) -> Result<()> {
658 let (cmd, args) = get_launch_command(url, locate)?;
659
660 let mut command = ProcessCommand::new(&cmd);
661 command.args(&args);
662
663 if wait {
664 let status = command.status().map_err(|e| {
665 ClickError::usage(format!("Failed to launch '{}': {}", url, e))
666 })?;
667
668 if !status.success() {
669 return Err(ClickError::usage(format!(
670 "Launch command failed with exit code: {:?}",
671 status.code()
672 )));
673 }
674 } else {
675 command.spawn().map_err(|e| {
677 ClickError::usage(format!("Failed to launch '{}': {}", url, e))
678 })?;
679 }
680
681 Ok(())
682}
683
684fn get_launch_command(url: &str, locate: bool) -> Result<(String, Vec<String>)> {
686 #[cfg(target_os = "macos")]
687 {
688 if locate {
689 Ok(("open".to_string(), vec!["-R".to_string(), url.to_string()]))
690 } else {
691 Ok(("open".to_string(), vec![url.to_string()]))
692 }
693 }
694
695 #[cfg(target_os = "linux")]
696 {
697 if locate {
698 let path = std::path::Path::new(url);
701 if let Some(parent) = path.parent() {
702 Ok((
703 "xdg-open".to_string(),
704 vec![parent.to_string_lossy().into_owned()],
705 ))
706 } else {
707 Err(ClickError::usage(format!(
708 "Cannot locate file: {}",
709 url
710 )))
711 }
712 } else {
713 Ok(("xdg-open".to_string(), vec![url.to_string()]))
714 }
715 }
716
717 #[cfg(target_os = "windows")]
718 {
719 if locate {
720 Ok((
722 "explorer".to_string(),
723 vec!["/select,".to_string() + url],
724 ))
725 } else {
726 Ok((
728 "cmd".to_string(),
729 vec!["/c".to_string(), "start".to_string(), "".to_string(), url.to_string()],
730 ))
731 }
732 }
733
734 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
735 {
736 let _ = locate; Err(ClickError::usage(format!(
738 "Platform not supported for launch: {}",
739 url
740 )))
741 }
742}
743
744pub fn strip_ansi_codes(text: &str) -> String {
754 let mut result = String::with_capacity(text.len());
755 let mut chars = text.chars().peekable();
756
757 while let Some(c) = chars.next() {
758 if c == '\x1b' {
759 if chars.peek() == Some(&'[') {
761 chars.next(); while let Some(&next) = chars.peek() {
764 chars.next();
765 if next.is_ascii_alphabetic() {
766 break;
767 }
768 }
769 }
770 } else {
771 result.push(c);
772 }
773 }
774
775 result
776}
777
778pub fn prompt<T, F>(
801 text: &str,
802 default: Option<T>,
803 hide_input: bool,
804 confirmation: bool,
805 type_converter: F,
806) -> Result<T>
807where
808 T: Clone + std::fmt::Display,
809 F: Fn(&str) -> std::result::Result<T, String>,
810{
811 loop {
812 let prompt_text = if let Some(ref def) = default {
814 format!("{} [{}]: ", text, def)
815 } else {
816 format!("{}: ", text)
817 };
818
819 let input = if hide_input {
821 read_hidden_input(&prompt_text)?
822 } else {
823 read_line(&prompt_text)?
824 };
825
826 let value = if input.is_empty() {
828 if let Some(def) = default.clone() {
829 return Ok(def);
830 } else {
831 echo("Error: This field is required.", true, true, None);
832 continue;
833 }
834 } else {
835 input
836 };
837
838 let converted = match type_converter(&value) {
840 Ok(v) => v,
841 Err(msg) => {
842 echo(&format!("Error: {}", msg), true, true, None);
843 continue;
844 }
845 };
846
847 if confirmation {
849 let confirm_prompt = "Repeat for confirmation: ".to_string();
850 let confirm_input = if hide_input {
851 read_hidden_input(&confirm_prompt)?
852 } else {
853 read_line(&confirm_prompt)?
854 };
855
856 if confirm_input != value {
857 echo(
858 "Error: The two entered values do not match.",
859 true,
860 true,
861 None,
862 );
863 continue;
864 }
865 }
866
867 return Ok(converted);
868 }
869}
870
871pub fn confirm(text: &str, default: Option<bool>, abort: bool) -> Result<bool> {
884 let suffix = match default {
885 Some(true) => " [Y/n]: ",
886 Some(false) => " [y/N]: ",
887 None => " [y/n]: ",
888 };
889
890 loop {
891 let prompt_text = format!("{}{}", text, suffix);
892 let input = read_line(&prompt_text)?;
893 let input_lower = input.to_lowercase();
894
895 let result = if input.is_empty() {
896 default
897 } else if input_lower == "y" || input_lower == "yes" {
898 Some(true)
899 } else if input_lower == "n" || input_lower == "no" {
900 Some(false)
901 } else {
902 echo("Error: invalid input", true, true, None);
903 continue;
904 };
905
906 match result {
907 Some(true) => return Ok(true),
908 Some(false) => {
909 if abort {
910 return Err(ClickError::Abort);
911 }
912 return Ok(false);
913 }
914 None => {
915 echo("Error: invalid input", true, true, None);
916 continue;
917 }
918 }
919 }
920}
921
922pub fn getchar(echo_char: bool) -> Result<char> {
938 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
939 use crossterm::terminal;
940
941 if terminal::enable_raw_mode().is_ok() {
942 let result = loop {
943 match event::read() {
944 Ok(Event::Key(KeyEvent {
945 code, modifiers, ..
946 })) => {
947 if modifiers.contains(KeyModifiers::CONTROL) {
948 if let KeyCode::Char('c') = code {
949 break Err(ClickError::Abort);
950 }
951 }
952 match code {
953 KeyCode::Char(c) => break Ok(c),
954 KeyCode::Enter => break Ok('\n'),
955 KeyCode::Backspace => break Ok('\x7f'),
956 KeyCode::Tab => break Ok('\t'),
957 KeyCode::Esc => break Ok('\x1b'),
958 _ => continue,
959 }
960 }
961 Ok(_) => continue,
962 Err(e) => {
963 break Err(ClickError::usage(format!("Failed to read key: {}", e)))
964 }
965 }
966 };
967 let _ = terminal::disable_raw_mode();
968
969 if let Ok(c) = &result {
970 if echo_char {
971 print!("{}", c);
972 let _ = io::stdout().flush();
973 }
974 }
975 return result;
976 }
977
978 let input = read_line("")?;
980 input.chars().next().ok_or(ClickError::Abort)
981}
982
983pub fn pause(info: Option<&str>) {
989 let message = info.unwrap_or("Press any key to continue...");
990 echo(message, false, false, None);
991
992 let _ = getchar(false);
994
995 println!();
997}
998
999fn read_line(prompt: &str) -> Result<String> {
1001 if !prompt.is_empty() {
1002 print!("{}", prompt);
1003 let _ = io::stdout().flush();
1004 }
1005
1006 let stdin = io::stdin();
1007 let mut line = String::new();
1008
1009 stdin
1010 .lock()
1011 .read_line(&mut line)
1012 .map_err(|e| ClickError::usage(format!("Failed to read input: {}", e)))?;
1013
1014 if line.ends_with('\n') {
1016 line.pop();
1017 if line.ends_with('\r') {
1018 line.pop();
1019 }
1020 }
1021
1022 Ok(line)
1023}
1024
1025fn read_hidden_input(prompt: &str) -> Result<String> {
1030 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
1031 use crossterm::terminal;
1032
1033 if !prompt.is_empty() {
1034 print!("{}", prompt);
1035 let _ = io::stdout().flush();
1036 }
1037
1038 if terminal::enable_raw_mode().is_ok() {
1040 let mut input = String::new();
1041 let result = loop {
1042 match event::read() {
1043 Ok(Event::Key(KeyEvent {
1044 code, modifiers, ..
1045 })) => {
1046 if modifiers.contains(KeyModifiers::CONTROL) {
1047 if let KeyCode::Char('c') = code {
1048 break Err(ClickError::Abort);
1049 }
1050 }
1051 match code {
1052 KeyCode::Enter => break Ok(input.clone()),
1053 KeyCode::Char(c) => input.push(c),
1054 KeyCode::Backspace => {
1055 input.pop();
1056 }
1057 _ => {}
1058 }
1059 }
1060 Ok(_) => continue,
1061 Err(_) => break Ok(input.clone()),
1062 }
1063 };
1064 let _ = terminal::disable_raw_mode();
1065 println!();
1066 return result;
1067 }
1068
1069 echo("(Warning: Input will be visible)", true, true, None);
1071 read_line("")
1072}
1073
1074pub struct ProgressBar {
1100 length: usize,
1102 position: usize,
1104 label: Option<String>,
1106 show_eta: bool,
1108 show_percent: bool,
1110 show_pos: bool,
1112 width: usize,
1114 start_time: Instant,
1116 finished: bool,
1118 is_tty: bool,
1120 #[allow(dead_code)]
1122 last_output_len: usize,
1123 fill_char: char,
1125 empty_char: char,
1127}
1128
1129impl ProgressBar {
1130 pub fn new(
1141 length: usize,
1142 label: Option<&str>,
1143 show_eta: bool,
1144 show_percent: bool,
1145 show_pos: bool,
1146 width: usize,
1147 ) -> Self {
1148 let bar = Self {
1149 length,
1150 position: 0,
1151 label: label.map(String::from),
1152 show_eta,
1153 show_percent,
1154 show_pos,
1155 width,
1156 start_time: Instant::now(),
1157 finished: false,
1158 is_tty: stdout_isatty(),
1159 last_output_len: 0,
1160 fill_char: '#',
1161 empty_char: '-',
1162 };
1163
1164 bar.render_internal();
1166 bar
1167 }
1168
1169 pub fn fill_char(mut self, c: char) -> Self {
1181 self.fill_char = c;
1182 self
1183 }
1184
1185 pub fn empty_char(mut self, c: char) -> Self {
1197 self.empty_char = c;
1198 self
1199 }
1200
1201 pub fn update(&mut self, n: usize) {
1207 if self.finished {
1208 return;
1209 }
1210
1211 self.position = (self.position + n).min(self.length);
1212 self.render_internal();
1213 }
1214
1215 pub fn set_position(&mut self, pos: usize) {
1221 if self.finished {
1222 return;
1223 }
1224
1225 self.position = pos.min(self.length);
1226 self.render_internal();
1227 }
1228
1229 pub fn finish(&mut self) {
1231 if self.finished {
1232 return;
1233 }
1234
1235 self.position = self.length;
1236 self.finished = true;
1237 self.render_internal();
1238
1239 if self.is_tty {
1241 println!();
1242 }
1243 }
1244
1245 pub fn render(&self) -> String {
1247 let mut parts = Vec::new();
1248
1249 if let Some(ref label) = self.label {
1251 parts.push(label.clone());
1252 }
1253
1254 let progress = if self.length > 0 {
1256 self.position as f64 / self.length as f64
1257 } else {
1258 0.0
1259 };
1260
1261 let filled = (progress * self.width as f64) as usize;
1263 let empty = self.width.saturating_sub(filled);
1264 let bar = format!(
1265 "[{}{}]",
1266 self.fill_char.to_string().repeat(filled),
1267 self.empty_char.to_string().repeat(empty)
1268 );
1269 parts.push(bar);
1270
1271 if self.show_percent {
1273 parts.push(format!("{:3.0}%", progress * 100.0));
1274 }
1275
1276 if self.show_pos {
1278 parts.push(format!("{}/{}", self.position, self.length));
1279 }
1280
1281 if self.show_eta && self.position > 0 && !self.finished {
1283 let elapsed = self.start_time.elapsed();
1284 let rate = self.position as f64 / elapsed.as_secs_f64();
1285 let remaining = self.length - self.position;
1286 let eta_secs = if rate > 0.0 {
1287 remaining as f64 / rate
1288 } else {
1289 0.0
1290 };
1291
1292 if eta_secs < 3600.0 {
1293 let mins = (eta_secs / 60.0) as u64;
1294 let secs = (eta_secs % 60.0) as u64;
1295 parts.push(format!("eta {:02}:{:02}", mins, secs));
1296 } else {
1297 let hours = (eta_secs / 3600.0) as u64;
1298 let mins = ((eta_secs % 3600.0) / 60.0) as u64;
1299 parts.push(format!("eta {}h {:02}m", hours, mins));
1300 }
1301 }
1302
1303 parts.join(" ")
1304 }
1305
1306 fn render_internal(&self) {
1308 let output = self.render();
1309
1310 if self.is_tty {
1311 print!("\r{}", output);
1313 let clear_len = self.last_output_len.saturating_sub(output.len());
1315 if clear_len > 0 {
1316 print!("{}", " ".repeat(clear_len));
1317 print!("\r{}", output);
1318 }
1319 let _ = io::stdout().flush();
1320 } else {
1321 println!("{}", output);
1323 }
1324 }
1325}
1326
1327impl Drop for ProgressBar {
1328 fn drop(&mut self) {
1329 if !self.finished && self.is_tty {
1330 println!();
1332 }
1333 }
1334}
1335
1336pub fn progressbar<I>(
1359 iter: I,
1360 length: Option<usize>,
1361 label: Option<&str>,
1362) -> ProgressBarIter<I::IntoIter>
1363where
1364 I: IntoIterator,
1365 I::IntoIter: ExactSizeIterator,
1366{
1367 let iter = iter.into_iter();
1368 let len = length.unwrap_or_else(|| iter.len());
1369
1370 ProgressBarIter {
1371 iter,
1372 bar: ProgressBar::new(len, label, true, true, true, 30),
1373 }
1374}
1375
1376pub struct ProgressBarIter<I> {
1378 iter: I,
1379 bar: ProgressBar,
1380}
1381
1382impl<I> Iterator for ProgressBarIter<I>
1383where
1384 I: Iterator,
1385{
1386 type Item = I::Item;
1387
1388 fn next(&mut self) -> Option<Self::Item> {
1389 match self.iter.next() {
1390 Some(item) => {
1391 self.bar.update(1);
1392 Some(item)
1393 }
1394 None => {
1395 self.bar.finish();
1396 None
1397 }
1398 }
1399 }
1400
1401 fn size_hint(&self) -> (usize, Option<usize>) {
1402 self.iter.size_hint()
1403 }
1404}
1405
1406impl<I: ExactSizeIterator> ExactSizeIterator for ProgressBarIter<I> {
1407 fn len(&self) -> usize {
1408 self.iter.len()
1409 }
1410}
1411
1412pub fn edit_text(
1429 text: Option<&str>,
1430 editor: Option<&str>,
1431 extension: &str,
1432 require_save: bool,
1433) -> Result<Option<String>> {
1434 use std::fs;
1435 use std::process::Command;
1436
1437 let temp_dir = std::env::temp_dir();
1439 let temp_file = temp_dir.join(format!("click_edit_{}.{}", std::process::id(), extension));
1440
1441 if let Some(initial) = text {
1443 fs::write(&temp_file, initial)
1444 .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1445 }
1446
1447 let mtime_before = fs::metadata(&temp_file)
1449 .ok()
1450 .and_then(|m| m.modified().ok());
1451
1452 let editor_cmd = editor
1454 .map(String::from)
1455 .or_else(|| std::env::var("VISUAL").ok())
1456 .or_else(|| std::env::var("EDITOR").ok())
1457 .unwrap_or_else(|| "vi".to_string());
1458
1459 let status = Command::new(&editor_cmd)
1461 .arg(&temp_file)
1462 .status()
1463 .map_err(|e| ClickError::usage(format!("Failed to run editor '{}': {}", editor_cmd, e)))?;
1464
1465 if !status.success() {
1466 let _ = fs::remove_file(&temp_file);
1467 return Err(ClickError::usage(format!(
1468 "Editor '{}' exited with error",
1469 editor_cmd
1470 )));
1471 }
1472
1473 if require_save {
1475 let mtime_after = fs::metadata(&temp_file)
1476 .ok()
1477 .and_then(|m| m.modified().ok());
1478
1479 if mtime_before == mtime_after {
1480 let _ = fs::remove_file(&temp_file);
1481 return Ok(None);
1482 }
1483 }
1484
1485 let content = fs::read_to_string(&temp_file)
1487 .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1488
1489 let _ = fs::remove_file(&temp_file);
1491
1492 Ok(Some(content))
1493}
1494
1495#[cfg(test)]
1500mod tests {
1501 use super::*;
1502
1503 #[test]
1504 fn test_color_codes() {
1505 assert_eq!(Color::Red.fg_code(), 31);
1506 assert_eq!(Color::Red.bg_code(), 41);
1507 assert_eq!(Color::BrightGreen.fg_code(), 92);
1508 assert_eq!(Color::BrightGreen.bg_code(), 102);
1509 assert_eq!(Color::Reset.fg_code(), 39);
1510 assert_eq!(Color::Reset.bg_code(), 49);
1511 }
1512
1513 #[test]
1514 fn test_style_basic() {
1515 let styled = style(
1516 "hello", None, None, false, false, false, false, false, false, false, false,
1517 );
1518 assert_eq!(styled, "hello");
1519 }
1520
1521 #[test]
1522 fn test_style_with_color() {
1523 let styled = style(
1524 "hello",
1525 Some(Color::Red),
1526 None,
1527 false,
1528 false,
1529 false,
1530 false,
1531 false,
1532 false,
1533 false,
1534 true,
1535 );
1536 assert_eq!(styled, "\x1b[31mhello\x1b[0m");
1537 }
1538
1539 #[test]
1540 fn test_style_bold() {
1541 let styled = style(
1542 "hello", None, None, true, false, false, false, false, false, false, true,
1543 );
1544 assert_eq!(styled, "\x1b[1mhello\x1b[0m");
1545 }
1546
1547 #[test]
1548 fn test_style_multiple() {
1549 let styled = style(
1550 "hello",
1551 Some(Color::Green),
1552 Some(Color::Black),
1553 true,
1554 false,
1555 true,
1556 false,
1557 false,
1558 false,
1559 false,
1560 true,
1561 );
1562 assert!(styled.starts_with("\x1b["));
1564 assert!(styled.contains("1"));
1565 assert!(styled.contains("4"));
1566 assert!(styled.contains("32"));
1567 assert!(styled.contains("40"));
1568 assert!(styled.ends_with("\x1b[0m"));
1569 }
1570
1571 #[test]
1572 fn test_strip_ansi_codes() {
1573 let styled = "\x1b[31mhello\x1b[0m world";
1574 let stripped = strip_ansi_codes(styled);
1575 assert_eq!(stripped, "hello world");
1576
1577 let plain = "no codes here";
1578 assert_eq!(strip_ansi_codes(plain), "no codes here");
1579 }
1580
1581 #[test]
1582 fn test_strip_ansi_codes_complex() {
1583 let styled = "\x1b[1;31;40mcomplex\x1b[0m";
1584 let stripped = strip_ansi_codes(styled);
1585 assert_eq!(stripped, "complex");
1586 }
1587
1588 #[test]
1589 fn test_get_terminal_size_returns_valid() {
1590 let (width, height) = get_terminal_size();
1591 assert!(width > 0);
1592 assert!(height > 0);
1593 }
1594
1595 #[test]
1596 fn test_progress_bar_render() {
1597 let bar = ProgressBar::new(100, Some("Test"), false, true, true, 20);
1598 let output = bar.render();
1599 assert!(output.contains("Test"));
1600 assert!(output.contains("["));
1601 assert!(output.contains("]"));
1602 assert!(output.contains("0/100"));
1603 assert!(output.contains("0%"));
1604 }
1605
1606 #[test]
1607 fn test_progress_bar_update() {
1608 let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1609 bar.update(50);
1610 let output = bar.render();
1611 assert!(output.contains("50%"));
1612 }
1613
1614 #[test]
1615 fn test_progress_bar_finish() {
1616 let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1617 bar.finish();
1618 let output = bar.render();
1619 assert!(output.contains("100%"));
1620 assert!(bar.finished);
1621 }
1622
1623 #[test]
1624 fn test_progress_bar_zero_length() {
1625 let bar = ProgressBar::new(0, None, false, true, false, 10);
1626 let output = bar.render();
1627 assert!(output.contains("0%"));
1628 }
1629
1630 #[test]
1631 fn test_progress_bar_custom_chars() {
1632 let mut bar = ProgressBar::new(100, None, false, false, false, 10)
1633 .fill_char('=')
1634 .empty_char(' ');
1635 bar.set_position(50);
1636 let output = bar.render();
1637 assert!(output.contains("[===== ]"));
1639 }
1640
1641 #[test]
1642 fn test_progress_bar_unicode_chars() {
1643 let bar = ProgressBar::new(100, None, false, false, false, 4)
1644 .fill_char('\u{2588}') .empty_char('\u{2591}'); let output = bar.render();
1647 assert!(output.contains("[\u{2591}\u{2591}\u{2591}\u{2591}]"));
1649 }
1650
1651 #[test]
1652 fn test_color_constants() {
1653 assert_eq!(BLACK, Color::Black);
1654 assert_eq!(RED, Color::Red);
1655 assert_eq!(GREEN, Color::Green);
1656 assert_eq!(YELLOW, Color::Yellow);
1657 assert_eq!(BLUE, Color::Blue);
1658 assert_eq!(MAGENTA, Color::Magenta);
1659 assert_eq!(CYAN, Color::Cyan);
1660 assert_eq!(WHITE, Color::White);
1661 assert_eq!(BRIGHT_BLACK, Color::BrightBlack);
1662 assert_eq!(BRIGHT_RED, Color::BrightRed);
1663 assert_eq!(BRIGHT_GREEN, Color::BrightGreen);
1664 assert_eq!(BRIGHT_YELLOW, Color::BrightYellow);
1665 assert_eq!(BRIGHT_BLUE, Color::BrightBlue);
1666 assert_eq!(BRIGHT_MAGENTA, Color::BrightMagenta);
1667 assert_eq!(BRIGHT_CYAN, Color::BrightCyan);
1668 assert_eq!(BRIGHT_WHITE, Color::BrightWhite);
1669 assert_eq!(RESET, Color::Reset);
1670 }
1671
1672 #[test]
1673 fn test_style_all_options() {
1674 let styled = style(
1675 "test",
1676 Some(Color::Blue),
1677 Some(Color::White),
1678 true, true, true, true, true, true, true, true, );
1687 assert!(styled.starts_with("\x1b["));
1688 assert!(styled.contains("1")); assert!(styled.contains("2")); assert!(styled.contains("3")); assert!(styled.contains("4")); assert!(styled.contains("5")); assert!(styled.contains("9")); assert!(styled.contains("53")); assert!(styled.contains("34")); assert!(styled.contains("47")); assert!(styled.ends_with("\x1b[0m"));
1698 }
1699
1700 #[test]
1701 fn test_style_no_reset() {
1702 let styled = style(
1703 "hello",
1704 Some(Color::Red),
1705 None,
1706 false,
1707 false,
1708 false,
1709 false,
1710 false,
1711 false,
1712 false,
1713 false,
1714 );
1715 assert!(styled.starts_with("\x1b[31m"));
1716 assert!(!styled.ends_with("\x1b[0m"));
1717 }
1718}