use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use trace_stream::render::RenderOptions;
use turbo_vision::core::event::Event;
use turbo_vision::core::geometry::Rect;
use turbo_vision::terminal::Terminal;
use turbo_vision::views::view::View;
use crate::pipeline::Pipeline;
use crate::registry::SessionId;
use crate::streamview::StreamView;
pub type SharedView = Rc<RefCell<StreamView>>;
#[derive(Debug)]
pub struct SharedStreamView(pub SharedView);
impl View for SharedStreamView {
fn bounds(&self) -> Rect {
self.0.borrow().bounds()
}
fn set_bounds(&mut self, bounds: Rect) {
self.0.borrow_mut().set_bounds(bounds);
}
fn draw(&mut self, terminal: &mut Terminal) {
self.0.borrow_mut().draw(terminal);
}
fn handle_event(&mut self, event: &mut Event) {
self.0.borrow_mut().handle_event(event);
}
fn can_focus(&self) -> bool {
true
}
fn get_palette(&self) -> Option<turbo_vision::core::palette::Palette> {
None
}
}
#[derive(Debug)]
pub struct SessionState {
pub name: String,
pub port: u16,
pub view: SharedView,
pub pipeline: Pipeline,
pub connected: bool,
}
impl SessionState {
#[must_use]
pub fn window_title(&self) -> String {
let base = format_title(&self.name, self.port);
if self.connected {
base
} else {
format!("{base} [disconnected]")
}
}
}
#[must_use]
pub fn format_title(name: &str, port: u16) -> String {
if port == 0 {
name.to_string()
} else {
format!("{name} :{port}")
}
}
#[derive(Debug, Default)]
pub struct Sessions {
inner: HashMap<SessionId, SessionState>,
}
impl Sessions {
pub fn insert(
&mut self,
id: SessionId,
name: String,
port: u16,
view: SharedView,
opts: RenderOptions,
) {
self.inner.insert(
id,
SessionState {
name,
port,
view,
pipeline: Pipeline::new(opts),
connected: false,
},
);
}
pub fn get_mut(&mut self, id: SessionId) -> Option<&mut SessionState> {
self.inner.get_mut(&id)
}
pub fn remove(&mut self, id: SessionId) -> Option<SessionState> {
self.inner.remove(&id)
}
pub fn feed(&mut self, id: SessionId, data: &[u8]) {
if let Some(s) = self.inner.get_mut(&id) {
let mut view = s.view.borrow_mut();
s.pipeline.feed(data, &mut view);
}
}
pub fn mark_reconnected(&mut self, id: SessionId) {
if let Some(s) = self.inner.get_mut(&id) {
s.connected = true;
let mut view = s.view.borrow_mut();
s.pipeline.feed(b"\n-- reconnected --\n", &mut view);
}
}
pub fn mark_attached(&mut self, id: SessionId, reattached: bool) {
if reattached {
self.mark_reconnected(id);
} else if let Some(s) = self.inner.get_mut(&id) {
s.connected = true;
}
}
pub fn mark_disconnected(&mut self, id: SessionId) {
if let Some(s) = self.inner.get_mut(&id) {
s.connected = false;
let mut view = s.view.borrow_mut();
s.pipeline.finish(&mut view);
}
}
pub fn set_options(&mut self, opts: RenderOptions) {
for s in self.inner.values_mut() {
let mut view = s.view.borrow_mut();
s.pipeline.set_options(opts, &mut view);
}
}
pub fn clear(&mut self, id: SessionId) {
if let Some(s) = self.inner.get_mut(&id) {
s.view.borrow_mut().clear();
}
}
#[must_use]
pub fn plain_text(&self, id: SessionId) -> Option<String> {
self.inner.get(&id).map(|s| s.view.borrow().plain_text())
}
#[must_use]
pub fn window_title(&self, id: SessionId) -> Option<String> {
self.inner.get(&id).map(SessionState::window_title)
}
}
#[cfg(test)]
mod tests {
use super::*;
use trace_stream::render::RenderOptions;
fn opts() -> RenderOptions {
RenderOptions {
use_color: true,
format_thinking: true,
format_markdown: true,
}
}
fn view() -> SharedView {
Rc::new(RefCell::new(StreamView::new(Rect::new(0, 0, 80, 24))))
}
#[test]
fn feed_reaches_the_session_view() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
sessions.feed(1, b"hello\n");
assert!(sessions.plain_text(1).unwrap().contains("hello"));
}
#[test]
fn window_title_reflects_connection_state() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
assert_eq!(
sessions.window_title(1).unwrap(),
"demo :4242 [disconnected]"
);
sessions.mark_reconnected(1);
assert_eq!(sessions.window_title(1).unwrap(), "demo :4242");
sessions.mark_disconnected(1);
assert_eq!(
sessions.window_title(1).unwrap(),
"demo :4242 [disconnected]"
);
}
#[test]
fn window_title_omits_a_zero_port() {
let mut sessions = Sessions::default();
sessions.insert(1, "anon-1".into(), 0, view(), opts());
assert_eq!(sessions.window_title(1).unwrap(), "anon-1 [disconnected]");
sessions.mark_reconnected(1);
assert_eq!(sessions.window_title(1).unwrap(), "anon-1");
}
#[test]
fn mark_attached_first_attach_connects_without_a_rule() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
sessions.mark_attached(1, false);
assert_eq!(sessions.window_title(1).unwrap(), "demo :4242");
assert!(!sessions.plain_text(1).unwrap().contains("reconnected"));
}
#[test]
fn mark_attached_reattach_connects_and_draws_a_rule() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
sessions.mark_attached(1, true);
assert_eq!(sessions.window_title(1).unwrap(), "demo :4242");
assert!(sessions.plain_text(1).unwrap().contains("reconnected"));
}
#[test]
fn mark_reconnected_draws_a_horizontal_rule() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
sessions.mark_reconnected(1);
assert!(sessions.plain_text(1).unwrap().contains("reconnected"));
}
#[test]
fn clear_empties_the_scrollback() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
sessions.feed(1, b"hello\n");
sessions.clear(1);
assert_eq!(sessions.plain_text(1).unwrap(), "");
}
#[test]
fn set_options_applies_to_every_session() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
let mut new_opts = opts();
new_opts.format_markdown = false;
sessions.set_options(new_opts);
assert!(
!sessions
.get_mut(1)
.unwrap()
.pipeline
.options()
.format_markdown
);
}
#[test]
fn remove_drops_the_session() {
let mut sessions = Sessions::default();
sessions.insert(1, "demo".into(), 4242, view(), opts());
assert!(sessions.remove(1).is_some());
assert!(sessions.plain_text(1).is_none());
}
#[test]
fn unknown_id_returns_none_everywhere() {
let sessions = Sessions::default();
assert!(sessions.plain_text(99).is_none());
assert!(sessions.window_title(99).is_none());
}
}