taler-rust

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

repl.rs (7570B)


      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, marker::PhantomData};
     18 
     19 use nu_ansi_term::Color;
     20 use reedline::{
     21     ColumnarMenu, CompletionResult, DefaultHinter, FileBackedHistory, KeyCode, KeyModifiers,
     22     MenuBuilder as _, Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, Vi,
     23     default_vi_insert_keybindings, default_vi_normal_keybindings, default_vi_visual_keybindings,
     24 };
     25 
     26 use crate::cli::clap_parse;
     27 
     28 pub struct Repl<T: clap::Parser> {
     29     relp: Reedline,
     30     phantom: PhantomData<T>,
     31 }
     32 
     33 impl<T: clap::Parser> Repl<T> {
     34     pub fn new(history: &str) -> Self {
     35         let history = Box::new(
     36             FileBackedHistory::with_file(1000, history.into())
     37                 .expect("Error configuring history with file"),
     38         );
     39         let completion_menu = Box::new(
     40             ColumnarMenu::default()
     41                 .with_name("completion_menu")
     42                 .with_columns(4)
     43                 .with_column_width(None)
     44                 .with_column_padding(2),
     45         );
     46 
     47         let mut insert = default_vi_insert_keybindings();
     48         insert.add_binding(
     49             KeyModifiers::NONE,
     50             KeyCode::Tab,
     51             ReedlineEvent::UntilFound(vec![
     52                 ReedlineEvent::Menu("completion_menu".to_string()),
     53                 ReedlineEvent::MenuNext,
     54             ]),
     55         );
     56         let repl = Reedline::create()
     57             .with_history(history)
     58             .with_completer(Box::new(ClapCompleter(T::command().clone())))
     59             .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
     60             .with_quick_completions(true)
     61             .with_partial_completions(true)
     62             .with_hinter(Box::new(DefaultHinter::default().with_style(
     63                 nu_ansi_term::Style::new().italic().fg(Color::LightGray),
     64             )))
     65             .with_edit_mode(Box::new(Vi::new(
     66                 insert,
     67                 default_vi_normal_keybindings(),
     68                 default_vi_visual_keybindings(),
     69             )));
     70 
     71         Self {
     72             relp: repl,
     73             phantom: PhantomData,
     74         }
     75     }
     76 
     77     pub fn read_line(&mut self, left: &str, right: &str) -> Option<T> {
     78         loop {
     79             let Signal::Success(buf) = self.relp.read_line(&ReplPrompt(left, right)).unwrap()
     80             else {
     81                 return None;
     82             };
     83             match clap_parse(&buf) {
     84                 Ok(cmd) => return Some(cmd),
     85                 Err(e) => {
     86                     e.print().unwrap();
     87                 }
     88             }
     89         }
     90     }
     91 }
     92 
     93 struct ClapCompleter(clap::Command);
     94 
     95 impl reedline::Completer for ClapCompleter {
     96     fn complete(&mut self, line: &str, pos: usize) -> CompletionResult {
     97         let mut suggestions = Vec::new();
     98         let text_before_cursor = &line[..pos];
     99 
    100         let words = shlex::split(text_before_cursor).unwrap_or_default();
    101         let is_whitespace_at_end = text_before_cursor.ends_with(' ');
    102 
    103         let mut active_cmd = &self.0;
    104         let mut last_word = "";
    105 
    106         // Determine the word currently being typed
    107         if !is_whitespace_at_end && !words.is_empty() {
    108             last_word = words.last().unwrap();
    109         }
    110 
    111         // Identify which subcommand we are currently inside by walking the tokens
    112         let tokens = if is_whitespace_at_end {
    113             &words[..]
    114         } else {
    115             &words[..words.len().saturating_sub(1)]
    116         };
    117 
    118         for token in tokens {
    119             if let Some(subcmd) = active_cmd
    120                 .get_subcommands()
    121                 .find(|s| s.get_name() == *token)
    122             {
    123                 active_cmd = subcmd;
    124             }
    125         }
    126 
    127         let start = pos - last_word.len();
    128         let span = Span::new(start, pos);
    129 
    130         // Suggest Long Flags
    131         if last_word.starts_with("--") {
    132             for arg in active_cmd.get_arguments() {
    133                 if let Some(long) = arg.get_long() {
    134                     let flag = format!("--{}", long);
    135                     if flag.starts_with(last_word) {
    136                         suggestions.push(Suggestion {
    137                             value: flag,
    138                             description: arg.get_help().map(|s| s.to_string()),
    139                             span,
    140                             append_whitespace: true,
    141                             ..Suggestion::default()
    142                         });
    143                     }
    144                 }
    145             }
    146         }
    147         // Suggest Short & Long Flags
    148         else if last_word.starts_with("-") {
    149             for arg in active_cmd.get_arguments() {
    150                 if let Some(short) = arg.get_short() {
    151                     let flag = format!("-{}", short);
    152                     if flag.starts_with(last_word) {
    153                         suggestions.push(Suggestion {
    154                             value: flag,
    155                             description: arg.get_help().map(|s| s.to_string()),
    156                             span,
    157                             append_whitespace: true,
    158                             ..Suggestion::default()
    159                         });
    160                     }
    161                 }
    162                 if let Some(long) = arg.get_long() {
    163                     let flag = format!("--{}", long);
    164                     if flag.starts_with(last_word) {
    165                         suggestions.push(Suggestion {
    166                             value: flag,
    167                             description: arg.get_help().map(|s| s.to_string()),
    168                             span,
    169                             append_whitespace: true,
    170                             ..Suggestion::default()
    171                         });
    172                     }
    173                 }
    174             }
    175         }
    176         // Suggest Subcommands
    177         else {
    178             for subcmd in active_cmd.get_subcommands() {
    179                 if subcmd.get_name().starts_with(last_word) {
    180                     suggestions.push(Suggestion {
    181                         value: subcmd.get_name().to_string(),
    182                         description: subcmd.get_about().map(|s| s.to_string()),
    183                         span,
    184                         append_whitespace: true,
    185                         ..Suggestion::default()
    186                     });
    187                 }
    188             }
    189         }
    190 
    191         CompletionResult::fresh(suggestions)
    192     }
    193 }
    194 
    195 struct ReplPrompt<'a>(&'a str, &'a str);
    196 
    197 impl<'a> reedline::Prompt for ReplPrompt<'a> {
    198     fn render_prompt_left(&self) -> Cow<'_, str> {
    199         Cow::Borrowed(self.0)
    200     }
    201 
    202     fn render_prompt_right(&self) -> Cow<'_, str> {
    203         Cow::Borrowed(self.1)
    204     }
    205 
    206     fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> {
    207         Cow::Borrowed(">")
    208     }
    209 
    210     fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
    211         Cow::Borrowed(":")
    212     }
    213 
    214     fn render_prompt_history_search_indicator(
    215         &self,
    216         _: reedline::PromptHistorySearch,
    217     ) -> Cow<'_, str> {
    218         Cow::Borrowed(">")
    219     }
    220 }