File src/PipeDial.cpp added (mode: 100644) (index 0000000..8052571) |
|
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 "PipeDial.hpp" |
|
20 |
|
|
|
21 |
|
#include <string> |
|
22 |
|
#include <utility> |
|
23 |
|
#include <vector> |
|
24 |
|
|
|
25 |
|
#include "cursed/Prompt.hpp" |
|
26 |
|
#include "cursed/Track.hpp" |
|
27 |
|
|
|
28 |
|
#include "vle/KeyDispatcher.hpp" |
|
29 |
|
#include "vle/Mode.hpp" |
|
30 |
|
#include "vle/Modes.hpp" |
|
31 |
|
|
|
32 |
|
#include "PromptRequest.hpp" |
|
33 |
|
|
|
34 |
|
// Help message. |
|
35 |
|
static const std::wstring helpText = LR"( |
|
36 |
|
SUMMARY OF PIPEDIAL SHORTCUTS |
|
37 |
|
============================= |
|
38 |
|
|
|
39 |
|
{Escape} -- abort partially typed shortcut. |
|
40 |
|
|
|
41 |
|
Normal Mode |
|
42 |
|
----------- |
|
43 |
|
|
|
44 |
|
q -- quit the application with no selection. |
|
45 |
|
|
|
46 |
|
h -- enter Help mode. |
|
47 |
|
|
|
48 |
|
{Enter} -- select current item and quit. |
|
49 |
|
|
|
50 |
|
e -- edit current item and accept the result. |
|
51 |
|
|
|
52 |
|
[count]gg -- put cursor on the first or [count]-th element. |
|
53 |
|
|
|
54 |
|
[count]G -- put cursor on the last or [count]-th element. |
|
55 |
|
|
|
56 |
|
[count]j -- move cursor [count] (1 by default) elements down. |
|
57 |
|
|
|
58 |
|
[count]k -- move cursor [count] (1 by default) elements up. |
|
59 |
|
|
|
60 |
|
Help Mode |
|
61 |
|
--------- |
|
62 |
|
|
|
63 |
|
h -- return to Normal mode.)"; |
|
64 |
|
|
|
65 |
|
PipeDial::PipeDial(std::wstring titleText, std::vector<std::wstring> lines) |
|
66 |
|
: title(std::move(titleText)), help(helpText), quit(false) |
|
67 |
|
{ |
|
68 |
|
list.setItems(std::move(lines)); |
|
69 |
|
|
|
70 |
|
track.addItem(&title); |
|
71 |
|
track.addItem(&list); |
|
72 |
|
track.addItem(&inputBuf); |
|
73 |
|
|
|
74 |
|
std::vector<vle::Mode> allModes; |
|
75 |
|
allModes.emplace_back(buildNormalMode()); |
|
76 |
|
allModes.emplace_back(buildHelpMode()); |
|
77 |
|
modes.setModes(std::move(allModes)); |
|
78 |
|
modes.switchTo("normal"); |
|
79 |
|
} |
|
80 |
|
|
|
81 |
|
vle::Mode |
|
82 |
|
PipeDial::buildNormalMode() |
|
83 |
|
{ |
|
84 |
|
vle::Mode normalMode("normal"); |
|
85 |
|
|
|
86 |
|
normalMode.setUsesCount(true); |
|
87 |
|
normalMode.addShortcut({ L"G", [&](int count) { |
|
88 |
|
if (count < 0) { |
|
89 |
|
list.moveToLast(); |
|
90 |
|
} else { |
|
91 |
|
list.moveToPos(count); |
|
92 |
|
} |
|
93 |
|
}, "go to the last or [count]-th item" }); |
|
94 |
|
normalMode.addShortcut({ L"gg", [&](int count) { |
|
95 |
|
if (count < 0) { |
|
96 |
|
list.moveToFirst(); |
|
97 |
|
} else { |
|
98 |
|
list.moveToPos(count); |
|
99 |
|
} |
|
100 |
|
}, "go to the first or [count]-th item" }); |
|
101 |
|
normalMode.addShortcut({ L"j", [&](int count) { |
|
102 |
|
list.moveDown(count < 0 ? 1 : count); |
|
103 |
|
}, "go to item below" }); |
|
104 |
|
normalMode.addShortcut({ L"k", [&](int count) { |
|
105 |
|
list.moveUp(count < 0 ? 1 : count); |
|
106 |
|
}, "go to item above" }); |
|
107 |
|
normalMode.addShortcut({ L"q", [&]() { |
|
108 |
|
quit = true; |
|
109 |
|
}, "quit the application with no selection" }); |
|
110 |
|
normalMode.addShortcut({ L"\n", [&]() { |
|
111 |
|
quit = true; |
|
112 |
|
result = list.getCurrent(); |
|
113 |
|
}, "select current item and quit" }); |
|
114 |
|
normalMode.addShortcut({ L"h", [&]() { |
|
115 |
|
modes.switchTo("help"); |
|
116 |
|
screen.setMainWidget(&help); |
|
117 |
|
}, "display help" }); |
|
118 |
|
normalMode.addShortcut({ L"e", [&]() { |
|
119 |
|
cursed::Prompt prompt; |
|
120 |
|
cursed::Track tmpTrack; |
|
121 |
|
tmpTrack.addItem(&title); |
|
122 |
|
tmpTrack.addItem(&list); |
|
123 |
|
tmpTrack.addItem(&prompt); |
|
124 |
|
|
|
125 |
|
PromptRequest request(input, screen, prompt); |
|
126 |
|
screen.setMainWidget(&tmpTrack); |
|
127 |
|
if (PromptResult r = request.prompt(L"Edit and accept: ", |
|
128 |
|
list.getCurrent())) { |
|
129 |
|
result = r.getResult(); |
|
130 |
|
quit = true; |
|
131 |
|
} else { |
|
132 |
|
screen.setMainWidget(&track); |
|
133 |
|
} |
|
134 |
|
}, "edit and accept" }); |
|
135 |
|
|
|
136 |
|
return normalMode; |
|
137 |
|
} |
|
138 |
|
|
|
139 |
|
vle::Mode |
|
140 |
|
PipeDial::buildHelpMode() |
|
141 |
|
{ |
|
142 |
|
vle::Mode helpMode("help"); |
|
143 |
|
|
|
144 |
|
helpMode.addShortcut({ L"h", [&]() { |
|
145 |
|
modes.switchTo("normal"); |
|
146 |
|
screen.setMainWidget(&track); |
|
147 |
|
}, "display help" }); |
|
148 |
|
helpMode.addShortcut({ L"j", [&]() { |
|
149 |
|
help.scrollDown(); |
|
150 |
|
}, "go to item below" }); |
|
151 |
|
helpMode.addShortcut({ L"k", [&]() { |
|
152 |
|
help.scrollUp(); |
|
153 |
|
}, "go to item above" }); |
|
154 |
|
|
|
155 |
|
return helpMode; |
|
156 |
|
} |
|
157 |
|
|
|
158 |
|
std::wstring |
|
159 |
|
PipeDial::run() |
|
160 |
|
{ |
|
161 |
|
screen.setMainWidget(&track); |
|
162 |
|
screen.draw(); |
|
163 |
|
|
|
164 |
|
vle::KeyDispatcher dispatcher; |
|
165 |
|
|
|
166 |
|
while (cursed::InputElement ie = input.read()) { |
|
167 |
|
if (ie.isTerminalResize()) { |
|
168 |
|
screen.resize(); |
|
169 |
|
screen.draw(); |
|
170 |
|
continue; |
|
171 |
|
} |
|
172 |
|
|
|
173 |
|
if (!ie.isFunctionalKey()) { |
|
174 |
|
dispatcher.dispatch(ie); |
|
175 |
|
} |
|
176 |
|
|
|
177 |
|
if (quit) { |
|
178 |
|
break; |
|
179 |
|
} |
|
180 |
|
|
|
181 |
|
inputBuf.setText(dispatcher.getPendingInput()); |
|
182 |
|
screen.draw(); |
|
183 |
|
} |
|
184 |
|
|
|
185 |
|
return result; |
|
186 |
|
} |
File src/PipeDial.hpp added (mode: 100644) (index 0000000..5e2a893) |
|
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__PIPEDIAL_HPP__ |
|
20 |
|
#define PIPEDIAL__PIPEDIAL_HPP__ |
|
21 |
|
|
|
22 |
|
#include "cursed/Init.hpp" |
|
23 |
|
#include "cursed/Input.hpp" |
|
24 |
|
#include "cursed/Label.hpp" |
|
25 |
|
#include "cursed/List.hpp" |
|
26 |
|
#include "cursed/Screen.hpp" |
|
27 |
|
#include "cursed/Text.hpp" |
|
28 |
|
#include "cursed/Track.hpp" |
|
29 |
|
|
|
30 |
|
#include "vle/Modes.hpp" |
|
31 |
|
|
|
32 |
|
#include <string> |
|
33 |
|
#include <vector> |
|
34 |
|
|
|
35 |
|
// Core state and behaviour of the application. |
|
36 |
|
class PipeDial |
|
37 |
|
{ |
|
38 |
|
public: |
|
39 |
|
// Performs initialization. |
|
40 |
|
PipeDial(std::wstring titleText, std::vector<std::wstring> lines); |
|
41 |
|
|
|
42 |
|
PipeDial(const PipeDial &rhs) = delete; |
|
43 |
|
PipeDial(PipeDial &&rhs) = delete; |
|
44 |
|
PipeDial & operator=(const PipeDial &rhs) = delete; |
|
45 |
|
PipeDial & operator=(PipeDial &&rhs) = delete; |
|
46 |
|
|
|
47 |
|
public: |
|
48 |
|
// Runs the app. Returns user's pick, empty result means "nothing was |
|
49 |
|
// picked". |
|
50 |
|
std::wstring run(); |
|
51 |
|
|
|
52 |
|
private: |
|
53 |
|
// Constructs normal mode. |
|
54 |
|
vle::Mode buildNormalMode(); |
|
55 |
|
// Constructs help mode. |
|
56 |
|
vle::Mode buildHelpMode(); |
|
57 |
|
|
|
58 |
|
private: |
|
59 |
|
cursed::Init cursesInit; // Curses initialization manager. |
|
60 |
|
cursed::Screen screen; // Screen manager. |
|
61 |
|
cursed::Input input; // Source of input events. |
|
62 |
|
|
|
63 |
|
vle::Modes modes; // Modes manager. |
|
64 |
|
|
|
65 |
|
cursed::Label title; // Title of the list. |
|
66 |
|
cursed::List list; // List of items. |
|
67 |
|
cursed::Label inputBuf; // Input buffer for incomplete sequences. |
|
68 |
|
|
|
69 |
|
cursed::Text help; // Contents of the help mode. |
|
70 |
|
|
|
71 |
|
cursed::Track track; // Track of the normal mode. |
|
72 |
|
|
|
73 |
|
bool quit; // Whether quitting was requested. |
|
74 |
|
std::wstring result; // Picked value. |
|
75 |
|
}; |
|
76 |
|
|
|
77 |
|
#endif // PIPEDIAL__PIPEDIAL_HPP__ |
File src/main.cpp changed (mode: 100644) (index d681106..cf796e7) |
26 |
26 |
#include <utility> |
#include <utility> |
27 |
27 |
#include <vector> |
#include <vector> |
28 |
28 |
|
|
29 |
|
#include "cursed/Init.hpp" |
|
30 |
|
#include "cursed/Input.hpp" |
|
31 |
|
#include "cursed/Label.hpp" |
|
32 |
|
#include "cursed/List.hpp" |
|
33 |
|
#include "cursed/Pos.hpp" |
|
34 |
|
#include "cursed/Prompt.hpp" |
|
35 |
|
#include "cursed/Screen.hpp" |
|
36 |
|
#include "cursed/Size.hpp" |
|
37 |
|
#include "cursed/Text.hpp" |
|
38 |
|
#include "cursed/Track.hpp" |
|
39 |
|
|
|
40 |
|
#include "vle/utils/macros.h" |
|
41 |
|
#include "vle/KeyDispatcher.hpp" |
|
42 |
|
#include "vle/Mode.hpp" |
|
43 |
|
#include "vle/Modes.hpp" |
|
44 |
|
|
|
45 |
29 |
#include "Args.hpp" |
#include "Args.hpp" |
46 |
|
#include "PromptRequest.hpp" |
|
|
30 |
|
#include "PipeDial.hpp" |
47 |
31 |
#include "utils.hpp" |
#include "utils.hpp" |
48 |
32 |
|
|
49 |
|
static std::wstring ui(const std::wstring &titleText, |
|
50 |
|
std::vector<std::wstring> &&lines); |
|
51 |
|
|
|
52 |
|
static const std::wstring helpText = LR"( |
|
53 |
|
SUMMARY OF PIPEDIAL SHORTCUTS |
|
54 |
|
============================= |
|
55 |
|
|
|
56 |
|
{Escape} -- abort partially typed shortcut. |
|
57 |
|
|
|
58 |
|
Normal Mode |
|
59 |
|
----------- |
|
60 |
|
|
|
61 |
|
q -- quit the application with no selection. |
|
62 |
|
|
|
63 |
|
h -- enter Help mode. |
|
64 |
|
|
|
65 |
|
{Enter} -- select current item and quit. |
|
66 |
|
|
|
67 |
|
e -- edit current item and accept the result. |
|
68 |
|
|
|
69 |
|
[count]gg -- put cursor on the first or [count]-th element. |
|
70 |
|
|
|
71 |
|
[count]G -- put cursor on the last or [count]-th element. |
|
72 |
|
|
|
73 |
|
[count]j -- move cursor [count] (1 by default) elements down. |
|
74 |
|
|
|
75 |
|
[count]k -- move cursor [count] (1 by default) elements up. |
|
76 |
|
|
|
77 |
|
Help Mode |
|
78 |
|
--------- |
|
79 |
|
|
|
80 |
|
h -- return to Normal mode.)"; |
|
81 |
|
|
|
82 |
33 |
int |
int |
83 |
34 |
main(int argc, const char *argv[]) |
main(int argc, const char *argv[]) |
84 |
35 |
{ |
{ |
|
... |
... |
main(int argc, const char *argv[]) |
96 |
47 |
|
|
97 |
48 |
std::unique_ptr<FILE, int (*)(FILE *)> out = reopenTermStdout(); |
std::unique_ptr<FILE, int (*)(FILE *)> out = reopenTermStdout(); |
98 |
49 |
|
|
99 |
|
std::string result = toNarrow(ui(toWide(args.getTitle()), |
|
100 |
|
std::move(lines))); |
|
|
50 |
|
PipeDial app(toWide(args.getTitle()), std::move(lines)); |
|
51 |
|
|
|
52 |
|
std::string result = toNarrow(app.run()); |
101 |
53 |
if (result.empty()) { |
if (result.empty()) { |
102 |
54 |
return EXIT_FAILURE; |
return EXIT_FAILURE; |
103 |
55 |
} |
} |
|
... |
... |
main(int argc, const char *argv[]) |
105 |
57 |
fprintf(out.get(), "%s\n", result.c_str()); |
fprintf(out.get(), "%s\n", result.c_str()); |
106 |
58 |
return EXIT_SUCCESS; |
return EXIT_SUCCESS; |
107 |
59 |
} |
} |
108 |
|
|
|
109 |
|
// Performs all user interaction via UI. Accepts list of lines to be displayed. |
|
110 |
|
// And returns the choice or an empty string. |
|
111 |
|
static std::wstring |
|
112 |
|
ui(const std::wstring &titleText, std::vector<std::wstring> &&lines) |
|
113 |
|
{ |
|
114 |
|
vle::Modes modes; |
|
115 |
|
|
|
116 |
|
cursed::Init cursesInit; |
|
117 |
|
cursed::Screen screen; |
|
118 |
|
cursed::Input input; |
|
119 |
|
cursed::Label title(titleText); |
|
120 |
|
cursed::Label inputBuf; |
|
121 |
|
|
|
122 |
|
cursed::List list; |
|
123 |
|
list.setItems(std::move(lines)); |
|
124 |
|
|
|
125 |
|
cursed::Text help(helpText); |
|
126 |
|
|
|
127 |
|
cursed::Track track; |
|
128 |
|
track.addItem(&title); |
|
129 |
|
track.addItem(&list); |
|
130 |
|
track.addItem(&inputBuf); |
|
131 |
|
|
|
132 |
|
bool quit; |
|
133 |
|
std::wstring result; |
|
134 |
|
|
|
135 |
|
vle::Mode normalMode("normal"); |
|
136 |
|
normalMode.setUsesCount(true); |
|
137 |
|
normalMode.addShortcut({ L"G", [&list](int count) { |
|
138 |
|
if (count < 0) { |
|
139 |
|
list.moveToLast(); |
|
140 |
|
} else { |
|
141 |
|
list.moveToPos(count); |
|
142 |
|
} |
|
143 |
|
}, |
|
144 |
|
"go to the last or [count]-th item" }); |
|
145 |
|
normalMode.addShortcut({ L"gg", [&list](int count) { |
|
146 |
|
if (count < 0) { |
|
147 |
|
list.moveToFirst(); |
|
148 |
|
} else { |
|
149 |
|
list.moveToPos(count); |
|
150 |
|
} |
|
151 |
|
}, |
|
152 |
|
"go to the first or [count]-th item" }); |
|
153 |
|
normalMode.addShortcut({ L"j", [&list](int count) { |
|
154 |
|
list.moveDown(count < 0 ? 1 : count); |
|
155 |
|
}, |
|
156 |
|
"go to item below" }); |
|
157 |
|
normalMode.addShortcut({ L"k", [&list](int count) { |
|
158 |
|
list.moveUp(count < 0 ? 1 : count); |
|
159 |
|
}, |
|
160 |
|
"go to item above" }); |
|
161 |
|
normalMode.addShortcut({ L"q", [&quit]() { quit = true; }, |
|
162 |
|
"quit the application with no selection" }); |
|
163 |
|
normalMode.addShortcut({ L"\n", [&quit, &result, &list]() { |
|
164 |
|
quit = true; |
|
165 |
|
result = list.getCurrent(); |
|
166 |
|
}, |
|
167 |
|
"select current item and quit" }); |
|
168 |
|
normalMode.addShortcut({ L"h", [&modes, &screen, &help]() { |
|
169 |
|
modes.switchTo("help"); |
|
170 |
|
screen.setMainWidget(&help); |
|
171 |
|
}, |
|
172 |
|
"display help" }); |
|
173 |
|
normalMode.addShortcut({ L"e", [&]() { |
|
174 |
|
cursed::Prompt prompt; |
|
175 |
|
cursed::Track tmpTrack; |
|
176 |
|
tmpTrack.addItem(&title); |
|
177 |
|
tmpTrack.addItem(&list); |
|
178 |
|
tmpTrack.addItem(&prompt); |
|
179 |
|
|
|
180 |
|
PromptRequest request(input, screen, prompt); |
|
181 |
|
screen.setMainWidget(&tmpTrack); |
|
182 |
|
if (PromptResult r = request.prompt(L"Edit and accept: ", |
|
183 |
|
list.getCurrent())) { |
|
184 |
|
result = r.getResult(); |
|
185 |
|
quit = true; |
|
186 |
|
} else { |
|
187 |
|
screen.setMainWidget(&track); |
|
188 |
|
} |
|
189 |
|
}, |
|
190 |
|
"edit and accept" }); |
|
191 |
|
|
|
192 |
|
vle::Mode helpMode("help"); |
|
193 |
|
helpMode.addShortcut({ L"h", [&modes, &screen, &track]() { |
|
194 |
|
modes.switchTo("normal"); |
|
195 |
|
screen.setMainWidget(&track); |
|
196 |
|
}, |
|
197 |
|
"display help" }); |
|
198 |
|
helpMode.addShortcut({ L"j", [&help]() { help.scrollDown(); }, |
|
199 |
|
"go to item below" }); |
|
200 |
|
helpMode.addShortcut({ L"k", [&help]() { help.scrollUp(); }, |
|
201 |
|
"go to item above" }); |
|
202 |
|
|
|
203 |
|
std::vector<vle::Mode> allModes; |
|
204 |
|
allModes.emplace_back(std::move(normalMode)); |
|
205 |
|
allModes.emplace_back(std::move(helpMode)); |
|
206 |
|
modes.setModes(std::move(allModes)); |
|
207 |
|
modes.switchTo("normal"); |
|
208 |
|
|
|
209 |
|
screen.setMainWidget(&track); |
|
210 |
|
screen.draw(); |
|
211 |
|
|
|
212 |
|
vle::KeyDispatcher dispatcher; |
|
213 |
|
|
|
214 |
|
while (cursed::InputElement ie = input.read()) { |
|
215 |
|
if (ie.isTerminalResize()) { |
|
216 |
|
screen.resize(); |
|
217 |
|
screen.draw(); |
|
218 |
|
continue; |
|
219 |
|
} |
|
220 |
|
|
|
221 |
|
if (!ie.isFunctionalKey()) { |
|
222 |
|
dispatcher.dispatch(ie); |
|
223 |
|
} |
|
224 |
|
|
|
225 |
|
if (quit) { |
|
226 |
|
break; |
|
227 |
|
} |
|
228 |
|
|
|
229 |
|
inputBuf.setText(dispatcher.getPendingInput()); |
|
230 |
|
screen.draw(); |
|
231 |
|
} |
|
232 |
|
|
|
233 |
|
return result; |
|
234 |
|
} |
|