use std::cell::RefCell;
use std::fmt;
use std::io;
use std::process::Command;
use std::rc::Rc;
use std::time::Duration;
use rich::{ColorSystem, Console, Segment};
use crate::component::{Component, Context, Flow, View};
use crate::event::{Button, Event, Key, Mouse, MouseKind};
use crate::paint::Painter;
use crate::policy::{Fallback, LineIo, NotInteractive, Policy, StdLineIo};
use crate::session::{Backend, Session, SessionOptions};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Outcome<T> {
Done(T),
Cancelled,
Interrupted,
}
impl<T> Outcome<T> {
pub fn value(self) -> Option<T> {
match self {
Outcome::Done(value) => Some(value),
_ => None,
}
}
}
struct Shared<T> {
outcome: Option<Outcome<T>>,
finished: bool,
}
impl<T> Shared<T> {
fn finish(&mut self, outcome: Outcome<T>) {
if !self.finished {
self.outcome = Some(outcome);
self.finished = true;
}
}
}
pub struct Handle<T>(Rc<RefCell<Shared<T>>>);
impl<T> Handle<T> {
pub fn take(&self) -> Option<Outcome<T>> {
self.0.borrow_mut().outcome.take()
}
pub fn is_finished(&self) -> bool {
self.0.borrow().finished
}
}
#[derive(Clone, Debug)]
pub struct LoopOptions {
pub height: Option<usize>,
pub transient: bool,
pub color_system: Option<Option<ColorSystem>>,
pub no_color: bool,
}
impl Default for LoopOptions {
fn default() -> Self {
LoopOptions {
height: None,
transient: false,
color_system: None,
no_color: std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()),
}
}
}
enum Step {
Continue,
Finished,
Handoff(Command),
}
trait Mounted {
fn start(&mut self, context: &Context<'_>) -> Option<Step>;
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Step;
fn render(&self, context: &Context<'_>) -> View;
fn tick(&self) -> Option<Duration>;
fn finished(&self) -> bool;
fn interrupt(&mut self);
}
struct Slot<C: Component> {
component: C,
result: Rc<RefCell<Shared<C::Output>>>,
started: bool,
}
impl<C: Component> Slot<C> {
fn step(&mut self, flow: Flow<C::Output>) -> Step {
let outcome = match flow {
Flow::Continue | Flow::Ignored => return Step::Continue,
Flow::Handoff(command) => return Step::Handoff(command),
Flow::Done(value) => Outcome::Done(value),
Flow::Cancel => Outcome::Cancelled,
};
self.result.borrow_mut().finish(outcome);
Step::Finished
}
}
impl<C: Component> Mounted for Slot<C> {
fn start(&mut self, context: &Context<'_>) -> Option<Step> {
if std::mem::replace(&mut self.started, true) {
return None;
}
let flow = self.component.start(context);
Some(self.step(flow))
}
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Step {
let flow = self.component.handle(event, context);
if let (Flow::Ignored, Some(key)) = (&flow, event.key()) {
let keymap = self.component.keymap();
let forward = keymap.is(key, crate::compose::FOCUS_NEXT);
if forward || keymap.is(key, crate::compose::FOCUS_PREVIOUS) {
self.component.focus_enter(forward);
}
}
self.step(flow)
}
fn render(&self, context: &Context<'_>) -> View {
self.component.render(context)
}
fn tick(&self) -> Option<Duration> {
self.component.tick()
}
fn finished(&self) -> bool {
self.result.borrow().finished
}
fn interrupt(&mut self) {
self.result.borrow_mut().finish(Outcome::Interrupted);
}
}
struct Timer<'a> {
interval: Duration,
next: Duration,
callback: Box<dyn FnMut() -> bool + 'a>,
}
pub struct EventLoop<'a> {
backend: Box<dyn Backend + 'a>,
options: LoopOptions,
console: Console,
painter: Painter,
mounted: Vec<(Box<dyn Mounted + 'a>, Option<Duration>)>,
timers: Vec<Timer<'a>>,
size: (u16, u16),
rows: Vec<usize>,
shown: Vec<Vec<Segment>>,
grab: Option<usize>,
primed: bool,
}
impl<'a> EventLoop<'a> {
pub fn new(backend: impl Backend + 'a, options: LoopOptions) -> EventLoop<'a> {
let size = backend.size();
let system = options.color_system.unwrap_or(Some(ColorSystem::Truecolor));
let console = Self::console(size, system, options.no_color);
EventLoop {
painter: Painter::new(system, options.no_color),
backend: Box::new(backend),
options,
console,
mounted: Vec::new(),
timers: Vec::new(),
size,
rows: Vec::new(),
shown: Vec::new(),
grab: None,
primed: false,
}
}
pub fn terminal(session: SessionOptions, options: LoopOptions) -> io::Result<EventLoop<'a>> {
let session = Session::start(session)?;
let system = options
.color_system
.or_else(|| session.color_system())
.unwrap_or_else(|| {
Console::builder()
.force_terminal(true)
.build()
.color_system()
});
Ok(EventLoop::new(
session,
LoopOptions {
color_system: Some(system),
..options
},
))
}
fn console(size: (u16, u16), system: Option<ColorSystem>, no_color: bool) -> Console {
Console::builder()
.width(size.0.max(1) as usize)
.height(size.1.max(1) as usize)
.force_terminal(true)
.color_system(system)
.no_color(no_color)
.build()
}
pub fn mount<C: Component + 'a>(&mut self, component: C) -> Handle<C::Output> {
let result = Rc::new(RefCell::new(Shared {
outcome: None,
finished: false,
}));
let tick = component.tick();
let next = tick.map(|interval| self.backend.elapsed() + interval);
self.mounted.push((
Box::new(Slot {
component,
result: Rc::clone(&result),
started: false,
}),
next,
));
Handle(result)
}
pub fn graphics(&mut self, source: std::sync::Arc<dyn rich_ext::frame::PlacementSource>) {
self.painter.set_graphics(Some(source));
}
pub fn every(&mut self, interval: Duration, callback: impl FnMut() -> bool + 'a) {
self.timers.push(Timer {
interval,
next: self.backend.elapsed() + interval,
callback: Box::new(callback),
});
}
fn space(&self) -> (usize, usize) {
let rows = self.size.1.max(1) as usize;
(
self.size.0.max(1) as usize,
self.options.height.map_or(rows, |height| height.min(rows)),
)
}
fn context(&self) -> Context<'_> {
let (width, height) = self.space();
Context {
console: &self.console,
width,
height,
}
}
fn paint(&mut self) -> io::Result<()> {
let context = self.context();
let max_rows = context.height;
let mut view = View::default();
let mut rows = Vec::with_capacity(self.mounted.len());
for (component, _) in &self.mounted {
rows.push(view.lines.len());
view.push(component.render(&context));
}
if let Some((row, column)) = view.cursor
&& let Some(line) = view.lines.get(row)
{
let shown = crate::paint::sanitized_column(line, column);
let last = context.width.saturating_sub(1).max(column);
view.cursor = Some((row, shown.min(last)));
}
for line in &mut view.lines {
crate::paint::sanitize_line(line);
if line.iter().map(Segment::cell_length).sum::<usize>() > context.width {
*line = Segment::adjust_line_length(line, context.width, None);
}
}
let out = self.painter.paint(&view, max_rows);
view.lines.truncate(max_rows);
if !out.is_empty() {
let plain = view
.lines
.iter()
.map(|line| line.iter().map(|s| s.text.as_str()).collect::<String>())
.collect::<Vec<_>>()
.join("\n");
self.backend.painted(&plain);
self.backend.write_frame(&out)?;
}
self.rows = rows;
self.shown = view.lines;
Ok(())
}
fn locate(&self, index: usize, mouse: Mouse) -> Option<Event> {
let region_row = self.region_row(mouse);
let (start, end) = self.extent(index);
let inside = region_row.filter(|row| (start..end).contains(row));
if let (MouseKind::Down(_), None) = (mouse.kind, inside) {
return None;
}
if let (MouseKind::Down(Button::Left), Some(row)) = (mouse.kind, inside)
&& let Some(url) = link_at(&self.shown[row], mouse.column as usize)
{
return Some(Event::Link(url));
}
let row = region_row.map_or(0, |row| row.saturating_sub(start));
Some(Event::Mouse(Mouse {
row: u16::try_from(row).unwrap_or(u16::MAX),
..mouse
}))
}
fn region_row(&self, mouse: Mouse) -> Option<usize> {
let top = if self.backend.alternate_screen() {
0
} else {
let rows = self.space().1;
(self.backend.origin() as usize).min(rows.saturating_sub(self.painter.extent()))
};
(mouse.row as usize).checked_sub(top)
}
fn extent(&self, index: usize) -> (usize, usize) {
let start = self.rows.get(index).copied().unwrap_or(0);
let end = self
.rows
.get(index + 1)
.copied()
.unwrap_or(self.shown.len());
(start, end)
}
fn mouse_target(&mut self, mouse: Mouse) -> Option<usize> {
let running = |index: &usize| !self.mounted[*index].0.finished();
let grabbed = self.grab.filter(running);
let under = self.region_row(mouse).and_then(|row| {
(0..self.mounted.len()).filter(running).find(|&index| {
let (start, end) = self.extent(index);
(start..end).contains(&row)
})
});
let first = (0..self.mounted.len()).find(running);
match mouse.kind {
MouseKind::Down(_) => {
self.grab = under;
under
}
MouseKind::Drag(_) => grabbed.or(under).or(first),
MouseKind::Up(_) => {
self.grab = None;
grabbed.or(under).or(first)
}
_ => under.or(first),
}
}
fn running(&self) -> bool {
self.mounted
.iter()
.any(|(component, _)| !component.finished())
}
fn deadline(&self) -> Option<Duration> {
let ticks = self
.mounted
.iter()
.filter(|(component, _)| !component.finished())
.filter_map(|(_, next)| *next);
let timers = self.timers.iter().map(|timer| timer.next);
let animation = self
.painter
.graphics()
.and_then(|source| source.next_change())
.map(|wait| self.backend.elapsed() + wait);
ticks.chain(timers).chain(animation).min()
}
fn fire_due(&mut self) -> io::Result<()> {
let now = self.backend.elapsed();
let mut timers = std::mem::take(&mut self.timers);
timers.retain_mut(|timer| {
if timer.next > now {
return true;
}
timer.next = now + timer.interval;
(timer.callback)()
});
timers.append(&mut self.timers);
self.timers = timers;
for index in 0..self.mounted.len() {
let due = self.mounted[index].1.is_some_and(|next| next <= now);
if due && !self.mounted[index].0.finished() {
let interval = self.mounted[index].0.tick();
self.mounted[index].1 = interval.map(|interval| now + interval);
self.deliver(index, &Event::Tick)?;
}
}
Ok(())
}
fn start(&mut self) -> io::Result<()> {
for index in 0..self.mounted.len() {
if self.mounted[index].0.finished() {
continue;
}
let (width, height) = self.space();
let context = Context {
console: &self.console,
width,
height,
};
if let Some(step) = self.mounted[index].0.start(&context) {
self.apply(index, step)?;
}
}
Ok(())
}
fn deliver(&mut self, index: usize, event: &Event) -> io::Result<()> {
let (width, height) = self.space();
let context = Context {
console: &self.console,
width,
height,
};
crate::clipboard::open(self.backend.clipboard().err());
let step = self.mounted[index].0.handle(event, &context);
for text in crate::clipboard::close() {
self.backend.copy(&text)?;
}
self.apply(index, step)
}
fn apply(&mut self, index: usize, step: Step) -> io::Result<()> {
if let Step::Handoff(mut command) = step {
let finish = self.painter.finish(false);
self.backend.write_frame(&finish)?;
let code = self.backend.handoff(&mut command)?;
self.painter.reset();
self.deliver(index, &Event::Returned(code))?;
}
Ok(())
}
fn resized(&mut self, columns: u16, rows: u16) -> io::Result<()> {
self.size = (columns, rows);
self.console = Self::console(
self.size,
self.console.color_system(),
self.options.no_color,
);
self.painter.invalidate();
let event = Event::Resize { columns, rows };
for index in 0..self.mounted.len() {
if !self.mounted[index].0.finished() {
self.deliver(index, &event)?;
}
}
Ok(())
}
pub fn run(&mut self) -> io::Result<()> {
let result = self.run_inner();
let written = self.finish();
result.and(written)
}
fn run_inner(&mut self) -> io::Result<()> {
self.prime()?;
while self.running() {
let event = self.backend.read(self.timeout())?;
self.turn(event)?;
}
Ok(())
}
fn prime(&mut self) -> io::Result<()> {
if !std::mem::replace(&mut self.primed, true) {
self.start()?;
self.paint()?;
}
Ok(())
}
fn turn(&mut self, event: Option<Event>) -> io::Result<()> {
match event {
None => {}
Some(Event::Key(key)) if key.is_interrupt() => {
for (component, _) in &mut self.mounted {
component.interrupt();
}
}
Some(Event::Key(key)) if key == Key::ctrl('z') && self.backend.can_suspend() => {
let finish = self.painter.finish(false);
self.backend.write_frame(&finish)?;
self.backend.suspend()?;
self.painter.reset();
let (columns, rows) = self.backend.size();
self.resized(columns, rows)?;
}
Some(Event::Resize { columns, rows }) => {
if self.backend.take_resumed() {
self.painter.reset();
}
self.resized(columns, rows)?;
}
Some(Event::Mouse(mouse)) => {
if let Some(index) = self.mouse_target(mouse)
&& let Some(event) = self.locate(index, mouse)
{
self.deliver(index, &event)?;
}
}
Some(event) => {
if let Some(index) = self
.mounted
.iter()
.position(|(component, _)| !component.finished())
{
self.deliver(index, &event)?;
}
}
}
self.fire_due()?;
self.start()?;
self.paint()
}
pub fn step(&mut self, event: Event) -> io::Result<bool> {
self.prime()?;
if self.running() {
self.turn(Some(event))?;
}
Ok(self.running())
}
pub fn pump(&mut self) -> io::Result<bool> {
if self.primed {
if self.running() {
self.turn(None)?;
}
} else {
self.prime()?;
}
Ok(self.running())
}
pub fn timeout(&self) -> Option<Duration> {
self.deadline()
.map(|deadline| deadline.saturating_sub(self.backend.elapsed()))
}
pub fn is_running(&self) -> bool {
self.running()
}
pub fn finish(&mut self) -> io::Result<()> {
let finish = self.painter.finish(self.options.transient);
self.backend.write_frame(&finish)
}
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
NotInteractive(NotInteractive),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(error) => write!(f, "{error}"),
Error::NotInteractive(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for Error {}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Error {
Error::Io(error)
}
}
#[derive(Clone, Debug, Default)]
pub struct RunOptions {
pub policy: Policy,
pub session: SessionOptions,
pub paint: LoopOptions,
}
pub fn run<C: Component>(component: C, options: &RunOptions) -> Result<Outcome<C::Output>, Error> {
run_drawing(component, options, None)
}
pub fn run_with_graphics<C: Component>(
component: C,
options: &RunOptions,
graphics: std::sync::Arc<dyn rich_ext::frame::PlacementSource>,
) -> Result<Outcome<C::Output>, Error> {
run_drawing(component, options, Some(graphics))
}
fn run_drawing<C: Component>(
component: C,
options: &RunOptions,
graphics: Option<std::sync::Arc<dyn rich_ext::frame::PlacementSource>>,
) -> Result<Outcome<C::Output>, Error> {
let mut component = component;
if let Err(reason) = options.policy.detect_for(options.session.output) {
return degrade(
&mut component,
options.policy.fallback,
reason,
&mut StdLineIo,
);
}
let mut session = options.session;
if component.mouse() {
session.mouse = true;
if session.output == crate::session::Output::Stderr {
session.alternate_screen = true;
}
}
let mut event_loop = EventLoop::terminal(session, options.paint.clone())?;
if let Some(graphics) = graphics {
event_loop.graphics(graphics);
}
let handle = event_loop.mount(component);
event_loop.run()?;
drop(event_loop);
Ok(handle.take().unwrap_or(Outcome::Interrupted))
}
pub fn degrade<C: Component>(
component: &mut C,
fallback: Fallback,
reason: crate::policy::Reason,
io: &mut dyn LineIo,
) -> Result<Outcome<C::Output>, Error> {
let default = |component: &C| {
component
.default_value()
.map(Outcome::Done)
.ok_or(Error::NotInteractive(NotInteractive::NoDefault(reason)))
};
match fallback {
Fallback::Error => Err(Error::NotInteractive(NotInteractive::Terminal(reason))),
Fallback::Default => default(component),
Fallback::Prompt => match component.prompt(&mut crate::policy::Shown(io)) {
Ok(Some(value)) => Ok(Outcome::Done(value)),
Ok(None) => Ok(Outcome::Cancelled),
Err(NotInteractive::NoPrompt) => default(component),
Err(NotInteractive::Ended) => {
Err(Error::NotInteractive(NotInteractive::NoDefault(reason)))
}
Err(NotInteractive::Interrupted) => Ok(Outcome::Interrupted),
Err(error) => Err(Error::NotInteractive(error)),
},
}
}
fn link_at(line: &[Segment], column: usize) -> Option<String> {
let mut at = 0;
for segment in line.iter().filter(|segment| !segment.control) {
let width = segment.cell_length();
if column < at + width {
return segment
.style
.as_ref()
.and_then(|style| style.link())
.filter(|url| !url.is_empty())
.map(str::to_string);
}
at += width;
}
None
}