use std::io::Write;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
use std::time::Duration;
use crossterm::event::{KeyEvent, KeyEventKind};
use super::cli::{Say, Session};
use super::editor::Editor;
use super::lifecycle::{Event, Face, Handoff, INTERRUPT_GRACE, Sinks};
use super::program_status::{self, Held, Reporting};
use super::screen::{Screen, Written};
use super::terminal::{Inside, Terminal};
use wire::CloseMode;
pub(crate) const UI_DEPTH: usize = 256;
const FACE_LOOK: Duration = Duration::from_millis(100);
pub(super) const CTRL_C_HINT: &str = " 复制请先选中文字;关闭城市用 /quit";
pub(crate) enum Show {
Line(String),
Notice(String),
Key(KeyEvent),
Record(wire::Committed),
Resized,
Lost,
}
fn sayer(to: &SyncSender<Show>, wrap: fn(String) -> Show) -> Say {
let to = to.clone();
Arc::new(move |line: String| drop(to.try_send(wrap(line))))
}
pub(crate) fn start(
terminal: Terminal,
inside: Inside,
watching: tokio::sync::broadcast::Receiver<wire::Committed>,
face: tokio::sync::watch::Receiver<Face>,
) -> (std::thread::JoinHandle<()>, Say) {
let (to, from) = std::sync::mpsc::sync_channel(UI_DEPTH);
let notice = sayer(&to, Show::Notice);
read_keys(to.clone());
read_records(to.clone(), watching);
let say = sayer(&to, Show::Line);
let writer = std::thread::spawn(move || {
let mut ui = Ui::new(terminal, inside, say);
ui.run(&from, face);
});
(writer, notice)
}
fn read_keys(to: SyncSender<Show>) {
std::thread::spawn(move || {
loop {
let show = match crossterm::event::read() {
Ok(crossterm::event::Event::Key(key)) if key.kind != KeyEventKind::Release => {
Show::Key(key)
}
Ok(crossterm::event::Event::Resize(..)) => Show::Resized,
Ok(_) => continue,
Err(_) => Show::Lost,
};
let lost = matches!(show, Show::Lost);
if to.send(show).is_err() || lost {
return;
}
}
});
}
fn read_records(
to: SyncSender<Show>,
mut watching: tokio::sync::broadcast::Receiver<wire::Committed>,
) {
use tokio::sync::broadcast::error::RecvError;
std::thread::spawn(move || {
loop {
let show = match watching.blocking_recv() {
Ok(committed) => Show::Record(committed),
Err(RecvError::Lagged(missed)) => Show::Line(format!(
" {missed} records were not shown here; the WebUI has them"
)),
Err(RecvError::Closed) => return,
};
drop(to.try_send(show));
}
});
}
struct Ui {
terminal: Terminal,
inside: Inside,
say: Say,
session: Option<Session>,
editor: Editor,
shown: Face,
screen: Screen,
asking: bool,
hinted: bool,
menu: bool,
transient: Option<String>,
pairing: tokio::sync::watch::Receiver<String>,
reporting: Reporting,
held: Held,
}
impl Ui {
fn new(terminal: Terminal, inside: Inside, say: Say) -> Ui {
let session = match Session::begin() {
Ok(session) => Some(session),
Err(err) => {
say(format!(" {err}"));
None
}
};
let pairing = inside.door.pairing_code();
Ui {
terminal,
inside,
say,
session,
editor: Editor::default(),
shown: Face::Opening,
screen: Screen::default(),
asking: false,
hinted: false,
menu: false,
transient: None,
pairing,
reporting: Reporting::from_environment(),
held: Held::OPENING,
}
}
fn run(&mut self, from: &Receiver<Show>, mut face: tokio::sync::watch::Receiver<Face>) {
let taken = self.screen.take();
self.drawn(taken);
self.header();
loop {
let now = *face.borrow_and_update();
if now != self.shown {
self.turn_to(now);
}
if matches!(self.pairing.has_changed(), Ok(true)) && self.shown == Face::QuietHost {
self.quiet();
}
if let Face::Gone(_) = self.shown {
return;
}
match from.recv_timeout(FACE_LOOK) {
Ok(show) => self.show(show),
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => return,
}
}
}
fn header(&mut self) {
let room = self
.session
.as_ref()
.map_or(kernel::consts_policy::HALL_MAYOR, |s| s.room.as_str())
.to_owned();
let line = format!(
"sprawling {} {room} {} /web opens the page",
self.terminal.city, self.terminal.bind
);
self.line(&line);
}
fn turn_to(&mut self, next: Face) {
self.shown = next;
match next {
Face::QuietHost => {
self.quiet();
self.recount();
}
Face::Cli => {
self.transient = None;
let left = self.screen.leave_quiet();
self.drawn(left);
self.draw_input();
self.recount();
}
Face::Stopping {
sinks: Sinks::Cut, ..
} => self.screen.cut(),
Face::Stopping {
mode,
sinks: Sinks::Held,
} => {
let progress = match mode {
CloseMode::Drain => {
" sprawling is closing; the runs under way finish first (/quit again stops them now)"
.to_owned()
}
CloseMode::Interrupt => format!(
" sprawling is closing; the runs under way stop at their next safe point, within {} s",
INTERRUPT_GRACE.as_secs()
),
};
self.notice(progress);
}
Face::Gone(handoff) => {
self.status(program_status::Event::Closed);
let given = self.screen.give_back();
self.drawn(given);
if handoff == Handoff::Skipped {
self.line(
" the city exits without a handoff; `sprawling resume` will close what was open",
);
}
}
Face::Opening | Face::Headless => {}
}
}
fn show(&mut self, show: Show) {
match show {
Show::Line(line) => self.line(&line),
Show::Notice(line) => self.notice(line),
Show::Key(key) => self.key(key),
Show::Record(committed) => {
self.heard(committed.record());
let lines = match self.session.as_mut() {
Some(session) => {
let room = session.room.clone();
session.seen.read(committed.record(), &room)
}
None => Vec::new(),
};
for line in lines {
self.line(&line);
}
}
Show::Resized => {
if self.shown == Face::QuietHost {
self.quiet();
}
}
Show::Lost => {
self.screen.cut();
self.inside.tell(Event::TerminalLost);
}
}
}
fn notice(&mut self, line: String) {
if self.screen.is_quiet() {
self.transient = Some(line);
self.quiet();
} else {
self.line(&line);
}
}
fn line(&mut self, line: &str) {
if self.screen.is_quiet() {
return;
}
let editor = match self.shown {
Face::QuietHost => None,
Face::Cli | Face::Stopping { .. } => Some(&self.editor),
Face::Opening | Face::Headless | Face::Gone(_) => None,
};
let written = self.screen.above(line, editor);
self.drawn(written);
}
fn quiet(&mut self) {
let mut lines = vec![self.terminal.url.clone()];
let code = self.pairing.borrow_and_update().clone();
lines.push(match &self.terminal.token {
Some(key) => format!("pairing code {code} key {key}"),
None => format!("pairing code {code}"),
});
if let Some(transient) = &self.transient {
lines.push(transient.trim().to_owned());
}
let written = self.screen.quiet(&lines);
self.drawn(written);
}
fn erase_input(&mut self) {
let written = self.screen.erase_input();
self.drawn(written);
}
fn draw_input(&mut self) {
let written = self.screen.draw_input(&self.editor);
self.drawn(written);
}
fn drawn(&self, written: Written) {
match written {
Written::Shown => {}
Written::Lost => self.inside.tell(Event::TerminalLost),
}
}
}
pub(crate) fn lines_to_stdout() -> Say {
let (to, from) = std::sync::mpsc::sync_channel::<String>(UI_DEPTH);
std::thread::spawn(move || {
let mut out = std::io::stdout();
for line in from {
if writeln!(out, "{line}").and_then(|()| out.flush()).is_err() {
return;
}
}
});
Arc::new(move |line: String| drop(to.try_send(line)))
}
mod keys;
mod reporting;