diff options
-rw-r--r-- | butl/buildfile | 1 | ||||
-rw-r--r-- | butl/pager | 86 | ||||
-rw-r--r-- | butl/pager.cxx | 191 |
3 files changed, 278 insertions, 0 deletions
diff --git a/butl/buildfile b/butl/buildfile index de51ad5..f075e30 100644 --- a/butl/buildfile +++ b/butl/buildfile @@ -8,6 +8,7 @@ lib{butl}: \ {hxx ixx cxx}{ filesystem } \ {hxx }{ multi-index } \ {hxx }{ optional } \ +{hxx cxx}{ pager } \ {hxx ixx txx cxx}{ path } \ {hxx }{ path-io } \ {hxx }{ path-map } \ diff --git a/butl/pager b/butl/pager new file mode 100644 index 0000000..4b39c2e --- /dev/null +++ b/butl/pager @@ -0,0 +1,86 @@ +// file : butl/pager -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUTL_PAGER +#define BUTL_PAGER + +#include <string> +#include <vector> +#include <iostream> + +#include <butl/process> +#include <butl/fdstream> + +namespace butl +{ + // Try to run the output through a pager program, such as more or less (no + // pun intended, less is used by default). If the default pager program is + // used, then the output is indented so that 80-character long lines will + // appear centered in the terminal. If the default pager program fails to + // start, then the output is sent directly to STDOUT. + // + // If the pager program is specified and is empty, then no pager is used + // and the output is sent directly to STDOUT. + // + // Throw std::system_error if there are problems with the pager program. + // + // Typical usage: + // + // try + // { + // pager p ("help for foo"); + // ostream& os (p.stream ()); + // + // os << "Foo is such and so ..."; + // + // if (!p.wait ()) + // ... // Pager program returned non-zero status. + // } + // catch (const std::system_error& e) + // { + // cerr << "pager error: " << e.what () << endl; + // } + // + class pager: protected std::streambuf + { + public: + ~pager () {wait ();} + + // If verbose is true, then print (to STDERR) the pager command line. + // + pager (const std::string& name, + bool verbose = false, + const std::string* pager = nullptr, + const std::vector<std::string>* pager_options = nullptr); + + std::ostream& + stream () {return os_.is_open () ? os_ : std::cout;} + + bool + wait (); + + // The streambuf output interface that implements indentation. You can + // override it to implement custom output pre-processing. + // + protected: + using int_type = std::streambuf::int_type; + using traits_type = std::streambuf::traits_type; + + virtual int_type + overflow (int_type); + + virtual int + sync (); + + private: + process p_; + ofdstream os_; + + std::string indent_; + int_type prev_ = '\n'; // Previous character. + std::streambuf* buf_ = nullptr; + }; +}; + +#endif // BUTL_PAGER diff --git a/butl/pager.cxx b/butl/pager.cxx new file mode 100644 index 0000000..4d0d9eb --- /dev/null +++ b/butl/pager.cxx @@ -0,0 +1,191 @@ +// file : butl/pager.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <butl/pager> + +#ifndef _WIN32 +# include <unistd.h> // close(), STDOUT_FILENO +# include <sys/ioctl.h> // ioctl() +#else +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include <windows.h> // GetConsoleScreenBufferInfo(), GetStdHandle() +# include <io.h> // _close() +#endif + +#include <chrono> +#include <thread> // this_thread::sleep_for() +#include <cstring> // strchr() +#include <system_error> + +using namespace std; + +namespace butl +{ + pager:: + pager (const string& name, + bool verbose, + const string* pager, + const vector<string>* pager_options) + { + // If we are using the default pager, try to get the terminal width + // so that we can center the output. + // + if (pager == nullptr) + { + size_t col (0); + +#ifndef _WIN32 +# ifdef TIOCGWINSZ + struct winsize w; + if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &w) == 0) + col = static_cast<size_t> (w.ws_col); +# endif +#else +#error TODO: needs testing + CONSOLE_SCREEN_BUFFER_INFO w; + if (GetConsoleScreenBufferInfo (GetStdHandle (STD_OUTPUT_HANDLE), &w)) + col = static_cast<size_t> (w.srWindow.Right - w.srWindow.Left + 1); +#endif + if (col > 80) + indent_.assign ((col - 80) / 2, ' '); + } + + vector<const char*> args; + string prompt; + + if (pager != nullptr) + { + if (pager->empty ()) + return; // No pager should be used. + + args.push_back (pager->c_str ()); + } + else + { + // By default try less (again, no pun intended). + // + prompt = "-Ps" + name + " (press q to quit, h for help)"; + + args.push_back ("less"); + args.push_back ("-R"); // Handle ANSI color. + args.push_back (prompt.c_str ()); + } + + // Add extra pager options. + // + if (pager_options != nullptr) + for (const string& o: *pager_options) + args.push_back (o.c_str ()); + + args.push_back (nullptr); + + if (verbose) + { + for (const char* const* p (args.data ()); *p != nullptr; ++p) + { + if (p != args.data ()) + cerr << ' '; + + // Quote if empty or contains spaces. + // + bool q (**p == '\0' || strchr (*p, ' ') != nullptr); + + if (q) + cerr << '"'; + + cerr << *p; + + if (q) + cerr << '"'; + } + cerr << endl; + } + + // Ignore errors and go without a pager unless the pager was specified + // by the user. + // + try + { + p_ = process (args.data (), -1); // Redirect child's STDIN to a pipe. + + // Wait a bit and see if the pager has exited before reading anything + // (e.g., because exec() couldn't find the program). If you know a + // cleaner way to handle this, let me know (and no, a select()-based + // approach doesn't work; the pipe is buffered and therefore is always + // ready for writing). + // + this_thread::sleep_for (chrono::milliseconds (50)); + + bool r; + if (p_.try_wait (r)) + { +#ifndef _WIN32 + ::close (p_.out_fd); +#else + _close (p_.out_fd); +#endif + if (pager != nullptr) + throw system_error (ECHILD, system_category ()); + } + else + os_.open (p_.out_fd); + } + catch (const process_error& e) + { + if (e.child ()) + { + cerr << args[0] << ": unable to execute: " << e.what () << endl; + exit (1); + } + + // Ignore unless it was a user-specified pager. + // + if (pager != nullptr) + throw; // Re-throw as system_error. + } + + // Setup the indentation machinery. + // + if (!indent_.empty ()) + buf_ = stream ().rdbuf (this); + } + + bool pager:: + wait () + { + // Teardown the indentation machinery. + // + if (buf_ != nullptr) + { + stream ().rdbuf (buf_); + buf_ = nullptr; + } + + os_.close (); + return p_.wait (); + } + + pager::int_type pager:: + overflow (int_type c) + { + if (prev_ == '\n' && c != '\n') // Don't indent blanks. + { + auto n (static_cast<streamsize> (indent_.size ())); + + if (buf_->sputn (indent_.c_str (), n) != n) + return traits_type::eof (); + } + + prev_ = c; + return buf_->sputc (c); + } + + int pager:: + sync () + { + return buf_->pubsync (); + } +} |