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