From 5f51a52ca6b41ba3865b42b272eeacc1287c762b Mon Sep 17 00:00:00 2001 From: wcjbr Date: Sat, 15 Aug 2026 19:11:54 +0800 Subject: [PATCH] First commit --- .clang-format | 6 + .gitignore | 3 + CMakeLists.txt | 22 ++ CMakePresets.json | 24 ++ ConsoleLib.cpp | 477 ++++++++++++++++++++++++++++++ README.md | 229 ++++++++++++++ build.bat | 6 + build.sh | 5 + docs/image.png | Bin 0 -> 6774 bytes example/CMakeLists.txt | 11 + example/CMakePresets.json | 6 + example/build.bat | 6 + example/build.sh | 5 + example/helloworld/CMakeLists.txt | 2 + example/helloworld/main.cpp | 14 + example/keyinput/CMakeLists.txt | 2 + example/keyinput/main.cpp | 63 ++++ include/ConsoleLib.h | 101 +++++++ 18 files changed, 982 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 ConsoleLib.cpp create mode 100644 README.md create mode 100644 build.bat create mode 100755 build.sh create mode 100644 docs/image.png create mode 100644 example/CMakeLists.txt create mode 100644 example/CMakePresets.json create mode 100644 example/build.bat create mode 100755 example/build.sh create mode 100644 example/helloworld/CMakeLists.txt create mode 100644 example/helloworld/main.cpp create mode 100644 example/keyinput/CMakeLists.txt create mode 100644 example/keyinput/main.cpp create mode 100644 include/ConsoleLib.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..9026068 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +Language: Cpp +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 +AllowShortIfStatementsOnASingleLine: false +AlignTrailingComments: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c00a89a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +.vscode +example/build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..36c5402 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.10.0) +project(ConsoleLib VERSION 0.1.0 LANGUAGES C CXX) + +option(ConsoleLib_BUILD_EXAMPLES "Build ConsoleLib examples" ON) + +# 静态库:Linux/macOS 生成 libConsoleLib.a,Windows 生成 ConsoleLib.lib +add_library(ConsoleLib STATIC ConsoleLib.cpp include/ConsoleLib.h) + +# 共享库:Linux/macOS 生成 libConsoleLib.so,Windows 生成 ConsoleLib.dll +add_library(ConsoleLib_shared SHARED ConsoleLib.cpp) + +target_include_directories(ConsoleLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(ConsoleLib_shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +set_target_properties(ConsoleLib_shared PROPERTIES + OUTPUT_NAME ConsoleLib + WINDOWS_EXPORT_ALL_SYMBOLS ON +) + +if(ConsoleLib_BUILD_EXAMPLES) + add_subdirectory(example) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..792edb7 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,24 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "Clang", + "displayName": "Clang (Ninja, Debug)", + "description": "使用 Clang 和 Ninja 配置 ConsoleLib / 示例", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install" + } + } + ], + "buildPresets": [ + { + "name": "Clang", + "configurePreset": "Clang" + } + ] +} diff --git a/ConsoleLib.cpp b/ConsoleLib.cpp new file mode 100644 index 0000000..3530fac --- /dev/null +++ b/ConsoleLib.cpp @@ -0,0 +1,477 @@ +#include "include/ConsoleLib.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +#ifdef __unix +#include +#include +#include +#include +#include +#include +#endif + +namespace ConsoleLib { +using ::Color; + +namespace { + +std::array g_keyTable{}; +std::mutex g_keyMutex; +std::atomic g_inputRunning{false}; +std::atomic g_inputFrequency{DEFAULT_CONSOLE_INPUT_FREQUENCY}; +std::thread g_inputThread; + +std::chrono::milliseconds currentPollInterval() { + std::uint8_t frequency = g_inputFrequency.load(std::memory_order_relaxed); + if (frequency == 0) { + frequency = 1; + } + return std::chrono::milliseconds(1000 / frequency); +} + +void recordKey(Keycode keycode) { + if (keycode <= KEY_UNKNOWN || keycode >= KEY_COUNT) { + return; + } + + std::lock_guard lock(g_keyMutex); + g_keyTable[static_cast(keycode)] = true; +} + +Keycode parseEscapeSequence(const char* buffer, size_t length) { + if (length < 2) { + return KEY_UNKNOWN; + } + + const unsigned char first = static_cast(buffer[0]); + + // Unix/Linux VT 序列:ESC [ X / ESC O X + if (first == 0x1B) { + if (length < 3) { + return KEY_UNKNOWN; + } + + if (buffer[1] == '[') { + switch (buffer[2]) { + case 'A': + return KEY_UP; + case 'B': + return KEY_DOWN; + case 'C': + return KEY_RIGHT; + case 'D': + return KEY_LEFT; + case 'H': + return KEY_HOME; + case 'F': + return KEY_END; + default: + return KEY_UNKNOWN; + } + } + + if (buffer[1] == 'O') { + switch (buffer[2]) { + case 'P': + return KEY_F1; + case 'Q': + return KEY_F2; + case 'R': + return KEY_F3; + case 'S': + return KEY_F4; + default: + return KEY_UNKNOWN; + } + } + + return KEY_UNKNOWN; + } + + // Windows 扩展键序列:0x00 / 0xE0 + 扫描码。 + if (first == 0x00 || first == 0xE0) { + switch (buffer[1]) { + case 'H': + return KEY_UP; + case 'P': + return KEY_DOWN; + case 'K': + return KEY_LEFT; + case 'M': + return KEY_RIGHT; + case 'G': + return KEY_HOME; + case 'O': + return KEY_END; + case ';': + return KEY_F1; + case '<': + return KEY_F2; + case '=': + return KEY_F3; + case '>': + return KEY_F4; + default: + return KEY_UNKNOWN; + } + } + + return KEY_UNKNOWN; +} + +std::string makeAnsiColorPrefix(Color color) { + return "\033[38;2;" + std::to_string((color >> 16) & 0xFFu) + ";" + + std::to_string((color >> 8) & 0xFFu) + ";" + std::to_string(color & 0xFFu) + "m"; +} + +} // namespace + +void startInputThread(); +void restoreConsoleMode(); + +#ifdef _WIN32 + +static HANDLE g_hStdin = INVALID_HANDLE_VALUE; +static HANDLE g_hStdout = INVALID_HANDLE_VALUE; +static DWORD g_dwOriginalInMode = 0; +static DWORD g_dwOriginalOutMode = 0; +static bool g_bInitialized = false; + +void initConsole() { + if (g_bInitialized) { + startInputThread(); + return; + } + + g_hStdin = GetStdHandle(STD_INPUT_HANDLE); + g_hStdout = GetStdHandle(STD_OUTPUT_HANDLE); + + if (g_hStdin == INVALID_HANDLE_VALUE || g_hStdout == INVALID_HANDLE_VALUE) { + return; + } + + if (!GetConsoleMode(g_hStdin, &g_dwOriginalInMode)) { + return; + } + + DWORD dwNewInMode = g_dwOriginalInMode; + dwNewInMode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); + dwNewInMode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + + if (!SetConsoleMode(g_hStdin, dwNewInMode)) { + return; + } + + if (!GetConsoleMode(g_hStdout, &g_dwOriginalOutMode)) { + return; + } + + DWORD dwNewOutMode = g_dwOriginalOutMode; + dwNewOutMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + if (!SetConsoleMode(g_hStdout, dwNewOutMode)) { + return; + } + + g_bInitialized = true; + startInputThread(); +} + +void clearConsole() { + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut == INVALID_HANDLE_VALUE) { + return; + } + + CONSOLE_SCREEN_BUFFER_INFO csbi; + DWORD count; + COORD homeCoords = {0, 0}; + + if (!GetConsoleScreenBufferInfo(hOut, &csbi)) { + return; + } + + count = csbi.dwSize.X * csbi.dwSize.Y; + FillConsoleOutputCharacterA(hOut, ' ', count, homeCoords, &count); + FillConsoleOutputAttribute(hOut, csbi.wAttributes, count, homeCoords, &count); + SetConsoleCursorPosition(hOut, homeCoords); +} + +void RawPrint(std::string_view text) { + if (g_hStdout == INVALID_HANDLE_VALUE) { + return; + } + + DWORD bytesToWrite = static_cast(text.size()); + DWORD bytesWritten = 0; + WriteFile(g_hStdout, text.data(), bytesToWrite, &bytesWritten, nullptr); +} + +void printToConsole(std::string_view data) { RawPrint(data); } + +void printToConsole(std::string_view data, Color color) { + std::string prefix = makeAnsiColorPrefix(color); + RawPrint(prefix); + RawPrint(data); + RawPrint("\033[0m"); +} + +char getinput() { + if (!g_bInitialized || g_hStdin == INVALID_HANDLE_VALUE) { + return 0; + } + + char buffer[16] = {0}; + DWORD charsRead = 0; + + if (ReadFile(g_hStdin, buffer, sizeof(buffer) - 1, &charsRead, NULL) && charsRead > 0) { + if (charsRead == 1) { + return buffer[0]; + } + + Keycode key = parseEscapeSequence(buffer, charsRead); + if (key != KEY_UNKNOWN) { + return 0; // 特殊键请使用 getConsoleKeyInput() / isKeyPressed() + } + return buffer[0]; + } + + return 0; +} + +void restoreConsoleMode() { + if (!g_bInitialized) { + return; + } + + if (g_hStdin != INVALID_HANDLE_VALUE) { + SetConsoleMode(g_hStdin, g_dwOriginalInMode); + } + if (g_hStdout != INVALID_HANDLE_VALUE) { + SetConsoleMode(g_hStdout, g_dwOriginalOutMode); + } + + g_bInitialized = false; +} + +void startInputThread() { + bool expected = false; + if (!g_inputRunning.compare_exchange_strong(expected, true)) { + return; + } + + if (g_inputThread.joinable()) { + g_inputThread.join(); + } + + g_inputThread = std::thread([] { + while (g_inputRunning.load(std::memory_order_relaxed)) { + if (g_bInitialized && g_hStdin != INVALID_HANDLE_VALUE) { + DWORD available = 0; + if (GetNumberOfConsoleInputEvents(g_hStdin, &available) && available > 0) { + char buffer[16] = {0}; + DWORD charsRead = 0; + + if (ReadFile(g_hStdin, buffer, sizeof(buffer) - 1, &charsRead, nullptr) && + charsRead > 0) { + if (charsRead == 1) { + recordKey(static_cast(static_cast(buffer[0]))); + } else { + Keycode key = parseEscapeSequence(buffer, charsRead); + if (key != KEY_UNKNOWN) { + recordKey(key); + } else { + recordKey(KEY_ESCAPE); + } + } + } + } + } + + std::this_thread::sleep_for(currentPollInterval()); + } + }); +} + +#endif // _WIN32 + +#ifdef __unix + +static struct termios original_termios; +static bool raw_mode_enabled = false; + +void initConsole() { + if (!raw_mode_enabled) { + struct termios raw; + if (tcgetattr(STDIN_FILENO, &original_termios) == -1) { + return; + } + + raw = original_termios; + + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN); + raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { + return; + } + + raw_mode_enabled = true; + } + + startInputThread(); +} + +void RawPrint(std::string_view text) { write(STDOUT_FILENO, text.data(), text.size()); } + +void printToConsole(std::string_view data) { RawPrint(data); } + +void printToConsole(std::string_view data, Color color) { + std::string prefix = makeAnsiColorPrefix(color); + RawPrint(prefix); + RawPrint(data); + RawPrint("\033[0m"); +} + +void clearConsole() { RawPrint("\033[2J\033[H"); } + +char getinput() { + char ch = 0; + return read(STDIN_FILENO, &ch, 1) == 1 ? ch : 0; +} + +void restoreConsoleMode() { + if (!raw_mode_enabled) { + return; + } + + tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios); + raw_mode_enabled = false; +} + +void startInputThread() { + bool expected = false; + if (!g_inputRunning.compare_exchange_strong(expected, true)) { + return; + } + + if (g_inputThread.joinable()) { + g_inputThread.join(); + } + + g_inputThread = std::thread([] { + std::string pending; + char buffer[32] = {0}; + + while (g_inputRunning.load(std::memory_order_relaxed)) { + ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer)); + if (n > 0) { + pending.append(buffer, static_cast(n)); + + size_t pos = 0; + while (pos < pending.size()) { + unsigned char c = static_cast(pending[pos]); + + if (c == 0x1B) { + if (pending.size() - pos >= 2 && + (pending[pos + 1] == '[' || pending[pos + 1] == 'O')) { + if (pending.size() - pos >= 3) { + Keycode key = parseEscapeSequence(pending.data() + pos, 3); + recordKey(key); + pos += 3; + continue; + } + break; // 序列还不完整,等待下一次读取 + } + + recordKey(KEY_ESCAPE); + ++pos; + continue; + } + + if (c < ASCII_TABLE_SIZE) { + recordKey(static_cast(c)); + } + ++pos; + } + + pending.erase(0, pos); + } else if (n < 0 && errno != EINTR) { + // If stdin is no longer readable, stop polling instead of spinning + // forever. + g_inputRunning.store(false); + break; + } + + std::this_thread::sleep_for(currentPollInterval()); + } + }); +} + +#endif // __unix + +void stopInputThread() { + g_inputRunning.store(false); + + if (g_inputThread.joinable()) { + g_inputThread.join(); + } +} + +std::array& getConsoleKeyInput() { + static thread_local std::array snapshot{}; + + std::lock_guard lock(g_keyMutex); + snapshot = g_keyTable; + g_keyTable.fill(false); + + return snapshot; +} + +bool isKeyPressed(Keycode key) { + if (key <= KEY_UNKNOWN || key >= KEY_COUNT) { + return false; + } + + std::lock_guard lock(g_keyMutex); + return g_keyTable[static_cast(key)]; +} +void sleep_ms(unsigned int ms) { +#ifdef _WIN32 + Sleep(static_cast(ms)); +#else + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +#endif +} + +void setInputFrequency(std::uint8_t frequency) { + g_inputFrequency.store(frequency == 0 ? 1 : frequency, std::memory_order_relaxed); +} + +std::uint8_t getInputFrequency() { return g_inputFrequency.load(std::memory_order_relaxed); } + +void quit(int exitCode) { + stopInputThread(); + restoreConsoleMode(); + exit(exitCode); +} +} // namespace ConsoleLib diff --git a/README.md b/README.md new file mode 100644 index 0000000..d980882 --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +# ConsoleLib + +> 一个用 C++ 编写的跨平台控制台操作库。 + +ConsoleLib 提供 Windows / Linux 下的控制台初始化、原始键盘输入、后台按键轮询、ANSI 真彩色输出、清屏和毫秒级休眠等功能,并同时产出静态库和动态库。 + +## 特性 + +- 控制台原始模式 / 原生模式初始化与恢复 +- 后台键盘输入线程,轮询并记录按键 +- 24 位 ANSI 真彩色输出 +- 清屏 +- 毫秒级跨平台休眠 `sleep_ms` +- 安全的 `quit` 退出 +- 同时生成静态库和动态库 + +## 平台产物 + +| 平台 | 静态库 | 动态库 | +| --- | --- | --- | +| Linux | `libConsoleLib.a` | `libConsoleLib.so` | +| Windows | `ConsoleLib.lib` | `ConsoleLib.dll` | + +## 环境要求 + +- CMake 3.10+(使用 `CMakePresets.json` 时建议 3.28+) +- 支持 C++17 的编译器(Clang / GCC / MSVC) +- 使用默认 preset 时需要 [Ninja](https://ninja-build.org/) + +## 目录结构 + +```text +ConsoleLib/ +├── CMakeLists.txt +├── CMakePresets.json +├── ConsoleLib.cpp +├── include/ +│ └── ConsoleLib.h +├── example/ +│ ├── CMakeLists.txt +│ ├── CMakePresets.json +│ ├── build.sh +│ ├── build.bat +│ ├── helloworld/ +│ │ ├── CMakeLists.txt +│ │ └── main.cpp +│ └── keyinput/ +│ ├── CMakeLists.txt +│ └── main.cpp +└── docs/ + └── image.png +``` + +## 快速开始 + +### 方式一:使用 CMake Presets(推荐) + +```bash +# 项目根目录 +./build.sh +``` + +等价命令: + +```bash +cmake --preset Clang +cmake --build --preset Clang -j +``` + +构建产物默认在 `build/`。 + +Windows 下可运行: + +```bat +build.bat +``` + +### 方式二:手动构建 + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug +cmake --build build -j +``` + +### 单独构建示例 + +```bash +cd example +./build.sh +``` + +或: + +```bash +cd example +cmake --preset Clang +cmake --build --preset Clang -j +./build/helloworld/helloworld +./build/keyinput/keyinput +``` + +## 示例 + +### HelloWorld + +`example/helloworld/main.cpp`: + +```cpp +#include + +using namespace ConsoleLib; + +signed main() { + initConsole(); + clearConsole(); + printToConsole("HelloWorld!\r\n"); + printToConsole("Red", 0xff0000); + printToConsole("Green", 0x00ff00); + printToConsole("Blue", 0x0000ff); + quit(0); +} +``` + +输出效果见 `docs/image.png`。 + +### 按键输入(含方向键) + +`example/keyinput/main.cpp` 展示了 `getConsoleKeyInput()` 与方向键、`Home`、`End`、`F1` ~ `F4` 以及普通可打印字符的检测: + +```cpp +#include + +using namespace ConsoleLib; + +signed main() { + initConsole(); + clearConsole(); + printToConsole("Press arrow keys, q or ESC to quit.\r\n"); + + while (true) { + auto &keys = getConsoleKeyInput(); + + if (keys[KEY_UP]) { + printToConsole("UP\r\n"); + } + if (keys[KEY_DOWN]) { + printToConsole("DOWN\r\n"); + } + if (keys[KEY_LEFT]) { + printToConsole("LEFT\r\n"); + } + if (keys[KEY_RIGHT]) { + printToConsole("RIGHT\r\n"); + } + + if (keys[KEY_q] || keys[KEY_ESCAPE]) { + break; + } + + sleep_ms(16); + } + + quit(0); +} +``` + +## 集成到你的项目 + +### 静态库 + +```cmake +add_subdirectory(ConsoleLib) +target_link_libraries(your_target PRIVATE ConsoleLib) +``` + +### 动态库 + +```cmake +add_subdirectory(ConsoleLib) +target_link_libraries(your_target PRIVATE ConsoleLib_shared) +``` + +`ConsoleLib` 目标已通过 `target_include_directories(... PUBLIC ...)` 暴露 `include/` 目录,因此直接包含即可: + +```cpp +#include +``` + +## API 参考 + +| 函数 | 说明 | +| --- | --- | +| `void initConsole()` | 初始化控制台并启动后台输入线程 | +| `void stopInputThread()` | 停止后台输入线程 | +| `void restoreConsoleMode()` | 恢复终端原始模式 / Windows 控制台模式 | +| `void RawPrint(std::string_view text)` | 直接输出文本,不做颜色处理 | +| `void printToConsole(std::string_view data)` | 打印文本 | +| `void printToConsole(std::string_view data, Color color)` | 打印指定颜色的文本 | +| `void clearConsole()` | 清屏并将光标移动到左上角 | +| `char getinput()` | 读取一个输入字符 | +| `std::array &getConsoleKeyInput()` | 返回并清空按键状态表,下标为 `Keycode` | +| `bool isKeyPressed(Keycode key)` | 查询某个按键当前是否被按下 | +| `void sleep_ms(unsigned int ms)` | 休眠指定毫秒数 | +| `void setInputFrequency(std::uint8_t frequency)` | 设置输入轮询频率(Hz) | +| `std::uint8_t getInputFrequency()` | 获取当前输入轮询频率 | +| `void quit(int exitCode)` | 停止线程、恢复终端并退出进程 | + +`Color` 为 `uint32_t`,格式为 `0xRRGGBB`。 + +## 按键码 + +`Keycode` 为 SDL 风格的按键码: + +- ASCII 字符直接使用其 ASCII 值,例如 `KEY_a`、`KEY_0`、`KEY_SPACE` +- 特殊键从 `128` 开始,例如 `KEY_UP`、`KEY_DOWN`、`KEY_LEFT`、`KEY_RIGHT`、`KEY_HOME`、`KEY_END`、`KEY_F1` ~ `KEY_F4` + +| 常量 | 含义 | +| --- | --- | +| `ASCII_TABLE_SIZE` | ASCII 范围大小,固定为 `128` | +| `KEYCODE_COUNT` | 按键状态表大小 | +| `DEFAULT_CONSOLE_INPUT_FREQUENCY` | 默认输入轮询频率(50 Hz) | + +`getConsoleKeyInput()` 返回长度为 `KEYCODE_COUNT` 的 `std::array`,下标即 `Keycode`;调用后内部按键记录会被清空。`isKeyPressed(Keycode)` 仅查询、不会清空记录。 + +## 说明 + +- `initConsole()` 会将终端切换到非回显的原始模式;程序结束前请调用 `quit()` 或 `restoreConsoleMode()`,否则终端状态可能无法恢复。 +- `quit()` 已包含 `stopInputThread()` 和 `restoreConsoleMode()`。 +- `getinput()` 仅返回单个 ASCII 字符;方向键、功能键等特殊键请使用 `getConsoleKeyInput()` 或 `isKeyPressed()`。 diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..0ca97ae --- /dev/null +++ b/build.bat @@ -0,0 +1,6 @@ +@echo off +cd /d "%~dp0" +cmake --preset Clang +if errorlevel 1 exit /b 1 +cmake --build --preset Clang -j +if errorlevel 1 exit /b 1 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5b03384 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +cmake --preset Clang +cmake --build --preset Clang -j diff --git a/docs/image.png b/docs/image.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1d006d5d7fac14c3d1a92ed2570de99a2deeb1 GIT binary patch literal 6774 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
  • sll$PH*73P80f|p-LA;k+T&YXqR zjho?X)Cgz$^)YEEkpJv6DA~LjRo+~RDl1o^^uu+axRe-@X8=cE7E} zGnZ#oqw+O<6vtaqLNIxS?J*)UKF@Xx8W?YQyqa|3mvXnx4#p+akyX*9C806#}SCKP!@WY*Wj;Rw17B5y8L*h+6 zk-GNp4$4Q5;`|?{s>_XckP@=0{Ma%08ZMJ4Buq>gw<%$_aLpn(MCzp-wez;AzRV9Amo1?cry`i&*WBu-AQfOyZ3 zvgVmGMW~DMDlQ=$C@FvFkk)zpJ|!o(ckSANnrXEmIN@x)^cv!vmTA}|Q>v$6!F&XH zCLvH^>K~ENq^akbVZMW?x;GqaRau8>aRjn1YoN4NcJ2|hxV!~%N;=Xjr=yZTvQc0W z&>dFbyGb1;qiKz%4uR=^y~_3NI+NP#a>wW)L#1*(LQB8RTqQ9@MS|=;U4A0t+njV_ z>(*Mw-g+x==bcd2=(X4C#Z<>ir53N;ccMD!7A;^ABm+smw#KK(OG*T$O_SuJNZTA_ zYn~hd^~)di>eW;2+%Kt~JaH1$FRLEbcMKwuHJ6@_gpQpQVb0vt9R+W`iIkx_e`jQ7 zLJ(&qbnF}<$_0(-TtNxv3Le^`p14SL$dwzilE!zBD^MwJXA zfM0Q{JL+RmETc+YBoe1|%jm2(5Irs^C`N^pB(-FIc9MnbOtxYG$43+9IZ|!C>PnR= z0jE#H<3rj$uhoh9m46a|jCw_6wl;tKQGkDe&HBBctSky8(w^37MwgzYe|vTpJ5w#^@jXmHe9hf}k0Tj%63Px%w49aq=%mn$B z6&@e2`bp~B4~4HShQDoFRh~a(GA#3$M;zU1G)8`SFGj8#sdOD%gU9lZ*?2hkhs?%= z`&&JLXSU8kYI1c{Prgj;^e+#8jwd@#Q~`m9uYM5EQh8cMDo?4d=JoNO4VZqzG`O8= zX--;tyiN`3=fX_0Vfk_acAHNS)#N;fI|#d*+Nf?A5AOOV5hfzptluBft1! z`Xj=WyTyJ(y)&Og7dz4^qwhn}yQ@(+Z3-&Cya#uyV^DJuj?1rG&roV&3l@e#WLC{Ys~W8%EKE2IZa#z< zCy2%0EWqBgf$&?gk-L>#jBCu-;ibEG^N(#S#3rJH2@|k{2^N%L`7|YX zO+twX#B>tR+EP`D+$D-sAlvnmVunc^Yl=4EjK~?2vaNbOUU5k&AIY=FSMN(m$3Yj~ zw2P_}D9JZpZ7>H5AtHzRN`uZg5r+2SYwc%_0~2vAH^s9@r1oWl0qMz>*jv{0wXKqW Y0hJ&y7+dXd9smFU07*qoM6N<$f-vwKjsO4v literal 0 HcmV?d00001 diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..5163d87 --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) +project(example LANGUAGES CXX) + +# 作为顶层项目单独构建时,带上 ConsoleLib;作为子目录时目标已存在。 +if(NOT TARGET ConsoleLib) + set(ConsoleLib_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ConsoleLib) +endif() + +add_subdirectory(helloworld) +add_subdirectory(keyinput) diff --git a/example/CMakePresets.json b/example/CMakePresets.json new file mode 100644 index 0000000..c562ff9 --- /dev/null +++ b/example/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 8, + "include": [ + "../CMakePresets.json" + ] +} diff --git a/example/build.bat b/example/build.bat new file mode 100644 index 0000000..0ca97ae --- /dev/null +++ b/example/build.bat @@ -0,0 +1,6 @@ +@echo off +cd /d "%~dp0" +cmake --preset Clang +if errorlevel 1 exit /b 1 +cmake --build --preset Clang -j +if errorlevel 1 exit /b 1 diff --git a/example/build.sh b/example/build.sh new file mode 100755 index 0000000..5b03384 --- /dev/null +++ b/example/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +cmake --preset Clang +cmake --build --preset Clang -j diff --git a/example/helloworld/CMakeLists.txt b/example/helloworld/CMakeLists.txt new file mode 100644 index 0000000..ff28d57 --- /dev/null +++ b/example/helloworld/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(helloworld main.cpp) +target_link_libraries(helloworld PRIVATE ConsoleLib) diff --git a/example/helloworld/main.cpp b/example/helloworld/main.cpp new file mode 100644 index 0000000..6809e72 --- /dev/null +++ b/example/helloworld/main.cpp @@ -0,0 +1,14 @@ +#include + +#include +#include +using namespace ConsoleLib; +signed main() { + initConsole(); + clearConsole(); + printToConsole("HelloWorld!\r\n"); + printToConsole("Red", 0xff0000); + printToConsole("Green", 0x00ff00); + printToConsole("Blue", 0x0000ff); + quit(0); +} diff --git a/example/keyinput/CMakeLists.txt b/example/keyinput/CMakeLists.txt new file mode 100644 index 0000000..47178ad --- /dev/null +++ b/example/keyinput/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(keyinput main.cpp) +target_link_libraries(keyinput PRIVATE ConsoleLib) diff --git a/example/keyinput/main.cpp b/example/keyinput/main.cpp new file mode 100644 index 0000000..4075efe --- /dev/null +++ b/example/keyinput/main.cpp @@ -0,0 +1,63 @@ +#include + +using namespace ConsoleLib; + +signed main() { + initConsole(); + clearConsole(); + + printToConsole("ConsoleLib key input example\r\n"); + printToConsole("Press arrow keys, Home/End, F1-F4, or any printable key.\r\n"); + printToConsole("Press q or ESC to quit.\r\n"); + + while (true) { + // getConsoleKeyInput() 返回按键表并清空本次记录。 + auto& keys = getConsoleKeyInput(); + + if (keys[KEY_UP]) { + printToConsole("UP\r\n"); + } + if (keys[KEY_DOWN]) { + printToConsole("DOWN\r\n"); + } + if (keys[KEY_LEFT]) { + printToConsole("LEFT\r\n"); + } + if (keys[KEY_RIGHT]) { + printToConsole("RIGHT\r\n"); + } + if (keys[KEY_HOME]) { + printToConsole("HOME\r\n"); + } + if (keys[KEY_END]) { + printToConsole("END\r\n"); + } + if (keys[KEY_F1]) { + printToConsole("F1\r\n"); + } + if (keys[KEY_F2]) { + printToConsole("F2\r\n"); + } + if (keys[KEY_F3]) { + printToConsole("F3\r\n"); + } + if (keys[KEY_F4]) { + printToConsole("F4\r\n"); + } + + for (int c = 32; c < static_cast(ASCII_TABLE_SIZE); ++c) { + if (keys[static_cast(c)]) { + char text[2] = {static_cast(c), '\0'}; + printToConsole(text); + } + } + + if (keys[KEY_q] || keys[KEY_ESCAPE]) { + break; + } + + sleep_ms(16); + } + + quit(0); +} diff --git a/include/ConsoleLib.h b/include/ConsoleLib.h new file mode 100644 index 0000000..3ad49b5 --- /dev/null +++ b/include/ConsoleLib.h @@ -0,0 +1,101 @@ +#ifndef CONSOLELIB_H +#define CONSOLELIB_H + +#include +#include +#include +#include + +// SDL 风格的按键码:ASCII 字符直接使用其 ASCII 值,特殊键从 128 开始。 +enum Keycode : uint16_t { + KEY_UNKNOWN = 0, + + KEY_BACKSPACE = 8, + KEY_TAB = 9, + KEY_RETURN = 13, + KEY_ESCAPE = 27, + KEY_SPACE = 32, + + KEY_0 = '0', + KEY_1 = '1', + KEY_2 = '2', + KEY_3 = '3', + KEY_4 = '4', + KEY_5 = '5', + KEY_6 = '6', + KEY_7 = '7', + KEY_8 = '8', + KEY_9 = '9', + + KEY_a = 'a', + KEY_b = 'b', + KEY_c = 'c', + KEY_d = 'd', + KEY_e = 'e', + KEY_f = 'f', + KEY_g = 'g', + KEY_h = 'h', + KEY_i = 'i', + KEY_j = 'j', + KEY_k = 'k', + KEY_l = 'l', + KEY_m = 'm', + KEY_n = 'n', + KEY_o = 'o', + KEY_p = 'p', + KEY_q = 'q', + KEY_r = 'r', + KEY_s = 's', + KEY_t = 't', + KEY_u = 'u', + KEY_v = 'v', + KEY_w = 'w', + KEY_x = 'x', + KEY_y = 'y', + KEY_z = 'z', + + KEY_DELETE = 127, + + KEY_UP = 128, + KEY_DOWN, + KEY_LEFT, + KEY_RIGHT, + KEY_HOME, + KEY_END, + KEY_F1, + KEY_F2, + KEY_F3, + KEY_F4, + + KEY_COUNT +}; + +constexpr size_t KEYCODE_COUNT = static_cast(KEY_COUNT); +constexpr size_t ASCII_TABLE_SIZE = 128; +constexpr uint8_t DEFAULT_CONSOLE_INPUT_FREQUENCY = 50; +typedef std::uint32_t Color; + +namespace ConsoleLib { +void initConsole(); +void stopInputThread(); +void restoreConsoleMode(); + +void RawPrint(std::string_view text); +void printToConsole(std::string_view data); +void printToConsole(std::string_view data, Color color); +void clearConsole(); + +char getinput(); + +// 按 Keycode 返回按键状态,并清空内部记录。 +std::array& getConsoleKeyInput(); +bool isKeyPressed(Keycode key); + +void sleep_ms(unsigned int ms); +void setInputFrequency(std::uint8_t frequency); +std::uint8_t getInputFrequency(); + +void quit(int exitCode); +} // namespace ConsoleLib + +#endif // CONSOLELIB_H