From 32b936ddee3e60c6aa1650658446ba9364103b08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:16:36 +0000 Subject: [PATCH 1/5] Initial plan From 649d782c33d0824354cd9a61b2625a11e9fa93f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:20:47 +0000 Subject: [PATCH 2/5] Add Windows platform support for terminal handling Co-authored-by: ericcurtin <1694275+ericcurtin@users.noreply.github.com> --- include/readline/terminal.h | 18 +++- src/buffer.cpp | 17 +++- src/history.cpp | 6 +- src/readline.cpp | 5 + src/terminal.cpp | 188 ++++++++++++++++++++++++++++++++++-- 5 files changed, 221 insertions(+), 13 deletions(-) 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..6828d8a 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -2,19 +2,193 @@ #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() { + 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) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('A'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_DOWN) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('B'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_RIGHT) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('C'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_LEFT) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('D'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_DELETE) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('3'); + char_queue_.push('~'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_HOME) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('H'); + queue_cv_.notify_one(); + continue; + } else if (vk == VK_END) { + std::lock_guard lock(queue_mutex_); + char_queue_.push('\x1b'); + char_queue_.push('['); + char_queue_.push('F'); + queue_cv_.notify_one(); + 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 +199,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 +209,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 +228,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 +275,8 @@ void Terminal::io_loop() { } } +#endif // _WIN32 + std::optional Terminal::read() { std::unique_lock lock(queue_mutex_); From 755c71b493dcfd4ff865cb03d5b18c09f689bcb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:22:04 +0000 Subject: [PATCH 3/5] Refactor Windows key handling to use helper function Co-authored-by: ericcurtin <1694275+ericcurtin@users.noreply.github.com> --- src/terminal.cpp | 51 ++++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/src/terminal.cpp b/src/terminal.cpp index 6828d8a..11caa22 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -102,6 +102,14 @@ bool Terminal::is_terminal(void* handle) { } 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; @@ -121,54 +129,25 @@ void Terminal::io_loop() { WORD vk = ir.Event.KeyEvent.wVirtualKeyCode; if (vk == VK_UP) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('A'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[A"); continue; } else if (vk == VK_DOWN) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('B'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[B"); continue; } else if (vk == VK_RIGHT) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('C'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[C"); continue; } else if (vk == VK_LEFT) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('D'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[D"); continue; } else if (vk == VK_DELETE) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('3'); - char_queue_.push('~'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[3~"); continue; } else if (vk == VK_HOME) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('H'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[H"); continue; } else if (vk == VK_END) { - std::lock_guard lock(queue_mutex_); - char_queue_.push('\x1b'); - char_queue_.push('['); - char_queue_.push('F'); - queue_cv_.notify_one(); + push_escape_sequence("\x1b[F"); continue; } From 18fc57a985b716797ec9a518d4b8bf7a7d10fb51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:23:14 +0000 Subject: [PATCH 4/5] Complete Windows platform support Co-authored-by: ericcurtin <1694275+ericcurtin@users.noreply.github.com> --- .../readline_cpp-config-version.cmake | 43 +++++++++++++++++++ _codeql_detected_source_root | 1 + 2 files changed, 44 insertions(+) create mode 100644 _codeql_build_dir/readline_cpp-config-version.cmake create mode 120000 _codeql_detected_source_root diff --git a/_codeql_build_dir/readline_cpp-config-version.cmake b/_codeql_build_dir/readline_cpp-config-version.cmake new file mode 100644 index 0000000..a445dd1 --- /dev/null +++ b/_codeql_build_dir/readline_cpp-config-version.cmake @@ -0,0 +1,43 @@ +# This is a basic version file for the Config-mode of find_package(). +# It is used by write_basic_package_version_file() as input file for configure_file() +# to create a version-file which can be installed along a config.cmake file. +# +# The created file sets PACKAGE_VERSION_EXACT if the current version string and +# the requested version string are exactly the same and it sets +# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version. +# The variable CVF_VERSION must be set before calling configure_file(). + +set(PACKAGE_VERSION "1.0.0") + +if (PACKAGE_FIND_VERSION_RANGE) + # Package version must be in the requested version range + if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN) + OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX) + OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX))) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + endif() +else() + if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() + endif() +endif() + + +# if the installed or the using project don't have CMAKE_SIZEOF_VOID_P set, ignore it: +if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "8" STREQUAL "") + return() +endif() + +# check that the installed version has the same 32/64bit-ness as the one which is currently searching: +if(NOT CMAKE_SIZEOF_VOID_P STREQUAL "8") + math(EXPR installedBits "8 * 8") + set(PACKAGE_VERSION "${PACKAGE_VERSION} (${installedBits}bit)") + set(PACKAGE_VERSION_UNSUITABLE TRUE) +endif() diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file From e23cd4e237dcc14e3380889aa3d8ba86edaf1f71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:23:45 +0000 Subject: [PATCH 5/5] Add CodeQL artifacts to .gitignore and remove from tracking Co-authored-by: ericcurtin <1694275+ericcurtin@users.noreply.github.com> --- .gitignore | 4 ++ .../readline_cpp-config-version.cmake | 43 ------------------- _codeql_detected_source_root | 1 - 3 files changed, 4 insertions(+), 44 deletions(-) delete mode 100644 _codeql_build_dir/readline_cpp-config-version.cmake delete mode 120000 _codeql_detected_source_root 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/_codeql_build_dir/readline_cpp-config-version.cmake b/_codeql_build_dir/readline_cpp-config-version.cmake deleted file mode 100644 index a445dd1..0000000 --- a/_codeql_build_dir/readline_cpp-config-version.cmake +++ /dev/null @@ -1,43 +0,0 @@ -# This is a basic version file for the Config-mode of find_package(). -# It is used by write_basic_package_version_file() as input file for configure_file() -# to create a version-file which can be installed along a config.cmake file. -# -# The created file sets PACKAGE_VERSION_EXACT if the current version string and -# the requested version string are exactly the same and it sets -# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version. -# The variable CVF_VERSION must be set before calling configure_file(). - -set(PACKAGE_VERSION "1.0.0") - -if (PACKAGE_FIND_VERSION_RANGE) - # Package version must be in the requested version range - if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN) - OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX) - OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX))) - set(PACKAGE_VERSION_COMPATIBLE FALSE) - else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - endif() -else() - if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) - set(PACKAGE_VERSION_COMPATIBLE FALSE) - else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) - set(PACKAGE_VERSION_EXACT TRUE) - endif() - endif() -endif() - - -# if the installed or the using project don't have CMAKE_SIZEOF_VOID_P set, ignore it: -if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "8" STREQUAL "") - return() -endif() - -# check that the installed version has the same 32/64bit-ness as the one which is currently searching: -if(NOT CMAKE_SIZEOF_VOID_P STREQUAL "8") - math(EXPR installedBits "8 * 8") - set(PACKAGE_VERSION "${PACKAGE_VERSION} (${installedBits}bit)") - set(PACKAGE_VERSION_UNSUITABLE TRUE) -endif() diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root deleted file mode 120000 index 945c9b4..0000000 --- a/_codeql_detected_source_root +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file