Line data Source code
1 : // Copyright (c) 2018-2024 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // This program is free software; you can redistribute it and/or modify
4 : // it under the terms of the GNU General Public License as published by
5 : // the Free Software Foundation; either version 2 of the License, or
6 : // (at your option) any later version.
7 : //
8 : // This program is distributed in the hope that it will be useful,
9 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 : // GNU General Public License for more details.
12 : //
13 : // You should have received a copy of the GNU General Public License along
14 : // with this program; if not, write to the Free Software Foundation, Inc.,
15 : // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16 :
17 :
18 : // self
19 : //
20 : #include "eventdispatcher/cui_connection.h"
21 :
22 : #include <eventdispatcher/fd_buffer_connection.h>
23 :
24 :
25 :
26 : // snaplogger
27 : //
28 : #include <snaplogger/message.h>
29 :
30 :
31 : // snapdev
32 : //
33 : #include <snapdev/not_reached.h>
34 : #include <snapdev/not_used.h>
35 :
36 :
37 : // C++
38 : //
39 : #include <deque>
40 : #include <iostream>
41 :
42 :
43 : // C
44 : //
45 : #include <fcntl.h>
46 : #include <ncurses.h>
47 : #include <readline/history.h>
48 : #include <readline/readline.h>
49 : #include <unistd.h>
50 :
51 :
52 : // last include
53 : //
54 : #include <snapdev/poison.h>
55 :
56 :
57 :
58 : namespace ed
59 : {
60 :
61 :
62 : namespace detail
63 : {
64 :
65 : /** \brief This is the actual implementation of the ncurses application.
66 : *
67 : * This class is what generates the two panels in the console and write
68 : * titles and handles the resize and input/output.
69 : *
70 : * The `cui_connection` is the higher level user interface that allows you
71 : * to write to the console output. There is nothing you can do in the
72 : * input window.
73 : *
74 : * \todo
75 : * Later we'll add a statistics window so we can show various things
76 : * that we know of (i.e. number of messages, size transferred, etc.)
77 : *
78 : * \note
79 : * This class is very heavily based on a class written by ulfalizer
80 : * and found on github.com here:
81 : *
82 : * https://github.com/ulfalizer/readline-and-ncurses
83 : *
84 : * See also a post about this class on Stackoverflow.com:
85 : *
86 : * https://stackoverflow.com/questions/691652/using-gnu-readline-how-can-i-add-ncurses-in-the-same-program#28709979
87 : */
88 : class ncurses_impl
89 : {
90 : public:
91 : typedef std::shared_ptr<ncurses_impl> pointer_t;
92 :
93 : class io_pipe_connection
94 : : public ed::fd_buffer_connection
95 : {
96 : public:
97 0 : io_pipe_connection(int fd, ncurses_impl * impl)
98 0 : : fd_buffer_connection(fd, ed::fd_connection::mode_t::FD_MODE_READ)
99 0 : , f_impl(impl)
100 : {
101 0 : }
102 :
103 : // avoid copies (simplify bare pointer management too)
104 : //
105 : io_pipe_connection(io_pipe_connection const & rhs) = delete;
106 : io_pipe_connection & operator = (io_pipe_connection const & rhs) = delete;
107 :
108 0 : virtual void process_line(std::string const & line) override
109 : {
110 0 : if(line.find("error:") != std::string::npos)
111 : {
112 0 : f_impl->output(line
113 : , cui_connection::color_t::RED
114 : , cui_connection::color_t::WHITE);
115 : }
116 0 : else if(line.find("warning:") != std::string::npos)
117 : {
118 0 : f_impl->output(line
119 : , cui_connection::color_t::MAGENTA
120 : , cui_connection::color_t::WHITE);
121 : }
122 0 : else if(line.find("success:") != std::string::npos)
123 : {
124 0 : f_impl->output(line
125 : , cui_connection::color_t::GREEN
126 : , cui_connection::color_t::WHITE);
127 : }
128 : else
129 : {
130 0 : f_impl->output(line);
131 : }
132 0 : }
133 :
134 : private:
135 : ncurses_impl * f_impl;
136 : };
137 :
138 0 : static pointer_t ptr()
139 : {
140 0 : if(f_cui_connection->f_impl == nullptr)
141 : {
142 0 : ncurses_impl::fatal_error("ptr() called with f_cui_connection->f_impl set to nullptr");
143 : }
144 0 : return f_cui_connection->f_impl;
145 : }
146 :
147 0 : static ncurses_impl::pointer_t create_ncurses(cui_connection * ce, std::string const & history_filename)
148 : {
149 0 : if(ce->f_impl == nullptr)
150 : {
151 : // can't use std::make_shared() as the constructor is private
152 : //
153 0 : ce->f_impl.reset(new ncurses_impl(ce, history_filename));
154 :
155 0 : pointer_t p(ptr());
156 :
157 : // we call the open from here instead of the constructor
158 : // because we need f_cui_connection->f_impl to be defined for
159 : // fatal_error() to work properly
160 : //
161 0 : p->open_ncurse();
162 0 : p->open_readline();
163 0 : p->ready();
164 0 : }
165 0 : return ce->f_impl;
166 : }
167 :
168 : ncurses_impl(ncurses_impl const & rhs) = delete;
169 : ncurses_impl & operator = (ncurses_impl const & rhs) = delete;
170 :
171 0 : ~ncurses_impl()
172 : {
173 0 : close_readline();
174 0 : close_ncurse();
175 :
176 0 : f_cui_connection = nullptr; // let the user create a new console later
177 0 : }
178 :
179 0 : bool process_read()
180 : {
181 0 : if(f_redisplay)
182 : {
183 0 : f_redisplay = false;
184 0 : win_input_redisplay(false);
185 : }
186 :
187 0 : while(!f_should_exit)
188 : {
189 : // Using getch() here instead would refresh stdscr, overwriting the
190 : // initial contents of the other windows on startup
191 : //
192 0 : int const c(wgetch(f_win_input));
193 :
194 : //output("got [" + std::to_string(c) + "]");
195 0 : switch(c)
196 : {
197 0 : case ERR:
198 : // f_win_input is non-blocking, this happens when the
199 : // input buffer is empty and we are ready to return to
200 : // snap_communicator
201 : //
202 0 : return f_should_exit;
203 :
204 : // at this time handling ESC is "tough" because it happens
205 : // with many keys and since ncurses and readline are handling
206 : // things in some different ways, I'm not too sure where to
207 : // look at before to make it work properly...
208 : //
209 : //case '\033':
210 : // f_should_exit = true;
211 : // break;
212 :
213 0 : case KEY_RESIZE:
214 0 : resize();
215 0 : break;
216 :
217 : // Ctrl-L -- redraw screen
218 0 : case '\f':
219 0 : clear_output();
220 0 : break;
221 :
222 0 : default:
223 0 : forward_to_readline(c);
224 0 : break;
225 :
226 : }
227 : }
228 :
229 0 : return f_should_exit; // always true here at the moment
230 : }
231 :
232 0 : void restore_fd(FILE * f, FILE * & n, io_pipe_connection::pointer_t & c)
233 : {
234 : // this is the pipe (read-side), we can just close everything
235 : //
236 : // WARNING: the "socket" (file descriptor) does not get closed
237 : // automatically by the snap_fd_connection so we do
238 : // that here "manually"
239 : //
240 0 : c->close();
241 0 : ed::communicator::instance()->remove_connection(c);
242 0 : c.reset();
243 :
244 : // f is the pipe (write-side) and it can directly be replaced
245 : // by the old stdout or stderr file descriptor
246 : //
247 0 : dup2(fileno(n), fileno(f));
248 :
249 : // and for the new file descriptor we do not need it anymore
250 : //
251 0 : fclose(n);
252 0 : n = nullptr;
253 0 : }
254 :
255 0 : void process_quit()
256 : {
257 0 : }
258 :
259 0 : void output(std::string const & line,
260 : cui_connection::color_t f = cui_connection::color_t::NORMAL,
261 : cui_connection::color_t b = cui_connection::color_t::NORMAL)
262 : {
263 0 : pointer_t p(ptr());
264 :
265 0 : if(!f_first_line)
266 : {
267 0 : wprintw(f_win_output, "\n");
268 : }
269 :
270 : // save all the lines in f_output vector so we can redraw it in
271 : // case of a resize
272 : //
273 : // one day we may work on Page Up/Down to scroll through
274 : // this buffer too!
275 : //
276 0 : p->f_output.push_back(line);
277 0 : while(p->f_output.size() > 1000)
278 : {
279 0 : p->f_output.pop_front();
280 : }
281 :
282 : // TODO: make this work when one of the colors is not set to NORMAL
283 : //
284 0 : if(f != cui_connection::color_t::NORMAL
285 0 : || b != cui_connection::color_t::NORMAL)
286 : {
287 0 : int const pair((static_cast<NCURSES_COLOR_T>(f) | (static_cast<NCURSES_COLOR_T>(b) << 4)) + 1);
288 0 : wattron(f_win_output, COLOR_PAIR(pair));
289 : }
290 :
291 0 : if(wprintw(f_win_output, "%s", line.c_str()) != OK)
292 : {
293 0 : fatal_error("wprintw() to output window failed");
294 : snapdev::NOT_REACHED();
295 : }
296 0 : if(wrefresh(p->f_win_output) != OK)
297 : {
298 0 : fatal_error("wrefresh() to output window failed");
299 : snapdev::NOT_REACHED();
300 : }
301 0 : f_first_line = false;
302 :
303 0 : if(f != cui_connection::color_t::NORMAL
304 0 : || b != cui_connection::color_t::NORMAL)
305 : {
306 0 : int const pair((static_cast<NCURSES_COLOR_T>(f) | (static_cast<NCURSES_COLOR_T>(b) << 4)) + 1);
307 0 : wattroff(f_win_output, COLOR_PAIR(pair));
308 : }
309 :
310 : // TODO: we could use a timer on this object that will
311 : // instantly timeout on the next run() loop so that
312 : // that way the cursor gets set only once
313 : //
314 0 : set_cursor();
315 0 : if(wrefresh(f_win_input) != OK)
316 : {
317 0 : fatal_error("wrefresh() failed");
318 : }
319 0 : }
320 :
321 0 : void clear_output()
322 : {
323 : // lose all output
324 : //
325 0 : f_output.clear();
326 :
327 : // makes the next refresh repaint the screen from scratch
328 : //
329 0 : if(clearok(curscr, TRUE) != OK)
330 : {
331 0 : fatal_error("clearok() failed in clear_output()");
332 : snapdev::NOT_REACHED();
333 : }
334 :
335 : // we will next be writing a first line again
336 : //
337 0 : f_first_line = true;
338 :
339 : // resize and reposition windows in case that got messed
340 : // up somehow
341 : //
342 0 : resize();
343 0 : }
344 :
345 0 : void refresh()
346 : {
347 0 : wrefresh(f_win_output);
348 0 : wrefresh(f_win_input);
349 0 : }
350 :
351 0 : void set_prompt(std::string const & prompt)
352 : {
353 0 : rl_callback_handler_install(prompt.c_str(), got_command);
354 0 : }
355 :
356 : private:
357 : /** \brief Duplicate one of stdout or stderr and create a pipe instead.
358 : *
359 : * For the rest of the software to be able to write to stdout and
360 : * stderr without having to overhaul the whole entire thing, we
361 : * want to hijack the stdout and stderr file descriptor and
362 : * replace it with a pipe.
363 : *
364 : * This function does that, but first it saves the existing stdout
365 : * and stderr in a new FILE object so that way we can still access
366 : * our terminal in ncurses.
367 : *
368 : * \warning
369 : * The pipe under Linux is limited to 64Kb. If we reach that limit
370 : * before we can read the data, then anything more will be lost.
371 : * (because we make the pipe non-block, if too much data is written,
372 : * it will fail.) It should not happen with the existing code, but
373 : * that's something to keep in mind.
374 : *
375 : * \param[in] f The file being updated (stdout or stderr).
376 : * \param[out] n Where the new ncurses terminal file descriptor gets
377 : * saved (so we transfer stdout or stderr to this FILE *
378 : * which this function creates and saves in this variable)
379 : * \param[out] c Where the new connection gets saved.
380 : */
381 0 : void initialize_fd(FILE * f, FILE * & n, io_pipe_connection::pointer_t & c)
382 : {
383 : // copy the existing fd
384 : //
385 0 : int const d(dup(fileno(f)));
386 0 : if(d == -1)
387 : {
388 0 : fatal_error("Could not duplicate file descriptor");
389 : }
390 :
391 : // create a new FILE object with that fd
392 : // ncurses will be using that fd for output/errors
393 : //
394 0 : n = fdopen(d, "a");
395 0 : if(n == nullptr)
396 : {
397 0 : fatal_error("Could not create FILE from new descriptor");
398 : }
399 :
400 : // create a pipe for the old stdout/stderr
401 : //
402 0 : int p[2]{0, 0};
403 0 : int const r(pipe2(p, O_NONBLOCK));
404 0 : if(r != 0)
405 : {
406 0 : fatal_error("Could not create a pipe to replace stdout or stderr");
407 : }
408 :
409 : // replace the stdout/stderr fd here
410 : // then close the duplicate pipe
411 : // note that this way the replacement is done atomically
412 : //
413 0 : int const fd(dup2(p[1], fileno(f))); // replace stdout or stderr here
414 0 : if(fd == -1)
415 : {
416 0 : fatal_error("Could not replace stdout or stderr with new fd from pipe");
417 : }
418 0 : if(::close(p[1]) == -1)
419 : {
420 0 : SNAP_LOG_WARNING
421 0 : << "could not close pipe "
422 : << p[1]
423 0 : << " after dup2() to "
424 : << fileno(f)
425 : << SNAP_LOG_SEND;
426 : }
427 :
428 : // create a communicator connection with the other side of the pipe
429 : // (note that in effect we are writing to ourselves, which means
430 : // the stdout and stderr streams must not be given more than 64Kb
431 : // in a row or the process will block/fail in weird ways.)
432 : //
433 0 : c = std::make_shared<io_pipe_connection>(p[0], this);
434 0 : if(!ed::communicator::instance()->add_connection(c))
435 : {
436 0 : fatal_error("could not add stdout/stderr stream replacement");
437 : }
438 0 : }
439 :
440 0 : void open_ncurse()
441 : {
442 : // setup locale
443 : //
444 0 : if(setlocale(LC_ALL, "") == nullptr)
445 : {
446 0 : fatal_error("Failed to set locale attributes from environment");
447 : }
448 :
449 : // transform the I/O organization so we can capture stdout and
450 : // stderr data and print it cleanly in the output window (otherwise
451 : // it appears wherever and the screen looks like crap.)
452 : //
453 : // so... we do the following steps:
454 : //
455 : // duplicate stdout
456 : // fdopen with duplicate of stdout
457 : //
458 : // create pipe A
459 : // dup2 pipe output (write-side) to stdout
460 : // create an fd connection with input (read-side)
461 : // add connection to communicator
462 : //
463 : // duplicate stderr
464 : // fdopen with duplicate of stderr
465 : //
466 : // create pipe B
467 : // dup2 pipe output (write-side) to stderr
468 : // create an fd connection with input (read-side)
469 : // add connection to communicator
470 : //
471 : // after that, the two new connections we created out pipes will
472 : // be able to read anything that gets written to stdout and stderr
473 : // (and we can have something in our connections telling us which
474 : // color to use so we can very easily distinguish both types)
475 : //
476 : // the duplicates of stdout and stdin are to be used in the
477 : // newterm() function when initializing our ncurses environment
478 : // which is why we do that work before initializing ncurses
479 : //
480 0 : initialize_fd(stdout, f_ncurses_stdout, f_stdout_pipe);
481 0 : initialize_fd(stderr, f_ncurses_stderr, f_stderr_pipe);
482 :
483 : // initialize screen with our moved terminal
484 : // (we don't actually need f_ncurses_stderr)
485 : //
486 0 : f_term = newterm(nullptr, f_ncurses_stdout, stdin);
487 0 : if(f_term == nullptr)
488 : {
489 0 : fatal_error("newterm() failed to initialize ncurses");
490 : snapdev::NOT_REACHED();
491 : }
492 0 : set_term(f_term);
493 :
494 0 : f_win_main = stdscr;
495 0 : if(f_win_main == nullptr)
496 : {
497 0 : fatal_error("initscr() failed to initialize ncurses");
498 : snapdev::NOT_REACHED();
499 : }
500 :
501 : // we've got a screen, we're in visual mode now
502 : //
503 0 : f_visual_mode = true;
504 :
505 : // initialize colors
506 : //
507 0 : if(has_colors())
508 : {
509 0 : if(start_color() != OK)
510 : {
511 0 : fatal_error("start_color() failed");
512 : snapdev::NOT_REACHED();
513 : }
514 0 : if(use_default_colors() != OK)
515 : {
516 0 : fatal_error("use_default_colors() failed");
517 : snapdev::NOT_REACHED();
518 : }
519 :
520 : // I'm not too sure how to handle this one...
521 : // at this time I create pairs with all the 8 default colors
522 : // (so that's 8 x 8 = 64 pairs)
523 : //
524 0 : for(NCURSES_COLOR_T f(-1); f < 8; ++f)
525 : {
526 0 : for(NCURSES_COLOR_T b(-1); b < 8; ++b)
527 : {
528 0 : int const pair(((f + 1) | ((b + 1) << 4)) + 1);
529 0 : init_pair(pair, f, b);
530 : }
531 : }
532 : }
533 :
534 0 : getmaxyx(f_win_main, f_screen_height, f_screen_width);
535 0 : if(f_screen_height < 5)
536 : {
537 0 : fatal_error("your console is not tall enough for this application");
538 : snapdev::NOT_REACHED();
539 : }
540 :
541 0 : if(cbreak() != OK)
542 : {
543 0 : fatal_error("cbreak() failed");
544 : snapdev::NOT_REACHED();
545 : }
546 0 : if(noecho() != OK)
547 : {
548 0 : fatal_error("noecho() failed");
549 : snapdev::NOT_REACHED();
550 : }
551 0 : if(nonl() != OK)
552 : {
553 0 : fatal_error("nonl() failed");
554 : snapdev::NOT_REACHED();
555 : }
556 0 : if(intrflush(nullptr, false) != OK)
557 : {
558 0 : fatal_error("intrflush() failed");
559 : snapdev::NOT_REACHED();
560 : }
561 :
562 : // IMPORTANT:
563 : // Do not enable keypad() as we want to pass unadulterated
564 : // input to readline()
565 : //
566 : // Only having keypad(win, TRUE) is the only way we can detect
567 : // whether the ESC key was used. I think the timeout is small
568 : // enough on a Linux box because it would be set to the minimum
569 : // of a keyboard repeat which is around 200ms.
570 :
571 : // Explicitly specify a "very visible" cursor to make sure it's
572 : // at least consistent when we turn the cursor on and off (maybe
573 : // it would make sense to query it and use the value we get back
574 : // too). "normal" vs. "very visible" makes no difference in
575 : // gnome-terminal or xterm. Let this fail for terminals that
576 : // do not support cursor visibility adjustments.
577 : //
578 0 : curs_set(2); // ignore errors
579 :
580 0 : draw_borders();
581 :
582 : // create two child windows
583 : //
584 0 : f_win_output = newwin(f_screen_height - 7, f_screen_width - 2, 1, 1);
585 0 : if(f_win_output == nullptr)
586 : {
587 0 : fatal_error("could not create output window");
588 : snapdev::NOT_REACHED();
589 : }
590 :
591 0 : f_win_input = newwin(4, f_screen_width - 2, f_screen_height - 5, 1);
592 0 : if(f_win_input == nullptr)
593 : {
594 0 : fatal_error("could not create input window");
595 : snapdev::NOT_REACHED();
596 : }
597 :
598 : // allow strings longer than the message window and show only the
599 : // last part if the string doesn't fit
600 : //
601 0 : if(scrollok(f_win_output, TRUE) != OK)
602 : {
603 0 : fatal_error("scrollok() failed; could not setup output window to scoll on large lines");
604 : snapdev::NOT_REACHED();
605 : }
606 : //if(scrollok(f_win_input, TRUE) != OK) -- TBD
607 : //{
608 : // fatal_error("scrollok() failed; could not setup input window to scoll on large lines");
609 : //}
610 :
611 : // we want to make the wgetch() function non-blocking so that way
612 : // other things can happen
613 : //
614 0 : wtimeout(f_win_input, 0);
615 :
616 : // to make sure the cursor gets at the right place
617 : //
618 0 : f_redisplay = true;
619 0 : }
620 :
621 0 : void close_ncurse()
622 : {
623 0 : if(f_visual_mode)
624 : {
625 0 : if(f_win_output != nullptr)
626 : {
627 0 : delwin(f_win_output);
628 0 : f_win_output = nullptr;
629 : }
630 :
631 0 : if(f_win_input != nullptr)
632 : {
633 0 : delwin(f_win_input);
634 0 : f_win_input = nullptr;
635 : }
636 :
637 : // f_win_main -- this is handled by f_term
638 :
639 : // make sure endwin() is only called in visual mode.
640 : //
641 : // also, it has to be called before we destroy the terminal
642 : // (f_term)
643 : //
644 : // Note: calling it twice does not seem to be supported
645 : // and messed with the cursor position.
646 : //
647 0 : if(endwin() != OK)
648 : {
649 0 : SNAP_LOG_WARNING
650 : << "endwin() failed"
651 : << SNAP_LOG_SEND;
652 : }
653 :
654 0 : if(f_term != nullptr)
655 : {
656 0 : delscreen(f_term);
657 0 : f_term = nullptr;
658 : }
659 :
660 0 : f_visual_mode = false;
661 : }
662 :
663 0 : if(f_stdout_pipe != nullptr)
664 : {
665 0 : restore_fd(stdout, f_ncurses_stdout, f_stdout_pipe);
666 : }
667 0 : if(f_stderr_pipe != nullptr)
668 : {
669 0 : restore_fd(stderr, f_ncurses_stderr, f_stderr_pipe);
670 : }
671 0 : }
672 :
673 0 : static int show_help(int count, int c)
674 : {
675 0 : snapdev::NOT_USED(count, c);
676 :
677 0 : f_cui_connection->process_help();
678 :
679 : // it worked, return 0
680 0 : return 0;
681 : }
682 :
683 0 : void open_readline()
684 : {
685 : // disable auto-completion
686 : //
687 0 : if(rl_bind_key('\t', rl_insert) != 0)
688 : {
689 0 : fatal_error("invalid key passed to rl_bind_key()");
690 : snapdev::NOT_REACHED();
691 : }
692 :
693 0 : if(rl_bind_keyseq("\\eOP" /* F1 */, &show_help) != 0)
694 : {
695 0 : fatal_error("invalid key (^[OP a.k.a. F1) sequence passed to rl_bind_keyseq");
696 : snapdev::NOT_REACHED();
697 : }
698 :
699 : // TODO: allow for not using history
700 : //
701 0 : using_history();
702 0 : read_history(f_history_filename.c_str());
703 :
704 : // let ncurses do all terminal and signal handling
705 : //
706 0 : rl_catch_signals = 0;
707 0 : rl_catch_sigwinch = 0;
708 0 : rl_deprep_term_function = nullptr;
709 0 : rl_prep_term_function = nullptr;
710 :
711 : // prevent readline from setting the LINES and COLUMNS environment
712 : // variables, which override dynamic size adjustments in ncurses.
713 : // When using the alternate readline interface (as we do here),
714 : // LINES and COLUMNS are not updated if the terminal is resized
715 : // between two calls to rl_callback_read_char() (which is almost
716 : // always the case)
717 : //
718 0 : rl_change_environment = 0;
719 :
720 : // Handle input by manually feeding characters to readline
721 : // (TODO: save those pointers so the close_readline() can restore
722 : // what there wasthere instead of assuming the defaults.)
723 : //
724 0 : f_has_handlers = true;
725 0 : rl_getc_function = readline_getc;
726 0 : rl_input_available_hook = readline_input_avail;
727 0 : rl_redisplay_function = readline_redisplay;
728 :
729 0 : set_prompt("> ");
730 0 : }
731 :
732 0 : void close_readline()
733 : {
734 0 : if(f_has_handlers)
735 : {
736 0 : rl_getc_function = rl_getc;
737 0 : rl_input_available_hook = nullptr;
738 0 : rl_redisplay_function = rl_redisplay;
739 0 : rl_callback_handler_remove();
740 0 : f_has_handlers = false;
741 : }
742 0 : }
743 :
744 0 : void ready()
745 : {
746 0 : output("Ready.\nType /help or F1 for help screen.");
747 0 : }
748 :
749 0 : void draw_borders()
750 : {
751 : // setup the background window with borders and names
752 : //
753 0 : wborder(f_win_main, 0, 0, 0, 0, 0, 0, 0, 0);
754 0 : mvwaddch(f_win_main, f_screen_height - 6, 0, ACS_LTEE);
755 0 : mvwhline(f_win_main, f_screen_height - 6, 1, ACS_HLINE, f_screen_width - 2);
756 0 : mvwaddch(f_win_main, f_screen_height - 6, f_screen_width - 1, ACS_RTEE);
757 0 : mvwprintw(f_win_main, 0, 2, " Output ");
758 0 : mvwprintw(f_win_main, f_screen_height - 6, 2, " Console (Ctrl-D on empty line to exit) ");
759 0 : wrefresh(f_win_main);
760 0 : }
761 :
762 0 : static int readline_getc(FILE * dummy)
763 : {
764 0 : pointer_t p(ptr());
765 :
766 0 : snapdev::NOT_USED(dummy);
767 :
768 0 : p->f_input_available = 0; // false
769 :
770 0 : return p->f_input;
771 0 : }
772 :
773 0 : void forward_to_readline(int c)
774 : {
775 : // extend character without sign
776 : //
777 0 : f_input = c;
778 :
779 0 : f_input_available = 1; // true
780 :
781 0 : rl_callback_read_char();
782 0 : }
783 :
784 0 : static int readline_input_avail()
785 : {
786 0 : pointer_t p(ptr());
787 0 : return p->f_input_available;
788 0 : }
789 :
790 0 : static void readline_redisplay()
791 : {
792 0 : pointer_t p(ptr());
793 0 : p->win_input_redisplay(false);
794 0 : }
795 :
796 0 : static void got_command(char * line)
797 : {
798 0 : pointer_t p(ptr());
799 :
800 0 : p->f_redisplay = true;
801 :
802 0 : if(line == nullptr)
803 : {
804 : // Ctrl-D pressed on empty line
805 : //
806 0 : p->f_should_exit = true;
807 :
808 0 : f_cui_connection->process_quit();
809 : }
810 : else
811 : {
812 0 : std::string l(line);
813 0 : if(!l.empty())
814 : {
815 : // add to history
816 : //
817 0 : add_history(line);
818 0 : write_history(p->f_history_filename.c_str());
819 :
820 0 : p->output(l);
821 :
822 0 : f_cui_connection->process_command(l);
823 : }
824 0 : free(line);
825 :
826 0 : p->win_input_redisplay(false);
827 0 : }
828 0 : }
829 :
830 0 : void win_output_redisplay(bool for_resize)
831 : {
832 0 : if(werase(f_win_output) != OK)
833 : {
834 0 : fatal_error("werase() of output window failed");
835 : snapdev::NOT_REACHED();
836 : }
837 :
838 0 : draw_borders();
839 :
840 : // redraw the output buffer
841 : //
842 : // DO NOT USE the output() function for a few reasons:
843 : //
844 : // 1. it will call wrefresh() on each call (argh!)
845 : // 2. it will re-add the buffer to itself
846 : // 3. the change of f_oupt may crash the for() loop
847 : //
848 0 : char const * nl = "";
849 0 : for(auto const & l : f_output)
850 : {
851 0 : if(wprintw(f_win_output, "%s%s", nl, l.c_str()) != OK)
852 : {
853 0 : fatal_error("wprintw() to output window failed");
854 : snapdev::NOT_REACHED();
855 : }
856 0 : nl = "\n";
857 : }
858 :
859 : // We batch window updates when resizing
860 : //
861 0 : if(for_resize)
862 : {
863 0 : if(wnoutrefresh(f_win_output) != OK)
864 : {
865 0 : fatal_error("wnoutrefresh() of output window failed");
866 : snapdev::NOT_REACHED();
867 : }
868 : }
869 : else
870 : {
871 0 : if(wrefresh(f_win_output) != OK)
872 : {
873 0 : fatal_error("wrefresh() of output window failed");
874 : snapdev::NOT_REACHED();
875 : }
876 : }
877 0 : }
878 :
879 : /** \brief Redraw the input window.
880 : *
881 : * Each time the user enters a character on the keyboard this
882 : * function gets called. It will redraw the input window to its
883 : * current state.
884 : *
885 : * By default the function will update the screen with a call
886 : * to wrefresh(). If you set the for_resize flag to true, then it
887 : * calls wnoutrefresh() instead, which marks the window for refresh
888 : * but does not refresh it right away.
889 : *
890 : * The function also positions the cursor.
891 : *
892 : * \param[in] for_resize Whether this is called by the resize or not.
893 : */
894 0 : void win_input_redisplay(bool for_resize)
895 : {
896 0 : if(werase(f_win_input) != OK)
897 : {
898 0 : fatal_error("werase() failed");
899 : snapdev::NOT_REACHED();
900 : }
901 :
902 : // this might write a string wider than the terminal currently,
903 : // so don't check for errors
904 : //
905 0 : mvwprintw(f_win_input, 0, 0, "%s%s", rl_display_prompt, rl_line_buffer);
906 :
907 0 : set_cursor();
908 :
909 : // we batch window updates when resizing
910 : //
911 0 : if(for_resize)
912 : {
913 0 : if(wnoutrefresh(f_win_input) != OK)
914 : {
915 0 : fatal_error("wnoutrefresh() failed");
916 : snapdev::NOT_REACHED();
917 : }
918 : }
919 : else
920 : {
921 0 : if(wrefresh(f_win_input) != OK)
922 : {
923 0 : fatal_error("wrefresh() failed");
924 : snapdev::NOT_REACHED();
925 : }
926 : }
927 0 : }
928 :
929 : /** \brief Place the cursor.
930 : *
931 : * The function calculates the position of the cursor in the input
932 : * window and then moves the cursor there.
933 : */
934 0 : void set_cursor()
935 : {
936 : // WARNING: we have two test because if the prompt includes a
937 : // tab then the size is different than without a tab
938 : // in there (at least that's the only thing that could
939 : // affect the calculation done in strnwidth())
940 : //
941 0 : size_t const prompt_width = strnwidth(rl_display_prompt, SIZE_MAX, 0);
942 : size_t const cursor_col = prompt_width +
943 0 : strnwidth(rl_line_buffer, rl_point, prompt_width);
944 :
945 0 : int const x(cursor_col % (f_screen_width - 2));
946 0 : int const y(cursor_col / (f_screen_width - 2));
947 0 : if(y >= 4)
948 : {
949 : // hide the cursor if it lies outside the window
950 : // otherwise it breaks the wmove() call
951 : //
952 0 : curs_set(0);
953 : }
954 : else
955 : {
956 0 : if(wmove(f_win_input, y, x) != OK)
957 : {
958 0 : fatal_error("wmove() failed");
959 : snapdev::NOT_REACHED();
960 : }
961 0 : curs_set(2);
962 : }
963 :
964 0 : }
965 :
966 : /** \brief We got a resize signal, make sure to redraw everything.
967 : *
968 : * Whenever the user resizes his console, this function gets called
969 : * to resize the windows and move them around as required. The
970 : * function updates the screen width and height so all the other
971 : * functions don't have to read those two parameters over and
972 : * over again.
973 : */
974 0 : void resize()
975 : {
976 : // get the new width and height of the screen
977 : //
978 0 : getmaxyx(f_win_main, f_screen_height, f_screen_width);
979 :
980 0 : if(f_screen_height < 5)
981 : {
982 0 : fatal_error("window too small after resize");
983 : snapdev::NOT_REACHED();
984 : }
985 :
986 0 : if(wresize(f_win_output, f_screen_height - 7, f_screen_width - 2) != OK)
987 : {
988 0 : fatal_error("wresize of output window failed");
989 : snapdev::NOT_REACHED();
990 : }
991 0 : if(wresize(f_win_input, 4, f_screen_width - 2) != OK)
992 : {
993 0 : fatal_error("wresize of input window failed");
994 : snapdev::NOT_REACHED();
995 : }
996 :
997 0 : if(mvwin(f_win_input, f_screen_height - 5, 1) != OK)
998 : {
999 0 : fatal_error("mvwin of input window failed");
1000 : snapdev::NOT_REACHED();
1001 : }
1002 :
1003 : // batch refreshes and commit them with doupdate()
1004 0 : win_output_redisplay(true);
1005 0 : win_input_redisplay(true);
1006 :
1007 0 : if(doupdate() != OK)
1008 : {
1009 0 : fatal_error("doupdate() after wresize() failed");
1010 : snapdev::NOT_REACHED();
1011 : }
1012 0 : }
1013 :
1014 : /** \brief End this software with an error.
1015 : *
1016 : * This function is expected to close the ncurse screen and then
1017 : * write an error message in the output before exiting with 1.
1018 : *
1019 : * \param[in] msg The error message to display.
1020 : */
1021 0 : [[noreturn]] static void fatal_error(char const * msg)
1022 : {
1023 : // don't use ptr() since it calls fatal_error() if the
1024 : // pointer is nullptr
1025 : //
1026 0 : if(f_cui_connection->f_impl != nullptr)
1027 : {
1028 0 : f_cui_connection->f_impl->close_ncurse();
1029 0 : f_cui_connection->f_impl.reset();
1030 : }
1031 0 : SNAP_LOG_FATAL
1032 : << msg
1033 : << SNAP_LOG_SEND;
1034 0 : std::cerr << msg << std::endl;
1035 0 : exit(1);
1036 : }
1037 :
1038 : /** \brief Calculate the width of a string.
1039 : *
1040 : * This function is used to calculate the cursor position.
1041 : *
1042 : * The function knows how to calculate the width of any character:
1043 : *
1044 : * \li multi-byte
1045 : * \li multi-column
1046 : * \li combining
1047 : *
1048 : * Unfortunately, this is a copy of the readline() function which is
1049 : * not being exported so we do not have access to it and have had to
1050 : * rewrite it here.
1051 : *
1052 : * The function returns the total width in columns of the string.
1053 : * The `n` parameter can be used to limit the number of characters
1054 : * to check. The `offset` can be used to ignore a certain number
1055 : * of characters at the start.
1056 : *
1057 : * \note
1058 : * The function will makes a guess for malformed strings (strings with
1059 : * invalid multi-byte characters).
1060 : *
1061 : * \param[in] s The string for which we calculate the width.
1062 : * \param[in] n The maximum number of characters to count or SIZE_MAX.
1063 : * \param[in] offset The offset at which to start calculating the size.
1064 : */
1065 0 : size_t strnwidth(char const * s, size_t n, size_t offset)
1066 : {
1067 0 : mbstate_t shift_state = mbstate_t();
1068 :
1069 : // Start in the initial shift state
1070 : //memset(&shift_state, '\0', sizeof(shift_state));
1071 :
1072 0 : size_t width(0);
1073 0 : size_t wc_len(0);
1074 0 : for(size_t i(0); i < n; i += wc_len)
1075 : {
1076 : // Extract the next multibyte character
1077 0 : wchar_t wc(0);
1078 0 : wc_len = mbrtowc(&wc, s + i, MB_CUR_MAX, &shift_state);
1079 0 : switch(wc_len)
1080 : {
1081 0 : case 0:
1082 : // Reached the end of the string
1083 0 : goto done;
1084 :
1085 0 : case static_cast<size_t>(-1):
1086 : case static_cast<size_t>(-2):
1087 : // Failed to extract character. Guess that each character is one
1088 : // byte/column wide each starting from the invalid character to
1089 : // keep things simple.
1090 0 : width += strnlen(s + i, n - i);
1091 0 : goto done;
1092 :
1093 : }
1094 :
1095 0 : if (wc == '\t')
1096 : {
1097 0 : width = ((width + offset + 8) & ~7) - offset;
1098 : }
1099 : else
1100 : {
1101 : // TODO: readline also outputs ~<letter> and the like for some
1102 : // non-printable characters
1103 : //
1104 0 : width += iswcntrl(wc) ? 2 : std::max(0, wcwidth(wc));
1105 : }
1106 : }
1107 :
1108 0 : done:
1109 0 : return width;
1110 : }
1111 :
1112 : private:
1113 : /** \brief Initialize the ncurses_impl object.
1114 : *
1115 : * This function saves the `cui_connection` pointer to the static
1116 : * pointer defined here.
1117 : *
1118 : * Note that you can't have more than one `ncurses_impl` at a time
1119 : * (not just because of that static, trust me!) The constructor
1120 : * of the `create_ncurses()` function makes sure of that, although
1121 : * it is not tested properly at this point.
1122 : *
1123 : * \note
1124 : * I use a bare pointer because it is a one to one relationship
1125 : * like a parent (`cui_connection`) and a child (`ncurses_impl`).
1126 : * If that goes wrong, then we've got a much better problem.
1127 : *
1128 : * \param[in] ce The `cui_connection` pointer.
1129 : */
1130 0 : ncurses_impl(cui_connection * ce, std::string const & history_filename)
1131 0 : {
1132 0 : f_cui_connection = ce;
1133 :
1134 : // keep the default is not specified
1135 : //
1136 0 : if(!history_filename.empty())
1137 : {
1138 0 : f_history_filename = history_filename;
1139 : }
1140 :
1141 : // if it starts with "~/", consider changing the "~" with "$HOME"
1142 : //
1143 0 : if(f_history_filename.length() > 1
1144 0 : && f_history_filename[0] == '~'
1145 0 : && f_history_filename[1] == '/')
1146 : {
1147 0 : char * home = getenv("HOME");
1148 0 : if(home != nullptr
1149 0 : && *home != '\0')
1150 : {
1151 : // replace the '~' with the $HOME contents
1152 : //
1153 0 : f_history_filename = home + f_history_filename.substr(1);
1154 : }
1155 : }
1156 :
1157 : // WARNING: our initialization required f_cui_connection to be defined
1158 : // so better not have anything in the constructor
1159 : // at this point...
1160 0 : }
1161 :
1162 : //static ncurses_impl::pointer_t f_nc;
1163 : static cui_connection * f_cui_connection; // initialized below (because it is static)
1164 : FILE * f_ncurses_stdout = nullptr;
1165 : FILE * f_ncurses_stderr = nullptr;
1166 : io_pipe_connection::pointer_t f_stdout_pipe = io_pipe_connection::pointer_t();
1167 : io_pipe_connection::pointer_t f_stderr_pipe = io_pipe_connection::pointer_t();
1168 : std::string f_history_filename = std::string("~/.snap_history");
1169 : SCREEN * f_term = nullptr;
1170 : WINDOW * f_win_main = nullptr;
1171 : WINDOW * f_win_output = nullptr;
1172 : WINDOW * f_win_input = nullptr;
1173 : int f_screen_width = 0;
1174 : int f_screen_height = 0;
1175 : std::deque<std::string> f_output = std::deque<std::string>();
1176 : bool f_visual_mode = false;
1177 : bool f_has_handlers = false;
1178 : bool f_should_exit = false;
1179 : bool f_first_line = true;
1180 : bool f_redisplay = true;
1181 : int f_input_available = 0;
1182 : int f_input = 0;
1183 : };
1184 :
1185 :
1186 : //ncurses_impl::pointer_t ncurses_impl::f_nc;
1187 : cui_connection * ncurses_impl::f_cui_connection = nullptr;
1188 :
1189 :
1190 :
1191 : } // namespace detail
1192 :
1193 :
1194 :
1195 0 : cui_connection::cui_connection(std::string const & history_filename)
1196 0 : : fd_connection(fileno(stdin), mode_t::FD_MODE_READ)
1197 : {
1198 0 : f_impl = detail::ncurses_impl::create_ncurses(this, history_filename);
1199 0 : }
1200 :
1201 0 : cui_connection::~cui_connection()
1202 : {
1203 0 : f_impl.reset();
1204 0 : }
1205 :
1206 0 : void cui_connection::output(std::string const & line)
1207 : {
1208 0 : f_impl->output(line);
1209 0 : }
1210 :
1211 0 : void cui_connection::output(std::string const & line, cui_connection::color_t f, cui_connection::color_t b)
1212 : {
1213 0 : f_impl->output(line, f, b);
1214 0 : }
1215 :
1216 0 : void cui_connection::clear_output()
1217 : {
1218 0 : f_impl->clear_output();
1219 0 : }
1220 :
1221 0 : void cui_connection::refresh()
1222 : {
1223 0 : f_impl->refresh();
1224 0 : }
1225 :
1226 0 : void cui_connection::set_prompt(std::string const & prompt)
1227 : {
1228 0 : f_impl->set_prompt(prompt);
1229 0 : }
1230 :
1231 0 : void cui_connection::process_read()
1232 : {
1233 0 : if(f_impl->process_read())
1234 : {
1235 : // we're done, user hit Ctrl-D or /quit
1236 : //
1237 0 : mark_done();
1238 : }
1239 0 : }
1240 :
1241 :
1242 : /** \brief Close the stdout and stderr connections.
1243 : *
1244 : * You must call this function whenever yours gets called.
1245 : *
1246 : * Whenever you create a console, it redirects the stdout and stderr
1247 : * to a couple of connections (using pipes). This is used to send
1248 : * the output to out output console instead of wherever on the screen.
1249 : *
1250 : * The quit must be called if you want to get rid of those two
1251 : * connections and thus have the snap_communicator::run() function
1252 : * returns as expected.
1253 : */
1254 0 : void cui_connection::process_quit()
1255 : {
1256 0 : f_impl->process_quit();
1257 0 : }
1258 :
1259 :
1260 : /** \brief Called whenever the Help key is hit.
1261 : *
1262 : * This callback gives you the opportunity to implement a function
1263 : * whenever the help key is hit. You may ignore that key entirely
1264 : * by not implementing this callback.
1265 : */
1266 0 : void cui_connection::process_help()
1267 : {
1268 0 : }
1269 :
1270 :
1271 : } // namespace ed
1272 : // vim: ts=4 sw=4 et
|