Make the log buffer show logs

This commit is contained in:
2023-11-01 18:56:54 +01:00
parent b81906c318
commit 1c13d2c784
9 changed files with 169 additions and 39 deletions

View File

@ -49,6 +49,7 @@ signal-hook = "0.3.17"
matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk.git", rev = "91e7f2f7224b8ada17ab639d60da10dad98aeaf9", features = ["eyre", "markdown"] }
# UI
ansi-to-tui = "3.1.0"
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
strip-ansi-escapes = "0.2.0"

View File

@ -15,7 +15,9 @@
*/
use std::cell::RefCell;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::RwLock;
use color_eyre::eyre::{Result, WrapErr};
use crossterm::event::KeyEvent;
@ -51,7 +53,11 @@ pub struct App {
}
impl App {
pub async fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
pub async fn new(
tick_rate: f64,
frame_rate: f64,
mem_log: &'static RwLock<VecDeque<String>>,
) -> Result<Self> {
let home = Home::new();
let fps = FpsCounter::default();
let config = Config::new()?;
@ -116,7 +122,7 @@ impl App {
components: vec![Box::new(home), Box::new(fps)],
should_quit: AtomicBool::new(false),
should_suspend: false,
buffers: Buffers::new(Box::new(LogBuffer::new())),
buffers: Buffers::new(Box::new(LogBuffer::new(mem_log))),
clients,
config,
commands: RataCommands::new(),

View File

@ -14,13 +14,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::RwLock;
use ratatui::text::Text;
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use super::Buffer;
pub struct LogBuffer {}
pub struct LogBuffer {
lines: &'static RwLock<VecDeque<String>>,
}
impl LogBuffer {
pub fn new() -> Self {
LogBuffer {}
pub fn new(lines: &'static RwLock<VecDeque<String>>) -> Self {
LogBuffer { lines }
}
}
@ -28,4 +38,21 @@ impl Buffer for LogBuffer {
fn short_name(&self) -> String {
"ratatrix".to_owned()
}
fn content(&self) -> Text {
let lines = self
.lines
.read()
.expect("LogBuffer could not get log's RwLock as it is poisoned");
let (slice1, slice2) = lines.as_slices();
let text = if slice1.is_empty() {
slice2.join("\n")
} else if slice2.is_empty() {
slice1.join("\n")
} else {
format!("{}\n{}", slice1.join("\n"), slice2.join("\n"))
};
use ansi_to_tui::IntoText;
text.clone().into_text().unwrap_or_else(|_| text.into())
}
}

View File

@ -24,6 +24,7 @@ pub use room::RoomBuffer;
pub trait Buffer {
/// A short human-readable name for the room, eg. to show in compact buflist
fn short_name(&self) -> String;
fn content(&self) -> ratatui::text::Text;
}
pub struct Buffers {

View File

@ -35,4 +35,8 @@ impl Buffer for RoomBuffer {
fn short_name(&self) -> String {
self.room_id.as_str().to_owned()
}
fn content(&self) -> ratatui::text::Text {
format!("Timeline of {} goes here", self.room_id).into()
}
}

View File

@ -67,11 +67,7 @@ impl Component for Home {
.draw(frame, layout[0], buffers)
.context("Error drawing buflist")?;
frame.render_widget(
Paragraph::new(format!(
"content of {} goes here",
buffers.active_buffer().short_name()
))
.block(Block::new().borders(Borders::ALL)),
Paragraph::new(buffers.active_buffer().content()).block(Block::new().borders(Borders::ALL)),
layout[1],
);
Ok(())

118
src/log.rs Normal file
View File

@ -0,0 +1,118 @@
/*
* Copyright (C) 2023 Valentin Lorentz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::VecDeque;
use std::sync::RwLock;
use color_eyre::eyre::Result;
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use crate::utils::{get_data_dir, LOG_ENV, LOG_FILE};
/// Maximum number of log lines to be stored in memory
const MAX_MEM_LOG_LINES: usize = 100;
pub struct LogWriter {
lines: &'static RwLock<VecDeque<String>>,
last_line: Vec<u8>,
}
impl LogWriter {
fn new(lines: &'static RwLock<VecDeque<String>>) -> Self {
LogWriter {
lines,
last_line: Vec::new(),
}
}
}
impl std::io::Write for LogWriter {
fn write(&mut self, bytes: &[u8]) -> Result<usize, std::io::Error> {
self.last_line.extend(bytes);
if bytes.contains(&b'\n') {
// Need to do this because tracing_subscriber never ever call flush() itself?
self.flush()?;
}
Ok(bytes.len())
}
fn flush(&mut self) -> Result<(), std::io::Error> {
let mut new_lines: Vec<&[u8]> = self.last_line.split(|&c| c == b'\n').collect();
if new_lines.len() == 1 {
// No new line, nothing to do
} else {
let last_line = new_lines.pop().expect("Split returned empty vec").to_vec();
let mut lines = self.lines.write().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"LogWriter could not get log's RwLock as it is poisoned: {}",
e
),
)
})?;
for new_line in new_lines.into_iter() {
if lines.len() >= MAX_MEM_LOG_LINES {
lines.pop_front();
}
lines.push_back(String::from_utf8_lossy(new_line).to_string());
}
self.last_line.clear();
self.last_line.extend(&last_line[..]);
}
Ok(())
}
}
pub fn initialize_logging() -> Result<&'static RwLock<VecDeque<String>>> {
let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
std::env::set_var(
"RUST_LOG",
std::env::var("RUST_LOG")
.or_else(|_| std::env::var(LOG_ENV.clone()))
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
);
let file_subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_target(false)
.with_ansi(false)
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
// We keep the log until the application stops, so we might as well return it as &'static
let lines = Box::leak(Box::new(RwLock::new(VecDeque::with_capacity(
MAX_MEM_LOG_LINES,
))));
let mem_subscriber = tracing_subscriber::fmt::layer()
.with_ansi(true)
.with_file(true)
.with_line_number(true)
.with_target(false)
.with_writer(|| LogWriter::new(lines))
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(mem_subscriber)
.with(file_subscriber)
.with(ErrorLayer::default())
.init();
Ok(lines)
}

View File

@ -9,6 +9,7 @@ pub mod cli;
pub mod commands;
pub mod components;
pub mod config;
pub mod log;
pub mod mode;
pub mod plugins;
pub mod tui;
@ -20,16 +21,17 @@ use color_eyre::eyre::Result;
use crate::{
app::App,
utils::{initialize_logging, initialize_panic_handler, version},
log::initialize_logging,
utils::{initialize_panic_handler, version},
};
async fn tokio_main() -> Result<()> {
initialize_logging()?;
let mem_log = initialize_logging()?;
initialize_panic_handler()?;
let args = Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate).await?;
let mut app = App::new(args.tick_rate, args.frame_rate, mem_log).await?;
app.run().await?;
Ok(())

View File

@ -27,7 +27,7 @@ lazy_static! {
}
fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME"))
ProjectDirs::from("re.trix.rata", "Ratatrix", env!("CARGO_PKG_NAME"))
}
pub fn initialize_panic_handler() -> Result<()> {
@ -103,31 +103,6 @@ pub fn get_config_dir() -> PathBuf {
directory
}
pub fn initialize_logging() -> Result<()> {
let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
std::env::set_var(
"RUST_LOG",
std::env::var("RUST_LOG")
.or_else(|_| std::env::var(LOG_ENV.clone()))
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
);
let file_subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_target(false)
.with_ansi(false)
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(file_subscriber)
.with(ErrorLayer::default())
.init();
Ok(())
}
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
/// than printing to stdout.
///