feat(gui): list connected viewers and let the host kick them
Track viewers by endpoint id instead of a bare count. The JSON event stream gains viewer_joined / viewer_left (each carrying the id), replacing viewer_count; active/max still ride along so the count display is unchanged. The host screen now renders one row per connected viewer with a Kick button. Clicking it sends `kick <id>` to the headless child over a new stdin command channel, which the host turns into a per-viewer CancellationToken cancel; the existing teardown path then emits the leave, so a kick and a self-disconnect look identical downstream. The stdin channel only runs under --output json (the GUI shell-out) and on a detached OS thread, so a read parked on stdin can't hold up the host's Ctrl+C shutdown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+32
-1
@@ -168,6 +168,9 @@ struct HostState {
|
||||
copied: bool,
|
||||
last_refusal: Option<String>,
|
||||
error: Option<String>,
|
||||
/// Endpoint ids of the currently-connected viewers, in arrival order.
|
||||
/// Drives the per-viewer list and its Kick buttons.
|
||||
viewers: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for HostState {
|
||||
@@ -186,6 +189,7 @@ impl Default for HostState {
|
||||
copied: false,
|
||||
last_refusal: None,
|
||||
error: None,
|
||||
viewers: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,6 +366,23 @@ impl PixelPassApp {
|
||||
ui.add_space(6.0);
|
||||
ui.label(format!("Viewers: {} / {}", self.host.active, self.host.max));
|
||||
|
||||
// Per-viewer list with a Kick button each. Collect the click first so
|
||||
// we're not borrowing self.host.viewers while we reach for the child.
|
||||
let mut kick: Option<String> = None;
|
||||
for id in &self.host.viewers {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("• endpoint {}", short_id(id)));
|
||||
if ui.small_button("Kick").clicked() {
|
||||
kick = Some(id.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Some(id) = kick
|
||||
&& let Some(p) = &mut self.host.proc
|
||||
{
|
||||
p.send_command(&format!("kick {id}"));
|
||||
}
|
||||
|
||||
if let Some(info) = &self.host.info {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{} · {}", info.display, info.capture))
|
||||
@@ -452,6 +473,7 @@ impl PixelPassApp {
|
||||
self.host.max = 0;
|
||||
self.host.capturing = false;
|
||||
self.host.copied = false;
|
||||
self.host.viewers.clear();
|
||||
|
||||
let mut args = vec![
|
||||
"--host".to_string(),
|
||||
@@ -483,6 +505,7 @@ impl PixelPassApp {
|
||||
self.host.capturing = false;
|
||||
self.host.ticket = None;
|
||||
self.host.copied = false;
|
||||
self.host.viewers.clear();
|
||||
}
|
||||
|
||||
/// Drain the host child's event channel into state, and detect an
|
||||
@@ -543,9 +566,17 @@ impl PixelPassApp {
|
||||
cap_source: max_viewers_source,
|
||||
});
|
||||
}
|
||||
ChildEvent::ViewerCount { active, max } => {
|
||||
ChildEvent::ViewerJoined { id, active, max } => {
|
||||
self.host.active = active;
|
||||
self.host.max = max;
|
||||
if !self.host.viewers.contains(&id) {
|
||||
self.host.viewers.push(id);
|
||||
}
|
||||
}
|
||||
ChildEvent::ViewerLeft { id, active, max } => {
|
||||
self.host.active = active;
|
||||
self.host.max = max;
|
||||
self.host.viewers.retain(|v| v != &id);
|
||||
}
|
||||
ChildEvent::Capture { state } => {
|
||||
self.host.capturing = matches!(state, child::CaptureState::Started);
|
||||
|
||||
Reference in New Issue
Block a user