Procházet zdrojové kódy

[UI] libreadline support added via rustyline

Now categories can be tab-completed when run in terminal

Signed-off-by: Slava Barinov <rayslava@gmail.com>
Slava Barinov před 3 roky
rodič
revize
0a25a76bac
4 změnil soubory, kde provedl 164 přidání a 16 odebrání
  1. 3 3
      extern/ui.cpp
  2. 1 1
      src/categories.rs
  3. 4 0
      src/main.rs
  4. 156 12
      src/ui.rs

+ 3 - 3
extern/ui.cpp

@@ -47,7 +47,7 @@ public:
   void draw();
   void setState(ushort aState, Boolean enable);
   void scrollDraw();
-  Boolean valid(ushort command);
+  Boolean valid(ushort command) const;
 
 private:
   virtual const char *streamableName() const { return name; }
@@ -68,7 +68,7 @@ public:
 protected:
   virtual void sizeLimits(TPoint& min, TPoint& max) override {
     TWindow::sizeLimits(min, max);
-    min.x = size.x/2+10;
+    min.x = size.x / 2 + 10;
   };
 
 };
@@ -122,7 +122,7 @@ void TItemViewer::setState(ushort aState, Boolean enable) {
     setLimit(limit.x, limit.y);
 }
 
-Boolean TItemViewer::valid(ushort) { return isValid; }
+Boolean TItemViewer::valid(ushort) const { return isValid; }
 
 void *TItemViewer::read(ipstream &is) {
   char *fName = NULL;

+ 1 - 1
src/categories.rs

@@ -109,7 +109,7 @@ pub fn get_category(item: &str, storage: &mut CatStats, accounts: &[String]) ->
         };
         let cats: Vec<&String> = accounts
             .iter()
-            .filter(|acc| acc.contains("Expense:"))
+            .filter(|acc| acc.contains("Expenses:"))
             .collect();
         let cat = input_category(item, &topcat, &cats);
         if cat.is_empty() {

+ 4 - 0
src/main.rs

@@ -1,5 +1,7 @@
 use qif_generator::account::{Account, AccountType};
 
+use log::LevelFilter;
+use pretty_env_logger;
 use std::path::PathBuf;
 use structopt::StructOpt;
 
@@ -51,6 +53,8 @@ struct Cli {
 
 #[cfg(not(tarpaulin_include))]
 fn main() {
+    pretty_env_logger::init();
+    log::debug!("Log started");
     let args = Cli::from_args();
 
     #[cfg(feature = "telegram")]

+ 156 - 12
src/ui.rs

@@ -1,6 +1,16 @@
+use rustyline::completion::Completer;
+use rustyline::config::OutputStreamType;
+use rustyline::error::ReadlineError;
+use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
+use rustyline::hint::{Hinter, HistoryHinter};
+use rustyline::line_buffer::LineBuffer;
+use rustyline::validate::{self, MatchingBracketValidator, Validator};
+use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyEvent};
+use rustyline_derive::Helper;
+use std::borrow::Cow::{self, Borrowed, Owned};
 #[cfg(feature = "tv")]
 use std::ffi::CString;
-use std::io::{stdin, stdout, Write};
+use std::io::{stdout, Write};
 #[cfg(feature = "tv")]
 use std::os::raw::c_char;
 
@@ -19,17 +29,151 @@ pub fn run_tv() {
     println!("Hello, world!");
 }
 
-pub fn input_category(item: &str, cat: &str, cats: &[&String]) -> String {
-    let mut x = String::with_capacity(64);
-    if !cat.is_empty() {
-        print!("'{}'? (default: {}) > ", item, cat);
-    } else {
-        print!(
-            "'{}'? (no default, possible categories: {:?}) > ",
-            item, cats
-        );
+struct CatCompleter<'a> {
+    completions: &'a [&'a String],
+}
+
+impl Completer for CatCompleter<'_> {
+    type Candidate = String;
+
+    fn complete(
+        &self,
+        line: &str,
+        _pos: usize,
+        _ctx: &Context<'_>,
+    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
+        let results: Vec<String> = self
+            .completions
+            .iter()
+            .filter(|comp| comp.contains(line))
+            .map(|s| s.to_string())
+            .collect();
+
+        Ok((0, results))
+    }
+
+    fn update(&self, _line: &mut LineBuffer, _start: usize, _elected: &str) {}
+}
+
+impl<'a> CatCompleter<'a> {
+    pub fn new(completions: &'a [&'a String]) -> Self {
+        log::debug!("Completions: {:#?}", completions);
+        Self { completions }
+    }
+}
+
+#[derive(Helper)]
+struct CatHelper<'a> {
+    completer: CatCompleter<'a>,
+    highlighter: MatchingBracketHighlighter,
+    validator: MatchingBracketValidator,
+    hinter: HistoryHinter,
+    colored_prompt: String,
+}
+
+impl Completer for CatHelper<'_> {
+    type Candidate = String;
+
+    fn complete(
+        &self,
+        line: &str,
+        pos: usize,
+        ctx: &Context<'_>,
+    ) -> Result<(usize, Vec<Self::Candidate>), ReadlineError> {
+        self.completer.complete(line, pos, ctx)
     }
+}
+
+impl Hinter for CatHelper<'_> {
+    type Hint = String;
+
+    fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
+        self.hinter.hint(line, pos, ctx)
+    }
+}
+
+impl Highlighter for CatHelper<'_> {
+    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
+        &'s self,
+        prompt: &'p str,
+        default: bool,
+    ) -> Cow<'b, str> {
+        if default {
+            Borrowed(&self.colored_prompt)
+        } else {
+            Borrowed(prompt)
+        }
+    }
+
+    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
+        Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
+    }
+
+    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
+        self.highlighter.highlight(line, pos)
+    }
+
+    fn highlight_char(&self, line: &str, pos: usize) -> bool {
+        self.highlighter.highlight_char(line, pos)
+    }
+}
+
+impl Validator for CatHelper<'_> {
+    fn validate(
+        &self,
+        ctx: &mut validate::ValidationContext,
+    ) -> rustyline::Result<validate::ValidationResult> {
+        self.validator.validate(ctx)
+    }
+
+    fn validate_while_typing(&self) -> bool {
+        self.validator.validate_while_typing()
+    }
+}
+
+pub fn input_category(item: &str, cat: &str, cats: &[&String]) -> String {
     let _ = stdout().flush();
-    stdin().read_line(&mut x).expect("Error reading input");
-    String::from(x.trim_end_matches('\n'))
+
+    let config = Config::builder()
+        .history_ignore_space(true)
+        .check_cursor_position(true)
+        .completion_type(CompletionType::List)
+        .edit_mode(EditMode::Emacs)
+        .output_stream(OutputStreamType::Stdout)
+        .build();
+    let h = CatHelper {
+        completer: CatCompleter::new(cats),
+        highlighter: MatchingBracketHighlighter::new(),
+        hinter: HistoryHinter {},
+        colored_prompt: "".to_owned(),
+        validator: MatchingBracketValidator::new(),
+    };
+    let mut rl = Editor::with_config(config);
+    rl.set_helper(Some(h));
+    rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward);
+    rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward);
+    if rl.load_history("history.txt").is_err() {
+        log::debug!("No previous history.");
+    }
+    let mut result = String::new();
+    let p = format!("{} ({})> ", item, cat);
+    rl.helper_mut().expect("No helper").colored_prompt =
+        format!("\x1b[1;33m{} \x1b[1;32m({})\x1b[0m\x1b[1;37m> ", item, cat);
+    let readline = rl.readline(&p);
+    match readline {
+        Ok(line) => {
+            rl.add_history_entry(line.as_str());
+            result = line;
+        }
+        Err(ReadlineError::Interrupted) => {
+            println!("Interrupted");
+        }
+        Err(ReadlineError::Eof) => {
+            println!("Encountered Eof");
+        }
+        Err(err) => {
+            println!("Error: {:?}", err);
+        }
+    }
+    String::from(result.trim_end_matches('\n'))
 }