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
665 .status()
666 .map_err(|e| ClickError::usage(format!("Failed to launch '{}': {}", url, e)))?;
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
677 .spawn()
678 .map_err(|e| ClickError::usage(format!("Failed to launch '{}': {}", url, e)))?;
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!("Cannot locate file: {}", url)))
708 }
709 } else {
710 Ok(("xdg-open".to_string(), vec![url.to_string()]))
711 }
712 }
713
714 #[cfg(target_os = "windows")]
715 {
716 if locate {
717 Ok(("explorer".to_string(), vec!["/select,".to_string() + url]))
719 } else {
720 Ok((
722 "cmd".to_string(),
723 vec![
724 "/c".to_string(),
725 "start".to_string(),
726 "".to_string(),
727 url.to_string(),
728 ],
729 ))
730 }
731 }
732
733 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
734 {
735 let _ = locate; Err(ClickError::usage(format!(
737 "Platform not supported for launch: {}",
738 url
739 )))
740 }
741}
742
743pub fn strip_ansi_codes(text: &str) -> String {
753 let mut result = String::with_capacity(text.len());
754 let mut chars = text.chars().peekable();
755
756 while let Some(c) = chars.next() {
757 if c == '\x1b' {
758 if chars.peek() == Some(&'[') {
760 chars.next(); while let Some(&next) = chars.peek() {
763 chars.next();
764 if next.is_ascii_alphabetic() {
765 break;
766 }
767 }
768 }
769 } else {
770 result.push(c);
771 }
772 }
773
774 result
775}
776
777pub fn prompt<T, F>(
800 text: &str,
801 default: Option<T>,
802 hide_input: bool,
803 confirmation: bool,
804 type_converter: F,
805) -> Result<T>
806where
807 T: Clone + std::fmt::Display,
808 F: Fn(&str) -> std::result::Result<T, String>,
809{
810 loop {
811 let prompt_text = if let Some(ref def) = default {
813 format!("{} [{}]: ", text, def)
814 } else {
815 format!("{}: ", text)
816 };
817
818 let input = if hide_input {
820 read_hidden_input(&prompt_text)?
821 } else {
822 read_line(&prompt_text)?
823 };
824
825 let value = if input.is_empty() {
827 if let Some(def) = default.clone() {
828 return Ok(def);
829 } else {
830 echo("Error: This field is required.", true, true, None);
831 continue;
832 }
833 } else {
834 input
835 };
836
837 let converted = match type_converter(&value) {
839 Ok(v) => v,
840 Err(msg) => {
841 echo(&format!("Error: {}", msg), true, true, None);
842 continue;
843 }
844 };
845
846 if confirmation {
848 let confirm_prompt = "Repeat for confirmation: ".to_string();
849 let confirm_input = if hide_input {
850 read_hidden_input(&confirm_prompt)?
851 } else {
852 read_line(&confirm_prompt)?
853 };
854
855 if confirm_input != value {
856 echo(
857 "Error: The two entered values do not match.",
858 true,
859 true,
860 None,
861 );
862 continue;
863 }
864 }
865
866 return Ok(converted);
867 }
868}
869
870pub fn confirm(text: &str, default: Option<bool>, abort: bool) -> Result<bool> {
883 let suffix = match default {
884 Some(true) => " [Y/n]: ",
885 Some(false) => " [y/N]: ",
886 None => " [y/n]: ",
887 };
888
889 loop {
890 let prompt_text = format!("{}{}", text, suffix);
891 let input = read_line(&prompt_text)?;
892 let input_lower = input.to_lowercase();
893
894 let result = if input.is_empty() {
895 default
896 } else if input_lower == "y" || input_lower == "yes" {
897 Some(true)
898 } else if input_lower == "n" || input_lower == "no" {
899 Some(false)
900 } else {
901 echo("Error: invalid input", true, true, None);
902 continue;
903 };
904
905 match result {
906 Some(true) => return Ok(true),
907 Some(false) => {
908 if abort {
909 return Err(ClickError::Abort);
910 }
911 return Ok(false);
912 }
913 None => {
914 echo("Error: invalid input", true, true, None);
915 continue;
916 }
917 }
918 }
919}
920
921pub fn getchar(echo_char: bool) -> Result<char> {
937 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
938 use crossterm::terminal;
939
940 if terminal::enable_raw_mode().is_ok() {
941 let result = loop {
942 match event::read() {
943 Ok(Event::Key(KeyEvent {
944 code, modifiers, ..
945 })) => {
946 if modifiers.contains(KeyModifiers::CONTROL) {
947 if let KeyCode::Char('c') = code {
948 break Err(ClickError::Abort);
949 }
950 }
951 match code {
952 KeyCode::Char(c) => break Ok(c),
953 KeyCode::Enter => break Ok('\n'),
954 KeyCode::Backspace => break Ok('\x7f'),
955 KeyCode::Tab => break Ok('\t'),
956 KeyCode::Esc => break Ok('\x1b'),
957 _ => continue,
958 }
959 }
960 Ok(_) => continue,
961 Err(e) => break Err(ClickError::usage(format!("Failed to read key: {}", e))),
962 }
963 };
964 let _ = terminal::disable_raw_mode();
965
966 if let Ok(c) = &result {
967 if echo_char {
968 print!("{}", c);
969 let _ = io::stdout().flush();
970 }
971 }
972 return result;
973 }
974
975 let input = read_line("")?;
977 input.chars().next().ok_or(ClickError::Abort)
978}
979
980pub fn pause(info: Option<&str>) {
986 let message = info.unwrap_or("Press any key to continue...");
987 echo(message, false, false, None);
988
989 let _ = getchar(false);
991
992 println!();
994}
995
996fn read_line(prompt: &str) -> Result<String> {
998 if !prompt.is_empty() {
999 print!("{}", prompt);
1000 let _ = io::stdout().flush();
1001 }
1002
1003 let stdin = io::stdin();
1004 let mut line = String::new();
1005
1006 stdin
1007 .lock()
1008 .read_line(&mut line)
1009 .map_err(|e| ClickError::usage(format!("Failed to read input: {}", e)))?;
1010
1011 if line.ends_with('\n') {
1013 line.pop();
1014 if line.ends_with('\r') {
1015 line.pop();
1016 }
1017 }
1018
1019 Ok(line)
1020}
1021
1022fn read_hidden_input(prompt: &str) -> Result<String> {
1027 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
1028 use crossterm::terminal;
1029
1030 if !prompt.is_empty() {
1031 print!("{}", prompt);
1032 let _ = io::stdout().flush();
1033 }
1034
1035 if terminal::enable_raw_mode().is_ok() {
1037 let mut input = String::new();
1038 let result = loop {
1039 match event::read() {
1040 Ok(Event::Key(KeyEvent {
1041 code, modifiers, ..
1042 })) => {
1043 if modifiers.contains(KeyModifiers::CONTROL) {
1044 if let KeyCode::Char('c') = code {
1045 break Err(ClickError::Abort);
1046 }
1047 }
1048 match code {
1049 KeyCode::Enter => break Ok(input.clone()),
1050 KeyCode::Char(c) => input.push(c),
1051 KeyCode::Backspace => {
1052 input.pop();
1053 }
1054 _ => {}
1055 }
1056 }
1057 Ok(_) => continue,
1058 Err(_) => break Ok(input.clone()),
1059 }
1060 };
1061 let _ = terminal::disable_raw_mode();
1062 println!();
1063 return result;
1064 }
1065
1066 echo("(Warning: Input will be visible)", true, true, None);
1068 read_line("")
1069}
1070
1071pub struct ProgressBar {
1097 length: usize,
1099 position: usize,
1101 label: Option<String>,
1103 show_eta: bool,
1105 show_percent: bool,
1107 show_pos: bool,
1109 width: usize,
1111 start_time: Instant,
1113 finished: bool,
1115 is_tty: bool,
1117 #[allow(dead_code)]
1119 last_output_len: usize,
1120 fill_char: char,
1122 empty_char: char,
1124}
1125
1126impl ProgressBar {
1127 pub fn new(
1138 length: usize,
1139 label: Option<&str>,
1140 show_eta: bool,
1141 show_percent: bool,
1142 show_pos: bool,
1143 width: usize,
1144 ) -> Self {
1145 let bar = Self {
1146 length,
1147 position: 0,
1148 label: label.map(String::from),
1149 show_eta,
1150 show_percent,
1151 show_pos,
1152 width,
1153 start_time: Instant::now(),
1154 finished: false,
1155 is_tty: stdout_isatty(),
1156 last_output_len: 0,
1157 fill_char: '#',
1158 empty_char: '-',
1159 };
1160
1161 bar.render_internal();
1163 bar
1164 }
1165
1166 pub fn fill_char(mut self, c: char) -> Self {
1178 self.fill_char = c;
1179 self
1180 }
1181
1182 pub fn empty_char(mut self, c: char) -> Self {
1194 self.empty_char = c;
1195 self
1196 }
1197
1198 pub fn update(&mut self, n: usize) {
1204 if self.finished {
1205 return;
1206 }
1207
1208 self.position = (self.position + n).min(self.length);
1209 self.render_internal();
1210 }
1211
1212 pub fn set_position(&mut self, pos: usize) {
1218 if self.finished {
1219 return;
1220 }
1221
1222 self.position = pos.min(self.length);
1223 self.render_internal();
1224 }
1225
1226 pub fn finish(&mut self) {
1228 if self.finished {
1229 return;
1230 }
1231
1232 self.position = self.length;
1233 self.finished = true;
1234 self.render_internal();
1235
1236 if self.is_tty {
1238 println!();
1239 }
1240 }
1241
1242 pub fn render(&self) -> String {
1244 let mut parts = Vec::new();
1245
1246 if let Some(ref label) = self.label {
1248 parts.push(label.clone());
1249 }
1250
1251 let progress = if self.length > 0 {
1253 self.position as f64 / self.length as f64
1254 } else {
1255 0.0
1256 };
1257
1258 let filled = (progress * self.width as f64) as usize;
1260 let empty = self.width.saturating_sub(filled);
1261 let bar = format!(
1262 "[{}{}]",
1263 self.fill_char.to_string().repeat(filled),
1264 self.empty_char.to_string().repeat(empty)
1265 );
1266 parts.push(bar);
1267
1268 if self.show_percent {
1270 parts.push(format!("{:3.0}%", progress * 100.0));
1271 }
1272
1273 if self.show_pos {
1275 parts.push(format!("{}/{}", self.position, self.length));
1276 }
1277
1278 if self.show_eta && self.position > 0 && !self.finished {
1280 let elapsed = self.start_time.elapsed();
1281 let rate = self.position as f64 / elapsed.as_secs_f64();
1282 let remaining = self.length - self.position;
1283 let eta_secs = if rate > 0.0 {
1284 remaining as f64 / rate
1285 } else {
1286 0.0
1287 };
1288
1289 if eta_secs < 3600.0 {
1290 let mins = (eta_secs / 60.0) as u64;
1291 let secs = (eta_secs % 60.0) as u64;
1292 parts.push(format!("eta {:02}:{:02}", mins, secs));
1293 } else {
1294 let hours = (eta_secs / 3600.0) as u64;
1295 let mins = ((eta_secs % 3600.0) / 60.0) as u64;
1296 parts.push(format!("eta {}h {:02}m", hours, mins));
1297 }
1298 }
1299
1300 parts.join(" ")
1301 }
1302
1303 fn render_internal(&self) {
1305 let output = self.render();
1306
1307 if self.is_tty {
1308 print!("\r{}", output);
1310 let clear_len = self.last_output_len.saturating_sub(output.len());
1312 if clear_len > 0 {
1313 print!("{}", " ".repeat(clear_len));
1314 print!("\r{}", output);
1315 }
1316 let _ = io::stdout().flush();
1317 } else {
1318 println!("{}", output);
1320 }
1321 }
1322}
1323
1324impl Drop for ProgressBar {
1325 fn drop(&mut self) {
1326 if !self.finished && self.is_tty {
1327 println!();
1329 }
1330 }
1331}
1332
1333pub fn progressbar<I>(
1356 iter: I,
1357 length: Option<usize>,
1358 label: Option<&str>,
1359) -> ProgressBarIter<I::IntoIter>
1360where
1361 I: IntoIterator,
1362 I::IntoIter: ExactSizeIterator,
1363{
1364 let iter = iter.into_iter();
1365 let len = length.unwrap_or_else(|| iter.len());
1366
1367 ProgressBarIter {
1368 iter,
1369 bar: ProgressBar::new(len, label, true, true, true, 30),
1370 }
1371}
1372
1373pub struct ProgressBarIter<I> {
1375 iter: I,
1376 bar: ProgressBar,
1377}
1378
1379impl<I> Iterator for ProgressBarIter<I>
1380where
1381 I: Iterator,
1382{
1383 type Item = I::Item;
1384
1385 fn next(&mut self) -> Option<Self::Item> {
1386 match self.iter.next() {
1387 Some(item) => {
1388 self.bar.update(1);
1389 Some(item)
1390 }
1391 None => {
1392 self.bar.finish();
1393 None
1394 }
1395 }
1396 }
1397
1398 fn size_hint(&self) -> (usize, Option<usize>) {
1399 self.iter.size_hint()
1400 }
1401}
1402
1403impl<I: ExactSizeIterator> ExactSizeIterator for ProgressBarIter<I> {
1404 fn len(&self) -> usize {
1405 self.iter.len()
1406 }
1407}
1408
1409pub fn edit_text(
1426 text: Option<&str>,
1427 editor: Option<&str>,
1428 extension: &str,
1429 require_save: bool,
1430) -> Result<Option<String>> {
1431 use std::fs;
1432 use std::process::Command;
1433
1434 let temp_dir = std::env::temp_dir();
1436 let temp_file = temp_dir.join(format!("click_edit_{}.{}", std::process::id(), extension));
1437
1438 if let Some(initial) = text {
1440 fs::write(&temp_file, initial)
1441 .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1442 }
1443
1444 let mtime_before = fs::metadata(&temp_file)
1446 .ok()
1447 .and_then(|m| m.modified().ok());
1448
1449 let editor_cmd = editor
1451 .map(String::from)
1452 .or_else(|| std::env::var("VISUAL").ok())
1453 .or_else(|| std::env::var("EDITOR").ok())
1454 .unwrap_or_else(|| "vi".to_string());
1455
1456 let status = Command::new(&editor_cmd)
1458 .arg(&temp_file)
1459 .status()
1460 .map_err(|e| ClickError::usage(format!("Failed to run editor '{}': {}", editor_cmd, e)))?;
1461
1462 if !status.success() {
1463 let _ = fs::remove_file(&temp_file);
1464 return Err(ClickError::usage(format!(
1465 "Editor '{}' exited with error",
1466 editor_cmd
1467 )));
1468 }
1469
1470 if require_save {
1472 let mtime_after = fs::metadata(&temp_file)
1473 .ok()
1474 .and_then(|m| m.modified().ok());
1475
1476 if mtime_before == mtime_after {
1477 let _ = fs::remove_file(&temp_file);
1478 return Ok(None);
1479 }
1480 }
1481
1482 let content = fs::read_to_string(&temp_file)
1484 .map_err(|e| ClickError::file_error(&temp_file, e.to_string()))?;
1485
1486 let _ = fs::remove_file(&temp_file);
1488
1489 Ok(Some(content))
1490}
1491
1492#[cfg(test)]
1497mod tests {
1498 use super::*;
1499
1500 #[test]
1501 fn test_color_codes() {
1502 assert_eq!(Color::Red.fg_code(), 31);
1503 assert_eq!(Color::Red.bg_code(), 41);
1504 assert_eq!(Color::BrightGreen.fg_code(), 92);
1505 assert_eq!(Color::BrightGreen.bg_code(), 102);
1506 assert_eq!(Color::Reset.fg_code(), 39);
1507 assert_eq!(Color::Reset.bg_code(), 49);
1508 }
1509
1510 #[test]
1511 fn test_style_basic() {
1512 let styled = style(
1513 "hello", None, None, false, false, false, false, false, false, false, false,
1514 );
1515 assert_eq!(styled, "hello");
1516 }
1517
1518 #[test]
1519 fn test_style_with_color() {
1520 let styled = style(
1521 "hello",
1522 Some(Color::Red),
1523 None,
1524 false,
1525 false,
1526 false,
1527 false,
1528 false,
1529 false,
1530 false,
1531 true,
1532 );
1533 assert_eq!(styled, "\x1b[31mhello\x1b[0m");
1534 }
1535
1536 #[test]
1537 fn test_style_bold() {
1538 let styled = style(
1539 "hello", None, None, true, false, false, false, false, false, false, true,
1540 );
1541 assert_eq!(styled, "\x1b[1mhello\x1b[0m");
1542 }
1543
1544 #[test]
1545 fn test_style_multiple() {
1546 let styled = style(
1547 "hello",
1548 Some(Color::Green),
1549 Some(Color::Black),
1550 true,
1551 false,
1552 true,
1553 false,
1554 false,
1555 false,
1556 false,
1557 true,
1558 );
1559 assert!(styled.starts_with("\x1b["));
1561 assert!(styled.contains("1"));
1562 assert!(styled.contains("4"));
1563 assert!(styled.contains("32"));
1564 assert!(styled.contains("40"));
1565 assert!(styled.ends_with("\x1b[0m"));
1566 }
1567
1568 #[test]
1569 fn test_strip_ansi_codes() {
1570 let styled = "\x1b[31mhello\x1b[0m world";
1571 let stripped = strip_ansi_codes(styled);
1572 assert_eq!(stripped, "hello world");
1573
1574 let plain = "no codes here";
1575 assert_eq!(strip_ansi_codes(plain), "no codes here");
1576 }
1577
1578 #[test]
1579 fn test_strip_ansi_codes_complex() {
1580 let styled = "\x1b[1;31;40mcomplex\x1b[0m";
1581 let stripped = strip_ansi_codes(styled);
1582 assert_eq!(stripped, "complex");
1583 }
1584
1585 #[test]
1586 fn test_get_terminal_size_returns_valid() {
1587 let (width, height) = get_terminal_size();
1588 assert!(width > 0);
1589 assert!(height > 0);
1590 }
1591
1592 #[test]
1593 fn test_progress_bar_render() {
1594 let bar = ProgressBar::new(100, Some("Test"), false, true, true, 20);
1595 let output = bar.render();
1596 assert!(output.contains("Test"));
1597 assert!(output.contains("["));
1598 assert!(output.contains("]"));
1599 assert!(output.contains("0/100"));
1600 assert!(output.contains("0%"));
1601 }
1602
1603 #[test]
1604 fn test_progress_bar_update() {
1605 let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1606 bar.update(50);
1607 let output = bar.render();
1608 assert!(output.contains("50%"));
1609 }
1610
1611 #[test]
1612 fn test_progress_bar_finish() {
1613 let mut bar = ProgressBar::new(100, None, false, true, false, 10);
1614 bar.finish();
1615 let output = bar.render();
1616 assert!(output.contains("100%"));
1617 assert!(bar.finished);
1618 }
1619
1620 #[test]
1621 fn test_progress_bar_zero_length() {
1622 let bar = ProgressBar::new(0, None, false, true, false, 10);
1623 let output = bar.render();
1624 assert!(output.contains("0%"));
1625 }
1626
1627 #[test]
1628 fn test_progress_bar_custom_chars() {
1629 let mut bar = ProgressBar::new(100, None, false, false, false, 10)
1630 .fill_char('=')
1631 .empty_char(' ');
1632 bar.set_position(50);
1633 let output = bar.render();
1634 assert!(output.contains("[===== ]"));
1636 }
1637
1638 #[test]
1639 fn test_progress_bar_unicode_chars() {
1640 let bar = ProgressBar::new(100, None, false, false, false, 4)
1641 .fill_char('\u{2588}') .empty_char('\u{2591}'); let output = bar.render();
1644 assert!(output.contains("[\u{2591}\u{2591}\u{2591}\u{2591}]"));
1646 }
1647
1648 #[test]
1649 fn test_color_constants() {
1650 assert_eq!(BLACK, Color::Black);
1651 assert_eq!(RED, Color::Red);
1652 assert_eq!(GREEN, Color::Green);
1653 assert_eq!(YELLOW, Color::Yellow);
1654 assert_eq!(BLUE, Color::Blue);
1655 assert_eq!(MAGENTA, Color::Magenta);
1656 assert_eq!(CYAN, Color::Cyan);
1657 assert_eq!(WHITE, Color::White);
1658 assert_eq!(BRIGHT_BLACK, Color::BrightBlack);
1659 assert_eq!(BRIGHT_RED, Color::BrightRed);
1660 assert_eq!(BRIGHT_GREEN, Color::BrightGreen);
1661 assert_eq!(BRIGHT_YELLOW, Color::BrightYellow);
1662 assert_eq!(BRIGHT_BLUE, Color::BrightBlue);
1663 assert_eq!(BRIGHT_MAGENTA, Color::BrightMagenta);
1664 assert_eq!(BRIGHT_CYAN, Color::BrightCyan);
1665 assert_eq!(BRIGHT_WHITE, Color::BrightWhite);
1666 assert_eq!(RESET, Color::Reset);
1667 }
1668
1669 #[test]
1670 fn test_style_all_options() {
1671 let styled = style(
1672 "test",
1673 Some(Color::Blue),
1674 Some(Color::White),
1675 true, true, true, true, true, true, true, true, );
1684 assert!(styled.starts_with("\x1b["));
1685 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"));
1695 }
1696
1697 #[test]
1698 fn test_style_no_reset() {
1699 let styled = style(
1700 "hello",
1701 Some(Color::Red),
1702 None,
1703 false,
1704 false,
1705 false,
1706 false,
1707 false,
1708 false,
1709 false,
1710 false,
1711 );
1712 assert!(styled.starts_with("\x1b[31m"));
1713 assert!(!styled.ends_with("\x1b[0m"));
1714 }
1715}