Enhance CLI output: UTF-8 console codepage, VT colors, clean status cards
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod mount;
|
pub mod mount;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
pub mod ui;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
|
|||||||
+53
-33
@@ -2,12 +2,12 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use tracing::info;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams};
|
use sanctum::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams};
|
||||||
use sanctum::mount::{format_drive, mount_container, unmount_drive};
|
use sanctum::mount::{format_drive, mount_container, unmount_drive};
|
||||||
use sanctum::storage::Database;
|
use sanctum::storage::Database;
|
||||||
|
use sanctum::ui;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "sanctum")]
|
#[command(name = "sanctum")]
|
||||||
@@ -73,10 +73,11 @@ fn handle_init(container_path: &Path) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("============================================================");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!(" Sanctum Container Initialisierung");
|
println!("│ Sanctum — Neuen verschlüsselten Container erstellen │");
|
||||||
println!(" Zieldatei: {}", container_path.display());
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!("============================================================");
|
println!(" Zieldatei: {}", container_path.display());
|
||||||
|
println!();
|
||||||
|
|
||||||
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
let password = rpassword::prompt_password("Master-Passwort eingeben: ")
|
||||||
.context("Fehler beim Einlesen des Passworts")?;
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
@@ -92,20 +93,21 @@ fn handle_init(container_path: &Path) -> Result<()> {
|
|||||||
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
bail!("Die eingegebenen Passwörter stimmen nicht überein!");
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Generiere KDF-Salt und leite KEK via Argon2id ab ...");
|
println!();
|
||||||
|
ui::step(1, 4, "🔑", "Leite KEK via Argon2id ab (M=64MB, T=3, P=4)...");
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
let kdf_params = KdfParams::default();
|
let kdf_params = KdfParams::default();
|
||||||
let kek = derive_kek(&password, &salt, &kdf_params)
|
let kek = derive_kek(&password, &salt, &kdf_params)
|
||||||
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
.context("KDF-Schlüsselableitung fehlgeschlagen")?;
|
||||||
|
|
||||||
info!("Erzeuge kryptografisch sicheren DEK via CSPRNG (OsRng) ...");
|
ui::step(2, 4, "🎲", "Erzeuge kryptografisch sicheren DEK via CSPRNG...");
|
||||||
let dek = generate_dek();
|
let dek = generate_dek();
|
||||||
|
|
||||||
info!("Verschlüssele DEK mit KEK via AES-256-GCM ...");
|
ui::step(3, 4, "🔒", "Verschlüssele DEK via AES-256-GCM...");
|
||||||
let (wrapped_dek, header_nonce, header_tag) =
|
let (wrapped_dek, header_nonce, header_tag) =
|
||||||
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
wrap_dek(&kek, &dek).context("DEK-Wrapping fehlgeschlagen")?;
|
||||||
|
|
||||||
info!("Initialisiere SQLite-Containerstruktur mit WAL-Modus ...");
|
ui::step(4, 4, "📦", "Initialisiere SQLite-Containerstruktur & WAL-Modus...");
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
.context("Konnte SQLite-Containerdatei nicht anlegen")?;
|
||||||
|
|
||||||
@@ -116,31 +118,23 @@ fn handle_init(container_path: &Path) -> Result<()> {
|
|||||||
.context("Fehler beim finalen WAL-Checkpoint")?;
|
.context("Fehler beim finalen WAL-Checkpoint")?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("✔ Sanctum-Container erfolgreich initialisiert!");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!(" Container: {}", container_path.display());
|
println!("│ ✔ Sanctum-Container erfolgreich initialisiert! │");
|
||||||
println!(" Format: Version 1 (Magic: SANCTUM\\0)");
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" KDF: Argon2id (M=64MB, T=3, P=4)");
|
println!();
|
||||||
println!(" Cipher: AES-256-GCM mit AEAD-Swap-Protection (1-MB Chunks)");
|
println!(" • Container: {}", container_path.display());
|
||||||
|
println!(" • Format: Version 1 (Magic: SANCTUM\\0)");
|
||||||
|
println!(" • KDF: Argon2id (M=64MB, T=3, P=4)");
|
||||||
|
println!(" • Cipher: AES-256-GCM (1-MB Chunks, AEAD Swap-Schutz)");
|
||||||
|
println!();
|
||||||
|
println!(" Befehl zum Einbinden als Netzlaufwerk:");
|
||||||
|
println!(" {}", ui::cyan(&format!("sanctum mount --path \"{}\" --drive S", container_path.display())));
|
||||||
println!();
|
println!();
|
||||||
println!("Zum Einbinden ausführen:");
|
|
||||||
println!(
|
|
||||||
" sanctum mount --path \"{}\" --drive S",
|
|
||||||
container_path.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
async fn run() -> Result<()> {
|
||||||
async fn main() -> Result<()> {
|
|
||||||
// Tracing / Logging initialisieren (Standard-Filter: info)
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_env_filter(
|
|
||||||
EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| EnvFilter::new("sanctum=info,dav_server=warn,hyper=warn")),
|
|
||||||
)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
@@ -149,20 +143,46 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Commands::Mount { path, drive, port } => {
|
Commands::Mount { path, drive, port } => {
|
||||||
let drive_char = parse_drive_letter(&drive)?;
|
let drive_char = parse_drive_letter(&drive)?;
|
||||||
let password = rpassword::prompt_password(format!(
|
let prompt_text = format!(
|
||||||
"Master-Passwort für Container '{}' eingeben: ",
|
"Master-Passwort für Container '{}' eingeben: ",
|
||||||
path.display()
|
path.display()
|
||||||
))
|
);
|
||||||
.context("Fehler beim Einlesen des Passworts")?;
|
let password = rpassword::prompt_password(prompt_text)
|
||||||
|
.context("Fehler beim Einlesen des Passworts")?;
|
||||||
|
|
||||||
mount_container(&path, drive_char, port, &password).await?;
|
mount_container(&path, drive_char, port, &password).await?;
|
||||||
}
|
}
|
||||||
Commands::Unmount { drive } => {
|
Commands::Unmount { drive } => {
|
||||||
let drive_char = parse_drive_letter(&drive)?;
|
let drive_char = parse_drive_letter(&drive)?;
|
||||||
|
let drive_str = format_drive(drive_char);
|
||||||
|
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
unmount_drive(drive_char)?;
|
unmount_drive(drive_char)?;
|
||||||
println!("✔ Laufwerk {} erfolgreich getrennt.", format_drive(drive_char));
|
println!("{}", ui::green("OK"));
|
||||||
|
println!("{} Laufwerk {} erfolgreich getrennt.", ui::green("✔"), drive_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Windows Konsole für UTF-8 (Code Page 65001) und VT ANSI Processing konfigurieren
|
||||||
|
ui::init_console();
|
||||||
|
|
||||||
|
// Tracing / Logging initialisieren (Standard-Filter: warn)
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| EnvFilter::new("sanctum=warn,dav_server=warn,hyper=warn")),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if let Err(err) = run().await {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("{} {:#}", ui::red("✖ Fehler:"), err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-31
@@ -10,10 +10,11 @@ use hyper::service::service_fn;
|
|||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::crypto::{derive_kek, unwrap_dek};
|
use crate::crypto::{derive_kek, unwrap_dek};
|
||||||
use crate::storage::Database;
|
use crate::storage::Database;
|
||||||
|
use crate::ui;
|
||||||
use crate::vfs::SanctumFs;
|
use crate::vfs::SanctumFs;
|
||||||
|
|
||||||
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
||||||
@@ -24,7 +25,6 @@ pub fn format_drive(drive_letter: char) -> String {
|
|||||||
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`.
|
/// Trennt ein Windows-Netzlaufwerk via `net use <DRIVE>: /delete /y`.
|
||||||
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
info!("Trennen des Netzlaufwerks {} ...", drive_str);
|
|
||||||
|
|
||||||
let output = Command::new("net")
|
let output = Command::new("net")
|
||||||
.args(["use", &drive_str, "/delete", "/y"])
|
.args(["use", &drive_str, "/delete", "/y"])
|
||||||
@@ -42,14 +42,12 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Laufwerk {} erfolgreich getrennt.", drive_str);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/ /persistent:no` ein.
|
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/ /persistent:no` ein.
|
||||||
fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
|
fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
|
||||||
let url = format!("http://127.0.0.1:{}/", port);
|
let url = format!("http://127.0.0.1:{}/", port);
|
||||||
info!("Verbinde Netzlaufwerk {} mit {} ...", drive_str, url);
|
|
||||||
|
|
||||||
let output = Command::new("net")
|
let output = Command::new("net")
|
||||||
.args(["use", drive_str, &url, "/persistent:no"])
|
.args(["use", drive_str, &url, "/persistent:no"])
|
||||||
@@ -67,7 +65,6 @@ fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Laufwerk {} erfolgreich eingebunden!", drive_str);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,23 +84,20 @@ pub async fn mount_container(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
println!();
|
||||||
"Öffne Container '{}' ...",
|
ui::step(1, 4, "📦", "Öffne Container & verifiziere Header...");
|
||||||
container_path.display()
|
|
||||||
);
|
|
||||||
let db = Database::open(container_path)
|
let db = Database::open(container_path)
|
||||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||||
|
|
||||||
info!("Lese Header und verifiziere Magic Bytes ...");
|
|
||||||
let meta = db
|
let meta = db
|
||||||
.read_meta()
|
.read_meta()
|
||||||
.context("Konnte Container-Header nicht lesen")?;
|
.context("Konnte Container-Header nicht lesen")?;
|
||||||
|
|
||||||
info!("Leite KEK via Argon2id ab ...");
|
ui::step(2, 4, "🔑", "Leite KEK via Argon2id ab...");
|
||||||
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
let kek = derive_kek(password, &meta.kdf_salt, &meta.kdf_params)
|
||||||
.context("Schlüsselableitung fehlgeschlagen")?;
|
.context("Schlüsselableitung fehlgeschlagen")?;
|
||||||
|
|
||||||
info!("Entschlüssele DEK via AES-256-GCM ...");
|
ui::step(3, 4, "🔓", "Entschlüssele DEK via AES-256-GCM...");
|
||||||
let dek = unwrap_dek(
|
let dek = unwrap_dek(
|
||||||
&kek,
|
&kek,
|
||||||
&meta.wrapped_dek,
|
&meta.wrapped_dek,
|
||||||
@@ -124,10 +118,6 @@ pub async fn mount_container(
|
|||||||
let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await {
|
let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(_) if requested_port.is_none() => {
|
Err(_) if requested_port.is_none() => {
|
||||||
info!(
|
|
||||||
"Standard-Port {} belegt, wähle dynamischen freien Port ...",
|
|
||||||
port_to_try
|
|
||||||
);
|
|
||||||
TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
|
TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
|
||||||
.await
|
.await
|
||||||
.context("Konnte keinen lokalen TCP-Port binden")?
|
.context("Konnte keinen lokalen TCP-Port binden")?
|
||||||
@@ -139,7 +129,16 @@ pub async fn mount_container(
|
|||||||
|
|
||||||
let bound_addr = listener.local_addr()?;
|
let bound_addr = listener.local_addr()?;
|
||||||
let bound_port = bound_addr.port();
|
let bound_port = bound_addr.port();
|
||||||
info!("WebDAV-Server lauscht auf http://{}", bound_addr);
|
|
||||||
|
ui::step(
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
"🌐",
|
||||||
|
&format!(
|
||||||
|
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
||||||
|
bound_port, drive_str
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
|
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ pub async fn mount_container(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ = shutdown_rx.changed() => {
|
_ = shutdown_rx.changed() => {
|
||||||
info!("WebDAV-Server-Task empfängt Shutdown-Signal.");
|
debug!("WebDAV-Server-Task empfängt Shutdown-Signal.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,13 +189,15 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("============================================================");
|
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||||
println!(" Sanctum Container erfolgreich gemountet!");
|
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
||||||
println!(" Pfad: {}", container_path.display());
|
println!("└─────────────────────────────────────────────────────────────┘");
|
||||||
println!(" Laufwerk: {}", drive_str);
|
println!();
|
||||||
println!(" WebDAV URL: http://127.0.0.1:{}/", bound_port);
|
println!(" • Container: {}", container_path.display());
|
||||||
println!(" Drücke [Ctrl+C] zum sauberen Trennen und Schließen.");
|
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||||
println!("============================================================");
|
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||||
|
println!();
|
||||||
|
println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp"));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Warten auf Strg+C
|
// Warten auf Strg+C
|
||||||
@@ -205,12 +206,15 @@ pub async fn mount_container(
|
|||||||
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
|
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
info!("Beendigungssignal (Ctrl+C) erhalten.");
|
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||||
info!("Trennen des Windows-Netzlaufwerks {} ...", drive_str);
|
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
|
|
||||||
// Automatisches Unmount
|
// Automatisches Unmount
|
||||||
if let Err(e) = unmount_drive(drive_letter) {
|
if let Err(e) = unmount_drive(drive_letter) {
|
||||||
warn!("Warnung beim automatischen Unmount: {e}");
|
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
||||||
|
} else {
|
||||||
|
println!("{}", ui::green("OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP Server beenden
|
// HTTP Server beenden
|
||||||
@@ -218,11 +222,17 @@ pub async fn mount_container(
|
|||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
|
|
||||||
// SQLite WAL Checkpoint erzwingen
|
// SQLite WAL Checkpoint erzwingen
|
||||||
info!("Führe SQLite WAL-Checkpoint aus (PRAGMA wal_checkpoint(TRUNCATE)) ...");
|
print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]"));
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
if let Err(e) = db.checkpoint() {
|
if let Err(e) = db.checkpoint() {
|
||||||
error!("Fehler beim WAL-Checkpoint: {e}");
|
println!("{}", ui::red(&format!("Fehler ({e})")));
|
||||||
|
} else {
|
||||||
|
println!("{}", ui::green("OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Sanctum Container wurde sicher und vollständig geschlossen.");
|
println!();
|
||||||
|
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔"));
|
||||||
|
println!();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
static VT_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Initialisiert die Windows-Konsole:
|
||||||
|
/// - Setzt Code-Page 65001 (UTF-8) für Ein- und Ausgabe (verhindert Umlaut-Fehler wie `f├╝r`)
|
||||||
|
/// - Aktiviert VIRTUAL_TERMINAL_PROCESSING für saubere ANSI-Farb- und Escape-Sequenz-Darstellung
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn init_console() {
|
||||||
|
const CP_UTF8: u32 = 65001;
|
||||||
|
const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5; // (DWORD)-11
|
||||||
|
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
|
||||||
|
|
||||||
|
extern "system" {
|
||||||
|
fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
|
||||||
|
fn SetConsoleCP(wCodePageID: u32) -> i32;
|
||||||
|
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
|
||||||
|
fn GetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, lpMode: *mut u32) -> i32;
|
||||||
|
fn SetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, dwMode: u32) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _ = SetConsoleOutputCP(CP_UTF8);
|
||||||
|
let _ = SetConsoleCP(CP_UTF8);
|
||||||
|
|
||||||
|
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
if !handle.is_null() && handle as isize != -1 {
|
||||||
|
let mut mode: u32 = 0;
|
||||||
|
if GetConsoleMode(handle, &mut mode) != 0 {
|
||||||
|
if SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 {
|
||||||
|
VT_ENABLED.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn init_console() {
|
||||||
|
VT_ENABLED.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_vt_enabled() -> bool {
|
||||||
|
VT_ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[90m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn green(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1;32m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cyan(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1;36m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yellow(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1;33m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn red(s: &str) -> String {
|
||||||
|
if is_vt_enabled() {
|
||||||
|
format!("\x1b[1;31m{s}\x1b[0m")
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gibt einen formatierten Fortschrittsschritt aus: ` [1/4] 📦 Schrittbeschreibung...`
|
||||||
|
pub fn step(num: u8, total: u8, icon: &str, msg: &str) {
|
||||||
|
let tag = cyan(&format!("[{}/{}]", num, total));
|
||||||
|
println!(" {} {} {}", tag, icon, msg);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user