diff options
| author | Daniel Jankowski <Daniel.Jankowski@rub.de> | 2024-04-16 10:59:15 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-04-16 10:59:15 +0200 |
| commit | 072392300409c11930cb07f56fab03eda2c24d4a (patch) | |
| tree | a009352ec85fa705d239166873d14ef469b3fbea /src | |
| parent | fix: toggle pane frames with floating panes (diff) | |
| download | sl0pstatus-072392300409c11930cb07f56fab03eda2c24d4a.tar.gz sl0pstatus-072392300409c11930cb07f56fab03eda2c24d4a.tar.xz sl0pstatus-072392300409c11930cb07f56fab03eda2c24d4a.zip | |
feat: remote control zjstatus with pipes (#48)
* chore: update to latest version of zellij
* fix: deprecated functions in chrono
* chore: update nix deps
* chore: remove deprecated chrono functions
* feat: add refresh via pipes with line protocol
* fix: handle multiple lines in pipe
* fix(clippy): char instead of str
* feat: add notification widget
* chore: update nix flake
* chore: update cargo deps
* chore: update cargo deps
* chore: update nix flake
* chore: clean up imports for tracing
* chore: update nix flake
* chore: update cargo deps
* chore: update nix flake
* chore: update cargo deps
* chore: remove pinning of chrono
Since chrono-tz now fixed the deprecations from chrono
we can finally remove the pinning.
* chore: update nix flake
* chore: update cargo deps
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/zjstatus.rs | 59 | ||||
| -rw-r--r-- | src/config.rs | 4 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/pipe.rs | 103 | ||||
| -rw-r--r-- | src/widgets/command.rs | 6 | ||||
| -rw-r--r-- | src/widgets/mod.rs | 1 | ||||
| -rw-r--r-- | src/widgets/notification.rs | 78 |
7 files changed, 232 insertions, 20 deletions
diff --git a/src/bin/zjstatus.rs b/src/bin/zjstatus.rs index 1f0de0e..86cbf70 100644 --- a/src/bin/zjstatus.rs +++ b/src/bin/zjstatus.rs @@ -1,25 +1,20 @@ -use config::ModuleConfig; -use widgets::{ - command::{CommandResult, CommandWidget}, - datetime::DateTimeWidget, - mode::ModeWidget, - session::SessionWidget, - swap_layout::SwapLayoutWidget, - tabs::TabsWidget, - widget::Widget, -}; - -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - use zellij_tile::prelude::*; use chrono::Local; -use std::{collections::BTreeMap, fs::File, sync::Arc, usize}; +use std::{collections::BTreeMap, sync::Arc, usize}; use uuid::Uuid; use zjstatus::{ - config::{self, UpdateEventMask, ZellijState}, - frames, widgets, + config::{self, ModuleConfig, UpdateEventMask, ZellijState}, + frames, pipe, + widgets::command::{CommandResult, CommandWidget}, + widgets::datetime::DateTimeWidget, + widgets::mode::ModeWidget, + widgets::notification::NotificationWidget, + widgets::session::SessionWidget, + widgets::swap_layout::SwapLayoutWidget, + widgets::tabs::TabsWidget, + widgets::widget::Widget, }; #[derive(Default)] @@ -36,6 +31,9 @@ register_plugin!(State); #[cfg(feature = "tracing")] fn init_tracing() { + use std::fs::File; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + let file = File::create(".zjstatus.log"); let file = match file { Ok(file) => file, @@ -88,9 +86,34 @@ impl ZellijPlugin for State { sessions: Vec::new(), start_time: Local::now(), cache_mask: 0, + incoming_notification: None, }; } + fn pipe(&mut self, pipe_message: PipeMessage) -> bool { + let mut should_render = false; + + match pipe_message.source { + PipeSource::Cli(_) => { + if let Some(input) = pipe_message.payload { + should_render = pipe::parse_protocol(&mut self.state, &input); + } + } + PipeSource::Plugin(_) => { + if let Some(input) = pipe_message.payload { + should_render = pipe::parse_protocol(&mut self.state, &input); + } + } + PipeSource::Keybind => { + if let Some(input) = pipe_message.payload { + should_render = pipe::parse_protocol(&mut self.state, &input); + } + } + } + + should_render + } + #[tracing::instrument(skip_all, fields(event_type))] fn update(&mut self, event: Event) -> bool { let mut should_render = false; @@ -267,6 +290,10 @@ fn register_widgets(configuration: &BTreeMap<String, String>) -> BTreeMap<String Arc::new(SessionWidget::new(configuration)), ); widget_map.insert("tabs".to_owned(), Arc::new(TabsWidget::new(configuration))); + widget_map.insert( + "notifications".to_owned(), + Arc::new(NotificationWidget::new(configuration)), + ); tracing::debug!("registered widgets: {:?}", widget_map.keys()); diff --git a/src/config.rs b/src/config.rs index ec1efed..2c12fe4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ use zellij_tile::prelude::*; use crate::{ border::{parse_border_config, BorderConfig, BorderPosition}, render::FormattedPart, - widgets::{command::CommandResult, widget::Widget}, + widgets::{command::CommandResult, notification, widget::Widget}, }; use chrono::{DateTime, Local}; @@ -20,6 +20,7 @@ pub struct ZellijState { pub tabs: Vec<TabInfo>, pub sessions: Vec<SessionInfo>, pub start_time: DateTime<Local>, + pub incoming_notification: Option<notification::Message>, pub cache_mask: u8, } @@ -37,6 +38,7 @@ pub fn event_mask_from_widget_name(name: &str) -> u8 { "command" => UpdateEventMask::Always as u8, "datetime" => UpdateEventMask::Always as u8, "mode" => UpdateEventMask::Mode as u8, + "notifications" => UpdateEventMask::Always as u8, "session" => UpdateEventMask::Session as u8, "swap_layout" => UpdateEventMask::Tab as u8, "tabs" => UpdateEventMask::Tab as u8, @@ -1,5 +1,6 @@ pub mod border; pub mod config; pub mod frames; +pub mod pipe; pub mod render; pub mod widgets; diff --git a/src/pipe.rs b/src/pipe.rs new file mode 100644 index 0000000..f88db89 --- /dev/null +++ b/src/pipe.rs @@ -0,0 +1,103 @@ +use std::ops::Sub; + +use chrono::{Duration, Local}; + +use crate::{ + config::ZellijState, + widgets::{command::TIMESTAMP_FORMAT, notification}, +}; + +/// Parses the line protocol and updates the state accordingly +/// +/// The protocol is as follows: +/// +/// zjstatus::command_name::args +/// +/// It first starts with `zjstatus` as a prefix to indicate that the line is +/// used for the line protocol and zjstatus should parse it. It is followed +/// by the command name and then the arguments. The following commands are +/// available: +/// +/// - `rerun` - Reruns the command with the given name (like in the config) as +/// argument. E.g. `zjstatus::rerun::command_1` +/// +/// The function returns a boolean indicating whether the state has been +/// changed and the UI should be re-rendered. +#[tracing::instrument(skip(state))] +pub fn parse_protocol(state: &mut ZellijState, input: &str) -> bool { + tracing::debug!("parsing protocol"); + let lines = input.split('\n').collect::<Vec<&str>>(); + + let mut should_render = false; + for line in lines { + let line_renders = process_line(state, line); + + if line_renders { + should_render = true; + } + } + + should_render +} + +#[tracing::instrument(skip_all)] +fn process_line(state: &mut ZellijState, line: &str) -> bool { + let parts = line.split("::").collect::<Vec<&str>>(); + + if parts.len() < 3 { + return false; + } + + if parts[0] != "zjstatus" { + return false; + } + + tracing::debug!("command: {}", parts[1]); + + let mut should_render = false; + #[allow(clippy::single_match)] + match parts[1] { + "rerun" => { + rerun_command(state, parts[2]); + + should_render = true; + } + "notify" => { + notify(state, parts[2]); + + should_render = true; + } + _ => {} + } + + should_render +} + +fn notify(state: &mut ZellijState, message: &str) { + state.incoming_notification = Some(notification::Message { + body: message.to_string(), + received_at: Local::now(), + }); +} + +fn rerun_command(state: &mut ZellijState, command_name: &str) { + let command_result = state.command_results.get(command_name); + + if command_result.is_none() { + return; + } + + let mut command_result = command_result.unwrap().clone(); + + let ts = Sub::<Duration>::sub(Local::now(), Duration::try_days(1).unwrap()); + + command_result.context.insert( + "timestamp".to_string(), + ts.format(TIMESTAMP_FORMAT).to_string(), + ); + + state.command_results.remove(command_name); + state + .command_results + .insert(command_name.to_string(), command_result.clone()); +} diff --git a/src/widgets/command.rs b/src/widgets/command.rs index 05bc94a..9e095c9 100644 --- a/src/widgets/command.rs +++ b/src/widgets/command.rs @@ -210,13 +210,13 @@ fn get_timestamp_from_event_or_default( return Local::now(); } - return Sub::<Duration>::sub(Local::now(), Duration::days(1)); + return Sub::<Duration>::sub(Local::now(), Duration::try_days(1).unwrap()); } let command_result = command_result.unwrap(); let ts_context = command_result.context.get("timestamp"); if ts_context.is_none() { - return Sub::<Duration>::sub(Local::now(), Duration::days(1)); + return Sub::<Duration>::sub(Local::now(), Duration::try_days(1).unwrap()); } let ts_context = ts_context.unwrap(); @@ -226,7 +226,7 @@ fn get_timestamp_from_event_or_default( match DateTime::parse_from_str(ts_context, TIMESTAMP_FORMAT) { Ok(ts) => ts.into(), - Err(_) => Sub::<Duration>::sub(Local::now(), Duration::days(1)), + Err(_) => Sub::<Duration>::sub(Local::now(), Duration::try_days(1).unwrap()), } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 9fcd130..1b20e48 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,6 +1,7 @@ pub mod command; pub mod datetime; pub mod mode; +pub mod notification; pub mod swap_layout; pub mod session; pub mod tabs; diff --git a/src/widgets/notification.rs b/src/widgets/notification.rs new file mode 100644 index 0000000..5023fa7 --- /dev/null +++ b/src/widgets/notification.rs @@ -0,0 +1,78 @@ +use std::collections::BTreeMap; + +use chrono::{DateTime, Local}; + +use crate::render::FormattedPart; +use crate::{config::ZellijState, widgets::widget::Widget}; + +#[derive(Clone, Debug, Default)] +pub struct Message { + pub body: String, + pub received_at: DateTime<Local>, +} + +pub struct NotificationWidget { + show_interval: i64, + format_unread: Vec<FormattedPart>, + format_no_notifications: Vec<FormattedPart>, +} + +impl NotificationWidget { + pub fn new(config: &BTreeMap<String, String>) -> Self { + let format_unread = match config.get("notification_format_unread") { + Some(f) => FormattedPart::multiple_from_format_string(f), + None => FormattedPart::multiple_from_format_string(""), + }; + + let format_no_notifications = match config.get("notification_format_no_notifications") { + Some(f) => FormattedPart::multiple_from_format_string(f), + None => FormattedPart::multiple_from_format_string(""), + }; + + let show_interval = match config.get("notification_show_interval") { + Some(i) => i.parse::<i64>().unwrap_or(5), + None => 5, + }; + + Self { + show_interval, + format_unread, + format_no_notifications, + } + } +} + +impl Widget for NotificationWidget { + fn process(&self, _name: &str, state: &ZellijState) -> String { + let message = match state.incoming_notification { + Some(ref message) => message.clone(), + None => Message::default(), + }; + + let no_new = + message.received_at.timestamp() + self.show_interval < Local::now().timestamp(); + + tracing::debug!("no_new: {}", no_new); + + let format = match no_new { + true => self.format_no_notifications.clone(), + false => self.format_unread.clone(), + }; + + let mut output = "".to_owned(); + + for f in format.iter() { + let mut content = f.content.clone(); + + if content.contains("{message}") { + content = content.replace("{message}", message.body.as_str()); + } + + output = format!("{}{}", output, f.format_string(&content)); + } + + output.to_owned() + } + + fn process_click(&self, _state: &ZellijState, _pos: usize) {} +} |
