cpp-terminal 1.0.0
Small C++ library for writing multiplatform terminal applications
Loading...
Searching...
No Matches
prompt.cpp
Go to the documentation of this file.
1/*
2* cpp-terminal
3* C++ library for writing multi-platform terminal applications.
4*
5* SPDX-FileCopyrightText: 2019-2025 cpp-terminal
6*
7* SPDX-License-Identifier: MIT
8*/
9
11
17#include "cpp-terminal/key.hpp"
22#include "cpp-terminal/tty.hpp"
23#include "position.hpp"
24
25#include <iostream>
26
27Term::Result Term::prompt(const std::string& message, const std::string& first_option, const std::string& second_option, const std::string& prompt_indicator, bool immediate)
28{
29 Term::terminal.setOptions(Option::NoClearScreen, Option::NoSignalKeys, Option::Cursor, Term::Option::Raw);
30 std::cout << message << " [" << first_option << '/' << second_option << ']' << prompt_indicator << ' ' << std::flush;
31
33 {
34 std::cout << '\n' << std::flush;
35 return Result::Error;
36 }
37
38 Term::Key key;
39
40 if(immediate)
41 {
42 while(true)
43 {
44 key = Term::read_event();
45 if(key == Term::Key::NoKey) continue;
46 if(key == Term::Key::y || key == Term::Key::Y)
47 {
48 std::cout << '\n' << std::flush;
49 return Result::Yes;
50 }
51 else if(key == Term::Key::n || key == Term::Key::N)
52 {
53 std::cout << '\n' << std::flush;
54 return Result::No;
55 }
56 else if(key == Term::Key::Ctrl_C || key == Term::Key::Ctrl_D)
57 {
58 std::cout << '\n' << std::flush;
59 return Result::Abort;
60 }
61 else if(key == Term::Key::Enter)
62 {
63 std::cout << '\n' << std::flush;
64 return Result::None;
65 }
66 else
67 {
68 std::cout << '\n' << std::flush;
69 return Result::Invalid;
70 }
71 }
72 }
73 else
74 {
75 std::string input;
76 while(true)
77 {
78 key = Term::read_event();
79 if(key == Term::Key::NoKey) continue;
80 if(key >= 'a' && key <= 'z')
81 {
82 std::cout << (char)key << std::flush;
83 input.push_back(static_cast<char>(key));
84 }
85 else if(key >= 'A' && key <= 'Z')
86 {
87 std::cout << (char)key << std::flush;
88 input.push_back(static_cast<char>(key.tolower())); // convert upper case to lowercase
89 }
90 else if(key == Term::Key::Ctrl_C || key == Term::Key::Ctrl_D)
91 {
92 std::cout << '\n';
93 return Result::Abort;
94 }
95 else if(key == Term::Key::Backspace)
96 {
97 if(input.empty() != 0)
98 {
99 std::cout << "\u001b[D \u001b[D" << std::flush; // erase last line and move the cursor back
100 input.pop_back();
101 }
102 }
103 else if(key == Term::Key::Enter)
104 {
105 if(input == "y" || input == "yes")
106 {
107 std::cout << '\n' << std::flush;
108 return Result::Yes;
109 }
110 else if(input == "n" || input == "no")
111 {
112 std::cout << '\n' << std::flush;
113 return Result::No;
114 }
115 else if(input.empty())
116 {
117 std::cout << '\n' << std::flush;
118 return Result::None;
119 }
120 else
121 {
122 std::cout << '\n' << std::flush;
123 return Result::Invalid;
124 }
125 }
126 }
127 }
128}
129
130Term::Result_simple Term::prompt_simple(const std::string& message)
131{
132 switch(prompt(message, "y", "N", ":", false))
133 {
134 case Result::Yes: return Result_simple::Yes;
135 case Result::Abort: return Result_simple::Abort;
136 case Result::No: // falls through
137 case Result::Error: // falls through
138 case Result::None: // falls through
139 case Result::Invalid:
140 default: return Result_simple::No;
141 }
142}
143
144std::string Term::concat(const std::vector<std::string>& lines)
145{
146 std::string s;
147 for(auto& line: lines) { s.append(line + "\n"); }
148 return s;
149}
150
151std::vector<std::string> Term::split(const std::string& s)
152{
153 std::size_t j = 0;
154 std::vector<std::string> lines;
155 lines.emplace_back("");
156 if(s[s.size() - 1] != '\n') throw Term::Exception("\\n is required");
157 for(std::size_t i = 0; i < s.size() - 1; i++)
158 {
159 if(s[i] == '\n')
160 {
161 j++;
162 lines.emplace_back("");
163 }
164 else { lines[j].push_back(s[i]); }
165 }
166 return lines;
167}
168
169char32_t UU(const std::string& s)
170{
171 std::u32string s2 = Term::Private::utf8_to_utf32(s);
172 if(s2.size() != 1) throw Term::Exception("U(s): s not a codepoint.");
173 return s2[0];
174}
175
176void Term::print_left_curly_bracket(Term::Window& scr, const std::size_t& x, const std::size_t& y1, const std::size_t& y2)
177{
178 std::size_t h{y2 - y1 + 1};
179 if(h == 1) { scr.set_char(x, y1, UU("]")); }
180 else
181 {
182 scr.set_char(x, y1, UU("┐"));
183 for(std::size_t j = y1 + 1; j <= y2 - 1; j++) { scr.set_char(x, j, UU("│")); }
184 scr.set_char(x, y2, UU("┘"));
185 }
186}
187
188void Term::render(Term::Window& scr, const Model& model, const std::size_t& cols)
189{
190 scr.clear();
191 print_left_curly_bracket(scr, cols, 1, model.lines.size());
192 scr.print_str(cols - 6, model.lines.size(), std::to_string(model.cursor_row) + "," + std::to_string(model.cursor_col));
193 for(std::size_t j = 0; j < model.lines.size(); j++)
194 {
195 if(j == 0)
196 {
197 scr.fill_fg(1, j + 1, model.prompt_string.size(), model.lines.size(), Term::Color::Name::Green);
198 scr.fill_style(1, j + 1, model.prompt_string.size(), model.lines.size(), Term::Style::Bold);
199 scr.print_str(1, j + 1, model.prompt_string);
200 }
201 else
202 {
203 for(std::size_t i = 0; i < model.prompt_string.size() - 1; i++) { scr.set_char(i + 1, j + 1, '.'); }
204 }
205 scr.print_str(model.prompt_string.size() + 1, j + 1, model.lines[j]);
206 }
207 scr.set_cursor_pos(model.prompt_string.size() + model.cursor_col, model.cursor_row);
208}
209
210std::string Term::prompt_multiline(const std::string& prompt_string, std::vector<std::string>& m_history, std::function<bool(std::string)>& iscomplete)
211{
212 Term::Cursor cursor;
213 Term::Screen screen({Term::Rows(25), Term::Columns(80)});
214 bool term_attached = Term::is_stdin_a_tty();
215 if(is_stdin_a_tty())
216 {
217 cursor = cursor_position();
218 screen = screen_size();
219 }
220
221 Model model;
222 model.prompt_string = prompt_string;
223
224 // Make a local copy of history that can be modified by the user. All
225 // changes will be forgotten once a command is submitted.
226 std::vector<std::string> history = m_history;
227 std::size_t history_pos = history.size();
228 history.push_back(concat(model.lines)); // Push back empty input
229
230 Term::Window scr({Term::Columns(screen.columns()), Term::Rows(1)});
231 Term::Key key;
232 render(scr, model, screen.columns());
233 std::cout << scr.render(1, cursor.row(), term_attached) << std::flush;
234 bool not_complete = true;
235 while(not_complete)
236 {
237 key = Term::read_event();
238 if(key == Term::Key::NoKey) continue;
239 if(key.isprint())
240 {
241 std::string before = model.lines[model.cursor_row - 1].substr(0, model.cursor_col - 1);
242 std::string newchar;
243 newchar.push_back(static_cast<char>(key));
244 std::string after = model.lines[model.cursor_row - 1].substr(model.cursor_col - 1);
245 model.lines[model.cursor_row - 1] = before += newchar += after;
246 model.cursor_col++;
247 }
248 else if(key == Key::Ctrl_D)
249 {
250 if(model.lines.size() == 1 && model.lines[model.cursor_row - 1].empty())
251 {
252 model.lines[model.cursor_row - 1].push_back(static_cast<char>(Key::Ctrl_D));
253 std::cout << "\n" << std::flush;
254 m_history.push_back(model.lines[0]);
255 return model.lines[0];
256 }
257 }
258 else
259 {
260 switch(key)
261 {
262 case Key::Enter:
263 not_complete = !iscomplete(concat(model.lines));
264 if(not_complete) key = Key(static_cast<Term::Key>(Term::MetaKey::Value::Alt + Term::Key::Enter));
265 else
266 break;
268 case Key::Backspace:
269 if(model.cursor_col > 1)
270 {
271 std::string before = model.lines[model.cursor_row - 1].substr(0, model.cursor_col - 2);
272 std::string after = model.lines[model.cursor_row - 1].substr(model.cursor_col - 1);
273 model.lines[model.cursor_row - 1] = before + after;
274 model.cursor_col--;
275 }
276 else if(model.cursor_col == 1 && model.cursor_row > 1)
277 {
278 model.cursor_col = model.lines[model.cursor_row - 2].size() + 1;
279 model.lines[model.cursor_row - 2] += model.lines[model.cursor_row - 1];
280 model.lines.erase(model.lines.begin() + static_cast<long>(model.cursor_row) - 1);
281 model.cursor_row--;
282 }
283 break;
284 case Key::Del:
285 if(model.cursor_col <= model.lines[model.cursor_row - 1].size())
286 {
287 std::string before = model.lines[model.cursor_row - 1].substr(0, model.cursor_col - 1);
288 std::string after = model.lines[model.cursor_row - 1].substr(model.cursor_col);
289 model.lines[model.cursor_row - 1] = before + after;
290 }
291 break;
292 case Key::ArrowLeft:
293 if(model.cursor_col > 1) { model.cursor_col--; }
294 break;
295 case Key::ArrowRight:
296 if(model.cursor_col <= model.lines[model.cursor_row - 1].size()) { model.cursor_col++; }
297 break;
298 case Key::Home: model.cursor_col = 1; break;
299 case Key::End: model.cursor_col = model.lines[model.cursor_row - 1].size() + 1; break;
300 case Key::ArrowUp:
301 if(model.cursor_row == 1)
302 {
303 if(history_pos > 0)
304 {
305 history[history_pos] = concat(model.lines);
306 history_pos--;
307 model.lines = split(history[history_pos]);
308 model.cursor_row = model.lines.size();
309 if(model.cursor_col > model.lines[model.cursor_row - 1].size() + 1) { model.cursor_col = model.lines[model.cursor_row - 1].size() + 1; }
310 if(model.lines.size() > scr.columns()) { scr.set_h(model.lines.size()); }
311 }
312 }
313 else
314 {
315 model.cursor_row--;
316 if(model.cursor_col > model.lines[model.cursor_row - 1].size() + 1) { model.cursor_col = model.lines[model.cursor_row - 1].size() + 1; }
317 }
318 break;
319 case Key::ArrowDown:
320 if(model.cursor_row == model.lines.size())
321 {
322 if(history_pos < history.size() - 1)
323 {
324 history[history_pos] = concat(model.lines);
325 history_pos++;
326 model.lines = split(history[history_pos]);
327 model.cursor_row = 1;
328 if(model.cursor_col > model.lines[model.cursor_row - 1].size() + 1) { model.cursor_col = model.lines[model.cursor_row - 1].size() + 1; }
329 if(model.lines.size() > scr.columns()) { scr.set_h(model.lines.size()); }
330 }
331 }
332 else
333 {
334 model.cursor_row++;
335 if(model.cursor_col > model.lines[model.cursor_row - 1].size() + 1) { model.cursor_col = model.lines[model.cursor_row - 1].size() + 1; }
336 }
337 break;
338 case Key::Ctrl_N:
339 {
340 std::string before = model.lines[model.cursor_row - 1].substr(0, model.cursor_col - 1);
341 std::string after = model.lines[model.cursor_row - 1].substr(model.cursor_col - 1);
342 model.lines[model.cursor_row - 1] = before;
343 if(model.cursor_row < model.lines.size())
344 {
345 // Not at the bottom row, can't push back
346 model.lines.insert(model.lines.begin() + static_cast<long>(model.cursor_row), after);
347 }
348 else { model.lines.push_back(after); }
349 model.cursor_col = 1;
350 model.cursor_row++;
351 if(model.lines.size() > scr.columns()) { scr.set_h(model.lines.size()); }
352 break;
353 }
354 default: break;
355 }
356 }
357 render(scr, model, screen.columns());
358 std::cout << scr.render(1, cursor.row(), term_attached) << std::flush;
359 if(cursor.row() + scr.columns() - 1 > screen.rows())
360 {
361 cursor = Cursor({Row(static_cast<std::uint16_t>(screen.rows() - (scr.columns() - 1))), Column(cursor.column())});
362 std::cout << scr.render(1, cursor.row(), term_attached) << std::flush;
363 }
364 }
365 std::string line_skips;
366 for(std::size_t i = 0; i <= model.lines.size() - model.cursor_row; i++) { line_skips += "\n"; }
367 std::cout << line_skips << std::flush;
368 m_history.push_back(concat(model.lines));
369 return concat(model.lines);
370}
@ Green
Green FG: 32, BG: 42.
std::size_t column() const
Definition cursor.cpp:16
std::size_t row() const
Definition cursor.cpp:14
@ Ctrl_D
Definition key.hpp:100
@ Enter
Definition key.hpp:231
@ Backspace
Definition key.hpp:229
@ NoKey
Definition key.hpp:92
@ Ctrl_C
Definition key.hpp:99
constexpr Key tolower() const
Definition key.hpp:390
std::vector< std::string > lines
Definition prompt.hpp:69
std::size_t cursor_row
Definition prompt.hpp:72
std::size_t cursor_col
Definition prompt.hpp:71
std::string prompt_string
Definition prompt.hpp:68
void setOptions(const Args &&... args)
Represents a rectangular window, as a 2D array of characters and their attributes.
Definition window.hpp:34
void print_str(const std::size_t &column, const std::size_t &, const std::string &, const std::size_t &=0, bool=false)
Definition window.cpp:97
void set_char(const std::size_t &column, const std::size_t &row, const char32_t &character)
Definition window.cpp:46
void fill_style(const std::size_t &column, const std::size_t &, const std::size_t &, const std::size_t &, const Style &)
Definition window.cpp:140
void set_cursor_pos(const std::size_t &column, const std::size_t &row)
Definition window.cpp:78
void fill_fg(const std::size_t &column, const std::size_t &, const std::size_t &, const std::size_t &, const Color &)
Definition window.cpp:124
void clear()
Definition window.cpp:189
#define CPP_TERMINAL_FALLTHROUGH
Definition macros.hpp:40
std::u32string utf8_to_utf32(const std::string &str)
std::string prompt_multiline(const std::string &, std::vector< std::string > &, std::function< bool(std::string)> &)
Definition prompt.cpp:210
std::vector< std::string > split(const std::string &)
Definition prompt.cpp:151
void print_left_curly_bracket(Term::Window &, const std::size_t &, const std::size_t &, const std::size_t &)
Definition prompt.cpp:176
Term::Event read_event()
Definition input.cpp:338
Result prompt(const std::string &message, const std::string &first_option, const std::string &second_option, const std::string &prompt_indicator, bool)
A simple yes/no prompt, requires the user to press the ENTER key to continue.
Definition prompt.cpp:27
std::string concat(const std::vector< std::string > &)
Definition prompt.cpp:144
Result_simple prompt_simple(const std::string &message)
The most simple prompt possible, requires the user to press enter to continue.
Definition prompt.cpp:130
void render(Term::Window &, const Model &, const std::size_t &)
Definition prompt.cpp:188
@ Raw
Set terminal in raw mode.
Term::Cursor cursor_position()
Definition cursor.cpp:26
@ Bold
Thick text font.
Result
Definition prompt.hpp:22
Term::Terminal & terminal
Definition terminal.cpp:19
Screen screen_size()
Definition screen.cpp:24
bool is_stdin_a_tty()
Check if stdin is a tty.
Definition tty.cpp:32
Result_simple
Definition prompt.hpp:45
char32_t UU(const std::string &s)
Definition prompt.cpp:169