Skip to main content

taba_cli/tui/
mod.rs

1//! The interactive profiles screen (doc 19b, section 18.11.2): a list and a form navigated with
2//! the arrows. The logic (`screen`, `form`, `keys`) is pure and tested; `term` is the thin part that puts the
3//! terminal in raw mode.
4
5use std::io;
6use std::time::Instant;
7
8use taba_client::{Call, Config};
9
10use crate::app::{CliError, Ctx};
11use crate::profile::{ConfigFile, Profile};
12
13pub mod form;
14pub mod keys;
15pub mod screen;
16pub mod term;
17
18#[cfg(feature = "tui")]
19pub mod api;
20#[cfg(feature = "tui")]
21pub mod app;
22#[cfg(feature = "tui")]
23pub mod editor;
24#[cfg(feature = "tui")]
25pub mod effect;
26#[cfg(feature = "tui")]
27pub mod history;
28#[cfg(feature = "tui")]
29pub mod i18n;
30#[cfg(feature = "tui")]
31pub mod keymap;
32#[cfg(feature = "tui")]
33pub mod menu;
34#[cfg(feature = "tui")]
35pub mod model;
36#[cfg(feature = "tui")]
37pub mod msg;
38#[cfg(feature = "tui")]
39pub mod prom;
40#[cfg(feature = "tui")]
41pub mod screens;
42#[cfg(all(feature = "tui", test))]
43pub mod testing;
44#[cfg(feature = "tui")]
45pub mod theme;
46#[cfg(feature = "tui")]
47pub mod update;
48#[cfg(feature = "tui")]
49pub mod view;
50#[cfg(feature = "tui")]
51pub mod worker;
52
53pub use keys::Key;
54use screen::{App, Flow, Services};
55
56/// What to draw: the lines (with ANSI color sequences, if `Style.color`) and where the cursor is.
57#[derive(Debug, Clone, PartialEq, Eq, Default)]
58pub struct Frame {
59    pub lines: Vec<String>,
60    /// Line and column (zero-based) of the cursor; `None` hides the cursor.
61    pub cursor: Option<(usize, usize)>,
62}
63
64#[derive(Debug, Clone, Copy)]
65pub struct Style {
66    pub color: bool,
67}
68
69impl Style {
70    /// `text` with the given SGR code (`1` bold, `2` dim, `31` red...), if there is color.
71    pub fn paint(self, code: &str, text: &str) -> String {
72        if self.color && !text.is_empty() {
73            format!("\x1b[{code}m{text}\x1b[0m")
74        } else {
75            text.to_owned()
76        }
77    }
78}
79
80/// The terminal in raw mode. When dropped, it gives the terminal back as it was.
81pub trait Terminal {
82    /// Columns and lines.
83    fn size(&self) -> (usize, usize);
84    /// The next key; `None` when the input ended.
85    fn read_key(&mut self) -> io::Result<Option<Key>>;
86    fn draw(&mut self, frame: &Frame) -> io::Result<()>;
87    fn color(&self) -> bool;
88}
89
90/// Runs the screen until the person quits.
91pub fn run(ctx: &mut Ctx<'_>, terminal: &mut dyn Terminal) -> Result<(), CliError> {
92    let file = ctx.load_config()?;
93    let path = ctx.config_path()?.display().to_string();
94    let session = ctx.env("TABA_CLI_PROFILE").filter(|v| !v.is_empty());
95    let mut app = App::new(file, path, session);
96    let style = Style {
97        color: terminal.color(),
98    };
99    let mut services = CtxServices { ctx };
100    loop {
101        let (width, height) = terminal.size();
102        terminal
103            .draw(&app.render(style, width, height))
104            .map_err(|e| CliError::File(format!("failed to draw the screen: {e}")))?;
105        match terminal.read_key() {
106            Ok(Some(key)) => {
107                if app.handle(key, &mut services) == Flow::Quit {
108                    return Ok(());
109                }
110            }
111            Ok(None) => return Ok(()),
112            Err(e) => return Err(CliError::File(format!("failed to read the keyboard: {e}"))),
113        }
114    }
115}
116
117struct CtxServices<'a, 'b> {
118    ctx: &'a mut Ctx<'b>,
119}
120
121impl Services for CtxServices<'_, '_> {
122    fn save(&mut self, file: &ConfigFile) -> Result<(), String> {
123        self.ctx.save_config(file).map_err(|e| match e {
124            CliError::File(m) | CliError::Usage(m) => m,
125            other => format!("{other:?}"),
126        })
127    }
128
129    fn check(&mut self, profile: &Profile) -> Result<String, String> {
130        let config = connection_config(profile)?;
131        let url = config.base_url.clone();
132        let client = self.ctx.platform.client(config).map_err(|e| e.message)?;
133        let started = Instant::now();
134        // `GET /topics?limit=1` on the client API: checks the address, TLS and the token all at once.
135        match client.call(&Call::new("GET", "/topics").query("limit", 1)) {
136            Ok(_) => Ok(format!(
137                "ok: {url} answered in {} ms",
138                started.elapsed().as_millis()
139            )),
140            Err(e) if e.status == 401 || e.status == 403 => Err(format!(
141                "{url} respondeu {} ({}): confira o token",
142                e.status, e.code
143            )),
144            Err(e) if e.status > 0 => Err(format!("{url} respondeu {} ({})", e.status, e.code)),
145            Err(e) => Err(format!("failed: {}", e.message)),
146        }
147    }
148}
149
150/// A profile's client configuration, with the built-in values where it defines nothing.
151pub(crate) fn connection_config(profile: &Profile) -> Result<Config, String> {
152    let number = |field: &str, default: u64| {
153        profile
154            .get(field)
155            .and_then(|v| v.parse().ok())
156            .unwrap_or(default)
157    };
158    let mut config = Config::new(profile.get("url").unwrap_or(crate::profile::DEFAULT_URL));
159    config.connect_timeout = std::time::Duration::from_secs(number("connect-timeout", 5));
160    config.request_timeout = std::time::Duration::from_secs(number("timeout", 30));
161    config.max_attempts = 1;
162    config.user_agent = format!("taba-cli/{}", env!("CARGO_PKG_VERSION"));
163    if let Some(token) = profile.get("token") {
164        config.token = Some(token.to_owned());
165    }
166    if let Some(path) = profile.get("token-file") {
167        let token = crate::app::read_token_file(path).map_err(|e| match e {
168            CliError::File(m) => m,
169            other => format!("{other:?}"),
170        })?;
171        config.token = Some(token);
172    }
173    if let Some(path) = profile.get("ca-file") {
174        config.ca_pem = Some(
175            std::fs::read(path).map_err(|e| format!("could not read the ca-file {path}: {e}"))?,
176        );
177    }
178    Ok(config)
179}
180
181#[cfg(test)]
182mod tests;