• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KazDragon / munin / 31000645644

05 Aug 2026 11:14AM UTC coverage: 98.216% (-0.1%) from 98.322%
31000645644

push

github

web-flow
Expose more automation with a component ID field and more complete JSON representations (#284)

* Hide composite internals from automation JSON

* Lock button automation JSON as a leaf

* Expose automation ids on Munin components

* Report automation ids on composite Munin widgets

* doc: add CONTEXT.md for AI clarity

* feat: with_id() helper for assigning IDs in flow

* doc: add CONTEXT.md for AI clarity

* Expose button accessible name in JSON

* Expose image accessible name in JSON

* Derive button accessible name from content

* Expose edit text in introspection JSON

* Report edit content in introspection JSON

* Report edit caret in introspection JSON

* Name edit JSON snapshot in tests

* Move focus on tab key in containers

* Move focus on back tab key in containers

* Name container virtual key extraction

* Share container keypress test helper

* Document normal keyboard focus navigation

* Add button JSON coverage

* Cover unfocused button JSON state

* Cover image JSON bounds

* Cover image JSON automation id

* Cover image JSON preferred size

* Cover image JSON focus and cursor state

* Cover image JSON fill changes

* Cover image JSON single-line content changes

* Join image JSON accessible names across lines

* Cover image JSON multi-line constructor names

* Cover image JSON empty name boundary

* Cover focused image JSON state

* Cover cleared image JSON content

* Cover edit JSON type and base fields

* Cover edit JSON preferred size

* Cover unfocused edit JSON state

* Cover focused edit JSON state

* Cover edit JSON cursor state

* Cover edit JSON set text

* Cover container JSON automation id

* Cover container JSON position

* Cover container JSON size

* Refactor container JSON shape assertions

* Cover container JSON current layout

* Refactor container JSON mock layout setup

* Add text area JSON type snapshot

* Report text area text in JSON

* Report text area caret position in JSON

* Cover text area component JSON fiel... (continued)

147 of 151 new or added lines in 14 files covered. (97.35%)

1 existing line in 1 file now uncovered.

2643 of 2691 relevant lines covered (98.22%)

496.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.38
/src/edit.cpp
1
#include "munin/edit.hpp"
2

3
#include "munin/render_surface.hpp"
4

5
#include <boost/range/adaptor/filtered.hpp>
6
#include <terminalpp/algorithm/for_each_in_region.hpp>
7
#include <terminalpp/mouse.hpp>
8
#include <terminalpp/virtual_key.hpp>
9

10
#include <algorithm>
11
#include <memory>
12

13
namespace munin {
14

15
// ==========================================================================
16
// EDIT::IMPLEMENTATION STRUCTURE
17
// ==========================================================================
18
struct edit::impl
19
{
20
    // ======================================================================
21
    // CONSTRUCTOR
22
    // ======================================================================
23
    explicit impl(edit &self) : self_(self)
108 ✔
24
    {
25
    }
108 ✔
26

27
    // ======================================================================
28
    // GET_CONTENT
29
    // ======================================================================
30
    [[nodiscard]] terminalpp::string const &get_content() const
260 ✔
31
    {
32
        return content_;
260 ✔
33
    }
34

35
    // ======================================================================
36
    // GET_LENGTH
37
    // ======================================================================
38
    [[nodiscard]] edit::text_index get_length() const
922 ✔
39
    {
40
        return static_cast<edit::text_index>(content_.size());
922 ✔
41
    }
42

43
    // ======================================================================
44
    // GET_CARET_POSITION
45
    // ======================================================================
46
    [[nodiscard]] edit::text_index get_caret_position() const
119 ✔
47
    {
48
        return caret_position_;
119 ✔
49
    }
50

51
    // ======================================================================
52
    // SET_CARET_POSITION
53
    // ======================================================================
54
    void set_caret_position(edit::text_index position)
199 ✔
55
    {
56
        caret_position_ = std::clamp(position, 0, get_length());
199 ✔
57
        update_cursor_position();
199 ✔
58
    }
199 ✔
59

60
    // ======================================================================
61
    // GET_CURSOR_POSITION
62
    // ======================================================================
63
    [[nodiscard]] terminalpp::point get_cursor_position() const
177 ✔
64
    {
65
        return cursor_position_;
177 ✔
66
    }
67

68
    // ======================================================================
69
    // UPDATE_CURSOR_POSITION
70
    // ======================================================================
71
    void update_cursor_position()
302 ✔
72
    {
73
        cursor_position_ = {
604 ✔
74
            std::clamp(
302 ✔
75
                caret_position_, 0, std::max(0, self_.get_size().width_ - 1)),
302 ✔
76
            0};
77
        self_.on_cursor_position_changed();
302 ✔
78
    }
302 ✔
79

80
    // ======================================================================
81
    // SET_TEXT
82
    // ======================================================================
83
    void set_text(terminalpp::string const &text)
4 ✔
84
    {
85
        auto const old_caret_position = caret_position_;
4 ✔
86

87
        content_ = text;
4 ✔
88

89
        auto const new_caret_position = std::min(
4 ✔
90
            old_caret_position, static_cast<text_index>(content_.size()));
4 ✔
91

92
        set_caret_position(new_caret_position);
4 ✔
93

94
        self_.on_preferred_size_changed();
4 ✔
95

96
        self_.on_redraw({
12 ✔
97
            {{0, 0}, self_.get_size()}
4 ✔
98
        });
99
    }
4 ✔
100

101
    // ======================================================================
102
    // INSERT_TEXT
103
    // ======================================================================
104
    void insert_text(terminalpp::string const &text)
89 ✔
105
    {
106
        using std::begin;
107
        using std::end;
108

109
        auto const &is_visible_in_edits = [&](auto const &element) {
436 ✔
110
            auto const &is_control_element = [](auto const &control_element) {
687 ✔
111
                return control_element.glyph_.character_
112
                    <= terminalpp::detail::ascii::esc;
340 ✔
113
            };
114

115
            return is_printable(element.glyph_) && !is_control_element(element);
347 ✔
116
        };
117

118
        auto const old_content_length = get_length();
89 ✔
119

120
        auto const insertable_text =
121
            text | boost::adaptors::filtered(is_visible_in_edits);
89 ✔
122

123
        auto const old_caret_position = caret_position_;
89 ✔
124

125
        content_.insert(
178 ✔
126
            content_.begin() + caret_position_,
89 ✔
127
            begin(insertable_text),
89 ✔
128
            end(insertable_text));
89 ✔
129

130
        auto const new_content_length = get_length();
89 ✔
131
        auto const added_content_length =
89 ✔
132
            new_content_length - old_content_length;
133

134
        set_caret_position(caret_position_ + added_content_length);
89 ✔
135

136
        // Adding new text causes not only the space under the old cursor to
137
        // be redrawn, but also everything to the right of that as it is shifted
138
        // along one character.
139

140
        // This must then be trimmed to the extents of the space currently taken
141
        // by the edit.
142
        auto const remaining_space =
143
            std::max(self_.get_size().width_ - old_caret_position, 0);
89 ✔
144

145
        auto const changed_text_length =
146
            std::min(new_content_length - old_caret_position, remaining_space);
89 ✔
147

148
        self_.on_preferred_size_changed();
89 ✔
149

150
        self_.on_redraw({
178 ✔
151
            {{old_caret_position, 0}, {changed_text_length, 1}}
152
        });
153
    }
89 ✔
154

155
    // ======================================================================
156
    // EVENT
157
    // ======================================================================
158
    void key_event(terminalpp::virtual_key const &vk)
61 ✔
159
    {
160
        switch (vk.key)
61 ✔
161
        {
162
            case terminalpp::vk::cursor_left:
5 ✔
163
                handle_cursor_left();
5 ✔
164
                break;
5 ✔
165

166
            case terminalpp::vk::cursor_right:
3 ✔
167
                handle_cursor_right();
3 ✔
168
                break;
3 ✔
169

170
            case terminalpp::vk::home:
8 ✔
171
                handle_home();
8 ✔
172
                break;
8 ✔
173

174
            case terminalpp::vk::end:
8 ✔
175
                handle_end();
8 ✔
176
                break;
8 ✔
177

178
            case terminalpp::vk::bs:
13 ✔
179
                // Fall-through
180
            case terminalpp::vk::del:
181
                handle_backspace();
13 ✔
182
                break;
13 ✔
183

184
            default:
24 ✔
185
                handle_text(static_cast<char>(vk.key));
24 ✔
186
                break;
24 ✔
187
        }
188
    }
61 ✔
189

190
    // ======================================================================
191
    // MOUSE_EVENT
192
    // ======================================================================
193
    void mouse_event(terminalpp::mouse::event const &mouse)
15 ✔
194
    {
195
        if (mouse.action_ == terminalpp::mouse::event_type::left_button_down)
15 ✔
196
        {
197
            set_caret_position(
13 ✔
198
                static_cast<edit::text_index>(mouse.position_.x_));
13 ✔
199
            self_.set_focus();
13 ✔
200
        }
201
    }
15 ✔
202

203
private:
204
    // ======================================================================
205
    // HANDLE_CURSOR_LEFT
206
    // ======================================================================
207
    void handle_cursor_left()
5 ✔
208
    {
209
        set_caret_position(caret_position_ - 1);
5 ✔
210
    }
5 ✔
211

212
    // ======================================================================
213
    // HANDLE_CURSOR_RIGHT
214
    // ======================================================================
215
    void handle_cursor_right()
3 ✔
216
    {
217
        set_caret_position(caret_position_ + 1);
3 ✔
218
    }
3 ✔
219

220
    // ======================================================================
221
    // HANDLE_HOME
222
    // ======================================================================
223
    void handle_home()
8 ✔
224
    {
225
        set_caret_position(0);
8 ✔
226
    }
8 ✔
227

228
    // ======================================================================
229
    // HANDLE_HOME
230
    // ======================================================================
231
    void handle_end()
8 ✔
232
    {
233
        set_caret_position(get_length());
8 ✔
234
    }
8 ✔
235

236
    // ======================================================================
237
    // HANDLE_BACKSPACE
238
    // ======================================================================
239
    void handle_backspace()
13 ✔
240
    {
241
        if (caret_position_ != 0)
13 ✔
242
        {
243
            auto const redraw_amount = (get_length() - caret_position_) + 1;
9 ✔
244

245
            auto const erased_content =
246
                content_.begin() + (caret_position_ - 1);
9 ✔
247
            content_.erase(erased_content, erased_content + 1);
9 ✔
248

249
            set_caret_position(caret_position_ - 1);
9 ✔
250
            self_.on_preferred_size_changed();
9 ✔
251

252
            self_.on_redraw({
27 ✔
253
                {{caret_position_, 0}, {redraw_amount, 1}}
254
            });
255
        }
256
    }
13 ✔
257

258
    // ======================================================================
259
    // HANDLE_TEXT
260
    // ======================================================================
261
    void handle_text(char ch)
24 ✔
262
    {
263
        terminalpp::string text;
24 ✔
264
        text += static_cast<terminalpp::byte>(ch);
24 ✔
265

266
        insert_text(text);
24 ✔
267
    }
24 ✔
268

269
    edit &self_;
270
    terminalpp::string content_;
271
    edit::text_index caret_position_ = 0;
272
    terminalpp::point cursor_position_{0, 0};
273
};
274

275
// ==========================================================================
276
// CONSTRUCTOR
277
// ==========================================================================
278
edit::edit() : pimpl_(std::make_unique<impl>(*this))
108 ✔
279
{
280
}
108 ✔
281

282
// ==========================================================================
283
// DESTRUCTOR
284
// ==========================================================================
285
edit::~edit() = default;
108 ✔
286

287
// ==========================================================================
288
// SET_CARET_POSITION
289
// ==========================================================================
290
void edit::set_caret_position(edit::text_index position)
7 ✔
291
{
292
    pimpl_->set_caret_position(position);
7 ✔
293
}
7 ✔
294

295
// ==========================================================================
296
// GET_CARET_POSITION
297
// ==========================================================================
298
edit::text_index edit::get_caret_position() const
119 ✔
299
{
300
    return pimpl_->get_caret_position();
119 ✔
301
}
302

303
// ==========================================================================
304
// GET_LENGTH
305
// ==============s============================================================
306
edit::text_index edit::get_length() const
528 ✔
307
{
308
    return pimpl_->get_length();
528 ✔
309
}
310

311
// ==========================================================================
312
// GET_TEXT
313
// ==========================================================================
314
terminalpp::string edit::get_text() const
50 ✔
315
{
316
    return pimpl_->get_content();
50 ✔
317
}
318

319
// ==========================================================================
320
// INSERT_TEXT
321
// ==========================================================================
322
void edit::insert_text(terminalpp::string const &text)
65 ✔
323
{
324
    pimpl_->insert_text(text);
65 ✔
325
    on_cursor_position_changed();
65 ✔
326
}
65 ✔
327

328
// ==========================================================================
329
// SET_TEXT
330
// ==========================================================================
331
void edit::set_text(terminalpp::string const &content)
4 ✔
332
{
333
    pimpl_->set_text(content);
4 ✔
334
}
4 ✔
335

336
// ==========================================================================
337
// DO_SET_SIZE
338
// ==========================================================================
339
void edit::do_set_size(terminalpp::extent const &size)
103 ✔
340
{
341
    basic_component::do_set_size(size);
103 ✔
342
    pimpl_->update_cursor_position();
103 ✔
343
}
103 ✔
344

345
// ==========================================================================
346
// DO_GET_PREFERRED_SIZE
347
// ==========================================================================
348
terminalpp::extent edit::do_get_preferred_size() const
38 ✔
349
{
350
    // An edit would prefer to have a width that is the length of the text
351
    // content, plus one space for a cursor to sit at the end.
352
    return terminalpp::extent{get_length() + 1, 1};
38 ✔
353
}
354

355
// ==========================================================================
356
// DO_GET_CURSOR_STATE
357
// ==========================================================================
358
bool edit::do_get_cursor_state() const
16 ✔
359
{
360
    return true;
16 ✔
361
}
362

363
// ==========================================================================
364
// DO_GET_CURSOR_POSITION
365
// ==========================================================================
366
terminalpp::point edit::do_get_cursor_position() const
177 ✔
367
{
368
    return pimpl_->get_cursor_position();
177 ✔
369
}
370

371
// ==========================================================================
372
// DO_GET_CURSOR_POSITION
373
// ==========================================================================
374
void edit::do_set_cursor_position(terminalpp::point const &position)
53 ✔
375
{
376
    // For an edit, setting the cursor position is identical to setting
377
    // the caret to the same location.
378
    pimpl_->set_caret_position(position.x_);
53 ✔
379
}
53 ✔
380

381
// ==========================================================================
382
// DO_DRAW
383
// ==========================================================================
384
void edit::do_draw(
168 ✔
385
    render_surface &surface, terminalpp::rectangle const &region) const
386
{
387
    terminalpp::for_each_in_region(
168 ✔
388
        surface,
389
        region,
390
        [this, size = get_size()](
168 ✔
391
            terminalpp::element &elem,
392
            terminalpp::coordinate_type column,  // NOLINT
393
            terminalpp::coordinate_type row) {
394
            if (column < get_length())
487 ✔
395
            {
396
                elem = pimpl_->get_content()[column];
210 ✔
397
            }
398
            else if (column < size.width_)
277 ✔
399
            {
400
                elem = ' ';
277 ✔
401
            }
402
        });
487 ✔
403
}
168 ✔
404

405
// ==========================================================================
406
// DO_EVENT
407
// ==========================================================================
408
void edit::do_event(std::any const &ev)
76 ✔
409
{
410
    if (auto const *vk = std::any_cast<terminalpp::virtual_key>(&ev);
76 ✔
411
        vk != nullptr)
412
    {
413
        pimpl_->key_event(*vk);
61 ✔
414
        return;
61 ✔
415
    }
416
    else if (auto const *mouse = std::any_cast<terminalpp::mouse::event>(&ev);
15 ✔
417
             mouse != nullptr)
418
    {
419
        pimpl_->mouse_event(*mouse);
15 ✔
420
        return;
15 ✔
421
    }
422
}
423

424
// ==========================================================================
425
// DO_TO_JSON
426
// ==========================================================================
427
nlohmann::json edit::do_to_json() const
11 ✔
428
{
429
    auto json = basic_component::do_to_json();
11 ✔
430
    json["type"] = "edit";
11 ✔
431
    json["text"] = terminalpp::to_string(get_text());
11 ✔
432
    json["caret_position"] = get_caret_position();
11 ✔
433
    return json;
11 ✔
NEW
434
}
×
435

436
// ==========================================================================
437
// MAKE_EDIT
438
// ==========================================================================
439
std::shared_ptr<edit> make_edit()
35 ✔
440
{
441
    return std::make_shared<edit>();
35 ✔
442
}
443

444
}  // namespace munin
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc