瀏覽代碼

feat(cli): colorful cli (#3635)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 3 年之前
父節點
當前提交
49d2f13fc0
共有 4 個文件被更改,包括 291 次插入227 次删除
  1. 6 0
      .changes/colorful-cli.md
  2. 5 5
      tooling/bundler/src/bundle/updater_bundle.rs
  3. 275 219
      tooling/cli/src/info.rs
  4. 5 3
      tooling/cli/src/init.rs

+ 6 - 0
.changes/colorful-cli.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": "patch"
+"cli.js": "patch"
+---
+
+Improve readability of the `info` subcommand output.

+ 5 - 5
tooling/bundler/src/bundle/updater_bundle.rs

@@ -18,11 +18,7 @@ use std::{fs::File, io::prelude::*};
 use zip::write::FileOptions;
 
 use crate::{bundle::Bundle, Settings};
-use std::{
-  ffi::OsStr,
-  fs::{self},
-  io::Write,
-};
+use std::{fs, io::Write};
 
 use anyhow::Context;
 use std::path::{Path, PathBuf};
@@ -43,6 +39,8 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
 // This is the Mac OS App packaged
 #[cfg(target_os = "macos")]
 fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
+  use std::ffi::OsStr;
+
   // find our .app or rebuild our bundle
   let bundle_path = match bundles
     .iter()
@@ -83,6 +81,8 @@ fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<P
 // No assets are replaced
 #[cfg(target_os = "linux")]
 fn bundle_update(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
+  use std::ffi::OsStr;
+
   // build our app actually we support only appimage on linux
   let bundle_path = match bundles
     .iter()

+ 275 - 219
tooling/cli/src/info.rs

@@ -7,6 +7,7 @@ use crate::helpers::{
 };
 use crate::Result;
 use clap::Parser;
+use colored::Colorize;
 use serde::Deserialize;
 
 use std::{
@@ -311,96 +312,6 @@ fn active_rust_toolchain() -> crate::Result<Option<String>> {
   Ok(toolchain)
 }
 
-struct InfoBlock {
-  section: bool,
-  key: &'static str,
-  value: Option<String>,
-  suffix: Option<String>,
-}
-
-impl InfoBlock {
-  fn new(key: &'static str) -> Self {
-    Self {
-      section: false,
-      key,
-      value: None,
-      suffix: None,
-    }
-  }
-
-  fn section(mut self) -> Self {
-    self.section = true;
-    self
-  }
-
-  fn value<V: Into<Option<String>>>(mut self, value: V) -> Self {
-    self.value = value.into();
-    self
-  }
-
-  fn suffix<S: Into<Option<String>>>(mut self, suffix: S) -> Self {
-    self.suffix = suffix.into();
-    self
-  }
-
-  fn display(&self) {
-    if self.section {
-      println!();
-    }
-    print!("{}", self.key);
-    if let Some(value) = &self.value {
-      print!(" - {}", value);
-    }
-    if let Some(suffix) = &self.suffix {
-      print!("{}", suffix);
-    }
-    println!();
-  }
-}
-
-struct VersionBlock {
-  section: bool,
-  key: &'static str,
-  version: Option<String>,
-  target_version: Option<String>,
-}
-
-impl VersionBlock {
-  fn new<V: Into<Option<String>>>(key: &'static str, version: V) -> Self {
-    Self {
-      section: false,
-      key,
-      version: version.into(),
-      target_version: None,
-    }
-  }
-
-  fn target_version<V: Into<Option<String>>>(mut self, version: V) -> Self {
-    self.target_version = version.into();
-    self
-  }
-
-  fn display(&self) {
-    if self.section {
-      println!();
-    }
-    print!("{}", self.key);
-    if let Some(version) = &self.version {
-      print!(" - {}", version);
-    } else {
-      print!(" - Not installed");
-    }
-    if let (Some(version), Some(target_version)) = (&self.version, &self.target_version) {
-      let version = semver::Version::parse(version).unwrap();
-      let target_version = semver::Version::parse(target_version).unwrap();
-      if version < target_version {
-        print!(" (outdated, latest: {})", target_version);
-      }
-    }
-    println!();
-  }
-}
-
 fn crate_version(
   tauri_dir: &Path,
   manifest: Option<&CargoManifest>,
@@ -529,23 +440,120 @@ fn crate_version(
   (crate_version_string, suffix)
 }
 
+fn indent(spaces: usize) {
+  print!(
+    "{}",
+    vec![0; spaces].iter().map(|_| " ").collect::<String>()
+  );
+}
+
+struct Section(&'static str);
+impl Section {
+  fn display(&self) {
+    println!();
+    println!("{}", self.0.yellow().bold());
+  }
+}
+
+struct VersionBlock {
+  name: String,
+  version: String,
+  target_version: String,
+  indentation: usize,
+  skip_update: bool,
+}
+
+impl VersionBlock {
+  fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
+    Self {
+      name: name.into(),
+      version: version.into(),
+      target_version: "".into(),
+      indentation: 2,
+      skip_update: false,
+    }
+  }
+
+  fn skip_update(mut self) -> Self {
+    self.skip_update = true;
+    self
+  }
+
+  fn target_version(mut self, version: impl Into<String>) -> Self {
+    self.target_version = version.into();
+    self
+  }
+
+  fn display(&self) {
+    indent(self.indentation);
+    print!("{} ", "›".cyan());
+    print!("{}", self.name.bold());
+    print!(": ");
+    print!(
+      "{}",
+      if self.version.is_empty() {
+        "Not installed!".red().to_string()
+      } else {
+        self.version.clone()
+      }
+    );
+    if !self.target_version.is_empty() && !self.skip_update {
+      print!(
+        "({}, latest: {})",
+        "outdated".red(),
+        self.target_version.green()
+      );
+    }
+    println!();
+  }
+}
+
+struct InfoBlock {
+  key: String,
+  value: String,
+  indentation: usize,
+}
+
+impl InfoBlock {
+  fn new(key: impl Into<String>, val: impl Into<String>) -> Self {
+    Self {
+      key: key.into(),
+      value: val.into(),
+      indentation: 2,
+    }
+  }
+
+  fn display(&self) {
+    indent(self.indentation);
+    print!("{} ", "›".cyan());
+    print!("{}", self.key.bold());
+    print!(": ");
+    print!("{}", self.value.clone());
+    println!();
+  }
+}
+
 pub fn command(_options: Options) -> Result<()> {
+  Section("Environment").display();
+
   let os_info = os_info::get();
-  InfoBlock {
-    section: true,
-    key: "Operating System",
-    value: Some(format!(
-      "{}, version {} {:?}",
+  VersionBlock::new(
+    "OS",
+    format!(
+      "{} {} {:?}",
       os_info.os_type(),
       os_info.version(),
       os_info.bitness()
-    )),
-    suffix: None,
-  }
+    ),
+  )
   .display();
 
   #[cfg(windows)]
-  VersionBlock::new("Webview2", webview2_version().unwrap_or_default()).display();
+  VersionBlock::new(
+    "Webview2",
+    webview2_version().unwrap_or_default().unwrap_or_default(),
+  )
+  .display();
 
   #[cfg(windows)]
   {
@@ -554,24 +562,12 @@ pub fn command(_options: Options) -> Result<()> {
       .unwrap_or_default();
 
     if build_tools.is_empty() {
-      InfoBlock {
-        section: false,
-        key: "Visual Studio Build Tools - Not installed",
-        value: None,
-        suffix: None,
-      }
-      .display();
+      InfoBlock::new("MSVC", "").display();
     } else {
-      InfoBlock {
-        section: false,
-        key: "Visual Studio Build Tools:",
-        value: None,
-        suffix: None,
-      }
-      .display();
-
+      InfoBlock::new("MSVC", "").display();
       for i in build_tools {
-        VersionBlock::new("  ", i).display();
+        indent(6);
+        println!("{}", format!("{} {}", "-".cyan(), i));
       }
     }
   }
@@ -585,6 +581,87 @@ pub fn command(_options: Options) -> Result<()> {
     .unwrap_or_default();
   panic::set_hook(hook);
 
+  let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json"))?;
+  VersionBlock::new(
+    "Node.js",
+    get_version("node", &[])
+      .unwrap_or_default()
+      .unwrap_or_default()
+      .chars()
+      .skip(1)
+      .collect::<String>(),
+  )
+  .target_version(metadata.js_cli.node.replace(">= ", ""))
+  .skip_update()
+  .display();
+
+  VersionBlock::new(
+    "npm",
+    get_version("npm", &[])
+      .unwrap_or_default()
+      .unwrap_or_default(),
+  )
+  .display();
+  VersionBlock::new(
+    "pnpm",
+    get_version("pnpm", &[])
+      .unwrap_or_default()
+      .unwrap_or_default(),
+  )
+  .display();
+  VersionBlock::new(
+    "yarn",
+    get_version("yarn", &[])
+      .unwrap_or_default()
+      .unwrap_or_default(),
+  )
+  .display();
+  VersionBlock::new(
+    "rustup",
+    get_version("rustup", &[])
+      .unwrap_or_default()
+      .map(|v| {
+        let mut s = v.split(' ');
+        s.next();
+        s.next().unwrap().to_string()
+      })
+      .unwrap_or_default(),
+  )
+  .display();
+  VersionBlock::new(
+    "rustc",
+    get_version("rustc", &[])
+      .unwrap_or_default()
+      .map(|v| {
+        let mut s = v.split(' ');
+        s.next();
+        s.next().unwrap().to_string()
+      })
+      .unwrap_or_default(),
+  )
+  .display();
+  VersionBlock::new(
+    "cargo",
+    get_version("cargo", &[])
+      .unwrap_or_default()
+      .map(|v| {
+        let mut s = v.split(' ');
+        s.next();
+        s.next().unwrap().to_string()
+      })
+      .unwrap_or_default(),
+  )
+  .display();
+  InfoBlock::new(
+    "Rust toolchain",
+    active_rust_toolchain()
+      .unwrap_or_default()
+      .unwrap_or_default(),
+  )
+  .display();
+
+  Section("Packages").display();
+
   let mut package_manager = PackageManager::Npm;
   if let Some(app_dir) = &app_dir {
     let file_names = read_dir(app_dir)
@@ -601,76 +678,29 @@ pub fn command(_options: Options) -> Result<()> {
       .collect::<Vec<String>>();
     package_manager = get_package_manager(&file_names)?;
   }
-
-  if let Some(node_version) = get_version("node", &[]).unwrap_or_default() {
-    InfoBlock::new("Node.js environment").section().display();
-    let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json"))?;
-    VersionBlock::new(
-      "  Node.js",
-      node_version.chars().skip(1).collect::<String>(),
-    )
-    .target_version(metadata.js_cli.node.replace(">= ", ""))
-    .display();
-
-    VersionBlock::new("  @tauri-apps/cli", metadata.js_cli.version)
-      .target_version(npm_latest_version(&package_manager, "@tauri-apps/cli").unwrap_or_default())
-      .display();
-    if let Some(app_dir) = &app_dir {
-      VersionBlock::new(
-        "  @tauri-apps/api",
-        npm_package_version(&package_manager, "@tauri-apps/api", app_dir).unwrap_or_default(),
-      )
-      .target_version(npm_latest_version(&package_manager, "@tauri-apps/api").unwrap_or_default())
-      .display();
-    }
-
-    InfoBlock::new("Global packages").section().display();
-
-    VersionBlock::new("  npm", get_version("npm", &[]).unwrap_or_default()).display();
-    VersionBlock::new("  pnpm", get_version("pnpm", &[]).unwrap_or_default()).display();
-    VersionBlock::new("  yarn", get_version("yarn", &[]).unwrap_or_default()).display();
-  }
-
-  InfoBlock::new("Rust environment").section().display();
-  VersionBlock::new(
-    "  rustup",
-    get_version("rustup", &[]).unwrap_or_default().map(|v| {
-      let mut s = v.split(' ');
-      s.next();
-      s.next().unwrap().to_string()
-    }),
-  )
-  .display();
   VersionBlock::new(
-    "  rustc",
-    get_version("rustc", &[]).unwrap_or_default().map(|v| {
-      let mut s = v.split(' ');
-      s.next();
-      s.next().unwrap().to_string()
-    }),
+    format!("{} {}", "@tauri-apps/cli", "[NPM]".dimmed()),
+    metadata.js_cli.version,
   )
-  .display();
-  VersionBlock::new(
-    "  cargo",
-    get_version("cargo", &[]).unwrap_or_default().map(|v| {
-      let mut s = v.split(' ');
-      s.next();
-      s.next().unwrap().to_string()
-    }),
+  .target_version(
+    npm_latest_version(&package_manager, "@tauri-apps/cli")
+      .unwrap_or_default()
+      .unwrap_or_default(),
   )
   .display();
-  VersionBlock::new("  toolchain", active_rust_toolchain().unwrap_or_default()).display();
-
-  if let Some(app_dir) = app_dir {
-    InfoBlock::new("App directory structure")
-      .section()
-      .display();
-    for entry in read_dir(app_dir)? {
-      let entry = entry?;
-      if entry.path().is_dir() {
-        println!("/{}", entry.path().file_name().unwrap().to_string_lossy());
-      }
-    }
+  if let Some(app_dir) = &app_dir {
+    VersionBlock::new(
+      format!("{} {}", "@tauri-apps/api", "[NPM]".dimmed()),
+      npm_package_version(&package_manager, "@tauri-apps/api", app_dir)
+        .unwrap_or_default()
+        .unwrap_or_default(),
+    )
+    .target_version(
+      npm_latest_version(&package_manager, "@tauri-apps/api")
+        .unwrap_or_default()
+        .unwrap_or_default(),
+    )
+    .display();
   }
 
   let hook = panic::take_hook();
@@ -683,9 +713,7 @@ pub fn command(_options: Options) -> Result<()> {
   panic::set_hook(hook);
 
   if tauri_dir.is_some() || app_dir.is_some() {
-    InfoBlock::new("App").section().display();
-
-    if let Some(tauri_dir) = tauri_dir {
+    if let Some(tauri_dir) = tauri_dir.clone() {
       let manifest: Option<CargoManifest> =
         if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
           toml::from_str(&manifest_contents).ok()
@@ -700,46 +728,57 @@ pub fn command(_options: Options) -> Result<()> {
         };
 
       for (dep, label) in [
-        ("tauri", "  tauri"),
-        ("tauri-build", "  tauri-build"),
-        ("tao", "  tao"),
-        ("wry", "  wry"),
+        ("tauri", format!("{} {}", "tauri", "[RUST]".dimmed())),
+        (
+          "tauri-build",
+          format!("{} {}", "tauri-build", "[RUST]".dimmed()),
+        ),
+        ("tao", format!("{} {}", "tao", "[RUST]".dimmed())),
+        ("wry", format!("{} {}", "wry", "[RUST]".dimmed())),
       ] {
         let (version_string, version_suffix) =
           crate_version(&tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
-        InfoBlock::new(label)
-          .value(version_string)
-          .suffix(version_suffix)
-          .display();
+        VersionBlock::new(
+          label,
+          format!(
+            "{},{}",
+            version_string,
+            version_suffix.unwrap_or_else(|| "".into())
+          ),
+        )
+        .display();
       }
+    }
+  }
 
+  if tauri_dir.is_some() || app_dir.is_some() {
+    Section("App").display();
+    if tauri_dir.is_some() {
       if let Ok(config) = get_config(None) {
         let config_guard = config.lock().unwrap();
         let config = config_guard.as_ref().unwrap();
-        InfoBlock::new("  build-type")
-          .value(if config.tauri.bundle.active {
+        InfoBlock::new(
+          "build-type",
+          if config.tauri.bundle.active {
             "bundle".to_string()
           } else {
             "build".to_string()
-          })
-          .display();
-        InfoBlock::new("  CSP")
-          .value(
-            config
-              .tauri
-              .security
-              .csp
-              .as_ref()
-              .map(|c| c.to_string())
-              .unwrap_or_else(|| "unset".to_string()),
-          )
-          .display();
-        InfoBlock::new("  distDir")
-          .value(config.build.dist_dir.to_string())
-          .display();
-        InfoBlock::new("  devPath")
-          .value(config.build.dev_path.to_string())
-          .display();
+          },
+        )
+        .display();
+        InfoBlock::new(
+          "CSP",
+          config
+            .tauri
+            .security
+            .csp
+            .clone()
+            .map(|c| c.to_string())
+            .unwrap_or_else(|| "unset".to_string()),
+        )
+        .display();
+        InfoBlock::new("distDir", config.build.dist_dir.to_string()).display();
+        InfoBlock::new("devPath", config.build.dev_path.to_string()).display();
       }
     }
 
@@ -747,14 +786,10 @@ pub fn command(_options: Options) -> Result<()> {
       if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
         let (framework, bundler) = infer_framework(&package_json);
         if let Some(framework) = framework {
-          InfoBlock::new("  framework")
-            .value(framework.to_string())
-            .display();
+          InfoBlock::new("framework", framework.to_string()).display();
         }
         if let Some(bundler) = bundler {
-          InfoBlock::new("  bundler")
-            .value(bundler.to_string())
-            .display();
+          InfoBlock::new("bundler", bundler.to_string()).display();
         }
       } else {
         println!("package.json not found");
@@ -762,6 +797,27 @@ pub fn command(_options: Options) -> Result<()> {
     }
   }
 
+  if let Some(app_dir) = app_dir {
+    Section("App directory structure").display();
+    let dirs = read_dir(app_dir)?
+      .filter(|p| p.is_ok() && p.as_ref().unwrap().path().is_dir())
+      .collect::<Vec<Result<std::fs::DirEntry, _>>>();
+    let dirs_len = dirs.len();
+    for (i, entry) in dirs.into_iter().enumerate() {
+      let entry = entry?;
+      let prefix = if i + 1 == dirs_len {
+        "└─".cyan()
+      } else {
+        "├─".cyan()
+      };
+      println!(
+        "  {} {}",
+        prefix,
+        entry.path().file_name().unwrap().to_string_lossy()
+      );
+    }
+  }
+
   Ok(())
 }
 

+ 5 - 3
tooling/cli/src/init.rs

@@ -194,17 +194,19 @@ pub fn command(mut options: Options) -> Result<()> {
 
 fn request_input<T>(prompt: &str, default: Option<T>, skip: bool) -> Result<Option<T>>
 where
-  T: Clone + FromStr + Display,
+  T: Clone + FromStr + Display + ToString,
   T::Err: Display + std::fmt::Debug,
 {
   if skip {
     Ok(default)
   } else {
-    let mut builder = Input::new();
+    let theme = dialoguer::theme::ColorfulTheme::default();
+    let mut builder = Input::with_theme(&theme);
     builder.with_prompt(prompt);
 
     if let Some(v) = default {
-      builder.default(v);
+      builder.default(v.clone());
+      builder.with_initial_text(v.to_string());
     }
 
     builder.interact_text().map(Some).map_err(Into::into)