Skip to main content

data_beans/interactive/
mod.rs

1//! Full-screen terminal views (feature `tui`), and line prompts for when
2//! there is no terminal.
3//!
4//! - [`ui`]: the shared pieces for building a view. The palette (terminal
5//!   foreground plus one accent), [`ui::panel`], [`ui::header`],
6//!   [`ui::help_line`], [`ui::input_line`], the [`ui::Screen`] trait driven by
7//!   [`ui::run_screen`], and histograms: [`ui::Scale`], [`ui::Binning`],
8//!   [`ui::Binned`], [`ui::HistPlot`], and [`ui::MirrorPlot`] for two series
9//!   around one zero line.
10//! - [`cutoff_tui`]: row and column nnz cutoff picker over plain nnz vectors.
11//! - [`stat_tui`]: table-and-histogram explorer over plain name and value
12//!   columns, optionally marking entries and handing them back.
13//!
14//! Open a view only when [`tui_available`]; otherwise fall back to text.
15//!
16//! The views run in a legume-tui session ([`TOOL`]): a command opens it
17//! first thing and hands it to [`ui::Tui`] or [`ui::run_screen_in`]; where
18//! the session is hosted, the command keeps running when the connection
19//! drops. [`ui::run_screen`] runs a view in a plain session of its own
20//! (`legume_tui::Session::plain`).
21
22pub mod cutoff_tui;
23pub mod stat_tui;
24pub mod ui;
25
26use std::io::{self, IsTerminal, Write};
27
28/// Whether a full-screen session can run: both stdin and stdout are a terminal.
29/// Otherwise callers fall back to the line prompts below.
30pub fn tui_available() -> bool {
31    io::stdin().is_terminal() && io::stdout().is_terminal()
32}
33
34/// The tool data-beans' sessions belong to: they are named
35/// `data-beans-squeeze-1`, `data-beans-stat-2`, and `data-beans attach`
36/// finds them.
37pub const TOOL: legume_tui::Tool = legume_tui::Tool::new("data-beans");
38
39/// The detach and switch keys for a title bar ([`ui::title_bar`]), each
40/// with what it does in a word (`detach`, `switch`); none when the views
41/// run plain.
42pub fn session_keys() -> Vec<(String, String)> {
43    legume_tui::help_keys()
44        .into_iter()
45        .map(|k| (k.key.to_lowercase(), k.short))
46        .collect()
47}
48
49/// The paragraph a command's long help ends with about its session:
50/// `lead` first, for a command whose view is optional (when it runs as a
51/// session), then how it runs, the detach and switch keys, how to return,
52/// and `more` at the end. Its views are always opened on something
53/// named, so a new start never offers the live sessions; `data-beans
54/// attach` does.
55pub fn session_help(lead: Option<&str>, more: &str) -> String {
56    let help = legume_tui::cli::SessionHelp::new()
57        .what_goes_on("the command goes on")
58        .resume_hint(Some(
59            "Starting it again starts a new one (naming the detached ones); \
60             `data-beans attach` with no name",
61        ))
62        .more(more);
63    let help = match lead {
64        Some(lead) => help.lead(lead),
65        None => help,
66    };
67    legume_tui::cli::session_help(&TOOL, help)
68}
69
70/// User action after viewing histogram or other interactive prompts
71#[derive(Debug, Clone)]
72pub enum UserAction {
73    Proceed,
74    AdjustCutoffs(usize, usize),
75    Cancel,
76}
77
78/// Prompt user for action in interactive mode after showing histogram
79pub fn prompt_user_action(
80    current_row_cutoff: usize,
81    current_col_cutoff: usize,
82) -> anyhow::Result<UserAction> {
83    println!("\nOptions:");
84    println!("  [p] Proceed with current cutoffs");
85    println!("  [a] Adjust cutoffs");
86    println!("  [c] Cancel");
87    print!("\nChoose an option (p/a/c): ");
88    io::stdout().flush()?;
89
90    let mut input = String::new();
91    io::stdin().read_line(&mut input)?;
92    let choice = input.trim().to_lowercase();
93
94    match choice.as_str() {
95        "p" | "proceed" | "y" | "yes" => Ok(UserAction::Proceed),
96        "c" | "cancel" | "n" | "no" => Ok(UserAction::Cancel),
97        "a" | "adjust" => {
98            let new_row_cutoff = prompt_cutoff_value("row", current_row_cutoff)?;
99            let new_col_cutoff = prompt_cutoff_value("column", current_col_cutoff)?;
100            Ok(UserAction::AdjustCutoffs(new_row_cutoff, new_col_cutoff))
101        }
102        _ => {
103            println!("Invalid choice. Cancelling operation.");
104            Ok(UserAction::Cancel)
105        }
106    }
107}
108
109/// Prompt user for a single cutoff value
110fn prompt_cutoff_value(label: &str, current: usize) -> anyhow::Result<usize> {
111    print!("\nEnter new {} nnz cutoff (current: {}): ", label, current);
112    io::stdout().flush()?;
113
114    let mut input = String::new();
115    io::stdin().read_line(&mut input)?;
116
117    let value = input.trim().parse::<usize>().unwrap_or(current);
118    Ok(value)
119}
120
121/// Simple yes/no confirmation prompt
122pub fn confirm(message: &str) -> anyhow::Result<bool> {
123    print!("{} (y/n): ", message);
124    io::stdout().flush()?;
125
126    let mut input = String::new();
127    io::stdin().read_line(&mut input)?;
128    let choice = input.trim().to_lowercase();
129
130    Ok(matches!(choice.as_str(), "y" | "yes"))
131}
132
133#[cfg(test)]
134#[path = "tests/session.rs"]
135mod tests;