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

KazDragon / munin / 30995401802

05 Aug 2026 09:57AM UTC coverage: 98.216% (-0.1%) from 98.322%
30995401802

Pull #284

github

web-flow
Merge 7b6af72b3 into 3b2fdd11f
Pull Request #284: Expose more automation with a component ID field and more complete JSON representations

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

98.44
/src/text_area.cpp
1
#include "munin/text_area.hpp"
2

3
#include "munin/render_surface.hpp"
4

5
#include <boost/range/algorithm_ext/insert.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
using namespace terminalpp::literals;  // NOLINT
14

15
namespace munin {
16

17
// ==========================================================================
18
// TEXT_AREA::IMPLEMENTATION STRUCTURE
19
// ==========================================================================
20
struct text_area::impl
21
{
22
    // ======================================================================
23
    // CONSTRUCTOR
24
    // ======================================================================
25
    explicit impl(text_area &self) : self_(self)
214✔
26
    {
27
        laid_out_text_.emplace_back();
214✔
28
    }
214✔
29

30
    // ======================================================================
31
    // GET_PREFERRED_SIZE
32
    // ======================================================================
33
    auto get_preferred_size()
425✔
34
    {
35
        return terminalpp::extent{
425✔
36
            width_,
37
            std::max(
425✔
38
                static_cast<terminalpp::coordinate_type>(1),
850✔
39
                static_cast<terminalpp::coordinate_type>(
425✔
40
                    laid_out_text_.size()))};
850✔
41
    }
42

43
    // ======================================================================
44
    // GET_LENGTH
45
    // ======================================================================
46
    [[nodiscard]] auto get_length() const
458✔
47
    {
48
        return static_cast<text_area::text_index>(text_.size());
458✔
49
    }
50

51
    // ======================================================================
52
    // GET_CARET_POSITION
53
    // ======================================================================
54
    [[nodiscard]] auto get_caret_position() const
75✔
55
    {
56
        return caret_position_;
75✔
57
    }
58

59
    // ======================================================================
60
    // SET_CARET_POSITION
61
    // ======================================================================
62
    void set_caret_position(text_area::text_index position)
258✔
63
    {
64
        caret_position_ = std::clamp(position, 0, get_length());
258✔
65
        update_cursor_position();
258✔
66
    }
258✔
67

68
    // ======================================================================
69
    // GET_CURSOR_POSITION
70
    // ======================================================================
71
    [[nodiscard]] auto get_cursor_position() const
338✔
72
    {
73
        return cursor_position_;
338✔
74
    }
75

76
    // ======================================================================
77
    // SET_CURSOR_POSITION
78
    // ======================================================================
79
    void set_cursor_position(terminalpp::point const &position)
182✔
80
    {
81
        cursor_position_ = clamp_cursor_position(position);
182✔
82
        update_caret_position();
182✔
83
        self_.on_cursor_position_changed();
182✔
84
    }
182✔
85

86
    // ======================================================================
87
    // INSERT_TEXT
88
    // ======================================================================
89
    void insert_text(terminalpp::string const &text)
198✔
90
    {
91
        auto const new_caret_position =
92
            static_cast<text_area::text_index>(get_length() + text.size());
198✔
93

94
        boost::insert(text_, text_.begin() + caret_position_, text);
198✔
95
        layout_text();
198✔
96

97
        set_caret_position(new_caret_position);
198✔
98

99
        self_.on_preferred_size_changed();
198✔
100
        self_.on_redraw({
594✔
101
            {{}, self_.get_size()}
198✔
102
        });
103
    }
198✔
104

105
    // ======================================================================
106
    // INSERT_TEXT
107
    // ======================================================================
108
    void insert_text(
8✔
109
        terminalpp::string const &text, text_area::text_index position)
110
    {
111
        boost::insert(text_, text_.begin() + position, text);
8✔
112
        layout_text();
8✔
113

114
        self_.on_preferred_size_changed();
8✔
115
        self_.on_redraw({
24✔
116
            {{}, self_.get_size()}
8✔
117
        });
118
    }
8✔
119

120
    // ======================================================================
121
    // GET_TEXT
122
    // ======================================================================
123
    [[nodiscard]] auto get_text() const
71✔
124
    {
125
        return text_;
71✔
126
    }
127

128
    // ======================================================================
129
    // DRAW
130
    // ======================================================================
131
    void draw(render_surface &surface, terminalpp::rectangle const &region)
53✔
132
    {
133
        terminalpp::for_each_in_region(
53✔
134
            surface,
135
            region,
136
            [this](
53✔
137
                terminalpp::element &elem,
138
                terminalpp::coordinate_type column,
139
                terminalpp::coordinate_type row) {
140
                elem = (laid_out_text_.size() > row
1,172✔
141
                        && laid_out_text_[row].size() > column)
534✔
142
                         ? laid_out_text_[row][column]
1,706✔
143
                         : ' ';
144
            });
1,172✔
145
    }
53✔
146

147
    // ======================================================================
148
    // EVENT
149
    // ======================================================================
150
    void event(std::any const &ev)
101✔
151
    {
152
        if (auto const *mouse_event =
101✔
153
                std::any_cast<terminalpp::mouse::event>(&ev);
101✔
154
            mouse_event != nullptr)
155
        {
156
            handle_mouse_event(*mouse_event);
2✔
157
            return;
2✔
158
        }
159

160
        if (auto const *keypress_event =
99✔
161
                std::any_cast<terminalpp::virtual_key>(&ev);
99✔
162
            keypress_event != nullptr)
163
        {
164
            handle_keypress_event(*keypress_event);
99✔
165
            return;
99✔
166
        }
167
    }
168

169
    // ======================================================================
170
    // RESIZE
171
    // ======================================================================
172
    void resize(terminalpp::extent size)
200✔
173
    {
174
        width_ = size.width_;
200✔
175
        layout_text();
200✔
176
        update_cursor_position();
200✔
177
    }
200✔
178

179
private:
180
    // ======================================================================
181
    // LAYOUT_TEXT
182
    // ======================================================================
183
    void layout_text()
422✔
184
    {
185
        laid_out_text_.clear();
422✔
186
        laid_out_text_.emplace_back();
422✔
187

188
        bool wrap = false;
422✔
189

190
        for (auto const &ch : text_)
31,746✔
191
        {
192
            bool const wrapped = std::exchange(wrap, false);
31,324✔
193

194
            if (ch.glyph_.character_ == '\n')
31,324✔
195
            {
196
                // If we just wrapped a line, then absorb this newline.
197
                if (!wrapped)
1,283✔
198
                {
199
                    laid_out_text_.emplace_back();
1,154✔
200
                }
201
            }
202
            else
203
            {
204
                laid_out_text_.back() += ch;
30,041✔
205
            }
206

207
            if (laid_out_text_.back().size() == width_)
31,324✔
208
            {
209
                wrap = true;
912✔
210
                laid_out_text_.emplace_back();
912✔
211
            }
212
        }
213
    }
422✔
214

215
    // ======================================================================
216
    // CLAMP_CURSOR_POSITION
217
    // ======================================================================
218
    terminalpp::point clamp_cursor_position(terminalpp::point position)
182✔
219
    {
220
        // Fit the requested cursor position with the bounds of the laid
221
        // out text.
222
        position.y_ = std::clamp(
364✔
223
            position.y_,
224
            terminalpp::coordinate_type{0},
×
225
            static_cast<terminalpp::coordinate_type>(
×
226
                laid_out_text_.size() - 1));
182✔
227
        position.x_ = std::clamp(
546✔
228
            position.x_,
229
            terminalpp::coordinate_type{0},
182✔
230
            std::min(
231
                static_cast<terminalpp::coordinate_type>(
182✔
232
                    std::max(width_ - 1, 0)),
×
233
                static_cast<terminalpp::coordinate_type>(
182✔
234
                    laid_out_text_[position.y_].size())));
182✔
235

236
        return position;
182✔
237
    }
238

239
    // ======================================================================
240
    // ADVANCE_CURSOR
241
    // ======================================================================
242
    auto advance_cursor(
39,589✔
243
        terminalpp::point cursor,
244
        terminalpp::element const element_under_caret,
245
        terminalpp::coordinate_type const width,
246
        bool &wrap)
247
    {
248
        auto const wrapped = std::exchange(wrap, false);
39,589✔
249

250
        if (element_under_caret == '\n')
39,589✔
251
        {
252
            if (!wrapped)
1,508✔
253
            {
254
                cursor.x_ = 0;
1,347✔
255
                ++cursor.y_;
1,347✔
256
            }
257
        }
258
        else if ((cursor.x_ + 1) == width)
38,081✔
259
        {
260
            cursor.x_ = 0;
336✔
261
            ++cursor.y_;
336✔
262
            wrap = true;
336✔
263
        }
264
        else
265
        {
266
            ++cursor.x_;
37,745✔
267
        }
268

269
        return cursor;
39,589✔
270
    }
271

272
    // ======================================================================
273
    // UPDATE_CURSOR_POSITION
274
    // ======================================================================
275
    void update_cursor_position()
458✔
276
    {
277
        cursor_position_ = {0, 0};
458✔
278
        bool wrapped = false;
458✔
279

280
        for (text_area::text_index index = 0; index < caret_position_; ++index)
33,656✔
281
        {
282
            cursor_position_ =
283
                advance_cursor(cursor_position_, text_[index], width_, wrapped);
33,198✔
284
        }
285

286
        self_.on_cursor_position_changed();
458✔
287
    }
458✔
288

289
    // ======================================================================
290
    // UPDATE_CARET_POSITION
291
    // ======================================================================
292
    void update_caret_position()
182✔
293
    {
294
        auto current_position = terminalpp::point{};
182✔
295
        auto wrapped = false;
182✔
296
        caret_position_ = 0;
182✔
297

298
        auto const &wrapped_newline_is_under_cursor = [&]() {
182✔
299
            return wrapped && caret_position_ < text_.size()
3✔
300
                && text_[caret_position_] == '\n';
185✔
301
        };
182✔
302

303
        while (current_position != cursor_position_)
6,573✔
304
        {
305
            current_position = advance_cursor(
6,391✔
306
                current_position, text_[caret_position_], width_, wrapped);
6,391✔
307

308
            ++caret_position_;
6,391✔
309
        }
310

311
        if (wrapped_newline_is_under_cursor())
182✔
312
        {
313
            ++caret_position_;
3✔
314
        }
315
    }
182✔
316

317
    // ======================================================================
318
    // HANDLE_MOUSE_EVENT
319
    // ======================================================================
320
    void handle_mouse_event(terminalpp::mouse::event const &ev)
2✔
321
    {
322
        self_.set_focus();
2✔
323
        self_.set_cursor_position(ev.position_);
2✔
324
    }
2✔
325

326
    // ======================================================================
327
    // HANDLE_CURSOR_LEFT
328
    // ======================================================================
329
    void handle_cursor_left(int repeat_count)
11✔
330
    {
331
        set_caret_position(caret_position_ - repeat_count);
11✔
332
    }
11✔
333

334
    // ======================================================================
335
    // HANDLE_CURSOR_RIGHT
336
    // ======================================================================
337
    void handle_cursor_right(int repeat_count)
9✔
338
    {
339
        set_caret_position(caret_position_ + repeat_count);
9✔
340
    }
9✔
341

342
    // ======================================================================
343
    // HANDLE_CURSOR_UP
344
    // ======================================================================
345
    void handle_cursor_up(int repeat_count)
20✔
346
    {
347
        set_cursor_position(
20✔
348
            {cursor_position_.x_,
349
             std::max(cursor_position_.y_ - repeat_count, 0)});
20✔
350
    }
20✔
351

352
    // ======================================================================
353
    // HANDLE_CURSOR_DOWN
354
    // ======================================================================
355
    void handle_cursor_down(int repeat_count)
11✔
356
    {
357
        set_cursor_position(
11✔
358
            {cursor_position_.x_,
359
             std::max(cursor_position_.y_ + repeat_count, 0)});
11✔
360
    }
11✔
361

362
    // ======================================================================
363
    // HANDLE_HOME
364
    // ======================================================================
365
    void handle_home(terminalpp::vk_modifier modifiers)
12✔
366
    {
367
        set_cursor_position(
12✔
368
            {0,
369
             modifiers == terminalpp::vk_modifier::ctrl ? 0
370
                                                        : cursor_position_.y_});
371
    }
12✔
372

373
    // ======================================================================
374
    // HANDLE_END
375
    // ======================================================================
376
    void handle_end(terminalpp::vk_modifier modifiers)
12✔
377
    {
378
        set_cursor_position(
18✔
379
            {width_,
380
             modifiers == terminalpp::vk_modifier::ctrl
381
                 ? static_cast<terminalpp::coordinate_type>(
382
                       laid_out_text_.size() - 1)
6✔
383
                 : cursor_position_.y_});
384
    }
12✔
385

386
    // ======================================================================
387
    // HANDLE_BACKSPACE
388
    // ======================================================================
389
    void handle_backspace()
10✔
390
    {
391
        if (caret_position_ != 0)
10✔
392
        {
393
            auto const size = self_.get_size();
6✔
394

395
            text_.erase(
12✔
396
                text_.begin() + caret_position_ - 1,
6✔
397
                text_.begin() + caret_position_);
6✔
398
            set_caret_position(caret_position_ - 1);
6✔
399

400
            layout_text();
6✔
401
            self_.on_redraw({
24✔
402
                {{0, cursor_position_.y_},
403
                 {size.width_, size.height_ - cursor_position_.y_}}
6✔
404
            });
405
        }
406
    }
10✔
407

408
    // ======================================================================
409
    // HANDLE_ENTER
410
    // ======================================================================
411
    void handle_enter()
4✔
412
    {
413
        insert_text("\n"_ts, caret_position_);
4✔
414
        set_caret_position(caret_position_ + 1);
4✔
415

416
        layout_text();
4✔
417
        self_.on_redraw({
12✔
418
            {{0, cursor_position_.y_},
419
             {width_,
420
              static_cast<terminalpp::coordinate_type>(
421
                  laid_out_text_.size() - cursor_position_.y_)}}
4✔
422
        });
423
    }
4✔
424

425
    // ======================================================================
426
    // HANDLE_CONTROL_KEY
427
    // ======================================================================
428
    void handle_control_key(terminalpp::virtual_key const &ev)
93✔
429
    {
430
        switch (ev.key)
93✔
431
        {
432
            case terminalpp::vk::cursor_left:
11✔
433
                handle_cursor_left(ev.repeat_count);
11✔
434
                break;
11✔
435

436
            case terminalpp::vk::cursor_right:
9✔
437
                handle_cursor_right(ev.repeat_count);
9✔
438
                break;
9✔
439

440
            case terminalpp::vk::cursor_up:
20✔
441
                handle_cursor_up(ev.repeat_count);
20✔
442
                break;
20✔
443

444
            case terminalpp::vk::cursor_down:
11✔
445
                handle_cursor_down(ev.repeat_count);
11✔
446
                break;
11✔
447

448
            case terminalpp::vk::home:
12✔
449
                handle_home(ev.modifiers);
12✔
450
                break;
12✔
451

452
            case terminalpp::vk::end:
12✔
453
                handle_end(ev.modifiers);
12✔
454
                break;
12✔
455

456
            case terminalpp::vk::del:
10✔
457
                // Fall-through
458
            case terminalpp::vk::bs:
459
                handle_backspace();
10✔
460
                break;
10✔
461

462
            case terminalpp::vk::enter:
4✔
463
                handle_enter();
4✔
464
                break;
4✔
465

466
            default:
4✔
467
                break;
4✔
468
        }
469
    }
93✔
470

471
    // ======================================================================
472
    // HANDLE_TEXT
473
    // ======================================================================
474
    void handle_text(terminalpp::byte by)
6✔
475
    {
476
        text_.insert(text_.begin() + caret_position_, by);
6✔
477
        set_caret_position(caret_position_ + 1);
6✔
478

479
        layout_text();
6✔
480
        self_.on_redraw({
18✔
481
            {{0, cursor_position_.y_},
482
             {width_,
483
              static_cast<terminalpp::coordinate_type>(
484
                  laid_out_text_.size() - cursor_position_.y_)}}
6✔
485
        });
486
    }
6✔
487

488
    // ======================================================================
489
    // HANDLE_KEYPRESS_EVENT
490
    // ======================================================================
491
    void handle_keypress_event(terminalpp::virtual_key const &ev)
99✔
492
    {
493
        if (terminalpp::is_control_key(ev.key))
99✔
494
        {
495
            handle_control_key(ev);
93✔
496
        }
497
        else
498
        {
499
            handle_text(static_cast<terminalpp::byte>(ev.key));
6✔
500
        }
501
    }
99✔
502

503
    text_area &self_;
504
    terminalpp::string text_;
505
    terminalpp::coordinate_type width_{0};
506
    std::vector<terminalpp::string> laid_out_text_;
507

508
    text_area::text_index caret_position_{0};
509
    terminalpp::point cursor_position_{0, 0};
510
};
511

512
// ==========================================================================
513
// CONSTRUCTOR
514
// ==========================================================================
515
text_area::text_area() : pimpl_(std::make_unique<impl>(*this))
214✔
516
{
517
}
214✔
518

519
// ==========================================================================
520
// DESTRUCTOR
521
// ==========================================================================
522
text_area::~text_area() = default;
214✔
523

524
// ==========================================================================
525
// GET_CARET_POSITION
526
// ==========================================================================
527
text_area::text_index text_area::get_caret_position() const
75✔
528
{
529
    return pimpl_->get_caret_position();
75✔
530
}
531

532
// ==========================================================================
533
// SET_CARET_POSITION
534
// ==========================================================================
535
void text_area::set_caret_position(text_index position)
24✔
536
{
537
    pimpl_->set_caret_position(position);
24✔
538
}
24✔
539

540
// ==========================================================================
541
// GET_LENGTH
542
// ==========================================================================
543
text_area::text_index text_area::get_length() const
2✔
544
{
545
    return pimpl_->get_length();
2✔
546
}
547

548
// ==========================================================================
549
// INSERT_TEXT
550
// ==========================================================================
551
void text_area::insert_text(terminalpp::string const &text)
198✔
552
{
553
    pimpl_->insert_text(text);
198✔
554
}
198✔
555

556
// ==========================================================================
557
// INSERT_TEXT
558
// ==========================================================================
559
void text_area::insert_text(
4✔
560
    terminalpp::string const &text, text_area::text_index position)
561
{
562
    pimpl_->insert_text(text, position);
4✔
563
}
4✔
564

565
// ==========================================================================
566
// GET_TEXT
567
// ==========================================================================
568
terminalpp::string text_area::get_text() const
71✔
569
{
570
    return pimpl_->get_text();
71✔
571
}
572

573
// ==========================================================================
574
// DO_SET_SIZE
575
// ==========================================================================
576
void text_area::do_set_size(terminalpp::extent const &size)
200✔
577
{
578
    auto const old_preferred_size = get_preferred_size();
200✔
579

580
    basic_component::do_set_size(size);
200✔
581
    pimpl_->resize(size);
200✔
582

583
    if (get_preferred_size() != old_preferred_size)
200✔
584
    {
585
        on_preferred_size_changed();
200✔
586
    }
587
}
200✔
588

589
// ==========================================================================
590
// DO_GET_PREFERRED_SIZE
591
// ==========================================================================
592
terminalpp::extent text_area::do_get_preferred_size() const
425✔
593
{
594
    return pimpl_->get_preferred_size();
425✔
595
}
596

597
// ==========================================================================
598
// DO_GET_CURSOR_POSITION
599
// ==========================================================================
600
terminalpp::point text_area::do_get_cursor_position() const
338✔
601
{
602
    return pimpl_->get_cursor_position();
338✔
603
}
604

605
// ==========================================================================
606
// DO_GET_CURSOR_POSITION
607
// ==========================================================================
608
void text_area::do_set_cursor_position(terminalpp::point const &position)
127✔
609
{
610
    pimpl_->set_cursor_position(position);
127✔
611
}
127✔
612

613
// ==========================================================================
614
// DO_GET_CURSOR_STATE
615
// ==========================================================================
616
bool text_area::do_get_cursor_state() const
10✔
617
{
618
    return true;
10✔
619
}
620

621
// ==========================================================================
622
// DO_DRAW
623
// ==========================================================================
624
void text_area::do_draw(
53✔
625
    render_surface &surface, terminalpp::rectangle const &region) const
626
{
627
    pimpl_->draw(surface, region);
53✔
628
}
53✔
629

630
// ==========================================================================
631
// DO_EVENT
632
// ==========================================================================
633
void text_area::do_event(std::any const &ev)
101✔
634
{
635
    pimpl_->event(ev);
101✔
636
}
101✔
637

638
// ==========================================================================
639
// DO_TO_JSON
640
// ==========================================================================
641
nlohmann::json text_area::do_to_json() const
9✔
642
{
643
    auto json = basic_component::do_to_json();
9✔
644
    json["type"] = "text_area";
9✔
645
    json["text"] = terminalpp::to_string(get_text());
9✔
646
    json["caret_position"] = get_caret_position();
9✔
647
    return json;
9✔
NEW
648
}
×
649

650
// ==========================================================================
651
// MAKE_TEXT_AREA
652
// ==========================================================================
653
std::shared_ptr<text_area> make_text_area()
1✔
654
{
655
    return std::make_shared<text_area>();
1✔
656
}
657

658
}  // 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