diff --git a/src/commands/notify.rs b/src/commands/notify.rs index f10a0cb..f080b43 100644 --- a/src/commands/notify.rs +++ b/src/commands/notify.rs @@ -13,6 +13,7 @@ use lexopt::Arg; use crate::env::Env; use crate::error::Error; +use crate::log::warning; use crate::parse::TSigInfo; use crate::Args; @@ -223,24 +224,18 @@ impl Notify { } let Ok(name) = Name::>::from_str(server) else { - writeln!( - env.stderr(), - "warning: invalid domain name \"{server}\", skipping." - ); + warning!(env, "invalid domain name \"{server}\", skipping."); continue; }; let Ok(hosts) = resolver.lookup_host(&name).await else { - writeln!( - env.stderr(), - "warning: could not resolve host \"{name}\", skipping." - ); + warning!(env, "could not resolve host \"{name}\", skipping."); continue; }; if hosts.is_empty() { - writeln!( - env.stderr(), + warning!( + env, "skipping bad address: {name}: Name or service not known" ); continue; diff --git a/src/env/fake.rs b/src/env/fake.rs index 0b58840..7f99232 100644 --- a/src/env/fake.rs +++ b/src/env/fake.rs @@ -61,11 +61,17 @@ impl Env for FakeEnv { } fn stdout(&self) -> Stream { - Stream(self.stdout.clone()) + Stream { + writer: self.stdout.clone(), + is_terminal: false, + } } fn stderr(&self) -> Stream { - Stream(self.stderr.clone()) + Stream { + writer: self.stderr.clone(), + is_terminal: false, + } } fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path> { diff --git a/src/env/mod.rs b/src/env/mod.rs index 68fdd8c..fb4b1cd 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -59,7 +59,10 @@ pub trait Env { /// [`std::io::Write`]. Additionally, this `write_fmt` does not return a /// result. This means that we can use the [`write!`] and [`writeln`] macros /// without handling errors. -pub struct Stream(T); +pub struct Stream { + writer: T, + is_terminal: bool, +} impl Stream { pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) { @@ -68,7 +71,11 @@ impl Stream { // fake stream also does not return an error. If this fails, it means // we can't write to stdout anymore so a graceful exit will be very // hard anyway. - self.0.write_fmt(args).unwrap(); + self.writer.write_fmt(args).unwrap(); + } + + pub fn is_terminal(&self) -> bool { + self.is_terminal } } diff --git a/src/env/real.rs b/src/env/real.rs index 5b9e5b3..ff97f50 100644 --- a/src/env/real.rs +++ b/src/env/real.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; use std::fmt; -use std::io; +use std::io::{self, IsTerminal}; use std::path::Path; use domain::net::client::protocol::{AsyncConnect, AsyncDgramRecv, AsyncDgramSend, UdpConnect}; @@ -19,11 +19,19 @@ impl Env for RealEnv { } fn stdout(&self) -> Stream { - Stream(FmtWriter(io::stdout())) + let stdout = io::stdout(); + Stream { + is_terminal: stdout.is_terminal(), + writer: FmtWriter(io::stdout()), + } } fn stderr(&self) -> Stream { - Stream(FmtWriter(io::stderr())) + let stderr = io::stderr(); + Stream { + is_terminal: stderr.is_terminal(), + writer: FmtWriter(io::stdout()), + } } fn in_cwd<'a>(&self, path: &'a impl AsRef) -> std::borrow::Cow<'a, std::path::Path> { diff --git a/src/error.rs b/src/error.rs index 3bce26e..cdf50bc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,7 @@ use std::{error, io}; use domain::base::wire::ParseError; use crate::env::Env; +use crate::log::error; //------------ Error --------------------------------------------------------- @@ -57,33 +58,22 @@ impl Error { /// Pretty-print this error. pub fn pretty_print(&self, env: impl Env) { - use std::io::IsTerminal; let mut err = env.stderr(); - let error = match &self.0.primary { + let msg = match &self.0.primary { // Clap errors are already styled. We don't want our own pretty // styling around that and context does not make sense for command // line arguments either. So we just print the styled string that // clap produces and return. PrimaryError::Clap(e) => { + let mut err = env.stderr(); writeln!(err, "{}", e.render().ansi()); return; } PrimaryError::Other(error) => error, }; - // NOTE: This is a multicall binary, so argv[0] is necessary for - // program operation. We would fail very early if it didn't exist. - let prog = std::env::args().next().unwrap(); - let term = std::io::stderr().is_terminal(); - - let error_marker = if term { - "\x1B[31mERROR:\x1B[0m" - } else { - "ERROR:" - }; - - writeln!(err, "[{prog}] {error_marker} {error}"); + error!(env, "{msg}"); for context in &self.0.context { writeln!(err, "... while {context}"); } diff --git a/src/lib.rs b/src/lib.rs index c8b435f..1c83b77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod args; pub mod commands; pub mod env; pub mod error; +pub mod log; pub mod parse; pub mod util; diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..b47579c --- /dev/null +++ b/src/log.rs @@ -0,0 +1,54 @@ +use std::fmt::Display; + +use crate::env::Env; + +mod color { + pub const BLUE: u8 = 34; + pub const YELLOW: u8 = 33; + pub const RED: u8 = 31; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LogLevel { + Info, + Warning, + Error, +} + +impl LogLevel { + fn color(self) -> u8 { + match self { + Self::Info => color::BLUE, + Self::Warning => color::YELLOW, + Self::Error => color::RED, + } + } + + fn text(self) -> &'static str { + match self { + LogLevel::Info => "INFO", + LogLevel::Warning => "WARNING", + LogLevel::Error => "ERROR", + } + } +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.text()) + } +} + +struct Logger(&'static Env); + +pub fn log(env: impl Env, level: LogLevel, text: impl Display) { + let mut err = env.stderr(); + let prog = std::env::args().next().unwrap(); + + if err.is_terminal() { + let color = level.color(); + writeln!(err, "[{prog}] \x1B[{color}m{level}\x1B[0m: {text}"); + } else { + writeln!(err, "[{prog}] {level}: {text}"); + } +}