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