From eb0d9f9d3f3c8b0095c176fd723bf4bfd5cca088 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 24 Apr 2026 13:13:40 +0000 Subject: [PATCH 1/5] add logic for marking a location --- src/action.h | 3 +++ src/constants.h | 4 +++- src/controller.cpp | 5 +++++ src/model.cpp | 10 ++++++++++ src/model.h | 15 +++++++++++++++ src/view.cpp | 13 +++++++++---- 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/action.h b/src/action.h index e6a9455..43f850f 100644 --- a/src/action.h +++ b/src/action.h @@ -238,6 +238,9 @@ template } break; + // At some point, we'll need to figure out how to redraw the whole screen faster + // to prevent flickering, but the whole screen will need redrawing when a mark + // is changed case ActionType::MoveCursorUp: { if constexpr (std::is_same_v) { auto logger = spdlog::get("basic_logger"); diff --git a/src/constants.h b/src/constants.h index 5849dab..f953704 100644 --- a/src/constants.h +++ b/src/constants.h @@ -10,7 +10,9 @@ const int TAB_SIZE = 4; const std::size_t LINE_BORDER = 100; const rawterm::Color COLOR_UI_BG = rawterm::Colors::gray; -const rawterm::Color COLOR_DARK_YELLOW = rawterm::Color("#ffdd33"); +const rawterm::Color COLOR_DARK_YELLOW = rawterm::Color("#FFdd33"); +// Should match ansi color code 92 +const rawterm::Color COLOR_GREEN = rawterm::Color("#5ffa68"); const std::string COLOR_ALERT = "\x1b[41m\x1b[37m"; const std::string WHITESPACE = " \t\n\r\f\v"; diff --git a/src/controller.cpp b/src/controller.cpp index 4b758e9..bb7b175 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -284,6 +284,11 @@ void Controller::start_action_engine() { parse_action(&view, Action {ActionType::MoveCursorRight}); redraw_all = redraw.value(); + // Add mark + } else if (k.value() == rawterm::Key('m')) { + auto k2 = rawterm::wait_for_input(); + if (k2.isCharInput()) { view.get_active_model()->add_mark(k2.code); } + // add new line and go to insert mode (below) } else if (k.value() == rawterm::Key('o')) { if (is_readonly_model()) { continue; } diff --git a/src/model.cpp b/src/model.cpp index 6d22161..f9b1504 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -550,3 +550,13 @@ void Model::dedent_curr_line() { const std::size_t line_size = buf.at(current_line).size(); buf.at(current_line) = buf.at(current_line).substr(to_delete, line_size); } + +void Model::add_mark(const char c) { + marks.insert_or_assign(c, Mark(current_line, current_char)); +} + +[[nodiscard]] bool Model::is_marked(const std::size_t idx) const { + return std::find_if(marks.begin(), marks.end(), [&](const auto& map) { + return map.second.line_pos == idx; + }) != marks.end(); +} diff --git a/src/model.h b/src/model.h index 4d705ca..96ad382 100644 --- a/src/model.h +++ b/src/model.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,17 @@ struct WordPos { uint_t lineno; }; +struct Mark { + unsigned int line_pos; + unsigned int char_pos; + + explicit Mark(unsigned int l, unsigned int c) : line_pos(l), char_pos(c) {} + + [[nodiscard]] bool operator==(const Mark& other) const { + return line_pos == other.line_pos && char_pos == other.char_pos; + } +}; + struct Model { ModelType type = ModelType::BUF; std::vector buf; @@ -34,6 +46,7 @@ struct Model { unsigned int current_char = 0; // 0-indexed std::string search_str = ""; std::size_t vertical_offset = 0; + std::unordered_map marks = {}; // Num of lines offset to view unsigned int view_offset = 0; @@ -73,6 +86,8 @@ struct Model { std::optional find_next_str(std::string_view); void indent_curr_line(); void dedent_curr_line(); + void add_mark(const char); + [[nodiscard]] bool is_marked(const std::size_t) const; }; #endif // MODEL_H diff --git a/src/view.cpp b/src/view.cpp index 6764243..8ebd8de 100644 --- a/src/view.cpp +++ b/src/view.cpp @@ -92,6 +92,8 @@ void View::draw_screen() { if (LINE_NUMBERS) { rawterm::Color c = COLOR_UI_BG; if (idx == get_active_model()->current_line + 1) { c = COLOR_DARK_YELLOW; } + if (get_active_model()->is_marked(idx)) { c = COLOR_GREEN; } + screen += rawterm::set_foreground(std::format("{:>{}}\u2502", idx, line_number_offset), c); } @@ -227,10 +229,13 @@ void View::draw_line(const Draw_Line_dir::values redraw_prev) { std::string_view curr_line = get_active_model()->buf.at(idx); if (LINE_NUMBERS) { - const rawterm::Color color = - (idx == get_active_model()->current_line) ? COLOR_DARK_YELLOW : COLOR_UI_BG; - line += rawterm::set_foreground( - std::format("{:>{}}\u2502", idx + 1, line_number_offset), color); + // TODO: refactor lineno colour into it's own view method + rawterm::Color c = COLOR_UI_BG; + if (idx == get_active_model()->current_line) { c = COLOR_DARK_YELLOW; } + if (get_active_model()->is_marked(idx)) { c = COLOR_GREEN; } + + line += + rawterm::set_foreground(std::format("{:>{}}\u2502", idx + 1, line_number_offset), c); } const std::size_t vert_offset = get_active_model()->vertical_offset; From 91a863383905207e8f842aab381bb71de8cd694c Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 24 Apr 2026 13:14:19 +0000 Subject: [PATCH 2/5] unit test --- tests/model_test.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 1c621c7..c9a4f93 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -1,5 +1,7 @@ #include "model.h" +#include + #include #include #include @@ -734,3 +736,20 @@ TEST_CASE("dedent_curr_line", "[model]") { REQUIRE(m.buf.at(1).size() == 3); REQUIRE(m.buf.at(0).at(0) == 'f'); } + +TEST_CASE("add_mark", "[model]") { + auto m = Model({" foo", "bar"}, ""); + m.add_mark('a'); + + REQUIRE(m.marks.size() == 1); + REQUIRE(m.marks.at('a') == Mark(0, 0)); + REQUIRE_THROWS_AS(m.marks.at('b'), std::out_of_range); +} + +TEST_CASE("is_marked", "[model]") { + auto m = Model({" foo", "bar"}, ""); + m.add_mark('a'); + + REQUIRE(m.is_marked(0)); + REQUIRE(!m.is_marked(1)); +} From b2d70fd7def638c050bde1b67a5ed07c3a0a3909 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 24 Apr 2026 13:14:36 +0000 Subject: [PATCH 3/5] restore catch2 setting --- run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.py b/run.py index 46846cd..f357dd3 100755 --- a/run.py +++ b/run.py @@ -160,7 +160,7 @@ def test(testname: str | None, asan: bool) -> int: create_read_only_file() testname = testname if testname is not None else "" - test_flags: str = "-r compact --order rand" + test_flags: str = "-sr compact --order rand" shell_cmd: str = f"./build/tests/test_exe {test_flags} {testname}" return run_shell_cmd( From aecaab18a746f80c769d5f5e4e8c199549920814 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Sat, 25 Apr 2026 19:27:29 +0000 Subject: [PATCH 4/5] integration test --- tests/integration/normal_mode_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/normal_mode_test.py b/tests/integration/normal_mode_test.py index 45a184d..b60b190 100644 --- a/tests/integration/normal_mode_test.py +++ b/tests/integration/normal_mode_test.py @@ -558,3 +558,14 @@ def test_left_chevron_key(r: TmuxRunner): def test_right_chevron_key(r: TmuxRunner): r.press('>') assert " This" in r.lines()[0] + + +@setup("tests/fixture/lorem_ipsum.txt") +def test_m_key(r: TmuxRunner): + r.type_str("j" * 3) + r.type_str("ma") + r.press('j') + + target_line: str = r.color_screenshot()[3] + assert "4\u2502" in target_line + assert target_line.startswith("\x1b[38;2;95;250;104m") From 56e41dbfa0b9f93a8e68b7bc7bd9adfa6af78c07 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Tue, 28 Apr 2026 08:49:29 +0000 Subject: [PATCH 5/5] update readme/changelog --- CHANGELOG.md | 1 + README.md | 77 ++++++++++++++++++++++++++-------------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61759b2..ba2d93b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Implement `;!` to execute a shell command from the command bar * Disabled continuous deployment in GHA. See `.github/workflows/build_artifact.yml` for details +* `M` now marks the current row to the given letter on the keyboard * Resolved crash when the user tries to `delete word` past the end of a line diff --git a/README.md b/README.md index 8cfb857..98ff937 100644 --- a/README.md +++ b/README.md @@ -37,44 +37,45 @@ $ ./run.py --release switch to `write` mode to insert any characters. In `read` mode, you can perform the following actions (alphabetically ordered): -| Key | Description | -|---------------|-------------------------------------------------------| -| a | Go to write mode right of the current character | -| A | Go to write mode at the end of the current line | -| b | Move cursor back one word | -| dl | Delete the current line | -| dw | Delete the current word | -| e | Find next entered char ahead in file | -| f | Move cursor to end of current word | -| F | Find next entered char back in file | -| g | Go to top of file | -| G | Go to bottom of file | -| h | Move cursor left | -| i | Go to write mode left of the current character | -| j | Move cursor down | -| J | Move current line down one | -| k | Move cursor up | -| K | Move current line up one | -| l | Move cursor right | -| o | Enter newline below and go to write mode | -| O | Move cursor right | -| r | Replace char under cursor with next entered character | -| R | Redo | -| tn | Go to next tab | -| tp | Go to previous tab | -| tt | Open new tab | -| u | Undo | -| w | Move cursor forward one word | -| x | Delete character under the cursor | -| z | Center the current line in the screen | -| ; | Go to command mode | -| ~ | Switch case of char under cursor | -| [ | Go to beginning of paragraph (previous empty line) | -| ] | Go to end of paragraph (next empty line) | -| _ | Go to beginning of line | -| $ | Go to end of line | -| > | Indent current line | -| < | Dedent current line | +| Key | Description | +|---------------|----------------------------------------------------------| +| a | Go to write mode right of the current character | +| A | Go to write mode at the end of the current line | +| b | Move cursor back one word | +| dl | Delete the current line | +| dw | Delete the current word | +| e | Find next entered char ahead in file | +| f | Move cursor to end of current word | +| F | Find next entered char back in file | +| g | Go to top of file | +| G | Go to bottom of file | +| h | Move cursor left | +| i | Go to write mode left of the current character | +| j | Move cursor down | +| J | Move current line down one | +| k | Move cursor up | +| K | Move current line up one | +| l | Move cursor right | +| m | Mark the current row against a provided letter (ex `ma`) | +| o | Enter newline below and go to write mode | +| O | Move cursor right | +| r | Replace char under cursor with next entered character | +| R | Redo | +| tn | Go to next tab | +| tp | Go to previous tab | +| tt | Open new tab | +| u | Undo | +| w | Move cursor forward one word | +| x | Delete character under the cursor | +| z | Center the current line in the screen | +| ; | Go to command mode | +| ~ | Switch case of char under cursor | +| [ | Go to beginning of paragraph (previous empty line) | +| ] | Go to end of paragraph (next empty line) | +| _ | Go to beginning of line | +| $ | Go to end of line | +| > | Indent current line | +| < | Dedent current line | ### Command mode Likewise, you can switch to `command` mode with the semicolon `;` key. The