From 9a2de98b3f3e4a327cc4e765bc2d105e9bc376a4 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Sat, 28 Sep 2024 11:19:55 -0700 Subject: [PATCH 1/6] Route logging output to inside app --- src/logger.rs | 112 +++++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 15 ++++++- 2 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/logger.rs b/src/logger.rs index 5ac4f3ca..d5625564 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,8 +1,17 @@ -use std::env; +use std::{ + env, mem, + sync::mpsc, + thread, + time::{Duration, Instant}, +}; +use ::log::Log; +use chrono::{DateTime, Utc}; use data::log; +use tokio::sync::mpsc as tokio_mpsc; +use tokio_stream::wrappers::ReceiverStream; -pub fn setup(is_debug: bool) -> Result<(), log::Error> { +pub fn setup(is_debug: bool) -> Result>, log::Error> { let level_filter = env::var("RUST_LOG") .ok() .as_deref() @@ -11,29 +20,94 @@ pub fn setup(is_debug: bool) -> Result<(), log::Error> { .unwrap_or(log::Level::Debug) .to_level_filter(); - let mut logger = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}:{} -- {}", - chrono::Local::now().format("%H:%M:%S%.3f"), - record.level(), - message - )) - }) + let mut io_sink = fern::Dispatch::new().format(|out, message, record| { + out.finish(format_args!( + "{}:{} -- {}", + chrono::Local::now().format("%H:%M:%S%.3f"), + record.level(), + message + )) + }); + + if is_debug { + io_sink = io_sink.chain(std::io::stdout()); + } else { + let log_file = log::file()?; + + io_sink = io_sink.chain(log_file); + } + + let (channel_sink, reciever) = channel_logger(); + + fern::Dispatch::new() .level(log::LevelFilter::Off) .level_for("panic", log::LevelFilter::Error) .level_for("iced_wgpu", log::LevelFilter::Info) .level_for("data", level_filter) - .level_for("halloy", level_filter); + .level_for("halloy", level_filter) + .chain(io_sink) + .chain(channel_sink) + .apply()?; - if is_debug { - logger = logger.chain(std::io::stdout()); - } else { - let log_file = log::file()?; + Ok(reciever) +} - logger = logger.chain(log_file); +fn channel_logger() -> (Box, ReceiverStream>) { + let (log_sender, log_receiver) = mpsc::channel(); + let (async_sender, async_receiver) = tokio_mpsc::channel(1); + + struct Sink { + sender: mpsc::Sender, + } + + impl Log for Sink { + fn enabled(&self, _metadata: &::log::Metadata) -> bool { + true + } + + fn log(&self, record: &::log::Record) { + let _ = self.sender.send(Record { + timestamp: Utc::now(), + level: record.level(), + message: format!("{}", record.args()), + }); + } + + fn flush(&self) {} } - logger.apply()?; - Ok(()) + thread::spawn(move || { + const BATCH_SIZE: usize = 25; + const BATCH_TIMEOUT: Duration = Duration::from_millis(250); + + let mut batch = Vec::with_capacity(BATCH_SIZE); + let mut timeout = Instant::now(); + + loop { + if let Ok(log) = log_receiver.recv_timeout(BATCH_TIMEOUT) { + batch.push(log); + } + + if batch.len() >= BATCH_SIZE + || (!batch.is_empty() && timeout.elapsed() >= BATCH_TIMEOUT) + { + timeout = Instant::now(); + + let _ = async_sender + .blocking_send(mem::replace(&mut batch, Vec::with_capacity(BATCH_SIZE))); + } + } + }); + + ( + Box::new(Sink { sender: log_sender }), + ReceiverStream::new(async_receiver), + ) +} + +#[derive(Debug)] +pub struct Record { + pub timestamp: DateTime, + pub level: log::Level, + pub message: String, } diff --git a/src/main.rs b/src/main.rs index c7b4f370..da292d00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ use iced::widget::{column, container}; use iced::{padding, Length, Subscription, Task}; use screen::{dashboard, help, migration, welcome}; use tokio::runtime; +use tokio_stream::wrappers::ReceiverStream; use self::event::{events, Event}; use self::modal::Modal; @@ -57,7 +58,7 @@ pub fn main() -> Result<(), Box> { // Prepare notifications. notification::prepare(); - logger::setup(is_debug).expect("setup logging"); + let log_stream = logger::setup(is_debug).expect("setup logging"); log::info!("halloy {} has started", environment::formatted_version()); log::info!("config dir: {:?}", environment::config_dir()); log::info!("data dir: {:?}", environment::data_dir()); @@ -94,7 +95,7 @@ pub fn main() -> Result<(), Box> { .scale_factor(Halloy::scale_factor) .subscription(Halloy::subscription) .settings(settings(&config_load)) - .run_with(move || Halloy::new(config_load.clone(), destination.clone())) + .run_with(move || Halloy::new(config_load, destination, log_stream)) .inspect_err(|err| log::error!("{}", err))?; Ok(()) @@ -126,6 +127,7 @@ struct Halloy { servers: server::Map, modal: Option, main_window: Window, + logs: Vec, } impl Halloy { @@ -188,6 +190,7 @@ impl Halloy { config, modal: None, main_window, + logs: vec![], }, command, ) @@ -219,12 +222,14 @@ pub enum Message { AppearanceChange(appearance::Mode), Window(window::Id, window::Event), WindowSettingsSaved(Result<(), window::Error>), + Logging(Vec), } impl Halloy { fn new( config_load: Result, url_received: Option, + log_stream: ReceiverStream>, ) -> (Halloy, Task) { let (main_window, open_main_window) = window::open(window::Settings { size: window::default_size(), @@ -242,6 +247,7 @@ impl Halloy { open_main_window.then(|_| Task::none()), command, latest_remote_version, + Task::stream(log_stream).map(Message::Logging), ]; if let Some(url) = url_received { @@ -890,6 +896,11 @@ impl Halloy { } } + Task::none() + } + Message::Logging(messages) => { + self.logs.extend(messages); + Task::none() } } From aac1fcf7696b3db4f81cc74e62efd8e7b5f1382b Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Mon, 30 Sep 2024 09:38:55 -0700 Subject: [PATCH 2/6] Hook up logging to new buffer & history --- data/src/config/sidebar.rs | 3 ++ data/src/history.rs | 5 ++ data/src/history/manager.rs | 41 ++++++++++++++++- data/src/history/metadata.rs | 1 + data/src/log.rs | 35 +++++++++++++- data/src/message.rs | 18 ++++++++ data/src/message/source.rs | 1 + data/src/pane.rs | 1 + src/buffer.rs | 30 +++++++++--- src/buffer/channel.rs | 1 + src/buffer/logs.rs | 81 +++++++++++++++++++++++++++++++++ src/buffer/query.rs | 1 + src/buffer/scroll_view.rs | 2 + src/logger.rs | 20 +++----- src/main.rs | 29 +++++++++--- src/screen/dashboard.rs | 64 ++++++++++++++++++++++++-- src/screen/dashboard/pane.rs | 3 ++ src/screen/dashboard/sidebar.rs | 17 +++++++ src/widget/message_content.rs | 21 +++++++++ 19 files changed, 342 insertions(+), 32 deletions(-) create mode 100644 src/buffer/logs.rs diff --git a/data/src/config/sidebar.rs b/data/src/config/sidebar.rs index 64bffebc..2a25753e 100644 --- a/data/src/config/sidebar.rs +++ b/data/src/config/sidebar.rs @@ -69,6 +69,8 @@ pub struct Buttons { pub reload_config: bool, #[serde(default = "default_bool_true")] pub theme_editor: bool, + #[serde(default = "default_bool_true")] + pub logs: bool, } impl Default for Buttons { @@ -78,6 +80,7 @@ impl Default for Buttons { command_bar: default_bool_true(), reload_config: default_bool_true(), theme_editor: default_bool_true(), + logs: default_bool_true(), } } } diff --git a/data/src/history.rs b/data/src/history.rs index be1a0776..5ced1c7d 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -31,6 +31,7 @@ pub enum Kind { Server, Channel(String), Query(Nick), + Logs, } impl Kind { @@ -39,6 +40,7 @@ impl Kind { Kind::Server => None, Kind::Channel(channel) => Some(channel), Kind::Query(nick) => Some(nick.as_ref()), + Kind::Logs => None, } } } @@ -49,6 +51,7 @@ impl fmt::Display for Kind { Kind::Server => write!(f, "server"), Kind::Channel(channel) => write!(f, "channel {channel}"), Kind::Query(nick) => write!(f, "user {}", nick), + Kind::Logs => write!(f, "logs"), } } } @@ -59,6 +62,7 @@ impl From for Kind { message::Target::Server { .. } => Kind::Server, message::Target::Channel { channel, .. } => Kind::Channel(channel), message::Target::Query { nick, .. } => Kind::Query(nick), + message::Target::Logs => Kind::Logs, } } } @@ -154,6 +158,7 @@ async fn path(server: &server::Server, kind: &Kind) -> Result { Kind::Server => format!("{server}"), Kind::Channel(channel) => format!("{server}channel{channel}"), Kind::Query(nick) => format!("{server}nickname{}", nick), + Kind::Logs => "log".to_string(), }; let hashed_name = seahash::hash(name.as_bytes()); diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 209842b3..e8045014 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -12,12 +12,24 @@ use crate::user::Nick; use crate::{config, input}; use crate::{server, Buffer, Config, Input, Server, User}; +// Hack since log messages are app wide and not scoped to any server +const LOG_SERVER_NAME: &str = ""; + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Resource { pub server: server::Server, pub kind: history::Kind, } +impl Resource { + pub fn logs() -> Self { + Self { + server: Server::from(LOG_SERVER_NAME), + kind: history::Kind::Logs, + } + } +} + #[derive(Debug)] pub enum Message { LoadFull( @@ -112,7 +124,10 @@ impl Manager { log::warn!("failed to close history for {kind} on {server}: {error}") } Message::Flushed(server, kind, Ok(_)) => { - log::debug!("flushed history for {kind} on {server}",); + // Will cause flush loop if we emit a log every time we flush logs + if !matches!(kind, history::Kind::Logs) { + log::debug!("flushed history for {kind} on {server}",); + } } Message::Flushed(server, kind, Err(error)) => { log::warn!("failed to flush history for {kind} on {server}: {error}") @@ -225,6 +240,17 @@ impl Manager { ) } + pub fn record_log( + &mut self, + record: crate::log::Record, + ) -> Option> { + self.data.add_message( + Server::from(LOG_SERVER_NAME), + history::Kind::Logs, + crate::Message::log(record), + ) + } + pub fn update_read_marker( &mut self, server: Server, @@ -282,6 +308,19 @@ impl Manager { ) } + pub fn get_log_messages( + &self, + limit: Option, + buffer_config: &config::Buffer, + ) -> Option> { + self.data.history_view( + &Server::from(LOG_SERVER_NAME), + &history::Kind::Logs, + limit, + buffer_config, + ) + } + pub fn get_unique_queries(&self, server: &Server) -> Vec<&Nick> { let Some(map) = self.data.map.get(server) else { return vec![]; diff --git a/data/src/history/metadata.rs b/data/src/history/metadata.rs index 89273682..65223663 100644 --- a/data/src/history/metadata.rs +++ b/data/src/history/metadata.rs @@ -118,6 +118,7 @@ async fn path(server: &server::Server, kind: &Kind) -> Result { Kind::Server => format!("{server}-metadata"), Kind::Channel(channel) => format!("{server}channel{channel}-metadata"), Kind::Query(nick) => format!("{server}nickname{}-metadata", nick), + Kind::Logs => "log-metadata".to_string(), }; let hashed_name = seahash::hash(name.as_bytes()); diff --git a/data/src/log.rs b/data/src/log.rs index cc06c5fe..25975e77 100644 --- a/data/src/log.rs +++ b/data/src/log.rs @@ -1,7 +1,9 @@ use std::path::PathBuf; use std::{fs, io}; -pub use log::*; +use chrono::{DateTime, Utc}; + +use serde::{Deserialize, Serialize}; use crate::environment; @@ -26,6 +28,37 @@ fn path() -> Result { Ok(parent.join("halloy.log")) } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Record { + pub timestamp: DateTime, + pub level: Level, + pub message: String, +} + +#[derive( + Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Serialize, Deserialize, strum::Display, +)] +#[strum(serialize_all = "UPPERCASE")] +pub enum Level { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From for Level { + fn from(level: log::Level) -> Self { + match level { + log::Level::Error => Level::Error, + log::Level::Warn => Level::Warn, + log::Level::Info => Level::Info, + log::Level::Debug => Level::Debug, + log::Level::Trace => Level::Trace, + } + } +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] diff --git a/data/src/message.rs b/data/src/message.rs index cfa037d6..6b661f81 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -116,6 +116,7 @@ pub enum Target { nick: Nick, source: Source, }, + Logs, } impl Target { @@ -124,6 +125,7 @@ impl Target { Target::Server { .. } => None, Target::Channel { prefix, .. } => prefix.as_ref(), Target::Query { .. } => None, + Target::Logs => None, } } @@ -132,6 +134,7 @@ impl Target { Target::Server { source } => source, Target::Channel { source, .. } => source, Target::Query { source, .. } => source, + Target::Logs => &Source::Internal(source::Internal::Logs), } } } @@ -233,6 +236,18 @@ impl Message { match &self.content { Content::Plain(s) => Some(s), Content::Fragments(_) => None, + Content::Log(_) => None, + } + } + + pub fn log(record: crate::log::Record) -> Self { + Self { + received_at: Posix::now(), + server_time: record.timestamp, + direction: Direction::Received, + target: Target::Logs, + content: Content::Log(record), + id: None, } } } @@ -467,6 +482,7 @@ fn parse_user_and_channel_fragments(text: &str, channel_users: &[User]) -> Vec), + Log(crate::log::Record), } impl Content { @@ -474,6 +490,7 @@ impl Content { match self { Content::Plain(s) => s.into(), Content::Fragments(fragments) => fragments.iter().map(Fragment::as_str).join("").into(), + Content::Log(record) => (&record.message).into(), } } } @@ -1073,6 +1090,7 @@ pub fn references_user(sender: NickRef, own_nick: NickRef, message: &Message) -> Content::Fragments(fragments) => fragments .iter() .any(|f| references_user_text(sender, own_nick, f.as_str())), + Content::Log(_) => false, } } diff --git a/data/src/message/source.rs b/data/src/message/source.rs index 49cb4968..7e25e08d 100644 --- a/data/src/message/source.rs +++ b/data/src/message/source.rs @@ -15,6 +15,7 @@ pub enum Source { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Internal { Status(Status), + Logs, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/data/src/pane.rs b/data/src/pane.rs index b58ad73e..fee56d56 100644 --- a/data/src/pane.rs +++ b/data/src/pane.rs @@ -16,6 +16,7 @@ pub enum Pane { }, Empty, FileTransfers, + Logs, } #[derive(Debug, Clone, Copy, Deserialize, Serialize)] diff --git a/src/buffer.rs b/src/buffer.rs index d8f393e0..7b735572 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3,10 +3,11 @@ use data::user::Nick; use data::{buffer, file_transfer, history, Config}; use iced::Task; -use self::channel::Channel; -use self::file_transfers::FileTransfers; -use self::query::Query; -use self::server::Server; +pub use self::channel::Channel; +pub use self::file_transfers::FileTransfers; +pub use self::logs::Logs; +pub use self::query::Query; +pub use self::server::Server; use crate::screen::dashboard::sidebar; use crate::widget::Element; use crate::Theme; @@ -15,6 +16,7 @@ pub mod channel; pub mod empty; pub mod file_transfers; mod input_view; +pub mod logs; pub mod query; mod scroll_view; pub mod server; @@ -27,6 +29,7 @@ pub enum Buffer { Server(Server), Query(Query), FileTransfers(FileTransfers), + Logs(Logs), } #[derive(Debug, Clone)] @@ -35,6 +38,7 @@ pub enum Message { Server(server::Message), Query(query::Message), FileTransfers(file_transfers::Message), + Log(logs::Message), } pub enum Event { @@ -55,6 +59,7 @@ impl Buffer { Buffer::Server(state) => Some(&state.buffer), Buffer::Query(state) => Some(&state.buffer), Buffer::FileTransfers(_) => None, + Buffer::Logs(_) => None, } } @@ -142,6 +147,7 @@ impl Buffer { Buffer::FileTransfers(state) => { file_transfers::view(state, file_transfers).map(Message::FileTransfers) } + Buffer::Logs(state) => logs::view(state, history, config, theme).map(Message::Log), } } @@ -167,7 +173,7 @@ impl Buffer { pub fn focus(&self) -> Task { match self { - Buffer::Empty | Buffer::FileTransfers(_) => Task::none(), + Buffer::Empty | Buffer::FileTransfers(_) | Buffer::Logs(_) => Task::none(), Buffer::Channel(channel) => channel.focus().map(Message::Channel), Buffer::Server(server) => server.focus().map(Message::Server), Buffer::Query(query) => query.focus().map(Message::Query), @@ -176,7 +182,7 @@ impl Buffer { pub fn reset(&mut self) { match self { - Buffer::Empty | Buffer::FileTransfers(_) => {} + Buffer::Empty | Buffer::FileTransfers(_) | Buffer::Logs(_) => {} Buffer::Channel(channel) => channel.reset(), Buffer::Server(server) => server.reset(), Buffer::Query(query) => query.reset(), @@ -190,7 +196,9 @@ impl Buffer { ) -> Task { if let Some(buffer) = self.data().cloned() { match self { - Buffer::Empty | Buffer::Server(_) | Buffer::FileTransfers(_) => Task::none(), + Buffer::Empty | Buffer::Server(_) | Buffer::FileTransfers(_) | Buffer::Logs(_) => { + Task::none() + } Buffer::Channel(channel) => channel .input_view .insert_user(nick, buffer, history) @@ -220,6 +228,10 @@ impl Buffer { .scroll_view .scroll_to_start() .map(|message| Message::Query(query::Message::ScrollView(message))), + Buffer::Logs(log) => log + .scroll_view + .scroll_to_start() + .map(|message| Message::Log(logs::Message::ScrollView(message))), } } @@ -238,6 +250,10 @@ impl Buffer { .scroll_view .scroll_to_end() .map(|message| Message::Query(query::Message::ScrollView(message))), + Buffer::Logs(log) => log + .scroll_view + .scroll_to_end() + .map(|message| Message::Log(logs::Message::ScrollView(message))), } } } diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index b97e55e8..944c6fca 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -237,6 +237,7 @@ pub fn view<'a>( .into(), ) } + message::Source::Internal(message::source::Internal::Logs) => None, } }, ) diff --git a/src/buffer/logs.rs b/src/buffer/logs.rs new file mode 100644 index 00000000..615f43cf --- /dev/null +++ b/src/buffer/logs.rs @@ -0,0 +1,81 @@ +use data::{history, message, Config}; +use iced::widget::container; +use iced::{Length, Task}; + +use super::{scroll_view, user_context}; +use crate::widget::{message_content, Element}; +use crate::{theme, Theme}; + +#[derive(Debug, Clone)] +pub enum Message { + ScrollView(scroll_view::Message), +} + +pub enum Event { + UserContext(user_context::Event), + OpenChannel(String), + History(Task), +} + +pub fn view<'a>( + state: &'a Logs, + history: &'a history::Manager, + config: &'a Config, + theme: &'a Theme, +) -> Element<'a, Message> { + let messages = container( + scroll_view::view( + &state.scroll_view, + scroll_view::Kind::Log, + history, + config, + move |message, _, _| match message.target.source() { + message::Source::Internal(message::source::Internal::Logs) => Some( + container(message_content( + &message.content, + theme, + scroll_view::Message::Link, + theme::selectable_text::default, + config, + )) + .into(), + ), + _ => None, + }, + ) + .map(Message::ScrollView), + ) + .height(Length::Fill); + + container(messages) + .width(Length::Fill) + .height(Length::Fill) + .padding(8) + .into() +} + +#[derive(Debug, Clone, Default)] +pub struct Logs { + pub scroll_view: scroll_view::State, +} + +impl Logs { + pub fn new() -> Self { + Self::default() + } + + pub fn update(&mut self, message: Message) -> (Task, Option) { + match message { + Message::ScrollView(message) => { + let (command, event) = self.scroll_view.update(message); + + let event = event.map(|event| match event { + scroll_view::Event::UserContext(event) => Event::UserContext(event), + scroll_view::Event::OpenChannel(channel) => Event::OpenChannel(channel), + }); + + (command.map(Message::ScrollView), event) + } + } + } +} diff --git a/src/buffer/query.rs b/src/buffer/query.rs index 9a6b8ebb..bb5908e4 100644 --- a/src/buffer/query.rs +++ b/src/buffer/query.rs @@ -178,6 +178,7 @@ pub fn view<'a>( .into(), ) } + message::Source::Internal(message::source::Internal::Logs) => None, } }, ) diff --git a/src/buffer/scroll_view.rs b/src/buffer/scroll_view.rs index 5667f8ec..0dfe9a43 100644 --- a/src/buffer/scroll_view.rs +++ b/src/buffer/scroll_view.rs @@ -33,6 +33,7 @@ pub enum Kind<'a> { Server(&'a Server), Channel(&'a Server, &'a str), Query(&'a Server, &'a Nick), + Log, } pub fn view<'a>( @@ -58,6 +59,7 @@ pub fn view<'a>( Kind::Query(server, user) => { history.get_query_messages(server, user, Some(state.limit), &config.buffer) } + Kind::Log => history.get_log_messages(Some(state.limit), &config.buffer), }) else { return column![].into(); diff --git a/src/logger.rs b/src/logger.rs index d5625564..6d9fba37 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -5,13 +5,14 @@ use std::{ time::{Duration, Instant}, }; -use ::log::Log; -use chrono::{DateTime, Utc}; -use data::log; +use chrono::Utc; +use log::Log; use tokio::sync::mpsc as tokio_mpsc; use tokio_stream::wrappers::ReceiverStream; -pub fn setup(is_debug: bool) -> Result>, log::Error> { +pub use data::log::{Error, Record}; + +pub fn setup(is_debug: bool) -> Result>, Error> { let level_filter = env::var("RUST_LOG") .ok() .as_deref() @@ -32,7 +33,7 @@ pub fn setup(is_debug: bool) -> Result>, log::Error> if is_debug { io_sink = io_sink.chain(std::io::stdout()); } else { - let log_file = log::file()?; + let log_file = data::log::file()?; io_sink = io_sink.chain(log_file); } @@ -68,7 +69,7 @@ fn channel_logger() -> (Box, ReceiverStream>) { fn log(&self, record: &::log::Record) { let _ = self.sender.send(Record { timestamp: Utc::now(), - level: record.level(), + level: record.level().into(), message: format!("{}", record.args()), }); } @@ -104,10 +105,3 @@ fn channel_logger() -> (Box, ReceiverStream>) { ReceiverStream::new(async_receiver), ) } - -#[derive(Debug)] -pub struct Record { - pub timestamp: DateTime, - pub level: log::Level, - pub message: String, -} diff --git a/src/main.rs b/src/main.rs index da292d00..484f9a85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,8 @@ mod widget; mod window; use std::collections::HashSet; -use std::env; use std::time::{Duration, Instant}; +use std::{env, mem}; use appearance::{theme, Theme}; use chrono::Utc; @@ -127,7 +127,7 @@ struct Halloy { servers: server::Map, modal: Option, main_window: Window, - logs: Vec, + pending_logs: Vec, } impl Halloy { @@ -190,7 +190,7 @@ impl Halloy { config, modal: None, main_window, - logs: vec![], + pending_logs: vec![], }, command, ) @@ -898,10 +898,27 @@ impl Halloy { Task::none() } - Message::Logging(messages) => { - self.logs.extend(messages); + Message::Logging(mut records) => { + let Screen::Dashboard(dashboard) = &mut self.screen else { + self.pending_logs.extend(records); - Task::none() + return Task::none(); + }; + + // We've moved from non-dashboard screen to dashboard, prepend records + if !self.pending_logs.is_empty() { + records = mem::take(&mut self.pending_logs) + .into_iter() + .chain(records) + .collect(); + } + + Task::batch( + records + .into_iter() + .map(|record| dashboard.record_log(record)), + ) + .map(Message::Dashboard) } } } diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index c80c4fb2..3607c064 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -19,7 +19,6 @@ use self::command_bar::CommandBar; use self::pane::Pane; use self::sidebar::Sidebar; use self::theme_editor::ThemeEditor; -use crate::buffer::file_transfers::FileTransfers; use crate::buffer::{self, Buffer}; use crate::widget::{ anchored_overlay, context_menu, selectable_text, shortcut, Column, Element, Row, @@ -454,6 +453,7 @@ impl Dashboard { sidebar::Event::ToggleFileTransfers => { (self.toggle_file_transfers(config, main_window), None) } + sidebar::Event::ToggleLogs => (self.toggle_logs(config, main_window), None), sidebar::Event::ToggleCommandBar => ( self.toggle_command_bar( &closed_buffers(self, main_window.id, clients), @@ -1061,7 +1061,7 @@ impl Dashboard { for (id, pane) in panes.main.iter() { if let Buffer::Empty = &pane.buffer { self.panes.main.panes.entry(*id).and_modify(|p| { - *p = Pane::new(Buffer::FileTransfers(FileTransfers::new()), config) + *p = Pane::new(Buffer::FileTransfers(buffer::FileTransfers::new()), config) }); self.last_changed = Some(Instant::now()); @@ -1075,7 +1075,51 @@ impl Dashboard { if let Some((window, pane)) = self.focus.take() { if let Some(state) = self.panes.get_mut(main_window.id, window, pane) { - state.buffer = Buffer::FileTransfers(FileTransfers::new()); + state.buffer = Buffer::FileTransfers(buffer::FileTransfers::new()); + self.last_changed = Some(Instant::now()); + + commands.extend(vec![ + self.reset_pane(main_window, window, pane), + self.focus_pane(main_window, window, pane), + ]); + } + } + + Task::batch(commands) + } + + fn toggle_logs(&mut self, config: &Config, main_window: &Window) -> Task { + let panes = self.panes.clone(); + + // If logs already is open, we close it. + for (window, id, pane) in panes.iter(main_window.id) { + if matches!(pane.buffer, Buffer::Logs(_)) { + return self.close_pane(main_window, window, id); + } + } + + // If we only have one pane, and its empty, we replace it. + if self.panes.len() == 1 { + for (id, pane) in panes.main.iter() { + if let Buffer::Empty = &pane.buffer { + self.panes + .main + .panes + .entry(*id) + .and_modify(|p| *p = Pane::new(Buffer::Logs(buffer::Logs::new()), config)); + self.last_changed = Some(Instant::now()); + + return self.focus_pane(main_window, main_window.id, *id); + } + } + } + + let mut commands = vec![]; + let _ = self.new_pane(pane_grid::Axis::Vertical, config, main_window); + + if let Some((window, pane)) = self.focus.take() { + if let Some(state) = self.panes.get_mut(main_window.id, window, pane) { + state.buffer = Buffer::Logs(buffer::Logs::new()); self.last_changed = Some(Instant::now()); commands.extend(vec![ @@ -1216,6 +1260,14 @@ impl Dashboard { } } + pub fn record_log(&mut self, record: data::log::Record) -> Task { + if let Some(task) = self.history.record_log(record) { + Task::perform(task, Message::History) + } else { + Task::none() + } + } + pub fn broadcast( &mut self, server: &Server, @@ -1602,7 +1654,11 @@ impl Dashboard { buffer::Settings::default(), )), data::Pane::FileTransfers => Configuration::Pane(Pane::with_settings( - Buffer::FileTransfers(FileTransfers::new()), + Buffer::FileTransfers(buffer::FileTransfers::new()), + buffer::Settings::default(), + )), + data::Pane::Logs => Configuration::Pane(Pane::with_settings( + Buffer::Logs(buffer::Logs::new()), buffer::Settings::default(), )), } diff --git a/src/screen/dashboard/pane.rs b/src/screen/dashboard/pane.rs index 6e712b44..6c9d6e4e 100644 --- a/src/screen/dashboard/pane.rs +++ b/src/screen/dashboard/pane.rs @@ -82,6 +82,7 @@ impl Pane { format!("{nick} @ {server}") } Buffer::FileTransfers(_) => "File Transfers".to_string(), + Buffer::Logs(_) => "Logs".to_string(), }; let title_bar = self.title_bar.view( @@ -132,6 +133,7 @@ impl Pane { kind: history::Kind::Query(query.nick.clone()), }), Buffer::FileTransfers(_) => None, + Buffer::Logs(_) => Some(history::Resource::logs()), } } @@ -302,6 +304,7 @@ impl From for data::Pane { Buffer::Server(state) => data::Buffer::Server(state.server), Buffer::Query(state) => data::Buffer::Query(state.server, state.nick), Buffer::FileTransfers(_) => return data::Pane::FileTransfers, + Buffer::Logs(_) => return data::Pane::Logs, }; data::Pane::Buffer { diff --git a/src/screen/dashboard/sidebar.rs b/src/screen/dashboard/sidebar.rs index 0e107621..c576abd3 100644 --- a/src/screen/dashboard/sidebar.rs +++ b/src/screen/dashboard/sidebar.rs @@ -26,6 +26,7 @@ pub enum Message { Swap(window::Id, pane_grid::Pane, window::Id, pane_grid::Pane), Leave(Buffer), ToggleFileTransfers, + ToggleLogs, ToggleCommandBar, ToggleThemeEditor, ReloadingConfigFile, @@ -44,6 +45,7 @@ pub enum Event { Swap(window::Id, pane_grid::Pane, window::Id, pane_grid::Pane), Leave(Buffer), ToggleFileTransfers, + ToggleLogs, ToggleCommandBar, ToggleThemeEditor, OpenReleaseWebsite, @@ -89,6 +91,7 @@ impl Sidebar { ), Message::Leave(buffer) => (Task::none(), Some(Event::Leave(buffer))), Message::ToggleFileTransfers => (Task::none(), Some(Event::ToggleFileTransfers)), + Message::ToggleLogs => (Task::none(), Some(Event::ToggleLogs)), Message::ToggleCommandBar => (Task::none(), Some(Event::ToggleCommandBar)), Message::ToggleThemeEditor => (Task::none(), Some(Event::ToggleThemeEditor)), Message::ReloadingConfigFile => { @@ -217,6 +220,20 @@ impl Sidebar { )); } + if config.buttons.logs { + let logs_open = panes + .iter(main_window) + .any(|(_, _, pane)| matches!(pane.buffer, crate::buffer::Buffer::Logs(_))); + menu_buttons = menu_buttons.push(new_button( + // TODO: Update + icon::copy(), + theme::text::primary, + Some(Message::ToggleLogs), + logs_open, + "Logs", + )); + } + let width = if config.position.is_horizontal() { Length::Shrink } else { diff --git a/src/widget/message_content.rs b/src/widget/message_content.rs index 33b2b66e..e96e5a40 100644 --- a/src/widget/message_content.rs +++ b/src/widget/message_content.rs @@ -2,6 +2,7 @@ use data::appearance::theme::randomize_color; use data::user::NickColor; use data::{message, Config}; use iced::widget::span; +use iced::widget::text::Span; use iced::{border, Length}; use crate::{font, Theme}; @@ -138,5 +139,25 @@ fn message_content_impl<'a, T: Copy + 'a, M: 'a>( text.into() } + data::message::Content::Log(record) => { + let mut spans: Vec> = vec![]; + + spans.extend( + config + .buffer + .format_timestamp(&record.timestamp) + .map(|ts| span(ts).color(theme.colors().buffer.timestamp)), + ); + + spans.extend([ + span(format!("{: <5}", record.level)).color(theme.colors().text.secondary), + span(" "), + span(&record.message), + ]); + + selectable_rich_text::(spans) + .style(style) + .into() + } } } From 7b6c4d940118b50f54861a4ce434d337601eabd0 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Mon, 30 Sep 2024 09:48:24 -0700 Subject: [PATCH 3/6] Fix bug to handle dashboard events on popout windows --- src/main.rs | 26 ++++++++++++-------------- src/screen/dashboard.rs | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index 484f9a85..b63b0bad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -779,20 +779,18 @@ impl Halloy { }, }, Message::Event(window, event) => { - // Events only enabled for main window - if window == self.main_window.id { - if let Screen::Dashboard(dashboard) = &mut self.screen { - return dashboard - .handle_event( - event, - &self.clients, - &self.version, - &self.config, - &mut self.theme, - &self.main_window, - ) - .map(Message::Dashboard); - } + if let Screen::Dashboard(dashboard) = &mut self.screen { + return dashboard + .handle_event( + window, + event, + &self.clients, + &self.version, + &self.config, + &mut self.theme, + &self.main_window, + ) + .map(Message::Dashboard); } Task::none() diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index 3607c064..3fc37dc6 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -3,8 +3,8 @@ use data::environment::RELEASE_WEBSITE; use data::history::ReadMarker; use std::collections::HashMap; use std::path::PathBuf; -use std::slice; use std::time::{Duration, Instant}; +use std::{convert, slice}; use data::config; use data::file_transfer; @@ -55,7 +55,7 @@ pub enum Message { Shortcut(shortcut::Command), FileTransfer(file_transfer::task::Update), SendFileSelected(Server, Nick, Option), - CloseContextMenu(bool), + CloseContextMenu(window::Id, bool), ThemeEditor(theme_editor::Message), ConfigReloaded(Result), } @@ -792,9 +792,9 @@ impl Dashboard { } } } - Message::CloseContextMenu(any_closed) => { + Message::CloseContextMenu(window, any_closed) => { if !any_closed { - if self.is_pane_maximized() { + if self.is_pane_maximized() && window == main_window.id { self.panes.main.restore(); } else { self.focus = None; @@ -996,6 +996,7 @@ impl Dashboard { pub fn handle_event( &mut self, + window: window::Id, event: event::Event, clients: &data::client::Map, version: &Version, @@ -1009,11 +1010,11 @@ impl Dashboard { Escape => { // Order of operations // - // - Close command bar + // - Close command bar (if main window) // - Close context menu - // - Restore maximized pane + // - Restore maximized pane (if main window) // - Unfocus - if self.command_bar.is_some() { + if self.command_bar.is_some() && window == main_window.id { self.toggle_command_bar( &closed_buffers(self, main_window.id, clients), version, @@ -1022,7 +1023,8 @@ impl Dashboard { main_window, ) } else { - context_menu::close(Message::CloseContextMenu) + context_menu::close(convert::identity) + .map(move |any_closed| Message::CloseContextMenu(window, any_closed)) } } Copy => selectable_text::selected(Message::SelectedText), From 5ccbe82c5c69a6bb0dc1c4491bbeb2b5162f093a Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Mon, 30 Sep 2024 09:57:12 -0700 Subject: [PATCH 4/6] log -> logs --- data/src/history.rs | 2 +- data/src/history/metadata.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/history.rs b/data/src/history.rs index 5ced1c7d..033bac92 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -158,7 +158,7 @@ async fn path(server: &server::Server, kind: &Kind) -> Result { Kind::Server => format!("{server}"), Kind::Channel(channel) => format!("{server}channel{channel}"), Kind::Query(nick) => format!("{server}nickname{}", nick), - Kind::Logs => "log".to_string(), + Kind::Logs => "logs".to_string(), }; let hashed_name = seahash::hash(name.as_bytes()); diff --git a/data/src/history/metadata.rs b/data/src/history/metadata.rs index 65223663..22b471a9 100644 --- a/data/src/history/metadata.rs +++ b/data/src/history/metadata.rs @@ -118,7 +118,7 @@ async fn path(server: &server::Server, kind: &Kind) -> Result { Kind::Server => format!("{server}-metadata"), Kind::Channel(channel) => format!("{server}channel{channel}-metadata"), Kind::Query(nick) => format!("{server}nickname{}-metadata", nick), - Kind::Logs => "log-metadata".to_string(), + Kind::Logs => "logs-metadata".to_string(), }; let hashed_name = seahash::hash(name.as_bytes()); From 8c783c1d624545ace9c275b8b40691ca7224dde8 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 30 Sep 2024 23:03:04 +0200 Subject: [PATCH 5/6] Updated icon --- assets/fontello/config.json | 6 ++++++ fonts/halloy-icons.ttf | Bin 9528 -> 9832 bytes src/icon.rs | 4 ++++ src/screen/dashboard/sidebar.rs | 3 +-- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/assets/fontello/config.json b/assets/fontello/config.json index 83035ce9..26bb70de 100644 --- a/assets/fontello/config.json +++ b/assets/fontello/config.json @@ -113,6 +113,12 @@ "css": "popup", "code": 59406, "src": "typicons" + }, + { + "uid": "8ea66d97faf9816abd34f48f3f26d216", + "css": "bucket", + "code": 59408, + "src": "entypo" } ] } \ No newline at end of file diff --git a/fonts/halloy-icons.ttf b/fonts/halloy-icons.ttf index ac175638da6aa61fbdaaec08e0f788e6e49f4353..300006683def899d5199b7a811c6c7f8aa45b633 100644 GIT binary patch delta 730 zcmXYu?@N zHvsGcfV;_bJiEV<>LgwVU=Nb_b2idxR1t$=Fguh^`2Nfg?-1&T;uBfYjl{1h%0r{m zgHJzQ+6E9_%D)&+#RnQ8KCK)LWQom)D`W>se8{DdnxzLpNvvMr^by`XX zsvUgSUZA^{3@QPBeEesi0Q@8_8JO}maaKRai$}$E{*3Fv0UiiLe_LOJjd4x=8OFF9 z1=Ax65d+2rXW}6UBU6w%hkVs{57AXjX-Cg)bG4)T_$*@)fODdyUei4>adzl@?ALwk=ME7WL3F cZ;}#|$$P1s7|0|iq~yqWax|5nw7xg|1r)8KzW@LL delta 437 zcmaFiv%_nGa{UVi2F3se28M*>+{A(jtIuBm@;5LrFy$qel_&tkfk3PQNbsc50V!Kyd*ezXC{eq~}zoF(jN{%D}*)z`(%Uk&&91qAC8ypMgPe1yJ5B z11P|5%NzvcKLGMoGIC2Q9NEMEUuIwsVgU;HIagSRa}nDSu+xM$FB7(A21+~<1G+-I YgO?>GKe?DKIkPA^CzWCIQt`ul02T^r5&!@I diff --git a/src/icon.rs b/src/icon.rs index a89a89bf..171a6f75 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -72,6 +72,10 @@ pub fn popout<'a>() -> Text<'a> { to_text('\u{E80E}') } +pub fn logs<'a>() -> Text<'a> { + to_text('\u{E810}') +} + fn to_text<'a>(unicode: char) -> Text<'a> { text(unicode.to_string()) .line_height(LineHeight::Relative(1.0)) diff --git a/src/screen/dashboard/sidebar.rs b/src/screen/dashboard/sidebar.rs index c576abd3..89731dd0 100644 --- a/src/screen/dashboard/sidebar.rs +++ b/src/screen/dashboard/sidebar.rs @@ -225,8 +225,7 @@ impl Sidebar { .iter(main_window) .any(|(_, _, pane)| matches!(pane.buffer, crate::buffer::Buffer::Logs(_))); menu_buttons = menu_buttons.push(new_button( - // TODO: Update - icon::copy(), + icon::logs(), theme::text::primary, Some(Message::ToggleLogs), logs_open, From 5b84d1f7c6c81107ed570d8a95b8da6867c8e910 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Mon, 30 Sep 2024 23:10:07 +0200 Subject: [PATCH 6/6] Added command bar entry --- src/screen/dashboard.rs | 3 +++ src/screen/dashboard/command_bar.rs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index 3fc37dc6..2c30636e 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -600,6 +600,9 @@ impl Dashboard { command_bar::Buffer::ToggleFileTransfers => { (self.toggle_file_transfers(config, main_window), None) } + command_bar::Buffer::ToggleLogs => { + (self.toggle_logs(config, main_window), None) + }, }, command_bar::Command::Configuration(command) => match command { command_bar::Configuration::OpenDirectory => { diff --git a/src/screen/dashboard/command_bar.rs b/src/screen/dashboard/command_bar.rs index c341a024..78196b28 100644 --- a/src/screen/dashboard/command_bar.rs +++ b/src/screen/dashboard/command_bar.rs @@ -129,6 +129,7 @@ pub enum Buffer { Popout, Merge, ToggleFileTransfers, + ToggleLogs, } #[derive(Debug, Clone)] @@ -200,7 +201,7 @@ impl Buffer { resize_buffer: data::buffer::Resize, main_window: window::Id, ) -> Vec { - let mut list = vec![Buffer::New, Buffer::ToggleFileTransfers]; + let mut list = vec![Buffer::New, Buffer::ToggleFileTransfers, Buffer::ToggleLogs]; if let Some((window, _)) = focus { list.push(Buffer::Close); @@ -298,6 +299,7 @@ impl std::fmt::Display for Buffer { Buffer::Popout => write!(f, "Pop out buffer"), Buffer::Merge => write!(f, "Merge buffer"), Buffer::ToggleFileTransfers => write!(f, "Toggle File Transfers"), + Buffer::ToggleLogs => write!(f, "Toggle Logs"), } } }