• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Blightmud / Blightmud / 22914577116

10 Mar 2026 05:04PM UTC coverage: 73.711% (+0.2%) from 73.55%
22914577116

push

github

web-flow
Enables persisting of the last command in prompt (#1378)

* Enables persisting of the last command in prompt

The last command sent will be persisted in the prompt but highlighted
with blue background. If you hit enter the same command will be sent
againt. If you start typing the prompt will clear.
If you press 'Left' the last command will become the current prompt and
you can append to it.

* Adds some unit tests for 'last command'

* Updates the helpfile

* Cleans up debug logging

* Clear last command on Bkspc

* Tab starts editing last command.

128 of 153 new or added lines in 5 files covered. (83.66%)

1 existing line in 1 file now uncovered.

9710 of 13173 relevant lines covered (73.71%)

408.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.69
/src/ui/command.rs
1
use crate::event::QuitMethod;
2
use crate::model::{Completions, Line, PromptMask, Servers};
3
use crate::{event::Event, tts::TTSController};
4
use crate::{lua::LuaScript, lua::UiEvent, session::Session, SaveData};
5
use log::debug;
6
use rs_complete::CompletionTree;
7
use std::collections::HashSet;
8
use std::{
9
    io::stdin,
10
    sync::{mpsc::Sender, Arc, Mutex},
11
};
12
use std::{mem, thread};
13
use termion::{event::Key, input::TermRead};
14

15
#[derive(Default)]
16
struct CompletionStepData {
17
    options: Vec<String>,
18
    index: usize,
19
    base: String,
20
}
21

22
impl CompletionStepData {
23
    fn is_empty(&self) -> bool {
4✔
24
        self.options.is_empty()
4✔
25
    }
4✔
26

27
    fn set_options(&mut self, base: &str, options: Vec<String>) {
1✔
28
        self.options = options;
1✔
29
        self.base = base.to_string();
1✔
30
    }
1✔
31

32
    fn clear(&mut self) {
264✔
33
        self.options.clear();
264✔
34
        self.index = 0;
264✔
35
    }
264✔
36

37
    fn next(&mut self) -> Option<&String> {
2✔
38
        if !self.is_empty() {
2✔
39
            let last_index = self.index;
2✔
40
            self.index = (self.index + 1) % (self.options.len() + 1);
2✔
41
            self.options.get(last_index).or(Some(&self.base))
2✔
42
        } else {
43
            None
×
44
        }
45
    }
2✔
46
}
47

48
pub struct CommandBuffer {
49
    buffer: Vec<char>,
50
    last_buffer: Vec<char>,
51
    last_command_enabled: bool,
52
    cursor_pos: usize,
53
    completion_tree: CompletionTree,
54
    completion: CompletionStepData,
55
    prompt_mask: PromptMask,
56
    script: Arc<Mutex<LuaScript>>,
57
    tts_ctrl: Arc<Mutex<TTSController>>,
58
}
59

60
impl CommandBuffer {
61
    pub fn new(
166✔
62
        tts_ctrl: Arc<Mutex<TTSController>>,
166✔
63
        script: Arc<Mutex<LuaScript>>,
166✔
64
        last_command_enabled: bool,
166✔
65
    ) -> Self {
166✔
66
        let mut completion = CompletionTree::with_inclusions(&['/', '_']);
166✔
67
        completion.set_min_word_len(3);
166✔
68

69
        Self {
166✔
70
            buffer: vec![],
166✔
71
            last_buffer: vec![],
166✔
72
            last_command_enabled,
166✔
73
            cursor_pos: 0,
166✔
74
            completion_tree: completion,
166✔
75
            completion: CompletionStepData::default(),
166✔
76
            prompt_mask: PromptMask::new(),
166✔
77
            script,
166✔
78
            tts_ctrl,
166✔
79
        }
166✔
80
    }
166✔
81

82
    pub fn get_buffer(&mut self) -> String {
42✔
83
        self.buffer.iter().collect::<String>()
42✔
84
    }
42✔
85

86
    pub fn get_masked_buffer(&self) -> String {
×
87
        self.prompt_mask.mask_buffer(&self.buffer)
×
88
    }
×
89

90
    pub fn get_mask(&self) -> &PromptMask {
×
91
        &self.prompt_mask
×
92
    }
×
93

94
    pub fn get_last_command_mask(&self) -> PromptMask {
1✔
95
        let mut mask = PromptMask::new();
1✔
96
        mask.insert(0, "\x1b[44m".to_string()); // BG blue
1✔
97
        mask.insert(self.last_buffer.len() as i32, "\x1b[0m".to_string());
1✔
98
        mask
1✔
99
    }
1✔
100

101
    pub fn enable_last_command(&mut self, enabled: bool) {
2✔
102
        self.last_command_enabled = enabled;
2✔
103
        if !enabled {
2✔
104
            self.last_buffer.clear();
1✔
105
        }
1✔
106
    }
2✔
107

108
    pub fn get_pos(&self) -> usize {
27✔
109
        self.cursor_pos
27✔
110
    }
27✔
111

112
    fn submit(&mut self) -> String {
12✔
113
        // If we have no buffer then swap in the last buffer
114
        if self.last_command_enabled && self.buffer.is_empty() {
12✔
115
            mem::swap(&mut self.last_buffer, &mut self.buffer);
1✔
116
        }
11✔
117

118
        // Insert history
119
        let cmd = if !self.buffer.is_empty() {
12✔
120
            let command = self.get_buffer();
11✔
121
            self.completion_tree.insert(&command);
11✔
122
            command
11✔
123
        } else {
124
            String::new()
1✔
125
        };
126

127
        if self.last_command_enabled {
12✔
128
            // Clear the last buffer and swap them
8✔
129
            self.last_buffer.clear();
8✔
130
            mem::swap(&mut self.last_buffer, &mut self.buffer);
8✔
131
        } else {
8✔
132
            self.buffer.clear();
4✔
133
        }
4✔
134

135
        self.clear_mask();
12✔
136
        self.cursor_pos = 0;
12✔
137

138
        cmd
12✔
139
    }
12✔
140

141
    fn step_left(&mut self) {
10✔
142
        if self.cursor_pos > 0 {
10✔
143
            self.cursor_pos -= 1;
9✔
144
        }
9✔
145
    }
10✔
146

147
    fn step_right(&mut self) {
266✔
148
        // Actualize the last buffer if the current is empty
149
        if self.buffer.is_empty() {
266✔
150
            mem::swap(&mut self.last_buffer, &mut self.buffer);
2✔
151
            self.cursor_pos = self.buffer.len();
2✔
152
        } else if self.cursor_pos < self.buffer.len() {
264✔
153
            self.cursor_pos += 1;
264✔
154
        }
264✔
155
    }
266✔
156

157
    fn move_to_start(&mut self) {
8✔
158
        self.cursor_pos = 0;
8✔
159
    }
8✔
160

161
    fn move_to_end(&mut self) {
5✔
162
        self.cursor_pos = self.buffer.len();
5✔
163
    }
5✔
164

165
    fn step_word_right(&mut self) {
13✔
166
        let origin = (self.cursor_pos + 1).min(self.buffer.len());
13✔
167
        self.cursor_pos = if let Some(pos) = self.buffer[origin..].iter().position(|c| *c == ' ') {
50✔
168
            origin + pos
9✔
169
        } else {
170
            self.buffer.len()
4✔
171
        }
172
    }
13✔
173

174
    fn step_word_left(&mut self) {
8✔
175
        let origin = self.cursor_pos.max(1) - 1;
8✔
176
        self.cursor_pos = if let Some(pos) = self.buffer[0..origin].iter().rposition(|c| *c == ' ')
24✔
177
        {
178
            pos + 1
3✔
179
        } else {
180
            0
5✔
181
        }
182
    }
8✔
183

184
    fn delete_to_end(&mut self) {
2✔
185
        self.buffer.drain(self.cursor_pos..self.buffer.len());
2✔
186
        self.clear_mask();
2✔
187
    }
2✔
188

189
    fn delete_from_start(&mut self) {
2✔
190
        self.buffer.drain(0..self.cursor_pos);
2✔
191
        self.cursor_pos = 0;
2✔
192
    }
2✔
193

194
    fn delete_right(&mut self) {
4✔
195
        if self.cursor_pos < self.buffer.len() {
4✔
196
            self.buffer.remove(self.cursor_pos);
2✔
197
            self.clear_mask();
2✔
198
        }
2✔
199
    }
4✔
200

201
    fn delete_word_right(&mut self) {
3✔
202
        let origin = self.cursor_pos;
3✔
203
        self.step_word_right();
3✔
204
        if origin != self.cursor_pos {
3✔
205
            self.buffer.drain(origin..self.cursor_pos);
2✔
206
            self.clear_mask();
2✔
207
            self.cursor_pos = origin;
2✔
208
        }
2✔
209
    }
3✔
210

211
    fn delete_word_left(&mut self) {
3✔
212
        let origin = self.cursor_pos;
3✔
213
        self.step_word_left();
3✔
214
        if origin != self.cursor_pos {
3✔
215
            self.buffer.drain(self.cursor_pos..origin);
2✔
216
            self.clear_mask();
2✔
217
        }
2✔
218
    }
3✔
219

220
    fn remove(&mut self) -> Option<char> {
6✔
221
        if self.cursor_pos > 0 {
6✔
222
            let removed = if self.cursor_pos < self.buffer.len() {
4✔
223
                Some(self.buffer.remove(self.cursor_pos - 1))
4✔
224
            } else {
225
                self.buffer.pop()
×
226
            };
227
            self.clear_mask();
4✔
228
            self.step_left();
4✔
229
            removed
4✔
230
        } else {
231
            None
2✔
232
        }
233
    }
6✔
234

235
    fn push_key(&mut self, c: char) {
264✔
236
        if self.cursor_pos >= self.buffer.len() {
264✔
237
            self.buffer.push(c);
255✔
238
        } else {
255✔
239
            self.buffer.insert(self.cursor_pos, c);
9✔
240
        }
9✔
241
        self.clear_mask();
264✔
242
        self.completion.clear();
264✔
243
        self.step_right();
264✔
244
    }
264✔
245

246
    fn tab_complete(&mut self) {
2✔
247
        if self.buffer.is_empty() {
2✔
NEW
248
            // Actualize the last buffer if the current is empty
×
NEW
249
            mem::swap(&mut self.last_buffer, &mut self.buffer);
×
NEW
250
            self.cursor_pos = self.buffer.len();
×
251
        } else if self.buffer.len() > 1 {
2✔
252
            // Otherwise attempt a tab-complete
253
            if self.completion.is_empty() {
2✔
254
                let mut completions = Completions::default();
1✔
255
                let strbuf = self.get_buffer();
1✔
256
                completions.merge(self.script.lock().unwrap().tab_complete(&strbuf));
1✔
257
                if let Some(mut options) = self.completion_tree.complete(&strbuf) {
1✔
258
                    completions.add_all(&mut options);
1✔
259
                }
1✔
260

261
                // Remove duplicates but preserve order of occurence
262
                let mut occurences: HashSet<&String> = HashSet::new();
1✔
263
                let completions = completions.iter().fold(vec![], |mut acc, word| {
1✔
264
                    if !occurences.contains(word) {
1✔
265
                        acc.push(word.clone());
1✔
266
                    }
1✔
267
                    occurences.insert(word);
1✔
268
                    acc
1✔
269
                });
1✔
270

271
                self.completion.set_options(&strbuf, completions);
1✔
272
            }
1✔
273
            if let Some(comp) = self.completion.next() {
2✔
274
                self.tts_ctrl.lock().unwrap().speak(comp, true);
2✔
275
                self.buffer = comp.chars().collect();
2✔
276
                self.clear_mask();
2✔
277
                self.cursor_pos = self.buffer.len();
2✔
278
            }
2✔
279
        }
×
280
    }
2✔
281

282
    pub fn clear(&mut self) {
1✔
283
        self.buffer.clear();
1✔
284
        self.clear_mask();
1✔
285
        self.cursor_pos = self.buffer.len();
1✔
286
    }
1✔
287

288
    pub fn set(&mut self, line: String) {
×
289
        self.buffer = line.chars().collect();
×
290
        self.clear_mask();
×
291
        self.cursor_pos = self.buffer.len();
×
292
    }
×
293

294
    pub fn set_pos(&mut self, pos: usize) {
3✔
295
        self.cursor_pos = pos.min(self.buffer.len());
3✔
296
    }
3✔
297

298
    pub fn set_mask(&mut self, mask: PromptMask) -> &PromptMask {
×
299
        self.prompt_mask += mask;
×
300
        &self.prompt_mask
×
301
    }
×
302

303
    pub fn clear_mask(&mut self) {
291✔
304
        self.prompt_mask.clear();
291✔
305
    }
291✔
306
}
307

308
fn parse_mouse_event(event: termion::event::MouseEvent, writer: &Sender<Event>) {
×
309
    use termion::event::{MouseButton, MouseEvent};
310
    match event {
×
311
        MouseEvent::Press(MouseButton::WheelUp, ..) => writer.send(Event::ScrollUp).unwrap(),
×
312
        MouseEvent::Press(MouseButton::WheelDown, ..) => writer.send(Event::ScrollDown).unwrap(),
×
313
        _ => {}
×
314
    }
315
}
×
316

317
fn parse_key_event(
×
318
    key: termion::event::Key,
×
319
    buffer: &mut CommandBuffer,
×
320
    writer: &Sender<Event>,
×
321
    tts_ctrl: &mut Arc<Mutex<TTSController>>,
×
322
    script: &mut Arc<Mutex<LuaScript>>,
×
323
) {
×
324
    match key {
×
325
        Key::Char('\n') => {
326
            let mut line = Line::from(buffer.submit());
×
327
            line.flags.source = Some("user".to_string());
×
328
            writer.send(Event::ServerInput(line)).unwrap();
×
329
            if let Ok(mut script) = script.lock() {
×
330
                script.set_prompt_content(String::new(), 0);
×
331
            }
×
332
        }
333
        Key::Char('\t') => buffer.tab_complete(),
×
334
        Key::Char(c) => {
×
335
            tts_ctrl.lock().unwrap().key_press(c);
×
336
            buffer.push_key(c);
×
337
            if let Ok(mut script) = script.lock() {
×
338
                script.set_prompt_content(buffer.get_buffer(), buffer.get_pos());
×
339
            }
×
NEW
340
            buffer.last_buffer.clear();
×
341
        }
342
        Key::Ctrl('l') => writer.send(Event::Redraw).unwrap(),
×
343
        Key::Ctrl('c') => {
×
344
            writer.send(Event::Quit(QuitMethod::CtrlC)).unwrap();
×
345
        }
×
346

347
        // Input navigation
348
        Key::Left => buffer.step_left(),
×
349
        Key::Right => buffer.step_right(),
×
350
        Key::Backspace => {
351
            if let Some(c) = buffer.remove() {
×
352
                if let Ok(mut tts_ctrl) = tts_ctrl.lock() {
×
353
                    tts_ctrl.key_press(c);
×
354
                }
×
355
            }
×
356
            if let Ok(mut script) = script.lock() {
×
357
                script.set_prompt_content(buffer.get_buffer(), buffer.get_pos());
×
358
            }
×
NEW
359
            buffer.last_buffer.clear();
×
360
        }
361
        Key::Delete => buffer.delete_right(),
×
362
        _ => {}
×
363
    };
364
}
×
365

366
fn check_command_binds(
16✔
367
    cmd: termion::event::Key,
16✔
368
    buffer: &mut CommandBuffer,
16✔
369
    script: &Arc<Mutex<LuaScript>>,
16✔
370
    writer: &Sender<Event>,
16✔
371
) -> bool {
16✔
372
    let mut ran = false;
16✔
373
    if let Ok(mut script) = script.lock() {
16✔
374
        ran = match cmd {
16✔
375
            Key::Ctrl(c) => script.check_bindings(&human_key("ctrl-", c)),
8✔
376
            Key::CtrlUp => script.check_bindings("ctrl-up"),
×
377
            Key::CtrlDown => script.check_bindings("ctrl-down"),
×
378
            Key::CtrlLeft => script.check_bindings("ctrl-left"),
×
379
            Key::CtrlRight => script.check_bindings("ctrl-right"),
×
380
            Key::AltUp => script.check_bindings("alt-up"),
×
381
            Key::AltDown => script.check_bindings("alt-down"),
×
382
            Key::AltLeft => script.check_bindings("alt-left"),
×
383
            Key::AltRight => script.check_bindings("alt-right"),
×
384
            Key::ShiftUp => script.check_bindings("shift-up"),
×
385
            Key::ShiftDown => script.check_bindings("shift-down"),
×
386
            Key::ShiftLeft => script.check_bindings("shift-left"),
×
387
            Key::ShiftRight => script.check_bindings("shift-right"),
×
388
            Key::Alt(c) => script.check_bindings(&human_key("alt-", c)),
4✔
389
            Key::F(n) => script.check_bindings(&format!("f{n}")),
×
390
            Key::PageUp => script.check_bindings("pageup") || script.check_bindings("page up"),
1✔
391
            Key::PageDown => {
392
                script.check_bindings("pagedown") || script.check_bindings("page down")
1✔
393
            }
394
            Key::Home => script.check_bindings("home"),
1✔
395
            Key::End => script.check_bindings("end"),
1✔
396
            Key::Up => script.check_bindings("up"),
×
397
            Key::Down => script.check_bindings("down"),
×
398
            _ => false,
×
399
        }
400
    }
×
401
    handle_script_ui_io(buffer, script, writer);
16✔
402
    ran
16✔
403
}
16✔
404

405
/// Convert a key combination to a human-readable form.
406
fn human_key(prefix: &str, c: char) -> String {
18✔
407
    let mut out = prefix.to_owned();
18✔
408
    match c {
18✔
409
        '\u{7f}' => out.push_str("backspace"),
3✔
410
        '\u{1b}' => out.push_str("escape"),
2✔
411
        _ => out.push(c),
13✔
412
    }
413
    out
18✔
414
}
18✔
415

416
fn check_escape_bindings(
×
417
    escape: &str,
×
418
    buffer: &mut CommandBuffer,
×
419
    script: &Arc<Mutex<LuaScript>>,
×
420
    writer: &Sender<Event>,
×
421
) {
×
422
    if let Ok(mut script) = script.lock() {
×
423
        if !script.check_bindings(&escape.to_lowercase()) {
×
424
            writer
×
425
                .send(Event::Info(format!("Unknown command: {escape:?}")))
×
426
                .unwrap();
×
427
        }
×
428
    }
×
429
    handle_script_ui_io(buffer, script, writer);
×
430
    writer
×
431
        .send(Event::UserInputBuffer(
×
432
            buffer.get_buffer(),
×
433
            buffer.get_pos(),
×
434
        ))
×
435
        .unwrap();
×
436
}
×
437

438
fn handle_script_ui_io(
16✔
439
    buffer: &mut CommandBuffer,
16✔
440
    script: &Arc<Mutex<LuaScript>>,
16✔
441
    writer: &Sender<Event>,
16✔
442
) {
16✔
443
    if let Ok(mut script) = script.lock() {
16✔
444
        script.get_ui_events().iter().for_each(|event| match event {
16✔
445
            UiEvent::StepLeft => buffer.step_left(),
1✔
446
            UiEvent::StepRight => buffer.step_right(),
1✔
447
            UiEvent::StepToStart => buffer.move_to_start(),
1✔
448
            UiEvent::StepToEnd => buffer.move_to_end(),
1✔
449
            UiEvent::StepWordLeft => buffer.step_word_left(),
1✔
450
            UiEvent::StepWordRight => buffer.step_word_right(),
1✔
451
            UiEvent::Remove => {
1✔
452
                buffer.remove();
1✔
453
            }
1✔
454
            UiEvent::DeleteToEnd => buffer.delete_to_end(),
1✔
455
            UiEvent::DeleteFromStart => buffer.delete_from_start(),
1✔
456
            UiEvent::DeleteWordLeft => buffer.delete_word_left(),
1✔
457
            UiEvent::DeleteWordRight => buffer.delete_word_right(),
1✔
458
            UiEvent::DeleteRight => buffer.delete_right(),
1✔
459
            UiEvent::ScrollDown => writer.send(Event::ScrollDown).unwrap(),
1✔
460
            UiEvent::ScrollUp => writer.send(Event::ScrollUp).unwrap(),
1✔
461
            UiEvent::ScrollTop => writer.send(Event::ScrollTop).unwrap(),
1✔
462
            UiEvent::ScrollBottom => writer.send(Event::ScrollBottom).unwrap(),
1✔
463
            UiEvent::Complete => buffer.tab_complete(),
×
464
            UiEvent::Unknown(_) => {}
×
465
        });
16✔
466
        script.set_prompt_content(buffer.get_buffer(), buffer.get_pos());
16✔
467
        script.get_output_lines().iter().for_each(|l| {
16✔
468
            writer.send(Event::Output(Line::from(l))).unwrap();
×
469
        });
×
470
    }
×
471
}
16✔
472

473
pub fn spawn_input_thread(session: Session) -> thread::JoinHandle<()> {
128✔
474
    thread::Builder::new()
128✔
475
        .name("input-thread".to_string())
128✔
476
        .spawn(move || {
128✔
477
            debug!("Input stream spawned");
128✔
478
            let writer = session.main_writer.clone();
128✔
479
            let mut script = session.lua_script.clone();
128✔
480
            let stdin = stdin();
128✔
481
            let buffer = session.command_buffer.clone();
128✔
482
            let mut tts_ctrl = session.tts_ctrl;
128✔
483

484
            if let Ok(mut buffer) = buffer.lock() {
128✔
485
                for server in Servers::load().keys() {
128✔
486
                    buffer.completion_tree.insert(server);
8✔
487
                }
8✔
488
                buffer
128✔
489
                    .completion_tree
128✔
490
                    .insert(include_str!("../../resources/completions.txt"));
128✔
491
            }
×
492

493
            for e in stdin.events() {
128✔
494
                match e.unwrap() {
×
495
                    termion::event::Event::Key(key) => {
×
496
                        if let Ok(mut buffer) = buffer.lock() {
×
497
                            let orig_pos = buffer.get_pos();
×
498
                            let orig_len = buffer.buffer.len();
×
499
                            let bind_ran = check_command_binds(key, &mut buffer, &script, &writer);
×
500
                            if !bind_ran {
×
501
                                parse_key_event(
×
502
                                    key,
×
503
                                    &mut buffer,
×
504
                                    &writer,
×
505
                                    &mut tts_ctrl,
×
506
                                    &mut script,
×
507
                                );
×
508
                            }
×
509
                            if orig_len == buffer.buffer.len() && orig_pos != buffer.get_pos() {
×
510
                                writer
×
511
                                    .send(Event::UserInputCursor(buffer.get_pos()))
×
512
                                    .unwrap();
×
513
                            } else if !bind_ran || orig_len != buffer.buffer.len() {
×
514
                                if let Ok(mut luascript) = script.lock() {
×
515
                                    luascript.set_prompt_mask_content(&buffer.prompt_mask);
×
516
                                    luascript
×
517
                                        .set_prompt_content(buffer.get_buffer(), buffer.get_pos());
×
518
                                }
×
519

520
                                // If 'last command' is applicable then render it.
NEW
521
                                if buffer.buffer.is_empty() && !buffer.last_buffer.is_empty() {
×
NEW
522
                                    let mask = buffer.get_last_command_mask();
×
NEW
523
                                    writer
×
NEW
524
                                        .send(Event::UserInputBuffer(
×
NEW
525
                                            mask.mask_buffer(&buffer.last_buffer),
×
NEW
526
                                            0,
×
NEW
527
                                        ))
×
NEW
528
                                        .unwrap();
×
NEW
529
                                } else {
×
NEW
530
                                    writer
×
NEW
531
                                        .send(Event::UserInputBuffer(
×
NEW
532
                                            buffer.get_buffer(),
×
NEW
533
                                            buffer.get_pos(),
×
NEW
534
                                        ))
×
NEW
535
                                        .unwrap();
×
NEW
536
                                }
×
537
                            }
×
538
                        }
×
539
                    }
540
                    termion::event::Event::Mouse(event) => parse_mouse_event(event, &writer),
×
541
                    termion::event::Event::Unsupported(bytes) => {
×
542
                        if let Ok(escape) = String::from_utf8(bytes.clone()) {
×
543
                            if let Ok(mut buffer) = buffer.lock() {
×
544
                                check_escape_bindings(
×
545
                                    &escape.to_lowercase(),
×
546
                                    &mut buffer,
×
547
                                    &script,
×
548
                                    &writer,
×
549
                                );
×
550
                            }
×
551
                        } else {
×
552
                            writer
×
553
                                .send(Event::Info(format!("Unknown command: {bytes:?}")))
×
554
                                .unwrap();
×
555
                        }
×
556
                    }
557
                }
558
            }
559
            debug!("Input stream closing");
128✔
560
        })
128✔
561
        .unwrap()
128✔
562
}
128✔
563

564
#[cfg(test)]
565
mod command_test {
566

567
    use std::sync::mpsc::{channel, Receiver, Sender};
568
    use std::sync::{Arc, Mutex};
569

570
    use termion::event::Key;
571

572
    use super::check_command_binds;
573
    use super::CommandBuffer;
574
    use crate::lua::LuaScriptBuilder;
575
    use crate::tts::TTSController;
576
    use crate::Event;
577

578
    fn push_string(buffer: &mut CommandBuffer, msg: &str) {
24✔
579
        msg.chars().for_each(|c| buffer.push_key(c));
263✔
580
    }
24✔
581

582
    fn get_command() -> (CommandBuffer, Receiver<Event>) {
16✔
583
        let (tx, rx): (Sender<Event>, Receiver<Event>) = channel();
16✔
584
        let buffer = CommandBuffer::new(
16✔
585
            Arc::new(Mutex::new(TTSController::new(false, true))),
16✔
586
            Arc::new(Mutex::new(
16✔
587
                LuaScriptBuilder::new(tx).dimensions((100, 100)).build(),
16✔
588
            )),
589
            false,
590
        );
591
        (buffer, rx)
16✔
592
    }
16✔
593

594
    #[test]
595
    fn test_editing() {
1✔
596
        let mut buffer = get_command().0;
1✔
597

598
        push_string(&mut buffer, "test is test");
1✔
599
        assert_eq!(buffer.get_buffer(), "test is test");
1✔
600
        assert_eq!(buffer.get_pos(), 12);
1✔
601
        buffer.step_left();
1✔
602
        buffer.step_left();
1✔
603
        buffer.step_left();
1✔
604
        buffer.step_left();
1✔
605
        buffer.remove();
1✔
606
        buffer.remove();
1✔
607
        buffer.remove();
1✔
608
        buffer.remove();
1✔
609
        assert_eq!(buffer.get_buffer(), "testtest");
1✔
610
        assert_eq!(buffer.get_pos(), 4);
1✔
611
        push_string(&mut buffer, " confirm ");
1✔
612
        assert_eq!(buffer.get_buffer(), "test confirm test");
1✔
613
        assert_eq!(buffer.get_pos(), 13);
1✔
614
    }
1✔
615

616
    #[test]
617
    fn test_no_zero_index_remove_crash() {
1✔
618
        let mut buffer = get_command().0;
1✔
619
        buffer.push_key('t');
1✔
620
        buffer.step_left();
1✔
621
        assert_eq!(buffer.get_pos(), 0);
1✔
622
        buffer.remove();
1✔
623
        assert_eq!(buffer.get_pos(), 0);
1✔
624
    }
1✔
625

626
    #[test]
627
    fn test_input_navigation() {
1✔
628
        let mut buffer = get_command().0;
1✔
629
        push_string(&mut buffer, "some random words");
1✔
630
        buffer.step_word_left();
1✔
631
        assert_eq!(buffer.cursor_pos, 12);
1✔
632
        buffer.step_word_left();
1✔
633
        assert_eq!(buffer.cursor_pos, 5);
1✔
634
        buffer.step_word_left();
1✔
635
        assert_eq!(buffer.cursor_pos, 0);
1✔
636
        buffer.step_word_left();
1✔
637
        assert_eq!(buffer.cursor_pos, 0);
1✔
638
        buffer.step_word_right();
1✔
639
        assert_eq!(buffer.cursor_pos, 4);
1✔
640
        buffer.step_word_right();
1✔
641
        assert_eq!(buffer.cursor_pos, 11);
1✔
642
        buffer.step_word_right();
1✔
643
        assert_eq!(buffer.cursor_pos, 17);
1✔
644
        buffer.step_word_right();
1✔
645
        assert_eq!(buffer.cursor_pos, 17);
1✔
646
    }
1✔
647

648
    #[test]
649
    fn test_end_start_navigation() {
1✔
650
        let mut buffer = get_command().0;
1✔
651
        push_string(&mut buffer, "some random words");
1✔
652
        buffer.move_to_start();
1✔
653
        assert_eq!(buffer.cursor_pos, 0);
1✔
654
        buffer.move_to_start();
1✔
655
        assert_eq!(buffer.cursor_pos, 0);
1✔
656
        buffer.move_to_end();
1✔
657
        assert_eq!(buffer.cursor_pos, 17);
1✔
658
        buffer.move_to_end();
1✔
659
        assert_eq!(buffer.cursor_pos, 17);
1✔
660
    }
1✔
661

662
    #[test]
663
    fn test_delete_rest_of_line() {
1✔
664
        let mut buffer = get_command().0;
1✔
665
        push_string(&mut buffer, "some random words");
1✔
666
        buffer.move_to_start();
1✔
667
        buffer.step_word_right();
1✔
668
        buffer.delete_from_start();
1✔
669
        assert_eq!(buffer.get_buffer(), " random words");
1✔
670
    }
1✔
671

672
    #[test]
673
    fn test_delete_from_start_of_line() {
1✔
674
        let mut buffer = get_command().0;
1✔
675
        push_string(&mut buffer, "some random words");
1✔
676
        buffer.move_to_start();
1✔
677
        buffer.step_word_right();
1✔
678
        buffer.step_word_right();
1✔
679
        buffer.delete_to_end();
1✔
680
        assert_eq!(buffer.get_buffer(), "some random");
1✔
681
    }
1✔
682

683
    #[test]
684
    fn test_delete_right() {
1✔
685
        let mut buffer = get_command().0;
1✔
686
        push_string(&mut buffer, "some random words");
1✔
687
        buffer.move_to_start();
1✔
688
        buffer.step_word_right();
1✔
689
        buffer.delete_right();
1✔
690
        assert_eq!(buffer.get_buffer(), "somerandom words");
1✔
691
        buffer.delete_right();
1✔
692
        assert_eq!(buffer.get_buffer(), "someandom words");
1✔
693
        buffer.move_to_end();
1✔
694
        buffer.delete_right();
1✔
695
        assert_eq!(buffer.get_buffer(), "someandom words");
1✔
696
    }
1✔
697

698
    #[test]
699
    fn test_delete_word_left() {
1✔
700
        let mut buffer = get_command().0;
1✔
701
        push_string(&mut buffer, "some random words");
1✔
702
        buffer.move_to_end();
1✔
703
        buffer.delete_word_left();
1✔
704
        assert_eq!(buffer.get_buffer(), "some random ");
1✔
705
        buffer.move_to_start();
1✔
706
        buffer.step_word_right();
1✔
707
        buffer.delete_word_left();
1✔
708
        assert_eq!(buffer.get_buffer(), " random ");
1✔
709
    }
1✔
710

711
    #[test]
712
    fn test_delete_word_right() {
1✔
713
        let mut buffer = get_command().0;
1✔
714
        push_string(&mut buffer, "some random words");
1✔
715
        buffer.move_to_start();
1✔
716
        buffer.delete_word_right();
1✔
717
        assert_eq!(buffer.get_buffer(), " random words");
1✔
718
        buffer.delete_word_right();
1✔
719
        assert_eq!(buffer.get_buffer(), " words");
1✔
720
    }
1✔
721

722
    #[test]
723
    fn test_fancy_chars() {
1✔
724
        let mut buffer = get_command().0;
1✔
725
        let input = "some weird chars: ÅÖÄø æĸœ→ €ßðߪ“";
1✔
726
        push_string(&mut buffer, input);
1✔
727
        assert_eq!(input.chars().count(), buffer.buffer.len());
1✔
728
        assert_ne!(input.len(), buffer.buffer.len());
1✔
729
        assert_eq!(buffer.get_buffer().len(), input.len());
1✔
730
    }
1✔
731

732
    #[test]
733
    fn test_human_key() {
1✔
734
        use super::human_key;
735

736
        assert_eq!(human_key("alt-", '\u{7f}'), "alt-backspace");
1✔
737
        assert_eq!(human_key("ctrl-", '\u{7f}'), "ctrl-backspace");
1✔
738
        assert_eq!(human_key("alt-", '\u{1b}'), "alt-escape");
1✔
739
        assert_eq!(human_key("ctrl-", '\u{1b}'), "ctrl-escape");
1✔
740
        assert_eq!(human_key("ctrl-", 'd'), "ctrl-d");
1✔
741
        assert_eq!(human_key("f", 'x'), "fx");
1✔
742
    }
1✔
743

744
    #[test]
745
    fn test_completions() {
1✔
746
        let mut buffer = get_command().0;
1✔
747
        push_string(&mut buffer, "batman");
1✔
748
        buffer.submit();
1✔
749
        push_string(&mut buffer, "bat");
1✔
750
        buffer.tab_complete();
1✔
751
        assert_eq!(buffer.completion.options, vec!["batman".to_string()]);
1✔
752
    }
1✔
753

754
    #[test]
755
    fn test_completion_with_big_chars() {
1✔
756
        // Issue #522
757
        let mut buffer = get_command().0;
1✔
758
        push_string(&mut buffer, "fend");
1✔
759
        buffer.completion.options = vec!["fender🎸".to_string()];
1✔
760
        buffer.tab_complete();
1✔
761
        assert_eq!(buffer.completion.options, vec!["fender🎸".to_string()]);
1✔
762
        assert_eq!(buffer.buffer, vec!['f', 'e', 'n', 'd', 'e', 'r', '🎸']);
1✔
763
        assert_eq!(buffer.cursor_pos, 7);
1✔
764
    }
1✔
765

766
    #[test]
767
    fn test_pos_cursor() {
1✔
768
        let mut buffer = get_command().0;
1✔
769
        let input = "Gibson Les Paul";
1✔
770
        push_string(&mut buffer, input);
1✔
771
        assert_eq!(buffer.get_pos(), input.len());
1✔
772
        buffer.set_pos(1000);
1✔
773
        assert_eq!(buffer.get_pos(), input.len());
1✔
774
        buffer.set_pos(0);
1✔
775
        assert_eq!(buffer.get_pos(), 0);
1✔
776
        buffer.set_pos(2);
1✔
777
        assert_eq!(buffer.get_pos(), 2);
1✔
778
        buffer.clear();
1✔
779
        assert_eq!(buffer.get_pos(), 0);
1✔
780
        push_string(&mut buffer, input);
1✔
781
        assert_eq!(buffer.get_pos(), input.len());
1✔
782
    }
1✔
783

784
    #[test]
785
    fn test_lua_key_binds() {
1✔
786
        let tts = Arc::new(Mutex::new(TTSController::new(false, false)));
1✔
787

788
        let (tx, _rx): (Sender<Event>, Receiver<Event>) = channel();
1✔
789
        let script = Arc::new(Mutex::new(
1✔
790
            LuaScriptBuilder::new(tx.clone())
1✔
791
                .dimensions((100, 100))
1✔
792
                .build(),
1✔
793
        ));
794
        let mut buffer = CommandBuffer::new(tts, script.clone(), false);
1✔
795

796
        assert!(check_command_binds(
1✔
797
            Key::Alt('b'),
1✔
798
            &mut buffer,
1✔
799
            &script,
1✔
800
            &tx
1✔
801
        ));
802
        assert!(check_command_binds(
1✔
803
            Key::Alt('f'),
1✔
804
            &mut buffer,
1✔
805
            &script,
1✔
806
            &tx
1✔
807
        ));
808
        assert!(check_command_binds(
1✔
809
            Key::Alt('d'),
1✔
810
            &mut buffer,
1✔
811
            &script,
1✔
812
            &tx
1✔
813
        ));
814
        assert!(check_command_binds(
1✔
815
            Key::Alt('\u{7f}'),
1✔
816
            &mut buffer,
1✔
817
            &script,
1✔
818
            &tx
1✔
819
        ));
820
        assert!(check_command_binds(
1✔
821
            Key::Ctrl('a'),
1✔
822
            &mut buffer,
1✔
823
            &script,
1✔
824
            &tx
1✔
825
        ));
826
        assert!(check_command_binds(
1✔
827
            Key::Ctrl('b'),
1✔
828
            &mut buffer,
1✔
829
            &script,
1✔
830
            &tx
1✔
831
        ));
832
        assert!(check_command_binds(
1✔
833
            Key::Ctrl('e'),
1✔
834
            &mut buffer,
1✔
835
            &script,
1✔
836
            &tx
1✔
837
        ));
838
        assert!(check_command_binds(
1✔
839
            Key::Ctrl('f'),
1✔
840
            &mut buffer,
1✔
841
            &script,
1✔
842
            &tx
1✔
843
        ));
844
        assert!(check_command_binds(
1✔
845
            Key::Ctrl('d'),
1✔
846
            &mut buffer,
1✔
847
            &script,
1✔
848
            &tx
1✔
849
        ));
850
        assert!(check_command_binds(
1✔
851
            Key::Ctrl('h'),
1✔
852
            &mut buffer,
1✔
853
            &script,
1✔
854
            &tx
1✔
855
        ));
856
        assert!(check_command_binds(
1✔
857
            Key::Ctrl('k'),
1✔
858
            &mut buffer,
1✔
859
            &script,
1✔
860
            &tx
1✔
861
        ));
862
        assert!(check_command_binds(
1✔
863
            Key::Ctrl('u'),
1✔
864
            &mut buffer,
1✔
865
            &script,
1✔
866
            &tx
1✔
867
        ));
868

869
        assert!(check_command_binds(Key::Home, &mut buffer, &script, &tx));
1✔
870
        assert!(check_command_binds(Key::End, &mut buffer, &script, &tx));
1✔
871
        assert!(check_command_binds(Key::PageUp, &mut buffer, &script, &tx));
1✔
872
        assert!(check_command_binds(
1✔
873
            Key::PageDown,
1✔
874
            &mut buffer,
1✔
875
            &script,
1✔
876
            &tx
1✔
877
        ));
878
    }
1✔
879

880
    fn get_command_with_last() -> (CommandBuffer, Receiver<Event>) {
6✔
881
        let (tx, rx): (Sender<Event>, Receiver<Event>) = channel();
6✔
882
        let buffer = CommandBuffer::new(
6✔
883
            Arc::new(Mutex::new(TTSController::new(false, true))),
6✔
884
            Arc::new(Mutex::new(
6✔
885
                LuaScriptBuilder::new(tx).dimensions((100, 100)).build(),
6✔
886
            )),
887
            true,
888
        );
889
        (buffer, rx)
6✔
890
    }
6✔
891

892
    #[test]
893
    fn test_last_command_stored_after_submit() {
1✔
894
        let mut buffer = get_command_with_last().0;
1✔
895
        push_string(&mut buffer, "hello");
1✔
896
        buffer.submit();
1✔
897
        // buffer should be empty, last_buffer should hold the command
898
        assert!(buffer.buffer.is_empty());
1✔
899
        assert_eq!(buffer.last_buffer.iter().collect::<String>(), "hello");
1✔
900
    }
1✔
901

902
    #[test]
903
    fn test_last_command_replayed_on_empty_submit() {
1✔
904
        let mut buffer = get_command_with_last().0;
1✔
905
        push_string(&mut buffer, "hello");
1✔
906
        buffer.submit();
1✔
907
        // submit again with empty buffer — should replay last command
908
        let replayed = buffer.submit();
1✔
909
        assert_eq!(replayed, "hello");
1✔
910
    }
1✔
911

912
    #[test]
913
    fn test_last_command_updates_on_new_submit() {
1✔
914
        let mut buffer = get_command_with_last().0;
1✔
915
        push_string(&mut buffer, "first");
1✔
916
        buffer.submit();
1✔
917
        push_string(&mut buffer, "second");
1✔
918
        buffer.submit();
1✔
919
        assert_eq!(buffer.last_buffer.iter().collect::<String>(), "second");
1✔
920
    }
1✔
921

922
    #[test]
923
    fn test_last_command_disabled_clears_last_buffer() {
1✔
924
        let mut buffer = get_command_with_last().0;
1✔
925
        push_string(&mut buffer, "hello");
1✔
926
        buffer.submit();
1✔
927
        assert!(!buffer.last_buffer.is_empty());
1✔
928
        buffer.enable_last_command(false);
1✔
929
        assert!(buffer.last_buffer.is_empty());
1✔
930
        assert!(!buffer.last_command_enabled);
1✔
931
    }
1✔
932

933
    #[test]
934
    fn test_last_command_enabled_sets_flag() {
1✔
935
        let mut buffer = get_command().0;
1✔
936
        assert!(!buffer.last_command_enabled);
1✔
937
        buffer.enable_last_command(true);
1✔
938
        assert!(buffer.last_command_enabled);
1✔
939
    }
1✔
940

941
    #[test]
942
    fn test_step_right_expands_last_command() {
1✔
943
        let mut buffer = get_command_with_last().0;
1✔
944
        push_string(&mut buffer, "hello");
1✔
945
        buffer.submit();
1✔
946
        assert!(buffer.buffer.is_empty());
1✔
947
        // step_right on empty buffer should swap in last_buffer
948
        buffer.step_right();
1✔
949
        assert_eq!(buffer.get_buffer(), "hello");
1✔
950
        assert_eq!(buffer.cursor_pos, "hello".len());
1✔
951
    }
1✔
952

953
    #[test]
954
    fn test_last_command_mask_covers_full_buffer() {
1✔
955
        let mut buffer = get_command_with_last().0;
1✔
956
        push_string(&mut buffer, "hello");
1✔
957
        buffer.submit();
1✔
958
        let mask = buffer.get_last_command_mask();
1✔
959
        // mask should have entries at 0 (open) and len of last_buffer (close)
960
        assert!(mask.contains_key(&0));
1✔
961
        assert!(mask.contains_key(&(buffer.last_buffer.len() as i32)));
1✔
962
    }
1✔
963

964
    #[test]
965
    fn test_last_command_not_stored_when_disabled() {
1✔
966
        let mut buffer = get_command().0; // last_command_enabled = false
1✔
967
        push_string(&mut buffer, "hello");
1✔
968
        buffer.submit();
1✔
969
        assert!(buffer.last_buffer.is_empty());
1✔
970
        assert!(buffer.buffer.is_empty());
1✔
971
    }
1✔
972

973
    #[test]
974
    fn test_last_command_empty_submit_when_disabled() {
1✔
975
        let mut buffer = get_command().0; // last_command_enabled = false
1✔
976
        push_string(&mut buffer, "hello");
1✔
977
        buffer.submit();
1✔
978
        // empty submit should return empty string, not replay last
979
        let result = buffer.submit();
1✔
980
        assert_eq!(result, String::new());
1✔
981
    }
1✔
982
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc