xaizek / libcursedrl (License: GPLv3+) (since 2019-03-24)
libcursed extension for integration with readline.
<root> / PromptRequest.cpp (aaf8c276c97af0d983abbed159ec7896b410f395) (8,604B) (mode 100644) [raw]
// libcursedrl -- libcursed extension for integration with readline
// 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/history.h>
#include <readline/readline.h>

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

using namespace cursedrl;

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.
};

// Hides cursor when an object of this type leaves scope.
class AutoHideCursor
{
public:
    // Remembers argument.
    explicit AutoHideCursor(cursed::Screen &screen) : screen(screen)
    { }

    // Hides cursor.
    ~AutoHideCursor() try {
        screen.hideCursor();
    } catch (...) { }

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

private:
    cursed::Screen &screen; // Screen on which to operate.
};

}

void
PromptRequest::loadHistory(const std::string &path)
{
    static_cast<void>(read_history(path.c_str()));
}

void
PromptRequest::saveHistory(const std::string &path)
{
    static_cast<void>(write_history(path.c_str()));
}

PromptResult::PromptResult(std::wstring input, bool accepted)
    : input(std::move(input)), accepted(accepted)
{ }

PromptRequest::PromptRequest(cursed::Screen &screen, cursed::Prompt &promptArea)
    : input(cursed::Keypad::Disabled), screen(screen), promptArea(promptArea)
{
    promptHi.setBold(true);
}

void
PromptRequest::setOnInputChanged(inputChangedFunc newOnInputChanged)
{
    onInputChanged = std::move(newOnInputChanged);
}

void
PromptRequest::setCompleter(completerFunc newCompleter)
{
    completer = std::move(newCompleter);
}

PromptResult
PromptRequest::prompt(const std::wstring &invitation,
                      const std::wstring &initial)
{
    screen.showCursor(&promptArea);
    AutoHideCursor autoHideCursor(screen);

    initialValue = cursed::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;
    };

    if (completer) {
        rl_attempted_completion_function = [](const char text[], int start,
                                              int end) {
            return instance->rlComplete(text, start, end);
        };
    } else {
        rl_attempted_completion_function = 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;

    // We substitute Escape code with Ctrl-C in our input handler.
    rl_bind_key('\x03', [](int a, int b) {
        static rl_command_func_t *accept = rl_named_function("accept-line");

        instance->cancelled = true;
        return accept(a, b);
    });

    cancelled = false;

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

    if (line == nullptr) {
        cancelled = true;
    } else if (*line != '\0') {
        add_history(line.get());
    }

    std::wstring input = (line == nullptr ? L"" : cursed::toWide(line.get()));
    return PromptResult(input, !cancelled);
}

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(cursed::toWide(rl_line_buffer));
        lastValue = rl_line_buffer;
    }

    std::wstring prompt = cursed::toWide(rl_display_prompt);
    std::wstring beforeCursor = cursed::toWide({ rl_line_buffer,
                                                 rl_line_buffer + rl_point });
    std::wstring afterCursor = cursed::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(promptHi(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 = cursed::toNarrow(std::wstring(1, ie));
            if (narrow.empty()) {
                // Conversion failed, end input.
                inputBuf.push_back(EOF);
                break;
            }

            if (narrow == "\x1b" && !input.peek()) {
                narrow = "\x03";
            }
            inputBuf.insert(inputBuf.cend(), narrow.cbegin(), narrow.cend());
            break;
        }
    }
}

void
PromptRequest::updateScreen()
{
    rl_resize_terminal();
    screen.resize();
    screen.draw();
}

char **
PromptRequest::rlComplete(const char text[], int start, int /*end*/)
{
    std::string prefix(rl_line_buffer, rl_line_buffer + start);
    std::vector<std::wstring> completions = completer(cursed::toWide(prefix),
                                                      cursed::toWide(text));
    if (completions.empty()) {
        return nullptr;
    }

    const std::size_t n = completions.size();
    auto array = static_cast<char **>(std::malloc(sizeof(char *)*(n + 1U)));
    for (unsigned int i = 0U; i < n; ++i) {
        try {
            array[i] = strdup(cursed::toNarrow(completions[i]).c_str());
        } catch (...) {
            for (unsigned int j = 0U; j < i; ++j) {
                std::free(array[j]);
            }
            std::free(array);
            throw;
        }
    }
    array[n] = nullptr;
    return array;
}
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/libcursedrl

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

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