-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathClientPlatformSpecific.cpp
More file actions
575 lines (475 loc) · 14.8 KB
/
ClientPlatformSpecific.cpp
File metadata and controls
575 lines (475 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
#include "ClientPlatformSpecific.hpp"
// signal types
#include <csignal>
// std::filesystem
#include <filesystem>
// std::*fstream
#include <fstream>
// CoreSdk_ShutDown
#include "ManusSDK.h"
// std::map
#include <map>
// Ncurses terminal functions.
#include <ncursesw/ncurses.h>
// Termios terminal functions.
#include <termios.h>
#include <cstring>
#include <unistd.h>
// spdlog replacement
#include "ClientLogging.hpp"
using namespace ManusSDK;
/// @brief Reset a signal handler to its default, and then call it.
/// For signal types and explanation, see:
/// https://www.gnu.org/software/libc/manual/html_node/Standard-Signals.html
#define CALL_DEFAULT_SIGNAL_HANDLER(p_SignalType) \
/* Reset the handler for this signal to the default. */ \
signal(p_SignalType, SIG_DFL); \
/* Re-raise this signal, causing the normal handler to run. */ \
raise(p_SignalType);
const std::string SDKClientPlatformSpecific::s_SlashForFilesystemPath = "/";
/// @brief Handle a signal telling the SDK client to quit.
/// A generic signal used to "politely ask a program to terminate".
/// On Linux, this can be sent by using the Gnome System Monitor and telling
/// the SDK client process to end.
static void HandleTerminationSignal(int p_Parameter)
{
ClientLog::error(
"Termination signal sent with parameter {}.",
p_Parameter);
CoreSdk_ShutDown();
CALL_DEFAULT_SIGNAL_HANDLER(SIGTERM);
}
/// @brief Handle an interrupt signal.
/// Called when the INTR character is typed - usually ctrl + c.
static void HandleInterruptSignal(int p_Parameter)
{
ClientLog::error(
"Interrupt signal sent with parameter {}.",
p_Parameter);
CoreSdk_ShutDown();
CALL_DEFAULT_SIGNAL_HANDLER(SIGINT);
}
/// @brief Handle a quit signal.
/// Called when the QUIT character is typed - usually ctrl + \.
static void HandleQuitSignal(int p_Parameter)
{
ClientLog::error(
"Quit signal sent with parameter {}.",
p_Parameter);
CoreSdk_ShutDown();
CALL_DEFAULT_SIGNAL_HANDLER(SIGQUIT);
}
/// @brief Handle a hangup signal.
/// Called to report that the user's terminal has disconnected.
/// This can happen when connecting over the network, for example.
/// It also happens if the terminal window is closed while debugging.
static void HandleHangupSignal(int p_Parameter)
{
ClientLog::error(
"Hang-up signal sent with parameter {}.",
p_Parameter);
CoreSdk_ShutDown();
CALL_DEFAULT_SIGNAL_HANDLER(SIGHUP);
}
/// @brief Initialise Ncurses so that we can check for input.
static bool InitializeNcurses(void)
{
const WINDOW* const t_Window = initscr();
if (!t_Window)
{
ClientLog::error("Failed to initialise the screen.");
return false;
}
// Don't buffer input until a newline or carriage return is typed.
if (cbreak() != OK)
{
ClientLog::error("Failed to make input unbuffered.");
return false;
}
// Don't echo input.
if (noecho() != OK)
{
ClientLog::error("Failed to disable input echoing.");
return false;
}
// Don't make newlines when the return key is pressed.
if (nonl() != OK)
{
ClientLog::error("Failed to disable newlines.");
return false;
}
// Do not flush the screen when interrupt/break/quit is pressed.
if (intrflush(stdscr, FALSE) != OK)
{
ClientLog::error("Failed to disable screen flushing.");
return false;
}
// Make getch non-blocking.
if (nodelay(stdscr, TRUE) != OK)
{
ClientLog::error("Failed to make nodelay non-blocking.");
return false;
}
// Enable handling the keypad ("function keys" like the arrow keys).
if (keypad(stdscr, TRUE) != OK)
{
ClientLog::error("Failed to enable keypad input.");
return false;
}
// 添加设置以保留历史记录和避免窗口大小影响
// 禁用滚动功能,让输出直接到终端
scrollok(stdscr, FALSE);
// 设置为不使用替代屏幕缓冲区,这样历史记录会保留
// 这个需要在initscr之前设置,但我们可以通过环境变量来控制
// 或者直接退出ncurses模式进行普通输出
endwin();
// 重新初始化,但这次不使用全屏模式
// 我们只需要输入功能,输出使用标准方式
newterm(NULL, stdout, stdin);
return true;
}
/// @brief Initialise Termios so that terminal output is correct.
static bool InitializeTermios(void)
{
// For some reason, printf and spdlog strings require a carriage return
// in addition to a newline after running initscr().
// This code sets the "output modes" flag to treat newline characters
// as newline+carriage-return characters.
// https://arstechnica.com/civis/viewtopic.php?t=70699
termios t_Settings;
if (tcgetattr(STDIN_FILENO, &t_Settings) != 0)
{
ClientLog::error("Failed to get Termios settings.");
return false;
}
t_Settings.c_oflag |= ONLCR;
if (tcsetattr(0, TCSANOW, &t_Settings) != 0)
{
ClientLog::error("Failed to set Termios settings.");
return false;
}
return true;
}
/// @brief Register our signal handling functions.
static bool SetUpSignalHandlers(void)
{
{
const __sighandler_t t_OldTerminationHandler = signal(
SIGTERM,
HandleTerminationSignal);
if (t_OldTerminationHandler == SIG_ERR)
{
ClientLog::error("Failed to set termination signal handler.");
return false;
}
}
{
const __sighandler_t t_OldInterruptHandler = signal(
SIGINT,
HandleInterruptSignal);
if (t_OldInterruptHandler == SIG_ERR)
{
ClientLog::error("Failed to set interrupt signal handler.");
return false;
}
}
{
const __sighandler_t t_OldQuitHandler = signal(
SIGQUIT,
HandleQuitSignal);
if (t_OldQuitHandler == SIG_ERR)
{
ClientLog::error("Failed to set quit signal handler.");
return false;
}
}
{
const __sighandler_t t_OldHangupHandler = signal(
SIGHUP,
HandleHangupSignal);
if (t_OldHangupHandler == SIG_ERR)
{
ClientLog::error("Failed to set hang-up signal handler.");
return false;
}
}
return true;
}
/// @brief Handles keyboard input.
/// Requires things to be set up with Ncurses and Termios.
class ClientInput
{
public:
/// @brief Update the state of the keyboard.
void Update(void)
{
// Reset the state of the last update.
for (
InputMap_t::iterator t_Key = m_PressedLastUpdate.begin();
t_Key != m_PressedLastUpdate.end();
t_Key++)
{
t_Key->second = false;
}
// Copy the current state to the last update's state, and clear the
// current state.
for (
InputMap_t::iterator t_Key = m_CurrentlyPressed.begin();
t_Key != m_CurrentlyPressed.end();
t_Key++)
{
m_PressedLastUpdate[t_Key->first] =
t_Key->second;
t_Key->second = false;
}
// Get the new state.
int t_Ch = getch();
while (t_Ch != ERR)
{
if (t_Ch >= 'a' && t_Ch <= 'z')
{
// Unlike with Windows' GetAsyncKeyState(), upper case and
// lower case characters have different key numbers with
// getch().
// Since all WasKeyPressed calls (as of writing this) use
// upper case, lower case keys need to be converted to work
// on Linux.
// Note that this does break the ability to check for lower
// case key presses.
t_Ch = toupper(t_Ch);
}
m_CurrentlyPressed[t_Ch] = true;
t_Ch = getch();
}
}
/// @brief Get the key's current state.
/// Note that unlike IsPressed(), this also stores the result for use in
/// the next key state check.
bool GetKey(const int p_Key)
{
bool t_IsPressed = IsPressed(p_Key);
m_PreviousKeyState[p_Key] = t_IsPressed;
return t_IsPressed;
}
/// @brief Was this key pressed since the last check?
/// Note that unlike WasJustPressed(), this checks if the key was pressed
/// since the last time a GetKey* function was called.
bool GetKeyDown(const int p_Key)
{
const bool t_IsPressed = IsPressed(p_Key);
const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
const bool t_PreviousValue =
t_PreviousState == m_PreviousKeyState.end()
? false
: t_PreviousState->second;
const bool t_Down = t_IsPressed && !t_PreviousValue;
m_PreviousKeyState[p_Key] = t_Down;
return t_Down;
}
/// @brief Was this key released since the last check?
/// Note that unlike WasJustReleased(), this checks if the key was released
/// since the last time a GetKey* function was called.
bool GetKeyUp(const int p_Key)
{
const bool t_IsPressed = IsPressed(p_Key);
const auto t_PreviousState = m_PreviousKeyState.find(p_Key);
const bool t_PreviousValue =
t_PreviousState == m_PreviousKeyState.end()
? false
: t_PreviousState->second;
const bool t_Up = !t_IsPressed && t_PreviousValue;
m_PreviousKeyState[p_Key] = t_Up;
return t_Up;
}
private:
/// @brief Get the key's current state.
/// Note that unlike GetKey(), this does not store the result for use in
/// the next key state check.
bool IsPressed(const int p_Key) const
{
auto t_CurrentlyPressed = m_CurrentlyPressed.find(p_Key);
if (t_CurrentlyPressed == m_CurrentlyPressed.end())
{
return false;
}
return t_CurrentlyPressed->second;
}
/// @brief Was this key pressed since the last input update?
/// Note that unlike GetKeyDown(), this function will return the same value
/// until the next keyboard state update.
bool WasJustPressed(const int p_Key) const
{
return !WasPressedLastUpdate(p_Key) && IsPressed(p_Key);
}
/// @brief Was this key released since the last input update?
/// Note that unlike GetKeyUp(), this function will return the same value
/// until the next keyboard state update.
bool WasJustReleased(const int p_Key) const
{
return WasPressedLastUpdate(p_Key) && !IsPressed(p_Key);
}
/// @brief Was this key pressed the previous input update?
bool WasPressedLastUpdate(const int p_Key) const
{
auto t_StateLastUpdate = m_PressedLastUpdate.find(p_Key);
if (t_StateLastUpdate == m_PressedLastUpdate.end())
{
return false;
}
return t_StateLastUpdate->second;
}
typedef std::map<int, bool> InputMap_t;
InputMap_t m_CurrentlyPressed;
InputMap_t m_PressedLastUpdate;
// The Windows GetKey* functions only update the key state when a GetKey*
// function gets called. To make the Linux input work the same way, this
// map is used.
InputMap_t m_PreviousKeyState;
};
static ClientInput g_Input;
bool SDKClientPlatformSpecific::PlatformSpecificInitialization(void)
{
const bool t_NcursesResult = InitializeNcurses();
const bool t_TermiosResult = InitializeTermios();
const bool t_SignalResult = SetUpSignalHandlers();
return t_NcursesResult && t_TermiosResult && t_SignalResult;
}
bool SDKClientPlatformSpecific::PlatformSpecificShutdown(void)
{
// Ncurses.
endwin();
return true;
}
void SDKClientPlatformSpecific::UpdateInput(void)
{
g_Input.Update();
}
/*static*/ bool SDKClientPlatformSpecific::CopyString(
char* const p_Target,
const size_t p_MaxLengthThatWillFitInTarget,
const std::string& p_Source)
{
if (!p_Target)
{
ClientLog::error(
"Tried to copy a string, but the target was null. The string was \"{}\".",
p_Source.c_str());
return false;
}
if (p_MaxLengthThatWillFitInTarget == 0)
{
ClientLog::error(
"Tried to copy a string, but the target's size is zero. The string was \"{}\".",
p_Source.c_str());
return false;
}
if (p_MaxLengthThatWillFitInTarget <= p_Source.length())
{
ClientLog::error(
"Tried to copy a string that was longer than {} characters, which makes it too big for its target buffer. The string was \"{}\".",
p_MaxLengthThatWillFitInTarget,
p_Source.c_str());
return false;
}
strcpy(p_Target, p_Source.c_str());
return true;
}
bool SDKClientPlatformSpecific::ResizeWindow(
const short int p_ConsoleWidth,
const short int p_ConsoleHeight,
const short int p_ConsoleScrollback)
{
// https://apple.stackexchange.com/questions/33736/can-a-terminal-window-be-resized-with-a-terminal-command/47841#47841
// Use a control sequence to resize the window.
// Seems to be supported by the default terminal used in Gnome, as well
// as Mac OS.
// \\e[ -> ASCII ESC character (number 27, or 0x1B)
// -> control sequence introducer
// 8; -> resize the window
// y;xt -> The first number is the height, the second the width.
// Scrollback can't and doesn't need to be set here for Linux.
printf("\e[8;%d;%dt", p_ConsoleHeight, p_ConsoleWidth);
ClearConsole();
// None of the ncurses functions for resizing the terminal actually
// seem to do anything.
/*if (resizeterm(180, 180) != OK)
{
return false;
}*/
return true;
}
void SDKClientPlatformSpecific::ApplyConsolePosition(
const int p_ConsoleCurrentOffset)
{
printf("\e[%d;1H", p_ConsoleCurrentOffset);
}
/*static*/ void SDKClientPlatformSpecific::ClearConsole(void)
{
// https://stackoverflow.com/questions/4062045/clearing-terminal-in-linux-with-c-code
// Use a control sequence to clear the terminal and move the cursor.
// Seems to be supported by the default terminal used in Gnome,
// as well as Mac OS.
// \\e[ -> ASCII ESC character (number 27, or 0x1B) -> control sequence introducer
// 2 -> the entire screen
// J -> clear the screen
//printf("\e[2J\n");
// Move the cursor to row 1 column 1.
//printf("\e[1;1H\n");
// 注释掉clear()调用以保留历史输出,避免清屏
/*
if (clear() != OK)
{
ClientLog::error("Failed to clear the screen.");
}
refresh();
*/
}
bool SDKClientPlatformSpecific::GetKey(const int p_Key)
{
return g_Input.GetKey(p_Key);
}
bool SDKClientPlatformSpecific::GetKeyDown(const int p_Key)
{
return g_Input.GetKeyDown(p_Key);
}
bool SDKClientPlatformSpecific::GetKeyUp(const int p_Key)
{
return g_Input.GetKeyUp(p_Key);
}
std::string SDKClientPlatformSpecific::GetDocumentsDirectoryPath_UTF8(void)
{
const char* const t_Xdg = getenv("XDG_DOCUMENTS_DIR");
// Backup - the documents folder is usually going to be in $HOME/Documents.
const char* const t_Home = getenv("HOME");
if (!t_Xdg && !t_Home)
{
return std::string("");
}
const std::string t_DocumentsDir =
(!t_Xdg || strlen(t_Xdg) == 0)
? std::string(t_Home) + std::string("/Documents")
: std::string(t_Xdg);
return t_DocumentsDir;
}
std::ifstream SDKClientPlatformSpecific::GetInputFileStream(
std::string p_Path_UTF8)
{
return std::ifstream(p_Path_UTF8, std::ifstream::binary);
}
std::ofstream SDKClientPlatformSpecific::GetOutputFileStream(
std::string p_Path_UTF8)
{
return std::ofstream(p_Path_UTF8, std::ofstream::binary);
}
bool SDKClientPlatformSpecific::DoesFolderOrFileExist(std::string p_Path_UTF8)
{
return std::filesystem::exists(p_Path_UTF8);
}
void SDKClientPlatformSpecific::CreateFolderIfItDoesNotExist(
std::string p_Path_UTF8)
{
if (!DoesFolderOrFileExist(p_Path_UTF8))
{
std::filesystem::create_directories(p_Path_UTF8);
}
}