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 }