Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
build/
cmake-build-*/

# CodeQL artifacts
_codeql_build_dir/
_codeql_detected_source_root

# Compiled binaries
*.o
*.a
Expand Down
18 changes: 17 additions & 1 deletion include/readline/terminal.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#pragma once

#ifdef _WIN32
#include <windows.h>
#else
#include <termios.h>
#include <unistd.h>
#endif

#include <optional>
#include <thread>
#include <queue>
Expand All @@ -20,14 +25,25 @@ class Terminal {
void unset_raw_mode();
bool is_raw_mode() const { return raw_mode_; }
std::optional<char> 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> char_queue_;
std::mutex queue_mutex_;
Expand Down
17 changes: 15 additions & 2 deletions src/buffer.cpp
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
#include "readline/buffer.h"
#include "readline/types.h"
#include <iostream>
#include <sys/ioctl.h>
#include <unistd.h>
#include <algorithm>
#include <cstring>

#ifdef _WIN32
#include <windows.h>
#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());
}
Expand Down
6 changes: 5 additions & 1 deletion src/history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#include <sstream>
#include <stdexcept>
#include <cstdlib>
#include <sys/stat.h>

namespace readline {

Expand All @@ -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");
}
Expand Down
5 changes: 5 additions & 0 deletions src/readline.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include "readline/readline.h"
#include "readline/types.h"
#include <iostream>

#ifndef _WIN32
#include <signal.h>
#endif

namespace readline {

Expand Down Expand Up @@ -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: {
Expand Down
167 changes: 158 additions & 9 deletions src/terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,172 @@
#include "readline/errors.h"
#include <stdexcept>
#include <iostream>
#include <signal.h>
#include <cstdio>

#ifdef _WIN32
#include <conio.h>
#else
#include <signal.h>
#include <cerrno>
#include <unistd.h>
#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>(handle), &mode) != 0;
}

void Terminal::io_loop() {
auto push_escape_sequence = [this](const char* seq) {
std::lock_guard<std::mutex> 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<std::mutex> 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() {
Expand All @@ -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();
}
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -107,6 +254,8 @@ void Terminal::io_loop() {
}
}

#endif // _WIN32

std::optional<char> Terminal::read() {
std::unique_lock<std::mutex> lock(queue_mutex_);

Expand Down
Loading