Merge 79069a05bb into 58062860af
This commit is contained in:
commit
c1f5bdc788
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 */
|
||||
|
|
@ -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__);
|
||||
}
|
||||
|
|
@ -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);
|
||||
1456
tools/run/run.cpp
1456
tools/run/run.cpp
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue