Public Access
IRC App: Alt scrolls, Up/Down recall sent lines; /j for /join
- Alt + ; / Alt + . scroll the Buffer back and forward - Up / Down (Fn + ; / Fn + .) browse the last 30 sent lines, shell-style, returning to the draft past the newest (InputHistory, host-tested) - /j is short for /join Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -326,7 +326,7 @@ void IrcSession::command(int b, const std::string& text, int64_t utc) {
|
|||||||
const IrcBuffer& buf = buffers_[b];
|
const IrcBuffer& buf = buffers_[b];
|
||||||
bool inChannel = buf.type == IrcBuffer::Type::Channel;
|
bool inChannel = buf.type == IrcBuffer::Type::Channel;
|
||||||
|
|
||||||
if (verb == "join") {
|
if (verb == "join" || verb == "j") {
|
||||||
auto [channel, key] = firstWord(rest);
|
auto [channel, key] = firstWord(rest);
|
||||||
if (channel.empty()) return info(b, "Usage: /join #channel", utc);
|
if (channel.empty()) return info(b, "Usage: /join #channel", utc);
|
||||||
if (!isChannel(channel)) channel = "#" + channel;
|
if (!isChannel(channel)) channel = "#" + channel;
|
||||||
@@ -362,7 +362,7 @@ void IrcSession::command(int b, const std::string& text, int64_t utc) {
|
|||||||
} else if (verb == "raw" || verb == "quote") {
|
} else if (verb == "raw" || verb == "quote") {
|
||||||
if (!rest.empty()) send(rest);
|
if (!rest.empty()) send(rest);
|
||||||
} else {
|
} else {
|
||||||
info(b, "Unknown command /" + verb + " (try /join /part /msg /me /nick /topic /names /quit /raw)", utc);
|
info(b, "Unknown command /" + verb + " (try /join or /j, /part /msg /me /nick /topic /names /quit /raw)", utc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
// Lines already entered, browsed shell-style: up() goes back in time, down() forward, and past the
|
||||||
|
// newest line the draft being typed comes back.
|
||||||
|
class InputHistory {
|
||||||
|
public:
|
||||||
|
explicit InputHistory(size_t capacity) : capacity_(capacity) {}
|
||||||
|
|
||||||
|
void add(const std::string& line) {
|
||||||
|
browsing_ = -1;
|
||||||
|
if (line.empty() || (!lines_.empty() && lines_.back() == line)) return;
|
||||||
|
lines_.push_back(line);
|
||||||
|
if (lines_.size() > capacity_) lines_.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// `current` is what the editor holds now (kept as the draft when browsing starts).
|
||||||
|
bool up(const std::string& current, std::string& out) {
|
||||||
|
if (lines_.empty() || browsing_ == 0) return false;
|
||||||
|
if (browsing_ < 0) {
|
||||||
|
draft_ = current;
|
||||||
|
browsing_ = static_cast<int>(lines_.size());
|
||||||
|
}
|
||||||
|
out = lines_[--browsing_];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool down(std::string& out) {
|
||||||
|
if (browsing_ < 0) return false;
|
||||||
|
if (++browsing_ >= static_cast<int>(lines_.size())) {
|
||||||
|
browsing_ = -1;
|
||||||
|
out = draft_;
|
||||||
|
} else {
|
||||||
|
out = lines_[browsing_];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t capacity_;
|
||||||
|
std::deque<std::string> lines_;
|
||||||
|
int browsing_ = -1; // index being shown, -1 when editing the draft
|
||||||
|
std::string draft_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
+14
-2
@@ -80,13 +80,24 @@ bool IrcApp::onKey(const KeyEvent& e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IrcApp::onChatKey(const KeyEvent& e) {
|
bool IrcApp::onChatKey(const KeyEvent& e) {
|
||||||
|
// Alt + ; / Alt + . scroll the Buffer; Up / Down (Fn + ; / Fn + .) recall sent lines.
|
||||||
|
if (e.key == Key::Char && e.alt && (e.ch == ';' || e.ch == '.')) {
|
||||||
|
if (e.ch == ';') scroll_++; // older
|
||||||
|
else if (scroll_ > 0) scroll_--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::string recalled;
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case Key::Char: input_.insert(e.ch); break;
|
case Key::Char: input_.insert(e.ch); break;
|
||||||
case Key::Delete: input_.backspace(); break;
|
case Key::Delete: input_.backspace(); break;
|
||||||
case Key::Left: input_.left(); break;
|
case Key::Left: input_.left(); break;
|
||||||
case Key::Right: input_.right(); break;
|
case Key::Right: input_.right(); break;
|
||||||
case Key::Up: scroll_++; break; // older
|
case Key::Up:
|
||||||
case Key::Down: if (scroll_ > 0) scroll_--; break;
|
if (history_.up(input_.text(), recalled)) input_.setText(recalled);
|
||||||
|
break;
|
||||||
|
case Key::Down:
|
||||||
|
if (history_.down(recalled)) input_.setText(recalled);
|
||||||
|
break;
|
||||||
case Key::Tab: {
|
case Key::Tab: {
|
||||||
int count = irc_.withSession([](IrcSession& s) { return s.bufferCount(); });
|
int count = irc_.withSession([](IrcSession& s) { return s.bufferCount(); });
|
||||||
view((current_ + 1) % count);
|
view((current_ + 1) % count);
|
||||||
@@ -95,6 +106,7 @@ bool IrcApp::onChatKey(const KeyEvent& e) {
|
|||||||
case Key::Select: {
|
case Key::Select: {
|
||||||
std::string text = input_.text();
|
std::string text = input_.text();
|
||||||
if (text.empty()) break;
|
if (text.empty()) break;
|
||||||
|
history_.add(text);
|
||||||
input_.setText("");
|
input_.setText("");
|
||||||
scroll_ = 0;
|
scroll_ = 0;
|
||||||
if (lowered(text) == "/settings") {
|
if (lowered(text) == "/settings") {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "event_bus.h"
|
#include "event_bus.h"
|
||||||
|
#include "input_history.h"
|
||||||
#include "line_editor.h"
|
#include "line_editor.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "services/irc_service.h"
|
#include "services/irc_service.h"
|
||||||
@@ -48,6 +49,7 @@ class IrcApp : public App {
|
|||||||
uint32_t seenRevision_ = 0;
|
uint32_t seenRevision_ = 0;
|
||||||
IrcService::Status seenStatus_ = IrcService::Status::Stopped;
|
IrcService::Status seenStatus_ = IrcService::Status::Stopped;
|
||||||
LineEditor input_{400};
|
LineEditor input_{400};
|
||||||
|
InputHistory history_{30};
|
||||||
ListModel fields_{theme::kContent.h / theme::kLineHeight};
|
ListModel fields_{theme::kContent.h / theme::kLineHeight};
|
||||||
bool editing_ = false;
|
bool editing_ = false;
|
||||||
LineEditor fieldEditor_{63};
|
LineEditor fieldEditor_{63};
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include "input_history.h"
|
||||||
|
|
||||||
|
using namespace roro;
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
void test_up_brings_back_previous_lines_newest_first() {
|
||||||
|
InputHistory h(10);
|
||||||
|
h.add("one");
|
||||||
|
h.add("two");
|
||||||
|
std::string line;
|
||||||
|
TEST_ASSERT_TRUE(h.up("", line));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("two", line.c_str());
|
||||||
|
TEST_ASSERT_TRUE(h.up("two", line));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("one", line.c_str());
|
||||||
|
TEST_ASSERT_FALSE(h.up("one", line)); // nothing older
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_down_returns_to_the_draft_being_typed() {
|
||||||
|
InputHistory h(10);
|
||||||
|
h.add("one");
|
||||||
|
std::string line;
|
||||||
|
h.up("half typed", line);
|
||||||
|
TEST_ASSERT_TRUE(h.down(line));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("half typed", line.c_str());
|
||||||
|
TEST_ASSERT_FALSE(h.down(line)); // already at the draft
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_adding_resets_browsing_and_skips_repeats_and_empty_lines() {
|
||||||
|
InputHistory h(10);
|
||||||
|
h.add("same");
|
||||||
|
h.add("same");
|
||||||
|
h.add("");
|
||||||
|
std::string line;
|
||||||
|
h.up("", line);
|
||||||
|
TEST_ASSERT_FALSE(h.up(line, line)); // only one entry kept
|
||||||
|
h.add("next");
|
||||||
|
TEST_ASSERT_TRUE(h.up("", line));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("next", line.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_keeps_only_the_most_recent_lines() {
|
||||||
|
InputHistory h(2);
|
||||||
|
h.add("a");
|
||||||
|
h.add("b");
|
||||||
|
h.add("c");
|
||||||
|
std::string line;
|
||||||
|
h.up("", line);
|
||||||
|
h.up(line, line);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("b", line.c_str());
|
||||||
|
TEST_ASSERT_FALSE(h.up(line, line));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_empty_history_does_nothing() {
|
||||||
|
InputHistory h(5);
|
||||||
|
std::string line = "draft";
|
||||||
|
TEST_ASSERT_FALSE(h.up("draft", line));
|
||||||
|
TEST_ASSERT_FALSE(h.down(line));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("draft", line.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_up_brings_back_previous_lines_newest_first);
|
||||||
|
RUN_TEST(test_down_returns_to_the_draft_being_typed);
|
||||||
|
RUN_TEST(test_adding_resets_browsing_and_skips_repeats_and_empty_lines);
|
||||||
|
RUN_TEST(test_keeps_only_the_most_recent_lines);
|
||||||
|
RUN_TEST(test_empty_history_does_nothing);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -366,6 +366,16 @@ void test_input_message_and_commands() {
|
|||||||
TEST_ASSERT_EQUAL_STRING("/not a command", f.session->buffer(b).lines.back().text.c_str());
|
TEST_ASSERT_EQUAL_STRING("/not a command", f.session->buffer(b).lines.back().text.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_j_is_short_for_join() {
|
||||||
|
Fixture f;
|
||||||
|
f.registerNow();
|
||||||
|
f.session->input(0, "/j #short", 0);
|
||||||
|
f.session->input(0, "/j #locked k3y", 0);
|
||||||
|
f.take();
|
||||||
|
TEST_ASSERT_TRUE(f.sent("JOIN #short"));
|
||||||
|
TEST_ASSERT_TRUE(f.sent("JOIN #locked k3y"));
|
||||||
|
}
|
||||||
|
|
||||||
void test_msg_opens_a_query_buffer() {
|
void test_msg_opens_a_query_buffer() {
|
||||||
Fixture f;
|
Fixture f;
|
||||||
f.registerNow();
|
f.registerNow();
|
||||||
@@ -483,6 +493,7 @@ int main() {
|
|||||||
RUN_TEST(test_action_and_ctcp_version);
|
RUN_TEST(test_action_and_ctcp_version);
|
||||||
RUN_TEST(test_topic_is_kept);
|
RUN_TEST(test_topic_is_kept);
|
||||||
RUN_TEST(test_input_message_and_commands);
|
RUN_TEST(test_input_message_and_commands);
|
||||||
|
RUN_TEST(test_j_is_short_for_join);
|
||||||
RUN_TEST(test_msg_opens_a_query_buffer);
|
RUN_TEST(test_msg_opens_a_query_buffer);
|
||||||
RUN_TEST(test_text_in_the_server_buffer_explains_instead_of_sending);
|
RUN_TEST(test_text_in_the_server_buffer_explains_instead_of_sending);
|
||||||
RUN_TEST(test_quit_sends_and_is_remembered);
|
RUN_TEST(test_quit_sends_and_is_remembered);
|
||||||
|
|||||||
Reference in New Issue
Block a user