diff --git a/Cargo.lock b/Cargo.lock index f7442a64..5ed0873a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,8 @@ dependencies = [ "bitflags", "edid-rs", "egui", + "serde", + "serde_json", "slog", "slog-async", "slog-scope", @@ -654,6 +656,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jni-sys" version = "0.3.0" @@ -1123,6 +1131,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + [[package]] name = "scan_fmt" version = "0.2.6" @@ -1147,6 +1161,17 @@ version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +[[package]] +name = "serde_json" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "slog" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index d196d366..507eaca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ slog-term = "2.8" slog-async = "2.7" slog-scope = "4.4" slog-stdlog = "4.1" +serde = { version = "1", optional = true } +serde_json = { version = "1", optional = true } egui = { version = "0.16", optional = true } edid-rs = { version = "0.1" } thiserror = "1.0.26" @@ -32,4 +34,4 @@ optional = true [features] default = [] -debug = ["egui", "smithay-egui"] \ No newline at end of file +debug = ["egui", "smithay-egui", "serde", "serde_json"] \ No newline at end of file diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index 4d8d6eda..45a5848b 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -3,7 +3,7 @@ use crate::state::Common; #[cfg(feature = "debug")] use crate::{ - debug::{debug_ui, fps_ui}, + debug::{debug_ui, fps_ui, log_ui}, state::Fps, }; @@ -44,7 +44,9 @@ pub fn render_output( let mut area = state.spaces.global_space(); area.loc = state.spaces.space_relative_output_geometry((0, 0), output); - //let output_geo = state.spaces.output_geometry(output); + if let Some(log_ui) = log_ui(state, area, scale, output_geo.size.w as f32 * 0.6) { + custom_elements.push(Box::new(log_ui)); + } if let Some(debug_overlay) = debug_ui(state, area, scale) { custom_elements.push(Box::new(debug_overlay)); } diff --git a/src/debug.rs b/src/debug.rs index 84f3f1a7..4e9d5db6 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -96,7 +96,7 @@ pub fn debug_ui( return None; } - Some(state.egui.state.run( + Some(state.egui.debug_state.run( |ctx| { egui::Window::new("Workspaces") .default_pos([0.0, 300.0]) @@ -206,3 +206,94 @@ pub fn debug_ui( state.egui.modifiers.clone(), )) } + +pub fn log_ui( + state: &mut Common, + area: Rectangle, + scale: f64, + default_width: f32, +) -> Option { + if !state.egui.active { + return None; + } + + Some(state.egui.log_state.run( + |ctx| { + egui::SidePanel::right("Log") + .default_width(default_width) + .show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + for (i, record) in state.log.debug_buffer.lock().unwrap().iter().enumerate() { + let mut message = egui::text::LayoutJob::single_section( + record.level.as_short_str().to_string(), + egui::TextFormat::simple(egui::TextStyle::Monospace, match record.level { + slog::Level::Critical => egui::Color32::RED, + slog::Level::Error => egui::Color32::LIGHT_RED, + slog::Level::Warning => egui::Color32::LIGHT_YELLOW, + slog::Level::Info => egui::Color32::LIGHT_BLUE, + slog::Level::Debug => egui::Color32::LIGHT_GREEN, + slog::Level::Trace => egui::Color32::GRAY, + }) + ); + message.append(&record.message, 1.0, egui::TextFormat::simple( + egui::TextStyle::Body, egui::Color32::WHITE, + )); + egui::containers::CollapsingHeader::new(message) + .id_source(i) + .show(ui, |ui| { + for (k, v) in &record.kv { + ui.horizontal(|ui| { + ui.add(egui::Label::new(egui::RichText::new(k).code()) + .sense(egui::Sense::click())) + .on_hover_cursor(egui::CursorIcon::PointingHand); + render_value(ui, v); + }); + } + }); + } + }) + }); + }, + area, + scale, + state.egui.alpha, + &state.start_time, + state.egui.modifiers.clone(), + )) +} + +fn render_value(ui: &mut egui::Ui, value: &serde_json::Value) { + use serde_json::Value::*; + + match value { + Null => { ui.label(egui::RichText::new("null").code()); }, + Bool(val) => { ui.label(egui::RichText::new(format!("{}", val)).code()); }, + Number(val) => { ui.label(egui::RichText::new(format!("{}", val)).code()); }, + String(val) => { ui.label(val); }, + Array(list) => { + ui.vertical(|ui| { + ui.label("["); + for val in list { + ui.horizontal(|ui| { + ui.add_space(4.0); + render_value(ui, val); + }); + } + ui.label("]"); + }); + }, + Object(map) => { + ui.vertical(|ui| { + for (k, val) in map { + ui.horizontal(|ui| { + ui.add_space(4.0); + ui.add(egui::Label::new(egui::RichText::new(k).code())); + render_value(ui, val); + }); + } + }); + }, + }; +} + + \ No newline at end of file diff --git a/src/input/mod.rs b/src/input/mod.rs index f8e293b2..2a8f7065 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -159,7 +159,8 @@ impl Common { } #[cfg(feature = "debug")] { - self.egui.state.handle_device_added(&device); + self.egui.debug_state.handle_device_added(&device); + self.egui.log_state.handle_device_added(&device); } } InputEvent::DeviceRemoved { device } => { @@ -183,7 +184,8 @@ impl Common { } #[cfg(feature = "debug")] { - self.egui.state.handle_device_added(&device); + self.egui.debug_state.handle_device_added(&device); + self.egui.log_state.handle_device_added(&device); } } InputEvent::Keyboard { event, .. } => { @@ -225,14 +227,23 @@ impl Common { } if self.seats.iter().position(|x| x == seat).unwrap() == 0 && self.egui.active - && self.egui.state.wants_keyboard() { - self.egui.state.handle_keyboard( - &handle, - state == KeyState::Pressed, - modifiers.clone(), - ); - return FilterResult::Intercept(()); + if self.egui.debug_state.wants_keyboard() { + self.egui.debug_state.handle_keyboard( + &handle, + state == KeyState::Pressed, + modifiers.clone(), + ); + return FilterResult::Intercept(()); + } + if self.egui.log_state.wants_keyboard() { + self.egui.log_state.handle_keyboard( + &handle, + state == KeyState::Pressed, + modifiers.clone(), + ); + return FilterResult::Intercept(()); + } } } @@ -291,7 +302,10 @@ impl Common { #[cfg(feature = "debug")] if self.seats.iter().position(|x| x == seat).unwrap() == 0 { self.egui - .state + .debug_state + .handle_pointer_motion(position.to_i32_round()); + self.egui + .log_state .handle_pointer_motion(position.to_i32_round()); } break; @@ -321,7 +335,10 @@ impl Common { #[cfg(feature = "debug")] if self.seats.iter().position(|x| x == seat).unwrap() == 0 { self.egui - .state + .debug_state + .handle_pointer_motion(position.to_i32_round()); + self.egui + .log_state .handle_pointer_motion(position.to_i32_round()); } break; @@ -342,16 +359,27 @@ impl Common { #[cfg(feature = "debug")] if self.seats.iter().position(|x| x == seat).unwrap() == 0 && self.egui.active - && self.egui.state.wants_pointer() { - if let Some(button) = event.button() { - self.egui.state.handle_pointer_button( - button, - event.state() == ButtonState::Pressed, - self.egui.modifiers.clone(), - ); + if self.egui.debug_state.wants_pointer() { + if let Some(button) = event.button() { + self.egui.debug_state.handle_pointer_button( + button, + event.state() == ButtonState::Pressed, + self.egui.modifiers.clone(), + ); + } + break; + } + if self.egui.log_state.wants_pointer() { + if let Some(button) = event.button() { + self.egui.log_state.handle_pointer_button( + button, + event.state() == ButtonState::Pressed, + self.egui.modifiers.clone(), + ); + } + break; } - break; } let serial = SERIAL_COUNTER.next_serial(); @@ -436,19 +464,33 @@ impl Common { #[cfg(feature = "debug")] if self.seats.iter().position(|x| x == seat).unwrap() == 0 && self.egui.active - && self.egui.state.wants_pointer() { - self.egui.state.handle_pointer_axis( - event - .amount_discrete(Axis::Horizontal) - .or_else(|| event.amount(Axis::Horizontal).map(|x| x * 3.0)) - .unwrap_or(0.0), - event - .amount_discrete(Axis::Vertical) - .or_else(|| event.amount(Axis::Vertical).map(|x| x * 3.0)) - .unwrap_or(0.0), - ); - break; + if self.egui.debug_state.wants_pointer() { + self.egui.debug_state.handle_pointer_axis( + event + .amount_discrete(Axis::Horizontal) + .or_else(|| event.amount(Axis::Horizontal).map(|x| x * 3.0)) + .unwrap_or(0.0), + event + .amount_discrete(Axis::Vertical) + .or_else(|| event.amount(Axis::Vertical).map(|x| x * 3.0)) + .unwrap_or(0.0), + ); + break; + } + if self.egui.log_state.wants_pointer() { + self.egui.log_state.handle_pointer_axis( + event + .amount_discrete(Axis::Horizontal) + .or_else(|| event.amount(Axis::Horizontal).map(|x| x * 3.0)) + .unwrap_or(0.0), + event + .amount_discrete(Axis::Vertical) + .or_else(|| event.amount(Axis::Vertical).map(|x| x * 3.0)) + .unwrap_or(0.0), + ); + break; + } } let userdata = seat.user_data(); diff --git a/src/logger/mod.rs b/src/logger/mod.rs new file mode 100644 index 00000000..281cd80f --- /dev/null +++ b/src/logger/mod.rs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-only + +#[cfg(feature = "debug")] +use std::{ + collections::VecDeque, + sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}, +}; + +use anyhow::Result; +use slog::Drain; + +#[cfg(feature = "debug")] +mod serializer; + +#[cfg(feature = "debug")] +const MAX_RECORDS: usize = 1000; + +#[cfg(feature = "debug")] +pub type LogBuffer = Arc>>; + +#[cfg(feature = "debug")] +#[derive(Clone)] +struct DebugDrain { + buffer: LogBuffer, + dirty_flag:Arc, +} + +pub struct LogState { + _guard: slog_scope::GlobalLoggerGuard, + #[cfg(feature = "debug")] + pub dirty_flag:Arc, + #[cfg(feature = "debug")] + pub debug_buffer: LogBuffer, +} + +#[cfg(feature = "debug")] +pub struct OwnedRecord { + pub message: String, + pub level: slog::Level, + pub kv: serde_json::map::Map, +} + +#[cfg(feature = "debug")] +impl DebugDrain { + fn new() -> (DebugDrain, LogBuffer, Arc) { + let dirty_flag = Arc::new(AtomicBool::new(false)); + let buffer = Arc::new(Mutex::new(VecDeque::new())); + ( + DebugDrain { + buffer: buffer.clone(), + dirty_flag: dirty_flag.clone() + }, + buffer, + dirty_flag, + ) + } +} + +#[cfg(feature = "debug")] +impl Drain for DebugDrain { + type Ok = (); + type Err = slog::Error; + + fn log( + &self, + record: &slog::Record<'_>, + values: &slog::OwnedKVList + ) -> Result { + use slog::KV; + use serializer::SerdeSerializer; + use serde_json::value::{ + Serializer as ValueSerializer, + Value, + }; + + let mut serializer = SerdeSerializer::start(ValueSerializer, None)?; + values.serialize(record, &mut serializer)?; + record.kv().serialize(record, &mut serializer)?; + let value = match serializer.end().map_err(|_| slog::Error::Other)? { + Value::Object(map) => map, + _ => unreachable!(), + }; + + let mut buffer = self.buffer.lock().unwrap(); + buffer.push_front(OwnedRecord { + message: format!("{}", record.msg()), + level: record.level(), + kv: value, + }); + buffer.truncate(MAX_RECORDS); + self.dirty_flag.store(true, Ordering::SeqCst); + + Ok(()) + } +} + +pub fn init_logger() -> Result { + let decorator = slog_term::TermDecorator::new().stderr().build(); + // usually we would not want to use a Mutex here, but this is usefull for a prototype, + // to make sure we do not miss any in-flight messages, when we crash. + #[cfg(not(feature = "debug"))] + let logger = slog::Logger::root( + std::sync::Mutex::new( + slog_term::CompactFormat::new(decorator) + .build() + .ignore_res(), + ) + .fuse(), + slog::o!(), + ); + #[cfg(feature = "debug")] + let (debug_drain, debug_buffer, dirty_flag) = DebugDrain::new(); + #[cfg(feature = "debug")] + let logger = slog::Logger::root( + slog::Duplicate::new( + std::sync::Mutex::new( + slog_term::CompactFormat::new(decorator) + .build() + .ignore_res() + ), + debug_drain, + ) + .fuse(), + slog::o!(), + ); + + let _guard = slog_scope::set_global_logger(logger); + slog_stdlog::init().unwrap(); + + slog_scope::info!("Version: {}", std::env!("CARGO_PKG_VERSION")); + if cfg!(feature = "debug") { + slog_scope::debug!( + "Debug build ({})", + std::option_env!("GIT_HASH").unwrap_or("Unknown") + ); + } + + Ok(LogState { + _guard, + #[cfg(feature = "debug")] + debug_buffer, + #[cfg(feature = "debug")] + dirty_flag, + }) +} \ No newline at end of file diff --git a/src/logger/serializer.rs b/src/logger/serializer.rs new file mode 100644 index 00000000..b442b1f9 --- /dev/null +++ b/src/logger/serializer.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MPL-2.0 OR MIT OR Apache-2.0 +// Taken from slog_json: +// https://github.com/slog-rs/json/blob/c45d09422d7114222ee5b7f6b0123de2605c7864/src/lib.rs#L39-L160 + +use serde::ser::SerializeMap; +use serde::serde_if_integer128; +use slog::Key; +use std::{fmt, io, result}; + +use std::cell::RefCell; +use std::fmt::Write; + +thread_local! { + static TL_BUF: RefCell = RefCell::new(String::with_capacity(128)) +} + +/// `slog::Serializer` adapter for `serde::Serializer` +/// +/// Newtype to wrap serde Serializer, so that `Serialize` can be implemented +/// for it +pub struct SerdeSerializer { + /// Current state of map serializing: `serde::Serializer::MapState` + ser_map: S::SerializeMap, +} + +impl SerdeSerializer { + /// Start serializing map of values + pub fn start(ser: S, len: Option) -> result::Result { + let ser_map = ser.serialize_map(len).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("serde serialization error: {}", e), + ) + })?; + Ok(SerdeSerializer { ser_map }) + } + + /// Finish serialization, and return the serializer + pub fn end(self) -> result::Result { + self.ser_map.end() + } +} + +macro_rules! impl_m( + ($s:expr, $key:expr, $val:expr) => ({ + let k_s: &str = $key.as_ref(); + $s.ser_map.serialize_entry(k_s, $val) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("serde serialization error: {}", e)))?; + Ok(()) + }); +); + +impl slog::Serializer for SerdeSerializer +where + S: serde::Serializer, +{ + fn emit_bool(&mut self, key: Key, val: bool) -> slog::Result { + impl_m!(self, key, &val) + } + + fn emit_unit(&mut self, key: Key) -> slog::Result { + impl_m!(self, key, &()) + } + + fn emit_char(&mut self, key: Key, val: char) -> slog::Result { + impl_m!(self, key, &val) + } + + fn emit_none(&mut self, key: Key) -> slog::Result { + let val: Option<()> = None; + impl_m!(self, key, &val) + } + fn emit_u8(&mut self, key: Key, val: u8) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i8(&mut self, key: Key, val: i8) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u16(&mut self, key: Key, val: u16) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i16(&mut self, key: Key, val: i16) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_usize(&mut self, key: Key, val: usize) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_isize(&mut self, key: Key, val: isize) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u32(&mut self, key: Key, val: u32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i32(&mut self, key: Key, val: i32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_f32(&mut self, key: Key, val: f32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u64(&mut self, key: Key, val: u64) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i64(&mut self, key: Key, val: i64) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_f64(&mut self, key: Key, val: f64) -> slog::Result { + impl_m!(self, key, &val) + } + serde_if_integer128! { + fn emit_u128(&mut self, key: Key, val: u128) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i128(&mut self, key: Key, val: i128) -> slog::Result { + impl_m!(self, key, &val) + } + } + fn emit_str(&mut self, key: Key, val: &str) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_arguments( + &mut self, + key: Key, + val: &fmt::Arguments, + ) -> slog::Result { + TL_BUF.with(|buf| { + let mut buf = buf.borrow_mut(); + + buf.write_fmt(*val).unwrap(); + + let res = { || impl_m!(self, key, &*buf) }(); + buf.clear(); + res + }) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5bc8f3d7..b96dc2b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,13 @@ use smithay::reexports::{ }; use anyhow::{Context, Result}; -use slog::Drain; use std::sync::atomic::Ordering; pub mod backend; pub mod input; pub mod shell; pub mod state; +mod logger; pub mod utils; #[cfg(feature = "debug")] @@ -20,7 +20,7 @@ pub mod debug; fn main() -> Result<()> { // setup logger - let _guard = init_logger(); + let log = logger::init_logger()?; slog_scope::info!("Cosmic starting up!"); // init event loop @@ -28,7 +28,7 @@ fn main() -> Result<()> { // init wayland let display = init_wayland_display(&mut event_loop)?; // init state - let mut state = state::State::new(display, event_loop.handle()); + let mut state = state::State::new(display, event_loop.handle(), log); // init backend backend::init_backend_auto(&mut event_loop, &mut state)?; @@ -60,33 +60,6 @@ fn main() -> Result<()> { Ok(()) } -fn init_logger() -> Result { - let decorator = slog_term::TermDecorator::new().stderr().build(); - // usually we would not want to use a Mutex here, but this is usefull for a prototype, - // to make sure we do not miss any in-flight messages, when we crash. - let logger = slog::Logger::root( - std::sync::Mutex::new( - slog_term::CompactFormat::new(decorator) - .build() - .ignore_res(), - ) - .fuse(), - slog::o!(), - ); - let guard = slog_scope::set_global_logger(logger); - slog_stdlog::init().unwrap(); - - slog_scope::info!("Version: {}", std::env!("CARGO_PKG_VERSION")); - if cfg!(feature = "debug") { - slog_scope::debug!( - "Debug build ({})", - std::option_env!("GIT_HASH").unwrap_or("Unknown") - ); - } - - Ok(guard) -} - fn init_wayland_display(event_loop: &mut EventLoop) -> Result { let mut display = Display::new(); let socket_name = display.add_socket_auto()?; diff --git a/src/state.rs b/src/state.rs index f659353b..b5b37ee1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,6 +3,7 @@ use crate::{ backend::{kms::KmsState, winit::WinitState, x11::X11State}, shell::{init_shell, workspaces::Workspaces, ShellStates}, + logger::LogState, }; use smithay::{ reexports::{ @@ -47,13 +48,15 @@ pub struct Common { pub start_time: Instant, pub should_stop: bool, + pub log: LogState, #[cfg(feature = "debug")] pub egui: Egui, } #[cfg(feature = "debug")] pub struct Egui { - pub state: smithay_egui::EguiState, + pub debug_state: smithay_egui::EguiState, + pub log_state: smithay_egui::EguiState, pub modifiers: smithay::wayland::seat::ModifiersState, pub active: bool, pub alpha: f32, @@ -122,7 +125,7 @@ pub fn get_dnd_icon(seat: &Seat) -> Option { } impl State { - pub fn new(mut display: Display, handle: LoopHandle<'static, State>) -> State { + pub fn new(mut display: Display, handle: LoopHandle<'static, State>, log: LogState) -> State { init_shm_global(&mut display, vec![], None); init_xdg_output_manager(&mut display, None); let shell_handles = init_shell(&mut display); @@ -151,6 +154,11 @@ impl State { None, ); + #[cfg(not(feature = "debug"))] + let dirty_flag = Arc::new(AtomicBool::new(false)); + #[cfg(feature = "debug")] + let dirty_flag = log.dirty_flag.clone(); + State { common: Common { display: Rc::new(RefCell::new(display)), @@ -159,7 +167,7 @@ impl State { spaces: Workspaces::new(), shell: shell_handles, pending_toplevels: Vec::new(), - dirty_flag: Arc::new(AtomicBool::new(false)), + dirty_flag, seats: vec![initial_seat.clone()], last_active_seat: initial_seat, @@ -167,9 +175,15 @@ impl State { start_time: Instant::now(), should_stop: false, + log, #[cfg(feature = "debug")] egui: Egui { - state: smithay_egui::EguiState::new(smithay_egui::EguiMode::Continuous), + debug_state: smithay_egui::EguiState::new(smithay_egui::EguiMode::Continuous), + log_state: { + let mut state = smithay_egui::EguiState::new(smithay_egui::EguiMode::Continuous); + state.set_zindex(0); + state + }, modifiers: Default::default(), active: false, alpha: 1.0,