xaizek / dit (License: GPLv3) (since 2018-12-07)
Command-line task keeper that remembers all old values and is meant to combine several orthogonal features to be rather flexible in managing items.
<root> / src / integration.cpp (7df6765dfd65c0c2be1208feefcc491e580c09b0) (10KiB) (mode 100644) [raw]
// Copyright (C) 2015 xaizek <xaizek@posteo.net>
//
// This file is part of dit.
//
// dit is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public
// License as published by the Free Software Foundation.
//
// dit is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with dit.  If not, see <http://www.gnu.org/licenses/>.

#include "integration.hpp"

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>

#include <cstdio>

#include <fstream>
#include <iostream>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>

#include <boost/algorithm/string/trim.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream_buffer.hpp>
#include <boost/optional.hpp>
#include <boost/scope_exit.hpp>

#include "utils/fs.hpp"
#include "utils/memory.hpp"

namespace io = boost::iostreams;

static std::pair<unsigned int, unsigned int> getTerminalSize();
static void writeBufferFile(const std::string &path, const std::string &key,
                            const std::string &current);
static bool editBufferFile(const std::string &path);
static std::string readEditedValue(const std::string &path);

/**
 * @brief Base for hidden internals of RedirectToPager class.
 */
class RedirectToPager::Impl
{
public:
    //! Virtual destructor.
    virtual ~Impl() = default;
};

/**
 * @brief Redirects standard output into a pager.
 *
 * The redirection happens only if number of lines exceeds screen height,
 * otherwise lines are just dumped onto the screen as is.
 */
class PagerRedirect : public RedirectToPager::Impl
{
public:
    /**
     * @brief Custom stream buffer that spawns pager for large outputs only.
     *
     * Collect up to terminal height lines.  If buffer is closed with this limit
     * not reached, it prints lines on std::cout.  If we hit the limit in the
     * process of output, it opens a pager and feeds it all collected output and
     * everything that comes next.
     */
    class ScreenPageBuffer
    {
    public:
        //! Type of character used by this buffer.
        using char_type = char;
        //! Category of functionality provided by this buffer implementation.
        using category = boost::iostreams::sink_tag;

    public:
        /**
         * @brief Constructs the buffer.
         *
         * @param pagerCmd Command to invoke a pager.
         * @param screenHeight Height of terminal in lines.
         * @param out Storage for output stream buffer backed up by a file.
         */
        ScreenPageBuffer(const std::string &pagerCmd,
                         unsigned int screenHeight,
                         io::stream_buffer<io::file_descriptor_sink> *out);
        /**
         * @brief Dumps output onto the screen or waits for pager to finish.
         */
        ~ScreenPageBuffer();

    public:
        /**
         * @brief Writes @p n characters from @p s.
         *
         * @param s Character buffer.
         * @param n Size of the buffer.
         *
         * @returns Number of successfully written characters.
         */
        std::streamsize write(const char s[], std::streamsize n);

    private:
        /**
         * @brief Writes single character.
         *
         * @param c Character to write.
         *
         * @returns @c true on success, @c false otherwise.
         */
        bool put(char c);
        /**
         * @brief Opens pager for output.
         */
        void openPager();

    private:
        //! Whether redirection into pager is enabled.
        bool redirectToPager = false;
        //! Number of output lines collected so far.
        unsigned int nLines = 0U;
        //! Command to invoke a pager.
        std::string pagerCmd;
        //! Height of terminal in lines.
        unsigned int screenHeight;
        //! Output collected so far.
        std::string buffer;
        //! Process id of a pager.
        pid_t pid;

        /**
         * @brief Pointer to buffer stored in RedirectToPager.
         *
         * This is not by value, because ScreenPageBuffer needs to be copyable.
         */
        io::stream_buffer<io::file_descriptor_sink> *out;
    };

public:
    /**
     * @brief Replaces buffer of @c std::cout with ScreenPageBuffer.
     *
     * @param pagerCmd Command to invoke a pager.
     */
    PagerRedirect(const std::string &pagerCmd)
        : screenPageBuffer(pagerCmd, getTerminalSize().second, &out)
    {
        rdbuf = std::cout.rdbuf(&screenPageBuffer);
    }

    /**
     * @brief Restores original buffer of @c std::cout.
     */
    ~PagerRedirect()
    {
        // Flush the stream to make sure that we put all contents we want
        // through the custom stream buffer.
        std::cout.flush();

        std::cout.rdbuf(rdbuf);
    }

private:
    //! This is stored for ScreenPageBuffer class.
    io::stream_buffer<io::file_descriptor_sink> out;
    //! Custom buffer implementation.
    io::stream_buffer<ScreenPageBuffer> screenPageBuffer;
    //! Original buffer of @c std::cout.
    std::streambuf *rdbuf;
};

using ScreenPageBuffer = PagerRedirect::ScreenPageBuffer;

ScreenPageBuffer::ScreenPageBuffer(const std::string &pagerCmd,
                                   unsigned int screenHeight,
                               io::stream_buffer<io::file_descriptor_sink> *out)
    : pagerCmd(pagerCmd), screenHeight(screenHeight - 1U), out(out)
{
}

ScreenPageBuffer::~ScreenPageBuffer()
{
    if (redirectToPager) {
        out->close();
        int wstatus;
        waitpid(pid, &wstatus, 0);
    } else {
        std::cout << buffer;
    }
}

std::streamsize
ScreenPageBuffer::write(const char s[], std::streamsize n)
{
    for (std::streamsize i = 0U; i < n; ++i) {
        if (!put(s[i])) {
            return i;
        }
    }
    return n;
}

bool
ScreenPageBuffer::put(char c)
{
    if (redirectToPager) {
        return boost::iostreams::put(*out, c);
    }

    if (c == '\n') {
        ++nLines;
    }

    if (nLines > screenHeight) {
        openPager();
        redirectToPager = true;
        for (char c : buffer) {
            if (!boost::iostreams::put(*out, c)) {
                return false;
            }
        }
        return boost::iostreams::put(*out, c);
    }

    buffer.push_back(c);
    return true;
}

void
ScreenPageBuffer::openPager()
{
    int pipePair[2];
    if (pipe(pipePair) != 0) {
        throw std::runtime_error("Failed to create a pipe");
    }
    BOOST_SCOPE_EXIT_ALL(pipePair) { close(pipePair[0]); };

    pid = fork();
    if (pid == -1) {
        close(pipePair[1]);
        throw std::runtime_error("Fork has failed");
    }
    if (pid == 0) {
        close(pipePair[1]);
        if (dup2(pipePair[0], STDIN_FILENO) == -1) {
            _Exit(EXIT_FAILURE);
        }
        close(pipePair[0]);
        execlp("/bin/sh", "sh", "-c", pagerCmd.c_str(),
               static_cast<char *>(nullptr));
        _Exit(127);
    }

    out->open(io::file_descriptor_sink(pipePair[1],
                                       boost::iostreams::close_handle));
}

RedirectToPager::RedirectToPager(const std::string &pagerCmd)
    : impl(isOutputToTerminal() ? make_unique<PagerRedirect>(pagerCmd)
                                : nullptr)
{
}

RedirectToPager::~RedirectToPager() = default;

/**
 * @brief Retrieves terminal width and height in characters.
 *
 * @returns Pair of actual terminal width and height, or maximum possible values
 *          of the type.
 */
static std::pair<unsigned int, unsigned int>
getTerminalSize()
{
    winsize ws;
    if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) != 0) {
        return {
            std::numeric_limits<unsigned int>::max(),
            std::numeric_limits<unsigned int>::max()
        };
    }

    return { ws.ws_col, ws.ws_row };
}

boost::optional<std::string>
editValue(const std::string &key, const std::string &current)
{
    // Generate name of a temporary file and write the file.
    TempFile tmpFile("buf");
    writeBufferFile(tmpFile, key, current);

    if (!editBufferFile(tmpFile)) {
        return {};
    }

    return readEditedValue(tmpFile);
}

/**
 * @brief Writes buffer file with which user will be presented.
 *
 * @param path Path to temporary file.
 * @param key Key (name of the value).
 * @param current Current value to be edited.
 */
static void
writeBufferFile(const std::string &path, const std::string &key,
                const std::string &current)
{
    std::ofstream ofs(path);
    if (!ofs) {
        throw std::runtime_error("Failed to open temporary file: " + path);
    }

    // TODO: add project name and description of action to the file.
    // TODO: maybe additional text should be placed at the bottom.

    // Write current value and instructions to the file.
    ofs << "# Edit value for the \"" << key << "\" below.  First (this) line "
           "will be ignored.\n" << (current.empty() ? "\n" : current);
}

/**
 * @brief Invokes editor to edit the file.
 *
 * @param path Path to the file that should be opened.
 *
 * @returns @c true on success and @c false on cancellation/error.
 */
static bool
editBufferFile(const std::string &path)
{
    // TODO: maybe check for empty file and treat it as an error.

    const char *editorVar = std::getenv("EDITOR");
    std::string editor = (editorVar == NULL) ? "vim" : editorVar;

    int status = std::system((editor + " " + path).c_str());
    return (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS);
}

/**
 * @brief Reads new value from a file.
 *
 * @param path Path to temporary file.
 *
 * @returns The value.
 */
static std::string
readEditedValue(const std::string &path)
{
    std::ifstream ifs(path);
    if (!ifs) {
        throw std::runtime_error("Failed to open temporary file: " + path);
    }

    // Discard first line.
    ifs.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    // Read the rest of the file.
    std::string value {
        std::istreambuf_iterator<char>(ifs),
        std::istreambuf_iterator<char>()
    };

    boost::trim_if(value, [](char c) { return std::isspace(c) || c == '\n'; } );
    return value;
}

bool
isOutputToTerminal()
{
    return isatty(fileno(stdout));
}

unsigned int
getTerminalWidth()
{
    return getTerminalSize().first;
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/dit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/dit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master