pub mod app;
pub mod map;
pub mod model;
pub mod source;
pub mod static_report;
pub mod ui;
use std::io::{self, IsTerminal, Stdout, Write};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use crate::inspect::app::App;
use crate::CliError;
struct TerminalGuard {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl TerminalGuard {
fn enter() -> Result<Self, CliError> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let terminal = Terminal::new(CrosstermBackend::new(stdout))?;
Ok(TerminalGuard { terminal })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
let _ = self.terminal.show_cursor();
}
}
fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.should_quit = true
}
KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => app.next_tab(),
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => app.prev_tab(),
KeyCode::Down | KeyCode::Char('j') => app.scroll_down(),
KeyCode::Up | KeyCode::Char('k') => app.scroll_up(),
KeyCode::Char('g') => app.to_top(),
KeyCode::Char('G') => app.to_bottom(),
_ => {}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InspectMode {
Tui,
Static,
}
pub fn select_mode(force_static: bool, is_tty: bool) -> InspectMode {
if force_static || !is_tty {
InspectMode::Static
} else {
InspectMode::Tui
}
}
pub fn run_inspect(source: &str, force_static: bool) -> Result<(), CliError> {
run_inspect_with_tty(source, force_static, io::stdout().is_terminal())
}
pub fn run_inspect_with_tty(
source: &str,
force_static: bool,
is_tty: bool,
) -> Result<(), CliError> {
let model = source::load_model(source)?;
if select_mode(force_static, is_tty) == InspectMode::Static {
let report = static_report::render(&model);
if let Err(err) = io::stdout().write_all(report.as_bytes()) {
if err.kind() != io::ErrorKind::BrokenPipe {
return Err(err.into());
}
}
return Ok(());
}
let mut app = App::new(model.columns.len());
let mut guard = TerminalGuard::enter()?;
while !app.should_quit {
guard.terminal.draw(|f| ui::draw(f, &model, &app))?;
if event::poll(Duration::from_millis(200))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
handle_key(&mut app, key);
}
}
}
}
Ok(()) }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_tty_falls_back_to_the_static_report() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../conformance/inferable_types.fcb"
);
assert!(run_inspect_with_tty(path, false, false).is_ok());
}
#[test]
fn static_flag_wins_over_a_terminal() {
assert_eq!(select_mode(true, true), InspectMode::Static);
assert_eq!(select_mode(true, false), InspectMode::Static);
}
#[test]
fn a_terminal_without_the_flag_gets_the_tui() {
assert_eq!(select_mode(false, true), InspectMode::Tui);
assert_eq!(select_mode(false, false), InspectMode::Static);
}
#[test]
fn quit_keys_set_should_quit() {
use crate::inspect::app::App;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(3);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
);
assert!(app.should_quit);
let mut app = App::new(3);
handle_key(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(app.should_quit);
}
#[test]
fn ctrl_c_sets_should_quit_but_plain_c_does_not() {
use crate::inspect::app::App;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(3);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
);
assert!(app.should_quit);
let mut app = App::new(3);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
);
assert!(!app.should_quit);
}
#[test]
fn arrow_and_vim_keys_drive_navigation() {
use crate::inspect::app::{App, Tab};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let mut app = App::new(3);
handle_key(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
assert_eq!(app.tab, Tab::Columns);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE),
);
assert_eq!(app.column_offset, 1);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE),
);
assert_eq!(app.column_offset, 0);
handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('G'), KeyModifiers::NONE),
);
assert_eq!(app.column_offset, 2);
}
}