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/// Adapt a `log` implementation to be used by `libghostty`.
53///
54/// `libghostty` log scopes are translated directly to `log`'s metadata
55/// target, and `log` implementation can choose to filter specific
56/// `libghostty` logs to be emitted.
57#[cfg(feature = "log")]
58impl<L: log::Log + 'static> Logger for L {
59    fn log(&self, level: Level, scope: &str, message: &str) {
60        let level = match level {
61            Level::Error => log::Level::Error,
62            Level::Warning => log::Level::Warn,
63            Level::Info => log::Level::Info,
64            Level::Debug => log::Level::Debug,
65        };
66        let args = format_args!("{message}");
67        let record = log::Record::builder()
68            .level(level)
69            .target(scope)
70            .args(args)
71            .build();
72
73        log::Log::log(&self, &record);
74    }
75}
76
77/// Log severity levels for the log callback.
78#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, int_enum::IntEnum)]
79#[repr(u32)]
80#[non_exhaustive]
81#[expect(missing_docs, reason = "missing upstream docs")]
82pub enum Level {
83    Error = ffi::SysLogLevel::ERROR,
84    Warning = ffi::SysLogLevel::WARNING,
85    Info = ffi::SysLogLevel::INFO,
86    Debug = ffi::SysLogLevel::DEBUG,
87}
88
89static LOGGER: RwLock<Option<Box<dyn Logger>>> = RwLock::new(None);
90
91/// Set the log callback.
92///
93/// When set, internal library log messages are delivered to this
94/// callback. When cleared (set to `None`), log messages are silently
95/// discarded.
96///
97/// Which log levels are emitted depends on the build mode of the library and
98/// is not configurable at runtime. Debug builds emit all levels (debug and
99/// above). Release builds emit info and above; debug-level messages are
100/// compiled out entirely and will never reach the callback.
101///
102/// # Examples
103///
104/// Use [`LogStderr`] as a simple way to write formatted logs to stderr:
105///
106/// ```
107/// use libghostty_vt::{set_logger, log::LogStderr};
108/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
109/// set_logger(Some(Box::new(LogStderr)))?;
110/// # Ok(())}
111/// ```
112///
113/// When the `log` feature is enabled, you can also redirect log messages
114/// to any `log`-compatible logger, including the one currently used globally:
115///
116/// ```ignore
117/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
118/// // Any logger will do, though usually you want to use the global logger
119/// libghostty_vt::set_logger(Some(Box::new(log::logger())))?;
120/// # Ok(())}
121/// ```
122pub fn set_logger(f: Option<Box<dyn Logger>>) -> Result<()> {
123    unsafe extern "C" fn callback(
124        _userdata: *mut std::ffi::c_void,
125        level: ffi::SysLogLevel::Type,
126        scope: *const u8,
127        scope_len: usize,
128        message: *const u8,
129        message_len: usize,
130    ) {
131        let scope = unsafe { std::slice::from_raw_parts(scope, scope_len) };
132        let Ok(scope) = std::str::from_utf8(scope) else {
133            return;
134        };
135        let message = unsafe { std::slice::from_raw_parts(message, message_len) };
136        let Ok(message) = std::str::from_utf8(message) else {
137            return;
138        };
139        let Ok(level) = Level::try_from(level) else {
140            return;
141        };
142
143        let Ok(log) = LOGGER.read() else {
144            return;
145        };
146        let Some(log) = log.as_deref() else {
147            return;
148        };
149        log.log(level, scope, message);
150    }
151
152    // Write out the matches here to coerce function items into function
153    // pointers, and trait impls into boxed trait objects. Yes, this is
154    // the simplest way to do so.
155    let ptr: ffi::SysLogFn = match f {
156        None => None,
157        Some(_) => Some(callback),
158    };
159    {
160        let Ok(mut logger) = LOGGER.write() else {
161            return Err(Error::InvalidValue);
162        };
163        *logger = f;
164    }
165    crate::sys_set(
166        ffi::SysOption::GHOSTTY_SYS_OPT_LOG,
167        ptr.map_or(std::ptr::null(), |p| p as *const std::ffi::c_void),
168    )
169}