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