sandbox-quant 1.0.7

Exchange-truth trading core for Binance Spot and Futures
Documentation
use std::io::{self, Write};

use crossterm::cursor::{
    position, MoveToColumn, MoveToNextLine, MoveUp, RestorePosition, SavePosition,
};
use crossterm::event::{read, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::execute;
use crossterm::style::{Print, PrintStyledContent, Stylize};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, size, Clear, ClearType, ScrollUp};

use crate::terminal::app::{TerminalApp, TerminalEvent, TerminalMode};
use crate::terminal::completion::{
    next_completion_index, previous_completion_index, scroll_lines_needed, ShellCompletion,
};

pub fn run_terminal<A: TerminalApp>(app: &mut A) -> Result<(), Box<dyn std::error::Error>> {
    match app.terminal_mode() {
        TerminalMode::Raw => {
            let intro_panel = app.intro_panel();
            execute!(
                io::stdout(),
                PrintStyledContent(intro_panel.cyan().bold()),
                Print("\n"),
                PrintStyledContent(app.help_heading().dark_grey()),
                Print("\n"),
                Print(app.help_text()),
                Print("\n")
            )?;
            enable_raw_mode()?;
            let result = loop_terminal_raw(app);
            disable_raw_mode()?;
            result
        }
        TerminalMode::Line => {
            println!("{}", app.intro_panel());
            println!("{}", app.help_heading());
            println!("{}", app.help_text());
            loop_terminal_line(app)
        }
    }
}

fn loop_terminal_raw<A: TerminalApp>(app: &mut A) -> Result<(), Box<dyn std::error::Error>> {
    let mut stdout = io::stdout();
    let mut buffer = String::new();
    let mut completion_index = 0usize;
    let mut rendered_menu_lines = 0usize;
    let mut completion_query: Option<String> = None;
    render_shell(
        &mut stdout,
        app,
        &buffer,
        completion_index,
        &mut rendered_menu_lines,
    )?;

    loop {
        if let Event::Key(key) = read()? {
            if key.kind != KeyEventKind::Press {
                continue;
            }
            match key.code {
                KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                    println!();
                    break;
                }
                KeyCode::Char(ch) => {
                    buffer.push(ch);
                    completion_index = 0;
                    completion_query = Some(buffer.clone());
                    render_shell(
                        &mut stdout,
                        app,
                        &buffer,
                        completion_index,
                        &mut rendered_menu_lines,
                    )?;
                }
                KeyCode::Backspace => {
                    buffer.pop();
                    completion_index = 0;
                    completion_query = Some(buffer.clone());
                    render_shell(
                        &mut stdout,
                        app,
                        &buffer,
                        completion_index,
                        &mut rendered_menu_lines,
                    )?;
                }
                KeyCode::Tab => {
                    let query = completion_query.clone().unwrap_or_else(|| buffer.clone());
                    let completions = app.complete(&query);
                    if completions.len() == 1 {
                        buffer = completions[0].value.clone();
                        completion_index = 0;
                        completion_query = Some(query);
                        render_shell(
                            &mut stdout,
                            app,
                            &buffer,
                            completion_index,
                            &mut rendered_menu_lines,
                        )?;
                    } else if !completions.is_empty() {
                        completion_index = (completion_index + 1) % completions.len();
                        buffer = completions[completion_index].value.clone();
                        completion_query = Some(query);
                        render_shell(
                            &mut stdout,
                            app,
                            &buffer,
                            completion_index,
                            &mut rendered_menu_lines,
                        )?;
                    }
                }
                KeyCode::Up => {
                    let query = completion_query.clone().unwrap_or_else(|| buffer.clone());
                    let completions = app.complete(&query);
                    if !completions.is_empty() {
                        completion_index =
                            previous_completion_index(completions.len(), completion_index);
                        buffer = completions[completion_index].value.clone();
                        completion_query = Some(query);
                        render_shell(
                            &mut stdout,
                            app,
                            &buffer,
                            completion_index,
                            &mut rendered_menu_lines,
                        )?;
                    }
                }
                KeyCode::Down => {
                    let query = completion_query.clone().unwrap_or_else(|| buffer.clone());
                    let completions = app.complete(&query);
                    if !completions.is_empty() {
                        completion_index =
                            next_completion_index(completions.len(), completion_index);
                        buffer = completions[completion_index].value.clone();
                        completion_query = Some(query);
                        render_shell(
                            &mut stdout,
                            app,
                            &buffer,
                            completion_index,
                            &mut rendered_menu_lines,
                        )?;
                    }
                }
                KeyCode::Enter => {
                    clear_completion_menu(&mut stdout, rendered_menu_lines)?;
                    rendered_menu_lines = 0;
                    println!();
                    let line = buffer.clone();
                    buffer.clear();
                    completion_index = 0;
                    completion_query = None;
                    match app.execute_line(&line) {
                        Ok(TerminalEvent::NoOutput) => {}
                        Ok(TerminalEvent::Output(output)) => {
                            print_multiline_block(&mut stdout, &output, true)?
                        }
                        Ok(TerminalEvent::Exit) => break,
                        Err(error) => print_error(&mut stdout, error)?,
                    }
                    render_shell(
                        &mut stdout,
                        app,
                        &buffer,
                        completion_index,
                        &mut rendered_menu_lines,
                    )?;
                }
                _ => {}
            }
        }
    }

    Ok(())
}

fn loop_terminal_line<A: TerminalApp>(app: &mut A) -> Result<(), Box<dyn std::error::Error>> {
    let stdin = io::stdin();
    let mut stdout = io::stdout();

    loop {
        print!("{}", app.prompt());
        stdout.flush()?;

        let mut line = String::new();
        if stdin.read_line(&mut line)? == 0 {
            break;
        }
        match app.execute_line(&line) {
            Ok(TerminalEvent::NoOutput) => {}
            Ok(TerminalEvent::Output(output)) => {
                writeln!(stdout, "{output}")?;
            }
            Ok(TerminalEvent::Exit) => break,
            Err(error) => writeln!(stdout, "error: {error}")?,
        }
    }

    Ok(())
}

fn render_prompt<A: TerminalApp>(stdout: &mut io::Stdout, app: &A, buffer: &str) -> io::Result<()> {
    execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
    execute!(stdout, Print(app.prompt()), Print(buffer))?;
    stdout.flush()
}

fn render_shell<A: TerminalApp>(
    stdout: &mut io::Stdout,
    app: &A,
    buffer: &str,
    completion_index: usize,
    rendered_menu_lines: &mut usize,
) -> io::Result<()> {
    let completions = app.complete(buffer);
    let expected_menu_lines = if should_show_completion_menu(buffer, &completions) {
        completions.len() + 1
    } else {
        0
    };
    let scrolled = ensure_vertical_space(stdout, expected_menu_lines)?;
    if scrolled > 0 {
        execute!(stdout, MoveUp(scrolled as u16))?;
    }
    render_prompt(stdout, app, buffer)?;
    execute!(stdout, SavePosition)?;
    let menu_lines = if should_show_completion_menu(buffer, &completions) {
        print_completion_menu(stdout, &completions, completion_index)?
    } else {
        0
    };

    let lines_to_clear = (*rendered_menu_lines).saturating_sub(menu_lines);
    for _ in 0..lines_to_clear {
        execute!(
            stdout,
            MoveToNextLine(1),
            MoveToColumn(0),
            Clear(ClearType::CurrentLine)
        )?;
    }

    *rendered_menu_lines = menu_lines;
    execute!(stdout, RestorePosition)?;
    stdout.flush()
}

fn clear_completion_menu(stdout: &mut io::Stdout, rendered_menu_lines: usize) -> io::Result<()> {
    execute!(stdout, SavePosition)?;
    for _ in 0..rendered_menu_lines {
        execute!(
            stdout,
            MoveToNextLine(1),
            MoveToColumn(0),
            Clear(ClearType::CurrentLine)
        )?;
    }
    execute!(stdout, RestorePosition)?;
    stdout.flush()
}

fn print_completion_menu(
    stdout: &mut io::Stdout,
    completions: &[ShellCompletion],
    selected: usize,
) -> io::Result<usize> {
    execute!(
        stdout,
        MoveToNextLine(1),
        MoveToColumn(0),
        Clear(ClearType::CurrentLine),
        PrintStyledContent("completions".dark_grey()),
    )?;

    for (index, item) in completions.iter().enumerate() {
        execute!(
            stdout,
            MoveToNextLine(1),
            MoveToColumn(0),
            Clear(ClearType::CurrentLine)
        )?;
        if index == selected {
            execute!(
                stdout,
                PrintStyledContent(">".cyan().bold()),
                Print(" "),
                PrintStyledContent(item.value.as_str().black().on_white()),
                Print("  "),
                PrintStyledContent(item.description.as_str().dark_grey()),
            )?;
        } else {
            execute!(
                stdout,
                Print("  "),
                PrintStyledContent(item.value.as_str().dark_grey()),
                Print("  "),
                PrintStyledContent(item.description.as_str().dark_grey()),
            )?;
        }
    }
    Ok(completions.len() + 1)
}

fn print_error(stdout: &mut io::Stdout, error: impl std::fmt::Display) -> io::Result<()> {
    print_multiline_block(stdout, &format!("error: {error}"), false)
}

fn print_multiline_block(stdout: &mut io::Stdout, text: &str, cyan_output: bool) -> io::Result<()> {
    for (index, line) in text.lines().enumerate() {
        if index == 0 {
            begin_output_block(stdout)?;
        } else {
            execute!(stdout, MoveToColumn(0))?;
        }

        if cyan_output {
            writeln!(stdout, "{}", line.cyan())?;
        } else if let Some(rest) = line.strip_prefix("error: ") {
            writeln!(stdout, "{} {}", "error:".red().bold(), rest.red())?;
        } else {
            writeln!(stdout, "{line}")?;
        }
    }
    Ok(())
}

fn begin_output_block(stdout: &mut io::Stdout) -> io::Result<()> {
    execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
    Ok(())
}

fn should_show_completion_menu(buffer: &str, completions: &[ShellCompletion]) -> bool {
    buffer.trim_start().starts_with('/') && !completions.is_empty()
}

fn ensure_vertical_space(stdout: &mut io::Stdout, lines_needed: usize) -> io::Result<usize> {
    if lines_needed == 0 {
        return Ok(0);
    }

    let Ok((_, row)) = position() else {
        return Ok(0);
    };
    let Ok((_, height)) = size() else {
        return Ok(0);
    };
    let overflow = scroll_lines_needed(row, height, lines_needed);
    if overflow > 0 {
        execute!(stdout, ScrollUp(overflow as u16))?;
    }
    Ok(overflow)
}