diff --git a/Cargo.toml b/Cargo.toml index 02ff648..36d25f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ inhibit = ["afrim/inhibit"] rhai = ["afrim/rhai"] [dependencies] -afrim = { version = "0.5.4", default-features = false, git = "https://github.com/pythonbrad/afrim", rev = "7e5daba" } +afrim = { version = "0.5.4", default-features = false, git = "https://github.com/pythonbrad/afrim", rev = "fcb7721" } +anyhow = "1.0.82" clap = "4.5.4" rstk = "0.3.0" serde = { version = "1.0.197", features = ["serde_derive"] } diff --git a/src/config.rs b/src/config.rs index daf42e0..69f7a2c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ +use anyhow::{Context, Result}; use serde::Deserialize; -use std::{error, fs, path::Path}; +use std::{fs, path::Path}; use toml::{self}; #[derive(Clone, Deserialize, Debug, Default)] @@ -95,9 +96,11 @@ impl Default for Info { } impl Config { - pub fn from_file(filepath: &Path) -> Result> { - let content = fs::read_to_string(filepath)?; - let config: Self = toml::from_str(&content)?; + pub fn from_file(filepath: &Path) -> Result { + let content = fs::read_to_string(filepath) + .with_context(|| format!("Couldn't open file {filepath:?}"))?; + let config: Self = toml::from_str(&content) + .with_context(|| format!("Failed to parse configuration file {filepath:?}"))?; Ok(config) } diff --git a/src/lib.rs b/src/lib.rs index 7eaabc4..5f3dbe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,52 @@ mod config; mod window; -use afrim::frontend::Frontend; +use afrim::frontend::{Command, Frontend}; +use anyhow::{anyhow, Result}; use rstk::*; +use std::sync::{ + mpsc::{Receiver, Sender}, + OnceLock, +}; +use std::thread; use window::{rstk_ext::init_rstk_ext, toolkit::ToolKit, tooltip::ToolTip}; pub use config::Config; -#[derive(Clone)] pub struct Wish { - window: rstk::TkTopLevel, + window: &'static rstk::TkTopLevel, tooltip: ToolTip, toolkit: ToolKit, + tx: Option>, + rx: Option>, } impl Wish { - pub fn init(config: config::Config) -> Self { - let wish = if cfg!(debug_assertions) { - rstk::trace_with("wish").unwrap() - } else { - rstk::start_wish().unwrap() - }; + fn init() -> &'static rstk::TkTopLevel { + static WISH: OnceLock = OnceLock::new(); + WISH.get_or_init(|| { + let wish = if cfg!(debug_assertions) { + rstk::trace_with("wish").unwrap() + } else { + rstk::start_wish().unwrap() + }; + + // The default behavior is to close the window. + // But since this window, represent the main window, + // we don't want an unexpected behavior. + // It's better for us to manage the close. + // + // Note that, this close button is on the title bar. + wish.on_close(Self::kill); - init_rstk_ext(); + init_rstk_ext(); + + wish + }) + } + pub fn from_config(config: config::Config) -> Self { + let wish = Self::init(); let tooltip = ToolTip::new(config.theme.to_owned().unwrap_or_default()); let toolkit = ToolKit::new(config.to_owned()); @@ -31,106 +54,163 @@ impl Wish { window: wish, tooltip, toolkit, + tx: None, + rx: None, } } - pub fn raise_error(&self, message: &str, detail: &str) { + pub fn raise_error(message: &str, detail: T) { rstk::message_box() - .parent(&self.window) + .parent(Self::init()) .icon(IconImage::Error) .title("Unexpected Error") .message(message) - .detail(detail) + .detail(&format!("{detail:?}")) .show(); - rstk::end_wish(); + Self::kill(); } - pub fn build(&mut self) { - self.tooltip.build(rstk::make_toplevel(&self.window)); + fn build(&mut self) { + self.tooltip.build(rstk::make_toplevel(self.window)); self.toolkit.build(self.window.to_owned()); } - pub fn listen(&self) { - rstk::mainloop(); - } - - pub fn destroy(&self) { + /// End the process (wish and rust). + /// + /// Note that a `process::exit` is called internally. + pub fn kill() { rstk::end_wish(); } } impl Frontend for Wish { - fn update_screen(&mut self, screen: (u64, u64)) { - self.tooltip.update_screen(screen); - } - - fn update_position(&mut self, position: (f64, f64)) { - self.tooltip.update_position(position); - } - - fn set_input(&mut self, text: &str) { - self.tooltip.set_input_text(text); - } - - fn set_page_size(&mut self, size: usize) { - self.tooltip.set_page_size(size); - } + fn init(&mut self, tx: Sender, rx: Receiver) -> Result<()> { + self.tx = Some(tx); + self.rx = Some(rx); + self.build(); - fn add_predicate(&mut self, code: &str, remaining_code: &str, text: &str) { - self.tooltip.add_predicate(code, remaining_code, text); + Ok(()) } + fn listen(&mut self) -> Result<()> { + if self.tx.as_ref().and(self.rx.as_ref()).is_none() { + return Err(anyhow!("you should config the channel first!")); + } - fn clear_predicates(&mut self) { - self.tooltip.clear(); - } + // We shouldn't forget to listen for GUI events. + thread::spawn(rstk::mainloop); - fn previous_predicate(&mut self) { - self.tooltip.select_previous_predicate(); - } + let tx = self.tx.as_ref().unwrap(); - fn next_predicate(&mut self) { - self.tooltip.select_next_predicate(); - } - - fn get_selected_predicate(&self) -> Option<&(String, String, String)> { - self.tooltip.get_selected_predicate() - } + loop { + let command = self.rx.as_ref().unwrap().recv()?; + match command { + Command::ScreenSize(screen) => self.tooltip.update_screen(screen), + Command::Position(position) => self.tooltip.update_position(position), + Command::InputText(input) => self.tooltip.set_input_text(input), + Command::PageSize(size) => self.tooltip.set_page_size(size), + // TODO: implement the pause/resume. + Command::State(_state) => {} + Command::Predicate(predicate) => self.tooltip.add_predicate(predicate), + Command::Update => self.tooltip.update(), + Command::Clear => self.tooltip.clear(), + Command::SelectPreviousPredicate => self.tooltip.select_previous_predicate(), + Command::SelectNextPredicate => self.tooltip.select_next_predicate(), + Command::SelectedPredicate => { + if let Some(predicate) = self.tooltip.get_selected_predicate() { + tx.send(Command::Predicate(predicate.to_owned()))?; + } else { + tx.send(Command::NoPredicate)?; + } + } + // TODO: complete the implementation + // to send GUI commands such as pause/resume. + Command::NOP => tx.send(Command::NOP)?, + Command::End => { + tx.send(Command::End)?; + self.window.destroy(); - fn display(&self) { - self.tooltip.update(); + return Ok(()); + } + _ => (), + } + } } } #[cfg(test)] mod tests { use crate::{Config, Wish}; - use afrim::frontend::Frontend; + use afrim::frontend::{Command, Frontend, Predicate}; use std::path::Path; + use std::sync::mpsc; use std::thread; use std::time::Duration; #[test] fn test_api() { let config = Config::from_file(Path::new("data/full_sample.toml")).unwrap(); - let mut afrim_wish = Wish::init(config); - afrim_wish.build(); + let mut afrim_wish = Wish::from_config(config); + assert!(afrim_wish.listen().is_err()); + let (tx1, rx1) = mpsc::channel(); + let (tx2, rx2) = mpsc::channel(); + + let afrim_wish_thread = thread::spawn(move || { + afrim_wish.init(tx2, rx1).unwrap(); + afrim_wish.listen().unwrap(); + }); + + tx1.send(Command::NOP).unwrap(); + assert_eq!(rx2.recv().unwrap(), Command::NOP); // Test without data. - afrim_wish.clear_predicates(); - afrim_wish.next_predicate(); - afrim_wish.previous_predicate(); - assert!(afrim_wish.get_selected_predicate().is_none()); - afrim_wish.display(); + tx1.send(Command::ScreenSize((480, 320))).unwrap(); + tx1.send(Command::Clear).unwrap(); + tx1.send(Command::SelectNextPredicate).unwrap(); + tx1.send(Command::SelectPreviousPredicate).unwrap(); + tx1.send(Command::SelectedPredicate).unwrap(); + assert_eq!(rx2.recv().unwrap(), Command::NoPredicate); + tx1.send(Command::Update).unwrap(); // Test the adding of predicates. - afrim_wish.set_page_size(3); - afrim_wish.set_input("Test started!"); - afrim_wish.add_predicate("test", "123", "ok"); - afrim_wish.add_predicate("test1", "23", "ok"); - afrim_wish.add_predicate("test12", "1", "ok"); - afrim_wish.add_predicate("test123", "", "ok"); - afrim_wish.add_predicate("test1234", "", ""); - afrim_wish.display(); + tx1.send(Command::PageSize(3)).unwrap(); + tx1.send(Command::InputText("Test started!".to_owned())) + .unwrap(); + tx1.send(Command::Predicate(Predicate { + code: "test".to_owned(), + remaining_code: "123".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + })) + .unwrap(); + tx1.send(Command::Predicate(Predicate { + code: "test1".to_owned(), + remaining_code: "23".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + })) + .unwrap(); + tx1.send(Command::Predicate(Predicate { + code: "test12".to_owned(), + remaining_code: "3".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + })) + .unwrap(); + tx1.send(Command::Predicate(Predicate { + code: "test123".to_owned(), + remaining_code: "".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + })) + .unwrap(); + tx1.send(Command::Predicate(Predicate { + code: "test1234".to_owned(), + remaining_code: "".to_owned(), + texts: vec!["".to_owned()], + can_commit: false, + })) + .unwrap(); + tx1.send(Command::Update).unwrap(); // Test the geometry. (0..100).for_each(|i| { @@ -138,22 +218,41 @@ mod tests { return; }; let i = i as f64; - afrim_wish.update_position((i, i)); + tx1.send(Command::Position((i, i))).unwrap(); thread::sleep(Duration::from_millis(100)); }); // Test the navigation. - afrim_wish.previous_predicate(); + tx1.send(Command::SelectPreviousPredicate).unwrap(); + tx1.send(Command::SelectedPredicate).unwrap(); assert_eq!( - afrim_wish.get_selected_predicate(), - Some(&("test1234".to_owned(), "".to_owned(), "".to_owned())) + rx2.recv().unwrap(), + Command::Predicate(Predicate { + code: "test123".to_owned(), + remaining_code: "".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + }) ); - afrim_wish.next_predicate(); + tx1.send(Command::SelectNextPredicate).unwrap(); + tx1.send(Command::SelectedPredicate).unwrap(); assert_eq!( - afrim_wish.get_selected_predicate(), - Some(&("test".to_owned(), "123".to_owned(), "ok".to_owned())) + rx2.recv().unwrap(), + Command::Predicate(Predicate { + code: "test".to_owned(), + remaining_code: "123".to_owned(), + texts: vec!["ok".to_owned()], + can_commit: false, + }) ); - afrim_wish.display(); - afrim_wish.destroy(); + tx1.send(Command::Update).unwrap(); + + // We end the communication. + tx1.send(Command::End).unwrap(); + assert_eq!(rx2.recv().unwrap(), Command::End); + assert!(rx2.recv().is_err()); + + // We wait the afrim to end properly. + afrim_wish_thread.join().unwrap(); } } diff --git a/src/main.rs b/src/main.rs index 5ec8262..6dbb272 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,8 @@ use afrim::{run, Config as ClafricaConfig}; use afrim_wish::{Config as WishConfig, Wish}; use clap::Parser; -use std::process; -use std::thread; -/// Afrim CLI. +/// Afrim Wish CLI. #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { @@ -21,37 +19,26 @@ struct Args { fn main() { let args = Args::parse(); - let wish_conf = WishConfig::from_file(&args.config_file).unwrap_or_else(|err| { - eprintln!("Problem parsing config file: {err}"); - process::exit(1); - }); - - let mut frontend = Wish::init(wish_conf); - frontend.build(); - - // We start the backend - { - let frontend = frontend.clone(); - - thread::spawn(move || { - let clafrica_conf = - ClafricaConfig::from_file(&args.config_file).unwrap_or_else(|err| { - frontend.raise_error("Problem parsing config file", &err.to_string()); - process::exit(1); - }); - - // End the program if check only. - if args.check { - frontend.destroy(); - } - - if let Err(e) = run(clafrica_conf, frontend.clone()) { - frontend.raise_error("Application error", &e.to_string()); - process::exit(1); - } - }); + let wish_conf = WishConfig::from_file(&args.config_file) + .map_err(|err| { + Wish::raise_error("Problem parsing config file", &err); + }) + .unwrap(); + + let wish = Wish::from_config(wish_conf); + + let clafrica_conf = ClafricaConfig::from_file(&args.config_file) + .map_err(|err| { + Wish::raise_error("Problem parsing config file", &err); + }) + .unwrap(); + + // End the program if check only. + if args.check { + Wish::kill(); } - // We start listening gui events - frontend.listen(); + if let Err(err) = run(clafrica_conf, wish) { + Wish::raise_error("Application error", &err); + } } diff --git a/src/window/tooltip.rs b/src/window/tooltip.rs index 95f9209..1f7470c 100644 --- a/src/window/tooltip.rs +++ b/src/window/tooltip.rs @@ -1,5 +1,6 @@ use super::config::Theme; use super::rstk_ext::*; +use afrim::frontend::Predicate; use rstk::*; use std::collections::HashMap; @@ -9,7 +10,7 @@ pub struct ToolTip { window: Option, cursor_widget: Option, predicates_widget: Option, - predicates: Vec<(String, String, String)>, + predicates: Vec, current_predicate_id: usize, page_size: usize, input: String, @@ -89,17 +90,25 @@ impl ToolTip { self.window.as_ref().unwrap().position(x, y); } - pub fn set_input_text(&mut self, text: &str) { - self.input = text.to_owned(); + pub fn set_input_text(&mut self, text: String) { + self.input = text; } pub fn set_page_size(&mut self, size: usize) { self.page_size = size; } - pub fn add_predicate(&mut self, code: &str, remaining_code: &str, text: &str) { - self.predicates - .push((code.to_owned(), remaining_code.to_owned(), text.to_owned())); + pub fn add_predicate(&mut self, predicate: Predicate) { + predicate + .texts + .iter() + .filter(|text| !text.is_empty()) + .for_each(|text| { + let mut predicate = predicate.clone(); + predicate.texts = vec![text.to_owned()]; + + self.predicates.push(predicate); + }); } pub fn clear(&mut self) { @@ -127,7 +136,7 @@ impl ToolTip { self.update(); } - pub fn get_selected_predicate(&self) -> Option<&(String, String, String)> { + pub fn get_selected_predicate(&self) -> Option<&Predicate> { self.predicates.get(self.current_predicate_id) } @@ -140,8 +149,13 @@ impl ToolTip { .chain(self.predicates.iter().enumerate()) .skip(self.current_predicate_id) .take(page_size) - .map(|(i, (_code, remaining_code, text))| { - format!("{}. {text} ~{remaining_code}", i + 1,) + .map(|(i, predicate)| { + format!( + "{}. {} ~{}", + i + 1, + predicate.texts[0], + predicate.remaining_code + ) }) .collect();