1 #include "terminal_ui.hh"
2 
3 #include "display_buffer.hh"
4 #include "event_manager.hh"
5 #include "exception.hh"
6 #include "file.hh"
7 #include "keys.hh"
8 #include "ranges.hh"
9 #include "string_utils.hh"
10 #include "diff.hh"
11 
12 #include <algorithm>
13 
14 #include <fcntl.h>
15 #include <csignal>
16 #include <sys/ioctl.h>
17 #include <unistd.h>
18 #include <strings.h>
19 
20 namespace Kakoune
21 {
22 
23 using std::min;
24 using std::max;
25 
fix_atom_text(StringView str)26 static String fix_atom_text(StringView str)
27 {
28     String res;
29     auto pos = str.begin();
30     for (auto it = str.begin(), end = str.end(); it != end; ++it)
31     {
32         char c = *it;
33         if (c >= 0 and c <= 0x1F)
34         {
35             res += StringView{pos, it};
36             res += String{Codepoint{(uint32_t)(0x2400 + c)}};
37             pos = it+1;
38         }
39     }
40     res += StringView{pos, str.end()};
41     return res;
42 }
43 
44 struct TerminalUI::Window::Line
45 {
46     struct Atom
47     {
48         String text;
49         ColumnCount skip = 0;
50         Face face;
51 
lengthKakoune::TerminalUI::Window::Line::Atom52         ColumnCount length() const { return text.column_length() + skip; }
resizeKakoune::TerminalUI::Window::Line::Atom53         void resize(ColumnCount size)
54         {
55             auto it = text.begin(), end = text.end();
56             while (it != end and size > 0)
57                 size -= codepoint_width(utf8::read_codepoint(it, end));
58 
59             if (size < 0) // possible if resizing to the middle of a double-width codepoint
60             {
61                 kak_assert(size == -1);
62                 utf8::to_previous(it, text.begin());
63                 skip = 1;
64             }
65             else
66                 skip = size;
67             text.resize(it - text.begin(), 0);
68         }
69 
operator ==Kakoune::TerminalUI::Window::Line70         friend bool operator==(const Atom& lhs, const Atom& rhs) { return lhs.text == rhs.text and lhs.skip == rhs.skip and lhs.face == rhs.face; }
operator !=Kakoune::TerminalUI::Window::Line71         friend bool operator!=(const Atom& lhs, const Atom& rhs) { return not (lhs == rhs); }
hash_valueKakoune::TerminalUI::Window::Line72         friend size_t hash_value(const Atom& atom) { return hash_values(atom.text, atom.skip, atom.face); }
73     };
74 
appendKakoune::TerminalUI::Window::Line75     void append(StringView text, ColumnCount skip, Face face)
76     {
77         if (not atoms.empty() and atoms.back().face == face and (atoms.back().skip == 0 or text.empty()))
78         {
79             atoms.back().text += fix_atom_text(text);
80             atoms.back().skip += skip;
81         }
82         else
83             atoms.push_back({fix_atom_text(text), skip, face});
84     }
85 
resizeKakoune::TerminalUI::Window::Line86     void resize(ColumnCount width)
87     {
88         auto it = atoms.begin();
89         ColumnCount column = 0;
90         for (; it != atoms.end() and column < width; ++it)
91             column += it->length();
92 
93         if (column < width)
94             append({}, width - column, atoms.empty() ? Face{} : atoms.back().face);
95         else
96         {
97             atoms.erase(it, atoms.end());
98             if (column > width)
99                 atoms.back().resize(atoms.back().length() - (column - width));
100         }
101     }
102 
erase_rangeKakoune::TerminalUI::Window::Line103     Vector<Atom>::iterator erase_range(ColumnCount pos, ColumnCount len)
104     {
105         struct Pos{ Vector<Atom>::iterator it; ColumnCount column; };
106         auto find_col = [pos=0_col, it=atoms.begin(), end=atoms.end()](ColumnCount col) mutable {
107             for (; it != end; ++it)
108             {
109                 auto atom_len = it->length();
110                 if (pos + atom_len >= col)
111                     return Pos{it, col - pos};
112                 pos += atom_len;
113             }
114             return Pos{it, 0_col};
115         };
116         Pos begin = find_col(pos);
117         Pos end = find_col(pos+len);
118 
119         auto make_tail = [](const Atom& atom, ColumnCount from) {
120             auto it = atom.text.begin(), end = atom.text.end();
121             while (it != end and from > 0)
122                 from -= codepoint_width(utf8::read_codepoint(it, end));
123 
124             if (from < 0) // can happen if tail starts in the middle of a double-width codepoint
125             {
126                 kak_assert(from == -1);
127                 return Atom{" " + StringView{it, end}, atom.skip, atom.face};
128             }
129             return Atom{{it, end}, atom.skip - from, atom.face};
130         };
131 
132         if (begin.it == end.it)
133         {
134             Atom tail = make_tail(*begin.it, end.column);
135             begin.it->resize(begin.column);
136             return (tail.text.empty() and tail.skip == 0) ? begin.it+1 : atoms.insert(begin.it+1, tail);
137         }
138 
139         begin.it->resize(begin.column);
140         if (end.column > 0)
141         {
142             if (end.column == end.it->length())
143                 ++end.it;
144             else
145                 *end.it = make_tail(*end.it, end.column);
146         }
147         return atoms.erase(begin.it+1, end.it);
148     }
149 
150     Vector<Atom> atoms;
151 };
152 
create(const DisplayCoord & p,const DisplayCoord & s)153 void TerminalUI::Window::create(const DisplayCoord& p, const DisplayCoord& s)
154 {
155     kak_assert(p.line >= 0 and p.column >= 0);
156     kak_assert(s.line >= 0 and s.column >= 0);
157     pos = p;
158     size = s;
159     lines.reset(new Line[(int)size.line]);
160 }
161 
destroy()162 void TerminalUI::Window::destroy()
163 {
164     pos = DisplayCoord{};
165     size = DisplayCoord{};
166     lines.reset();
167 }
168 
blit(Window & target)169 void TerminalUI::Window::blit(Window& target)
170 {
171     kak_assert(pos.line < target.size.line);
172     LineCount line_index = pos.line;
173     for (auto& line : ArrayView{lines.get(), (size_t)size.line})
174     {
175         line.resize(size.column);
176         auto& target_line = target.lines[(size_t)line_index];
177         target_line.resize(target.size.column);
178         target_line.atoms.insert(target_line.erase_range(pos.column, size.column), line.atoms.begin(), line.atoms.end());
179         if (++line_index == target.size.line)
180             break;
181     }
182 }
183 
draw(DisplayCoord pos,ConstArrayView<DisplayAtom> atoms,const Face & default_face)184 void TerminalUI::Window::draw(DisplayCoord pos,
185                               ConstArrayView<DisplayAtom> atoms,
186                               const Face& default_face)
187 {
188     if (pos.line >= size.line) // We might receive an out of date draw command after a resize
189         return;
190 
191     lines[(size_t)pos.line].resize(pos.column);
192     for (const DisplayAtom& atom : atoms)
193     {
194         StringView content = atom.content();
195         if (content.empty())
196             continue;
197 
198         auto face = merge_faces(default_face, atom.face);
199         if (content.back() == '\n')
200             lines[(int)pos.line].append(content.substr(0, content.length()-1), 1, face);
201         else
202             lines[(int)pos.line].append(content, 0, face);
203         pos.column += content.column_length();
204     }
205 
206     if (pos.column < size.column)
207         lines[(int)pos.line].append({}, size.column - pos.column, default_face);
208 }
209 
210 struct Writer : BufferedWriter<>
211 {
212     using Writer::BufferedWriter::BufferedWriter;
213     ~Writer() noexcept(false) = default;
214 };
215 
216 template<typename... Args>
format_with(Writer & writer,StringView format,Args &&...args)217 static void format_with(Writer& writer, StringView format, Args&&... args)
218 {
219     format_with([&](StringView s) { writer.write(s); }, format, std::forward<Args>(args)...);
220 }
221 
set_face(const Face & face,Writer & writer)222 void TerminalUI::Screen::set_face(const Face& face, Writer& writer)
223 {
224     static constexpr int fg_table[]{ 39, 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97 };
225     static constexpr int bg_table[]{ 49, 40, 41, 42, 43, 44, 45, 46, 47, 100, 101, 102, 103, 104, 105, 106, 107 };
226     static constexpr int ul_table[]{ 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
227     static constexpr const char* attr_table[]{ "0", "4", "4:3", "7", "5", "1", "2", "3", "9" };
228 
229     auto set_color = [&](bool fg, const Color& color, bool join) {
230         if (join)
231             writer.write(";");
232         if (color.isRGB())
233             format_with(writer, "{};2;{};{};{}", fg ? 38 : 48, color.r, color.g, color.b);
234         else
235             format_with(writer, "{}", (fg ? fg_table : bg_table)[(int)(char)color.color]);
236     };
237 
238     if (m_active_face == face)
239         return;
240 
241     writer.write("\033[");
242     bool join = false;
243     if (face.attributes != m_active_face.attributes)
244     {
245         for (int i = 0; i < std::size(attr_table); ++i)
246         {
247             if (face.attributes & (Attribute)(1 << i))
248                 format_with(writer, ";{}", attr_table[i]);
249         }
250         m_active_face.fg = m_active_face.bg = Color::Default;
251         join = true;
252     }
253     if (m_active_face.fg != face.fg)
254     {
255         set_color(true, face.fg, join);
256         join = true;
257     }
258     if (m_active_face.bg != face.bg)
259     {
260         set_color(false, face.bg, join);
261         join = true;
262     }
263     if (m_active_face.underline != face.underline)
264     {
265         if (join)
266             writer.write(";");
267         if (face.underline != Color::Default)
268         {
269             if (face.underline.isRGB())
270                 format_with(writer, "58:2::{}:{}:{}", face.underline.r, face.underline.g, face.underline.b);
271             else
272                 format_with(writer, "58:5:{}", ul_table[(int)(char)face.underline.color]);
273         }
274         else
275             format_with(writer, "59");
276     }
277     writer.write("m");
278 
279     m_active_face = face;
280 }
281 
output(bool force,bool synchronized,Writer & writer)282 void TerminalUI::Screen::output(bool force, bool synchronized, Writer& writer)
283 {
284     if (not lines)
285         return;
286 
287     if (force)
288     {
289         std::fill_n(hashes.get(), (size_t)size.line, 0);
290         writer.write("\033[m");
291         m_active_face = Face{};
292     }
293 
294     auto hash_line = [](const Line& line) {
295         return (hash_value(line.atoms) << 1) | 1; // ensure non-zero
296     };
297 
298     auto output_line = [&](const Line& line) {
299         ColumnCount pending_move = 0;
300         for (auto& [text, skip, face] : line.atoms)
301         {
302             if (text.empty() and skip == 0)
303                 continue;
304 
305             if (pending_move != 0)
306             {
307                 format_with(writer, "\033[{}C", (int)pending_move);
308                 pending_move = 0;
309             }
310             set_face(face, writer);
311             writer.write(text);
312             if (skip > 3 and face.attributes == Attribute{})
313             {
314                 writer.write("\033[K");
315                 pending_move = skip;
316             }
317             else if (skip > 0)
318                 writer.write(String{' ', skip});
319         }
320     };
321 
322     if (synchronized)
323     {
324         writer.write("\033[?2026h"); // begin synchronized update
325 
326         struct Change { int keep; int add; int del; };
327         Vector<Change> changes{Change{}};
328         auto new_hashes = ArrayView{lines.get(), (size_t)size.line} | transform(hash_line);
329         for_each_diff(hashes.get(), (int)size.line,
330                       new_hashes.begin(), (int)size.line,
331                       [&changes](DiffOp op, int len) mutable {
332             switch (op)
333             {
334                 case DiffOp::Keep:
335                     changes.push_back({len, 0, 0});
336                     break;
337                 case DiffOp::Add:
338                     changes.back().add += len;
339                     break;
340                 case DiffOp::Remove:
341                     changes.back().del += len;
342                     break;
343             }
344         });
345         std::copy(new_hashes.begin(), new_hashes.end(), hashes.get());
346 
347         int line = 0;
348         for (auto& change : changes)
349         {
350             line += change.keep;
351             if (int del = change.del - change.add; del > 0)
352             {
353                 format_with(writer, "\033[{}H\033[{}M", line + 1, del);
354                 line -= del;
355             }
356             line += change.del;
357         }
358 
359         line = 0;
360         for (auto& change : changes)
361         {
362             line += change.keep;
363             for (int i = 0; i < change.add; ++i)
364             {
365                 if (int add = change.add - change.del; i == 0 and add > 0)
366                     format_with(writer, "\033[{}H\033[{}L", line + 1, add);
367                 else
368                     format_with(writer, "\033[{}H", line + 1);
369 
370                 output_line(lines[line++]);
371             }
372         }
373 
374         writer.write("\033[?2026l"); // end synchronized update
375     }
376     else
377     {
378         for (int line = 0; line < (int)size.line; ++line)
379         {
380             auto hash = hash_line(lines[line]);
381             if (hash == hashes[line])
382                 continue;
383             hashes[line] = hash;
384 
385             format_with(writer, "\033[{}H", line + 1);
386             output_line(lines[line]);
387         }
388     }
389 }
390 
391 constexpr int TerminalUI::default_shift_function_key;
392 
393 static constexpr StringView assistant_cat[] =
394     { R"(  ___            )",
395       R"( (__ \           )",
396       R"(   / /          ╭)",
397       R"(  .' '·.        │)",
398       R"( '      ”       │)",
399       R"( ╰       /\_/|  │)",
400       R"(  | .         \ │)",
401       R"(  ╰_J`    | | | ╯)",
402       R"(      ' \__- _/  )",
403       R"(      \_\   \_\  )",
404       R"(                 )"};
405 
406 static constexpr StringView assistant_clippy[] =
407     { " ╭──╮   ",
408       " │  │   ",
409       " @  @  ╭",
410       " ││ ││ │",
411       " ││ ││ ╯",
412       " │╰─╯│  ",
413       " ╰───╯  ",
414       "        " };
415 
416 static constexpr StringView assistant_dilbert[] =
417     { R"(  დოოოოოდ   )",
418       R"(  |     |   )",
419       R"(  |     |  ╭)",
420       R"(  |-ᱛ ᱛ-|  │)",
421       R"( Ͼ   ∪   Ͽ │)",
422       R"(  |     |  ╯)",
423       R"( ˏ`-.ŏ.-´ˎ  )",
424       R"(     @      )",
425       R"(      @     )",
426       R"(            )"};
427 
sq(T x)428 template<typename T> T sq(T x) { return x * x; }
429 
430 static sig_atomic_t resize_pending = 0;
431 static sig_atomic_t stdin_closed = 0;
432 
433 template<sig_atomic_t* signal_flag>
signal_handler(int)434 static void signal_handler(int)
435 {
436     *signal_flag = 1;
437     EventManager::instance().force_signal(0);
438 }
439 
TerminalUI()440 TerminalUI::TerminalUI()
441     : m_cursor{CursorMode::Buffer, {}},
442       m_stdin_watcher{STDIN_FILENO, FdEvents::Read, EventMode::Urgent,
__anonc48239490802() 443                       [this](FDWatcher&, FdEvents, EventMode) {
444         if (not m_on_key)
445             return;
446 
447         while (auto key = get_next_key())
448         {
449             if (key == ctrl('z'))
450                 kill(0, SIGTSTP); // We suspend at this line
451             else
452                 m_on_key(*key);
453         }
454       }},
455       m_assistant(assistant_clippy)
456 {
457     if (not isatty(1))
458         throw runtime_error("stdout is not a tty");
459 
460     tcgetattr(STDIN_FILENO, &m_original_termios);
461 
462     setup_terminal();
463     set_raw_mode();
464     enable_mouse(true);
465 
466     set_signal_handler(SIGWINCH, &signal_handler<&resize_pending>);
467     set_signal_handler(SIGHUP, &signal_handler<&stdin_closed>);
__anonc48239490902(int)468     set_signal_handler(SIGTSTP, [](int){ TerminalUI::instance().suspend(); });
469 
470     check_resize(true);
471     redraw(false);
472 }
473 
~TerminalUI()474 TerminalUI::~TerminalUI()
475 {
476     enable_mouse(false);
477     restore_terminal();
478     tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
479     set_signal_handler(SIGWINCH, SIG_DFL);
480     set_signal_handler(SIGHUP, SIG_DFL);
481     set_signal_handler(SIGTSTP, SIG_DFL);
482 }
483 
suspend()484 void TerminalUI::suspend()
485 {
486     bool mouse_enabled = m_mouse_enabled;
487     enable_mouse(false);
488     tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
489     restore_terminal();
490 
491     auto current = set_signal_handler(SIGTSTP, SIG_DFL);
492     sigset_t unblock_sigtstp, old_mask;
493     sigemptyset(&unblock_sigtstp);
494     sigaddset(&unblock_sigtstp, SIGTSTP);
495     sigprocmask(SIG_UNBLOCK, &unblock_sigtstp, &old_mask);
496 
497     raise(SIGTSTP); // suspend here
498 
499     set_signal_handler(SIGTSTP, current);
500     sigprocmask(SIG_SETMASK, &old_mask, nullptr);
501 
502     setup_terminal();
503     check_resize(true);
504     set_raw_mode();
505     enable_mouse(mouse_enabled);
506 
507     refresh(true);
508 }
509 
set_raw_mode() const510 void TerminalUI::set_raw_mode() const
511 {
512     termios attr = m_original_termios;
513     attr.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
514     attr.c_oflag &= ~OPOST;
515     attr.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
516     attr.c_lflag |= NOFLSH;
517     attr.c_cflag &= ~(CSIZE | PARENB);
518     attr.c_cflag |= CS8;
519     attr.c_cc[VMIN] = attr.c_cc[VTIME] = 0;
520 
521     tcsetattr(STDIN_FILENO, TCSANOW, &attr);
522 }
523 
redraw(bool force)524 void TerminalUI::redraw(bool force)
525 {
526     m_window.blit(m_screen);
527 
528     if (m_menu.columns != 0 or m_menu.pos.column > m_status_len)
529         m_menu.blit(m_screen);
530 
531     m_info.blit(m_screen);
532 
533     Writer writer{STDOUT_FILENO};
534     m_screen.output(force, (bool)m_synchronized, writer);
535 
536     auto set_cursor_pos = [&](DisplayCoord c) {
537         format_with(writer, "\033[{};{}H", (int)c.line + 1, (int)c.column + 1);
538     };
539     if (m_cursor.mode == CursorMode::Prompt)
540         set_cursor_pos({m_status_on_top ? 0 : m_dimensions.line, m_cursor.coord.column});
541     else
542         set_cursor_pos(m_cursor.coord + content_line_offset());
543 }
544 
set_cursor(CursorMode mode,DisplayCoord coord)545 void TerminalUI::set_cursor(CursorMode mode, DisplayCoord coord)
546 {
547     m_cursor = Cursor{mode, coord};
548 }
549 
refresh(bool force)550 void TerminalUI::refresh(bool force)
551 {
552     if (m_dirty or force)
553         redraw(force);
554     m_dirty = false;
555 }
556 
557 static const DisplayLine empty_line = { String(" "), {} };
558 
draw(const DisplayBuffer & display_buffer,const Face & default_face,const Face & padding_face)559 void TerminalUI::draw(const DisplayBuffer& display_buffer,
560                       const Face& default_face,
561                       const Face& padding_face)
562 {
563     check_resize();
564 
565     const DisplayCoord dim = dimensions();
566     const LineCount line_offset = content_line_offset();
567     LineCount line_index = line_offset;
568     for (const DisplayLine& line : display_buffer.lines())
569         m_window.draw(line_index++, line.atoms(), default_face);
570 
571     auto face = merge_faces(default_face, padding_face);
572 
573     DisplayAtom padding{String{m_padding_char, m_padding_fill ? dim.column : 1}};
574 
575     while (line_index < dim.line + line_offset)
576         m_window.draw(line_index++, padding, face);
577 
578     m_dirty = true;
579 }
580 
draw_status(const DisplayLine & status_line,const DisplayLine & mode_line,const Face & default_face)581 void TerminalUI::draw_status(const DisplayLine& status_line,
582                              const DisplayLine& mode_line,
583                              const Face& default_face)
584 {
585     const LineCount status_line_pos = m_status_on_top ? 0 : m_dimensions.line;
586     m_window.draw(status_line_pos, status_line.atoms(), default_face);
587 
588     const auto mode_len = mode_line.length();
589     m_status_len = status_line.length();
590     const auto remaining = m_dimensions.column - m_status_len;
591     if (mode_len < remaining)
592     {
593         ColumnCount col = m_dimensions.column - mode_len;
594         m_window.draw({status_line_pos, col}, mode_line.atoms(), default_face);
595     }
596     else if (remaining > 2)
597     {
598         DisplayLine trimmed_mode_line = mode_line;
599         trimmed_mode_line.trim(mode_len + 2 - remaining, remaining - 2);
600         trimmed_mode_line.insert(trimmed_mode_line.begin(), { "…", {} });
601         kak_assert(trimmed_mode_line.length() == remaining - 1);
602 
603         ColumnCount col = m_dimensions.column - remaining + 1;
604         m_window.draw({status_line_pos, col}, trimmed_mode_line.atoms(), default_face);
605     }
606 
607     if (m_set_title)
608     {
609         Writer writer{STDOUT_FILENO};
610         constexpr char suffix[] = " - Kakoune\007";
611         writer.write("\033]2;");
612         // Fill title escape sequence buffer, removing non ascii characters
613         for (auto& atom : mode_line)
614         {
615             const auto str = atom.content();
616             for (auto it = str.begin(), end = str.end(); it != end; utf8::to_next(it, end))
617                 writer.write((*it >= 0x20 and *it <= 0x7e) ? *it : '?');
618         }
619         writer.write(suffix);
620     }
621 
622     m_dirty = true;
623 }
624 
check_resize(bool force)625 void TerminalUI::check_resize(bool force)
626 {
627     if (not force and not resize_pending)
628         return;
629 
630     resize_pending = 0;
631 
632     const int fd = open("/dev/tty", O_RDWR);
633     if (fd < 0)
634         return;
635     auto close_fd = on_scope_end([fd]{ ::close(fd); });
636 
637     DisplayCoord terminal_size{24_line, 80_col};
638     if (winsize ws; ioctl(fd, TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0)
639         terminal_size = {ws.ws_row, ws.ws_col};
640 
641     const bool info = (bool)m_info;
642     const bool menu = (bool)m_menu;
643     if (m_window) m_window.destroy();
644     if (info) m_info.destroy();
645     if (menu) m_menu.destroy();
646 
647     m_window.create({0, 0}, terminal_size);
648     m_screen.create({0, 0}, terminal_size);
649     m_screen.hashes.reset(new size_t[(int)terminal_size.line]{});
650     kak_assert(m_window);
651 
652     m_dimensions = terminal_size - 1_line;
653 
654     // if (char* csr = tigetstr((char*)"csr"))
655     //     putp(tparm(csr, 0, ws.ws_row));
656 
657     if (menu)
658         menu_show(Vector<DisplayLine>(std::move(m_menu.items)),
659                   m_menu.anchor, m_menu.fg, m_menu.bg, m_menu.style);
660     if (info)
661         info_show(m_info.title, m_info.content, m_info.anchor, m_info.face, m_info.style);
662 
663     set_resize_pending();
664 }
665 
get_next_key()666 Optional<Key> TerminalUI::get_next_key()
667 {
668     if (stdin_closed)
669     {
670         set_signal_handler(SIGWINCH, SIG_DFL);
671         set_signal_handler(SIGHUP, SIG_DFL);
672         if (m_window)
673             m_window.destroy();
674         m_stdin_watcher.disable();
675         return {};
676     }
677 
678     check_resize();
679 
680     if (m_resize_pending)
681     {
682         m_resize_pending = false;
683         return resize(dimensions());
684     }
685 
686     static auto get_char = []() -> Optional<unsigned char> {
687         if (not fd_readable(STDIN_FILENO))
688             return {};
689 
690         if (unsigned char c = 0; read(STDIN_FILENO, &c, 1) == 1)
691             return c;
692 
693         stdin_closed = 1;
694         return {};
695     };
696 
697     const auto c = get_char();
698     if (not c)
699         return {};
700 
701     static constexpr auto control = [](char c) { return c & 037; };
702 
703     auto convert = [this](Codepoint c) -> Codepoint {
704         if (c == control('m') or c == control('j'))
705             return Key::Return;
706         if (c == control('i'))
707             return Key::Tab;
708         if (c == m_original_termios.c_cc[VERASE])
709             return Key::Backspace;
710         if (c == 127) // when it's not backspace
711             return Key::Delete;
712         if (c == 27)
713             return Key::Escape;
714         return c;
715     };
716     auto parse_key = [&convert](unsigned char c) -> Key {
717         if (Codepoint cp = convert(c); cp > 255)
718             return Key{cp};
719         // Special case: you can type NUL with Ctrl-2 or Ctrl-Shift-2 or
720         // Ctrl-Backtick, but the most straightforward way is Ctrl-Space.
721         if (c == 0)
722             return ctrl(' ');
723         // Represent Ctrl-letter combinations in lower-case, to be clear
724         // that Shift is not involved.
725         if (c < 27)
726             return ctrl(c - 1 + 'a');
727         // Represent Ctrl-symbol combinations in "upper-case", as they are
728         // traditionally-rendered.
729         // Note that Escape is handled elsewhere.
730         if (c < 32)
731             return ctrl(c - 1 + 'A');
732 
733        struct Sentinel{};
734        struct CharIterator
735        {
736            unsigned char operator*() { if (not c) c = get_char().value_or((unsigned char)0); return *c; }
737            CharIterator& operator++() { c.reset(); return *this; }
738            bool operator==(const Sentinel&) const { return false; }
739            Optional<unsigned char> c;
740        };
741        return Key{utf8::codepoint(CharIterator{c}, Sentinel{})};
742     };
743 
744     auto parse_mask = [](int mask) {
745         Key::Modifiers mod = Key::Modifiers::None;
746         if (mask & 1)
747             mod |= Key::Modifiers::Shift;
748         if (mask & 2)
749             mod |= Key::Modifiers::Alt;
750         if (mask & 4)
751             mod |= Key::Modifiers::Control;
752         return mod;
753     };
754 
755     auto parse_csi = [this, &convert, &parse_mask]() -> Optional<Key> {
756         auto next_char = [] { return get_char().value_or((unsigned char)0xff); };
757         int params[16][4] = {};
758         auto c = next_char();
759         char private_mode = 0;
760         if (c == '?' or c == '<' or c == '=' or c == '>')
761         {
762             private_mode = c;
763             c = next_char();
764         }
765         for (int count = 0, subcount = 0; count < 16 and c >= 0x30 && c <= 0x3f; c = next_char())
766         {
767             if (isdigit(c))
768                 params[count][subcount] = params[count][subcount] * 10 + c - '0';
769             else if (c == ':' && subcount < 3)
770                 ++subcount;
771             else if (c == ';')
772             {
773                 ++count;
774                 subcount = 0;
775             }
776             else
777                 return {};
778         }
779         if (c != '$' and (c < 0x40 or c > 0x7e))
780             return {};
781 
782         auto mouse_button = [this](Key::Modifiers mod, Key::MouseButton button, Codepoint coord, bool release) {
783             auto mask = 1 << (int)button;
784             if (not release)
785             {
786                 mod |= (m_mouse_state & mask) ? Key::Modifiers::MousePos : Key::Modifiers::MousePress;
787                 m_mouse_state |= mask;
788             }
789             else
790             {
791                 mod |= Key::Modifiers::MouseRelease;
792                 m_mouse_state &= ~mask;
793             }
794             return Key{mod | Key::to_modifier(button), coord};
795         };
796 
797         auto mouse_scroll = [this](Key::Modifiers mod, bool down) -> Key {
798             return {mod | Key::Modifiers::Scroll,
799                     (Codepoint)((down ? 1 : -1) * m_wheel_scroll_amount)};
800         };
801 
802         auto masked_key = [&](Codepoint key, Codepoint shifted_key = 0) {
803             int mask = std::max(params[1][0] - 1, 0);
804             Key::Modifiers modifiers = parse_mask(mask);
805             if (shifted_key != 0 and (modifiers & Key::Modifiers::Shift))
806             {
807                 modifiers &= ~Key::Modifiers::Shift;
808                 key = shifted_key;
809             }
810             return Key{modifiers, key};
811         };
812 
813         switch (c)
814         {
815         case '$':
816             if (private_mode == '?' and next_char() == 'y') // DECRPM
817             {
818                 if (params[0][0] == 2026)
819                     m_synchronized.supported = (params[1][0] == 1 or params[1][0] == 2);
820                 return {Key::Invalid};
821             }
822             switch (params[0][0])
823             {
824             case 23: case 24:
825                 return Key{Key::Modifiers::Shift, Key::F11 + params[0][0] - 23}; // rxvt style
826             }
827             return {};
828         case 'A': return masked_key(Key::Up);
829         case 'B': return masked_key(Key::Down);
830         case 'C': return masked_key(Key::Right);
831         case 'D': return masked_key(Key::Left);
832         case 'E': return masked_key('5');        // Numeric keypad 5
833         case 'F': return masked_key(Key::End);   // PC/xterm style
834         case 'H': return masked_key(Key::Home);  // PC/xterm style
835         case 'P': return masked_key(Key::F1);
836         case 'Q': return masked_key(Key::F2);
837         case 'R': return masked_key(Key::F3);
838         case 'S': return masked_key(Key::F4);
839         case '~':
840             switch (params[0][0])
841             {
842             case 1: return masked_key(Key::Home);     // VT220/tmux style
843             case 2: return masked_key(Key::Insert);
844             case 3: return masked_key(Key::Delete);
845             case 4: return masked_key(Key::End);      // VT220/tmux style
846             case 5: return masked_key(Key::PageUp);
847             case 6: return masked_key(Key::PageDown);
848             case 7: return masked_key(Key::Home);     // rxvt style
849             case 8: return masked_key(Key::End);      // rxvt style
850             case 11: case 12: case 13: case 14: case 15:
851                 return masked_key(Key::F1 + params[0][0] - 11);
852             case 17: case 18: case 19: case 20: case 21:
853                 return masked_key(Key::F6 + params[0][0] - 17);
854             case 23: case 24:
855                 return masked_key(Key::F11 + params[0][0] - 23);
856             case 25: case 26:
857                 return Key{Key::Modifiers::Shift, Key::F3 + params[0][0] - 25}; // rxvt style
858             case 28: case 29:
859                 return Key{Key::Modifiers::Shift, Key::F5 + params[0][0] - 28}; // rxvt style
860             case 31: case 32:
861                 return Key{Key::Modifiers::Shift, Key::F7 + params[0][0] - 31}; // rxvt style
862             case 33: case 34:
863                 return Key{Key::Modifiers::Shift, Key::F9 + params[0][0] - 33}; // rxvt style
864             }
865             return {};
866         case 'u':
867             return masked_key(convert(static_cast<Codepoint>(params[0][0])),
868                               convert(static_cast<Codepoint>(params[0][1])));
869         case 'Z': return shift(Key::Tab);
870         case 'I': return {Key::FocusIn};
871         case 'O': return {Key::FocusOut};
872         case 'M': case 'm':
873             const bool sgr = private_mode == '<';
874             if (not sgr and c != 'M')
875                 return {};
876 
877             const Codepoint b = sgr ? params[0][0] : next_char() - 32;
878             const int x = (sgr ? params[1][0] : next_char() - 32) - 1;
879             const int y = (sgr ? params[2][0] : next_char() - 32) - 1;
880             auto coord = encode_coord({y - content_line_offset(), x});
881             Key::Modifiers mod = parse_mask((b >> 2) & 0x7);
882             switch (const int code = b & 0x43; code)
883             {
884             case 0: case 1: case 2:
885                 return mouse_button(mod, Key::MouseButton{code}, coord, c == 'm');
886             case 3:
887                 if (sgr)
888                     return {};
889                 else if (int guess = ffs(m_mouse_state) - 1; 0 <= guess and guess < 3)
890                     return mouse_button(mod, Key::MouseButton{guess}, coord, true);
891                 break;
892             case 64: return mouse_scroll(mod, false);
893             case 65: return mouse_scroll(mod, true);
894             }
895             return Key{Key::Modifiers::MousePos, coord};
896         }
897         return {};
898     };
899 
900     auto parse_ss3 = [&parse_mask]() -> Optional<Key> {
901         int raw_mask = 0;
902         char code = '0';
903         do {
904             raw_mask = raw_mask * 10 + (code - '0');
905             code = get_char().value_or((unsigned char)0xff);
906         } while (code >= '0' and code <= '9');
907         Key::Modifiers mod = parse_mask(std::max(raw_mask - 1, 0));
908 
909         switch (code)
910         {
911         case ' ': return Key{mod, ' '};
912         case 'A': return Key{mod, Key::Up};
913         case 'B': return Key{mod, Key::Down};
914         case 'C': return Key{mod, Key::Right};
915         case 'D': return Key{mod, Key::Left};
916         case 'F': return Key{mod, Key::End};
917         case 'H': return Key{mod, Key::Home};
918         case 'I': return Key{mod, Key::Tab};
919         case 'M': return Key{mod, Key::Return};
920         case 'P': return Key{mod, Key::F1};
921         case 'Q': return Key{mod, Key::F2};
922         case 'R': return Key{mod, Key::F3};
923         case 'S': return Key{mod, Key::F4};
924         case 'X': return Key{mod, '='};
925         case 'j': return Key{mod, '*'};
926         case 'k': return Key{mod, '+'};
927         case 'l': return Key{mod, ','};
928         case 'm': return Key{mod, '-'};
929         case 'n': return Key{mod, '.'};
930         case 'o': return Key{mod, '/'};
931         case 'p': return Key{mod, '0'};
932         case 'q': return Key{mod, '1'};
933         case 'r': return Key{mod, '2'};
934         case 's': return Key{mod, '3'};
935         case 't': return Key{mod, '4'};
936         case 'u': return Key{mod, '5'};
937         case 'v': return Key{mod, '6'};
938         case 'w': return Key{mod, '7'};
939         case 'x': return Key{mod, '8'};
940         case 'y': return Key{mod, '9'};
941         default: return {};
942         }
943     };
944 
945     if (*c != 27)
946         return parse_key(*c);
947 
948     if (auto next = get_char())
949     {
950         if (*next == '[') // potential CSI
951             return parse_csi().value_or(alt('['));
952         if (*next == 'O') // potential SS3
953             return parse_ss3().value_or(alt('O'));
954         return alt(parse_key(*next));
955     }
956     return Key{Key::Escape};
957 }
958 
959 template<typename T>
div_round_up(T a,T b)960 T div_round_up(T a, T b)
961 {
962     return (a - T(1)) / b + T(1);
963 }
964 
draw_menu()965 void TerminalUI::draw_menu()
966 {
967     // menu show may have not created the window if it did not fit.
968     // so be tolerant.
969     if (not m_menu)
970         return;
971 
972     const int item_count = (int)m_menu.items.size();
973     if (m_menu.columns == 0)
974     {
975         const auto win_width = m_menu.size.column - 4;
976         kak_assert(m_menu.size.line == 1);
977         ColumnCount pos = 0;
978 
979         m_menu.draw({0, 0}, DisplayAtom(m_menu.first_item > 0 ? "< " : ""), m_menu.bg);
980 
981         int i = m_menu.first_item;
982         for (; i < item_count and pos < win_width; ++i)
983         {
984             const DisplayLine& item = m_menu.items[i];
985             const ColumnCount item_width = item.length();
986             auto& face = i == m_menu.selected_item ? m_menu.fg : m_menu.bg;
987             m_menu.draw({0, pos+2}, item.atoms(), face);
988             if (pos + item_width >= win_width)
989                 m_menu.draw({0, win_width+2}, DisplayAtom("…"), m_menu.bg);
990             pos += item_width + 1;
991         }
992 
993         if (i != item_count)
994             m_menu.draw({0, win_width+3}, DisplayAtom(">"), m_menu.bg);
995 
996         m_dirty = true;
997         return;
998     }
999 
1000     const LineCount menu_lines = div_round_up(item_count, m_menu.columns);
1001     const LineCount win_height = m_menu.size.line;
1002     kak_assert(win_height <= menu_lines);
1003 
1004     const ColumnCount column_width = (m_menu.size.column - 1) / m_menu.columns;
1005 
1006     const LineCount mark_height = min(div_round_up(sq(win_height), menu_lines),
1007                                       win_height);
1008 
1009     const int menu_cols = div_round_up(item_count, (int)m_menu.size.line);
1010     const int first_col = m_menu.first_item / (int)m_menu.size.line;
1011 
1012     const LineCount mark_line = (win_height - mark_height) * first_col / max(1, menu_cols - m_menu.columns);
1013 
1014     for (auto line = 0_line; line < win_height; ++line)
1015     {
1016         for (int col = 0; col < m_menu.columns; ++col)
1017         {
1018             int item_idx = (first_col + col) * (int)m_menu.size.line + (int)line;
1019             auto& face = item_idx < item_count and item_idx == m_menu.selected_item ? m_menu.fg : m_menu.bg;
1020             auto atoms = item_idx < item_count ? m_menu.items[item_idx].atoms() : ConstArrayView<DisplayAtom>{};
1021             m_menu.draw({line, col * column_width}, atoms, face);
1022         }
1023         const bool is_mark = line >= mark_line and line < mark_line + mark_height;
1024         m_menu.draw({line, m_menu.size.column - 1}, DisplayAtom(is_mark ? "█" : "░"), m_menu.bg);
1025     }
1026     m_dirty = true;
1027 }
1028 
height_limit(MenuStyle style)1029 static LineCount height_limit(MenuStyle style)
1030 {
1031     switch (style)
1032     {
1033         case MenuStyle::Inline: return 10_line;
1034         case MenuStyle::Prompt: return 10_line;
1035         case MenuStyle::Search: return 3_line;
1036     }
1037     kak_assert(false);
1038     return 0_line;
1039 }
1040 
menu_show(ConstArrayView<DisplayLine> items,DisplayCoord anchor,Face fg,Face bg,MenuStyle style)1041 void TerminalUI::menu_show(ConstArrayView<DisplayLine> items,
1042                            DisplayCoord anchor, Face fg, Face bg,
1043                            MenuStyle style)
1044 {
1045     if (m_menu)
1046     {
1047         m_menu.destroy();
1048         m_dirty = true;
1049     }
1050 
1051     m_menu.fg = fg;
1052     m_menu.bg = bg;
1053     m_menu.style = style;
1054     m_menu.anchor = anchor;
1055 
1056     if (m_dimensions.column <= 2)
1057         return;
1058 
1059     const int item_count = items.size();
1060     m_menu.items.clear(); // make sure it is empty
1061     m_menu.items.reserve(item_count);
1062     const auto longest = accumulate(items | transform(&DisplayLine::length),
1063                                     1_col, [](auto&& lhs, auto&& rhs) { return std::max(lhs, rhs); });
1064 
1065     const ColumnCount max_width = m_dimensions.column - 1;
1066     const bool is_inline = style == MenuStyle::Inline;
1067     const bool is_search = style == MenuStyle::Search;
1068     m_menu.columns = is_search ? 0 : (is_inline ? 1 : max((int)(max_width / (longest+1)), 1));
1069 
1070     const LineCount max_height = min(height_limit(style), max(anchor.line, m_dimensions.line - anchor.line - 1));
1071     const LineCount height = is_search ?
1072         1 : (min<LineCount>(max_height, div_round_up(item_count, m_menu.columns)));
1073 
1074     const ColumnCount maxlen = (m_menu.columns > 1 and item_count > 1) ?
1075         max_width / m_menu.columns - 1 : max_width;
1076 
1077     for (auto& item : items)
1078     {
1079         m_menu.items.push_back(item);
1080         m_menu.items.back().trim(0, maxlen);
1081         kak_assert(m_menu.items.back().length() <= maxlen);
1082     }
1083 
1084     if (is_inline)
1085         anchor.line += content_line_offset();
1086 
1087     LineCount line = anchor.line + 1;
1088     ColumnCount column = std::max(0_col, std::min(anchor.column, m_dimensions.column - longest - 1));
1089     if (is_search)
1090     {
1091         line = m_status_on_top ? 0_line : m_dimensions.line;
1092         column = m_dimensions.column / 2;
1093     }
1094     else if (not is_inline)
1095         line = m_status_on_top ? 1_line : m_dimensions.line - height;
1096     else if (line + height > m_dimensions.line and anchor.line >= height)
1097         line = anchor.line - height;
1098 
1099     const auto width = is_search ? m_dimensions.column - m_dimensions.column / 2
1100                                  : (is_inline ? min(longest+1, m_dimensions.column)
1101                                               : m_dimensions.column);
1102     m_menu.create({line, column}, {height, width});
1103     m_menu.selected_item = item_count;
1104     m_menu.first_item = 0;
1105 
1106     draw_menu();
1107 
1108     if (m_info)
1109         info_show(m_info.title, m_info.content,
1110                   m_info.anchor, m_info.face, m_info.style);
1111 }
1112 
menu_select(int selected)1113 void TerminalUI::menu_select(int selected)
1114 {
1115     const int item_count = m_menu.items.size();
1116     if (selected < 0 or selected >= item_count)
1117     {
1118         m_menu.selected_item = -1;
1119         m_menu.first_item = 0;
1120     }
1121     else if (m_menu.columns == 0) // Do not columnize
1122     {
1123         m_menu.selected_item = selected;
1124         const ColumnCount width = m_menu.size.column - 3;
1125         int first = 0;
1126         ColumnCount item_col = 0;
1127         for (int i = 0; i <= selected; ++i)
1128         {
1129             const ColumnCount item_width = m_menu.items[i].length() + 1;
1130             if (item_col + item_width > width)
1131             {
1132                 first = i;
1133                 item_col = item_width;
1134             }
1135             else
1136                 item_col += item_width;
1137         }
1138         m_menu.first_item = first;
1139     }
1140     else
1141     {
1142         m_menu.selected_item = selected;
1143         const int menu_cols = div_round_up(item_count, (int)m_menu.size.line);
1144         const int first_col = m_menu.first_item / (int)m_menu.size.line;
1145         const int selected_col = m_menu.selected_item / (int)m_menu.size.line;
1146         if (selected_col < first_col)
1147             m_menu.first_item = selected_col * (int)m_menu.size.line;
1148         if (selected_col >= first_col + m_menu.columns)
1149             m_menu.first_item = min(selected_col, menu_cols - m_menu.columns) * (int)m_menu.size.line;
1150     }
1151     draw_menu();
1152 }
1153 
menu_hide()1154 void TerminalUI::menu_hide()
1155 {
1156     if (not m_menu)
1157         return;
1158 
1159     m_menu.items.clear();
1160     m_menu.destroy();
1161     m_dirty = true;
1162 
1163     // Recompute info as it does not have to avoid the menu anymore
1164     if (m_info)
1165         info_show(m_info.title, m_info.content, m_info.anchor, m_info.face, m_info.style);
1166 }
1167 
compute_pos(DisplayCoord anchor,DisplayCoord size,TerminalUI::Rect rect,TerminalUI::Rect to_avoid,bool prefer_above)1168 static DisplayCoord compute_pos(DisplayCoord anchor, DisplayCoord size,
1169                                 TerminalUI::Rect rect, TerminalUI::Rect to_avoid,
1170                                 bool prefer_above)
1171 {
1172     DisplayCoord pos;
1173     if (prefer_above)
1174     {
1175         pos = anchor - DisplayCoord{size.line};
1176         if (pos.line < 0)
1177             prefer_above = false;
1178     }
1179     auto rect_end = rect.pos + rect.size;
1180     if (not prefer_above)
1181     {
1182         pos = anchor + DisplayCoord{1_line};
1183         if (pos.line + size.line >= rect_end.line)
1184             pos.line = max(rect.pos.line, anchor.line - size.line);
1185     }
1186     if (pos.column + size.column >= rect_end.column)
1187         pos.column = max(rect.pos.column, rect_end.column - size.column);
1188 
1189     if (to_avoid.size != DisplayCoord{})
1190     {
1191         DisplayCoord to_avoid_end = to_avoid.pos + to_avoid.size;
1192 
1193         DisplayCoord end = pos + size;
1194 
1195         // check intersection
1196         if (not (end.line < to_avoid.pos.line or end.column < to_avoid.pos.column or
1197                  pos.line > to_avoid_end.line or pos.column > to_avoid_end.column))
1198         {
1199             pos.line = min(to_avoid.pos.line, anchor.line) - size.line;
1200             // if above does not work, try below
1201             if (pos.line < 0)
1202                 pos.line = max(to_avoid_end.line, anchor.line);
1203         }
1204     }
1205 
1206     return pos;
1207 }
1208 
wrap_lines(const DisplayLineList & lines,ColumnCount max_width)1209 static DisplayLineList wrap_lines(const DisplayLineList& lines, ColumnCount max_width)
1210 {
1211     DisplayLineList result;
1212     for (auto line : lines)
1213     {
1214         ColumnCount column = 0;
1215         for (auto it = line.begin(); it != line.end(); )
1216         {
1217             auto length = it->length();
1218             column += length;
1219             if (column > max_width)
1220             {
1221                 auto content = it->content().substr(0, length - (column - max_width));
1222                 auto pos = find_if(content | reverse(), [](char c) { return not is_word(c); });
1223                 if (pos != content.rend())
1224                     content = {content.begin(), pos.base()};
1225 
1226                 if (not content.empty())
1227                     it = ++line.split(it, content.column_length());
1228                 result.push_back(AtomList(std::make_move_iterator(line.begin()),
1229                                           std::make_move_iterator(it)));
1230                 it = line.erase(line.begin(), it);
1231                 column = 0;
1232             }
1233             else
1234                 ++it;
1235         }
1236         result.push_back(std::move(line));
1237     }
1238     return result;
1239 }
1240 
info_show(const DisplayLine & title,const DisplayLineList & content,DisplayCoord anchor,Face face,InfoStyle style)1241 void TerminalUI::info_show(const DisplayLine& title, const DisplayLineList& content,
1242                            DisplayCoord anchor, Face face, InfoStyle style)
1243 {
1244     info_hide();
1245 
1246     m_info.title = title;
1247     m_info.content = content;
1248     m_info.anchor = anchor;
1249     m_info.face = face;
1250     m_info.style = style;
1251 
1252     const bool framed = style == InfoStyle::Prompt or style == InfoStyle::Modal;
1253     const bool assisted = style == InfoStyle::Prompt and m_assistant.size() != 0;
1254 
1255     DisplayCoord max_size = m_dimensions;
1256     if (style == InfoStyle::MenuDoc)
1257         max_size.column = std::max(m_dimensions.column - (m_menu.pos.column + m_menu.size.column),
1258                                    m_menu.pos.column);
1259     else if (style != InfoStyle::Modal)
1260         max_size.line -= m_menu.size.line;
1261 
1262     const auto max_content_width = max_size.column - (framed ? 4 : 2) - (assisted ? m_assistant[0].column_length() : 0);
1263     if (max_content_width <= 0)
1264         return;
1265 
1266     auto compute_size = [](const DisplayLineList& lines) -> DisplayCoord {
1267         return {(int)lines.size(), accumulate(lines, 0_col, [](ColumnCount c, const DisplayLine& l) { return std::max(c, l.length()); })};
1268     };
1269 
1270     DisplayCoord content_size = compute_size(content);
1271     const bool wrap = content_size.column > max_content_width;
1272     DisplayLineList wrapped_content;
1273     if (wrap)
1274     {
1275         wrapped_content = wrap_lines(content, max_content_width);
1276         content_size = compute_size(wrapped_content);
1277     }
1278     const auto& lines = wrap ? wrapped_content : content;
1279 
1280     DisplayCoord size{content_size.line, std::max(content_size.column, title.length() + (framed ? 2 : 0))};
1281     if (framed)
1282         size += {2, 4};
1283     if (assisted)
1284         size = {std::max(LineCount{(int)m_assistant.size()-1}, size.line), size.column + m_assistant[0].column_length()};
1285     size = {std::min(max_size.line, size.line), std::min(max_size.column, size.column)};
1286 
1287     if ((framed and size.line < 3) or size.line <= 0)
1288         return;
1289 
1290     const Rect rect = {content_line_offset(), m_dimensions};
1291     if (style == InfoStyle::Prompt)
1292     {
1293         anchor = DisplayCoord{m_status_on_top ? 0 : m_dimensions.line, m_dimensions.column-1};
1294         anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
1295     }
1296     else if (style == InfoStyle::Modal)
1297     {
1298         auto half = [](const DisplayCoord& c) { return DisplayCoord{c.line / 2, c.column / 2}; };
1299         anchor = rect.pos + half(rect.size) - half(size);
1300     }
1301     else if (style == InfoStyle::MenuDoc)
1302     {
1303         const auto right_max_width = m_dimensions.column - (m_menu.pos.column + m_menu.size.column);
1304         const auto left_max_width = m_menu.pos.column;
1305         anchor.line = m_menu.pos.line;
1306         if (size.column <= right_max_width or right_max_width >= left_max_width)
1307             anchor.column = m_menu.pos.column + m_menu.size.column;
1308         else
1309             anchor.column = m_menu.pos.column - size.column;
1310     }
1311     else
1312     {
1313         anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
1314         anchor.line += content_line_offset();
1315     }
1316 
1317     m_info.create(anchor, size);
1318     for (auto line = 0_line; line < size.line; ++line)
1319     {
1320         auto draw_atoms = [&, this, pos=DisplayCoord{line}](auto&&... args) mutable {
1321             auto draw = overload(
1322                 [&](ColumnCount padding) {
1323                     pos.column += padding;
1324                 },
1325                 [&](String str) {
1326                     auto len = str.column_length();
1327                     m_info.draw(pos, DisplayAtom{std::move(str)}, face);
1328                     pos.column += len;
1329                 },
1330                 [&](const DisplayLine& atoms) {
1331                     m_info.draw(pos, atoms.atoms(), face);
1332                     pos.column += atoms.length();
1333                 });
1334             (draw(args), ...);
1335         };
1336 
1337         constexpr Codepoint dash{L'─'};
1338         constexpr Codepoint dotted_dash{L'┄'};
1339         if (assisted)
1340         {
1341             const auto assistant_top_margin = (size.line - m_assistant.size()+1) / 2;
1342             StringView assistant_line = (line >= assistant_top_margin) ?
1343                 m_assistant[(int)min(line - assistant_top_margin, LineCount{(int)m_assistant.size()}-1)]
1344               : m_assistant[(int)m_assistant.size()-1];
1345 
1346             draw_atoms(assistant_line.str());
1347         }
1348         if (not framed)
1349             draw_atoms(lines[(int)line]);
1350         else if (line == 0)
1351         {
1352             if (title.atoms().empty() or content_size.column < 2)
1353                 draw_atoms("╭─" + String{dash, content_size.column} + "─╮");
1354             else
1355             {
1356                 auto trimmed_title = title;
1357                 trimmed_title.trim(0, content_size.column - 2);
1358                 auto dash_count = content_size.column - trimmed_title.length() - 2;
1359                 String left{dash, dash_count / 2};
1360                 String right{dash, dash_count - dash_count / 2};
1361                 draw_atoms("╭─" + left + "┤", trimmed_title, "├" + right +"─╮");
1362             }
1363         }
1364         else if (line < size.line - 1 and line <= lines.size())
1365         {
1366             auto info_line = lines[(int)line - 1];
1367             const bool trimmed = info_line.trim(0, content_size.column);
1368             const ColumnCount padding = content_size.column - info_line.length();
1369             draw_atoms("│ ", info_line, padding, (trimmed ? "…│" : " │"));
1370         }
1371         else if (line == std::min<LineCount>((int)lines.size() + 1, size.line - 1))
1372             draw_atoms("╰─", String(line > lines.size() ? dash : dotted_dash, content_size.column), "─╯");
1373     }
1374     m_dirty = true;
1375 }
1376 
info_hide()1377 void TerminalUI::info_hide()
1378 {
1379     if (not m_info)
1380         return;
1381     m_info.destroy();
1382     m_dirty = true;
1383 }
1384 
set_on_key(OnKeyCallback callback)1385 void TerminalUI::set_on_key(OnKeyCallback callback)
1386 {
1387     m_on_key = std::move(callback);
1388     EventManager::instance().force_signal(0);
1389 }
1390 
dimensions()1391 DisplayCoord TerminalUI::dimensions()
1392 {
1393     return m_dimensions;
1394 }
1395 
content_line_offset() const1396 LineCount TerminalUI::content_line_offset() const
1397 {
1398     return m_status_on_top ? 1 : 0;
1399 }
1400 
set_resize_pending()1401 void TerminalUI::set_resize_pending()
1402 {
1403     m_resize_pending = true;
1404     EventManager::instance().force_signal(0);
1405 }
1406 
setup_terminal()1407 void TerminalUI::setup_terminal()
1408 {
1409     write(STDOUT_FILENO,
1410         "\033[?1049h" // enable alternative screen buffer
1411         "\033[?1004h" // enable focus notify
1412         "\033[>4;1m"  // request CSI u style key reporting
1413         "\033[>5u"    // kitty progressive enhancement - report shifted key codes
1414         "\033[22t"    // save the current window title
1415         "\033[?25l"   // hide cursor
1416         "\033="       // set application keypad mode, so the keypad keys send unique codes
1417         "\033[?2026$p" // query support for synchronize output
1418     );
1419 }
1420 
restore_terminal()1421 void TerminalUI::restore_terminal()
1422 {
1423     write(STDOUT_FILENO,
1424         "\033>"
1425         "\033[?25h"
1426         "\033[23t"
1427         "\033[<u"
1428         "\033[>4;0m"
1429         "\033[?1004l"
1430         "\033[?1049l"
1431         "\033[m" // set the terminal output back to default colours and style
1432     );
1433 }
1434 
enable_mouse(bool enabled)1435 void TerminalUI::enable_mouse(bool enabled)
1436 {
1437     if (enabled == m_mouse_enabled)
1438         return;
1439 
1440     m_mouse_enabled = enabled;
1441     if (enabled)
1442     {
1443         write(STDOUT_FILENO,
1444             "\033[?1006h" // force SGR mode
1445             "\033[?1000h" // enable mouse
1446             "\033[?1002h" // force enable report mouse position
1447         );
1448     }
1449     else
1450     {
1451         write(STDOUT_FILENO,
1452             "\033[?1002l"
1453             "\033[?1000l"
1454             "\033[?1006l"
1455         );
1456     }
1457 }
1458 
set_ui_options(const Options & options)1459 void TerminalUI::set_ui_options(const Options& options)
1460 {
1461     auto find = [&](StringView name) -> Optional<StringView> {
1462         if (auto it = options.find(name); it != options.end())
1463             return StringView{it->value};
1464         return {};
1465     };
1466 
1467     auto assistant = find("terminal_assistant").value_or("clippy");
1468     if (assistant == "clippy")
1469         m_assistant = assistant_clippy;
1470     else if (assistant == "cat")
1471         m_assistant = assistant_cat;
1472     else if (assistant == "dilbert")
1473         m_assistant = assistant_dilbert;
1474     else if (assistant == "none" or assistant == "off")
1475         m_assistant = ConstArrayView<StringView>{};
1476 
1477     auto to_bool = [](StringView s) { return s == "yes" or s == "true"; };
1478 
1479     m_status_on_top = find("terminal_status_on_top").map(to_bool).value_or(false);
1480     m_set_title = find("terminal_set_title").map(to_bool).value_or(true);
1481 
1482     auto synchronized = find("terminal_synchronized").map(to_bool);
1483     m_synchronized.set = (bool)synchronized;
1484     m_synchronized.requested = synchronized.value_or(false);
1485 
1486     m_shift_function_key = find("terminal_shift_function_key").map(str_to_int_ifp).value_or(default_shift_function_key);
1487 
1488     enable_mouse(find("terminal_enable_mouse").map(to_bool).value_or(true));
1489     m_wheel_scroll_amount = find("terminal_wheel_scroll_amount").map(str_to_int_ifp).value_or(3);
1490 
1491     m_padding_char = find("terminal_padding_char").map([](StringView s) { return s.column_length() < 1 ? ' ' : s[0_char]; }).value_or(Codepoint{'~'});
1492     m_padding_fill = find("terminal_padding_fill").map(to_bool).value_or(false);
1493 }
1494 
1495 }
1496