cvars_console/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::mem;
5
6use cvars::SetGet;
7
8/// Engine-independant parts of the in-game console.
9#[derive(Debug, Clone, Default)]
10pub struct Console {
11    /// The current contents of the prompt.
12    ///
13    /// Should always be kept in sync with what's displayed in the UI.
14    pub prompt: String,
15
16    /// Prompt to restore when using up and down keys. None if we're not currently walking through history.
17    prompt_saved: Option<String>,
18
19    /// Where we are in history when using up and down keys. None if we're not currently walking through history.
20    prompt_history_index: Option<usize>,
21
22    /// Input and output history.
23    ///
24    /// You should prepend input lines with "> " or something similar when displaying them.
25    pub history: Vec<HistoryLine>,
26
27    /// Where we are in the history view when scrolling using page up and down keys.
28    ///
29    /// This index is *one past* the last line to be displayed at the *bottom*
30    /// so that you can use it as the high end of a range.
31    pub history_view_end: usize,
32}
33
34impl Console {
35    /// Create a new console backend.
36    pub fn new() -> Self {
37        Console {
38            prompt: String::new(),
39            prompt_saved: None,
40            prompt_history_index: None,
41            history: Vec::new(),
42            history_view_end: 0,
43        }
44    }
45
46    /// Go back in command history.
47    ///
48    /// Save the prompt so that users can go back in history,
49    /// then come back to present and get what they typed back.
50    pub fn history_back(&mut self) {
51        let search_slice = if let Some(hi) = self.prompt_history_index {
52            &self.history[0..hi]
53        } else {
54            &self.history[..]
55        };
56        if let Some(new_index) = search_slice
57            .iter()
58            .rposition(|hist_line| hist_line.is_input)
59        {
60            self.prompt_history_index = Some(new_index);
61            if self.prompt_saved.is_none() {
62                self.prompt_saved = Some(self.prompt.clone());
63            }
64            self.prompt = self.history[new_index].text.clone();
65        }
66    }
67
68    /// Go forward in command history.
69    ///
70    /// Restore the saved prompt if get to the end.
71    pub fn history_forward(&mut self) {
72        if let Some(index) = self.prompt_history_index {
73            // Start after the current, otherwise we'd immediately find the current, not the next.
74            // It's ok to index 1 past the end.
75            let begin = index + 1;
76            let search_slice = &self.history[begin..];
77            if let Some(local_index) = search_slice.iter().position(|hist_line| hist_line.is_input)
78            {
79                // `position` starts counting from the iterator's start,
80                // not from history's start so we add the found index to what we skipped
81                // instead of using it directly.
82                let new_index = begin + local_index;
83                self.prompt_history_index = Some(new_index);
84                self.prompt = self.history[new_index].text.clone();
85            } else {
86                // We're at the end of history, restore the saved prompt.
87                self.prompt_history_index = None;
88                self.prompt = self.prompt_saved.take().unwrap();
89            }
90        }
91    }
92
93    /// Scroll up in the history view.
94    pub fn history_scroll_up(&mut self, count: usize) {
95        self.history_view_end = self.history_view_end.saturating_sub(count);
96        if self.history_view_end == 0 && !self.history.is_empty() {
97            // Keep at least one line in history when possible
98            // because scrolling up to an empty view looks weird.
99            self.history_view_end = 1;
100        }
101    }
102
103    /// Scroll down in the history view.
104    pub fn history_scroll_down(&mut self, count: usize) {
105        self.history_view_end = (self.history_view_end + count).min(self.history.len());
106    }
107
108    /// The user pressed enter - process the line of text
109    pub fn enter(&mut self, cvars: &mut dyn SetGet) {
110        let cmd = mem::take(&mut self.prompt);
111
112        self.print_input(&cmd);
113
114        // The actual command parsing logic
115        let res = self.execute_command(cvars, &cmd);
116        if let Err(msg) = res {
117            self.print(msg);
118        }
119
120        // Entering a new command resets the user's position in history to the end.
121        self.prompt_history_index = None;
122    }
123
124    /// Parse what the user typed and get or set a cvar
125    fn execute_command(&mut self, cvars: &mut dyn SetGet, cmd: &str) -> Result<(), String> {
126        // Splitting on whitespace also in effect trims leading and trailing whitespace.
127        let mut parts = cmd.split_whitespace();
128
129        let cvar_name = match parts.next() {
130            Some(name) => name,
131            None => return Ok(()),
132        };
133        if cvar_name == "help" || cvar_name == "?" {
134            self.print("Available actions:");
135            self.print("    help                 Print this message");
136            self.print("    <cvar name>          Print the cvar's value");
137            self.print("    <cvar name> <value>  Set the cvar's value");
138            return Ok(());
139        }
140
141        let cvar_value = match parts.next() {
142            Some(val) => val,
143            None => {
144                let val = cvars.get_string(cvar_name)?;
145                self.print(val);
146                return Ok(());
147            }
148        };
149        if let Some(rest) = parts.next() {
150            return Err(format!("expected only cvar name and value, found {rest}"));
151        }
152        cvars.set_str(cvar_name, cvar_value)
153    }
154
155    /// Print a line in the console and save it to history as output.
156    pub fn print<S: Into<String>>(&mut self, text: S) {
157        self.push_history_line(text.into(), false);
158    }
159
160    /// Print a line in the console and save it to history as input.
161    fn print_input<S: Into<String>>(&mut self, text: S) {
162        self.push_history_line(text.into(), true);
163    }
164
165    fn push_history_line(&mut self, text: String, is_input: bool) {
166        let hist_line = HistoryLine::new(text, is_input);
167        self.history.push(hist_line);
168
169        // LATER Make this configurable so adding new lines doesn't scroll the view.
170        self.history_view_end += 1;
171    }
172}
173
174/// A line in the console's history view.
175///
176/// Might have come from the user or is the result of running a command.
177#[derive(Debug, Clone)]
178pub struct HistoryLine {
179    /// The line's text.
180    pub text: String,
181    /// Whether the line is input from the user or output from running a command.
182    pub is_input: bool,
183}
184
185impl HistoryLine {
186    /// Create a new history line.
187    pub fn new(text: String, is_input: bool) -> Self {
188        Self { text, is_input }
189    }
190}