taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

repl.rs (7101B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::borrow::Cow;
     18 
     19 use nu_ansi_term::Color;
     20 use reedline::{
     21     ColumnarMenu, DefaultHinter, FileBackedHistory, KeyCode, KeyModifiers, MenuBuilder as _,
     22     Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, Vi,
     23     default_vi_insert_keybindings, default_vi_normal_keybindings,
     24 };
     25 
     26 use crate::cli::clap_parse;
     27 
     28 pub async fn test_repl<T: clap::Parser>(
     29     history: &str,
     30     prompt: String,
     31     prepare: impl AsyncFn(),
     32     run: impl AsyncFn(T) -> bool,
     33 ) {
     34     let history = Box::new(
     35         FileBackedHistory::with_file(1000, history.into())
     36             .expect("Error configuring history with file"),
     37     );
     38     let completion_menu = Box::new(
     39         ColumnarMenu::default()
     40             .with_name("completion_menu")
     41             .with_columns(4)
     42             .with_column_width(None)
     43             .with_column_padding(2),
     44     );
     45 
     46     let mut insert = default_vi_insert_keybindings();
     47     insert.add_binding(
     48         KeyModifiers::NONE,
     49         KeyCode::Tab,
     50         ReedlineEvent::UntilFound(vec![
     51             ReedlineEvent::Menu("completion_menu".to_string()),
     52             ReedlineEvent::MenuNext,
     53         ]),
     54     );
     55     let mut relp = Reedline::create()
     56         .with_history(history)
     57         .with_completer(Box::new(ClapCompleter(T::command())))
     58         .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
     59         .with_quick_completions(true)
     60         .with_partial_completions(true)
     61         .with_hinter(Box::new(DefaultHinter::default().with_style(
     62             nu_ansi_term::Style::new().italic().fg(Color::LightGray),
     63         )))
     64         .with_edit_mode(Box::new(Vi::new(insert, default_vi_normal_keybindings())));
     65 
     66     let prompt = ReplPrompt(prompt);
     67     loop {
     68         prepare().await;
     69         let Signal::Success(buf) = relp.read_line(&prompt).unwrap() else {
     70             break;
     71         };
     72         match clap_parse(&buf) {
     73             Ok(cmd) => {
     74                 if run(cmd).await {
     75                     break;
     76                 }
     77             }
     78             Err(e) => {
     79                 println!("{e}");
     80             }
     81         }
     82     }
     83 }
     84 
     85 struct ClapCompleter(clap::Command);
     86 
     87 impl reedline::Completer for ClapCompleter {
     88     fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
     89         let mut suggestions = Vec::new();
     90         let text_before_cursor = &line[..pos];
     91 
     92         let words = shlex::split(text_before_cursor).unwrap_or_default();
     93         let is_whitespace_at_end = text_before_cursor.ends_with(' ');
     94 
     95         let mut active_cmd = &self.0;
     96         let mut last_word = "";
     97 
     98         // Determine the word currently being typed
     99         if !is_whitespace_at_end && !words.is_empty() {
    100             last_word = words.last().unwrap();
    101         }
    102 
    103         // Identify which subcommand we are currently inside by walking the tokens
    104         let tokens = if is_whitespace_at_end {
    105             &words[..]
    106         } else {
    107             &words[..words.len().saturating_sub(1)]
    108         };
    109 
    110         for token in tokens {
    111             if let Some(subcmd) = active_cmd
    112                 .get_subcommands()
    113                 .find(|s| s.get_name() == *token)
    114             {
    115                 active_cmd = subcmd;
    116             }
    117         }
    118 
    119         let start = pos - last_word.len();
    120         let span = Span::new(start, pos);
    121 
    122         // Suggest Long Flags
    123         if last_word.starts_with("--") {
    124             for arg in active_cmd.get_arguments() {
    125                 if let Some(long) = arg.get_long() {
    126                     let flag = format!("--{}", long);
    127                     if flag.starts_with(last_word) {
    128                         suggestions.push(Suggestion {
    129                             value: flag,
    130                             description: arg.get_help().map(|s| s.to_string()),
    131                             span,
    132                             append_whitespace: true,
    133                             ..Suggestion::default()
    134                         });
    135                     }
    136                 }
    137             }
    138         }
    139         // Suggest Short & Long Flags
    140         else if last_word.starts_with("-") {
    141             for arg in active_cmd.get_arguments() {
    142                 if let Some(short) = arg.get_short() {
    143                     let flag = format!("-{}", short);
    144                     if flag.starts_with(last_word) {
    145                         suggestions.push(Suggestion {
    146                             value: flag,
    147                             description: arg.get_help().map(|s| s.to_string()),
    148                             span,
    149                             append_whitespace: true,
    150                             ..Suggestion::default()
    151                         });
    152                     }
    153                 }
    154                 if let Some(long) = arg.get_long() {
    155                     let flag = format!("--{}", long);
    156                     if flag.starts_with(last_word) {
    157                         suggestions.push(Suggestion {
    158                             value: flag,
    159                             description: arg.get_help().map(|s| s.to_string()),
    160                             span,
    161                             append_whitespace: true,
    162                             ..Suggestion::default()
    163                         });
    164                     }
    165                 }
    166             }
    167         }
    168         // Suggest Subcommands
    169         else {
    170             for subcmd in active_cmd.get_subcommands() {
    171                 if subcmd.get_name().starts_with(last_word) {
    172                     suggestions.push(Suggestion {
    173                         value: subcmd.get_name().to_string(),
    174                         description: subcmd.get_about().map(|s| s.to_string()),
    175                         span,
    176                         append_whitespace: true,
    177                         ..Suggestion::default()
    178                     });
    179                 }
    180             }
    181         }
    182 
    183         suggestions
    184     }
    185 }
    186 
    187 struct ReplPrompt(String);
    188 
    189 impl reedline::Prompt for ReplPrompt {
    190     fn render_prompt_left(&self) -> Cow<'_, str> {
    191         Cow::Borrowed(&self.0)
    192     }
    193 
    194     fn render_prompt_right(&self) -> Cow<'_, str> {
    195         Cow::Borrowed("")
    196     }
    197 
    198     fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> {
    199         Cow::Borrowed(">")
    200     }
    201 
    202     fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
    203         Cow::Borrowed(":")
    204     }
    205 
    206     fn render_prompt_history_search_indicator(
    207         &self,
    208         _: reedline::PromptHistorySearch,
    209     ) -> Cow<'_, str> {
    210         Cow::Borrowed(">")
    211     }
    212 }