LCOV - code coverage report
Current view: top level - eventdispatcher - cui_connection.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 1 382 0.3 %
Date: 2022-06-18 10:10:36 Functions: 2 47 4.3 %
Legend: Lines: hit not hit

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

Generated by: LCOV version 1.13