aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDaniel Jankowski <Daniel.Jankowski@rub.de>2024-04-16 10:59:15 +0200
committerGitHub <noreply@github.com>2024-04-16 10:59:15 +0200
commit072392300409c11930cb07f56fab03eda2c24d4a (patch)
treea009352ec85fa705d239166873d14ef469b3fbea /src
parentfix: toggle pane frames with floating panes (diff)
downloadsl0pstatus-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.rs59
-rw-r--r--src/config.rs4
-rw-r--r--src/lib.rs1
-rw-r--r--src/pipe.rs103
-rw-r--r--src/widgets/command.rs6
-rw-r--r--src/widgets/mod.rs1
-rw-r--r--src/widgets/notification.rs78
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,
diff --git a/src/lib.rs b/src/lib.rs
index 3b7bb51..345d4af 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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) {}
+}