diff --git a/crates/gpui_term/src/view/mod.rs b/crates/gpui_term/src/view/mod.rs index 7a7f758..e459b5a 100644 --- a/crates/gpui_term/src/view/mod.rs +++ b/crates/gpui_term/src/view/mod.rs @@ -64,6 +64,7 @@ pub trait ContextMenuProvider: Send + Sync + 'static { pub struct TerminalStatusIndicator { pub icon_path: SharedString, pub color: Hsla, + pub label: Option, } pub struct ImeState { diff --git a/crates/gpui_term/src/view/record.rs b/crates/gpui_term/src/view/record.rs index ca704dd..abc50ce 100644 --- a/crates/gpui_term/src/view/record.rs +++ b/crates/gpui_term/src/view/record.rs @@ -1,6 +1,9 @@ // Recording-specific UI helpers for `TerminalView`. -use gpui::{AnyElement, InteractiveElement, IntoElement, ParentElement, Styled, div, px}; +use gpui::{ + AnyElement, InteractiveElement, IntoElement, ParentElement, Styled, div, + prelude::FluentBuilder, px, +}; use gpui_component::{Icon, Theme}; use super::TerminalStatusIndicator; @@ -52,6 +55,7 @@ pub(crate) fn render_terminal_status_indicator( .right(px(if recording_active { 72.0 } else { 12.0 })) .flex() .items_center() + .gap(px(4.0)) .p(px(4.0)) .bg(theme.background.opacity(0.65)) .border_1() @@ -62,6 +66,9 @@ pub(crate) fn render_terminal_status_indicator( .path(indicator.icon_path) .text_color(indicator.color), ) + .when_some(indicator.label, |this, label| { + this.child(div().text_xs().text_color(indicator.color).child(label)) + }) .into_any_element() } diff --git a/termua/src/web/mod.rs b/termua/src/web/mod.rs index e751ac8..a645aca 100644 --- a/termua/src/web/mod.rs +++ b/termua/src/web/mod.rs @@ -537,6 +537,14 @@ impl WebShareServer { pub fn is_closed(&self) -> bool { self.closed.load(Ordering::Acquire) } + + pub fn client_count(&self) -> usize { + self.state + .lock() + .expect("web share state poisoned") + .clients + .len() + } } impl Drop for WebShareServer { @@ -943,6 +951,26 @@ mod tests { assert!(!access.can_input(1)); } + #[test] + fn web_share_reports_authenticated_client_count() { + smol::block_on(async { + let server = WebShareServer::bind("secret".into(), screen_with_text("screen")) + .await + .unwrap(); + assert_eq!(server.client_count(), 0); + let mut socket = connect_authenticated(&server).await; + assert_eq!(server.client_count(), 1); + socket.close(None).await.unwrap(); + for _ in 0..20 { + if server.client_count() == 0 { + break; + } + smol::Timer::after(std::time::Duration::from_millis(10)).await; + } + assert_eq!(server.client_count(), 0); + }); + } + #[test] fn approving_later_request_transfers_exclusive_control() { let mut access = ShareAccess::new("secret".into()); diff --git a/termua/src/window/main_window/actions.rs b/termua/src/window/main_window/actions.rs index bfc2dc8..5f92e93 100644 --- a/termua/src/window/main_window/actions.rs +++ b/termua/src/window/main_window/actions.rs @@ -651,6 +651,16 @@ impl TermuaWindow { cx.spawn(async move |this, cx| { loop { smol::Timer::after(std::time::Duration::from_secs(1)).await; + let client_count = expiring_server.client_count(); + let _ = this.update(cx, |this, cx| { + if this + .web_share_indicator + .set_client_count(terminal_id, client_count) + && let Some(terminal_view) = expiring_terminal_view.upgrade() + { + terminal_view.update(cx, |_, cx| cx.notify()); + } + }); if expiring_server.is_closed() || expiring_terminal.upgrade().is_none() || expiring_server.is_inactive_for(web_share_timeout) diff --git a/termua/src/window/main_window/state.rs b/termua/src/window/main_window/state.rs index ba3d2e2..1a1747b 100644 --- a/termua/src/window/main_window/state.rs +++ b/termua/src/window/main_window/state.rs @@ -152,8 +152,14 @@ struct TermuaContextMenuProvider { web_share_indicator: WebShareIndicator, } +#[derive(Clone)] +struct WebShareStatus { + url: String, + client_count: usize, +} + #[derive(Clone, Default)] -pub(crate) struct WebShareIndicator(Arc>>); +pub(crate) struct WebShareIndicator(Arc>>); impl gpui::Global for WebShareIndicator {} @@ -162,7 +168,13 @@ impl WebShareIndicator { self.0 .lock() .expect("web share indicator lock poisoned") - .insert(terminal_id, url); + .insert( + terminal_id, + WebShareStatus { + url, + client_count: 0, + }, + ); } pub(crate) fn deactivate(&self, terminal_id: gpui::EntityId) { @@ -191,7 +203,31 @@ impl WebShareIndicator { .lock() .expect("web share indicator lock poisoned") .get(&terminal_id) - .cloned() + .map(|status| status.url.clone()) + } + + pub(super) fn client_count_for(&self, terminal_id: gpui::EntityId) -> usize { + self.0 + .lock() + .expect("web share indicator lock poisoned") + .get(&terminal_id) + .map_or(0, |status| status.client_count) + } + + pub(super) fn set_client_count( + &self, + terminal_id: gpui::EntityId, + client_count: usize, + ) -> bool { + let mut shares = self.0.lock().expect("web share indicator lock poisoned"); + let Some(status) = shares.get_mut(&terminal_id) else { + return false; + }; + if status.client_count == client_count { + return false; + } + status.client_count = client_count; + true } pub(crate) fn count(&self) -> usize { @@ -212,11 +248,13 @@ pub(super) fn web_share_menu_label_key(active: bool) -> &'static str { pub(super) fn web_share_terminal_status_indicator( active: bool, + client_count: usize, cx: &App, ) -> Option { active.then(|| gpui_term::TerminalStatusIndicator { icon_path: TermuaIcon::Global.path().into(), color: cx.theme().danger, + label: (client_count > 0).then(|| client_count.to_string().into()), }) } @@ -274,6 +312,8 @@ impl gpui_term::ContextMenuProvider for TermuaContextMenuProvider { ) -> Option { web_share_terminal_status_indicator( self.web_share_indicator.is_active_for(terminal.entity_id()), + self.web_share_indicator + .client_count_for(terminal.entity_id()), cx, ) } diff --git a/termua/src/window/main_window/tests.rs b/termua/src/window/main_window/tests.rs index 9600484..4c9d3a7 100644 --- a/termua/src/window/main_window/tests.rs +++ b/termua/src/window/main_window/tests.rs @@ -810,11 +810,19 @@ fn web_share_context_menu_presentation_tracks_sharing_state(cx: &mut gpui::TestA "Terminal.ContextMenu.ShareWebActive" ); assert_eq!(TermuaIcon::Global.path(), "icons/global.svg"); - assert!(super::state::web_share_terminal_status_indicator(false, app).is_none()); - let status = super::state::web_share_terminal_status_indicator(true, app) + assert!(super::state::web_share_terminal_status_indicator(false, 0, app).is_none()); + let status = super::state::web_share_terminal_status_indicator(true, 0, app) .expect("active sharing should expose a terminal status indicator"); assert_eq!(status.icon_path.as_ref(), "icons/global.svg"); assert_eq!(status.color, app.theme().danger); + assert_eq!(status.label, None); + assert_eq!( + super::state::web_share_terminal_status_indicator(true, 2, app) + .expect("connected sharing should expose a terminal status indicator") + .label + .as_deref(), + Some("2") + ); let shared_terminal = app.new(|_| ()); let other_terminal = app.new(|_| ()); let indicator = super::state::WebShareIndicator::default(); @@ -830,6 +838,10 @@ fn web_share_context_menu_presentation_tracks_sharing_state(cx: &mut gpui::TestA indicator.url_for(other_terminal.entity_id()).as_deref(), Some("http://host/two") ); + assert_eq!(indicator.client_count_for(shared_terminal.entity_id()), 0); + assert!(indicator.set_client_count(shared_terminal.entity_id(), 2)); + assert!(!indicator.set_client_count(shared_terminal.entity_id(), 2)); + assert_eq!(indicator.client_count_for(shared_terminal.entity_id()), 2); indicator.deactivate(other_terminal.entity_id()); assert!(indicator.is_active_for(shared_terminal.entity_id())); assert!(!indicator.is_active_for(other_terminal.entity_id()));