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

Extract PipeDial application class
`main()` still contains input/output code.
Author: xaizek
Author date (UTC): 2019-01-19 11:23
Committer name: xaizek
Committer date (UTC): 2019-01-24 13:53
Parent(s): 125650b8d7fa068955ec29a198a86474f0862f33
Signing key: 99DC5E4DB05F6BE2
Tree: fea66da1405585ca86288d0cfbee3401843a9e74
File Lines added Lines deleted
src/PipeDial.cpp 186 0
src/PipeDial.hpp 77 0
src/main.cpp 4 179
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 }
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