diff --git a/include/readline/terminal.h b/include/readline/terminal.h index c0464a3..7fc6126 100644 --- a/include/readline/terminal.h +++ b/include/readline/terminal.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include #include #include @@ -9,6 +7,13 @@ #include #include +#ifdef _WIN32 + #include +#else + #include + #include +#endif + namespace readline { class Terminal { @@ -25,9 +30,16 @@ class Terminal { 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..1164109 100644 --- a/src/buffer.cpp +++ b/src/buffer.cpp @@ -1,22 +1,38 @@ #include "readline/buffer.h" #include "readline/types.h" #include -#include -#include #include #include +#ifdef _WIN32 + #define NOMINMAX + #include + #include + #define STDOUT_FILENO _fileno(stdout) +#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()); } @@ -95,7 +111,7 @@ void Buffer::move_left() { std::cout << cursor_left_n(1); } - int line = display_pos_ / line_width_ - 1; + int line = static_cast(display_pos_ / line_width_) - 1; bool has_space = get_line_spacing(line); if (has_space) { display_pos_ -= 1; @@ -120,14 +136,14 @@ void Buffer::move_right() { if (display_pos_ % line_width_ == 0) { std::cout << cursor_down_n(1) << CURSOR_BOL - << cursor_right_n(prompt_.get_prompt().length()); + << cursor_right_n(static_cast(prompt_.get_prompt().length())); } else if ((display_pos_ - r_length) % line_width_ == line_width_ - 1 && has_space) { std::cout << cursor_down_n(1) << CURSOR_BOL - << cursor_right_n(prompt_.get_prompt().length() + r_length); + << cursor_right_n(static_cast(prompt_.get_prompt().length()) + r_length); display_pos_ += 1; } else if (!line_has_space_.empty() && display_pos_ % line_width_ == line_width_ - 1 && has_space) { std::cout << cursor_down_n(1) << CURSOR_BOL - << cursor_right_n(prompt_.get_prompt().length()); + << cursor_right_n(static_cast(prompt_.get_prompt().length())); display_pos_ += 1; } else { std::cout << cursor_right_n(r_length); @@ -165,11 +181,11 @@ void Buffer::move_right_word() { void Buffer::move_to_start() { if (pos_ > 0) { - int curr_line = display_pos_ / line_width_; + int curr_line = static_cast(display_pos_ / line_width_); if (curr_line > 0) { std::cout << cursor_up_n(curr_line); } - std::cout << CURSOR_BOL << cursor_right_n(prompt_.get_prompt().length()); + std::cout << CURSOR_BOL << cursor_right_n(static_cast(prompt_.get_prompt().length())); pos_ = 0; display_pos_ = 0; } @@ -177,15 +193,15 @@ void Buffer::move_to_start() { void Buffer::move_to_end() { if (pos_ < buffer_.size()) { - int curr_line = display_pos_ / line_width_; - int total_lines = display_size() / line_width_; + int curr_line = static_cast(display_pos_ / line_width_); + int total_lines = static_cast(display_size() / line_width_); if (curr_line < total_lines) { std::cout << cursor_down_n(total_lines - curr_line); - int remainder = display_size() % line_width_; + int remainder = static_cast(display_size() % line_width_); std::cout << CURSOR_BOL - << cursor_right_n(prompt_.get_prompt().length() + remainder); + << cursor_right_n(static_cast(prompt_.get_prompt().length()) + remainder); } else { - std::cout << cursor_right_n(display_size() - display_pos_); + std::cout << cursor_right_n(static_cast(display_size() - display_pos_)); } pos_ = buffer_.size(); @@ -260,7 +276,7 @@ int Buffer::count_remaining_line_width(int place) { if (pos_ + counter < buffer_.size()) { char32_t r = buffer_[pos_ + counter]; place += char_width(r); - prev_len = to_utf8(r).length(); + prev_len = static_cast(to_utf8(r).length()); } else { break; } @@ -282,7 +298,7 @@ void Buffer::draw_remaining() { remaining_text.length())); if (!curr_line.empty()) { - std::cout << CLEAR_TO_EOL << curr_line << cursor_left_n(curr_line.length()); + std::cout << CLEAR_TO_EOL << curr_line << cursor_left_n(static_cast(curr_line.length())); } else { std::cout << CLEAR_TO_EOL; } @@ -369,7 +385,7 @@ void Buffer::delete_word() { void Buffer::replace(const std::u32string& text) { display_pos_ = 0; pos_ = 0; - int line_nums = display_size() / line_width_; + int line_nums = static_cast(display_size() / line_width_); buffer_.clear(); @@ -390,20 +406,20 @@ 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(ph.length()) << COLOR_DEFAULT; + std::cout << COLOR_GREY << ph << cursor_left_n(static_cast(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(prompt_.get_prompt().length()); + std::cout << CURSOR_RESET << cursor_right_n(static_cast(prompt_.get_prompt().length())); if (curr_pos > 0) { - int target_line = curr_pos / line_width_; + int target_line = static_cast(curr_pos / line_width_); if (target_line > 0) { std::cout << cursor_down_n(target_line); } - int remainder = curr_pos % line_width_; + int remainder = static_cast(curr_pos % line_width_); if (remainder > 0) { std::cout << cursor_right_n(remainder); } diff --git a/src/history.cpp b/src/history.cpp index b7f13ef..c3beff9 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -3,7 +3,6 @@ #include #include #include -#include namespace readline { @@ -12,9 +11,15 @@ History::History() { } void History::init() { - const char* home = std::getenv("HOME"); +#ifdef _WIN32 + const char* env_var = "USERPROFILE"; +#else + const char* env_var = "HOME"; +#endif + + const char* home = std::getenv(env_var); if (!home) { - throw std::runtime_error("HOME environment variable not set"); + return; } std::filesystem::path history_dir = std::filesystem::path(home) / ".readline"; diff --git a/src/readline.cpp b/src/readline.cpp index 108f4c5..62abd6e 100644 --- a/src/readline.cpp +++ b/src/readline.cpp @@ -89,7 +89,7 @@ std::string Readline::readline() { 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(ph.length()) + std::cout << COLOR_GREY << ph << cursor_left_n(static_cast(ph.length())) << COLOR_DEFAULT << std::flush; } @@ -226,7 +226,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..cf77638 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -2,17 +2,41 @@ #include "readline/errors.h" #include #include -#include -#include + +#ifdef _WIN32 + #include + #include + #define STDIN_FILENO _fileno(stdin) +#else + #include + #include + #include + #include +#endif namespace readline { Terminal::Terminal() - : fd_(STDIN_FILENO), raw_mode_(false), stop_io_loop_(false) { + : 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 } @@ -37,6 +61,34 @@ void Terminal::set_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"); @@ -56,10 +108,11 @@ void Terminal::set_raw_mode() { throw std::runtime_error("Failed to set terminal to raw mode"); } - raw_mode_ = true; - // 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()) { @@ -72,18 +125,67 @@ void Terminal::unset_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 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); @@ -105,6 +207,7 @@ void Terminal::io_loop() { } queue_cv_.notify_one(); } +#endif } std::optional Terminal::read() {