This commit is contained in:
Eric Curtin 2025-12-17 05:51:07 +02:00 committed by GitHub
commit c1f5bdc788
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1751 additions and 3531 deletions

View File

@ -599,7 +599,7 @@ $ echo "source ~/.llama-completion.bash" >> ~/.bashrc
- [stb-image](https://github.com/nothings/stb) - Single-header image format decoder, used by multimodal subsystem - Public domain
- [nlohmann/json](https://github.com/nlohmann/json) - Single-header JSON library, used by various tools/examples - MIT License
- [minja](https://github.com/google/minja) - Minimal Jinja parser in C++, used by various tools/examples - MIT License
- [linenoise.cpp](./tools/run/linenoise.cpp/linenoise.cpp) - C++ library that provides readline-like line editing capabilities, used by `llama-run` - BSD 2-Clause License
- [readline.cpp](https://github.com/ericcurtin/readline.cpp) - C++ library that provides readline-like line editing capabilities, used by `llama-run` - MIT License
- [curl](https://curl.se/) - Client-side URL transfer library, used by various tools/examples - [CURL License](https://curl.se/docs/copyright.html)
- [miniaudio.h](https://github.com/mackron/miniaudio) - Single-header audio format decoder, used by multimodal subsystem - Public domain
- [subprocess.h](https://github.com/sheredom/subprocess.h) - Single-header process launching solution for C and C++ - Public domain

View File

@ -19,6 +19,10 @@ vendor = {
"https://raw.githubusercontent.com/yhirose/cpp-httplib/refs/tags/v0.28.0/httplib.h": "vendor/cpp-httplib/httplib.h",
"https://raw.githubusercontent.com/sheredom/subprocess.h/b49c56e9fe214488493021017bf3954b91c7c1f5/subprocess.h": "vendor/sheredom/subprocess.h",
# readline.cpp: multi-file library for interactive line editing
# sync manually - no upstream repository yet
# located in vendor/readline.cpp/
}
for url, filename in vendor.items():

View File

@ -1,5 +1,32 @@
set(TARGET llama-run)
add_executable(${TARGET} run.cpp linenoise.cpp/linenoise.cpp)
if (MINGW)
# fix: https://github.com/ggml-org/llama.cpp/actions/runs/9651004652/job/26617901362?pr=8006
add_compile_definitions(_WIN32_WINNT=${GGML_WIN_VER})
endif()
# Include server source files (except server.cpp which has its own main())
set(SERVER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../server)
set(READLINE_DIR ${PROJECT_SOURCE_DIR}/vendor/readline.cpp)
set(TARGET_SRCS
run.cpp
${SERVER_DIR}/server-context.cpp
${SERVER_DIR}/server-context.h
${SERVER_DIR}/server-task.cpp
${SERVER_DIR}/server-task.h
${SERVER_DIR}/server-queue.cpp
${SERVER_DIR}/server-queue.h
${SERVER_DIR}/server-common.cpp
${SERVER_DIR}/server-common.h
${CMAKE_CURRENT_SOURCE_DIR}/run-chat.cpp
${CMAKE_CURRENT_SOURCE_DIR}/run-chat.h
${READLINE_DIR}/src/readline.cpp
${READLINE_DIR}/src/buffer.cpp
${READLINE_DIR}/src/history.cpp
${READLINE_DIR}/src/terminal.cpp
)
add_executable(${TARGET} ${TARGET_SRCS})
# TODO: avoid copying this code block from common/CMakeLists.txt
set(LLAMA_RUN_EXTRA_LIBS "")
@ -19,5 +46,17 @@ if (CMAKE_SYSTEM_NAME MATCHES "AIX")
target_link_libraries(${TARGET} PRIVATE -lbsd)
endif()
target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT} ${LLAMA_RUN_EXTRA_LIBS})
# Include directories for server headers and readline
target_include_directories(${TARGET} PRIVATE ${SERVER_DIR})
target_include_directories(${TARGET} PRIVATE ${SERVER_DIR}/../mtmd)
target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR})
target_include_directories(${TARGET} PRIVATE ${READLINE_DIR}/include)
target_include_directories(${TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(${TARGET} PRIVATE common mtmd llama ${CMAKE_THREAD_LIBS_INIT} ${LLAMA_RUN_EXTRA_LIBS})
if (WIN32)
target_link_libraries(${TARGET} PRIVATE ws2_32)
endif()
target_compile_features(${TARGET} PRIVATE cxx_std_17)

File diff suppressed because it is too large Load Diff

View File

@ -1,137 +0,0 @@
/* linenoise.h -- VERSION 1.0
*
* Guerrilla line editing library against the idea that a line editing lib
* needs to be 20,000 lines of C++ code.
*
* See linenoise.cpp for more information.
*
* ------------------------------------------------------------------------
*
* Copyright (c) 2010-2023, Salvatore Sanfilippo <antirez at gmail dot com>
* Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>
* Copyright (c) 2025, Eric Curtin <ericcurtin17 at gmail dot com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef __LINENOISE_H
#define __LINENOISE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h> /* For size_t. */
#include <stdlib.h>
extern const char * linenoiseEditMore;
/* The linenoiseState structure represents the state during line editing.
* We pass this state to functions implementing specific editing
* functionalities. */
struct linenoiseState {
int in_completion; /* The user pressed TAB and we are now in completion
* mode, so input is handled by completeLine(). */
size_t completion_idx; /* Index of next completion to propose. */
int ifd; /* Terminal stdin file descriptor. */
int ofd; /* Terminal stdout file descriptor. */
char * buf; /* Edited line buffer. */
size_t buflen; /* Edited line buffer size. */
const char * prompt; /* Prompt to display. */
size_t plen; /* Prompt length. */
size_t pos; /* Current cursor position. */
size_t oldcolpos; /* Previous refresh cursor column position. */
size_t len; /* Current edited line length. */
size_t cols; /* Number of columns in terminal. */
size_t oldrows; /* Rows used by last refreshed line (multiline mode) */
int history_index; /* The history index we are currently editing. */
};
struct linenoiseCompletions {
size_t len = 0;
char ** cvec = nullptr;
bool to_free = true;
~linenoiseCompletions() {
if (!to_free) {
return;
}
for (size_t i = 0; i < len; ++i) {
free(cvec[i]);
}
free(cvec);
}
};
/* Non blocking API. */
int linenoiseEditStart(struct linenoiseState * l, int stdin_fd, int stdout_fd, char * buf, size_t buflen,
const char * prompt);
const char * linenoiseEditFeed(struct linenoiseState * l);
void linenoiseEditStop(struct linenoiseState * l);
void linenoiseHide(struct linenoiseState * l);
void linenoiseShow(struct linenoiseState * l);
/* Blocking API. */
const char * linenoise(const char * prompt);
void linenoiseFree(void * ptr);
/* Completion API. */
typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *);
typedef const char *(linenoiseHintsCallback) (const char *, int * color, int * bold);
typedef void(linenoiseFreeHintsCallback)(const char *);
void linenoiseSetCompletionCallback(linenoiseCompletionCallback *);
void linenoiseSetHintsCallback(linenoiseHintsCallback *);
void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *);
void linenoiseAddCompletion(linenoiseCompletions *, const char *);
/* History API. */
int linenoiseHistoryAdd(const char * line);
int linenoiseHistorySetMaxLen(int len);
int linenoiseHistorySave(const char * filename);
int linenoiseHistoryLoad(const char * filename);
/* Other utilities. */
void linenoiseClearScreen(void);
void linenoiseSetMultiLine(int ml);
void linenoisePrintKeyCodes(void);
void linenoiseMaskModeEnable(void);
void linenoiseMaskModeDisable(void);
/* Encoding functions. */
typedef size_t(linenoisePrevCharLen)(const char * buf, size_t buf_len, size_t pos, size_t * col_len);
typedef size_t(linenoiseNextCharLen)(const char * buf, size_t buf_len, size_t pos, size_t * col_len);
typedef size_t(linenoiseReadCode)(int fd, char * buf, size_t buf_len, int * c);
void linenoiseSetEncodingFunctions(linenoisePrevCharLen * prevCharLenFunc, linenoiseNextCharLen * nextCharLenFunc,
linenoiseReadCode * readCodeFunc);
#ifdef __cplusplus
}
#endif
#endif /* __LINENOISE_H */

178
tools/run/run-chat.cpp Normal file
View File

@ -0,0 +1,178 @@
// run-chat.cpp - Console/chat mode functionality for llama-run
//
// This file contains the implementation of interactive chat mode and signal handling.
#include "run-chat.h"
#include "server-context.h"
#include "server-common.h"
#include "readline/readline.h"
#include "common.h"
#include <nlohmann/json.hpp>
#include <atomic>
#include <csignal>
#include <iostream>
using json = nlohmann::ordered_json;
#if defined(_WIN32)
#include <windows.h>
#endif
// Static globals for signal handling
static std::function<void(int)> shutdown_handler;
static std::atomic_flag is_terminating = ATOMIC_FLAG_INIT;
static inline void signal_handler(int signal) {
if (is_terminating.test_and_set()) {
// in case it hangs, we can force terminate the server by hitting Ctrl+C twice
// this is for better developer experience, we can remove when the server is stable enough
fprintf(stderr, "Received second interrupt, force terminating...\n");
exit(1);
}
shutdown_handler(signal);
}
void setup_signal_handlers(std::function<void(int)> handler) {
shutdown_handler = handler;
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
struct sigaction sigint_action;
sigint_action.sa_handler = signal_handler;
sigemptyset(&sigint_action.sa_mask);
sigint_action.sa_flags = 0;
sigaction(SIGINT, &sigint_action, NULL);
sigaction(SIGTERM, &sigint_action, NULL);
#elif defined (_WIN32)
auto console_ctrl_handler = +[](DWORD ctrl_type) -> BOOL {
return (ctrl_type == CTRL_C_EVENT) ? (signal_handler(SIGINT), true) : false;
};
SetConsoleCtrlHandler(reinterpret_cast<PHANDLER_ROUTINE>(console_ctrl_handler), true);
#endif
}
void run_chat_mode(const common_params & params, server_context & ctx_server) {
// Initialize readline
readline::Prompt prompt_config;
prompt_config.prompt = "> ";
prompt_config.alt_prompt = ". ";
prompt_config.placeholder = "Send a message";
readline::Readline rl(prompt_config);
rl.history_enable();
// Initialize server routes
server_routes routes(params, ctx_server);
// Message history
json messages = json::array();
// Flag to check if we should stop (used by should_stop callback)
std::atomic<bool> stop_requested = false;
auto should_stop = [&]() { return stop_requested.load(); };
while (true) {
// Read user input
std::string user_input;
try {
user_input = rl.readline();
} catch (const readline::eof_error&) {
printf("\n");
break;
} catch (const readline::interrupt_error&) {
printf("\nUse Ctrl + d or /bye to exit.\n");
continue;
}
if (user_input.empty()) {
continue;
}
if (user_input == "/bye") {
break;
}
// Add user message to history
messages.push_back({
{"role", "user"},
{"content", user_input}
});
// Create request for chat completions endpoint
server_http_req req{
{}, {}, "",
safe_json_to_str(json{
{"messages", messages},
{"stream", true}
}),
should_stop
};
// Reset stop flag
stop_requested = false;
// Call the chat completions endpoint
auto res = routes.post_chat_completions(req);
std::string curr_text;
if (res->is_stream()) {
std::string chunk;
bool interrupted = false;
while (res->next(chunk)) {
// Check for interrupt (Ctrl-C) during streaming
if (rl.check_interrupt()) {
printf("\n");
interrupted = true;
stop_requested = true;
break;
}
std::vector<std::string> lines = string_split<std::string>(chunk, '\n');
for (auto & line : lines) {
if (line.empty()) {
continue;
}
if (line == "[DONE]") {
break;
}
std::string & data = line;
if (string_starts_with(line, "data: ")) {
data = line.substr(6);
}
try {
auto data_json = json::parse(data);
if (data_json.contains("choices") && !data_json["choices"].empty() &&
data_json["choices"][0].contains("delta") &&
data_json["choices"][0]["delta"].contains("content") &&
!data_json["choices"][0]["delta"]["content"].is_null()) {
std::string new_text = data_json["choices"][0]["delta"]["content"].get<std::string>();
curr_text += new_text;
std::cout << new_text << std::flush;
}
} catch (const std::exception & e) {
LOG_ERR("%s: error parsing JSON: %s\n", __func__, e.what());
}
}
}
if (!interrupted) {
std::cout << std::endl;
if (!curr_text.empty()) {
messages.push_back({
{"role", "assistant"},
{"content", curr_text}
});
}
} else {
// Remove the user message since generation was interrupted
messages.erase(messages.end() - 1);
}
} else {
std::cout << res->data << std::endl;
}
}
LOG_INF("%s: exiting chat mode\n", __func__);
}

13
tools/run/run-chat.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "common.h"
#include <functional>
// Forward declarations
struct server_context;
// Run interactive chat mode
void run_chat_mode(const common_params & params, server_context & ctx_server);
// Setup platform-specific signal handlers for console interruption
void setup_signal_handlers(std::function<void(int)> handler);

File diff suppressed because it is too large Load Diff

76
vendor/readline.cpp/README.md vendored Normal file
View File

@ -0,0 +1,76 @@
# readline.cpp
A readline implementation providing an interactive line editing interface with history support.
## Features
- Interactive line editing
- Command history with navigation (up/down arrows)
- Word-based navigation (Alt+B/Alt+F)
- Line editing commands (Ctrl+A, Ctrl+E, Ctrl+K, etc.)
- Bracket paste support
- Customizable prompts
- History persistence
## Building
readline.cpp uses CMake. To build:
```bash
# Create build directory
mkdir build
cd build
# Configure
cmake ..
# Build
cmake --build .
# Run the example
./simple_example
```
## Requirements
- C++17 compiler (GCC 7+, Clang 5+, or MSVC 2017+)
- CMake 3.14 or higher
- OSes: Linux, macOS, Windows (open to others)
## Using the Library
```cpp
#include "readline/readline.h"
#include "readline/errors.h"
#include <iostream>
int main() {
readline::Prompt prompt;
prompt.prompt = "> ";
prompt.alt_prompt = ". ";
prompt.placeholder = "Enter a command";
try {
readline::Readline rl(prompt);
rl.history_enable();
while (true) {
try {
std::string line = rl.readline();
std::cout << "You entered: " << line << "\n";
} catch (const readline::eof_error&) {
break;
} catch (const readline::interrupt_error&) {
std::cout << "^C\n";
continue;
}
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
return 0;
}
```

View File

@ -0,0 +1,71 @@
#pragma once
#include <string>
#include <vector>
#include <locale>
#include <codecvt>
namespace readline {
struct Prompt {
std::string prompt = "> ";
std::string alt_prompt = ". ";
std::string placeholder = "";
std::string alt_placeholder = "";
bool use_alt = false;
std::string get_prompt() const {
return use_alt ? alt_prompt : prompt;
}
std::string get_placeholder() const {
return use_alt ? alt_placeholder : placeholder;
}
};
class Buffer {
public:
explicit Buffer(const Prompt& prompt);
~Buffer() = default;
void add(char32_t c);
void remove();
void delete_char();
void delete_before();
void delete_remaining();
void delete_word();
void move_left();
void move_right();
void move_left_word();
void move_right_word();
void move_to_start();
void move_to_end();
void replace(const std::u32string& text);
void clear_screen();
bool is_empty() const { return buffer_.empty(); }
std::string to_string() const;
size_t display_size() const;
private:
void add_char(char32_t c, bool insert);
void draw_remaining();
int count_remaining_line_width(int place);
bool get_line_spacing(int line) const;
int char_width(char32_t c) const;
std::string to_utf8(char32_t c) const;
std::string to_utf8(const std::u32string& str) const;
std::u32string buffer_;
std::vector<bool> line_has_space_;
Prompt prompt_;
size_t pos_ = 0;
size_t display_pos_ = 0;
int width_ = 80;
int height_ = 24;
int line_width_ = 70;
};
} // namespace readline

View File

@ -0,0 +1,26 @@
#pragma once
#include <exception>
#include <string>
namespace readline {
class interrupt_error : public std::exception {
public:
interrupt_error() = default;
const char* what() const noexcept override {
return "Interrupt";
}
};
class eof_error : public std::exception {
public:
eof_error() = default;
const char* what() const noexcept override {
return "EOF";
}
};
} // namespace readline

View File

@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <vector>
#include <filesystem>
namespace readline {
class History {
public:
History();
~History() = default;
void init();
void add(const std::string& line);
void compact();
void clear();
std::string prev();
std::string next();
size_t size() const { return buffer_.size(); }
void save();
bool enabled = true;
bool autosave = true;
size_t pos = 0;
size_t limit = 100;
private:
std::vector<std::string> buffer_;
std::filesystem::path filename_;
};
} // namespace readline

View File

@ -0,0 +1,38 @@
#pragma once
#include "readline/buffer.h"
#include "readline/history.h"
#include "readline/terminal.h"
#include "readline/errors.h"
#include <memory>
#include <string>
namespace readline {
class Readline {
public:
explicit Readline(const Prompt& prompt);
~Readline() = default;
std::string readline();
void history_enable() { history_->enabled = true; }
void history_disable() { history_->enabled = false; }
bool check_interrupt();
History* history() { return history_.get(); }
Terminal* terminal() { return terminal_.get(); }
bool is_pasting() const { return pasting_; }
private:
void history_prev(Buffer* buf, std::u32string& current_line_buf);
void history_next(Buffer* buf, std::u32string& current_line_buf);
std::u32string utf8_to_utf32(const std::string& str);
std::string utf32_to_utf8(const std::u32string& str);
Prompt prompt_;
std::unique_ptr<Terminal> terminal_;
std::unique_ptr<History> history_;
bool pasting_ = false;
};
} // namespace readline

View File

@ -0,0 +1,51 @@
#pragma once
#include <optional>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#ifdef _WIN32
#include <windows.h>
#else
#include <termios.h>
#include <unistd.h>
#endif
namespace readline {
class Terminal {
public:
Terminal();
~Terminal();
void set_raw_mode();
void unset_raw_mode();
bool is_raw_mode() const { return raw_mode_; }
std::optional<char> read();
std::optional<char> try_read();
bool is_terminal(int fd);
private:
void io_loop();
#ifdef _WIN32
HANDLE input_handle_;
HANDLE output_handle_;
DWORD original_input_mode_;
DWORD original_output_mode_;
#else
int fd_;
struct termios original_termios_;
#endif
bool raw_mode_;
std::thread io_thread_;
std::queue<char> char_queue_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::atomic<bool> stop_io_loop_;
};
} // namespace readline

View File

@ -0,0 +1,85 @@
#pragma once
#include <string>
namespace readline {
// Control characters
constexpr char CHAR_NULL = 0;
constexpr char CHAR_LINE_START = 1;
constexpr char CHAR_BACKWARD = 2;
constexpr char CHAR_INTERRUPT = 3;
constexpr char CHAR_DELETE = 4;
constexpr char CHAR_LINE_END = 5;
constexpr char CHAR_FORWARD = 6;
constexpr char CHAR_BELL = 7;
constexpr char CHAR_CTRL_H = 8;
constexpr char CHAR_TAB = 9;
constexpr char CHAR_CTRL_J = 10;
constexpr char CHAR_KILL = 11;
constexpr char CHAR_CTRL_L = 12;
constexpr char CHAR_ENTER = 13;
constexpr char CHAR_NEXT = 14;
constexpr char CHAR_PREV = 16;
constexpr char CHAR_BCK_SEARCH = 18;
constexpr char CHAR_FWD_SEARCH = 19;
constexpr char CHAR_TRANSPOSE = 20;
constexpr char CHAR_CTRL_U = 21;
constexpr char CHAR_CTRL_W = 23;
constexpr char CHAR_CTRL_Y = 25;
constexpr char CHAR_CTRL_Z = 26;
constexpr char CHAR_ESC = 27;
constexpr char CHAR_SPACE = 32;
constexpr char CHAR_ESCAPE_EX = 91;
constexpr char CHAR_BACKSPACE = 127;
// Special keys
constexpr char KEY_DEL = 51;
constexpr char KEY_UP = 65;
constexpr char KEY_DOWN = 66;
constexpr char KEY_RIGHT = 67;
constexpr char KEY_LEFT = 68;
constexpr char META_END = 70;
constexpr char META_START = 72;
// ANSI escape sequences
constexpr const char* ESC = "\x1b";
constexpr const char* CURSOR_SAVE = "\x1b[s";
constexpr const char* CURSOR_RESTORE = "\x1b[u";
constexpr const char* CURSOR_EOL = "\x1b[E";
constexpr const char* CURSOR_BOL = "\x1b[1G";
constexpr const char* CURSOR_HIDE = "\x1b[?25l";
constexpr const char* CURSOR_SHOW = "\x1b[?25h";
constexpr const char* CLEAR_TO_EOL = "\x1b[K";
constexpr const char* CLEAR_LINE = "\x1b[2K";
constexpr const char* CLEAR_SCREEN = "\x1b[2J";
constexpr const char* CURSOR_RESET = "\x1b[0;0f";
constexpr const char* COLOR_GREY = "\x1b[38;5;245m";
constexpr const char* COLOR_DEFAULT = "\x1b[0m";
constexpr const char* COLOR_BOLD = "\x1b[1m";
constexpr const char* START_BRACKETED_PASTE = "\x1b[?2004h";
constexpr const char* END_BRACKETED_PASTE = "\x1b[?2004l";
// Cursor movement functions
inline std::string cursor_up_n(int n) {
return std::string(ESC) + "[" + std::to_string(n) + "A";
}
inline std::string cursor_down_n(int n) {
return std::string(ESC) + "[" + std::to_string(n) + "B";
}
inline std::string cursor_right_n(int n) {
return std::string(ESC) + "[" + std::to_string(n) + "C";
}
inline std::string cursor_left_n(int n) {
return std::string(ESC) + "[" + std::to_string(n) + "D";
}
// Bracketed paste
constexpr char CHAR_BRACKETED_PASTE = 50;
constexpr const char* CHAR_BRACKETED_PASTE_START = "00~";
constexpr const char* CHAR_BRACKETED_PASTE_END = "01~";
} // namespace readline

435
vendor/readline.cpp/src/buffer.cpp vendored Normal file
View File

@ -0,0 +1,435 @@
#include "readline/buffer.h"
#include "readline/types.h"
#include <iostream>
#include <algorithm>
#include <cstring>
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#include <io.h>
#define STDOUT_FILENO _fileno(stdout)
#else
#include <sys/ioctl.h>
#include <unistd.h>
#endif
namespace readline {
Buffer::Buffer(const Prompt& prompt)
: prompt_(prompt) {
// Get terminal size
#ifdef _WIN32
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
width_ = csbi.srWindow.Right - csbi.srWindow.Left + 1;
height_ = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}
#else
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
width_ = ws.ws_col;
height_ = ws.ws_row;
}
#endif
line_width_ = width_ - static_cast<int>(prompt_.get_prompt().length());
}
int Buffer::char_width(char32_t c) const {
// Simplified width calculation
// Full CJK width detection would require ICU or similar library
if (c >= 0x1100 && c <= 0x115F) return 2; // Hangul Jamo
if (c >= 0x2E80 && c <= 0x9FFF) return 2; // CJK
if (c >= 0xAC00 && c <= 0xD7A3) return 2; // Hangul Syllables
if (c >= 0xF900 && c <= 0xFAFF) return 2; // CJK Compatibility Ideographs
if (c >= 0xFE10 && c <= 0xFE19) return 2; // Vertical forms
if (c >= 0xFE30 && c <= 0xFE6F) return 2; // CJK Compatibility Forms
if (c >= 0xFF00 && c <= 0xFF60) return 2; // Fullwidth Forms
if (c >= 0xFFE0 && c <= 0xFFE6) return 2; // Fullwidth Forms
if (c >= 0x20000 && c <= 0x2FFFD) return 2; // CJK Extensions
if (c >= 0x30000 && c <= 0x3FFFD) return 2; // CJK Extensions
return 1;
}
std::string Buffer::to_utf8(char32_t c) const {
std::string result;
if (c <= 0x7F) {
result += static_cast<char>(c);
} else if (c <= 0x7FF) {
result += static_cast<char>(0xC0 | ((c >> 6) & 0x1F));
result += static_cast<char>(0x80 | (c & 0x3F));
} else if (c <= 0xFFFF) {
result += static_cast<char>(0xE0 | ((c >> 12) & 0x0F));
result += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
result += static_cast<char>(0x80 | (c & 0x3F));
} else if (c <= 0x10FFFF) {
result += static_cast<char>(0xF0 | ((c >> 18) & 0x07));
result += static_cast<char>(0x80 | ((c >> 12) & 0x3F));
result += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
result += static_cast<char>(0x80 | (c & 0x3F));
}
return result;
}
std::string Buffer::to_utf8(const std::u32string& str) const {
std::string result;
for (char32_t c : str) {
result += to_utf8(c);
}
return result;
}
std::string Buffer::to_string() const {
return to_utf8(buffer_);
}
size_t Buffer::display_size() const {
size_t sum = 0;
for (char32_t c : buffer_) {
sum += char_width(c);
}
return sum;
}
bool Buffer::get_line_spacing(int line) const {
if (line >= 0 && line < static_cast<int>(line_has_space_.size())) {
return line_has_space_[line];
}
return false;
}
void Buffer::move_left() {
if (pos_ > 0) {
char32_t r = buffer_[pos_ - 1];
int r_length = char_width(r);
if (display_pos_ % line_width_ == 0) {
std::cout << cursor_up_n(1) << CURSOR_BOL << cursor_right_n(width_);
if (r_length == 2) {
std::cout << cursor_left_n(1);
}
int line = static_cast<int>(display_pos_ / line_width_) - 1;
bool has_space = get_line_spacing(line);
if (has_space) {
display_pos_ -= 1;
std::cout << cursor_left_n(1);
}
} else {
std::cout << cursor_left_n(r_length);
}
pos_ -= 1;
display_pos_ -= r_length;
}
}
void Buffer::move_right() {
if (pos_ < buffer_.size()) {
char32_t r = buffer_[pos_];
int r_length = char_width(r);
pos_ += 1;
bool has_space = get_line_spacing(display_pos_ / line_width_);
display_pos_ += r_length;
if (display_pos_ % line_width_ == 0) {
std::cout << cursor_down_n(1) << CURSOR_BOL
<< cursor_right_n(static_cast<int>(prompt_.get_prompt().length()));
} else if ((display_pos_ - r_length) % line_width_ == static_cast<size_t>(line_width_ - 1) && has_space) {
std::cout << cursor_down_n(1) << CURSOR_BOL
<< cursor_right_n(static_cast<int>(prompt_.get_prompt().length()) + r_length);
display_pos_ += 1;
} else if (!line_has_space_.empty() && display_pos_ % line_width_ == static_cast<size_t>(line_width_ - 1) && has_space) {
std::cout << cursor_down_n(1) << CURSOR_BOL
<< cursor_right_n(static_cast<int>(prompt_.get_prompt().length()));
display_pos_ += 1;
} else {
std::cout << cursor_right_n(r_length);
}
}
}
void Buffer::move_left_word() {
if (pos_ > 0) {
bool found_nonspace = false;
while (pos_ > 0) {
char32_t v = buffer_[pos_ - 1];
if (v == U' ') {
if (found_nonspace) {
break;
}
} else {
found_nonspace = true;
}
move_left();
}
}
}
void Buffer::move_right_word() {
if (pos_ < buffer_.size()) {
while (pos_ < buffer_.size()) {
move_right();
if (pos_ < buffer_.size() && buffer_[pos_] == U' ') {
break;
}
}
}
}
void Buffer::move_to_start() {
if (pos_ > 0) {
int curr_line = static_cast<int>(display_pos_ / line_width_);
if (curr_line > 0) {
std::cout << cursor_up_n(curr_line);
}
std::cout << CURSOR_BOL << cursor_right_n(static_cast<int>(prompt_.get_prompt().length()));
pos_ = 0;
display_pos_ = 0;
}
}
void Buffer::move_to_end() {
if (pos_ < buffer_.size()) {
int curr_line = static_cast<int>(display_pos_ / line_width_);
int total_lines = static_cast<int>(display_size() / line_width_);
if (curr_line < total_lines) {
std::cout << cursor_down_n(total_lines - curr_line);
int remainder = static_cast<int>(display_size() % line_width_);
std::cout << CURSOR_BOL
<< cursor_right_n(static_cast<int>(prompt_.get_prompt().length()) + remainder);
} else {
std::cout << cursor_right_n(static_cast<int>(display_size() - display_pos_));
}
pos_ = buffer_.size();
display_pos_ = display_size();
}
}
void Buffer::add(char32_t c) {
if (pos_ == buffer_.size()) {
add_char(c, false);
} else {
add_char(c, true);
}
}
void Buffer::add_char(char32_t c, bool insert) {
int r_length = char_width(c);
display_pos_ += r_length;
if (pos_ > 0) {
if (display_pos_ % line_width_ == 0) {
std::cout << to_utf8(c) << "\n" << prompt_.alt_prompt;
if (insert) {
if (display_pos_ / line_width_ - 1 < line_has_space_.size()) {
line_has_space_[display_pos_ / line_width_ - 1] = false;
}
} else {
line_has_space_.push_back(false);
}
} else if (display_pos_ % line_width_ < (display_pos_ - r_length) % line_width_) {
if (insert) {
std::cout << CLEAR_TO_EOL;
}
std::cout << "\n" << prompt_.alt_prompt;
display_pos_ += 1;
std::cout << to_utf8(c);
if (insert) {
if (display_pos_ / line_width_ - 1 < line_has_space_.size()) {
line_has_space_[display_pos_ / line_width_ - 1] = true;
}
} else {
line_has_space_.push_back(true);
}
} else {
std::cout << to_utf8(c);
}
} else {
std::cout << to_utf8(c);
}
if (insert) {
buffer_.insert(buffer_.begin() + pos_, c);
} else {
buffer_.push_back(c);
}
pos_ += 1;
if (insert) {
draw_remaining();
}
}
int Buffer::count_remaining_line_width(int place) {
int sum = 0;
int counter = -1;
int prev_len = 0;
while (place <= line_width_) {
counter += 1;
sum += prev_len;
if (pos_ + counter < buffer_.size()) {
char32_t r = buffer_[pos_ + counter];
place += char_width(r);
prev_len = static_cast<int>(to_utf8(r).length());
} else {
break;
}
}
return sum;
}
void Buffer::draw_remaining() {
int place = 0;
std::string remaining_text = to_utf8(buffer_.substr(pos_));
if (pos_ > 0) {
place = display_pos_ % line_width_;
}
std::cout << CURSOR_HIDE;
int curr_line_length = count_remaining_line_width(place);
std::string curr_line = remaining_text.substr(0, std::min(static_cast<size_t>(curr_line_length),
remaining_text.length()));
if (!curr_line.empty()) {
std::cout << CLEAR_TO_EOL << curr_line << cursor_left_n(static_cast<int>(curr_line.length()));
} else {
std::cout << CLEAR_TO_EOL;
}
std::cout << CURSOR_SHOW;
}
void Buffer::remove() {
if (!buffer_.empty() && pos_ > 0) {
char32_t r = buffer_[pos_ - 1];
int r_length = char_width(r);
if (display_pos_ % line_width_ == 0) {
std::cout << CURSOR_BOL << CLEAR_TO_EOL << cursor_up_n(1)
<< CURSOR_BOL << cursor_right_n(width_);
bool has_space = get_line_spacing(display_pos_ / line_width_ - 1);
if (has_space) {
display_pos_ -= 1;
std::cout << cursor_left_n(1);
}
if (r_length == 2) {
std::cout << cursor_left_n(1) << " " << cursor_left_n(2);
} else {
std::cout << " " << cursor_left_n(1);
}
} else {
std::cout << cursor_left_n(r_length);
for (int i = 0; i < r_length; ++i) {
std::cout << " ";
}
std::cout << cursor_left_n(r_length);
}
pos_ -= 1;
display_pos_ -= r_length;
buffer_.erase(buffer_.begin() + pos_);
if (pos_ < buffer_.size()) {
draw_remaining();
}
}
}
void Buffer::delete_char() {
if (!buffer_.empty() && pos_ < buffer_.size()) {
buffer_.erase(buffer_.begin() + pos_);
draw_remaining();
}
}
void Buffer::delete_before() {
while (pos_ > 0) {
remove();
}
}
void Buffer::delete_remaining() {
while (pos_ < buffer_.size()) {
delete_char();
}
}
void Buffer::delete_word() {
if (!buffer_.empty() && pos_ > 0) {
bool found_nonspace = false;
while (pos_ > 0) {
char32_t v = buffer_[pos_ - 1];
if (v == U' ') {
if (!found_nonspace) {
remove();
} else {
break;
}
} else {
found_nonspace = true;
remove();
}
}
}
}
void Buffer::replace(const std::u32string& text) {
display_pos_ = 0;
pos_ = 0;
int line_nums = static_cast<int>(display_size() / line_width_);
buffer_.clear();
std::cout << CURSOR_BOL << CLEAR_TO_EOL;
for (int i = 0; i < line_nums; ++i) {
std::cout << cursor_up_n(1) << CURSOR_BOL << CLEAR_TO_EOL;
}
std::cout << CURSOR_BOL << prompt_.get_prompt();
for (char32_t c : text) {
add(c);
}
}
void Buffer::clear_screen() {
std::cout << CLEAR_SCREEN << CURSOR_RESET << prompt_.get_prompt();
if (is_empty()) {
std::string ph = prompt_.get_placeholder();
std::cout << COLOR_GREY << ph << cursor_left_n(static_cast<int>(ph.length())) << COLOR_DEFAULT;
} else {
size_t curr_pos = display_pos_;
size_t curr_index = pos_;
pos_ = 0;
display_pos_ = 0;
draw_remaining();
std::cout << CURSOR_RESET << cursor_right_n(static_cast<int>(prompt_.get_prompt().length()));
if (curr_pos > 0) {
int target_line = static_cast<int>(curr_pos / line_width_);
if (target_line > 0) {
std::cout << cursor_down_n(target_line);
}
int remainder = static_cast<int>(curr_pos % line_width_);
if (remainder > 0) {
std::cout << cursor_right_n(remainder);
}
if (curr_pos % line_width_ == 0) {
std::cout << CURSOR_BOL << prompt_.alt_prompt;
}
}
pos_ = curr_index;
display_pos_ = curr_pos;
}
}
} // namespace readline

111
vendor/readline.cpp/src/history.cpp vendored Normal file
View File

@ -0,0 +1,111 @@
#include "readline/history.h"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <cstdlib>
namespace readline {
History::History() {
init();
}
void History::init() {
#ifdef _WIN32
const char* env_var = "USERPROFILE";
#else
const char* env_var = "HOME";
#endif
const char* home = std::getenv(env_var);
if (!home) {
return;
}
std::filesystem::path history_dir = std::filesystem::path(home) / ".readline";
filename_ = history_dir / "history";
// Create directory if it doesn't exist
if (!std::filesystem::exists(history_dir)) {
std::filesystem::create_directories(history_dir);
}
// Read existing history file
std::ifstream file(filename_);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
// Trim whitespace
line.erase(0, line.find_first_not_of(" \t\n\r"));
line.erase(line.find_last_not_of(" \t\n\r") + 1);
if (!line.empty()) {
add(line);
}
}
file.close();
}
}
void History::add(const std::string& line) {
buffer_.push_back(line);
compact();
pos = size();
if (autosave) {
save();
}
}
void History::compact() {
while (buffer_.size() > limit) {
buffer_.erase(buffer_.begin());
}
}
void History::clear() {
buffer_.clear();
}
std::string History::prev() {
if (pos > 0) {
pos--;
}
if (pos < buffer_.size()) {
return buffer_[pos];
}
return "";
}
std::string History::next() {
if (pos < buffer_.size()) {
pos++;
if (pos < buffer_.size()) {
return buffer_[pos];
}
}
return "";
}
void History::save() {
if (!enabled) {
return;
}
std::filesystem::path tmp_file = filename_;
tmp_file += ".tmp";
std::ofstream file(tmp_file);
if (!file.is_open()) {
throw std::runtime_error("Failed to open history file for writing");
}
for (const auto& line : buffer_) {
file << line << '\n';
}
file.close();
// Atomic rename
std::filesystem::rename(tmp_file, filename_);
}
} // namespace readline

287
vendor/readline.cpp/src/readline.cpp vendored Normal file
View File

@ -0,0 +1,287 @@
#include "readline/readline.h"
#include "readline/types.h"
#include <iostream>
#include <signal.h>
namespace readline {
Readline::Readline(const Prompt& prompt)
: prompt_(prompt),
terminal_(std::make_unique<Terminal>()),
history_(std::make_unique<History>()) {
}
std::u32string Readline::utf8_to_utf32(const std::string& str) {
std::u32string result;
size_t i = 0;
while (i < str.length()) {
char32_t codepoint = 0;
unsigned char c = str[i];
if (c <= 0x7F) {
codepoint = c;
i += 1;
} else if ((c & 0xE0) == 0xC0) {
if (i + 1 >= str.length()) break;
codepoint = ((c & 0x1F) << 6) | (str[i + 1] & 0x3F);
i += 2;
} else if ((c & 0xF0) == 0xE0) {
if (i + 2 >= str.length()) break;
codepoint = ((c & 0x0F) << 12) | ((str[i + 1] & 0x3F) << 6) | (str[i + 2] & 0x3F);
i += 3;
} else if ((c & 0xF8) == 0xF0) {
if (i + 3 >= str.length()) break;
codepoint = ((c & 0x07) << 18) | ((str[i + 1] & 0x3F) << 12) |
((str[i + 2] & 0x3F) << 6) | (str[i + 3] & 0x3F);
i += 4;
} else {
i += 1;
continue;
}
result += codepoint;
}
return result;
}
std::string Readline::utf32_to_utf8(const std::u32string& str) {
std::string result;
for (char32_t c : str) {
if (c <= 0x7F) {
result += static_cast<char>(c);
} else if (c <= 0x7FF) {
result += static_cast<char>(0xC0 | ((c >> 6) & 0x1F));
result += static_cast<char>(0x80 | (c & 0x3F));
} else if (c <= 0xFFFF) {
result += static_cast<char>(0xE0 | ((c >> 12) & 0x0F));
result += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
result += static_cast<char>(0x80 | (c & 0x3F));
} else if (c <= 0x10FFFF) {
result += static_cast<char>(0xF0 | ((c >> 18) & 0x07));
result += static_cast<char>(0x80 | ((c >> 12) & 0x3F));
result += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
result += static_cast<char>(0x80 | (c & 0x3F));
}
}
return result;
}
bool Readline::check_interrupt() {
// Ensure raw mode is set
if (!terminal_->is_raw_mode()) {
terminal_->set_raw_mode();
}
// Check if there's input available without blocking
auto opt_r = terminal_->try_read();
if (opt_r && *opt_r == CHAR_INTERRUPT) {
return true;
}
return false;
}
std::string Readline::readline() {
// Ensure raw mode is set and I/O thread is running
if (!terminal_->is_raw_mode()) {
terminal_->set_raw_mode();
}
std::string prompt = prompt_.get_prompt();
if (pasting_) {
prompt = prompt_.alt_prompt;
}
std::cout << prompt << std::flush;
Buffer buf(prompt_);
bool esc = false;
bool escex = false;
bool meta_del = false;
std::u32string current_line_buf;
while (true) {
bool show_placeholder = !pasting_ || prompt_.use_alt;
if (buf.is_empty() && show_placeholder) {
std::string ph = prompt_.get_placeholder();
std::cout << COLOR_GREY << ph << cursor_left_n(static_cast<int>(ph.length()))
<< COLOR_DEFAULT << std::flush;
}
auto opt_r = terminal_->read();
if (!opt_r) {
throw eof_error();
}
char r = *opt_r;
if (buf.is_empty()) {
std::cout << CLEAR_TO_EOL << std::flush;
}
if (escex) {
escex = false;
switch (r) {
case KEY_UP:
history_prev(&buf, current_line_buf);
break;
case KEY_DOWN:
history_next(&buf, current_line_buf);
break;
case KEY_LEFT:
buf.move_left();
break;
case KEY_RIGHT:
buf.move_right();
break;
case CHAR_BRACKETED_PASTE: {
std::string code;
for (int i = 0; i < 3; ++i) {
auto c = terminal_->read();
if (c) {
code += *c;
}
}
if (code == CHAR_BRACKETED_PASTE_START) {
pasting_ = true;
} else if (code == CHAR_BRACKETED_PASTE_END) {
pasting_ = false;
}
break;
}
case KEY_DEL:
if (buf.display_size() > 0) {
buf.delete_char();
}
meta_del = true;
break;
case META_START:
buf.move_to_start();
break;
case META_END:
buf.move_to_end();
break;
default:
continue;
}
continue;
} else if (esc) {
esc = false;
switch (r) {
case 'b':
buf.move_left_word();
break;
case 'f':
buf.move_right_word();
break;
case CHAR_BACKSPACE:
buf.delete_word();
break;
case CHAR_ESCAPE_EX:
escex = true;
break;
}
continue;
}
switch (r) {
case CHAR_NULL:
continue;
case CHAR_ESC:
esc = true;
break;
case CHAR_INTERRUPT:
throw interrupt_error();
case CHAR_PREV:
history_prev(&buf, current_line_buf);
break;
case CHAR_NEXT:
history_next(&buf, current_line_buf);
break;
case CHAR_LINE_START:
buf.move_to_start();
break;
case CHAR_LINE_END:
buf.move_to_end();
break;
case CHAR_BACKWARD:
buf.move_left();
break;
case CHAR_FORWARD:
buf.move_right();
break;
case CHAR_BACKSPACE:
case CHAR_CTRL_H:
buf.remove();
break;
case CHAR_TAB:
for (int i = 0; i < 8; ++i) {
buf.add(U' ');
}
break;
case CHAR_DELETE:
if (buf.display_size() > 0) {
buf.delete_char();
} else {
throw eof_error();
}
break;
case CHAR_KILL:
buf.delete_remaining();
break;
case CHAR_CTRL_U:
buf.delete_before();
break;
case CHAR_CTRL_L:
buf.clear_screen();
break;
case CHAR_CTRL_W:
buf.delete_word();
break;
case CHAR_CTRL_Z:
#ifndef _WIN32
kill(0, SIGSTOP);
#endif
return "";
case CHAR_ENTER:
case CHAR_CTRL_J: {
std::string output = buf.to_string();
if (!output.empty()) {
history_->add(output);
}
buf.move_to_end();
std::cout << std::endl;
return output;
}
default:
if (meta_del) {
meta_del = false;
continue;
}
if (r >= CHAR_SPACE || r == CHAR_ENTER || r == CHAR_CTRL_J) {
buf.add(static_cast<char32_t>(static_cast<unsigned char>(r)));
}
}
}
}
void Readline::history_prev(Buffer* buf, std::u32string& current_line_buf) {
if (history_->pos > 0) {
if (history_->pos == history_->size()) {
current_line_buf = utf8_to_utf32(buf->to_string());
}
buf->replace(utf8_to_utf32(history_->prev()));
}
}
void Readline::history_next(Buffer* buf, std::u32string& current_line_buf) {
if (history_->pos < history_->size()) {
buf->replace(utf8_to_utf32(history_->next()));
if (history_->pos == history_->size()) {
buf->replace(current_line_buf);
}
}
}
} // namespace readline

241
vendor/readline.cpp/src/terminal.cpp vendored Normal file
View File

@ -0,0 +1,241 @@
#include "readline/terminal.h"
#include "readline/errors.h"
#include <stdexcept>
#include <iostream>
#ifdef _WIN32
#include <io.h>
#include <cstdio>
#define STDIN_FILENO _fileno(stdin)
#else
#include <signal.h>
#include <cstdio>
#include <unistd.h>
#include <cerrno>
#endif
namespace readline {
Terminal::Terminal()
: raw_mode_(false), stop_io_loop_(false) {
#ifdef _WIN32
input_handle_ = GetStdHandle(STD_INPUT_HANDLE);
output_handle_ = GetStdHandle(STD_OUTPUT_HANDLE);
if (input_handle_ == INVALID_HANDLE_VALUE || output_handle_ == INVALID_HANDLE_VALUE) {
throw std::runtime_error("Failed to get console handles");
}
if (!is_terminal(STDIN_FILENO)) {
throw std::runtime_error("stdin is not a terminal");
}
#else
fd_ = STDIN_FILENO;
if (!is_terminal(fd_)) {
throw std::runtime_error("stdin is not a terminal");
}
#endif
// Don't start I/O thread yet - will be started when needed
}
Terminal::~Terminal() {
if (raw_mode_) {
unset_raw_mode();
}
stop_io_loop_ = true;
queue_cv_.notify_all();
// Detach the I/O thread - it will be terminated when the process exits
// We can't safely join it because it may be blocked on read()
if (io_thread_.joinable()) {
io_thread_.detach();
}
}
void Terminal::set_raw_mode() {
if (raw_mode_) {
return;
}
#ifdef _WIN32
// Get current console mode
if (!GetConsoleMode(input_handle_, &original_input_mode_)) {
throw std::runtime_error("Failed to get console input mode");
}
if (!GetConsoleMode(output_handle_, &original_output_mode_)) {
throw std::runtime_error("Failed to get console output mode");
}
// Set raw mode for input
DWORD input_mode = original_input_mode_;
input_mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
input_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
if (!SetConsoleMode(input_handle_, input_mode)) {
throw std::runtime_error("Failed to set console to raw mode");
}
// Enable virtual terminal processing for output (for ANSI escape sequences)
DWORD output_mode = original_output_mode_;
output_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode(output_handle_, output_mode)) {
// Restore input mode if output mode fails
SetConsoleMode(input_handle_, original_input_mode_);
throw std::runtime_error("Failed to enable virtual terminal processing");
}
#else
// Get current terminal settings
if (tcgetattr(fd_, &original_termios_) < 0) {
throw std::runtime_error("Failed to get terminal attributes");
}
struct termios raw = original_termios_;
// Set raw mode flags
raw.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
raw.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
raw.c_cflag &= ~(CSIZE | PARENB);
raw.c_cflag |= CS8;
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
if (tcsetattr(fd_, TCSAFLUSH, &raw) < 0) {
throw std::runtime_error("Failed to set terminal to raw mode");
}
// Disable stdout buffering for immediate character display
std::setvbuf(stdout, nullptr, _IONBF, 0);
#endif
raw_mode_ = true;
// Start I/O thread now that raw mode is set
if (!io_thread_.joinable()) {
io_thread_ = std::thread(&Terminal::io_loop, this);
}
}
void Terminal::unset_raw_mode() {
if (!raw_mode_) {
return;
}
#ifdef _WIN32
if (!SetConsoleMode(input_handle_, original_input_mode_)) {
throw std::runtime_error("Failed to restore console input mode");
}
if (!SetConsoleMode(output_handle_, original_output_mode_)) {
throw std::runtime_error("Failed to restore console output mode");
}
#else
if (tcsetattr(fd_, TCSANOW, &original_termios_) < 0) {
throw std::runtime_error("Failed to restore terminal settings");
}
#endif
raw_mode_ = false;
}
bool Terminal::is_terminal(int fd) {
#ifdef _WIN32
return _isatty(fd) != 0;
#else
return isatty(fd) != 0;
#endif
}
void Terminal::io_loop() {
#ifdef _WIN32
while (!stop_io_loop_) {
DWORD num_events = 0;
if (!GetNumberOfConsoleInputEvents(input_handle_, &num_events)) {
break;
}
if (num_events == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
INPUT_RECORD input_record;
DWORD num_read = 0;
if (!ReadConsoleInput(input_handle_, &input_record, 1, &num_read)) {
break;
}
if (num_read == 0) {
continue;
}
// Only process key events
if (input_record.EventType == KEY_EVENT && input_record.Event.KeyEvent.bKeyDown) {
char c = input_record.Event.KeyEvent.uChar.AsciiChar;
if (c != 0) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
char_queue_.push(c);
}
queue_cv_.notify_one();
}
}
}
#else
while (!stop_io_loop_) {
char c;
ssize_t n = ::read(fd_, &c, 1);
if (n < 0) {
if (errno == EINTR || errno == EAGAIN) {
continue;
}
break;
}
if (n == 0) {
break;
}
{
std::lock_guard<std::mutex> lock(queue_mutex_);
char_queue_.push(c);
}
queue_cv_.notify_one();
}
#endif
}
std::optional<char> Terminal::read() {
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait(lock, [this] {
return !char_queue_.empty() || stop_io_loop_;
});
if (stop_io_loop_ && char_queue_.empty()) {
return std::nullopt;
}
char c = char_queue_.front();
char_queue_.pop();
return c;
}
std::optional<char> Terminal::try_read() {
std::lock_guard<std::mutex> lock(queue_mutex_);
if (char_queue_.empty()) {
return std::nullopt;
}
char c = char_queue_.front();
char_queue_.pop();
return c;
}
} // namespace readline