use std::fmt::Display;
use std::io::{self, IsTerminal, Write};
use std::sync::OnceLock;
use anstyle::{AnsiColor, Style};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use serde::Serialize;
use waterui_cli::utils::set_std_output;
static SHELL: OnceLock<Shell> = OnceLock::new();
mod styles {
use super::{AnsiColor, Style};
pub const HEADER: Style = Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Green)));
pub const ERROR: Style = Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Red)));
pub const WARN: Style = Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Yellow)));
pub const NOTE: Style = Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Cyan)));
pub const DEBUG: Style = Style::new().fg_color(Some(anstyle::Color::Ansi(AnsiColor::Magenta)));
pub const TRACE: Style =
Style::new().fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightBlack)));
pub const TAG: Style = Style::new().bold();
}
pub fn init(json: bool) {
let shell = if json { Shell::json() } else { Shell::new() };
let _ = SHELL.set(shell);
}
pub fn get() -> &'static Shell {
SHELL
.get()
.expect("shell not initialized, call shell::init() first")
}
pub struct Shell {
output: ShellOut,
multi_progress: MultiProgress,
}
enum ShellOut {
Human,
Json,
}
impl Shell {
fn new() -> Self {
Self {
output: ShellOut::Human,
multi_progress: MultiProgress::new(),
}
}
fn json() -> Self {
Self {
output: ShellOut::Json,
multi_progress: MultiProgress::new(),
}
}
#[must_use]
pub const fn is_json(&self) -> bool {
matches!(self.output, ShellOut::Json)
}
#[must_use]
pub fn is_terminal(&self) -> bool {
match &self.output {
ShellOut::Human => io::stderr().is_terminal(),
ShellOut::Json => false,
}
}
pub fn status(&self, status: impl Display, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
let mut stderr = anstream::stderr().lock();
writeln!(
stderr,
"{}{}{} {message}",
styles::HEADER,
status,
styles::HEADER.render_reset()
)?;
stderr.flush()
}
ShellOut::Json => {
#[derive(Serialize)]
struct Status<'a> {
status: &'a str,
message: &'a str,
}
let json = serde_json::to_string(&Status {
status: &status.to_string(),
message: &message.to_string(),
})?;
writeln!(io::stdout(), "{json}")?;
io::stdout().flush()
}
}
}
pub fn error(&self, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
let mut stderr = anstream::stderr().lock();
write!(
stderr,
"{}error{}: ",
styles::ERROR,
styles::ERROR.render_reset()
)?;
writeln!(stderr, "{message}")?;
stderr.flush()
}
ShellOut::Json => {
#[derive(Serialize)]
struct Error<'a> {
level: &'static str,
message: &'a str,
}
let json = serde_json::to_string(&Error {
level: "error",
message: &message.to_string(),
})?;
writeln!(io::stdout(), "{json}")?;
io::stdout().flush()
}
}
}
pub fn warn(&self, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
let mut stderr = anstream::stderr().lock();
write!(
stderr,
"{}warning{}: ",
styles::WARN,
styles::WARN.render_reset()
)?;
writeln!(stderr, "{message}")?;
stderr.flush()
}
ShellOut::Json => {
#[derive(Serialize)]
struct Warning<'a> {
level: &'static str,
message: &'a str,
}
let json = serde_json::to_string(&Warning {
level: "warning",
message: &message.to_string(),
})?;
writeln!(io::stdout(), "{json}")?;
io::stdout().flush()
}
}
}
pub fn note(&self, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
let mut stderr = anstream::stderr().lock();
write!(
stderr,
"{}note{}: ",
styles::NOTE,
styles::NOTE.render_reset()
)?;
writeln!(stderr, "{message}")?;
stderr.flush()
}
ShellOut::Json => Ok(()),
}
}
pub fn println(&self, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
writeln!(anstream::stderr().lock(), "{message}")?;
Ok(())
}
ShellOut::Json => Ok(()),
}
}
pub fn device_log(
&self,
platform: &str,
level: tracing::Level,
message: impl Display,
) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
let mut stderr = anstream::stderr().lock();
let msg = message.to_string();
let (level_style, level_char) = match level {
tracing::Level::ERROR => (styles::ERROR, 'E'),
tracing::Level::WARN => (styles::WARN, 'W'),
tracing::Level::INFO => (styles::NOTE, 'I'),
tracing::Level::DEBUG => (styles::DEBUG, 'D'),
tracing::Level::TRACE => (styles::TRACE, 'V'),
};
let reset = Style::new().render_reset();
if let Some((tag, rest)) = parse_log_tag(&msg) {
writeln!(
stderr,
"{level_style}{platform}/{level_char}{reset} {tag_style}[{tag}]{reset} {rest}",
tag_style = styles::TAG,
)?;
} else {
writeln!(stderr, "{level_style}{platform}/{level_char}{reset} {msg}",)?;
}
stderr.flush()
}
ShellOut::Json => {
#[derive(Serialize)]
struct Log<'a> {
#[serde(rename = "type")]
ty: &'static str,
platform: &'a str,
level: &'a str,
message: &'a str,
}
let level_str = match level {
tracing::Level::ERROR => "error",
tracing::Level::WARN => "warn",
tracing::Level::INFO => "info",
tracing::Level::DEBUG => "debug",
tracing::Level::TRACE => "trace",
};
let json = serde_json::to_string(&Log {
ty: "log",
platform,
level: level_str,
message: &message.to_string(),
})?;
writeln!(io::stdout(), "{json}")?;
io::stdout().flush()
}
}
}
pub fn header(&self, message: impl Display) -> io::Result<()> {
match &self.output {
ShellOut::Human => {
writeln!(
anstream::stderr().lock(),
"{}▶ {}{}",
styles::HEADER,
message,
styles::HEADER.render_reset()
)?;
Ok(())
}
ShellOut::Json => Ok(()),
}
}
#[must_use]
pub fn spinner(&self, message: impl Into<String>) -> Option<ProgressBar> {
if !self.is_terminal() || self.is_json() {
return None;
}
let pb = self.multi_progress.add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.cyan} {msg}")
.expect("valid template"),
);
pb.set_message(message.into());
pb.enable_steady_tick(std::time::Duration::from_millis(80));
Some(pb)
}
}
fn parse_log_tag(msg: &str) -> Option<(&str, &str)> {
let msg = msg.trim();
if !msg.starts_with('[') {
return None;
}
let end = msg.find(']')?;
let tag = &msg[1..end];
let rest = msg[end + 1..].trim_start();
Some((tag, rest))
}
pub fn status(status: impl Display, message: impl Display) {
let _ = get().status(status, message);
}
pub fn device_log(platform: &str, level: tracing::Level, message: impl Display) {
let _ = get().device_log(platform, level, message);
}
#[doc(hidden)]
pub fn error_fn(message: impl Display) {
let _ = get().error(message);
}
#[doc(hidden)]
pub fn warn_fn(message: impl Display) {
let _ = get().warn(message);
}
#[doc(hidden)]
pub fn note_fn(message: impl Display) {
let _ = get().note(message);
}
#[doc(hidden)]
pub fn println(message: impl Display) {
let _ = get().println(message);
}
#[doc(hidden)]
pub fn header_fn(message: impl Display) {
let _ = get().header(message);
}
pub async fn display_output<Fut: Future>(fut: Fut) -> Fut::Output {
if is_interactive() {
set_std_output(true);
let result = fut.await;
set_std_output(false);
result
} else {
fut.await
}
}
pub fn spinner(message: impl Into<String>) -> Option<ProgressBar> {
get().spinner(message)
}
pub fn is_interactive() -> bool {
get().is_terminal() && !get().is_json()
}
#[macro_export]
macro_rules! success {
($($arg:tt)*) => {
$crate::shell::status("✓", format!($($arg)*))
};
}
#[macro_export]
macro_rules! line {
() => {
$crate::shell::println("")
};
($($arg:tt)*) => {
$crate::shell::println(format!($($arg)*))
};
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
$crate::shell::warn_fn(format!($($arg)*))
};
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
$crate::shell::error_fn(format!($($arg)*))
};
}
#[macro_export]
macro_rules! note {
($($arg:tt)*) => {
$crate::shell::note_fn(format!($($arg)*))
};
}
#[macro_export]
macro_rules! header {
($($arg:tt)*) => {
$crate::shell::header_fn(format!($($arg)*))
};
}