taler-rust

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

repl.rs (7399B)


      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, 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 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(insert, default_vi_normal_keybindings())));
     66 
     67         Self {
     68             relp: repl,
     69             phantom: PhantomData,
     70         }
     71     }
     72 
     73     pub fn read_line(&mut self, left: &str, right: &str) -> Option<T> {
     74         loop {
     75             let Signal::Success(buf) = self.relp.read_line(&ReplPrompt(left, right)).unwrap()
     76             else {
     77                 return None;
     78             };
     79             match clap_parse(&buf) {
     80                 Ok(cmd) => return Some(cmd),
     81                 Err(e) => {
     82                     e.print().unwrap();
     83                 }
     84             }
     85         }
     86     }
     87 }
     88 
     89 struct ClapCompleter(clap::Command);
     90 
     91 impl reedline::Completer for ClapCompleter {
     92     fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
     93         let mut suggestions = Vec::new();
     94         let text_before_cursor = &line[..pos];
     95 
     96         let words = shlex::split(text_before_cursor).unwrap_or_default();
     97         let is_whitespace_at_end = text_before_cursor.ends_with(' ');
     98 
     99         let mut active_cmd = &self.0;
    100         let mut last_word = "";
    101 
    102         // Determine the word currently being typed
    103         if !is_whitespace_at_end && !words.is_empty() {
    104             last_word = words.last().unwrap();
    105         }
    106 
    107         // Identify which subcommand we are currently inside by walking the tokens
    108         let tokens = if is_whitespace_at_end {
    109             &words[..]
    110         } else {
    111             &words[..words.len().saturating_sub(1)]
    112         };
    113 
    114         for token in tokens {
    115             if let Some(subcmd) = active_cmd
    116                 .get_subcommands()
    117                 .find(|s| s.get_name() == *token)
    118             {
    119                 active_cmd = subcmd;
    120             }
    121         }
    122 
    123         let start = pos - last_word.len();
    124         let span = Span::new(start, pos);
    125 
    126         // Suggest Long Flags
    127         if last_word.starts_with("--") {
    128             for arg in active_cmd.get_arguments() {
    129                 if let Some(long) = arg.get_long() {
    130                     let flag = format!("--{}", long);
    131                     if flag.starts_with(last_word) {
    132                         suggestions.push(Suggestion {
    133                             value: flag,
    134                             description: arg.get_help().map(|s| s.to_string()),
    135                             span,
    136                             append_whitespace: true,
    137                             ..Suggestion::default()
    138                         });
    139                     }
    140                 }
    141             }
    142         }
    143         // Suggest Short & Long Flags
    144         else if last_word.starts_with("-") {
    145             for arg in active_cmd.get_arguments() {
    146                 if let Some(short) = arg.get_short() {
    147                     let flag = format!("-{}", short);
    148                     if flag.starts_with(last_word) {
    149                         suggestions.push(Suggestion {
    150                             value: flag,
    151                             description: arg.get_help().map(|s| s.to_string()),
    152                             span,
    153                             append_whitespace: true,
    154                             ..Suggestion::default()
    155                         });
    156                     }
    157                 }
    158                 if let Some(long) = arg.get_long() {
    159                     let flag = format!("--{}", long);
    160                     if flag.starts_with(last_word) {
    161                         suggestions.push(Suggestion {
    162                             value: flag,
    163                             description: arg.get_help().map(|s| s.to_string()),
    164                             span,
    165                             append_whitespace: true,
    166                             ..Suggestion::default()
    167                         });
    168                     }
    169                 }
    170             }
    171         }
    172         // Suggest Subcommands
    173         else {
    174             for subcmd in active_cmd.get_subcommands() {
    175                 if subcmd.get_name().starts_with(last_word) {
    176                     suggestions.push(Suggestion {
    177                         value: subcmd.get_name().to_string(),
    178                         description: subcmd.get_about().map(|s| s.to_string()),
    179                         span,
    180                         append_whitespace: true,
    181                         ..Suggestion::default()
    182                     });
    183                 }
    184             }
    185         }
    186 
    187         suggestions
    188     }
    189 }
    190 
    191 struct ReplPrompt<'a>(&'a str, &'a str);
    192 
    193 impl<'a> reedline::Prompt for ReplPrompt<'a> {
    194     fn render_prompt_left(&self) -> Cow<'_, str> {
    195         Cow::Borrowed(self.0)
    196     }
    197 
    198     fn render_prompt_right(&self) -> Cow<'_, str> {
    199         Cow::Borrowed(self.1)
    200     }
    201 
    202     fn render_prompt_indicator(&self, _: reedline::PromptEditMode) -> Cow<'_, str> {
    203         Cow::Borrowed(">")
    204     }
    205 
    206     fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
    207         Cow::Borrowed(":")
    208     }
    209 
    210     fn render_prompt_history_search_indicator(
    211         &self,
    212         _: reedline::PromptHistorySearch,
    213     ) -> Cow<'_, str> {
    214         Cow::Borrowed(">")
    215     }
    216 }