xaizek / pipedial (License: GPLv3 only) (since 2019-01-08)
One more tool for selecting something in console.
Commit 849987b66547ff0db9f8a66075ee54c583863bce

Extract libcursedrl library (turn to submodule)
Author: xaizek
Author date (UTC): 2019-03-24 22:00
Committer name: xaizek
Committer date (UTC): 2019-03-24 22:00
Parent(s): ae261a73cb7919615d421856f88ce9417ef0d5c2
Signing key: 99DC5E4DB05F6BE2
Tree: eecd5869cb882d9b7af4312fb8378cfd8f2d47dd
File Lines added Lines deleted
.gitmodules 3 0
libs/cursedrl 1 0
src/PipeDial.cpp 6 6
src/PromptRequest.cpp 0 300
src/PromptRequest.hpp 0 132
File .gitmodules changed (mode: 100644) (index f42dd1f..4b15847)
1 1 [submodule "libs/cursed"] [submodule "libs/cursed"]
2 2 path = libs/cursed path = libs/cursed
3 3 url = https://code.reversed.top/user/xaizek/libcursed url = https://code.reversed.top/user/xaizek/libcursed
4 [submodule "libs/cursedrl"]
5 path = libs/cursedrl
6 url = https://code.reversed.top/user/xaizek/libcursedrl
File libs/cursedrl added (mode: 160000) (index 0000000..194ab84)
1 Subproject commit 194ab84b1f4185a7451f33146bfff2a812457013
File src/PipeDial.cpp changed (mode: 100644) (index 9cb8999..4dd2874)
26 26 #include "cursed/Prompt.hpp" #include "cursed/Prompt.hpp"
27 27 #include "cursed/Track.hpp" #include "cursed/Track.hpp"
28 28
29 #include "cursedrl/PromptRequest.hpp"
30
29 31 #include "vle/KeyDispatcher.hpp" #include "vle/KeyDispatcher.hpp"
30 32 #include "vle/Mode.hpp" #include "vle/Mode.hpp"
31 33 #include "vle/Modes.hpp" #include "vle/Modes.hpp"
32 34
33 #include "PromptRequest.hpp"
34
35 35 PipeDial::PipeDial(std::wstring titleText, std::vector<std::wstring> initLines) PipeDial::PipeDial(std::wstring titleText, std::vector<std::wstring> initLines)
36 36 : lines(std::move(initLines)), input(cursed::Keypad::Disabled), : lines(std::move(initLines)), input(cursed::Keypad::Disabled),
37 37 title(std::move(titleText)), quit(false) title(std::move(titleText)), quit(false)
 
... ... PipeDial::buildNormalMode()
193 193 tmpTrack.addItem(&list); tmpTrack.addItem(&list);
194 194 tmpTrack.addItem(&prompt); tmpTrack.addItem(&prompt);
195 195
196 PromptRequest request(screen, prompt);
196 cursedrl::PromptRequest request(screen, prompt);
197 197 screen.replaceTopWidget(&tmpTrack); screen.replaceTopWidget(&tmpTrack);
198 if (PromptResult r = request.prompt(L"Edit and accept: ",
199 list.getCurrent())) {
198 if (cursedrl::PromptResult r = request.prompt(L"Edit and accept: ",
199 list.getCurrent())) {
200 200 result = r.getResult(); result = r.getResult();
201 201 quit = true; quit = true;
202 202 } else { } else {
 
... ... PipeDial::buildNormalMode()
243 243 filterList.setItems(filtered); filterList.setItems(filtered);
244 244 }; };
245 245
246 PromptRequest request(screen, prompt);
246 cursedrl::PromptRequest request(screen, prompt);
247 247 request.setOnInputChanged(update); request.setOnInputChanged(update);
248 248 screen.replaceTopWidget(&tmpTrack); screen.replaceTopWidget(&tmpTrack);
249 249 if (request.prompt(L"&/", L"")) { if (request.prompt(L"&/", L"")) {
File src/PromptRequest.cpp deleted (index 3e637d7..0000000)
1 // pipedial -- terminal element picker
2 // Copyright (C) 2019 xaizek <xaizek@posteo.net>
3 //
4 // This file is part of pipedial.
5 //
6 // pipedial is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // pipedial is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with pipedial. If not, see <https://www.gnu.org/licenses/>.
18
19 #include "PromptRequest.hpp"
20
21 #include <stdio.h>
22
23 #include <csignal>
24 #include <cstdlib>
25 #include <cstring>
26
27 #include <memory>
28 #include <stdexcept>
29 #include <string>
30 #include <utility>
31
32 #include <readline/history.h>
33 #include <readline/readline.h>
34
35 #include "cursed/Input.hpp"
36 #include "cursed/Prompt.hpp"
37 #include "cursed/Screen.hpp"
38 #include "cursed/utils.hpp"
39
40 namespace {
41
42 // Saves and restores signal handling information.
43 class SignalSaver
44 {
45 public:
46 // Captures state of the signal.
47 explicit SignalSaver(int sigNum) : sigNum(sigNum)
48 {
49 if (sigaction(sigNum, nullptr, &sa) == -1) {
50 throw std::runtime_error("Failed wit save signal");
51 }
52 }
53
54 // Restores state of the signal.
55 ~SignalSaver()
56 {
57 (void)sigaction(sigNum, &sa, nullptr);
58 }
59
60 SignalSaver(const SignalSaver &rhs) = delete;
61 SignalSaver(SignalSaver &&rhs) = delete;
62 SignalSaver & operator=(const SignalSaver &rhs) = delete;
63 SignalSaver & operator=(SignalSaver &&rhs) = delete;
64
65 private:
66 int sigNum; // Signal number.
67 struct sigaction sa; // Saved signal handling information.
68 };
69
70 }
71
72 void
73 PromptRequest::loadHistory(const std::string &path)
74 {
75 static_cast<void>(read_history(path.c_str()));
76 }
77
78 void
79 PromptRequest::saveHistory(const std::string &path)
80 {
81 static_cast<void>(write_history(path.c_str()));
82 }
83
84 PromptResult::PromptResult() : hasInput(false)
85 { }
86
87 PromptResult::PromptResult(std::wstring result)
88 : result(std::move(result)), hasInput(true)
89 { }
90
91 PromptRequest::PromptRequest(cursed::Screen &screen, cursed::Prompt &promptArea)
92 : input(cursed::Keypad::Disabled), screen(screen), promptArea(promptArea)
93 {
94 screen.showCursor();
95
96 promptHi.setBold(true);
97 }
98
99 PromptRequest::~PromptRequest()
100 {
101 screen.hideCursor();
102 }
103
104 void
105 PromptRequest::setOnInputChanged(inputChangedFunc newOnInputChanged)
106 {
107 onInputChanged = std::move(newOnInputChanged);
108 }
109
110 void
111 PromptRequest::setCompleter(completerFunc newCompleter)
112 {
113 completer = std::move(newCompleter);
114 }
115
116 PromptResult
117 PromptRequest::prompt(const std::wstring &invitation,
118 const std::wstring &initial)
119 {
120 initialValue = cursed::toNarrow(initial);
121 lastValue = initialValue;
122
123 static PromptRequest *instance;
124 instance = this;
125
126 // Prevent displaying completion menu, which could mess up output.
127 rl_completion_display_matches_hook = [](char *[], int, int) {};
128
129 // Disable filename completion.
130 rl_completion_entry_function = [](const char *, int) -> char * {
131 return nullptr;
132 };
133
134 if (completer) {
135 rl_attempted_completion_function = [](const char text[], int start,
136 int end) {
137 return instance->rlComplete(text, start, end);
138 };
139 } else {
140 rl_attempted_completion_function = nullptr;
141 }
142
143 rl_startup_hook = []() { instance->rlStartup(); return 0; };
144 rl_redisplay_function = []() { instance->rlRedisplay(); };
145 rl_input_available_hook = []() { return instance->rlInputAvailable(); };
146 rl_getc_function = [](FILE *){ return instance->rlGetcFunction(); };
147
148 rl_catch_sigwinch = 0;
149 rl_change_environment = 0;
150
151 // We substitute Escape code with Ctrl-C in our input handler.
152 rl_bind_key('\x03', [](int a, int b) {
153 static rl_command_func_t *accept = rl_named_function("accept-line");
154
155 instance->cancelled = true;
156 return accept(a, b);
157 });
158
159 cancelled = false;
160
161 SignalSaver sigwinchSaver(SIGWINCH);
162 std::unique_ptr<char, decltype(&std::free)> line {
163 readline(cursed::toNarrow(invitation).c_str()), &std::free
164 };
165
166 if (line == nullptr) {
167 cancelled = true;
168 } else if (*line != '\0') {
169 add_history(line.get());
170 }
171
172 return (cancelled ? std::wstring() : cursed::toWide(line.get()));
173 }
174
175 void
176 PromptRequest::rlStartup()
177 {
178 rl_extend_line_buffer(initialValue.length() + 1U);
179 std::strcpy(rl_line_buffer, initialValue.c_str());
180 rl_point = initialValue.length();
181 rl_end = rl_point;
182 }
183
184 void
185 PromptRequest::rlRedisplay()
186 {
187 if (onInputChanged && rl_line_buffer != lastValue) {
188 onInputChanged(cursed::toWide(rl_line_buffer));
189 lastValue = rl_line_buffer;
190 }
191
192 std::wstring prompt = cursed::toWide(rl_display_prompt);
193 std::wstring beforeCursor = cursed::toWide({ rl_line_buffer,
194 rl_line_buffer + rl_point });
195 std::wstring afterCursor = cursed::toWide({ rl_line_buffer + rl_point,
196 rl_line_buffer + rl_end });
197
198 int promptWidth = wcswidth(prompt.c_str(), prompt.length());
199 int cursorCol = promptWidth
200 + wcswidth(beforeCursor.c_str(), beforeCursor.length());
201
202 promptArea.setText(promptHi(prompt + beforeCursor + afterCursor),
203 cursorCol);
204 screen.draw();
205 }
206
207 int
208 PromptRequest::rlInputAvailable()
209 {
210 if (!inputBuf.empty()) {
211 return true;
212 }
213
214 while (cursed::InputElement ie = input.peek()) {
215 if (!ie.isTerminalResize()) {
216 return true;
217 }
218
219 updateScreen();
220 }
221
222 return false;
223 }
224
225 int
226 PromptRequest::rlGetcFunction()
227 {
228 fetchInput();
229 int c = inputBuf.front();
230 inputBuf.erase(inputBuf.cbegin());
231 return c;
232 }
233
234 void
235 PromptRequest::fetchInput()
236 {
237 if (!inputBuf.empty()) {
238 return;
239 }
240
241 while (true) {
242 cursed::InputElement ie = input.read();
243 if (ie.isEndOfInput()) {
244 inputBuf.push_back(EOF);
245 break;
246 } else if (ie.isTerminalResize()) {
247 updateScreen();
248 } else {
249 // Turn `wchar_t` into `char[]` to be able to return one byte at a
250 // time.
251 std::string narrow = cursed::toNarrow(std::wstring(1, ie));
252 if (narrow.empty()) {
253 // Conversion failed, end input.
254 inputBuf.push_back(EOF);
255 break;
256 }
257
258 if (narrow == "\x1b" && !input.peek()) {
259 narrow = "\x03";
260 }
261 inputBuf.insert(inputBuf.cend(), narrow.cbegin(), narrow.cend());
262 break;
263 }
264 }
265 }
266
267 void
268 PromptRequest::updateScreen()
269 {
270 rl_resize_terminal();
271 screen.resize();
272 screen.draw();
273 }
274
275 char **
276 PromptRequest::rlComplete(const char text[], int start, int /*end*/)
277 {
278 std::string prefix(rl_line_buffer, rl_line_buffer + start);
279 std::vector<std::wstring> completions = completer(cursed::toWide(prefix),
280 cursed::toWide(text));
281 if (completions.empty()) {
282 return nullptr;
283 }
284
285 const std::size_t n = completions.size();
286 auto array = static_cast<char **>(std::malloc(sizeof(char *)*(n + 1U)));
287 for (unsigned int i = 0U; i < n; ++i) {
288 try {
289 array[i] = strdup(cursed::toNarrow(completions[i]).c_str());
290 } catch (...) {
291 for (unsigned int j = 0U; j < i; ++j) {
292 std::free(array[j]);
293 }
294 std::free(array);
295 throw;
296 }
297 }
298 array[n] = nullptr;
299 return array;
300 }
File src/PromptRequest.hpp deleted (index 51b0cca..0000000)
1 // pipedial -- terminal element picker
2 // Copyright (C) 2019 xaizek <xaizek@posteo.net>
3 //
4 // This file is part of pipedial.
5 //
6 // pipedial is free software: you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation, either version 3 of the License, or
9 // (at your option) any later version.
10 //
11 // pipedial is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
15 //
16 // You should have received a copy of the GNU General Public License
17 // along with pipedial. If not, see <https://www.gnu.org/licenses/>.
18
19 #ifndef PIPEDIAL__PROMPTREQUEST_HPP__
20 #define PIPEDIAL__PROMPTREQUEST_HPP__
21
22 #include <functional>
23 #include <string>
24 #include <vector>
25
26 #include "cursed/ColorTree.hpp"
27 #include "cursed/Input.hpp"
28
29 namespace cursed {
30
31 class Prompt;
32 class Screen;
33
34 }
35
36 // Describes result of a prompt request.
37 class PromptResult
38 {
39 friend class PromptRequest;
40
41 private:
42 // No input was provided (request was declined/cancelled).
43 PromptResult();
44 // An input was provided.
45 PromptResult(std::wstring result);
46
47 public:
48 // Retrieves input.
49 std::wstring getResult() const
50 { return result; }
51
52 // Checks if result contains input.
53 operator bool() const
54 { return hasInput; }
55
56 private:
57 std::wstring result; // Input.
58 bool hasInput; // Whether input makes sense.
59 };
60
61 // Provides text editing facility via prompt.
62 class PromptRequest
63 {
64 // Type of callback function invoked when input is changed.
65 using inputChangedFunc = std::function<void(const std::wstring &newInput)>;
66 // Type of callback function invoked to generate completions.
67 typedef std::function<
68 std::vector<std::wstring>(const std::wstring &prefix,
69 const std::wstring &what)
70 > completerFunc;
71
72 public:
73 // Remembers arguments and makes cursor visible.
74 PromptRequest(cursed::Screen &screen, cursed::Prompt &promptArea);
75 // Cleans up and hides cursor.
76 ~PromptRequest();
77
78 PromptRequest(const PromptRequest &rhs) = delete;
79 PromptRequest(PromptRequest &&rhs) = delete;
80 PromptRequest & operator=(const PromptRequest &rhs) = delete;
81 PromptRequest & operator=(PromptRequest &&rhs) = delete;
82
83 public:
84 // Loads history from the file.
85 static void loadHistory(const std::string &path);
86 // Stores history to the file.
87 static void saveHistory(const std::string &path);
88
89 public:
90 // Sets callback to be invoked when input is changed.
91 void setOnInputChanged(inputChangedFunc newOnInputChanged);
92 // Sets callback to be invoked to perform completion.
93 void setCompleter(completerFunc newCompleter);
94
95 // Performs the prompting. Blocks until it's done. Might throw
96 // `std::runtime_error`.
97 PromptResult prompt(const std::wstring &invitation,
98 const std::wstring &initial);
99
100 private:
101 // Readline's callback for initial preparations.
102 void rlStartup();
103 // Readline's callback for drawing prompt.
104 void rlRedisplay();
105 // Readline's callback for checking for more input.
106 int rlInputAvailable();
107 // Readline's callback for getting input.
108 int rlGetcFunction();
109 // Readline's callback for getting completions.
110 char ** rlComplete(const char text[], int start, int end);
111
112 // Reads more input if `inputBuf` is empty populating it as a result. After
113 // the function is invoked `inputBuf` is never empty.
114 void fetchInput();
115
116 // Resizes and redraws screen.
117 void updateScreen();
118
119 private:
120 cursed::Input input; // Source of input events.
121 cursed::Screen &screen; // Screen manager.
122 cursed::Prompt &promptArea; // Widget that displays the prompt.
123 inputChangedFunc onInputChanged; // Callback invoked when input is changed.
124 completerFunc completer; // Completion generator.
125 std::string initialValue; // Initial value of the prompt.
126 std::string lastValue; // Last seen input value.
127 std::vector<int> inputBuf; // Input queue.
128 cursed::Format promptHi; // Visual style of the prompt.
129 bool cancelled; // Whether input was cancelled.
130 };
131
132 #endif // PIPEDIAL__PROMPTREQUEST_HPP__
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