1use 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
58pub struct Frame {
59 pub lines: Vec<String>,
60 pub cursor: Option<(usize, usize)>,
62}
63
64#[derive(Debug, Clone, Copy)]
65pub struct Style {
66 pub color: bool,
67}
68
69impl Style {
70 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
80pub trait Terminal {
82 fn size(&self) -> (usize, usize);
84 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
90pub 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 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
150pub(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;