LCOV - code coverage report
Current view: top level - snapwebsites - snap_console.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 1 381 0.3 %
Date: 2019-12-15 17:13:15 Functions: 2 47 4.3 %
Legend: Lines: hit not hit

          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

Generated by: LCOV version 1.13