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