/ src / main.rs
main.rs
  1  mod autoproviders {
  2      include!(concat!(env!("OUT_DIR"), "/autoproviders.rs"));
  3  }
  4  mod custom;
  5  mod profile;
  6  
  7  use anyhow::Result;
  8  use chrono::Local;
  9  use clap::{Parser, Subcommand};
 10  use console::{strip_ansi_codes, style, Term};
 11  use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
 12  use profile::{styled, Profile};
 13  #[cfg(unix)]
 14  use std::sync::OnceLock;
 15  use std::sync::atomic::{AtomicUsize, Ordering};
 16  use std::{
 17      process::Stdio,
 18      sync::Arc,
 19      time::{Duration, Instant},
 20  };
 21  use tokio::{
 22      fs::File,
 23      io::{AsyncReadExt, AsyncWriteExt},
 24      process::Command,
 25      sync::Mutex,
 26  };
 27  
 28  #[cfg(windows)]
 29  unsafe extern "system" {
 30      fn SetThreadExecutionState(flags: u32) -> u32;
 31  }
 32  
 33  #[cfg(windows)]
 34  const ES_CONTINUOUS: u32 = 0x80000000;
 35  #[cfg(windows)]
 36  const ES_SYSTEM_REQUIRED: u32 = 0x00000001;
 37  #[cfg(windows)]
 38  const ES_DISPLAY_REQUIRED: u32 = 0x00000002;
 39  
 40  #[cfg(unix)]
 41  static ORIGINAL_TERMIOS: OnceLock<libc::termios> = OnceLock::new();
 42  
 43  static CTRLC_COUNT: AtomicUsize = AtomicUsize::new(0);
 44  
 45  #[cfg(unix)]
 46  fn suppress_stdin() {
 47      unsafe {
 48          let fd: i32 = 0;
 49          let mut original: libc::termios = std::mem::zeroed();
 50          if libc::tcgetattr(fd, &mut original) == 0 {
 51              ORIGINAL_TERMIOS.set(original).ok();
 52              let mut raw = original;
 53              raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::ISIG);
 54              raw.c_cc[libc::VMIN] = 1;
 55              raw.c_cc[libc::VTIME] = 0;
 56              libc::tcsetattr(fd, libc::TCSANOW, &raw);
 57          }
 58      }
 59  }
 60  
 61  #[cfg(unix)]
 62  fn restore_terminal() {
 63      if let Some(original) = ORIGINAL_TERMIOS.get() {
 64          unsafe { libc::tcsetattr(0, libc::TCSANOW, original); }
 65      }
 66  }
 67  
 68  pub trait Provider: Send + Sync {
 69      fn name(&self) -> &str;
 70      fn is_available(&self) -> bool;
 71      fn get_command(&self) -> String;
 72      fn needs_root(&self) -> bool { true }
 73      fn parse_progress(&self, _line: &str) -> Option<String> { None }
 74      fn parse_summary(&self, _log: &str) -> UpdateSummary { UpdateSummary::default() }
 75  }
 76  
 77  #[derive(Debug, Clone, Default)]
 78  pub struct UpdateSummary {
 79      pub upgraded: usize,
 80      pub installed: usize,
 81      pub removed: usize,
 82      pub packages: Vec<String>,
 83  }
 84  
 85  #[derive(Parser)]
 86  #[command(name = "bupdate", version, about = "Universal package updater")]
 87  #[command(after_help = concat!(
 88      "\x1b[36mExamples:\x1b[0m\n",
 89      "  bupdate                      Run all available providers\n",
 90      "  bupdate --shutdown           Update and shutdown when done\n",
 91      "  bupdate --suspend            Update and suspend when done\n",
 92      "  bupdate --lock               Update and lock screen when done\n",
 93      "  bupdate --list               Show detected providers\n",
 94      "  bupdate -i arch              Skip a provider\n",
 95      "  bupdate -i arch -i flatpak   Skip multiple providers\n",
 96      "  bupdate profile init         Create default profile\n",
 97      "  bupdate profile show         Show current profile",
 98  ))]
 99  struct Cli {
100      #[arg(long = "shutdown", help = "Shutdown the system after updates complete")]
101      shutdown: bool,
102  
103      #[arg(long = "suspend", help = "Suspend the system after updates complete")]
104      suspend: bool,
105  
106      #[arg(long = "lock", help = "Lock the screen after updates complete")]
107      lock: bool,
108  
109      #[arg(short = 'l', long = "list", help = "List detected active providers and exit")]
110      list: bool,
111  
112      #[arg(short = 'i', long = "ignore", value_name = "PROVIDER", help = "Skip specific providers")]
113      ignore: Vec<String>,
114  
115      #[command(subcommand)]
116      command: Option<Commands>,
117  }
118  
119  #[derive(Subcommand)]
120  enum Commands {
121      /// Manage bupdate profile (~/.config/bupdate/profile.toml)
122      Profile {
123          #[command(subcommand)]
124          action: ProfileAction,
125      },
126  }
127  
128  #[derive(Subcommand)]
129  enum ProfileAction {
130      /// Create a default profile configuration file
131      Init,
132      /// Show current profile settings
133      Show,
134      /// Print the profile file path
135      Path,
136  }
137  
138  fn bin_exists(name: &str) -> bool {
139      std::process::Command::new("which")
140          .arg(name)
141          .stdout(Stdio::null())
142          .stderr(Stdio::null())
143          .status()
144          .map(|s| s.success())
145          .unwrap_or(false)
146  }
147  
148  async fn try_sleep_inhibit() -> Option<tokio::process::Child> {
149      let strategies: &[&[&str]] = &[
150          &["systemd-inhibit", "--what=sleep:idle", "--who=bupdate", "--why=System_Update", "sleep", "infinity"],
151          &["caffeine"],
152          &["sh", "-c", "while true; do sleep 60; done"],
153      ];
154      for args in strategies {
155          if bin_exists(args[0]) {
156              if let Ok(child) = Command::new(args[0]).args(&args[1..]).spawn() {
157                  return Some(child);
158              }
159          }
160      }
161      None
162  }
163  
164  async fn run_first_available(strategies: &[&[&str]]) -> Result<()> {
165      for args in strategies {
166          if bin_exists(args[0]) {
167              let use_sudo = matches!(args[0], "shutdown" | "poweroff" | "halt" | "pm-suspend");
168              if use_sudo {
169                  Command::new("sudo")
170                      .args(*args)
171                      .spawn()?
172                      .wait()
173                      .await?;
174              } else {
175                  Command::new(args[0])
176                      .args(&args[1..])
177                      .spawn()?
178                      .wait()
179                      .await?;
180              }
181              return Ok(());
182          }
183      }
184      anyhow::bail!("No supported power management tool found")
185  }
186  
187  #[tokio::main]
188  async fn main() -> Result<()> {
189      let cli = Cli::parse();
190  
191      if let Some(Commands::Profile { action }) = &cli.command {
192          match action {
193              ProfileAction::Init => {
194                  let path = Profile::init()?;
195                  println!("{} {}", style("✓ Profile created:").green().bold(), path.display());
196                  let pdir = custom::init_providers_dir()?;
197                  println!("{} {}", style("✓ Providers dir:").green().bold(), pdir.display());
198                  return Ok(());
199              }
200              ProfileAction::Show => {
201                  let profile = Profile::load()?;
202                  profile.show()?;
203                  return Ok(());
204              }
205              ProfileAction::Path => {
206                  println!("{}", profile::profile_path()?.display());
207                  return Ok(());
208              }
209          }
210      }
211  
212      let profile = Arc::new(Profile::load()?);
213      let sym = &profile.symbols;
214      let sty = &profile.styles;
215      let msg = &profile.messages;
216  
217      let mut all_ignore: Vec<String> = cli.ignore.clone();
218      all_ignore.extend(profile.providers.ignore.iter().cloned());
219  
220      let mut all_providers: Vec<Box<dyn Provider>> = autoproviders::get_all_active();
221      all_providers.extend(custom::load_custom_providers());
222  
223      let active_providers: Vec<Box<dyn Provider>> = all_providers
224          .into_iter()
225          .filter(|p| {
226              let name = p.name();
227              if !profile.providers.only.is_empty() {
228                  return profile.providers.only.iter().any(|o| o.eq_ignore_ascii_case(name));
229              }
230              !all_ignore.iter().any(|ig| ig.eq_ignore_ascii_case(name))
231          })
232          .collect();
233  
234      if cli.list {
235          if active_providers.is_empty() {
236              println!("{}", styled(&msg.no_providers_list, &sty.info));
237          } else {
238              println!("{}", styled(&msg.list_header, &sty.header));
239              for p in &active_providers {
240                  println!("  {} {}", styled(&sym.bullet, &sty.bullet), styled(p.name(), &sty.provider_name));
241              }
242          }
243          return Ok(());
244      }
245  
246      if active_providers.is_empty() {
247          println!("\n{}\n", styled(&msg.no_providers, &sty.warning));
248          return Ok(());
249      }
250  
251      let any_needs_root = active_providers.iter().any(|p| p.needs_root());
252  
253      if any_needs_root && !cfg!(windows) {
254          if !std::process::Command::new("sudo")
255              .arg("-v")
256              .status()?
257              .success()
258          {
259              eprintln!("{}", styled(&format!("{} {}", sym.failure, msg.sudo_fail), &sty.error));
260              std::process::exit(1);
261          }
262  
263          tokio::spawn(async {
264              loop {
265                  tokio::time::sleep(Duration::from_secs(60)).await;
266                  let _ = Command::new("sudo").args(["-n", "true"]).output().await;
267              }
268          });
269      }
270  
271      let now = Local::now();
272      let run_id = now.format("%Y-%m-%d_%H-%M-%S").to_string();
273      let configured_log_dir = std::path::PathBuf::from(&profile.logging.log_dir);
274      let preferred_log_dir = configured_log_dir.join(&run_id);
275  
276      if cfg!(windows) {
277          let _ = std::fs::create_dir_all(&preferred_log_dir);
278      } else {
279          Command::new("sudo")
280              .args(["mkdir", "-p", &preferred_log_dir.to_string_lossy()])
281              .output()
282              .await?;
283          Command::new("sudo")
284              .args(["chmod", "777", &preferred_log_dir.to_string_lossy()])
285              .output()
286              .await?;
287      }
288      let log_dir = preferred_log_dir.to_string_lossy().into_owned();
289  
290      #[cfg(windows)]
291      unsafe { SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); }
292  
293      let sleep_lock = Arc::new(Mutex::new(if cfg!(windows) {
294          None
295      } else {
296          try_sleep_inhibit().await
297      }));
298  
299  
300      #[cfg(unix)]
301      suppress_stdin();
302  
303      let started = Instant::now();
304      let m = MultiProgress::with_draw_target(ProgressDrawTarget::stdout_with_hz(15));
305  
306      let total = active_providers.len();
307      println!(
308          "\n{}",
309          styled(
310              &format!("{} {} {}", sym.border, msg.header, sym.border),
311              &sty.header,
312          )
313      );
314      let subtitle_text = msg
315          .subtitle
316          .replace("{count}", &total.to_string())
317          .replace("{s}", if total == 1 { "" } else { "s" });
318      println!("{}\n", styled(&subtitle_text, &sty.subtitle));
319  
320      let tick_strs = profile.tick_strings();
321      let tick_refs: Vec<&str> = tick_strs.iter().map(|s| s.as_str()).collect();
322      let bar_style = ProgressStyle::with_template(&profile.format.progress_bar)?
323          .tick_strings(&tick_refs);
324  
325      let tick_ms = profile.spinner.tick_ms;
326  
327      let log_dir_arc = Arc::new(log_dir.clone());
328      let profile_arc = Arc::clone(&profile);
329      let mut tasks = vec![];
330  
331      for p in active_providers {
332          let pb = m.add(ProgressBar::new_spinner());
333          pb.set_style(bar_style.clone());
334          pb.enable_steady_tick(Duration::from_millis(tick_ms));
335  
336          let p_name = p.name().to_string();
337          pb.set_prefix(p_name.clone());
338  
339          let log_path = format!("{}/{}.log", log_dir_arc, p_name);
340          let prof = Arc::clone(&profile_arc);
341          let p_arc: Arc<dyn Provider> = Arc::from(p);
342  
343          tasks.push(tokio::spawn(async move {
344              let Ok(mut file) = File::create(&log_path).await else {
345                  return (p_name, false, UpdateSummary::default());
346              };
347  
348              let use_sudo = p_arc.needs_root() && !cfg!(windows);
349              let mut cmd = if cfg!(windows) {
350                  let mut c = Command::new("cmd");
351                  c.args(["/C", &p_arc.get_command()]);
352                  c
353              } else if use_sudo {
354                  let mut c = Command::new("sudo");
355                  c.args(["sh", "-c", &format!("LC_ALL=C {} 2>&1", p_arc.get_command())]);
356                  c
357              } else {
358                  let mut c = Command::new("sh");
359                  c.args(["-c", &format!("LC_ALL=C {} 2>&1", p_arc.get_command())]);
360                  c
361              };
362              if cfg!(windows) {
363                  cmd.current_dir("C:\\").stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
364              } else {
365                  cmd.current_dir("/").stdin(Stdio::null()).stdout(Stdio::piped());
366              }
367  
368              let Ok(mut child) = cmd.spawn() else {
369                  return (p_name, false, UpdateSummary::default());
370              };
371  
372              let Some(mut stdout) = child.stdout.take() else {
373                  return (p_name, false, UpdateSummary::default());
374              };
375              let max_len = (Term::stdout().size().1 as usize).saturating_sub(44).max(20);
376              let mut buf = [0u8; 4096];
377              let mut leftover = String::new();
378              let mut full_log = String::new();
379              let mut last_prog: Option<String> = None;
380  
381              loop {
382                  let n = match stdout.read(&mut buf).await {
383                      Ok(0) | Err(_) => break,
384                      Ok(n) => n,
385                  };
386                  let chunk = String::from_utf8_lossy(&buf[..n]);
387                  leftover.push_str(&chunk);
388  
389                  while let Some(pos) = leftover.find(|c: char| c == '\n' || c == '\r') {
390                      let segment: String = leftover.drain(..pos).collect();
391                      while leftover.starts_with('\n') || leftover.starts_with('\r') {
392                          leftover.drain(..1);
393                      }
394  
395                      let _ = file.write_all(format!("{}\n", segment).as_bytes()).await;
396  
397                      let stripped = strip_ansi_codes(&segment).to_string();
398                      let sanitized: String = stripped
399                          .chars()
400                          .filter(|c| !c.is_control() && *c != '\x7f' && *c != '\u{feff}')
401                          .filter(|c| !matches!(*c, '\x00'..='\x1f' | '\u{80}'..='\u{9f}'))
402                          .collect();
403  
404                      full_log.push_str(&sanitized);
405                      full_log.push('\n');
406  
407                      let clean: String = sanitized.trim().chars().take(max_len).collect();
408  
409                      if clean.is_empty() {
410                          continue;
411                      }
412  
413                      let full_line = sanitized.trim();
414                      if let Some(prog) = p_arc.parse_progress(full_line) {
415                          last_prog = Some(prog);
416                      }
417  
418                      let msg = if let Some(ref prog) = last_prog {
419                          prof.format
420                              .progress_line
421                              .replace("{progress}", &styled(prog, &prof.styles.progress))
422                              .replace("{arrow}", &styled(&prof.symbols.arrow, &prof.styles.arrow))
423                              .replace("{line}", &clean)
424                      } else {
425                          clean
426                      };
427                      let safe_msg: String = msg
428                          .chars()
429                          .filter(|c| !matches!(*c, '\n' | '\r' | '\t' | '\x0b' | '\x0c' | '\x08' | '\x7f'))
430                          .collect();
431                      pb.set_message(safe_msg);
432                  }
433              }
434  
435              let ok = child.wait().await.map(|s| s.success()).unwrap_or(false);
436              let summary = p_arc.parse_summary(&full_log);
437  
438              let mut parts = vec![];
439              if summary.upgraded > 0 { parts.push(format!("{} upgraded", summary.upgraded)); }
440              if summary.installed > 0 { parts.push(format!("{} installed", summary.installed)); }
441              if summary.removed > 0 { parts.push(format!("{} removed", summary.removed)); }
442              let detail = if parts.is_empty() {
443                  String::new()
444              } else {
445                  format!(" ({})", parts.join(", "))
446              };
447  
448              if ok {
449                  pb.finish_with_message(styled(
450                      &format!("{} {}{}", prof.symbols.success, prof.messages.success_text, detail),
451                      &prof.styles.success,
452                  ));
453              } else {
454                  pb.finish_with_message(styled(
455                      &format!("{} {}{}", prof.symbols.failure, prof.messages.failure_text, detail),
456                      &prof.styles.error,
457                  ));
458              }
459              (p_name, ok, summary)
460          }));
461      }
462  
463      let status_bar = m.add(ProgressBar::new_spinner());
464      status_bar.set_style(ProgressStyle::with_template("{msg}").unwrap());
465      status_bar.set_message("");
466  
467      let sb = status_bar.clone();
468      let profile_ctrlc = Arc::clone(&profile);
469      std::thread::spawn(move || {
470          use std::io::Read;
471          let msg = &profile_ctrlc.messages;
472          let sty = &profile_ctrlc.styles;
473          let mut buf = [0u8; 256];
474          loop {
475              match std::io::stdin().read(&mut buf) {
476                  Ok(0) | Err(_) => break,
477                  Ok(n) => {
478                      for &b in &buf[..n] {
479                          if b == 0x03 {
480                              let count = CTRLC_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
481                              if count == 1 {
482                                  sb.set_message(format!("\n{}", styled(&msg.interrupt_warn, &sty.warning)));
483                              } else {
484                                  sb.set_message(format!("\n{}", styled(&msg.interrupt_force, &sty.warning)));
485                                  #[cfg(unix)]
486                                  restore_terminal();
487                                  std::process::exit(130);
488                              }
489                          }
490                      }
491                  }
492              }
493          }
494      });
495  
496      let mut total_upgraded = 0usize;
497      let mut total_installed = 0usize;
498      let mut total_removed = 0usize;
499      let mut ok_count = 0usize;
500      let mut fail_count = 0usize;
501      let mut provider_results: Vec<(String, UpdateSummary)> = vec![];
502  
503      for t in tasks {
504          if let Ok((name, ok, summary)) = t.await {
505              if ok { ok_count += 1; } else { fail_count += 1; }
506              total_upgraded += summary.upgraded;
507              total_installed += summary.installed;
508              total_removed += summary.removed;
509              if summary.upgraded > 0 || summary.installed > 0 || summary.removed > 0 {
510                  provider_results.push((name, summary));
511              }
512          }
513      }
514  
515      status_bar.finish_and_clear();
516  
517      let elapsed = started.elapsed();
518      println!(
519          "\n\n{}",
520          styled(
521              &format!("{} {} {}", sym.border, msg.complete_header, sym.border),
522              &sty.header,
523          )
524      );
525  
526      let total_pkgs = total_upgraded + total_installed + total_removed;
527      if total_pkgs > 0 || fail_count > 0 {
528          let mut totals = vec![];
529          totals.push(format!("{}/{} ok", ok_count, ok_count + fail_count));
530          if total_upgraded > 0 { totals.push(format!("{} upgraded", total_upgraded)); }
531          if total_installed > 0 { totals.push(format!("{} installed", total_installed)); }
532          if total_removed > 0 { totals.push(format!("{} removed", total_removed)); }
533          if fail_count > 0 { totals.push(styled(&format!("{} failed", fail_count), &sty.error)); }
534          println!(
535              "   {} {}",
536              styled("Total:", &sty.info),
537              totals.join(&styled(" | ", &sty.info)),
538          );
539      }
540  
541      println!(
542          "   {} {:.1}s",
543          styled(&msg.duration_label, &sty.info),
544          elapsed.as_secs_f64()
545      );
546      println!(
547          "   {} {}",
548          styled(&msg.logs_label, &sty.info),
549          styled(&log_dir, &sty.log_path)
550      );
551  
552      if !provider_results.is_empty() {
553          let term_w = Term::stdout().size().1 as usize;
554          println!();
555          for (prov, s) in &provider_results {
556              let header = format!("   {} ", prov);
557              let header_styled = format!("   {} ", styled(prov, &sty.provider_name));
558              let indent = " ".repeat(header.len());
559              if !s.packages.is_empty() {
560                  let mut line = header_styled.clone();
561                  let mut line_len = header.len();
562                  for (i, pkg) in s.packages.iter().enumerate() {
563                      let sep = if i == 0 { "" } else { ", " };
564                      let addition = format!("{}{}", sep, pkg);
565                      if i > 0 && line_len + addition.len() > term_w {
566                          println!("{}", styled(&line, &sty.info));
567                          line = format!("{}{}", indent, pkg);
568                          line_len = indent.len() + pkg.len();
569                      } else {
570                          line.push_str(&styled(&addition, &sty.info));
571                          line_len += addition.len();
572                      }
573                  }
574                  println!("{}", line);
575              } else {
576                  let mut parts = vec![];
577                  if s.upgraded > 0 { parts.push(format!("{} upgraded", s.upgraded)); }
578                  if s.installed > 0 { parts.push(format!("{} installed", s.installed)); }
579                  if s.removed > 0 { parts.push(format!("{} removed", s.removed)); }
580                  println!(
581                      "{}{}",
582                      header_styled,
583                      styled(&parts.join(", "), &sty.info),
584                  );
585              }
586          }
587      }
588  
589      if let Some(mut child) = sleep_lock.lock().await.take() {
590          let _ = child.kill().await;
591      }
592      #[cfg(windows)]
593      unsafe { SetThreadExecutionState(ES_CONTINUOUS); }
594  
595      let do_shutdown = cli.shutdown || profile.post_update.shutdown;
596      let do_suspend = cli.suspend || profile.post_update.suspend;
597  
598      if do_shutdown {
599          println!("\n{}", styled(&msg.shutting_down, &sty.warning));
600          if cfg!(windows) {
601              Command::new("shutdown")
602                  .args(["/s", "/t", "0"])
603                  .spawn()?
604                  .wait()
605                  .await?;
606          } else {
607              run_first_available(&[
608                  &["shutdown", "now"],
609                  &["poweroff"],
610                  &["halt", "-p"],
611                  &["loginctl", "poweroff"],
612                  &["dbus-send", "--system", "--print-reply",
613                    "--dest=org.freedesktop.login1",
614                    "/org/freedesktop/login1",
615                    "org.freedesktop.login1.Manager.PowerOff",
616                    "boolean:true"],
617              ]).await?;
618          }
619      } else if do_suspend {
620          println!("\n{}", styled(&msg.suspending, &sty.warning));
621          if cfg!(windows) {
622              Command::new("rundll32")
623                  .args(["powrprof.dll,SetSuspendState", "0", "1", "0"])
624                  .spawn()?
625                  .wait()
626                  .await?;
627          } else {
628              run_first_available(&[
629                  &["systemctl", "suspend"],
630                  &["loginctl", "suspend"],
631                  &["zzz"],
632                  &["pm-suspend"],
633                  &["dbus-send", "--system", "--print-reply",
634                    "--dest=org.freedesktop.login1",
635                    "/org/freedesktop/login1",
636                    "org.freedesktop.login1.Manager.Suspend",
637                    "boolean:true"],
638                  &["acpitool", "-s"],
639              ]).await?;
640          }
641      }
642  
643      let do_lock = cli.lock || profile.post_update.lock;
644      if do_lock {
645          println!("\n{}", styled(&msg.locking, &sty.warning));
646          if cfg!(windows) {
647              Command::new("rundll32")
648                  .args(["user32.dll,LockWorkStation"])
649                  .spawn()?
650                  .wait()
651                  .await?;
652          } else {
653              run_first_available(&[
654                  &["loginctl", "lock-session"],
655                  &["xdg-screensaver", "lock"],
656                  &["gnome-screensaver-command", "-l"],
657                  &["mate-screensaver-command", "-l"],
658                  &["cinnamon-screensaver-command", "-l"],
659                  &["xflock4"],
660                  &["swaylock"],
661                  &["i3lock"],
662                  &["xscreensaver-command", "-lock"],
663                  &["light-locker-command", "-l"],
664                  &["dm-tool", "lock"],
665                  &["dbus-send", "--type=method_call",
666                    "--dest=org.freedesktop.ScreenSaver",
667                    "/ScreenSaver",
668                    "org.freedesktop.ScreenSaver.Lock"],
669              ]).await?;
670          }
671      }
672  
673      #[cfg(unix)]
674      restore_terminal();
675  
676      Ok(())
677  }