1 // MIT License
2 //
3 // Copyright (c) 2018 Chun-Xun Lin, Tsung-Wei Huang and Martin D. F. Wong
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included in all
13 // copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22
23 #ifndef PROMPT_HPP_
24 #define PROMPT_HPP_
25
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include <sys/ioctl.h>
29 #include <termios.h>
30 #include <pwd.h>
31 #include <errno.h>
32 #include <unistd.h>
33 #include <algorithm>
34 #include <unistd.h>
35 #include <sstream>
36 #include <iostream>
37 #include <memory>
38 #include <list>
39 #include <vector>
40 #include <cstring>
41 #include <fstream>
42 #include <string_view>
43 #include <filesystem>
44 #include <cassert>
45
46 // ------------------------------------------------------------------------------------------------
47
48 namespace prompt {
49
50 // Procedure: read_line
51 // Read one line from an input stream
52 //template <typename C, typename T, typename A>
53 //std::basic_istream<C, T>& read_line(
54 // std::basic_istream<C, T>& is,
55 // std::basic_string<C, T, A>& line
56 //) {
57 //
58 // line.clear();
59 //
60 // typename std::basic_istream<C, T>::sentry se(is, true);
61 // std::streambuf* sb = is.rdbuf();
62 //
63 // for(;;) {
64 // switch (int c = sb->sbumpc(); c) {
65 //
66 // // case 1: newline
67 // case '\n':
68 // return is;
69 // break;
70 //
71 // // case 2: carriage return
72 // case '\r':
73 // if(sb->sgetc() == '\n'){
74 // sb->sbumpc();
75 // }
76 // return is;
77 // break;
78 //
79 // // case 3: eof
80 // case std::streambuf::traits_type::eof():
81 // // Also handle the case when the last line has no line ending
82 // if(line.empty()) {
83 // is.setstate(std::ios::eofbit | std::ios::failbit);
84 // }
85 // return is;
86 // break;
87 //
88 // default:
89 // line.push_back(static_cast<char>(c));
90 // break;
91 // }
92 // }
93 //
94 // return is;
95 //}
96
97
98
99 template <typename C, typename T, typename A>
read_line(FILE * is,std::basic_string<C,T,A> & line)100 bool read_line(
101 FILE* is,
102 std::basic_string<C, T, A>& line
103 ) {
104
105 line.clear();
106
107 for(;;) {
108 switch (int c = ::fgetc(is); c) {
109
110 // case 1: newline
111 case '\n':
112 return true;
113 break;
114
115 // case 2: carriage return '\r\n'
116 case '\r':
117 if(auto nc = ::fgetc(is); nc != '\n'){
118 ::ungetc(nc, is);
119 }
120 return true;
121 break;
122
123 // case 3: eof
124 case EOF:
125 // Also handle the case when the last line has no line ending
126 if(line.empty()) {
127 // The line is empty (only EOF)
128 return false;
129 }
130 return true;
131 break;
132
133 default:
134 line.push_back(static_cast<char>(c));
135 break;
136 }
137 }
138 return true;
139 }
140
141
142 // ------------------------------------------------------------------------------------------------
143
144 // Function: count_prefix
145 // Count the the length of same prefix between two strings
146 template <typename C>
count_prefix(std::basic_string_view<typename C::value_type,typename C::traits_type> s1,std::basic_string_view<typename C::value_type,typename C::traits_type> s2)147 constexpr size_t count_prefix(
148 std::basic_string_view<typename C::value_type, typename C::traits_type> s1,
149 std::basic_string_view<typename C::value_type, typename C::traits_type> s2){
150 size_t i {0};
151 size_t len {std::min(s1.size(), s2.size())};
152 for(; i<len; ++i){
153 if(s1[i] != s2[i]){
154 break;
155 }
156 }
157 return i;
158 }
159
160 // ------------------------------------------------------------------------------------------------
161
162 // Class: RadixTree
163 template <typename C>
164 class RadixTree{
165
166 using value_type = typename C::value_type;
167 using traits_type = typename C::traits_type;
168 using allocator_type = typename C::allocator_type;
169
170 public:
171
172 struct Node {
173 bool is_word {false};
174 std::list<std::pair<C, std::unique_ptr<Node>>> children;
175 };
176
177 RadixTree() = default;
178 RadixTree(const std::vector<C>&);
179
180 bool exist(std::basic_string_view<value_type, traits_type>) const;
181 void insert(const C&);
182
183 std::vector<C> match_prefix(const C&) const;
184 std::vector<C> all_words() const;
185 C dump() const;
186
187 const Node& root() const;
188
189 private:
190
191 Node _root;
192
193 void _insert(std::basic_string_view<value_type, traits_type>, Node&);
194 void _match_prefix(std::vector<C>&, const Node&, const C&) const;
195 void _dump(const Node&, size_t, C&) const;
196
197 std::pair<const Node*, C> _search_prefix_node(
198 std::basic_string_view<value_type, traits_type>
199 ) const;
200 };
201
202
203 // Procedure: Ctor
204 template <typename C>
RadixTree(const std::vector<C> & words)205 RadixTree<C>::RadixTree(const std::vector<C>& words){
206 for(const auto& w: words){
207 insert(w);
208 }
209 }
210
211
212 // Procedure: dump
213 // Dump the radix tree into a string
214 template <typename C>
dump() const215 C RadixTree<C>::dump() const {
216 C t;
217 _dump(_root, 0, t);
218 t.append(1, '\n');
219 return t;
220 }
221
222 // Procedure: _dump
223 // Recursively traverse the tree and append each level to string
224 template <typename C>
_dump(const Node & n,size_t level,C & s) const225 void RadixTree<C>::_dump(const Node& n, size_t level, C& s) const {
226 for(auto &[k,v]: n.children){
227 s.append(level, '-');
228 s.append(1, ' ');
229 s += k;
230 s.append(1, '\n');
231 _dump(*v, level+1, s);
232 }
233 }
234
235 // Procedure: _match_prefix
236 // Find all words that match the given prefix
237 template <typename C>
_match_prefix(std::vector<C> & vec,const Node & n,const C & s) const238 void RadixTree<C>::_match_prefix(
239 std::vector<C> &vec,
240 const Node& n,
241 const C& s
242 ) const {
243 if(n.is_word){
244 vec.emplace_back(s);
245 }
246 for(auto& [k,v]:n.children){
247 _match_prefix(vec, *v, s+k);
248 }
249 }
250
251 // Procedure: all_words
252 // Extract all words in the radix tree
253 template <typename C>
all_words() const254 std::vector<C> RadixTree<C>::all_words() const {
255 std::vector<C> words;
256 for(auto &[k, v]: _root.children){
257 _match_prefix(words, *v, k);
258 }
259 return words;
260 }
261
262 // Procedure: match_prefix
263 // Collect all words that match the given prefix
264 template <typename C>
match_prefix(const C & prefix) const265 std::vector<C> RadixTree<C>::match_prefix(const C& prefix) const {
266 if(auto [prefix_node, suffix] = _search_prefix_node(prefix); prefix_node == nullptr){
267 return {};
268 }
269 else{
270 std::vector<C> matches;
271 if(prefix_node->is_word){
272 matches.emplace_back(prefix + suffix);
273 }
274 for(auto &[k, v]: prefix_node->children){
275 _match_prefix(matches, *v, prefix+suffix+k);
276 }
277 return matches;
278 }
279 }
280
281 // Procedure: _search_prefix_node
282 // Find the node that matches the given prefix
283 template <typename C>
_search_prefix_node(std::basic_string_view<value_type,traits_type> s) const284 std::pair<const typename RadixTree<C>::Node*, C> RadixTree<C>::_search_prefix_node(
285 std::basic_string_view<value_type, traits_type> s
286 ) const {
287
288 Node const *n = &_root;
289 std::basic_string<value_type, traits_type> suffix;
290 for(size_t pos=0; pos<s.size(); ){ // Search until full match
291 bool match = {false};
292 for(const auto& [k, v]: n->children){
293 if(auto num = count_prefix<C>(k, s.data()+pos); num>0){
294 pos += num;
295 if(pos == s.size()){
296 suffix = k.substr(num, k.size()-num);
297 }
298 n = v.get();
299 match = true;
300 break;
301 }
302 }
303 if(not match){
304 return {nullptr, suffix};
305 }
306 }
307 return {n, suffix};
308 }
309
310 // Procedure: exist
311 // Check whether the given word is in the radix tree or not
312 template <typename C>
exist(std::basic_string_view<value_type,traits_type> s) const313 bool RadixTree<C>::exist(std::basic_string_view<value_type, traits_type> s) const {
314
315 size_t pos {0};
316 Node const *n = &_root;
317 while(not n->children.empty()){ // Search until reaching the leaf
318 bool match {false};
319 for(const auto& [k, v]: n->children){
320 auto match_num = count_prefix<C>(k, s.data()+pos);
321 if(match_num > 0){
322 if(pos += match_num; pos == s.size()){
323 if(match_num == k.size() and v->is_word){
324 return true;
325 }
326 }
327 else{
328 n = v.get();
329 match = true;
330 }
331 break;
332 }
333 }
334 if(not match){
335 return false;
336 }
337 }
338 return false;
339 }
340
341 // Procedure: insert
342 // Insert a word into radix tree
343 template <typename C>
insert(const C & s)344 void RadixTree<C>::insert(const C& s){
345 if(s.empty()){ // Empty string not allowed
346 return;
347 }
348 _insert(s, _root);
349 }
350
351 // Procedure: _insert
352 // Insert a word into radix tree
353 template <typename C>
_insert(std::basic_string_view<value_type,traits_type> sv,Node & n)354 void RadixTree<C>::_insert(std::basic_string_view<value_type, traits_type> sv, Node& n){
355
356 // base case
357 if(sv.empty()) {
358 n.is_word = true;
359 return;
360 }
361
362 size_t match_num {0};
363
364 auto itr = std::find_if(n.children.begin(), n.children.end(), [&] (const auto& kv) {
365 if(match_num = count_prefix<C>(std::get<0>(kv), sv); match_num > 0) {
366 return true;
367 }
368 else return false;
369 });
370
371 if(match_num == 0){ // Base case 1
372 assert(itr == n.children.end());
373 auto& child = std::get<1>(n.children.emplace_back(std::make_pair(sv, new Node)));
374 child->is_word = true;
375 }
376 else {
377
378 if(match_num == itr->first.size()) {
379 _insert(sv.data() + match_num, *itr->second);
380 }
381 else {
382
383 auto& par = std::get<1>(n.children.emplace_back(
384 std::make_pair(
385 std::basic_string_view<value_type, traits_type>{sv.data(), match_num}, new Node))
386 );
387
388 par->children.emplace_back(
389 std::make_pair(itr->first.data()+match_num, std::move(itr->second))
390 );
391
392 n.children.erase(itr);
393
394 _insert(sv.data() + match_num, *par);
395 }
396 }
397 }
398
399
400 // Procedure: root
401 // Return the root of RadixTree
402 template <typename C>
root() const403 const typename RadixTree<C>::Node& RadixTree<C>::root() const{
404 return _root;
405 }
406
407 // ------------------------------------------------------------------------------------------------
408
409
410 // http://www.physics.udel.edu/~watson/scen103/ascii.html
411 enum class KEY{
412 KEY_NULL = 0, /* NULL */
413 CTRL_A = 1, /* Ctrl+a */
414 CTRL_B = 2, /* Ctrl-b */
415 CTRL_C = 3, /* Ctrl-c */
416 CTRL_D = 4, /* Ctrl-d */
417 CTRL_E = 5, /* Ctrl-e */
418 CTRL_F = 6, /* Ctrl-f */
419 CTRL_H = 8, /* Ctrl-h */
420 TAB = 9, /* Tab */
421 CTRL_K = 11, /* Ctrl+k */
422 CTRL_L = 12, /* Ctrl+l */
423 ENTER = 13, /* Enter */
424 CTRL_N = 14, /* Ctrl-n */
425 CTRL_P = 16, /* Ctrl-p */
426 CTRL_T = 20, /* Ctrl-t */
427 CTRL_U = 21, /* Ctrl+u */
428 CTRL_W = 23, /* Ctrl+w */
429 ESC = 27, /* Escape */
430 BACKSPACE = 127 /* Backspace */
431 };
432
433 enum class COLOR{
434 BLACK = 30,
435 RED,
436 GREEN,
437 YELLOW,
438 BLUE,
439 MAGNETA,
440 CYAN,
441 WHITE
442 };
443
444 class Prompt {
445
446 struct LineInfo{
447
448 std::string buf;
449 int history_trace {0};
450 size_t cur_pos {0}; // cursor position
451
452 void operator = (const LineInfo&);
resetprompt::Prompt::LineInfo453 inline void reset(){
454 cur_pos = history_trace = 0;
455 buf.clear();
456 }
457 };
458
459 public:
460
461 //Prompt(
462 // const std::string&, // Welcome message
463 // const std::string&, // prompt
464 // const filesystem::path& = filesystem::current_path()/".prompt_history", // history log path
465 // std::istream& = std::cin,
466 // std::ostream& = std::cout,
467 // std::ostream& = std::cerr,
468 // int = STDIN_FILENO
469 //);
470
471 //// TODO:
472 Prompt(
473 const std::string&, // Welcome message
474 const std::string&, // prompt
475 const std::filesystem::path& = std::filesystem::current_path()/".prompt_history", // history log path
476 FILE* = stdin,
477 std::ostream& = std::cout,
478 std::ostream& = std::cerr
479 );
480
481 ~Prompt();
482
483 bool readline(std::string&);
484
485 void set_history_size(size_t);
history_size() const486 size_t history_size() const { return _history.size(); };
487
488 void autocomplete(const std::string&);
489
490 private:
491
492 std::string _prompt;
493 std::filesystem::path _history_path;
494
495 // TODO: replace these with FILE* _is, _os, _es;
496 // fix inserters and extractors
497 //std::istream& _cin;
498 FILE* _is;
499 std::ostream& _cout;
500 std::ostream& _cerr;
501
502
503 // TODO: remove _infd and use ::fileno(_is);
504 //int _infd;
505
506 size_t _columns {80}; // default width of terminal is 80
507
508 RadixTree<std::string> _tree; // Radix tree for command autocomplete
509
510 std::string _obuf; // Buffer for _refresh_single_line
511
512 bool _unsupported_term();
513 bool _stdin_not_tty(std::string &);
514 bool _set_raw_mode();
515 void _disable_raw_mode();
516
517 // History
518 size_t _max_history_size {100};
519 std::list<std::string> _history;
520 void _add_history(const std::string&);
521 void _save_history();
522 void _load_history();
523
524 termios _orig_termios;
525 bool _has_orig_termios {false};
526 bool _save_orig_termios(int);
527
528 int _get_cursor_pos();
529 void _clear_screen();
530 size_t _terminal_columns();
531
532 void _edit_line(std::string&);
533
534 void _refresh_single_line(LineInfo&);
535
536 LineInfo _line;
537 LineInfo _line_save;
538
539 int _autocomplete_iterate_command();
540 void _autocomplete_command();
541 void _autocomplete_folder();
542
543 std::vector<std::string> _files_in_folder(const std::filesystem::path&) const;
544 std::vector<std::string> _files_match_prefix(const std::filesystem::path&) const;
545 std::string _dump_files(const std::vector<std::string>&, const std::filesystem::path&);
546 std::string _dump_options(const std::vector<std::string>&);
547 std::string _next_prefix(const std::vector<std::string>&, const size_t);
548
549
550 std::filesystem::path _user_home() const;
551 bool _has_read_access(const std::filesystem::path&) const;
552
553 // Key handling subroutine
554 void _key_backspace(LineInfo&);
555 void _key_delete(LineInfo&);
556 void _key_delete_prev_word(LineInfo&);
557
558 void _key_prev_history(LineInfo&);
559 void _key_next_history(LineInfo&);
560 void _key_history(LineInfo&, bool);
561 bool _key_handle_CSI(LineInfo&);
562
563 bool _append_character(LineInfo&, char);
564 };
565
566
567 // Copy assignment
operator =(const LineInfo & l)568 inline void Prompt::LineInfo::operator = (const LineInfo& l){
569 buf = l.buf;
570 cur_pos = l.cur_pos;
571 history_trace = l.history_trace;
572 }
573
574 //// Procedure: Ctor
575 //inline Prompt::Prompt(
576 // const std::string& welcome_msg,
577 // const std::string& pmt,
578 // const filesystem::path& path,
579 // std::istream& in,
580 // std::ostream& out,
581 // std::ostream& err,
582 // int infd
583 //):
584 // _prompt(pmt),
585 // _history_path(path),
586 // _cin(in),
587 // _cout(out),
588 // _cerr(err),
589 // _infd(infd)
590 //{
591 // if(::isatty(_infd)){
592 // _cout << welcome_msg;
593 // _columns = _terminal_columns();
594 // if(filesystem::exists(_history_path)){
595 // if(std::error_code ec; not filesystem::is_regular_file(_history_path, ec)){
596 // _cerr << "The history file is not a regular file\n";
597 // }
598 // else{
599 // _load_history();
600 // }
601 // }
602 // }
603 //}
604
605
606 // Procedure: Ctor
Prompt(const std::string & welcome_msg,const std::string & pmt,const std::filesystem::path & path,FILE * in,std::ostream & out,std::ostream & err)607 inline Prompt::Prompt(
608 const std::string& welcome_msg,
609 const std::string& pmt,
610 const std::filesystem::path& path,
611 FILE* in,
612 std::ostream& out,
613 std::ostream& err
614
615 ):
616 _prompt(pmt),
617 _history_path(path),
618 _is(in),
619 _cout(out),
620 _cerr(err)
621 {
622 if(::isatty(::fileno(_is))){
623 _cout << welcome_msg;
624 _columns = _terminal_columns();
625 if(std::filesystem::exists(_history_path)){
626 if(std::error_code ec; not std::filesystem::is_regular_file(_history_path, ec)){
627 _cerr << "The history file is not a regular file\n";
628 }
629 else{
630 _load_history();
631 }
632 }
633 }
634 }
635
636
637 // Procedure: Dtor
~Prompt()638 inline Prompt::~Prompt(){
639 // Restore the original mode if has kept
640 if(_has_orig_termios){
641 //::tcsetattr(_infd, TCSAFLUSH, &_orig_termios);
642 ::tcsetattr(::fileno(_is), TCSAFLUSH, &_orig_termios);
643 }
644 if(not _history.empty()){
645 _save_history();
646 }
647 }
648
649 // Procedure: autocomplete
650 // This function adds the word into radix tree
autocomplete(const std::string & word)651 inline void Prompt::autocomplete(const std::string& word){
652 _tree.insert(word);
653 }
654
655 // Procedure: history_size
656 // Change the max history size
set_history_size(size_t new_size)657 inline void Prompt::set_history_size(size_t new_size){
658 _max_history_size = new_size;
659 }
660
661
662 // Procedure: _save_history
663 // Save history commands to a file
_save_history()664 inline void Prompt::_save_history(){
665 std::ofstream ofs(_history_path);
666 if(not ofs.good()){
667 return;
668 }
669
670 for(const auto& c: _history){
671 ofs << c << '\n';
672 }
673 ofs.close();
674 }
675
676
677 // Procedure: _load_history
678 // Load history commands from a file
_load_history()679 inline void Prompt::_load_history(){
680 if(std::filesystem::exists(_history_path)){
681 std::ifstream ifs(_history_path);
682 std::string placeholder;
683 while(std::getline(ifs, placeholder)){
684 if(_history.size() == _max_history_size){
685 _history.pop_front();
686 }
687 _history.emplace_back(std::move(placeholder));
688 }
689 }
690 }
691
692 // Procedure: _add_history
693 // Add command to history list
_add_history(const std::string & hist)694 inline void Prompt::_add_history(const std::string &hist){
695 // hist cannot be empty and cannot be the same as the last one
696 if(hist.empty() or (not _history.empty() and _history.back() == hist)){
697 return ;
698 }
699 if(_history.size() == _max_history_size){
700 _history.pop_front();
701 }
702 _history.emplace_back(hist);
703 }
704
705 // Procedure: _stdin_not_tty
706 // Store input in a string if stdin is not from tty (from pipe or redirected file)
_stdin_not_tty(std::string & s)707 inline bool Prompt::_stdin_not_tty(std::string& s){
708 //read_line(_cin, s); // Read until newline, CR or EOF.
709 return read_line(_is, s);
710 }
711
712 // Procedure: _unsupported_term
713 // Check the terminal is supported or not
_unsupported_term()714 inline bool Prompt::_unsupported_term(){
715
716 static auto _unsupported_terms = {"dumb", "cons25", "emacs"};
717
718 if(char* term = getenv("TERM"); term == NULL){
719 return false;
720 }
721 else if(std::string_view str(term);
722 std::find(_unsupported_terms.begin(),
723 _unsupported_terms.end() , str) != _unsupported_terms.end()){
724 return true;
725 }
726 else {
727 return false;
728 }
729 }
730
731 // Procedure: readline
732 // This is the main entry of Prompt
readline(std::string & s)733 inline bool Prompt::readline(std::string& s) {
734 if(! ::isatty(::fileno(_is))) {
735 // not a tty, either from file or pipe; we don't limit the line size
736 if(!_stdin_not_tty(s)){
737 return false;
738 }
739
740 // TODO: ferror precedes feof
741 return ::ferror(_is) ? false : ::feof(_is) ? false : true;
742 //return _cin.eof() ? false : true;
743 }
744 else if(_unsupported_term()){
745 //read_line(_cin, s);
746 if(!read_line(_is, s)) return false;
747 return ::ferror(_is) ? false : ::feof(_is) ? false : true;
748 //return _cin.eof() ? false : true;
749 }
750 else{
751 if(not _set_raw_mode()){
752 return {};
753 }
754 _edit_line(s);
755 _add_history(s);
756 _disable_raw_mode();
757 // TODO: what?
758 //std::cout << '\n';
759 _cout << '\n';
760 return errno == EAGAIN ? false : true;
761 }
762 }
763
764 // Procedure: _save_orig_termios
765 // Save the original termios for recovering before exit
_save_orig_termios(int fd)766 inline bool Prompt::_save_orig_termios(int fd){
767 if(not _has_orig_termios){
768 if(::tcgetattr(fd, &_orig_termios) == -1){
769 return false;
770 }
771 _has_orig_termios = true;
772 }
773 return true;
774 }
775
776
777 // Procedure: _get_cursor_pos
778 // Return the current position of cursor
_get_cursor_pos()779 inline int Prompt::_get_cursor_pos(){
780 /* Report cursor location */
781 // x1b[6n : DSR - Device Status Report https://en.wikipedia.org/wiki/ANSI_escape_code
782 // The output will be like : ESC[n;mR, where "n" is the row and "m" is the column.
783 // Try this in ur terminal : echo -en "abc \x1b[6n"
784 if(not (_cout << "\x1b[6n")){
785 return -1;
786 }
787
788 char buf[32];
789 int cols, rows;
790
791 /* Read the response: ESC [ rows ; cols R */
792 for(size_t i=0; i<sizeof(buf)-1; i++){
793 //if(not(_cin >> buf[i]) or buf[i] == 'R'){
794 if(buf[i]=fgetc(_is); ::ferror(_is) || buf[i] == 'R'){
795 buf[i] = '\0';
796 break;
797 }
798 }
799
800 /* Parse it. */
801 if (static_cast<KEY>(buf[0]) != KEY::ESC or
802 buf[1] != '[' or
803 ::sscanf(buf+2, "%d;%d", &rows, &cols) != 2){
804 return -1;
805 }
806 return cols;
807 }
808
809
810
811 // Procedure: _clear_screen (ctrl + l)
_clear_screen()812 inline void Prompt::_clear_screen() {
813 // https://en.wikipedia.org/wiki/ANSI_escape_code
814 // CSI n ; m H : CUP - Cursor Position
815 // CSI n J : Erase in Display
816 //if(_cout.write("\x1b[H\x1b[2J",7); !_cout.good()){
817 if(not (_cout << "\x1b[H\x1b[2J")){
818 /* nothing to do, just to avoid warning. */
819 }
820 }
821
822 // Procedure: _set_raw_mode
823 // Set the fd to raw mode
_set_raw_mode()824 inline bool Prompt::_set_raw_mode(){
825 if(::isatty(::fileno(_is)) and _save_orig_termios(::fileno(_is)) == true){
826 struct termios raw;
827 raw = _orig_termios; /* modify the original mode */
828 /* input modes: no break, no CR to NL, no parity check, no strip char,
829 * no start/stop output control. */
830 raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
831 /* output modes - disable post processing */
832 raw.c_oflag &= ~(OPOST);
833 /* control modes - set 8 bit chars */
834 raw.c_cflag |= (CS8);
835 /* local modes - choing off, canonical off, no extended functions,
836 * no signal chars (^Z,^C) */
837 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
838 /* control chars - set return condition: min number of bytes and timer.
839 * We want read to return every single byte, without timeout. */
840 raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */
841
842 /* put terminal in raw mode after flushing */
843 if (::tcsetattr(::fileno(_is), TCSAFLUSH, &raw) >= 0){
844 return true;
845 }
846 }
847 errno = ENOTTY;
848 return false;
849 }
850
851
852 // Procedure: _disable_raw_mode
853 // Recover the termios to the original mode
_disable_raw_mode()854 inline void Prompt::_disable_raw_mode(){
855 if(_has_orig_termios){
856 ::tcsetattr(::fileno(_is), TCSAFLUSH, &_orig_termios);
857 }
858 }
859
860
861 // Procedure: _terminal_columns
862 // Get the number of columns of current line buf
_terminal_columns()863 inline size_t Prompt::_terminal_columns(){
864 // Use ioctl to get Window size
865 if(winsize ws; ::ioctl(1, TIOCGWINSZ, &ws) == -1 or ws.ws_col == 0){
866 int start = _get_cursor_pos();
867 if(start == -1 or not (_cout << "\x1b[999c")){
868 return 80;
869 }
870 int cols = _get_cursor_pos();
871 if(cols == -1){
872 return 80;
873 }
874 if(cols > start){
875 char seq[32];
876 // Move cursor back
877 ::snprintf(seq, 32, "\x1b[%dD", cols-start);
878 if(_cout.write(seq, strlen(seq)); not _cout.good()){
879 /* Fail to move cursor back*/
880 }
881 }
882 return cols;
883 }
884 else{
885 return ws.ws_col;
886 }
887 }
888
889
890 // Procedure: user_home
891 // Return the home folder of user
_user_home() const892 inline std::filesystem::path Prompt::_user_home() const{
893 auto home = ::getenv("HOME");
894 if(home == nullptr) {
895 home = ::getpwuid(::getuid())->pw_dir;
896 }
897 return home ? home : std::filesystem::current_path();
898 }
899
900
901 // Procedure: _has_read_access
902 // Check a file has read access grant to the user
_has_read_access(const std::filesystem::path & path) const903 inline bool Prompt::_has_read_access(const std::filesystem::path &path) const {
904 if(auto rval=::access(path.c_str(), F_OK); rval == 0){
905 if(rval = ::access(path.c_str(), R_OK); rval == 0){
906 return true;
907 }
908 }
909 return false;
910 }
911
912
913 // Procedure: _next_prefix
914 // Find the prefix among a set of strings starting from position n
_next_prefix(const std::vector<std::string> & words,const size_t n)915 inline std::string Prompt::_next_prefix(const std::vector<std::string>& words, const size_t n){
916 if(words.empty()){
917 return {};
918 }
919 else if(words.size() == 1){
920 return words[0].substr(n, words[0].size()-n);
921 }
922 else{
923 for(size_t end=n ;;++end){
924 if(words[0].size() <= end){
925 return words[0].substr(n, end-n);
926 }
927 for(size_t i=1; i<words.size(); i++){
928 if(words[i].size() <= end or words[i][end] != words[0][end]){
929 return words[i].substr(n, end-n);
930 }
931 }
932 }
933 }
934 }
935
936
937 // Procedure: _autocomplete_iterate_command
938 // This is the main entry for autocomplete iterating commands
_autocomplete_iterate_command()939 inline int Prompt::_autocomplete_iterate_command(){
940
941 if(auto words = _tree.match_prefix(_line.buf); words.empty()){
942 return 0;
943 }
944 else{
945 char c {0};
946 bool stop {false};
947 for(size_t i=0; not stop;){
948 if(i < words.size()){
949 _line_save = _line;
950 _line.cur_pos = words[i].size();
951 _line.buf = words[i];
952 _refresh_single_line(_line);
953 _line = _line_save;
954 }
955 else{
956 _refresh_single_line(_line);
957 }
958
959 //if(_cin.read(&c, 1); not _cin.good()){
960 if(c = ::fgetc(_is); ::ferror(_is)){
961 return -1;
962 }
963
964 switch(static_cast<KEY>(c)){
965 case KEY::TAB:
966 i = (i+1) % words.size();
967 break;
968 case KEY::ESC: // refresh the same thing again
969 _refresh_single_line(_line);
970 stop = true;
971 break;
972 default:
973 if(i < words.size()){
974 _line.buf = words[i];
975 _line.cur_pos = words[i].size();
976 }
977 stop = true;
978 break;
979 }
980 }
981 return c;
982 }
983 }
984
985
_dump_options(const std::vector<std::string> & opts)986 inline std::string Prompt::_dump_options(const std::vector<std::string>& opts){
987 if(opts.empty()){
988 return {};
989 }
990
991 const size_t col_width = std::max_element(opts.begin(), opts.end(),
992 [](const auto& a, const auto& b){
993 return a.size() < b.size();
994 }
995 )->size() + 4;
996
997 auto col_num = std::max(size_t{1}, _terminal_columns() / col_width);
998
999 std::string s;
1000 for(size_t i=0; i<opts.size(); ++i){
1001 if(i % col_num == 0) {
1002 s.append("\n\r\x1b[0K");
1003 }
1004
1005 s.append(opts[i]);
1006
1007 if(opts[i].size() < col_width){
1008 s.append(col_width - opts[i].size(), ' ');
1009 }
1010 }
1011 return s;
1012 }
1013
1014
1015 // Procedure: _autocomplete_command
1016 // This is the main entry for command autocomplete
_autocomplete_command()1017 inline void Prompt::_autocomplete_command(){
1018 if(auto words = _tree.match_prefix(_line.buf); words.empty()){
1019 }
1020 else{
1021 if(auto suffix = _next_prefix(words, _line.cur_pos); not suffix.empty()){
1022 _line.buf.insert(_line.cur_pos, suffix);
1023 _line.cur_pos += suffix.size();
1024 }
1025 if(auto s = _dump_options(words); s.size() > 0){
1026 s.append("\x1b[0K\n");
1027 _cout << s;
1028 }
1029 _refresh_single_line(_line);
1030 }
1031 }
1032
1033
1034 // Procedure: _files_match_prefix
1035 // Find all the files in a folder that match the prefix
_files_match_prefix(const std::filesystem::path & path) const1036 inline std::vector<std::string> Prompt::_files_match_prefix(
1037 const std::filesystem::path& path
1038 ) const {
1039 // Need to check in case path is a file in current folder (parent_path = "")
1040 auto folder = path.filename() == path ? std::filesystem::current_path() : path.parent_path();
1041 std::string prefix(path.filename());
1042
1043 std::vector<std::string> matches;
1044 if(std::error_code ec; _has_read_access(folder) and std::filesystem::is_directory(folder, ec)){
1045 for(const auto& p: std::filesystem::directory_iterator(folder)){
1046 std::string fname {p.path().filename()};
1047 if(fname.compare(0, prefix.size(), prefix) == 0){
1048 matches.emplace_back(std::move(fname));
1049 }
1050 }
1051 }
1052 return matches;
1053 }
1054
1055
1056 // Procedure: _files_in_folder
1057 // List all files in a given folder
_files_in_folder(const std::filesystem::path & path) const1058 inline std::vector<std::string> Prompt::_files_in_folder(
1059 const std::filesystem::path& path
1060 ) const {
1061 auto p = path.empty() ? std::filesystem::current_path() : path;
1062 std::vector<std::string> matches;
1063 // Check permission
1064 if(_has_read_access(p)){
1065 for(const auto& p: std::filesystem::directory_iterator(p)){
1066 matches.emplace_back(p.path().filename());
1067 }
1068 }
1069 return matches;
1070 }
1071
1072
1073 // Procedure: _dump_files
1074 // Format the strings for pretty print in terminal.
_dump_files(const std::vector<std::string> & v,const std::filesystem::path & path)1075 inline std::string Prompt::_dump_files(
1076 const std::vector<std::string>& v,
1077 const std::filesystem::path& path)
1078 {
1079 if(v.empty()){
1080 return {};
1081 }
1082
1083 const size_t col_width = std::max_element(v.begin(), v.end(),
1084 [](const auto& a, const auto& b){
1085 return a.size() < b.size();
1086 }
1087 )->size() + 4;
1088
1089 auto col_num = std::max(size_t{1}, _terminal_columns() / col_width);
1090
1091 std::string s;
1092
1093 char seq[64];
1094 snprintf(seq, 64, "\033[%d;1;49m", static_cast<int>(COLOR::BLUE));
1095 std::error_code ec;
1096 for(size_t i=0; i<v.size(); ++i){
1097
1098 if(i % col_num == 0) {
1099 s.append("\n\r\x1b[0K");
1100 }
1101
1102 if(std::filesystem::is_directory(path.native() + "/" + v[i], ec)){
1103 // A typical color code example : \033[31;1;4m
1104 // \033[ : begin of color code, 31 : red color, 1 : bold, 4 : underlined
1105 s.append(seq, strlen(seq));
1106 s.append(v[i]);
1107 s.append("\033[0m");
1108 }
1109 else{
1110 s.append(v[i]);
1111 }
1112 s.append(col_width - v[i].size(), ' ');
1113 }
1114 return s;
1115 }
1116
1117 // Procedure: _autocomplete_folder
1118 // The entry for folder autocomplete
_autocomplete_folder()1119 inline void Prompt::_autocomplete_folder(){
1120 std::string s;
1121 size_t ws_index = _line.buf.rfind(' ', _line.cur_pos) + 1;
1122
1123 std::filesystem::path p(
1124 _line.buf[ws_index] != '~' ?
1125 _line.buf.substr(ws_index, _line.cur_pos - ws_index) :
1126 _user_home().native() + _line.buf.substr(ws_index+1, _line.cur_pos-ws_index-1)
1127 );
1128
1129 if(std::error_code ec; p.empty() or std::filesystem::is_directory(p, ec)) {
1130 s = _dump_files(_files_in_folder(p), p);
1131 }
1132 else{
1133 auto match = _files_match_prefix(p);
1134 if(not match.empty()){
1135 s = _dump_files(match, p.parent_path());
1136 if(auto suffix = _next_prefix(match, p.filename().native().size()); not suffix.empty()){
1137 _line.buf.insert(_line.cur_pos, suffix);
1138 _line.cur_pos += suffix.size();
1139 p += suffix;
1140 // Append a '/' if is a folder and not the prefix of other files
1141 if(std::filesystem::is_directory(p, ec) and
1142 std::count_if(match.begin(), match.end(),
1143 [p = p.filename().native()](const auto& m)
1144 { return (m.size() >= p.size() and m.compare(0, p.size(), p) == 0); }) == 1){
1145 _line.buf.insert(_line.cur_pos, 1, '/');
1146 _line.cur_pos += 1;
1147 }
1148 }
1149 }
1150 }
1151 if(s.size() > 0){
1152 s.append("\x1b[0K\n");
1153 _cout << s;
1154 }
1155
1156 _refresh_single_line(_line);
1157 }
1158
1159 // Procedure: _key_delete_prev_word
1160 // Remove the word before cursor (including the whitespace between cursor and the word)
_key_delete_prev_word(LineInfo & line)1161 inline void Prompt::_key_delete_prev_word(LineInfo& line){
1162 // Remove all whitespace before cursor
1163 auto iter = std::find_if_not(line.buf.rbegin()+line.buf.size()-line.cur_pos, line.buf.rend(),
1164 [](const auto& c){ return c == ' ';});
1165 auto offset = std::distance(iter.base(), line.buf.begin()+line.cur_pos);
1166 line.buf.erase(iter.base(), line.buf.begin()+line.cur_pos);
1167 line.cur_pos -= offset;
1168
1169 // Remove the word before cursor and stop at the first ws
1170 iter = std::find_if_not(line.buf.rbegin()+line.buf.size()-line.cur_pos, line.buf.rend(),
1171 [](const auto& c){ return c != ' ';});
1172 offset = std::distance(iter.base(), line.buf.begin()+line.cur_pos);
1173 line.buf.erase(iter.base(), line.buf.begin()+line.cur_pos);
1174 line.cur_pos -= offset;
1175 }
1176
1177 // Procedure: _key_delete
1178 // Remove the character at cursor
_key_delete(LineInfo & line)1179 inline void Prompt::_key_delete(LineInfo &line){
1180 if(line.buf.size() > 0){
1181 line.buf.erase(line.cur_pos, 1);
1182 }
1183 }
1184
1185 // Procedure: _key_backspace
1186 // Remove the character before curosr
_key_backspace(LineInfo & line)1187 inline void Prompt::_key_backspace(LineInfo &line){
1188 if(line.cur_pos > 0){
1189 line.buf.erase(line.cur_pos-1, 1);
1190 line.cur_pos--;
1191 }
1192 }
1193
1194 // Procedure: _key_prev_history
1195 // Set line buffer to previous command in history
_key_prev_history(LineInfo & line)1196 inline void Prompt::_key_prev_history(LineInfo& line){
1197 _key_history(line, true);
1198 }
1199
1200 // Procedure: _key_next_history
1201 // Set line buffer to next command in history
_key_next_history(LineInfo & line)1202 inline void Prompt::_key_next_history(LineInfo& line){
1203 _key_history(line, false);
1204 }
1205
1206 // Procedure:_key_history
1207 // Set the line buffer to previous/next history command
_key_history(LineInfo & line,bool prev)1208 inline void Prompt::_key_history(LineInfo &line, bool prev){
1209 if(_history.size() > 1){
1210 *std::next(_history.begin(), _history.size()-1-line.history_trace) = line.buf;
1211
1212 if(line.history_trace += prev ? 1 : -1; line.history_trace < 0){
1213 line.history_trace = 0;
1214 }
1215 else if(line.history_trace >= static_cast<int>(_history.size())){
1216 line.history_trace = _history.size()-1;
1217 }
1218 else{
1219 auto it = std::next(_history.begin(), _history.size()-1-line.history_trace);
1220 line.buf = *it;
1221 line.cur_pos = line.buf.size();
1222 }
1223 }
1224 }
1225
1226 // Procedure: _append_character
1227 // Insert a character to the cursor position in line buffer
_append_character(LineInfo & line,char c)1228 inline bool Prompt::_append_character(LineInfo& line, char c){
1229 try{
1230 line.buf.insert(line.cur_pos, 1, c);
1231 }
1232 catch (const std::exception& e){
1233 _cerr << "append failed: " << e.what() << '\n';
1234 assert(false);
1235 }
1236
1237 if(line.cur_pos++; line.buf.size() == line.cur_pos){
1238 if(not (_cout << c)){
1239 return false;
1240 }
1241 }
1242 else{
1243 _refresh_single_line(line);
1244 }
1245 return true;
1246 }
1247
1248 // Procedure: _key_handle_CSI
1249 // Handle Control Sequence Introducer
_key_handle_CSI(LineInfo & line)1250 inline bool Prompt::_key_handle_CSI(LineInfo& line){
1251 char seq[3];
1252 //if(_cin.read(seq,1), _cin.read(seq+1,1); not _cin.good() ){
1253 if(seq[0] = ::fgetc(_is), seq[1] = ::fgetc(_is); ::ferror(_is)){
1254 return false;
1255 }
1256 if(seq[0] == '['){
1257 if(seq[1] >= '0' and seq[1] <= '9'){
1258 //if(_cin.read(seq+2, 1); not _cin.good()){
1259 if(seq[2] = ::fgetc(_is); ::ferror(_is)){
1260 return false;
1261 }
1262 if(seq[2] == '~' and seq[1] == '3'){
1263 _key_delete(line);
1264 }
1265 }
1266 else{
1267 switch(seq[1]){
1268 case 'A': // Prev history
1269 _key_prev_history(line);
1270 break;
1271 case 'B': // Next history
1272 _key_next_history(line);
1273 break;
1274 case 'C': // Move cursor to right
1275 if(line.cur_pos != line.buf.size()){
1276 line.cur_pos ++;
1277 }
1278 break;
1279 case 'D': // Move cursor to left
1280 if(line.cur_pos > 0){
1281 line.cur_pos --;
1282 }
1283 break;
1284 case 'H': // Home : move cursor to the starting
1285 line.cur_pos = 0;
1286 break;
1287 case 'F': // End : move cursor to the end
1288 line.cur_pos = line.buf.size();
1289 break;
1290 }
1291 }
1292 }
1293 else if(seq[0] == 'O'){
1294 switch(seq[1]){
1295 case 'H': // Home : move cursor to the starting
1296 line.cur_pos = 0;
1297 break;
1298 case 'F': // End : move cursor to the end
1299 line.cur_pos = line.buf.size();
1300 break;
1301 }
1302 }
1303 return true;
1304 }
1305
1306
1307 // Procedure: _edit_line
1308 // Handle the character input from the user
_edit_line(std::string & s)1309 inline void Prompt::_edit_line(std::string &s){
1310 if(not (_cout << _prompt)){
1311 return;
1312 }
1313
1314 _history.emplace_back("");
1315 _line.reset();
1316 s.clear();
1317 for(char c;;){
1318 //if(_cin.read(&c, 1); not _cin.good()){
1319 if(c = ::fgetc(_is); ::ferror(_is)){
1320 s = _line.buf;
1321 return ;
1322 }
1323
1324 // if user hits tab
1325 if(static_cast<KEY>(c) == KEY::TAB){
1326 if(_line.buf.empty()){
1327 continue;
1328 }
1329 else if(_line.buf.rfind(' ', _line.cur_pos) != std::string::npos){
1330 _autocomplete_folder();
1331 continue;
1332 }
1333 else{
1334 _autocomplete_command();
1335 continue;
1336 //if(c=_autocomplete_iterate_command(); c<0){
1337 //if(c=_autocomplete_command(); c<0){
1338 // // something wrong happened
1339 // return;
1340 //}
1341 //else if(c == 0){
1342 // continue;
1343 //}
1344 }
1345 }
1346
1347 // Proceed to process character
1348 switch(static_cast<KEY>(c)){
1349 case KEY::ENTER:
1350 _history.pop_back(); // Remove the emptry string history added in beginning
1351 s = _line.buf;
1352 return ;
1353 case KEY::CTRL_A: // Go to the start of the line
1354 if(_line.cur_pos != 0){
1355 _line.cur_pos = 0;
1356 }
1357 _refresh_single_line(_line);
1358 break;
1359 case KEY::CTRL_B: // Move cursor to left
1360 if(_line.cur_pos > 0){
1361 _line.cur_pos --;
1362 }
1363 _refresh_single_line(_line);
1364 break;
1365 case KEY::CTRL_C:
1366 _history.pop_back(); // Remove the emptry string history added in beginning
1367 errno = EAGAIN;
1368 return ;
1369 case KEY::CTRL_D: // Remove the char at the right of cursor.
1370 // If the line is empty, act as end-of-file
1371 if(_line.buf.size() > 0){
1372 _key_delete(_line);
1373 _refresh_single_line(_line);
1374 }
1375 else{
1376 _history.pop_back();
1377 return;
1378 }
1379 break;
1380 case KEY::CTRL_E: // Move cursor to end of line
1381 if(_line.cur_pos != _line.buf.size()){
1382 _line.cur_pos = _line.buf.size();
1383 }
1384 _refresh_single_line(_line);
1385 break;
1386 case KEY::CTRL_F: // Move cursor to right
1387 if(_line.cur_pos != _line.buf.size()){
1388 _line.cur_pos ++;
1389 }
1390 _refresh_single_line(_line);
1391 break;
1392 case KEY::BACKSPACE: // CTRL_H is the same as BACKSPACE
1393 case KEY::CTRL_H:
1394 _key_backspace(_line);
1395 _refresh_single_line(_line);
1396 break;
1397 case KEY::CTRL_K: // Delete from current to EOF
1398 _line.buf.erase(_line.cur_pos, _line.buf.size()-_line.cur_pos);
1399 _refresh_single_line(_line);
1400 break;
1401 case KEY::CTRL_L: // Clear screen
1402 _clear_screen();
1403 _refresh_single_line(_line);
1404 break;
1405 case KEY::CTRL_N: // Next history command
1406 _key_next_history(_line);
1407 _refresh_single_line(_line);
1408 break;
1409 case KEY::CTRL_P: // Previous history command
1410 _key_prev_history(_line);
1411 _refresh_single_line(_line);
1412 break;
1413 case KEY::CTRL_T: // Swap current char with previous
1414 if(_line.cur_pos > 0){
1415 std::swap(_line.buf[_line.cur_pos], _line.buf[_line.cur_pos-1]);
1416 if(_line.cur_pos < _line.buf.size()){
1417 _line.cur_pos ++;
1418 }
1419 _refresh_single_line(_line);
1420 }
1421 break;
1422 case KEY::CTRL_U: // Delete whole line
1423 _line.cur_pos = 0;
1424 _line.buf.clear();
1425 _refresh_single_line(_line);
1426 break;
1427 case KEY::CTRL_W: // Delete previous word
1428 _key_delete_prev_word(_line);
1429 _refresh_single_line(_line);
1430 break;
1431 case KEY::ESC:
1432 if(not _key_handle_CSI(_line)){
1433 _history.pop_back(); // Remove the emptry string history added in beginning
1434 return;
1435 }
1436 _refresh_single_line(_line);
1437 break;
1438 default:
1439 _append_character(_line, c);
1440 break;
1441 }
1442 }
1443 }
1444
1445
1446 // Procedure: _refresh_single_line
1447 // Flush the current line buffer on screen
_refresh_single_line(LineInfo & l)1448 inline void Prompt::_refresh_single_line(LineInfo &l){
1449 // 1. Append "move cursor to left" in the output buffer
1450 // 2. Append buf to output buffer
1451 // 3. Append "erase to the right" to the output buffer
1452 // 4. Append "forward cursor" to the output buffer : Adjust cursor to correct pos
1453 // 5. Write output buffer to fd
1454
1455 static const std::string CR {"\r"}; // Carriage Return (set cursor to left)
1456 static const std::string EL {"\x1b[0K"}; // Erase in Line (clear from cursor to the end of the line)
1457
1458 auto len {l.buf.size()};
1459 auto pos {l.cur_pos};
1460 size_t start {0};
1461
1462 if(_prompt.length()+pos >= _columns){
1463 start += _prompt.length()+pos-_columns-1;
1464 len -= _prompt.length()+pos-_columns-1;
1465 pos += (_prompt.length()+pos-_columns-1);
1466 }
1467
1468 if(_prompt.length()+len > _columns){
1469 len -= (_prompt.length()+len-_columns);
1470 }
1471
1472 char seq[64];
1473 ::snprintf(seq, 64, "\r\x1b[%dC", (int)(pos+_prompt.length()));
1474
1475 _obuf.clear();
1476 _obuf.reserve(CR.length()+_prompt.length()+len+EL.length()+strlen(seq));
1477 _obuf.append(CR);
1478 _obuf.append(_prompt);
1479 _obuf.append(l.buf.data() + start, len);
1480 _obuf.append(EL);
1481 _obuf.append(seq, strlen(seq));
1482
1483 if(not (_cout << _obuf)){
1484 _cerr << "Refresh line fail\n";
1485 }
1486 }
1487
1488 }; // end of namespace prompt. -------------------------------------------------------------------
1489
1490 #endif
1491
1492