Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
---
BasedOnStyle: WebKit
BreakBeforeBraces: Attach
SpaceAfterTemplateKeyword: false
...
Cpp11BracedListStyle: true
SpaceBeforeCpp11BracedList: false
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# AGENTS.md

## AI usage policy

AI-generated code is not merged without thorough human review and
supervision. No AI agent has unsupervised write access. Every AI-authored
change is audited for correctness, style, and safety before commit.

Agents must refuse to create commits. Only a human author may commit
changes to this repository.

## Build and test

mkdir build && cd build && cmake .. && make -j$(nproc)
./commandline_tests
bash scripts/clang_format_check.sh

## Testing constraints

`InteractiveBackend` cannot be unit tested directly. Its constructor saves
terminal attributes in a file-static global (shared across instances) and
spawns threads that block on stdin. Testing it requires either a pty
harness or extracting logic into a separate testable unit.

Tests that construct `Commandline` and depend on synchronous `on_write`
delivery must set `COMMANDLINE_FORCE_BUFFERED=1` in the environment before
construction. Without it, a tty stdin selects `InteractiveBackend`, whose
`on_write` fires asynchronously.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ if (COMMON_WARNINGS)
target_compile_options(commandline PRIVATE ${COMMON_WARNINGS})
endif ()


target_compile_definitions(commandline PRIVATE DOCTEST_CONFIG_DISABLE=1)
target_link_libraries(commandline PRIVATE Threads::Threads)

Expand Down
6 changes: 3 additions & 3 deletions src/backends/Backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ class Backend {
virtual void disable_key_debug() = 0;

// gets called when a command is ready
std::function<void(Backend&)> on_command { nullptr };
std::function<void(Backend&)> on_command{nullptr};

// gets called when tab is pressed and new suggestions are requested
std::function<std::vector<std::string>(Backend&, std::string, int)> on_autocomplete { nullptr };
std::function<std::vector<std::string>(Backend&, std::string, int)> on_autocomplete{nullptr};

// gets called on write(), for writing to a file or similar secondary logging system
std::function<void(const std::string&)> on_write { nullptr };
std::function<void(const std::string&)> on_write{nullptr};
};

}
2 changes: 1 addition & 1 deletion src/backends/BufferedBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ size_t lk::BufferedBackend::history_size() const {
void lk::BufferedBackend::clear_history() {
}
const std::vector<std::string>& lk::BufferedBackend::history() const {
static const std::vector<std::string> empty_vec {};
static const std::vector<std::string> empty_vec{};
return empty_vec;
}
void lk::BufferedBackend::set_history(const std::vector<std::string>&) {
Expand Down
10 changes: 5 additions & 5 deletions src/backends/BufferedBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ class BufferedBackend : public Backend {
private:
void thread_main();

mutable std::mutex m_cmd_mtx {};
mutable std::mutex m_out_mtx {};
mutable std::mutex m_prompt_mtx {};
mutable std::mutex m_shutdown_mtx {};
mutable std::mutex m_cmd_mtx{};
mutable std::mutex m_out_mtx{};
mutable std::mutex m_prompt_mtx{};
mutable std::mutex m_shutdown_mtx{};
bool m_shutdown = false;
std::deque<std::string> m_input_queue {};
std::deque<std::string> m_input_queue{};
std::string m_prompt;
std::thread m_thread;
};
Expand Down
82 changes: 59 additions & 23 deletions src/backends/InteractiveBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,39 @@ void lk::InteractiveBackend::handle_delete() {
}
}

lk::CsiAction lk::classify_csi_sequence(int c3, const std::function<int()>& getchar_fn) {
switch (c3) {
case 'A':
return CsiAction::GoBack;
case 'B':
return CsiAction::GoForward;
case 'D':
return CsiAction::GoLeft;
case 'C':
return CsiAction::GoRight;
case 0x48: // ESC[H — Home
return CsiAction::GoHome;
case 0x31: // ESC[1~ — Home (xterm variant)
if (getchar_fn() == '~')
return CsiAction::GoHome;
return CsiAction::None;
case 0x46: // ESC[F — End
return CsiAction::GoEnd;
case 0x34: // ESC[4~ — End (xterm variant)
if (getchar_fn() == '~')
return CsiAction::GoEnd;
return CsiAction::None;
case 0x33: // ESC[3~ — Delete
if (getchar_fn() == '~')
return CsiAction::Delete;
return CsiAction::None;
case 0x5a: // ESC[Z — Shift+Tab
return CsiAction::ShiftTab;
default:
return CsiAction::None;
}
}

void lk::InteractiveBackend::handle_escape_sequence(std::unique_lock<std::mutex>& guard) {
int c2 = impl::getchar_no_echo();
if (m_key_debug) {
Expand All @@ -166,34 +199,37 @@ void lk::InteractiveBackend::handle_escape_sequence(std::unique_lock<std::mutex>
if (m_key_debug) {
fprintf(stderr, "c3: 0x%.2x\n", c3);
}
if (c2 == '[' && history_enabled()) {
if (c3 == 'A') {
// up / back
go_back();
} else if (c3 == 'B') {
// down / forward
go_forward();
} else if (c3 == 'D') {
// left
if (c2 == '[') {
auto action = classify_csi_sequence(c3, impl::getchar_no_echo);
switch (action) {
case CsiAction::GoBack:
if (history_enabled())
go_back();
break;
case CsiAction::GoForward:
if (history_enabled())
go_forward();
break;
case CsiAction::GoLeft:
go_left();
} else if (c3 == 'C') {
// right
break;
case CsiAction::GoRight:
go_right();
} else if (c3 == 0x48 || c3 == 0x31) {
// HOME
break;
case CsiAction::GoHome:
go_to_begin();
} else if (c3 == 0x46) {
// END
break;
case CsiAction::GoEnd:
go_to_end();
} else if (c3 == 0x33) {
// DEL
int c4 = impl::getchar_no_echo();
if (c4 == '~') {
handle_delete();
}
} else if (c3 == 0x5a) {
// SHIFT+TAB
break;
case CsiAction::Delete:
handle_delete();
break;
case CsiAction::ShiftTab:
handle_tab(guard, false);
break;
case CsiAction::None:
break;
}
#elif defined(WINDOWS)
if (c2 == 'H') {
Expand Down
25 changes: 21 additions & 4 deletions src/backends/InteractiveBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ class InteractiveBackend : public Backend {
std::string m_prompt;

std::thread m_io_thread;
std::atomic<bool> m_shutdown { false };
bool m_key_debug { false };
std::atomic<bool> m_shutdown{false};
bool m_key_debug{false};

mutable std::mutex m_to_write_mutex;
std::queue<std::string> m_to_write;
std::condition_variable m_to_write_cond;
mutable std::mutex m_to_read_mutex;
std::queue<std::string> m_to_read;
bool m_history_enabled { false };
bool m_history_enabled{false};
mutable std::mutex m_history_mutex;
std::vector<std::string> m_history;
std::string m_history_temp_buffer;
size_t m_history_index { 0 };
size_t m_history_index{0};
size_t m_history_limit = (std::numeric_limits<size_t>::max)() - 1;
std::mutex m_current_buffer_mutex;
std::string m_current_buffer;
Expand All @@ -92,4 +92,21 @@ class InteractiveBackend : public Backend {
std::string m_buffer_before_autocomplete;
};

enum class CsiAction {
None,
GoBack,
GoForward,
GoLeft,
GoRight,
GoHome,
GoEnd,
Delete,
ShiftTab,
};

// Classify a CSI (ESC[...) escape sequence from its parameter byte c3.
// Calls getchar_fn to read additional bytes for multi-byte sequences
// such as ESC[1~ (Home), ESC[3~ (Delete), ESC[4~ (End).
CsiAction classify_csi_sequence(int c3, const std::function<int()>& getchar_fn);

}
18 changes: 11 additions & 7 deletions src/commandline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
#include <memory>
#include <utility>

Commandline::Commandline(const std::string& prompt) {
if (impl::is_interactive()) {
m_backend = std::unique_ptr<lk::Backend>(new lk::InteractiveBackend(prompt));
} else {
m_backend = std::unique_ptr<lk::Backend>(new lk::BufferedBackend(prompt));
}
void Commandline::wire_callbacks() {
m_backend->on_command = [this](lk::Backend&) {
if (on_command) {
on_command(*this);
Expand All @@ -32,7 +27,16 @@ Commandline::Commandline(const std::string& prompt) {
if (on_autocomplete) {
return on_autocomplete(*this, std::move(str), n);
} else {
return std::vector<std::string> {};
return std::vector<std::string>{};
}
};
}

Commandline::Commandline(const std::string& prompt) {
if (impl::is_interactive()) {
m_backend = std::unique_ptr<lk::Backend>(new lk::InteractiveBackend(prompt));
} else {
m_backend = std::unique_ptr<lk::Backend>(new lk::BufferedBackend(prompt));
}
wire_callbacks();
}
10 changes: 6 additions & 4 deletions src/commandline.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Commandline final {
public:
explicit Commandline(const std::string& prompt = "");
explicit Commandline(std::unique_ptr<lk::Backend> backend)
: m_backend(std::move(backend)) { }
: m_backend(std::move(backend)) { wire_callbacks(); }

bool has_command() const { return m_backend->has_command(); }
void write(const std::string& str) { m_backend->write(str); }
Expand Down Expand Up @@ -36,15 +36,17 @@ class Commandline final {
void disable_ansi_escape_removal_on_write() { m_ansi_escape_removal = false; }

// gets called when a command is ready
std::function<void(Commandline&)> on_command { nullptr };
std::function<void(Commandline&)> on_command{nullptr};

// gets called when tab is pressed and new suggestions are requested
std::function<std::vector<std::string>(Commandline&, std::string, int)> on_autocomplete { nullptr };
std::function<std::vector<std::string>(Commandline&, std::string, int)> on_autocomplete{nullptr};

// gets called on write(), for writing to a file or similar secondary logging system
std::function<void(const std::string&)> on_write { nullptr };
std::function<void(const std::string&)> on_write{nullptr};

private:
void wire_callbacks();

std::unique_ptr<lk::Backend> m_backend;
bool m_ansi_escape_removal = false;
};
2 changes: 1 addition & 1 deletion src/helper/ansi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ enum class State {
};

std::string ansi::remove_ansi_escape_codes(const std::string& original) {
std::string new_str {};
std::string new_str{};
new_str.reserve(original.size());

State state = State::Normal;
Expand Down
4 changes: 2 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ int main(int argc, char** argv) {

com.on_autocomplete = [](Commandline& com, std::string stub, int pos) -> std::vector<std::string> {
if (stub == "hello") {
return { "hello world" };
return {"hello world"};
} else if (stub.empty()) {
return { "a", "b", "c", "d" };
return {"a", "b", "c", "d"};
} else {
return {};
}
Expand Down
Loading
Loading