Skip to content

Commit af9dc90

Browse files
committed
feat(tool): rime_api_console with input feature histories switched by Up/Down
1 parent e4d835a commit af9dc90

File tree

4 files changed

+310
-14
lines changed

4 files changed

+310
-14
lines changed

tools/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ set(rime_console_deps
77
${rime_levers_library}
88
${rime_plugins_library})
99

10-
set(rime_api_console_src "rime_api_console.cc")
10+
set(rime_api_console_src "rime_api_console.cc" "line_editor.cc")
1111
add_executable(rime_api_console ${rime_api_console_src})
1212
target_link_libraries(rime_api_console ${rime_console_deps})
1313

tools/line_editor.cc

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
#include "line_editor.h"
2+
3+
#include <cctype>
4+
#ifdef _WIN32
5+
#include <conio.h>
6+
#else
7+
#include <termios.h>
8+
#include <unistd.h>
9+
#endif
10+
#include <cstdio>
11+
12+
LineEditor::LineEditor(size_t max_length) : max_length_(max_length) {}
13+
14+
bool LineEditor::ReadLine(std::string* out) {
15+
if (!out)
16+
return false;
17+
#ifndef _WIN32
18+
struct TermiosGuard {
19+
termios original;
20+
bool active = false;
21+
TermiosGuard() {
22+
if (!isatty(STDIN_FILENO))
23+
return;
24+
if (tcgetattr(STDIN_FILENO, &original) == 0) {
25+
termios raw = original;
26+
raw.c_lflag &= ~(ICANON | ECHO);
27+
raw.c_cc[VMIN] = 1;
28+
raw.c_cc[VTIME] = 0;
29+
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == 0) {
30+
active = true;
31+
}
32+
}
33+
}
34+
~TermiosGuard() {
35+
if (active) {
36+
tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
37+
}
38+
}
39+
} guard;
40+
#endif
41+
std::string line;
42+
size_t cursor = 0;
43+
history_position_ = history_.size();
44+
browsing_history_ = false;
45+
saved_line_.clear();
46+
last_rendered_length_ = 0;
47+
RefreshLine(line, cursor);
48+
while (true) {
49+
int ch = ReadChar();
50+
if (ch == -1) {
51+
putchar('\n');
52+
fflush(stdout);
53+
return false;
54+
}
55+
if (ch == '\r' || ch == '\n') {
56+
RefreshLine(line, line.size());
57+
putchar('\n');
58+
fflush(stdout);
59+
*out = line;
60+
if (!out->empty()) {
61+
bool whitespace_only = true;
62+
for (char ch : *out) {
63+
if (!std::isspace(static_cast<unsigned char>(ch))) {
64+
whitespace_only = false;
65+
break;
66+
}
67+
}
68+
if (!whitespace_only) {
69+
if (history_.empty() || history_.back() != *out) {
70+
history_.push_back(*out);
71+
}
72+
}
73+
}
74+
last_rendered_length_ = 0;
75+
return true;
76+
}
77+
if ((ch == 4 || ch == 26) && line.empty()) { // Ctrl-D / Ctrl-Z
78+
putchar('\n');
79+
fflush(stdout);
80+
return false;
81+
}
82+
if (ch == 3) { // Ctrl-C
83+
putchar('\n');
84+
fflush(stdout);
85+
return false;
86+
}
87+
if (ch == 127 || ch == 8) { // backspace
88+
if (cursor > 0) {
89+
line.erase(cursor - 1, 1);
90+
--cursor;
91+
RefreshLine(line, cursor);
92+
} else {
93+
EmitBell();
94+
}
95+
continue;
96+
}
97+
if (HandleEscapeSequence(ch, &line, &cursor))
98+
continue;
99+
if (IsPrintable(ch)) {
100+
if (line.size() >= max_length_) {
101+
EmitBell();
102+
continue;
103+
}
104+
line.insert(cursor, 1, static_cast<char>(ch));
105+
++cursor;
106+
RefreshLine(line, cursor);
107+
// editing after history recall should detach from history
108+
if (browsing_history_) {
109+
browsing_history_ = false;
110+
history_position_ = history_.size();
111+
}
112+
continue;
113+
}
114+
// ignore other keys
115+
}
116+
}
117+
118+
int LineEditor::ReadChar() {
119+
#ifdef _WIN32
120+
int ch = _getch();
121+
return ch;
122+
#else
123+
unsigned char c = 0;
124+
ssize_t n = read(STDIN_FILENO, &c, 1);
125+
if (n <= 0)
126+
return -1;
127+
return static_cast<int>(c);
128+
#endif
129+
}
130+
131+
bool LineEditor::HandleEscapeSequence(int ch, std::string* line, size_t* cursor) {
132+
if (!line || !cursor)
133+
return false;
134+
#ifdef _WIN32
135+
if (ch == 0 || ch == 224) {
136+
int next = _getch();
137+
switch (next) {
138+
case 72: // up
139+
RecallHistory(-1, line, cursor);
140+
return true;
141+
case 80: // down
142+
RecallHistory(1, line, cursor);
143+
return true;
144+
case 75: // left
145+
if (*cursor > 0) {
146+
--(*cursor);
147+
RefreshLine(*line, *cursor);
148+
} else {
149+
EmitBell();
150+
}
151+
return true;
152+
case 77: // right
153+
if (*cursor < line->size()) {
154+
++(*cursor);
155+
RefreshLine(*line, *cursor);
156+
} else {
157+
EmitBell();
158+
}
159+
return true;
160+
default:
161+
break;
162+
}
163+
}
164+
#else
165+
if (ch == 27) {
166+
int next1 = ReadChar();
167+
if (next1 == -1)
168+
return true;
169+
if (next1 == '[') {
170+
int next2 = ReadChar();
171+
if (next2 == -1)
172+
return true;
173+
switch (next2) {
174+
case 'A':
175+
RecallHistory(-1, line, cursor);
176+
return true;
177+
case 'B':
178+
RecallHistory(1, line, cursor);
179+
return true;
180+
case 'C':
181+
if (*cursor < line->size()) {
182+
++(*cursor);
183+
RefreshLine(*line, *cursor);
184+
} else {
185+
EmitBell();
186+
}
187+
return true;
188+
case 'D':
189+
if (*cursor > 0) {
190+
--(*cursor);
191+
RefreshLine(*line, *cursor);
192+
} else {
193+
EmitBell();
194+
}
195+
return true;
196+
default:
197+
break;
198+
}
199+
}
200+
return true;
201+
}
202+
#endif
203+
return false;
204+
}
205+
206+
void LineEditor::RecallHistory(int direction, std::string* line, size_t* cursor) {
207+
if (history_.empty()) {
208+
EmitBell();
209+
return;
210+
}
211+
if (!browsing_history_) {
212+
browsing_history_ = true;
213+
saved_line_ = *line;
214+
history_position_ = history_.size();
215+
}
216+
if (direction < 0) {
217+
if (history_position_ == 0) {
218+
EmitBell();
219+
return;
220+
}
221+
--history_position_;
222+
*line = history_[history_position_];
223+
} else {
224+
if (history_position_ + 1 >= history_.size()) {
225+
*line = saved_line_;
226+
browsing_history_ = false;
227+
history_position_ = history_.size();
228+
} else {
229+
++history_position_;
230+
*line = history_[history_position_];
231+
}
232+
}
233+
*cursor = line->size();
234+
RefreshLine(*line, *cursor);
235+
}
236+
237+
void LineEditor::RefreshLine(const std::string& line, size_t cursor) {
238+
printf("\r");
239+
fwrite(line.data(), 1, line.size(), stdout);
240+
size_t clear_width = 0;
241+
if (last_rendered_length_ > line.size()) {
242+
clear_width = last_rendered_length_ - line.size();
243+
}
244+
for (size_t i = 0; i < clear_width; ++i) {
245+
putchar(' ');
246+
}
247+
printf("\r");
248+
if (cursor > 0) {
249+
fwrite(line.data(), 1, cursor, stdout);
250+
}
251+
fflush(stdout);
252+
last_rendered_length_ = line.size();
253+
}
254+
255+
bool LineEditor::IsPrintable(int ch) const {
256+
if (ch < 0)
257+
return false;
258+
unsigned char uc = static_cast<unsigned char>(ch);
259+
return uc >= 32 && uc <= 126 && std::isprint(uc);
260+
}
261+
262+
void LineEditor::EmitBell() const {
263+
putchar('\a');
264+
fflush(stdout);
265+
}

tools/line_editor.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <string>
5+
#include <vector>
6+
7+
class LineEditor {
8+
public:
9+
explicit LineEditor(size_t max_length);
10+
11+
// Reads one line into out; returns false on EOF or interruption.
12+
bool ReadLine(std::string* out);
13+
14+
private:
15+
int ReadChar();
16+
bool HandleEscapeSequence(int ch, std::string* line, size_t* cursor);
17+
void RecallHistory(int direction, std::string* line, size_t* cursor);
18+
void RefreshLine(const std::string& line, size_t cursor);
19+
bool IsPrintable(int ch) const;
20+
void EmitBell() const;
21+
22+
size_t max_length_ = 0;
23+
std::vector<std::string> history_;
24+
size_t history_position_ = 0;
25+
bool browsing_history_ = false;
26+
std::string saved_line_;
27+
size_t last_rendered_length_ = 0;
28+
};

tools/rime_api_console.cc

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
#include <stdio.h>
88
#include <stdlib.h>
99
#include <string.h>
10+
#include <cctype>
11+
#include <string>
12+
#include <vector>
1013
#include <rime_api.h>
1114
#include "codepage.h"
15+
#include "line_editor.h"
16+
17+
// LineEditor is moved to tools/line_editor.{h,cc}
1218

1319
void print_status(RimeStatus* status) {
1420
printf("schema: %s / %s\n", status->schema_id, status->schema_name);
@@ -235,32 +241,29 @@ int main(int argc, char* argv[]) {
235241

236242
RimeSessionId session_id = 0;
237243
const int kMaxLength = 99;
238-
char line[kMaxLength + 1] = {0};
239-
while (fgets(line, kMaxLength, stdin) != NULL) {
240-
for (char* p = line; *p; ++p) {
241-
if (*p == '\r' || *p == '\n') {
242-
*p = '\0';
243-
break;
244-
}
245-
}
244+
LineEditor editor(kMaxLength);
245+
std::string line;
246+
while (editor.ReadLine(&line)) {
247+
if (line.empty())
248+
line = "\r";
246249
if (!rime->find_session(session_id) &&
247250
!(session_id = ensure_session(rime))) {
248251
SetConsoleOutputCodePage(codepage);
249252
return 1;
250253
}
251-
if (!strcmp(line, "exit"))
254+
if (!strcmp(line.c_str(), "exit"))
252255
break;
253-
else if (!strcmp(line, "reload")) {
256+
else if (!strcmp(line.c_str(), "reload")) {
254257
rime->destroy_session(session_id);
255258
rime->finalize();
256259
goto reload;
257260
}
258-
if (execute_special_command(line, session_id))
261+
if (execute_special_command(line.c_str(), session_id))
259262
continue;
260-
if (rime->simulate_key_sequence(session_id, line)) {
263+
if (rime->simulate_key_sequence(session_id, line.c_str())) {
261264
print(session_id);
262265
} else {
263-
fprintf(stderr, "Error processing key sequence: %s\n", line);
266+
fprintf(stderr, "Error processing key sequence: %s\n", line.c_str());
264267
}
265268
}
266269

0 commit comments

Comments
 (0)