mod channels;
mod dev_log;
mod events;
mod history;
mod keyboard;
mod mouse;
mod render_code_block;
mod render_document;
mod render_frame;
mod render_help_screen;
mod render_loading_bar;
mod render_node;
mod render_scrollbar;
mod render_span;
mod render_status_bar;
mod render_table;
mod render_theme_picker;
mod request_thread;
mod response;
mod span_style;
mod state;
mod theme;
mod utils;
mod write_text;
#[cfg(test)]
mod tests;
use events::handle_action;
use theme::InteractiveTheme;
pub use history::HistoryEntry;
use utils::set_cursor_shape;
use crate::{
commands::Commands,
logging::LogReader,
render_context::RenderContext,
renderer::interactive::state::{InputMode, InteractiveState, UiMode},
request::Request,
styled_string::{Document, DocumentNode, HeadingLevel, Span},
};
use crossbeam_channel::select;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::{
io::{self, stdout},
thread,
};
use channels::{RequestResponse, UiCommand};
use request_thread::request_thread_loop;
fn initial_document() -> Document<'static> {
Document::from(vec![
DocumentNode::Heading {
level: HeadingLevel::Title,
spans: vec![Span::plain("Loading...")],
},
DocumentNode::paragraph(vec![Span::plain(
"Loading documentation sources, please wait...",
)]),
])
}
pub fn render_interactive(
manifest_path: std::path::PathBuf,
local: bool,
render_context: RenderContext,
initial_command: Option<Commands>,
log_reader: LogReader,
) -> io::Result<()> {
use crate::format_context::FormatContext;
let format_context = FormatContext::new();
let request = Request::lazy(manifest_path, format_context, local);
thread::scope(|scope| {
render_interactive_impl(scope, &request, render_context, initial_command, log_reader)
})
}
fn render_interactive_impl<'scope, 'env: 'scope>(
scope: &'scope thread::Scope<'scope, 'env>,
request: &'env Request,
render_context: RenderContext,
initial_command: Option<Commands>,
log_reader: LogReader,
) -> io::Result<()> {
let interactive_theme = InteractiveTheme::from_render_context(&render_context);
let (cmd_tx, cmd_rx) = crossbeam_channel::unbounded::<UiCommand<'env>>();
let (resp_tx, resp_rx) = crossbeam_channel::unbounded::<RequestResponse<'env>>();
let ui_handle = scope.spawn(|| -> io::Result<()> {
ui_thread_loop(
render_context,
interactive_theme,
cmd_tx,
resp_rx,
log_reader,
)
});
request.populate();
let (document, _is_error, initial_entry) = initial_command
.unwrap_or_else(Commands::list)
.execute(request);
let _ = resp_tx.send(RequestResponse::Document {
doc: document,
entry: initial_entry,
});
request_thread_loop(request, cmd_rx, resp_tx);
ui_handle.join().unwrap()?;
Ok(())
}
fn ui_thread_loop<'a>(
render_context: RenderContext,
interactive_theme: InteractiveTheme,
cmd_tx: crossbeam_channel::Sender<UiCommand<'a>>,
resp_rx: crossbeam_channel::Receiver<RequestResponse<'a>>,
log_reader: LogReader,
) -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let mut state = InteractiveState::new(
initial_document(),
None, cmd_tx,
resp_rx,
render_context,
interactive_theme,
log_reader,
);
let (event_tx, event_rx) = crossbeam_channel::unbounded();
let _event_reader = thread::spawn(move || {
while let Ok(evt) = event::read() {
if event_tx.send(evt).is_err() {
break;
}
}
});
let timer_tick = crossbeam_channel::tick(std::time::Duration::from_millis(30));
terminal.draw(|frame| state.render_frame(frame))?;
state.update_cursor(&mut terminal);
let result = loop {
select! {
recv(state.log_reader.notify_receiver()) -> _ => {
if let Some(latest) = state.log_reader.peek_latest() {
if matches!(state.ui_mode, UiMode::Normal) {
state.ui.debug_message = latest.into();
}
}
}
recv(timer_tick) -> _ => {
if !state.loading.pending_request {
continue; }
}
recv(state.resp_rx) -> response => {
match response {
Ok(response) => {
if state.handle_response(response) {
break Ok(());
}
}
Err(_) => {
break Ok(());
}
}
}
recv(event_rx) -> event => {
match event {
Ok(Event::Key(key)) => {
if state.handle_key_event(key, &mut terminal) {
break Ok(());
}
}
Ok(Event::Mouse(mouse_event)) => {
state.handle_mouse_event(mouse_event, &terminal);
}
Ok(_) => {}
Err(_) => {
break Ok(());
}
}
}
}
state.handle_hover();
state.handle_click();
terminal.draw(|frame| state.render_frame(frame))?;
state.update_cursor(&mut terminal);
};
disable_raw_mode()?;
if state.ui.supports_cursor {
set_cursor_shape(terminal.backend_mut(), "default");
}
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
result
}
#[cfg(test)]
pub fn render_to_test_backend(
document: Document<'_>,
render_context: RenderContext,
) -> ratatui::backend::TestBackend {
use ratatui::{Terminal, backend::TestBackend};
let (cmd_tx, _cmd_rx) = crossbeam_channel::unbounded();
let (_resp_tx, resp_rx) = crossbeam_channel::unbounded();
let theme = InteractiveTheme::from_render_context(&render_context);
let (_, log_reader) = crate::logging::StatusLogBackend::new(100);
let mut state = state::InteractiveState::new(
document,
None,
cmd_tx,
resp_rx,
render_context,
theme,
log_reader,
);
let backend = TestBackend::new(80, 200); let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| state.render_frame(frame)).unwrap();
terminal.backend().clone()
}