aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/notifications.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-02 02:45:20 +0300
committerGitHub <noreply@github.com>2026-09-02 02:45:20 +0300
commitcc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (patch)
tree4d37c760099070f1a1488b61c7b7271875aafef8 /src/client/shell/notifications.rs
parent18e69891dca486d669a584facd80644bb51f54a2 (diff)
feat: add stable client endpoint compatibility (#3509)
* feat: add stable client endpoint compatibility * fix: keep portable-pty patch checks composable * ci: run maintenance checks in pull requests * fix: reject endpoint errors from terminal clients * feat: keep compatible servers running across updates
Diffstat (limited to 'src/client/shell/notifications.rs')
-rw-r--r--src/client/shell/notifications.rs216
1 files changed, 143 insertions, 73 deletions
diff --git a/src/client/shell/notifications.rs b/src/client/shell/notifications.rs
index 0310e57d..43fe566a 100644
--- a/src/client/shell/notifications.rs
+++ b/src/client/shell/notifications.rs
@@ -1,13 +1,16 @@
use super::*;
use ratatui::{
+ style::Color,
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Widget},
};
-pub(super) fn render_mobile_notification_banner(
+fn render_mobile_notice_banner(
buffer: &mut Buffer,
area: Rect,
- notification: &ClientVisibleNotification,
+ title: &str,
+ body: Option<&str>,
+ dot_color: Color,
offset_for_warning: bool,
palette: &Palette,
) -> Rect {
@@ -23,6 +26,49 @@ pub(super) fn render_mobile_notification_banner(
let background = palette.surface0;
Clear.render(rect, buffer);
buffer.set_style(rect, Style::default().bg(background));
+ let mut x = rect.x;
+ for (text, style) in [
+ (" ", Style::default().bg(background)),
+ ("●", Style::default().fg(dot_color).bg(background)),
+ (" ", Style::default().bg(background)),
+ (
+ title,
+ Style::default()
+ .fg(palette.text)
+ .bg(background)
+ .add_modifier(Modifier::BOLD),
+ ),
+ ] {
+ x = super::render::put_segment(buffer, x, rect.y, rect.right(), text, style);
+ }
+ if let Some(body) = body.filter(|body| !body.is_empty()) {
+ x = super::render::put_segment(
+ buffer,
+ x,
+ rect.y,
+ rect.right(),
+ " · ",
+ Style::default().fg(palette.overlay0).bg(background),
+ );
+ super::render::put_text(
+ buffer,
+ x,
+ rect.y,
+ rect.right().saturating_sub(x),
+ body,
+ Style::default().fg(palette.overlay0).bg(background),
+ );
+ }
+ rect
+}
+
+pub(super) fn render_mobile_notification_banner(
+ buffer: &mut Buffer,
+ area: Rect,
+ notification: &ClientVisibleNotification,
+ offset_for_warning: bool,
+ palette: &Palette,
+) -> Rect {
let event = &notification.event;
let title = match event.kind {
SemanticNotificationKind::NeedsAttention => event
@@ -45,84 +91,55 @@ pub(super) fn render_mobile_notification_banner(
palette.accent
}
};
- let mut x = rect.x;
- x = super::render::put_segment(
- buffer,
- x,
- rect.y,
- rect.right(),
- " ",
- Style::default().bg(background),
- );
- x = super::render::put_segment(
- buffer,
- x,
- rect.y,
- rect.right(),
- "●",
- Style::default().fg(dot_color).bg(background),
- );
- x = super::render::put_segment(
+ render_mobile_notice_banner(
buffer,
- x,
- rect.y,
- rect.right(),
- " ",
- Style::default().bg(background),
- );
- x = super::render::put_segment(
- buffer,
- x,
- rect.y,
- rect.right(),
+ area,
&title,
- Style::default()
- .fg(palette.text)
- .bg(background)
- .add_modifier(Modifier::BOLD),
- );
- if let Some(body) = event.body.as_deref().filter(|body| !body.is_empty()) {
- x = super::render::put_segment(
- buffer,
- x,
- rect.y,
- rect.right(),
- " · ",
- Style::default().fg(palette.overlay0).bg(background),
- );
- super::render::put_text(
- buffer,
- x,
- rect.y,
- rect.right().saturating_sub(x),
- body,
- Style::default().fg(palette.overlay0).bg(background),
- );
- }
- rect
+ event.body.as_deref(),
+ dot_color,
+ offset_for_warning,
+ palette,
+ )
}
-pub(super) fn render_visible_notification(
+pub(super) fn render_mobile_endpoint_notice_banner(
buffer: &mut Buffer,
area: Rect,
- notification: &ClientVisibleNotification,
- default_position: crate::config::ToastHerdrPosition,
+ notice: &ClientVisibleEndpointNotice,
+ offset_for_warning: bool,
+ palette: &Palette,
+) -> Rect {
+ render_mobile_notice_banner(
+ buffer,
+ area,
+ &notice.title,
+ Some(&notice.body),
+ palette.red,
+ offset_for_warning,
+ palette,
+ )
+}
+
+fn render_notification_card(
+ buffer: &mut Buffer,
+ area: Rect,
+ title: &str,
+ body: &str,
+ position: crate::config::ToastHerdrPosition,
top_offset: u16,
+ dot_color: Color,
palette: &Palette,
) -> Rect {
if area.is_empty() {
return Rect::default();
}
- let event = &notification.event;
- let body = event.body.as_deref().unwrap_or_default();
- let content_width = unicode_width::UnicodeWidthStr::width(event.title.as_str())
+ let content_width = unicode_width::UnicodeWidthStr::width(title)
.max(unicode_width::UnicodeWidthStr::width(body))
.saturating_add(6);
let width = u16::try_from(content_width)
.unwrap_or(u16::MAX)
.min(area.width);
let height: u16 = if body.is_empty() { 3 } else { 4 }.min(area.height);
- let position = event.position.unwrap_or(default_position);
let x = match position {
crate::config::ToastHerdrPosition::TopLeft
| crate::config::ToastHerdrPosition::BottomLeft => area.x,
@@ -148,19 +165,10 @@ pub(super) fn render_visible_notification(
let inner = block.inner(rect);
block.render(rect, buffer);
Paragraph::new(Line::from(vec![
- Span::styled(
- "●",
- Style::default().fg(match event.kind {
- SemanticNotificationKind::NeedsAttention => palette.red,
- SemanticNotificationKind::Finished => palette.blue,
- SemanticNotificationKind::UpdateInstalled | SemanticNotificationKind::Custom => {
- palette.accent
- }
- }),
- ),
+ Span::styled("●", Style::default().fg(dot_color)),
Span::raw(" "),
Span::styled(
- event.title.as_str(),
+ title,
Style::default()
.fg(palette.text)
.add_modifier(Modifier::BOLD),
@@ -185,6 +193,60 @@ pub(super) fn render_visible_notification(
rect
}
+pub(super) fn render_visible_notification(
+ buffer: &mut Buffer,
+ area: Rect,
+ notification: &ClientVisibleNotification,
+ default_position: crate::config::ToastHerdrPosition,
+ top_offset: u16,
+ palette: &Palette,
+) -> Rect {
+ let event = &notification.event;
+ let dot_color = match event.kind {
+ SemanticNotificationKind::NeedsAttention => palette.red,
+ SemanticNotificationKind::Finished => palette.blue,
+ SemanticNotificationKind::UpdateInstalled | SemanticNotificationKind::Custom => {
+ palette.accent
+ }
+ };
+ render_notification_card(
+ buffer,
+ area,
+ &event.title,
+ event.body.as_deref().unwrap_or_default(),
+ event.position.unwrap_or(default_position),
+ top_offset,
+ dot_color,
+ palette,
+ )
+}
+
+pub(super) fn render_endpoint_notice(
+ buffer: &mut Buffer,
+ area: Rect,
+ notice: &ClientVisibleEndpointNotice,
+ top_offset: u16,
+ palette: &Palette,
+) -> Rect {
+ render_notification_card(
+ buffer,
+ area,
+ &notice.title,
+ &notice.body,
+ crate::config::ToastHerdrPosition::TopRight,
+ top_offset,
+ match notice.key.kind {
+ ClientEndpointNoticeKind::Unsupported | ClientEndpointNoticeKind::Rejected => {
+ palette.red
+ }
+ ClientEndpointNoticeKind::Timeout | ClientEndpointNoticeKind::Unavailable => {
+ palette.yellow
+ }
+ },
+ palette,
+ )
+}
+
impl ClientShellState {
pub(super) fn focus_visible_notification(&mut self, outcome: &mut ClientShellInput) {
let Some(notification) = self.visible_notification.take() else {
@@ -246,6 +308,14 @@ impl ClientShellState {
self.visible_notification = None;
repaint = true;
}
+ if self
+ .visible_endpoint_notice
+ .as_ref()
+ .is_some_and(|visible| now >= visible.deadline)
+ {
+ self.visible_endpoint_notice = None;
+ repaint = true;
+ }
let pending = std::mem::take(&mut self.pending_notifications);
let mut effects = Vec::new();