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

tstack / lnav / 20284884426-2753

16 Dec 2025 10:23PM UTC coverage: 68.23% (-0.7%) from 68.903%
20284884426-2753

push

github

tstack
[log] show invalid utf hex dump in log view too

25 of 25 new or added lines in 2 files covered. (100.0%)

503 existing lines in 33 files now uncovered.

51170 of 74996 relevant lines covered (68.23%)

433797.6 hits per line

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

42.02
/src/textinput_curses.hh
1
/**
2
 * Copyright (c) 2025, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#ifndef textinput_curses_hh
31
#define textinput_curses_hh
32

33
#include <chrono>
34
#include <cstdint>
35
#include <deque>
36
#include <functional>
37
#include <map>
38
#include <memory>
39
#include <optional>
40
#include <string>
41
#include <utility>
42
#include <vector>
43

44
#include "base/attr_line.hh"
45
#include "base/line_range.hh"
46
#include "base/lnav.console.hh"
47
#include "document.sections.hh"
48
#include "pcrepp/pcre2pp.hh"
49
#include "plain_text_source.hh"
50
#include "text_format.hh"
51
#include "textview_curses.hh"
52
#include "view_curses.hh"
53

54
/**
55
 * A multi-line text input box that supports the following UX:
56
 *
57
 * - Pressing up:
58
 *   * on the first line moves the cursor to the beginning of the line;
59
 *   * on a line in the middle moves to the previous line and moves to the
60
 *     end of the line, if the previous line is shorter;
61
 *   * scrolls the view so that one line above the cursor is visible.
62
 * - Pressing down:
63
 *   * on the bottom line moves the cursor to the end of the line;
64
 *   * scrolls the view so that the cursor is visible;
65
 *   * does not move the cursor past the last line in the buffer.
66
 * - Typing a character:
67
 *   * inserts it at the cursor position
68
 *   * scrolls the view to the right
69
 * - Pressing backspace deletes the previous character.
70
 *   * At the beginning of a line, the current line is appended to the
71
 *     previous.
72
 *   * At the beginning of the buffer, nothing happens.
73
 * - Pressing CTRL-K:
74
 *   * without an active selection, copies the text from the cursor to
75
 *     the end of the line into the clipboard and then deletes it;
76
 *     - If the cursor is at the end of the line, the line-feed is deleted
77
 *       and the following line is appended to the current line.
78
 *     - Subsequent presses of CTRL-K append text to the clipboard if the
79
 *       cursor hasn't been moved by the user.
80
 *   * with a selection, copies the selected text into the clipboard
81
 *     and then deletes it.
82
 * - Pressing CTRL-Y:
83
 *   * Copies the contents of the clipboard into the buffer at the cursor
84
 *     location.
85
 * - Pressing CTRL-A moves the cursor to the beginning of the line.
86
 * - Pressing CTRL-E moves the cursor to the end of the line.
87
 * - Pressing HOME moves the cursor to the first line of the buffer.
88
 * - Pressing END moves the cursor to the last line of the buffer.
89
 * - Pressing CTRL-S switches to search mode:
90
 *   * Entering text will search for the first occurrence starting from
91
 *     the cursor position.
92
 *   * Pressing CTRL-S will move to the next occurrence.
93
 *     If nothing was found on the last press, pressing again will wrap
94
 *     around to the first line of the buffer.
95
 *   * Pressing CTRL-R will move to the previous occurrence.
96
 *     If nothing was found on the last press, pressing again will wrap
97
 *     around to the last line of the buffer.
98
 *   * Pressing ESC or any of the movement keys will cancel the search
99
 *     and move the cursor in the buffer.
100
 */
101
class textinput_curses : public view_curses {
102
public:
103
    static const attr_line_t& get_help_text();
104

105
    enum class direction_t {
106
        left,
107
        right,
108
        up,
109
        down,
110
    };
111

112
    struct movement {
113
        const direction_t hm_dir;
114
        const size_t hm_amount;
115

116
        movement(direction_t hm_dir, size_t amount)
×
117
            : hm_dir(hm_dir), hm_amount(amount)
×
118
        {
119
        }
120
    };
121

122
    struct input_point {
123
        int x{0};
124
        int y{0};
125

126
        static input_point home() { return input_point{}; }
127

128
        static input_point end()
129
        {
130
            return {
131
                std::numeric_limits<int>::max(),
132
                std::numeric_limits<int>::max(),
133
            };
134
        }
135

136
        input_point copy_with_x(int x) { return {x, this->y}; }
×
137

138
        input_point copy_with_y(int y) { return {this->x, y}; }
×
139

140
        bool operator<(const input_point& rhs) const
2✔
141
        {
142
            return this->y < rhs.y || (this->y == rhs.y && this->x < rhs.x);
2✔
143
        }
144

145
        bool operator==(const input_point& rhs) const
×
146
        {
147
            return this->x == rhs.x && this->y == rhs.y;
×
148
        }
149

150
        bool operator!=(const input_point& rhs) const
5✔
151
        {
152
            return this->x != rhs.x || this->y != rhs.y;
5✔
153
        }
154

155
        input_point operator+(const movement& rhs) const
×
156
        {
157
            auto retval = *this;
×
158
            switch (rhs.hm_dir) {
×
159
                case direction_t::left:
×
160
                    retval.x -= rhs.hm_amount;
×
161
                    break;
×
162
                case direction_t::right:
×
163
                    retval.x += rhs.hm_amount;
×
164
                    break;
×
165
                case direction_t::up:
×
166
                    retval.y -= rhs.hm_amount;
×
167
                    break;
×
168
                case direction_t::down:
×
169
                    retval.y += rhs.hm_amount;
×
170
                    break;
×
171
            }
172

173
            return retval;
×
174
        }
175

176
        input_point& operator+=(const movement& rhs)
×
177
        {
178
            switch (rhs.hm_dir) {
×
179
                case direction_t::left:
×
180
                    this->x -= rhs.hm_amount;
×
181
                    break;
×
182
                case direction_t::right:
×
183
                    this->x += rhs.hm_amount;
×
184
                    break;
×
185
                case direction_t::up:
×
186
                    this->y -= rhs.hm_amount;
×
187
                    break;
×
188
                case direction_t::down:
×
189
                    this->y += rhs.hm_amount;
×
190
                    break;
×
191
            }
192

193
            return *this;
×
194
        }
195
    };
196

197
    struct selected_range {
198
        input_point sr_start;
199
        input_point sr_end;
200

201
        static selected_range from_mouse(input_point start, input_point end)
202
        {
203
            return {start, end, bounds_t::inclusive};
×
204
        }
205

206
        static selected_range from_key(input_point start, input_point end)
1✔
207
        {
208
            return {start, end, bounds_t::exclusive};
1✔
209
        }
210

211
        static selected_range from_point(input_point ip)
3✔
212
        {
213
            return selected_range{ip};
3✔
214
        }
215

216
        static selected_range from_point_and_movement(input_point ip,
217
                                                      movement m)
218
        {
219
            return {ip + m, ip + m, bounds_t::inclusive};
×
220
        }
221

222
        bool empty() const { return this->sr_start == this->sr_end; }
×
223

224
        bool contains_line(int y) const
3✔
225
        {
226
            return this->sr_start.y <= y && y <= this->sr_end.y;
3✔
227
        }
228

229
        bool contains(const input_point& ip) const
×
230
        {
231
            auto lr_opt = this->range_for_line(ip.y);
×
232
            if (!lr_opt) {
×
233
                return false;
×
234
            }
235

236
            return lr_opt->lr_start <= ip.x && ip.x <= lr_opt->lr_end;
×
237
        }
238

239
        bool contains_exclusive(const input_point& ip) const
×
240
        {
241
            auto lr_opt = this->range_for_line(ip.y);
×
242
            if (!lr_opt) {
×
243
                return false;
×
244
            }
245

246
            return lr_opt->lr_start <= ip.x && ip.x < lr_opt->lr_end;
×
247
        }
248

249
        std::optional<line_range> range_for_line(int y) const
3✔
250
        {
251
            if (!this->contains_line(y)) {
3✔
252
                return std::nullopt;
×
253
            }
254

255
            line_range retval;
3✔
256

257
            if (y > this->sr_start.y) {
3✔
258
                retval.lr_start = 0;
×
259
            } else {
260
                retval.lr_start = this->sr_start.x;
3✔
261
            }
262
            if (y < this->sr_end.y) {
3✔
263
                retval.lr_end = -1;
×
264
            } else {
265
                retval.lr_end = this->sr_end.x;
3✔
266
            }
267

268
            return retval;
3✔
269
        }
270

271
    private:
272
        explicit selected_range(input_point ip) : sr_start{ip}, sr_end{ip} {}
3✔
273

274
        enum class bounds_t {
275
            inclusive,
276
            exclusive,
277
        };
278

279
        selected_range(input_point start, input_point end, bounds_t bounds)
1✔
280
            : sr_start(start < end ? start : end),
1✔
281
              sr_end(start < end
2✔
282
                         ? end
1✔
283
                         : (bounds == bounds_t::inclusive
284
                                ? (start + movement{direction_t::right, 1})
×
285
                                : start))
286
        {
287
        }
1✔
288
    };
289

290
    textinput_curses();
291

292
    textinput_curses(const textinput_curses&) = delete;
293

294
    void set_content(std::string al);
295

296
    void set_height(int height);
297

298
    std::optional<view_curses*> contains(int x, int y) override;
299

300
    bool handle_mouse(mouse_event& me) override;
301

302
    bool handle_help_key(const ncinput& ch);
303

304
    bool handle_search_key(const ncinput& ch);
305

306
    bool handle_key(const ncinput& ch);
307

308
    void content_to_lines(std::string content, int x);
309

310
    void update_lines();
311

312
    void ensure_cursor_visible();
313

314
    void focus();
315

316
    void blur();
317

318
    void abort();
319

320
    std::string get_content(bool trim = false) const;
321

322
    struct dimension_result {
323
        int dr_height{0};
324
        int dr_width{0};
325
        unsigned dr_full_height{0};
326
        unsigned dr_full_width{0};
327
    };
328

329
    dimension_result get_visible_dimensions() const;
330

331
    bool do_update() override;
332

333
    void open_popup_for_completion(line_range crange,
334
                                   std::vector<attr_line_t> possibilities);
335

336
    void open_popup_for_completion(
×
337
        int left, const std::vector<attr_line_t>& possibilities)
338
    {
339
        this->open_popup_for_completion(line_range{left, this->tc_cursor.x},
×
340
                                        possibilities);
341
    }
342

343
    void open_popup_for_history(std::vector<attr_line_t> possibilities);
344

345
    void apply_highlights();
346

347
    std::string replace_selection_no_change(string_fragment sf);
348

349
    void replace_selection(string_fragment sf);
350

351
    void move_cursor_by(movement move);
352

353
    void move_cursor_to(input_point ip);
354

355
    void clamp_point(input_point& ip) const
35✔
356
    {
357
        if (ip.y < 0) {
35✔
358
            ip.y = 0;
×
359
        }
360
        if (ip.y >= (int) this->tc_lines.size()) {
35✔
361
            ip.y = this->tc_lines.size() - 1;
×
362
        }
363
        if (ip.x < 0) {
35✔
364
            ip.x = 0;
×
365
        }
366
        if (ip.x >= (ssize_t) this->tc_lines[ip.y].column_width()) {
35✔
367
            ip.x = this->tc_lines[ip.y].column_width();
35✔
368
        }
369
    }
35✔
370

371
    selected_range clamp_selection(selected_range range)
×
372
    {
373
        this->clamp_point(range.sr_start);
×
374
        this->clamp_point(range.sr_end);
×
375

376
        return range;
×
377
    }
378

379
    void move_cursor_to_next_search_hit();
380

381
    void move_cursor_to_prev_search_hit();
382

383
    void tick(ui_clock::time_point now);
384

385
    int get_cursor_offset() const;
386

387
    input_point get_point_for_offset(int offset) const;
388

389
    bool is_cursor_at_end_of_line() const
×
390
    {
391
        return this->tc_cursor.x
×
392
            == (ssize_t) this->tc_lines[this->tc_cursor.y].column_width();
×
393
    }
394

395
    void clear_inactive_value()
2✔
396
    {
397
        this->tc_inactive_value.clear();
2✔
398
        this->set_needs_update();
2✔
399
    }
2✔
400

UNCOV
401
    void set_inactive_value(const std::string& str)
×
402
    {
UNCOV
403
        this->tc_inactive_value.with_ansi_string(str);
×
UNCOV
404
        this->set_needs_update();
×
405
    }
406

407
    void set_inactive_value(const attr_line_t& al)
5✔
408
    {
409
        this->tc_inactive_value = al;
5✔
410
        this->set_needs_update();
5✔
411
    }
5✔
412

413
    void clear_alt_value()
2✔
414
    {
415
        this->tc_alt_value.clear();
2✔
416
        this->set_needs_update();
2✔
417
    }
2✔
418

419
    void set_alt_value(const std::string& str)
4✔
420
    {
421
        this->tc_alt_value.with_ansi_string(str);
4✔
422
        this->set_needs_update();
4✔
423
    }
4✔
424

425
    void command_down(const ncinput& ch);
426

427
    void command_up(const ncinput& ch);
428

429
    enum class indent_mode_t {
430
        right,
431
        left,
432
        clear_left,
433
    };
434

435
    void command_indent(indent_mode_t mode);
436

437
    void add_mark(input_point pos, const lnav::console::user_message& msg);
438

439
    void sync_to_sysclip() const;
440

441
    enum class mode_t {
442
        editing,
443
        searching,
444
        show_help,
445
    };
446

447
    struct change_entry {
448
        change_entry(selected_range range, std::string content)
1✔
449
            : ce_range(range), ce_content(std::move(content))
1✔
450
        {
451
        }
1✔
452
        selected_range ce_range;
453
        std::string ce_content;
454
    };
455

456
    ncplane* tc_window{nullptr};
457
    size_t tc_max_popup_height{5};
458
    int tc_left{0};
459
    int tc_top{0};
460
    int tc_height{0};
461
    input_point tc_cursor;
462
    int tc_max_cursor_x{0};
463
    mode_t tc_mode{mode_t::editing};
464

465
    static const std::vector<attr_line_t>& unhandled_input();
466
    static const std::vector<attr_line_t>& no_changes();
467
    static const std::vector<attr_line_t>& external_edit_failed();
468

469
    std::optional<std::vector<attr_line_t>> tc_notice;
470
    attr_line_t tc_inactive_value;
471
    attr_line_t tc_alt_value;
472

473
    std::string tc_search;
474
    std::shared_ptr<lnav::pcre2pp::code> tc_search_code;
475
    std::optional<bool> tc_search_found;
476
    input_point tc_search_start_point;
477

478
    text_format_t tc_text_format{text_format_t::TF_PLAINTEXT};
479
    std::vector<attr_line_t> tc_lines;
480
    std::map<input_point, lnav::console::user_message> tc_marks;
481
    lnav::document::metadata tc_doc_meta;
482
    highlight_map_t tc_highlights;
483
    attr_line_t tc_prefix;
484

485
    std::string tc_suggestion;
486

487
    input_point tc_cursor_anchor;
488
    std::optional<selected_range> tc_drag_selection;
489
    std::optional<selected_range> tc_selection;
490

491
    input_point tc_cut_location;
492
    std::deque<std::string> tc_clipboard;
493
    std::optional<selected_range> tc_complete_range;
494
    textview_curses tc_popup;
495
    plain_text_source tc_popup_source;
496

497
    std::vector<change_entry> tc_change_log;
498

499
    textview_curses tc_help_view;
500
    plain_text_source tc_help_source;
501

502
    enum class popup_type_t {
503
        none,
504
        completion,
505
        history,
506
    };
507

508
    popup_type_t tc_popup_type{popup_type_t::none};
509

510
    std::optional<ui_clock::time_point> tc_last_tick_after_input;
511
    bool tc_timeout_fired{false};
512
    bool tc_in_popup_change{false};
513
    bool tc_abort_requested{false};
514

515
    std::function<void(textinput_curses&)> tc_on_help;
516
    std::function<void(textinput_curses&)> tc_on_focus;
517
    std::function<void(textinput_curses&)> tc_on_blur;
518
    std::function<void(textinput_curses&)> tc_on_abort;
519
    std::function<void(textinput_curses&)> tc_on_change;
520
    std::function<void(textinput_curses&)> tc_on_popup_change;
521
    std::function<void(textinput_curses&)> tc_on_popup_cancel;
522
    std::function<void(textinput_curses&)> tc_on_completion_request;
523
    std::function<void(textinput_curses&)> tc_on_completion;
524
    std::function<void(textinput_curses&)> tc_on_history_list;
525
    std::function<void(textinput_curses&)> tc_on_history_search;
526
    std::function<void(textinput_curses&)> tc_on_timeout;
527
    std::function<void(textinput_curses&)> tc_on_reformat;
528
    std::function<void(textinput_curses&)> tc_on_perform;
529
    std::function<void(textinput_curses&)> tc_on_external_open;
530
};
531

532
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc