xaizek / pipedial (License: GPLv3 only) (since 2019-01-08)
One more tool for selecting something in console.
<root> / src / PromptRequest.cpp (823cb9a0028ade8a0f38ce036ad925efdd9ba245) (5,635B) (mode 100644) [raw]
// pipedial -- terminal element picker
// Copyright (C) 2019 xaizek <xaizek@posteo.net>
//
// This file is part of pipedial.
//
// pipedial is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// pipedial 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 pipedial.  If not, see <https://www.gnu.org/licenses/>.

#include "PromptRequest.hpp"

#include <stdio.h>

#include <csignal>
#include <cstdlib>
#include <cstring>

#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

#include <readline/readline.h>

#include "cursed/Input.hpp"
#include "cursed/Prompt.hpp"
#include "cursed/Screen.hpp"

#include "utils.hpp"

namespace {

// Saves and restores signal handling information.
class SignalSaver
{
public:
    // Captures state of the signal.
    explicit SignalSaver(int sigNum) : sigNum(sigNum)
    {
        if (sigaction(sigNum, nullptr, &sa) == -1) {
            throw std::runtime_error("Failed wit save signal");
        }
    }

    // Restores state of the signal.
    ~SignalSaver()
    {
        (void)sigaction(sigNum, &sa, nullptr);
    }

    SignalSaver(const SignalSaver &rhs) = delete;
    SignalSaver(SignalSaver &&rhs) = delete;
    SignalSaver & operator=(const SignalSaver &rhs) = delete;
    SignalSaver & operator=(SignalSaver &&rhs) = delete;

private:
    int sigNum;          // Signal number.
    struct sigaction sa; // Saved signal handling information.
};

}

PromptResult::PromptResult() : hasInput(false)
{ }

PromptResult::PromptResult(std::wstring result)
    : result(std::move(result)), hasInput(true)
{ }

PromptRequest::PromptRequest(cursed::Input &input, cursed::Screen &screen,
                             cursed::Prompt &promptArea,
                             inputChangedFunc onInputChanged)
    : input(input), screen(screen), promptArea(promptArea),
      onInputChanged(std::move(onInputChanged))
{
    screen.showCursor();
}

PromptRequest::~PromptRequest()
{
    screen.hideCursor();
}

PromptResult
PromptRequest::prompt(const std::wstring &invitation,
                      const std::wstring &initial)
{
    initialValue = toNarrow(initial);
    lastValue = initialValue;

    static PromptRequest *instance;
    instance = this;

    // Prevent displaying completion menu, which could mess up output.
    rl_completion_display_matches_hook = [](char *[], int, int) {};

    // Disable filename completion.
    rl_completion_entry_function = [](const char *, int) -> char * {
        return nullptr;
    };

    rl_startup_hook = []() { instance->rlStartup(); return 0; };
    rl_redisplay_function = []() { instance->rlRedisplay(); };
    rl_input_available_hook = []() { return instance->rlInputAvailable(); };
    rl_getc_function = [](FILE *){ return instance->rlGetcFunction(); };

    rl_catch_sigwinch = 0;
    rl_change_environment = 0;

    SignalSaver sigwinchSaver(SIGWINCH);
    std::unique_ptr<char, decltype(&std::free)> line {
        readline(toNarrow(invitation).c_str()), &std::free
    };

    if (line == nullptr) {
        return {};
    }
    return toWide(line.get());
}

void
PromptRequest::rlStartup()
{
    rl_extend_line_buffer(initialValue.length() + 1U);
    std::strcpy(rl_line_buffer, initialValue.c_str());
    rl_point = initialValue.length();
    rl_end = rl_point;
}

void
PromptRequest::rlRedisplay()
{
    if (onInputChanged && rl_line_buffer != lastValue) {
        onInputChanged(toWide(rl_line_buffer));
        lastValue = rl_line_buffer;
    }

    std::wstring prompt = toWide(rl_display_prompt);
    std::wstring beforeCursor = toWide({ rl_line_buffer,
                                         rl_line_buffer + rl_point });
    std::wstring afterCursor = toWide({ rl_line_buffer + rl_point,
                                        rl_line_buffer + rl_end });

    int promptWidth = wcswidth(prompt.c_str(), prompt.length());
    int cursorCol = promptWidth
                  + wcswidth(beforeCursor.c_str(), beforeCursor.length());

    promptArea.setText(prompt + beforeCursor + afterCursor, cursorCol);
    screen.draw();
}

int
PromptRequest::rlInputAvailable()
{
    if (!inputBuf.empty()) {
        return true;
    }

    while (cursed::InputElement ie = input.peek()) {
        if (!ie.isTerminalResize()) {
            return true;
        }

        updateScreen();
    }

    return false;
}

int
PromptRequest::rlGetcFunction()
{
    fetchInput();
    int c = inputBuf.front();
    inputBuf.erase(inputBuf.cbegin());
    return c;
}

void
PromptRequest::fetchInput()
{
    if (!inputBuf.empty()) {
        return;
    }

    while (true) {
        cursed::InputElement ie = input.read();
        if (ie.isEndOfInput()) {
            inputBuf.push_back(EOF);
            break;
        } else if (ie.isTerminalResize()) {
            updateScreen();
        } else {
            // Turn `wchar_t` into `char[]` to be able to return one byte at a
            // time.
            std::string narrow = toNarrow(std::wstring(1, ie));
            inputBuf.insert(inputBuf.cend(), narrow.cbegin(), narrow.cend());
            break;
        }
    }
}

void
PromptRequest::updateScreen()
{
    rl_resize_terminal();
    screen.resize();
    screen.draw();
}
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/pipedial

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

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