use std::io::{self, Write};
use std::sync::mpsc::Receiver;
use std::sync::Arc;
use cliban_tenancy::Tenant;
use cliban_tui::app::App;
use cliban_tui::data::Data;
use cliban_tui::remote::{ChannelSession, RemoteBackend, RemoteInput};
use cliban_tui::{picker, runtime};
use ratatui::Terminal;
use russh::server::Handle;
use russh::ChannelId;
use tokio::sync::broadcast;
use crate::server::AppState;
pub const NO_TTY: &str = "cliband: the board requires a TTY; use ssh -t\r\n";
struct ChannelWriter {
rt: tokio::runtime::Handle,
handle: Handle,
channel: ChannelId,
buf: Vec<u8>,
}
impl Write for ChannelWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
self.buf.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
if self.buf.is_empty() {
return Ok(());
}
let data = std::mem::take(&mut self.buf);
self.rt
.block_on(self.handle.data(self.channel, data))
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "ssh channel closed"))
}
}
pub struct BoardTask {
pub rt: tokio::runtime::Handle,
pub state: Arc<AppState>,
pub handle: Handle,
pub channel: ChannelId,
pub size: (u16, u16),
pub tenants: Vec<Tenant>,
pub input: Receiver<RemoteInput>,
}
pub fn run_board(task: BoardTask) {
let rt = task.rt.clone();
let handle = task.handle.clone();
let channel = task.channel;
let exit = match board_session(task) {
Ok(()) => 0,
Err(e) => {
let disconnect = e.downcast_ref::<io::Error>().is_some_and(|io| {
matches!(
io.kind(),
io::ErrorKind::UnexpectedEof | io::ErrorKind::BrokenPipe
)
});
if !disconnect {
eprintln!("cliband: board session ended: {e}");
}
1
}
};
rt.block_on(async {
let _ = handle.exit_status_request(channel, exit).await;
let _ = handle.eof(channel).await;
let _ = handle.close(channel).await;
});
}
fn board_session(task: BoardTask) -> Result<(), Box<dyn std::error::Error>> {
let writer = ChannelWriter {
rt: task.rt,
handle: task.handle,
channel: task.channel,
buf: Vec::new(),
};
let (backend, size) = RemoteBackend::new(writer, task.size.0, task.size.1)?;
let mut terminal = Terminal::new(backend)?;
let mut session = ChannelSession::new(task.input, size);
let res = (|| -> Result<(), Box<dyn std::error::Error>> {
let picked = if task.tenants.len() == 1 {
Some(0)
} else {
let slugs: Vec<String> = task.tenants.iter().map(|t| t.slug.clone()).collect();
picker::pick(&mut terminal, &mut session, " pick a board ", &slugs)?
};
let Some(i) = picked else { return Ok(()) };
let tenant_handle = task.state.manager.handle(&task.tenants[i].id)?;
let mut feed = tenant_handle.changes.subscribe();
session.set_dirty_check(move || {
let mut dirty = false;
loop {
match feed.try_recv() {
Ok(()) => dirty = true,
Err(broadcast::error::TryRecvError::Lagged(_)) => dirty = true,
Err(_) => break, }
}
dirty
});
let publish = tenant_handle.changes.clone();
let mut data = Data::from_store(tenant_handle.store.clone())?;
data.set_on_mutate(move || {
let _ = publish.send(());
});
let mut app = App::new();
runtime::reload(&data, &mut app)?;
runtime::event_loop(&mut terminal, &mut session, &data, &mut app)
})();
let _ = terminal.backend_mut().leave_screen();
res
}