diff --git a/.gitignore b/.gitignore index 6262401..4afbb19 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ build/ cmake-build-*/ +# CodeQL artifacts +_codeql_build_dir/ +_codeql_detected_source_root + # Compiled binaries *.o *.a diff --git a/include/readline/terminal.h b/include/readline/terminal.h index c0464a3..6151553 100644 --- a/include/readline/terminal.h +++ b/include/readline/terminal.h @@ -1,7 +1,12 @@ #pragma once +#ifdef _WIN32 +#include +#else #include #include +#endif + #include #include #include @@ -20,14 +25,25 @@ class Terminal { void unset_raw_mode(); bool is_raw_mode() const { return raw_mode_; } std::optional read(); +#ifdef _WIN32 + bool is_terminal(void* handle); +#else bool is_terminal(int fd); +#endif private: void io_loop(); +#ifdef _WIN32 + HANDLE input_handle_; + HANDLE output_handle_; + DWORD original_input_mode_; + DWORD original_output_mode_; +#else int fd_; - bool raw_mode_; struct termios original_termios_; +#endif + bool raw_mode_; std::thread io_thread_; std::queue char_queue_; std::mutex queue_mutex_; diff --git a/src/buffer.cpp b/src/buffer.cpp index b8d9ddd..58ac5c7 100644 --- a/src/buffer.cpp +++ b/src/buffer.cpp @@ -1,22 +1,35 @@ #include "readline/buffer.h" #include "readline/types.h" #include -#include -#include #include #include +#ifdef _WIN32 +#include +#else +#include +#include +#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(prompt_.get_prompt().length()); } diff --git a/src/history.cpp b/src/history.cpp index b7f13ef..6aafdc8 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -3,7 +3,6 @@ #include #include #include -#include namespace readline { @@ -13,6 +12,11 @@ History::History() { void History::init() { const char* home = std::getenv("HOME"); +#ifdef _WIN32 + if (!home) { + home = std::getenv("USERPROFILE"); + } +#endif if (!home) { throw std::runtime_error("HOME environment variable not set"); } diff --git a/src/readline.cpp b/src/readline.cpp index 108f4c5..50ce5ee 100644 --- a/src/readline.cpp +++ b/src/readline.cpp @@ -1,7 +1,10 @@ #include "readline/readline.h" #include "readline/types.h" #include + +#ifndef _WIN32 #include +#endif namespace readline { @@ -226,7 +229,9 @@ std::string Readline::readline() { buf.delete_word(); break; case CHAR_CTRL_Z: +#ifndef _WIN32 kill(0, SIGSTOP); +#endif return ""; case CHAR_ENTER: case CHAR_CTRL_J: { diff --git a/src/terminal.cpp b/src/terminal.cpp index 850586f..11caa22 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -2,19 +2,172 @@ #include "readline/errors.h" #include #include -#include #include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + namespace readline { +#ifdef _WIN32 + +Terminal::Terminal() + : input_handle_(GetStdHandle(STD_INPUT_HANDLE)), + output_handle_(GetStdHandle(STD_OUTPUT_HANDLE)), + original_input_mode_(0), + original_output_mode_(0), + raw_mode_(false), + stop_io_loop_(false) { + + if (!is_terminal(input_handle_)) { + throw std::runtime_error("stdin is not a terminal"); + } +} + +Terminal::~Terminal() { + if (raw_mode_) { + unset_raw_mode(); + } + + stop_io_loop_ = true; + queue_cv_.notify_all(); + + if (io_thread_.joinable()) { + io_thread_.detach(); + } +} + +void Terminal::set_raw_mode() { + if (raw_mode_) { + return; + } + + // Save original console modes + 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 new_input_mode = original_input_mode_; + new_input_mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); + new_input_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + + if (!SetConsoleMode(input_handle_, new_input_mode)) { + throw std::runtime_error("Failed to set console input mode"); + } + + // Enable virtual terminal processing for output (ANSI escape sequences) + DWORD new_output_mode = original_output_mode_; + new_output_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + if (!SetConsoleMode(output_handle_, new_output_mode)) { + // Restore input mode if output mode fails + SetConsoleMode(input_handle_, original_input_mode_); + throw std::runtime_error("Failed to set console output mode"); + } + + raw_mode_ = true; + + // Disable stdout buffering for immediate character display + std::setvbuf(stdout, nullptr, _IONBF, 0); + + // Start I/O thread + if (!io_thread_.joinable()) { + io_thread_ = std::thread(&Terminal::io_loop, this); + } +} + +void Terminal::unset_raw_mode() { + if (!raw_mode_) { + return; + } + + SetConsoleMode(input_handle_, original_input_mode_); + SetConsoleMode(output_handle_, original_output_mode_); + + raw_mode_ = false; +} + +bool Terminal::is_terminal(void* handle) { + DWORD mode; + return GetConsoleMode(static_cast(handle), &mode) != 0; +} + +void Terminal::io_loop() { + auto push_escape_sequence = [this](const char* seq) { + std::lock_guard lock(queue_mutex_); + for (const char* p = seq; *p; ++p) { + char_queue_.push(*p); + } + queue_cv_.notify_one(); + }; + + while (!stop_io_loop_) { + INPUT_RECORD ir; + DWORD events_read; + + if (!ReadConsoleInput(input_handle_, &ir, 1, &events_read)) { + break; + } + + if (events_read == 0) { + continue; + } + + if (ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown) { + char c = ir.Event.KeyEvent.uChar.AsciiChar; + + // Handle special keys + WORD vk = ir.Event.KeyEvent.wVirtualKeyCode; + + if (vk == VK_UP) { + push_escape_sequence("\x1b[A"); + continue; + } else if (vk == VK_DOWN) { + push_escape_sequence("\x1b[B"); + continue; + } else if (vk == VK_RIGHT) { + push_escape_sequence("\x1b[C"); + continue; + } else if (vk == VK_LEFT) { + push_escape_sequence("\x1b[D"); + continue; + } else if (vk == VK_DELETE) { + push_escape_sequence("\x1b[3~"); + continue; + } else if (vk == VK_HOME) { + push_escape_sequence("\x1b[H"); + continue; + } else if (vk == VK_END) { + push_escape_sequence("\x1b[F"); + continue; + } + + if (c != 0) { + std::lock_guard lock(queue_mutex_); + char_queue_.push(c); + queue_cv_.notify_one(); + } + } + } +} + +#else // Unix implementation + Terminal::Terminal() : fd_(STDIN_FILENO), raw_mode_(false), stop_io_loop_(false) { if (!is_terminal(fd_)) { throw std::runtime_error("stdin is not a terminal"); } - - // Don't start I/O thread yet - will be started when needed } Terminal::~Terminal() { @@ -25,8 +178,6 @@ Terminal::~Terminal() { 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(); } @@ -37,14 +188,12 @@ void Terminal::set_raw_mode() { return; } - // 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); @@ -58,10 +207,8 @@ void Terminal::set_raw_mode() { raw_mode_ = true; - // Disable stdout buffering for immediate character display std::setvbuf(stdout, nullptr, _IONBF, 0); - // Start I/O thread now that raw mode is set if (!io_thread_.joinable()) { io_thread_ = std::thread(&Terminal::io_loop, this); } @@ -107,6 +254,8 @@ void Terminal::io_loop() { } } +#endif // _WIN32 + std::optional Terminal::read() { std::unique_lock lock(queue_mutex_);