use std::cell::RefCell;
use std::rc::Rc;
use rich::Style;
use rich_intuituive::interact::{Key, KeyCode};
use rich_intuituive::widget::{Canvas, DrawCx, EventCx, Used, Widget, WidgetEvent, widget};
use rich_intuituive::{Ctx, Node, Signal, signal};
use crate::host::{Command, ExitStatus, LocalPty, Notify, PtyHost};
use crate::term::TermCore;
use crate::wake::{Fed, connect};
pub const DEFAULT_SCROLLBACK: usize = 1000;
type ExitHandler = Box<dyn FnMut(ExitStatus, &mut Ctx)>;
struct PaneState {
core: TermCore,
wake: Signal<u64>,
status: Signal<Option<ExitStatus>>,
on_exit: Option<ExitHandler>,
}
impl Fed for PaneState {
fn install(&mut self, notify: Notify) {
self.core.set_notify(notify);
}
fn pump(this: &Rc<RefCell<Self>>, cx: &mut Ctx) {
let (pumped, wake, status) = {
let mut state = this.borrow_mut();
(state.core.pump(), state.wake, state.status)
};
if pumped.output || pumped.exit.is_some() {
wake.update(|n| *n = n.wrapping_add(1));
}
if let Some(exit) = pumped.exit {
status.set(Some(exit.clone()));
let handler = this.borrow_mut().on_exit.take();
if let Some(mut handler) = handler {
handler(exit, cx);
}
}
}
}
pub struct TerminalPane {
state: Rc<RefCell<PaneState>>,
release: Vec<Key>,
}
pub fn terminal(command: impl Into<Command>) -> TerminalPane {
terminal_with(LocalPty::new(command))
}
pub fn terminal_with(host: impl PtyHost + 'static) -> TerminalPane {
let state = Rc::new(RefCell::new(PaneState {
core: TermCore::new(Box::new(host), DEFAULT_SCROLLBACK),
wake: signal(0),
status: signal(None),
on_exit: None,
}));
connect(&state);
TerminalPane {
state,
release: Vec::new(),
}
}
impl TerminalPane {
#[must_use]
pub fn on_exit(self, handler: impl FnMut(ExitStatus, &mut Ctx) + 'static) -> TerminalPane {
self.state.borrow_mut().on_exit = Some(Box::new(handler));
self
}
pub fn status(&self) -> Signal<Option<ExitStatus>> {
self.state.borrow().status
}
#[must_use]
pub fn scrollback(self, rows: usize) -> TerminalPane {
self.state.borrow_mut().core.set_scrollback(rows);
self
}
#[must_use]
pub fn release_keys(mut self, keys: &str) -> TerminalPane {
self.release
.extend(keys.split_whitespace().filter_map(Key::parse));
self
}
pub fn node(self) -> Node {
widget(PaneWidget {
state: self.state,
release: self.release,
caret: None,
})
.on_key("ctrl+c", |cx| cx.quit())
}
}
impl From<TerminalPane> for Node {
fn from(pane: TerminalPane) -> Node {
pane.node()
}
}
struct PaneWidget {
state: Rc<RefCell<PaneState>>,
release: Vec<Key>,
caret: Option<(u16, u16)>,
}
impl Widget for PaneWidget {
fn name(&self) -> &'static str {
"terminal"
}
fn describe(&self) -> Option<String> {
let state = self.state.borrow();
Some(match &state.core.exit {
Some(status) => status.to_string(),
None if state.core.started() => "running".into(),
None => "not started".into(),
})
}
fn draw(&mut self, cx: &mut DrawCx, canvas: &mut Canvas) {
let mut state = self.state.borrow_mut();
state.wake.get();
let (width, height) = (canvas.width(), canvas.height());
state.core.fit(width, height);
if let Some(error) = state.core.error.clone() {
let style = cx.style("error", "red");
canvas.markup(cx.console(), 0, 0, width, &escape(&error), Some(&style));
self.caret = None;
return;
}
let lines = state.core.lines();
canvas.lines(&lines);
let back = state.core.scrolled();
if back > 0 {
let note = format!(" ↑{back} ");
let w = rich::cells::cell_len(¬e) as u16;
let style = Style::parse("reverse").unwrap_or_default();
canvas.print(width.saturating_sub(w), 0, ¬e, Some(&style));
}
self.caret = state.core.cursor();
}
fn event(&mut self, cx: &mut EventCx, event: &WidgetEvent) -> Used {
let mut state = self.state.borrow_mut();
if state.core.exit.is_some() || state.core.error.is_some() || !state.core.started() {
return Used::No;
}
match event {
WidgetEvent::Key(key) => {
if self.release.contains(key) {
return Used::No;
}
let page = cx.size().1.max(1) as isize;
match (key.code, key.modifiers.shift) {
(KeyCode::PageUp, true) => state.core.scroll_by(page),
(KeyCode::PageDown, true) => state.core.scroll_by(-page),
_ => state.core.key(*key),
}
cx.redraw();
Used::Yes
}
WidgetEvent::Paste(text) => {
state.core.paste(text);
cx.redraw();
Used::Yes
}
WidgetEvent::Mouse(mouse) => {
if !state.core.mouse(*mouse) {
return Used::No;
}
if state.core.wants_mouse() {
if mouse.kind
== rich_intuituive::interact::MouseKind::Down(
rich_intuituive::interact::Button::Left,
)
{
cx.capture_mouse();
}
} else {
cx.redraw();
}
Used::Yes
}
_ => Used::No,
}
}
fn focusable(&self) -> bool {
true
}
fn caret(&self) -> Option<(u16, u16)> {
self.caret
}
}
fn escape(text: &str) -> String {
text.replace('[', "\\[")
}