Skip to main content

libghostty_vt/
log.rs

1//! Logging functionality.
2use std::sync::RwLock;
3
4use crate::{
5    error::{Error, Result},
6    ffi,
7};
8
9/// Callback type for logging.
10///
11/// When installed, internal library log messages are delivered through
12/// this callback instead of being discarded. The embedder is responsible
13/// for formatting and routing log output.
14///
15/// When the log is unscoped (default scope), scope has zero length.
16///
17/// The callback must be safe to call from any thread.
18///
19/// See [`set_logger`] for more details.
20pub trait Logger: Send + Sync + 'static {
21    /// Log a message with the given level and scope.
22    fn log(&self, level: Level, scope: &str, message: &str);
23}
24
25/// Built-in log callback that writes to stderr.
26///
27/// Formats each message as `[level](scope): message\n`.
28///
29/// Can be passed directly to [`set_logger`]:
30///
31/// ```
32/// use libghostty_vt::log;
33/// log::set_logger(Some(Box::new(log::LogStderr)));
34/// ```
35#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
36pub struct LogStderr;
37impl Logger for LogStderr {
38    fn log(&self, level: Level, scope: &str, message: &str) {
39        unsafe {
40            ffi::ghostty_sys_log_stderr(
41                std::ptr::null_mut(),
42                level.into(),
43                scope.as_ptr(),
44                scope.len(),
45                message.as_ptr(),
46                message.len(),
47            );
48        }
49    }
50}
51
52#[cfg(feature = "tracing")]
53const TRACING_TARGET: &str = "libghostty_vt::ghostty";
54
55/// Adapt `libghostty` logs into `tracing` events.
56///
57/// This is the preferred integration for applications that already use
58/// `tracing`. The upstream callback only provides a level, scope, and message,
59/// so the scope is recorded as the structured `ghostty_scope` field rather than
60/// as the `tracing` target.
61#[cfg(feature = "tracing")]
62#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
63#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
64pub struct TracingLogger;
65#[cfg(feature = "tracing")]
66impl Logger for TracingLogger {
67    fn log(&self, level: Level, scope: &str, message: &str) {
68        match level {
69            Level::Error => {
70                tracing::error!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
71            }
72            Level::Warning => {
73                tracing::warn!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
74            }
75            Level::Info => {
76                tracing::info!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
77            }
78            Level::Debug => {
79                tracing::debug!(target: TRACING_TARGET, ghostty_scope = scope, "{message}");
80            }
81        }
82    }
83}
84
85/// Adapt a `log` implementation to be used by `libghostty`.
86///
87/// `libghostty` log scopes are translated directly to `log`'s metadata
88/// target, and `log` implementation can choose to filter specific
89/// `libghostty` logs to be emitted.
90#[cfg(feature = "log")]
91impl<L: log::Log + 'static> Logger for L {
92    fn log(&self, level: Level, scope: &str, message: &str) {
93        let level = match level {
94            Level::Error => log::Level::Error,
95            Level::Warning => log::Level::Warn,
96            Level::Info => log::Level::Info,
97            Level::Debug => log::Level::Debug,
98        };
99        let args = format_args!("{message}");
100        let record = log::Record::builder()
101            .level(level)
102            .target(scope)
103            .args(args)
104            .build();
105
106        log::Log::log(&self, &record);
107    }
108}
109
110/// Log severity levels for the log callback.
111#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, int_enum::IntEnum)]
112#[repr(u32)]
113#[non_exhaustive]
114#[expect(missing_docs, reason = "missing upstream docs")]
115pub enum Level {
116    Error = ffi::SysLogLevel::ERROR,
117    Warning = ffi::SysLogLevel::WARNING,
118    Info = ffi::SysLogLevel::INFO,
119    Debug = ffi::SysLogLevel::DEBUG,
120}
121
122static LOGGER: RwLock<Option<Box<dyn Logger>>> = RwLock::new(None);
123
124/// Set the log callback.
125///
126/// When set, internal library log messages are delivered to this
127/// callback. When cleared (set to `None`), log messages are silently
128/// discarded.
129///
130/// Which log levels are emitted depends on the build mode of the library and
131/// is not configurable at runtime. Debug builds emit all levels (debug and
132/// above). Release builds emit info and above; debug-level messages are
133/// compiled out entirely and will never reach the callback.
134///
135/// # Examples
136///
137/// Use [`LogStderr`] as a simple way to write formatted logs to stderr:
138///
139/// ```
140/// use libghostty_vt::{set_logger, log::LogStderr};
141/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
142/// set_logger(Some(Box::new(LogStderr)))?;
143/// # Ok(())}
144/// ```
145///
146/// When the `tracing` feature is enabled, you can redirect log messages to
147/// `tracing` events with `TracingLogger`:
148///
149/// ```ignore
150/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
151/// tracing_subscriber::fmt().init();
152/// libghostty_vt::set_logger(Some(Box::new(libghostty_vt::log::TracingLogger)))?;
153/// # Ok(())}
154/// ```
155///
156/// When the `log` feature is enabled, you can also redirect log messages
157/// to any `log`-compatible logger, including the one currently used globally:
158///
159/// ```ignore
160/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
161/// // Any logger will do, though usually you want to use the global logger
162/// libghostty_vt::set_logger(Some(Box::new(log::logger())))?;
163/// # Ok(())}
164/// ```
165pub fn set_logger(f: Option<Box<dyn Logger>>) -> Result<()> {
166    unsafe extern "C" fn callback(
167        _userdata: *mut std::ffi::c_void,
168        level: ffi::SysLogLevel::Type,
169        scope: *const u8,
170        scope_len: usize,
171        message: *const u8,
172        message_len: usize,
173    ) {
174        let scope = unsafe { std::slice::from_raw_parts(scope, scope_len) };
175        let Ok(scope) = std::str::from_utf8(scope) else {
176            return;
177        };
178        let message = unsafe { std::slice::from_raw_parts(message, message_len) };
179        let Ok(message) = std::str::from_utf8(message) else {
180            return;
181        };
182        let Ok(level) = Level::try_from(level) else {
183            return;
184        };
185
186        let Ok(log) = LOGGER.read() else {
187            return;
188        };
189        let Some(log) = log.as_deref() else {
190            return;
191        };
192        log.log(level, scope, message);
193    }
194
195    // Write out the matches here to coerce function items into function
196    // pointers, and trait impls into boxed trait objects. Yes, this is
197    // the simplest way to do so.
198    let ptr: ffi::SysLogFn = match f {
199        None => None,
200        Some(_) => Some(callback),
201    };
202    {
203        let Ok(mut logger) = LOGGER.write() else {
204            return Err(Error::InvalidValue);
205        };
206        *logger = f;
207    }
208    crate::sys_set(
209        ffi::SysOption::GHOSTTY_SYS_OPT_LOG,
210        ptr.map_or(std::ptr::null(), |p| p as *const std::ffi::c_void),
211    )
212}