2019-11-16 11:27:06 +01:00
|
|
|
|
#include "terminal_ui.hh"
|
2012-02-16 15:25:16 +01:00
|
|
|
|
|
2022-12-11 19:30:02 +01:00
|
|
|
|
#include "buffer_utils.hh"
|
2012-10-20 20:15:20 +02:00
|
|
|
|
#include "display_buffer.hh"
|
2013-04-09 20:05:40 +02:00
|
|
|
|
#include "event_manager.hh"
|
2019-11-08 22:19:45 +01:00
|
|
|
|
#include "exception.hh"
|
|
|
|
|
#include "file.hh"
|
2014-11-12 22:27:07 +01:00
|
|
|
|
#include "keys.hh"
|
2017-08-29 10:23:03 +02:00
|
|
|
|
#include "ranges.hh"
|
2017-10-09 16:12:42 +02:00
|
|
|
|
#include "string_utils.hh"
|
2020-02-18 12:54:46 +01:00
|
|
|
|
#include "diff.hh"
|
2012-10-08 14:27:43 +02:00
|
|
|
|
|
2014-12-23 14:34:21 +01:00
|
|
|
|
#include <algorithm>
|
2012-02-16 15:25:16 +01:00
|
|
|
|
|
2014-10-13 14:12:33 +02:00
|
|
|
|
#include <fcntl.h>
|
2017-01-08 23:30:15 +01:00
|
|
|
|
#include <csignal>
|
2012-10-27 14:18:52 +02:00
|
|
|
|
#include <sys/ioctl.h>
|
2014-10-13 14:12:33 +02:00
|
|
|
|
#include <unistd.h>
|
2020-06-28 11:48:55 +02:00
|
|
|
|
#include <strings.h>
|
2012-02-16 15:25:16 +01:00
|
|
|
|
|
|
|
|
|
namespace Kakoune
|
|
|
|
|
{
|
2012-06-06 01:15:19 +02:00
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
using std::min;
|
|
|
|
|
using std::max;
|
|
|
|
|
|
2021-08-17 12:40:30 +02:00
|
|
|
|
static String fix_atom_text(StringView str)
|
|
|
|
|
{
|
|
|
|
|
String res;
|
|
|
|
|
auto pos = str.begin();
|
|
|
|
|
for (auto it = str.begin(), end = str.end(); it != end; ++it)
|
|
|
|
|
{
|
2023-12-10 09:50:09 +01:00
|
|
|
|
unsigned char c = *it;
|
|
|
|
|
if (c <= 0x1F)
|
2021-08-17 12:40:30 +02:00
|
|
|
|
{
|
|
|
|
|
res += StringView{pos, it};
|
|
|
|
|
res += String{Codepoint{(uint32_t)(0x2400 + c)}};
|
|
|
|
|
pos = it+1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res += StringView{pos, str.end()};
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-30 12:47:18 +01:00
|
|
|
|
struct TerminalUI::Window::Line
|
|
|
|
|
{
|
|
|
|
|
struct Atom
|
|
|
|
|
{
|
|
|
|
|
String text;
|
2020-03-28 09:55:17 +01:00
|
|
|
|
ColumnCount skip = 0;
|
2019-11-30 12:47:18 +01:00
|
|
|
|
Face face;
|
2019-12-01 01:09:01 +01:00
|
|
|
|
|
2020-03-28 09:55:17 +01:00
|
|
|
|
ColumnCount length() const { return text.column_length() + skip; }
|
|
|
|
|
void resize(ColumnCount size)
|
|
|
|
|
{
|
2021-07-14 12:37:18 +02:00
|
|
|
|
auto it = text.begin(), end = text.end();
|
|
|
|
|
while (it != end and size > 0)
|
|
|
|
|
size -= codepoint_width(utf8::read_codepoint(it, end));
|
|
|
|
|
|
|
|
|
|
if (size < 0) // possible if resizing to the middle of a double-width codepoint
|
2020-03-28 09:55:17 +01:00
|
|
|
|
{
|
2021-07-14 12:37:18 +02:00
|
|
|
|
kak_assert(size == -1);
|
|
|
|
|
utf8::to_previous(it, text.begin());
|
|
|
|
|
skip = 1;
|
2020-03-28 09:55:17 +01:00
|
|
|
|
}
|
|
|
|
|
else
|
2021-07-14 12:37:18 +02:00
|
|
|
|
skip = size;
|
|
|
|
|
text.resize(it - text.begin(), 0);
|
2020-03-28 09:55:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-25 12:06:52 +02:00
|
|
|
|
friend bool operator==(const Atom& lhs, const Atom& rhs) = default;
|
2020-03-28 09:55:17 +01:00
|
|
|
|
friend size_t hash_value(const Atom& atom) { return hash_values(atom.text, atom.skip, atom.face); }
|
2019-11-30 12:47:18 +01:00
|
|
|
|
};
|
|
|
|
|
|
2021-07-20 11:53:06 +02:00
|
|
|
|
void append(StringView text, ColumnCount skip, Face face)
|
2019-12-01 01:09:01 +01:00
|
|
|
|
{
|
2020-03-28 09:55:17 +01:00
|
|
|
|
if (not atoms.empty() and atoms.back().face == face and (atoms.back().skip == 0 or text.empty()))
|
|
|
|
|
{
|
2021-08-17 12:40:30 +02:00
|
|
|
|
atoms.back().text += fix_atom_text(text);
|
2020-03-28 09:55:17 +01:00
|
|
|
|
atoms.back().skip += skip;
|
|
|
|
|
}
|
2019-12-01 01:09:01 +01:00
|
|
|
|
else
|
2021-08-17 12:40:30 +02:00
|
|
|
|
atoms.push_back({fix_atom_text(text), skip, face});
|
2019-12-01 01:09:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-30 12:47:18 +01:00
|
|
|
|
void resize(ColumnCount width)
|
|
|
|
|
{
|
|
|
|
|
auto it = atoms.begin();
|
|
|
|
|
ColumnCount column = 0;
|
|
|
|
|
for (; it != atoms.end() and column < width; ++it)
|
2020-03-28 09:55:17 +01:00
|
|
|
|
column += it->length();
|
2019-11-30 12:47:18 +01:00
|
|
|
|
|
|
|
|
|
if (column < width)
|
2020-03-28 09:55:17 +01:00
|
|
|
|
append({}, width - column, atoms.empty() ? Face{} : atoms.back().face);
|
2019-11-30 12:47:18 +01:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
atoms.erase(it, atoms.end());
|
|
|
|
|
if (column > width)
|
2020-03-28 09:55:17 +01:00
|
|
|
|
atoms.back().resize(atoms.back().length() - (column - width));
|
2019-11-30 12:47:18 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector<Atom>::iterator erase_range(ColumnCount pos, ColumnCount len)
|
|
|
|
|
{
|
2020-03-28 09:55:17 +01:00
|
|
|
|
struct Pos{ Vector<Atom>::iterator it; ColumnCount column; };
|
2019-11-30 12:47:18 +01:00
|
|
|
|
auto find_col = [pos=0_col, it=atoms.begin(), end=atoms.end()](ColumnCount col) mutable {
|
|
|
|
|
for (; it != end; ++it)
|
|
|
|
|
{
|
2020-03-28 09:55:17 +01:00
|
|
|
|
auto atom_len = it->length();
|
2019-11-30 12:47:18 +01:00
|
|
|
|
if (pos + atom_len >= col)
|
2020-03-28 09:55:17 +01:00
|
|
|
|
return Pos{it, col - pos};
|
2019-11-30 12:47:18 +01:00
|
|
|
|
pos += atom_len;
|
|
|
|
|
}
|
2020-03-28 09:55:17 +01:00
|
|
|
|
return Pos{it, 0_col};
|
2019-11-30 12:47:18 +01:00
|
|
|
|
};
|
|
|
|
|
Pos begin = find_col(pos);
|
|
|
|
|
Pos end = find_col(pos+len);
|
|
|
|
|
|
2020-03-28 09:55:17 +01:00
|
|
|
|
auto make_tail = [](const Atom& atom, ColumnCount from) {
|
2021-07-14 12:37:18 +02:00
|
|
|
|
auto it = atom.text.begin(), end = atom.text.end();
|
|
|
|
|
while (it != end and from > 0)
|
|
|
|
|
from -= codepoint_width(utf8::read_codepoint(it, end));
|
|
|
|
|
|
|
|
|
|
if (from < 0) // can happen if tail starts in the middle of a double-width codepoint
|
|
|
|
|
{
|
|
|
|
|
kak_assert(from == -1);
|
|
|
|
|
return Atom{" " + StringView{it, end}, atom.skip, atom.face};
|
|
|
|
|
}
|
|
|
|
|
return Atom{{it, end}, atom.skip - from, atom.face};
|
2020-03-28 09:55:17 +01:00
|
|
|
|
};
|
|
|
|
|
|
2019-11-30 12:47:18 +01:00
|
|
|
|
if (begin.it == end.it)
|
|
|
|
|
{
|
2020-03-28 09:55:17 +01:00
|
|
|
|
Atom tail = make_tail(*begin.it, end.column);
|
|
|
|
|
begin.it->resize(begin.column);
|
|
|
|
|
return (tail.text.empty() and tail.skip == 0) ? begin.it+1 : atoms.insert(begin.it+1, tail);
|
2019-11-30 12:47:18 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 09:55:17 +01:00
|
|
|
|
begin.it->resize(begin.column);
|
|
|
|
|
if (end.column > 0)
|
2019-11-30 12:47:18 +01:00
|
|
|
|
{
|
2020-03-28 09:55:17 +01:00
|
|
|
|
if (end.column == end.it->length())
|
2019-11-30 12:47:18 +01:00
|
|
|
|
++end.it;
|
|
|
|
|
else
|
2020-03-28 09:55:17 +01:00
|
|
|
|
*end.it = make_tail(*end.it, end.column);
|
2019-11-30 12:47:18 +01:00
|
|
|
|
}
|
|
|
|
|
return atoms.erase(begin.it+1, end.it);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector<Atom> atoms;
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-31 07:06:21 +02:00
|
|
|
|
void TerminalUI::Window::create(const DisplayCoord& p, const DisplayCoord& s)
|
|
|
|
|
{
|
|
|
|
|
kak_assert(p.line >= 0 and p.column >= 0);
|
|
|
|
|
kak_assert(s.line >= 0 and s.column >= 0);
|
|
|
|
|
pos = p;
|
|
|
|
|
size = s;
|
|
|
|
|
lines.reset(new Line[(int)size.line]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalUI::Window::destroy()
|
|
|
|
|
{
|
|
|
|
|
pos = DisplayCoord{};
|
|
|
|
|
size = DisplayCoord{};
|
|
|
|
|
lines.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-30 12:47:18 +01:00
|
|
|
|
void TerminalUI::Window::blit(Window& target)
|
|
|
|
|
{
|
2021-07-31 07:06:21 +02:00
|
|
|
|
kak_assert(pos.line < target.size.line);
|
|
|
|
|
LineCount line_index = pos.line;
|
|
|
|
|
for (auto& line : ArrayView{lines.get(), (size_t)size.line})
|
2019-11-30 12:47:18 +01:00
|
|
|
|
{
|
|
|
|
|
line.resize(size.column);
|
2021-07-31 07:06:21 +02:00
|
|
|
|
auto& target_line = target.lines[(size_t)line_index];
|
|
|
|
|
target_line.resize(target.size.column);
|
|
|
|
|
target_line.atoms.insert(target_line.erase_range(pos.column, size.column), line.atoms.begin(), line.atoms.end());
|
|
|
|
|
if (++line_index == target.size.line)
|
2020-05-25 14:43:40 +02:00
|
|
|
|
break;
|
2019-11-30 12:47:18 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 02:54:49 +01:00
|
|
|
|
void TerminalUI::Window::draw(DisplayCoord pos,
|
|
|
|
|
ConstArrayView<DisplayAtom> atoms,
|
2020-10-08 11:56:39 +02:00
|
|
|
|
const Face& default_face)
|
2019-12-01 01:09:01 +01:00
|
|
|
|
{
|
2021-07-31 07:06:21 +02:00
|
|
|
|
if (pos.line >= size.line) // We might receive an out of date draw command after a resize
|
2020-07-03 13:04:08 +02:00
|
|
|
|
return;
|
|
|
|
|
|
2020-03-28 02:54:49 +01:00
|
|
|
|
lines[(size_t)pos.line].resize(pos.column);
|
2019-12-01 01:09:01 +01:00
|
|
|
|
for (const DisplayAtom& atom : atoms)
|
|
|
|
|
{
|
|
|
|
|
StringView content = atom.content();
|
|
|
|
|
if (content.empty())
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
auto face = merge_faces(default_face, atom.face);
|
|
|
|
|
if (content.back() == '\n')
|
2021-07-20 11:53:06 +02:00
|
|
|
|
lines[(int)pos.line].append(content.substr(0, content.length()-1), 1, face);
|
2019-12-01 01:09:01 +01:00
|
|
|
|
else
|
2021-07-20 11:53:06 +02:00
|
|
|
|
lines[(int)pos.line].append(content, 0, face);
|
2020-03-28 02:54:49 +01:00
|
|
|
|
pos.column += content.column_length();
|
2019-12-01 01:09:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 02:54:49 +01:00
|
|
|
|
if (pos.column < size.column)
|
2020-03-28 09:55:17 +01:00
|
|
|
|
lines[(int)pos.line].append({}, size.column - pos.column, default_face);
|
2019-12-01 01:09:01 +01:00
|
|
|
|
}
|
|
|
|
|
|
Fix crash after multiple terminal resizes
When Kakoune's terminal is shown on my laptop monitor and I plug
in my external monitor, the terminal's workspace will move to that
external monitor. When this happens, Kakoune may segfault.
There are multiple resize events (SIGWINCH) in quick succession;
it crashes because we handle SIGWINCH during rendering.
The problem happens during execution of "TerminalUI::Screen::output"
(frame #18). When we receive SIGWINCH while writing to stdout, write(2)
fails with EAGAIN, prompting us to handle pending events (See ae001a1f9
(Run EventManager whenever writing to a file descriptor would block,
2022-05-10)). We update the screen size in check_resize() here:
#4 Kakoune::TerminalUI::check_resize (force=<optimized out>) at terminal_ui.cc:683
#5 Kakoune::TerminalUI::get_next_key () at terminal_ui.cc:719
#6 operator() (__closure=0x555555984198) at terminal_ui.cc:484
#7 std::__invoke_impl<void, Kakoune::TerminalUI::TerminalUI()::<lambda(Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode)>&, Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode> (__f=...) at /usr/include/c++/12.2.1/bits/invoke.h:61
#8 std::__invoke_r<void, Kakoune::TerminalUI::TerminalUI()::<lambda(Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode)>&, Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode> (__fn=...) at /usr/include/c++/12.2.1/bits/invoke.h:111
#9 std::_Function_handler<void(Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode), Kakoune::TerminalUI::TerminalUI()::<lambda(Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode)> >::_M_invoke(const std::_Any_data &, Kakoune::FDWatcher &, Kakoune::FdEvents &&, Kakoune::EventMode &&) (__functor=..., __args#0=..., __args#1=<optimized out>, __args#2=<optimized out>) at /usr/include/c++/12.2.1/bits/std_function.h:290
#10 std::function<void (Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode)>::operator()(Kakoune::FDWatcher&, Kakoune::FdEvents, Kakoune::EventMode) const (__args#2=<optimized out>, __args#1=<optimized out>, __args#0=...) at /usr/include/c++/12.2.1/bits/std_function.h:591
#11 Kakoune::FDWatcher::run (mode=Kakoune::EventMode::Urgent, events=<optimized out>) at event_manager.cc:28
#12 Kakoune::EventManager::handle_next_events (mode=mode@entry=Kakoune::EventMode::Urgent, sigmask=sigmask@entry=0x0, block=<optimized out>, block@entry=false) at event_manager.cc:143
#13 Kakoune::write (fd=1, data=...) at file.cc:273
#14 Kakoune::BufferedWriter<4096>::flush () at string.hh:236
#15 Kakoune::BufferedWriter<4096>::write (data="t file.hh:145
#16 Kakoune::TerminalUI::Screen::set_face (face=..., writer=...) at terminal_ui.cc:255
#17 operator() (line=..., __closure=<synthetic pointer>) at terminal_ui.cc:326
#18 Kakoune::TerminalUI::Screen::output (force=force@entry=true, synchronized=<optimized out>, writer=...) at terminal_ui.cc:402
#19 Kakoune::TerminalUI::redraw (force=force@entry=true) at terminal_ui.cc:571
#20 Kakoune::TerminalUI::refresh (force=<optimized out>) at terminal_ui.cc:592
#21 Kakoune::Client::redraw_ifn () at client.cc:282
#22 Kakoune::ClientManager::redraw_clients () at client_manager.cc:232
#23 Kakoune::run_server (session=..., server_init=..., client_init=..., init_buffer="fish-rust/src/ast.rs", init_coord=..., flags=Kakoune::ServerFlags::None, ui_type=Kakoune::UIType::Terminal,
debug_flags=<optimized out>, files=ArrayView<Kakoune::StringView> = {...}) at main.cc:893
#24 main (argc=<optimized out>, argv=<optimized out>) at main.cc:1243
Thereafter, "TerminalUI::Screen::output" resumes and crashes due to
a buffer overflow in "lines" which has been resized.
2023-03-12 09:25:55 +01:00
|
|
|
|
struct Writer : BufferedWriter<true>
|
2021-07-31 01:45:36 +02:00
|
|
|
|
{
|
|
|
|
|
using Writer::BufferedWriter::BufferedWriter;
|
|
|
|
|
~Writer() noexcept(false) = default;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template<typename... Args>
|
|
|
|
|
static void format_with(Writer& writer, StringView format, Args&&... args)
|
|
|
|
|
{
|
|
|
|
|
format_with([&](StringView s) { writer.write(s); }, format, std::forward<Args>(args)...);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerminalUI::Screen::set_face(const Face& face, Writer& writer)
|
2019-09-04 16:04:27 +02:00
|
|
|
|
{
|
2019-11-13 13:31:24 +01:00
|
|
|
|
static constexpr int fg_table[]{ 39, 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97 };
|
|
|
|
|
static constexpr int bg_table[]{ 49, 40, 41, 42, 43, 44, 45, 46, 47, 100, 101, 102, 103, 104, 105, 106, 107 };
|
2021-09-07 00:21:26 +02:00
|
|
|
|
static constexpr int ul_table[]{ 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
|
|
|
|
|
static constexpr const char* attr_table[]{ "0", "4", "4:3", "7", "5", "1", "2", "3", "9" };
|
2020-03-29 04:35:42 +02:00
|
|
|
|
|
2021-07-31 01:45:36 +02:00
|
|
|
|
auto set_color = [&](bool fg, const Color& color, bool join) {
|
2020-03-29 04:35:42 +02:00
|
|
|
|
if (join)
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write(";");
|
2019-11-13 13:31:24 +01:00
|
|
|
|
if (color.isRGB())
|
2021-07-31 01:45:36 +02:00
|
|
|
|
format_with(writer, "{};2;{};{};{}", fg ? 38 : 48, color.r, color.g, color.b);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
else
|
2021-07-31 01:45:36 +02:00
|
|
|
|
format_with(writer, "{}", (fg ? fg_table : bg_table)[(int)(char)color.color]);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
};
|
2019-09-04 16:04:27 +02:00
|
|
|
|
|
2020-03-29 04:35:42 +02:00
|
|
|
|
if (m_active_face == face)
|
|
|
|
|
return;
|
|
|
|
|
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write("\033[");
|
2020-03-29 04:35:42 +02:00
|
|
|
|
bool join = false;
|
|
|
|
|
if (face.attributes != m_active_face.attributes)
|
|
|
|
|
{
|
2020-11-02 20:55:15 +01:00
|
|
|
|
for (int i = 0; i < std::size(attr_table); ++i)
|
2019-11-13 13:31:24 +01:00
|
|
|
|
{
|
2020-03-29 04:35:42 +02:00
|
|
|
|
if (face.attributes & (Attribute)(1 << i))
|
2021-07-31 01:45:36 +02:00
|
|
|
|
format_with(writer, ";{}", attr_table[i]);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
}
|
2022-12-13 02:14:29 +01:00
|
|
|
|
m_active_face.fg = m_active_face.bg = m_active_face.underline = Color::Default;
|
2020-03-29 04:35:42 +02:00
|
|
|
|
join = true;
|
|
|
|
|
}
|
|
|
|
|
if (m_active_face.fg != face.fg)
|
|
|
|
|
{
|
|
|
|
|
set_color(true, face.fg, join);
|
|
|
|
|
join = true;
|
|
|
|
|
}
|
|
|
|
|
if (m_active_face.bg != face.bg)
|
2021-09-07 00:21:26 +02:00
|
|
|
|
{
|
2020-03-29 04:35:42 +02:00
|
|
|
|
set_color(false, face.bg, join);
|
2021-09-07 00:21:26 +02:00
|
|
|
|
join = true;
|
|
|
|
|
}
|
|
|
|
|
if (m_active_face.underline != face.underline)
|
|
|
|
|
{
|
|
|
|
|
if (join)
|
|
|
|
|
writer.write(";");
|
|
|
|
|
if (face.underline != Color::Default)
|
|
|
|
|
{
|
|
|
|
|
if (face.underline.isRGB())
|
|
|
|
|
format_with(writer, "58:2::{}:{}:{}", face.underline.r, face.underline.g, face.underline.b);
|
|
|
|
|
else
|
|
|
|
|
format_with(writer, "58:5:{}", ul_table[(int)(char)face.underline.color]);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
format_with(writer, "59");
|
|
|
|
|
}
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write("m");
|
2020-03-29 04:35:42 +02:00
|
|
|
|
|
|
|
|
|
m_active_face = face;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-31 01:45:36 +02:00
|
|
|
|
void TerminalUI::Screen::output(bool force, bool synchronized, Writer& writer)
|
2020-03-29 04:35:42 +02:00
|
|
|
|
{
|
2021-07-31 07:06:21 +02:00
|
|
|
|
if (not lines)
|
2020-03-29 04:35:42 +02:00
|
|
|
|
return;
|
2019-11-13 13:31:24 +01:00
|
|
|
|
|
2020-07-21 12:41:53 +02:00
|
|
|
|
if (force)
|
|
|
|
|
{
|
2021-07-31 07:06:21 +02:00
|
|
|
|
std::fill_n(hashes.get(), (size_t)size.line, 0);
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write("\033[m");
|
2020-07-21 12:41:53 +02:00
|
|
|
|
m_active_face = Face{};
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-31 07:06:21 +02:00
|
|
|
|
auto hash_line = [](const Line& line) {
|
|
|
|
|
return (hash_value(line.atoms) << 1) | 1; // ensure non-zero
|
|
|
|
|
};
|
|
|
|
|
|
2021-09-26 02:45:23 +02:00
|
|
|
|
auto output_line = [&](const Line& line) {
|
2021-09-09 00:59:26 +02:00
|
|
|
|
ColumnCount pending_move = 0;
|
2021-09-26 02:45:23 +02:00
|
|
|
|
for (auto& [text, skip, face] : line.atoms)
|
2019-09-04 16:04:27 +02:00
|
|
|
|
{
|
2021-09-09 00:59:26 +02:00
|
|
|
|
if (text.empty() and skip == 0)
|
|
|
|
|
continue;
|
2020-03-21 06:07:30 +01:00
|
|
|
|
|
2021-09-09 00:59:26 +02:00
|
|
|
|
if (pending_move != 0)
|
|
|
|
|
{
|
|
|
|
|
format_with(writer, "\033[{}C", (int)pending_move);
|
|
|
|
|
pending_move = 0;
|
|
|
|
|
}
|
|
|
|
|
set_face(face, writer);
|
|
|
|
|
writer.write(text);
|
|
|
|
|
if (skip > 3 and face.attributes == Attribute{})
|
2019-12-01 01:09:01 +01:00
|
|
|
|
{
|
2021-09-09 00:59:26 +02:00
|
|
|
|
writer.write("\033[K");
|
|
|
|
|
pending_move = skip;
|
2019-12-01 01:09:01 +01:00
|
|
|
|
}
|
2021-09-09 00:59:26 +02:00
|
|
|
|
else if (skip > 0)
|
|
|
|
|
writer.write(String{' ', skip});
|
2019-09-04 16:04:27 +02:00
|
|
|
|
}
|
2021-09-26 02:45:23 +02:00
|
|
|
|
};
|
2020-05-15 12:47:10 +02:00
|
|
|
|
|
2021-07-09 12:53:17 +02:00
|
|
|
|
if (synchronized)
|
2021-09-26 02:45:23 +02:00
|
|
|
|
{
|
|
|
|
|
writer.write("\033[?2026h"); // begin synchronized update
|
|
|
|
|
|
|
|
|
|
struct Change { int keep; int add; int del; };
|
|
|
|
|
Vector<Change> changes{Change{}};
|
|
|
|
|
auto new_hashes = ArrayView{lines.get(), (size_t)size.line} | transform(hash_line);
|
|
|
|
|
for_each_diff(hashes.get(), (int)size.line,
|
|
|
|
|
new_hashes.begin(), (int)size.line,
|
|
|
|
|
[&changes](DiffOp op, int len) mutable {
|
|
|
|
|
switch (op)
|
|
|
|
|
{
|
|
|
|
|
case DiffOp::Keep:
|
|
|
|
|
changes.push_back({len, 0, 0});
|
|
|
|
|
break;
|
|
|
|
|
case DiffOp::Add:
|
|
|
|
|
changes.back().add += len;
|
|
|
|
|
break;
|
|
|
|
|
case DiffOp::Remove:
|
|
|
|
|
changes.back().del += len;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
std::copy(new_hashes.begin(), new_hashes.end(), hashes.get());
|
|
|
|
|
|
|
|
|
|
int line = 0;
|
|
|
|
|
for (auto& change : changes)
|
|
|
|
|
{
|
|
|
|
|
line += change.keep;
|
|
|
|
|
if (int del = change.del - change.add; del > 0)
|
|
|
|
|
{
|
|
|
|
|
format_with(writer, "\033[{}H\033[{}M", line + 1, del);
|
|
|
|
|
line -= del;
|
|
|
|
|
}
|
|
|
|
|
line += change.del;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
line = 0;
|
|
|
|
|
for (auto& change : changes)
|
|
|
|
|
{
|
|
|
|
|
line += change.keep;
|
|
|
|
|
for (int i = 0; i < change.add; ++i)
|
|
|
|
|
{
|
|
|
|
|
if (int add = change.add - change.del; i == 0 and add > 0)
|
|
|
|
|
format_with(writer, "\033[{}H\033[{}L", line + 1, add);
|
|
|
|
|
else
|
|
|
|
|
format_with(writer, "\033[{}H", line + 1);
|
|
|
|
|
|
|
|
|
|
output_line(lines[line++]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.write("\033[?2026l"); // end synchronized update
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
for (int line = 0; line < (int)size.line; ++line)
|
|
|
|
|
{
|
|
|
|
|
auto hash = hash_line(lines[line]);
|
|
|
|
|
if (hash == hashes[line])
|
|
|
|
|
continue;
|
|
|
|
|
hashes[line] = hash;
|
|
|
|
|
|
|
|
|
|
format_with(writer, "\033[{}H", line + 1);
|
|
|
|
|
output_line(lines[line]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-04 16:04:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
constexpr int TerminalUI::default_shift_function_key;
|
2018-04-11 12:29:35 +02:00
|
|
|
|
|
2015-02-11 00:09:30 +01:00
|
|
|
|
static constexpr StringView assistant_cat[] =
|
2015-03-25 14:53:13 +01:00
|
|
|
|
{ R"( ___ )",
|
|
|
|
|
R"( (__ \ )",
|
|
|
|
|
R"( / / ╭)",
|
|
|
|
|
R"( .' '·. │)",
|
|
|
|
|
R"( ' ” │)",
|
|
|
|
|
R"( ╰ /\_/| │)",
|
|
|
|
|
R"( | . \ │)",
|
|
|
|
|
R"( ╰_J` | | | ╯)",
|
|
|
|
|
R"( ' \__- _/ )",
|
|
|
|
|
R"( \_\ \_\ )",
|
2015-02-09 14:33:54 +01:00
|
|
|
|
R"( )"};
|
|
|
|
|
|
2015-02-11 00:09:30 +01:00
|
|
|
|
static constexpr StringView assistant_clippy[] =
|
2015-02-05 23:40:08 +01:00
|
|
|
|
{ " ╭──╮ ",
|
|
|
|
|
" │ │ ",
|
|
|
|
|
" @ @ ╭",
|
|
|
|
|
" ││ ││ │",
|
|
|
|
|
" ││ ││ ╯",
|
|
|
|
|
" │╰─╯│ ",
|
|
|
|
|
" ╰───╯ ",
|
|
|
|
|
" " };
|
|
|
|
|
|
2017-03-08 17:45:15 +01:00
|
|
|
|
static constexpr StringView assistant_dilbert[] =
|
|
|
|
|
{ R"( დოოოოოდ )",
|
|
|
|
|
R"( | | )",
|
|
|
|
|
R"( | | ╭)",
|
|
|
|
|
R"( |-ᱛ ᱛ-| │)",
|
|
|
|
|
R"( Ͼ ∪ Ͽ │)",
|
|
|
|
|
R"( | | ╯)",
|
|
|
|
|
R"( ˏ`-.ŏ.-´ˎ )",
|
|
|
|
|
R"( @ )",
|
|
|
|
|
R"( @ )",
|
|
|
|
|
R"( )"};
|
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
template<typename T> T sq(T x) { return x * x; }
|
|
|
|
|
|
2014-06-09 14:26:54 +02:00
|
|
|
|
static sig_atomic_t resize_pending = 0;
|
2023-08-30 20:56:50 +02:00
|
|
|
|
static sig_atomic_t terminal_hungup = 0;
|
2014-06-09 14:26:54 +02:00
|
|
|
|
|
2018-04-29 14:27:28 +02:00
|
|
|
|
template<sig_atomic_t* signal_flag>
|
|
|
|
|
static void signal_handler(int)
|
2012-10-27 14:18:52 +02:00
|
|
|
|
{
|
2018-04-29 14:27:28 +02:00
|
|
|
|
*signal_flag = 1;
|
2012-10-27 15:02:17 +02:00
|
|
|
|
EventManager::instance().force_signal(0);
|
2012-10-27 14:18:52 +02:00
|
|
|
|
}
|
2012-09-05 00:49:59 +02:00
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
TerminalUI::TerminalUI()
|
2019-09-04 16:04:27 +02:00
|
|
|
|
: m_cursor{CursorMode::Buffer, {}},
|
2021-03-10 23:02:02 +01:00
|
|
|
|
m_stdin_watcher{STDIN_FILENO, FdEvents::Read, EventMode::Urgent,
|
2018-05-17 14:55:53 +02:00
|
|
|
|
[this](FDWatcher&, FdEvents, EventMode) {
|
2016-11-29 22:35:53 +01:00
|
|
|
|
if (not m_on_key)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
while (auto key = get_next_key())
|
2021-06-27 08:56:29 +02:00
|
|
|
|
{
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (*key == ctrl('z'))
|
2021-06-27 08:56:29 +02:00
|
|
|
|
kill(0, SIGTSTP); // We suspend at this line
|
2023-03-13 10:55:31 +01:00
|
|
|
|
else if (*key != Key::Invalid)
|
2021-06-27 08:56:29 +02:00
|
|
|
|
m_on_key(*key);
|
|
|
|
|
}
|
2015-02-09 14:33:54 +01:00
|
|
|
|
}},
|
2019-02-09 05:41:09 +01:00
|
|
|
|
m_assistant(assistant_clippy)
|
2012-09-05 00:49:59 +02:00
|
|
|
|
{
|
2019-11-08 22:19:45 +01:00
|
|
|
|
if (not isatty(1))
|
|
|
|
|
throw runtime_error("stdout is not a tty");
|
|
|
|
|
|
2019-09-08 14:53:06 +02:00
|
|
|
|
tcgetattr(STDIN_FILENO, &m_original_termios);
|
|
|
|
|
|
2019-11-13 13:31:24 +01:00
|
|
|
|
setup_terminal();
|
|
|
|
|
set_raw_mode();
|
2019-09-09 11:39:53 +02:00
|
|
|
|
enable_mouse(true);
|
|
|
|
|
|
2018-04-29 14:27:28 +02:00
|
|
|
|
set_signal_handler(SIGWINCH, &signal_handler<&resize_pending>);
|
2023-08-30 20:56:50 +02:00
|
|
|
|
set_signal_handler(SIGHUP, &signal_handler<&terminal_hungup>);
|
2019-11-16 11:27:06 +01:00
|
|
|
|
set_signal_handler(SIGTSTP, [](int){ TerminalUI::instance().suspend(); });
|
2015-06-30 01:31:26 +02:00
|
|
|
|
|
|
|
|
|
check_resize(true);
|
2019-09-04 16:04:27 +02:00
|
|
|
|
redraw(false);
|
2012-09-05 00:49:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
TerminalUI::~TerminalUI()
|
2012-09-05 00:49:59 +02:00
|
|
|
|
{
|
2023-08-30 20:56:50 +02:00
|
|
|
|
if (not terminal_hungup) {
|
2023-08-27 00:47:33 +02:00
|
|
|
|
enable_mouse(false);
|
|
|
|
|
restore_terminal();
|
|
|
|
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
|
|
|
|
|
}
|
2016-01-10 21:46:15 +01:00
|
|
|
|
set_signal_handler(SIGWINCH, SIG_DFL);
|
2023-08-27 00:47:33 +02:00
|
|
|
|
set_signal_handler(SIGHUP, SIG_IGN);
|
2019-09-09 12:11:04 +02:00
|
|
|
|
set_signal_handler(SIGTSTP, SIG_DFL);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::suspend()
|
2019-09-09 12:11:04 +02:00
|
|
|
|
{
|
|
|
|
|
bool mouse_enabled = m_mouse_enabled;
|
|
|
|
|
enable_mouse(false);
|
2020-05-17 12:32:25 +02:00
|
|
|
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
restore_terminal();
|
2019-09-09 12:11:04 +02:00
|
|
|
|
|
|
|
|
|
auto current = set_signal_handler(SIGTSTP, SIG_DFL);
|
|
|
|
|
sigset_t unblock_sigtstp, old_mask;
|
|
|
|
|
sigemptyset(&unblock_sigtstp);
|
|
|
|
|
sigaddset(&unblock_sigtstp, SIGTSTP);
|
|
|
|
|
sigprocmask(SIG_UNBLOCK, &unblock_sigtstp, &old_mask);
|
|
|
|
|
|
|
|
|
|
raise(SIGTSTP); // suspend here
|
|
|
|
|
|
|
|
|
|
set_signal_handler(SIGTSTP, current);
|
|
|
|
|
sigprocmask(SIG_SETMASK, &old_mask, nullptr);
|
|
|
|
|
|
2019-11-13 13:31:24 +01:00
|
|
|
|
setup_terminal();
|
2019-09-09 12:11:04 +02:00
|
|
|
|
check_resize(true);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
set_raw_mode();
|
2019-09-09 12:11:04 +02:00
|
|
|
|
enable_mouse(mouse_enabled);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
|
|
|
|
|
refresh(true);
|
2012-09-05 00:49:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::set_raw_mode() const
|
2019-09-09 11:39:53 +02:00
|
|
|
|
{
|
|
|
|
|
termios attr = m_original_termios;
|
|
|
|
|
attr.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
|
|
|
|
attr.c_oflag &= ~OPOST;
|
|
|
|
|
attr.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
|
|
|
|
|
attr.c_lflag |= NOFLSH;
|
|
|
|
|
attr.c_cflag &= ~(CSIZE | PARENB);
|
|
|
|
|
attr.c_cflag |= CS8;
|
|
|
|
|
attr.c_cc[VMIN] = attr.c_cc[VTIME] = 0;
|
|
|
|
|
|
2020-08-02 07:51:53 +02:00
|
|
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &attr);
|
2019-09-09 11:39:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::redraw(bool force)
|
2015-09-11 00:39:19 +02:00
|
|
|
|
{
|
2019-11-30 12:47:18 +01:00
|
|
|
|
m_window.blit(m_screen);
|
2017-04-12 11:39:17 +02:00
|
|
|
|
|
2018-06-20 00:12:53 +02:00
|
|
|
|
if (m_menu.columns != 0 or m_menu.pos.column > m_status_len)
|
2019-11-30 12:47:18 +01:00
|
|
|
|
m_menu.blit(m_screen);
|
2018-06-20 00:12:53 +02:00
|
|
|
|
|
2019-11-30 12:47:18 +01:00
|
|
|
|
m_info.blit(m_screen);
|
2017-04-12 11:39:17 +02:00
|
|
|
|
|
2021-07-31 01:45:36 +02:00
|
|
|
|
Writer writer{STDOUT_FILENO};
|
2021-10-23 00:42:31 +02:00
|
|
|
|
m_screen.output(force, (bool)m_synchronized, writer);
|
2020-10-08 11:56:39 +02:00
|
|
|
|
|
2021-07-31 01:45:36 +02:00
|
|
|
|
auto set_cursor_pos = [&](DisplayCoord c) {
|
|
|
|
|
format_with(writer, "\033[{};{}H", (int)c.line + 1, (int)c.column + 1);
|
2020-10-08 11:56:39 +02:00
|
|
|
|
};
|
2017-04-12 11:39:17 +02:00
|
|
|
|
if (m_cursor.mode == CursorMode::Prompt)
|
2019-11-13 13:31:24 +01:00
|
|
|
|
set_cursor_pos({m_status_on_top ? 0 : m_dimensions.line, m_cursor.coord.column});
|
2017-04-12 11:39:17 +02:00
|
|
|
|
else
|
2019-11-13 13:31:24 +01:00
|
|
|
|
set_cursor_pos(m_cursor.coord + content_line_offset());
|
2012-09-30 15:18:37 +02:00
|
|
|
|
}
|
2014-04-15 20:19:44 +02:00
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::set_cursor(CursorMode mode, DisplayCoord coord)
|
2017-04-12 11:39:17 +02:00
|
|
|
|
{
|
2019-09-04 16:04:27 +02:00
|
|
|
|
m_cursor = Cursor{mode, coord};
|
2017-04-12 11:39:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::refresh(bool force)
|
2014-04-15 20:19:44 +02:00
|
|
|
|
{
|
2016-03-07 14:54:20 +01:00
|
|
|
|
if (m_dirty or force)
|
2019-09-04 16:04:27 +02:00
|
|
|
|
redraw(force);
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-29 12:33:47 +02:00
|
|
|
|
static const DisplayLine empty_line = { String(" "), {} };
|
2016-02-17 14:32:05 +01:00
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::draw(const DisplayBuffer& display_buffer,
|
2020-10-08 11:56:39 +02:00
|
|
|
|
const Face& default_face,
|
|
|
|
|
const Face& padding_face)
|
2012-10-27 14:18:52 +02:00
|
|
|
|
{
|
2014-06-09 14:26:54 +02:00
|
|
|
|
check_resize();
|
|
|
|
|
|
2018-06-08 10:54:11 +02:00
|
|
|
|
const DisplayCoord dim = dimensions();
|
2018-06-03 04:06:29 +02:00
|
|
|
|
const LineCount line_offset = content_line_offset();
|
|
|
|
|
LineCount line_index = line_offset;
|
2012-10-20 20:15:20 +02:00
|
|
|
|
for (const DisplayLine& line : display_buffer.lines())
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_window.draw(line_index++, line.atoms(), default_face);
|
2012-02-16 15:25:16 +01:00
|
|
|
|
|
2019-09-16 15:01:13 +02:00
|
|
|
|
auto face = merge_faces(default_face, padding_face);
|
2020-03-28 02:54:49 +01:00
|
|
|
|
|
|
|
|
|
DisplayAtom padding{String{m_padding_char, m_padding_fill ? dim.column : 1}};
|
|
|
|
|
|
2018-06-08 10:54:11 +02:00
|
|
|
|
while (line_index < dim.line + line_offset)
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_window.draw(line_index++, padding, face);
|
2012-02-16 15:25:16 +01:00
|
|
|
|
|
2015-06-17 22:28:02 +02:00
|
|
|
|
m_dirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::draw_status(const DisplayLine& status_line,
|
2020-10-08 11:56:39 +02:00
|
|
|
|
const DisplayLine& mode_line,
|
|
|
|
|
const Face& default_face)
|
2015-06-17 22:28:02 +02:00
|
|
|
|
{
|
2019-09-04 16:04:27 +02:00
|
|
|
|
const LineCount status_line_pos = m_status_on_top ? 0 : m_dimensions.line;
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_window.draw(status_line_pos, status_line.atoms(), default_face);
|
2015-06-29 23:48:26 +02:00
|
|
|
|
|
2015-04-23 22:11:50 +02:00
|
|
|
|
const auto mode_len = mode_line.length();
|
2018-06-20 00:12:53 +02:00
|
|
|
|
m_status_len = status_line.length();
|
|
|
|
|
const auto remaining = m_dimensions.column - m_status_len;
|
2015-04-23 22:11:50 +02:00
|
|
|
|
if (mode_len < remaining)
|
2012-10-29 19:01:57 +01:00
|
|
|
|
{
|
2016-09-22 21:36:26 +02:00
|
|
|
|
ColumnCount col = m_dimensions.column - mode_len;
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_window.draw({status_line_pos, col}, mode_line.atoms(), default_face);
|
2012-10-29 19:01:57 +01:00
|
|
|
|
}
|
2015-04-23 22:11:50 +02:00
|
|
|
|
else if (remaining > 2)
|
|
|
|
|
{
|
|
|
|
|
DisplayLine trimmed_mode_line = mode_line;
|
2017-06-09 16:30:13 +02:00
|
|
|
|
trimmed_mode_line.trim(mode_len + 2 - remaining, remaining - 2);
|
2018-04-29 12:33:47 +02:00
|
|
|
|
trimmed_mode_line.insert(trimmed_mode_line.begin(), { "…", {} });
|
2015-04-23 22:11:50 +02:00
|
|
|
|
kak_assert(trimmed_mode_line.length() == remaining - 1);
|
|
|
|
|
|
2016-09-22 21:36:26 +02:00
|
|
|
|
ColumnCount col = m_dimensions.column - remaining + 1;
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_window.draw({status_line_pos, col}, trimmed_mode_line.atoms(), default_face);
|
2015-04-23 22:11:50 +02:00
|
|
|
|
}
|
2013-04-18 20:22:19 +02:00
|
|
|
|
|
2015-09-03 01:03:07 +02:00
|
|
|
|
if (m_set_title)
|
2013-04-18 20:22:19 +02:00
|
|
|
|
{
|
2021-07-31 01:45:36 +02:00
|
|
|
|
Writer writer{STDOUT_FILENO};
|
2016-10-04 21:13:15 +02:00
|
|
|
|
constexpr char suffix[] = " - Kakoune\007";
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write("\033]2;");
|
2016-10-04 21:13:15 +02:00
|
|
|
|
// Fill title escape sequence buffer, removing non ascii characters
|
2013-04-18 20:22:19 +02:00
|
|
|
|
for (auto& atom : mode_line)
|
2016-10-04 21:13:15 +02:00
|
|
|
|
{
|
|
|
|
|
const auto str = atom.content();
|
2021-07-31 01:45:36 +02:00
|
|
|
|
for (auto it = str.begin(), end = str.end(); it != end; utf8::to_next(it, end))
|
|
|
|
|
writer.write((*it >= 0x20 and *it <= 0x7e) ? *it : '?');
|
2016-10-04 21:13:15 +02:00
|
|
|
|
}
|
2021-07-31 01:45:36 +02:00
|
|
|
|
writer.write(suffix);
|
2013-04-18 20:22:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = true;
|
2012-02-16 15:25:16 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::check_resize(bool force)
|
2014-06-09 14:26:54 +02:00
|
|
|
|
{
|
2015-06-30 01:31:26 +02:00
|
|
|
|
if (not force and not resize_pending)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
resize_pending = 0;
|
|
|
|
|
|
|
|
|
|
const int fd = open("/dev/tty", O_RDWR);
|
2018-04-04 06:01:24 +02:00
|
|
|
|
if (fd < 0)
|
|
|
|
|
return;
|
2018-03-31 14:00:19 +02:00
|
|
|
|
auto close_fd = on_scope_end([fd]{ ::close(fd); });
|
2015-09-08 00:52:34 +02:00
|
|
|
|
|
2021-08-04 12:47:04 +02:00
|
|
|
|
DisplayCoord terminal_size{24_line, 80_col};
|
|
|
|
|
if (winsize ws; ioctl(fd, TIOCGWINSZ, &ws) == 0 and ws.ws_row > 0 and ws.ws_col > 0)
|
|
|
|
|
terminal_size = {ws.ws_row, ws.ws_col};
|
2015-06-30 01:31:26 +02:00
|
|
|
|
|
2018-03-31 14:00:19 +02:00
|
|
|
|
const bool info = (bool)m_info;
|
|
|
|
|
const bool menu = (bool)m_menu;
|
2019-09-04 16:04:27 +02:00
|
|
|
|
if (m_window) m_window.destroy();
|
2018-03-31 14:00:19 +02:00
|
|
|
|
if (info) m_info.destroy();
|
|
|
|
|
if (menu) m_menu.destroy();
|
2015-06-30 01:31:26 +02:00
|
|
|
|
|
2021-08-04 12:47:04 +02:00
|
|
|
|
m_window.create({0, 0}, terminal_size);
|
|
|
|
|
m_screen.create({0, 0}, terminal_size);
|
2021-07-31 07:06:21 +02:00
|
|
|
|
m_screen.hashes.reset(new size_t[(int)terminal_size.line]{});
|
2018-07-31 14:15:39 +02:00
|
|
|
|
kak_assert(m_window);
|
2015-10-13 14:52:02 +02:00
|
|
|
|
|
2021-08-04 12:47:04 +02:00
|
|
|
|
m_dimensions = terminal_size - 1_line;
|
2018-03-31 14:00:19 +02:00
|
|
|
|
|
2019-11-13 13:31:24 +01:00
|
|
|
|
// if (char* csr = tigetstr((char*)"csr"))
|
|
|
|
|
// putp(tparm(csr, 0, ws.ws_row));
|
2018-03-31 14:00:19 +02:00
|
|
|
|
|
|
|
|
|
if (menu)
|
2019-09-09 14:36:58 +02:00
|
|
|
|
menu_show(Vector<DisplayLine>(std::move(m_menu.items)),
|
|
|
|
|
m_menu.anchor, m_menu.fg, m_menu.bg, m_menu.style);
|
2018-03-31 14:00:19 +02:00
|
|
|
|
if (info)
|
|
|
|
|
info_show(m_info.title, m_info.content, m_info.anchor, m_info.face, m_info.style);
|
2015-06-30 01:31:26 +02:00
|
|
|
|
|
2018-06-19 12:20:38 +02:00
|
|
|
|
set_resize_pending();
|
2014-06-09 14:26:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
Optional<Key> TerminalUI::get_next_key()
|
2012-10-28 09:26:54 +01:00
|
|
|
|
{
|
2023-08-30 20:56:50 +02:00
|
|
|
|
if (terminal_hungup)
|
2018-04-29 14:27:28 +02:00
|
|
|
|
{
|
|
|
|
|
set_signal_handler(SIGWINCH, SIG_DFL);
|
2023-08-27 00:47:33 +02:00
|
|
|
|
set_signal_handler(SIGHUP, SIG_IGN);
|
2019-09-17 11:24:19 +02:00
|
|
|
|
if (m_window)
|
|
|
|
|
m_window.destroy();
|
2018-07-08 07:54:01 +02:00
|
|
|
|
m_stdin_watcher.disable();
|
2016-11-29 22:35:53 +01:00
|
|
|
|
return {};
|
2018-04-29 14:27:28 +02:00
|
|
|
|
}
|
2016-11-14 01:49:34 +01:00
|
|
|
|
|
2014-06-09 14:26:54 +02:00
|
|
|
|
check_resize();
|
|
|
|
|
|
2018-06-19 12:20:38 +02:00
|
|
|
|
if (m_resize_pending)
|
2018-06-08 10:54:11 +02:00
|
|
|
|
{
|
2018-06-19 12:20:38 +02:00
|
|
|
|
m_resize_pending = false;
|
|
|
|
|
return resize(dimensions());
|
2018-06-08 10:54:11 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-09 14:47:25 +02:00
|
|
|
|
static auto get_char = []() -> Optional<unsigned char> {
|
2020-11-02 12:52:59 +01:00
|
|
|
|
if (not fd_readable(STDIN_FILENO))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
if (unsigned char c = 0; read(STDIN_FILENO, &c, 1) == 1)
|
2019-09-08 09:19:41 +02:00
|
|
|
|
return c;
|
2020-11-02 12:52:59 +01:00
|
|
|
|
|
2023-08-30 20:56:50 +02:00
|
|
|
|
terminal_hungup = 1;
|
2019-09-08 09:19:41 +02:00
|
|
|
|
return {};
|
|
|
|
|
};
|
2012-10-28 09:26:54 +01:00
|
|
|
|
|
2019-09-08 09:19:41 +02:00
|
|
|
|
const auto c = get_char();
|
|
|
|
|
if (not c)
|
2018-12-09 11:04:22 +01:00
|
|
|
|
return {};
|
|
|
|
|
|
2020-10-08 11:56:39 +02:00
|
|
|
|
static constexpr auto control = [](char c) { return c & 037; };
|
|
|
|
|
|
2023-03-11 16:19:15 +01:00
|
|
|
|
static auto convert = [this](Codepoint c) -> Codepoint {
|
2019-09-08 09:19:41 +02:00
|
|
|
|
if (c == control('m') or c == control('j'))
|
2020-05-12 00:50:27 +02:00
|
|
|
|
return Key::Return;
|
2019-09-08 09:19:41 +02:00
|
|
|
|
if (c == control('i'))
|
2020-05-12 00:50:27 +02:00
|
|
|
|
return Key::Tab;
|
2021-12-10 22:12:08 +01:00
|
|
|
|
if (c == ' ')
|
|
|
|
|
return Key::Space;
|
2020-10-08 11:56:39 +02:00
|
|
|
|
if (c == m_original_termios.c_cc[VERASE])
|
2020-05-12 00:50:27 +02:00
|
|
|
|
return Key::Backspace;
|
2020-11-24 09:14:04 +01:00
|
|
|
|
if (c == 127) // when it's not backspace
|
|
|
|
|
return Key::Delete;
|
2021-02-11 10:14:19 +01:00
|
|
|
|
if (c == 27)
|
|
|
|
|
return Key::Escape;
|
2020-05-12 00:50:27 +02:00
|
|
|
|
return c;
|
|
|
|
|
};
|
2023-03-11 16:19:15 +01:00
|
|
|
|
static auto parse_key = [](unsigned char c) -> Key {
|
2020-05-12 00:50:27 +02:00
|
|
|
|
if (Codepoint cp = convert(c); cp > 255)
|
|
|
|
|
return Key{cp};
|
2020-10-30 14:43:34 +01:00
|
|
|
|
// Special case: you can type NUL with Ctrl-2 or Ctrl-Shift-2 or
|
|
|
|
|
// Ctrl-Backtick, but the most straightforward way is Ctrl-Space.
|
|
|
|
|
if (c == 0)
|
2021-12-10 22:12:08 +01:00
|
|
|
|
return ctrl(Key::Space);
|
2020-10-30 14:43:34 +01:00
|
|
|
|
// Represent Ctrl-letter combinations in lower-case, to be clear
|
|
|
|
|
// that Shift is not involved.
|
2019-09-08 09:19:41 +02:00
|
|
|
|
if (c < 27)
|
2020-05-12 00:50:27 +02:00
|
|
|
|
return ctrl(c - 1 + 'a');
|
2020-10-30 14:43:34 +01:00
|
|
|
|
// Represent Ctrl-symbol combinations in "upper-case", as they are
|
|
|
|
|
// traditionally-rendered.
|
|
|
|
|
// Note that Escape is handled elsewhere.
|
|
|
|
|
if (c < 32)
|
|
|
|
|
return ctrl(c - 1 + 'A');
|
2019-09-08 09:19:41 +02:00
|
|
|
|
|
2019-09-09 14:36:35 +02:00
|
|
|
|
struct Sentinel{};
|
|
|
|
|
struct CharIterator
|
2019-09-08 09:19:41 +02:00
|
|
|
|
{
|
2019-09-09 14:36:35 +02:00
|
|
|
|
unsigned char operator*() { if (not c) c = get_char().value_or((unsigned char)0); return *c; }
|
|
|
|
|
CharIterator& operator++() { c.reset(); return *this; }
|
|
|
|
|
bool operator==(const Sentinel&) const { return false; }
|
2019-09-08 09:19:41 +02:00
|
|
|
|
Optional<unsigned char> c;
|
|
|
|
|
};
|
2019-09-09 14:36:35 +02:00
|
|
|
|
return Key{utf8::codepoint(CharIterator{c}, Sentinel{})};
|
2017-01-30 14:38:56 +01:00
|
|
|
|
};
|
|
|
|
|
|
2023-03-11 16:19:15 +01:00
|
|
|
|
static auto parse_mask = [](int mask) {
|
2021-03-26 05:17:41 +01:00
|
|
|
|
Key::Modifiers mod = Key::Modifiers::None;
|
|
|
|
|
if (mask & 1)
|
|
|
|
|
mod |= Key::Modifiers::Shift;
|
|
|
|
|
if (mask & 2)
|
|
|
|
|
mod |= Key::Modifiers::Alt;
|
|
|
|
|
if (mask & 4)
|
|
|
|
|
mod |= Key::Modifiers::Control;
|
|
|
|
|
return mod;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-13 10:55:31 +01:00
|
|
|
|
auto parse_csi = [this]() -> Optional<Key> {
|
2019-09-08 09:19:41 +02:00
|
|
|
|
auto next_char = [] { return get_char().value_or((unsigned char)0xff); };
|
2021-10-30 14:21:51 +02:00
|
|
|
|
int params[16][4] = {};
|
2019-09-08 09:19:41 +02:00
|
|
|
|
auto c = next_char();
|
2019-09-07 07:34:58 +02:00
|
|
|
|
char private_mode = 0;
|
|
|
|
|
if (c == '?' or c == '<' or c == '=' or c == '>')
|
2018-12-09 01:20:03 +01:00
|
|
|
|
{
|
2019-09-07 07:34:58 +02:00
|
|
|
|
private_mode = c;
|
2019-09-08 09:19:41 +02:00
|
|
|
|
c = next_char();
|
2019-09-07 07:34:58 +02:00
|
|
|
|
}
|
2021-10-30 14:21:51 +02:00
|
|
|
|
for (int count = 0, subcount = 0; count < 16 and c >= 0x30 && c <= 0x3f; c = next_char())
|
2019-09-07 07:34:58 +02:00
|
|
|
|
{
|
2019-11-02 22:44:32 +01:00
|
|
|
|
if (isdigit(c))
|
2021-10-30 14:21:51 +02:00
|
|
|
|
params[count][subcount] = params[count][subcount] * 10 + c - '0';
|
|
|
|
|
else if (c == ':' && subcount < 3)
|
|
|
|
|
++subcount;
|
2019-09-07 07:34:58 +02:00
|
|
|
|
else if (c == ';')
|
2021-10-30 14:21:51 +02:00
|
|
|
|
{
|
2019-09-07 07:34:58 +02:00
|
|
|
|
++count;
|
2021-10-30 14:21:51 +02:00
|
|
|
|
subcount = 0;
|
|
|
|
|
}
|
2019-09-07 07:34:58 +02:00
|
|
|
|
else
|
2018-12-09 11:04:22 +01:00
|
|
|
|
return {};
|
2019-09-07 07:34:58 +02:00
|
|
|
|
}
|
2019-12-14 20:03:48 +01:00
|
|
|
|
if (c != '$' and (c < 0x40 or c > 0x7e))
|
2019-09-07 07:34:58 +02:00
|
|
|
|
return {};
|
2018-12-09 11:04:22 +01:00
|
|
|
|
|
2020-06-28 11:48:55 +02:00
|
|
|
|
auto mouse_button = [this](Key::Modifiers mod, Key::MouseButton button, Codepoint coord, bool release) {
|
|
|
|
|
auto mask = 1 << (int)button;
|
2019-09-07 07:34:58 +02:00
|
|
|
|
if (not release)
|
|
|
|
|
{
|
2020-06-28 11:48:55 +02:00
|
|
|
|
mod |= (m_mouse_state & mask) ? Key::Modifiers::MousePos : Key::Modifiers::MousePress;
|
2019-09-07 07:34:58 +02:00
|
|
|
|
m_mouse_state |= mask;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2020-06-28 11:48:55 +02:00
|
|
|
|
mod |= Key::Modifiers::MouseRelease;
|
2019-09-07 07:34:58 +02:00
|
|
|
|
m_mouse_state &= ~mask;
|
|
|
|
|
}
|
2020-06-28 11:48:55 +02:00
|
|
|
|
return Key{mod | Key::to_modifier(button), coord};
|
2019-09-07 07:34:58 +02:00
|
|
|
|
};
|
|
|
|
|
|
2019-09-08 09:19:41 +02:00
|
|
|
|
auto mouse_scroll = [this](Key::Modifiers mod, bool down) -> Key {
|
|
|
|
|
return {mod | Key::Modifiers::Scroll,
|
|
|
|
|
(Codepoint)((down ? 1 : -1) * m_wheel_scroll_amount)};
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-30 14:21:51 +02:00
|
|
|
|
auto masked_key = [&](Codepoint key, Codepoint shifted_key = 0) {
|
|
|
|
|
int mask = std::max(params[1][0] - 1, 0);
|
2021-05-28 12:15:56 +02:00
|
|
|
|
Key::Modifiers modifiers = parse_mask(mask);
|
2021-10-30 14:21:51 +02:00
|
|
|
|
if (shifted_key != 0 and (modifiers & Key::Modifiers::Shift))
|
2021-05-28 12:15:56 +02:00
|
|
|
|
{
|
|
|
|
|
modifiers &= ~Key::Modifiers::Shift;
|
2021-10-30 14:21:51 +02:00
|
|
|
|
key = shifted_key;
|
2021-05-28 12:15:56 +02:00
|
|
|
|
}
|
|
|
|
|
return Key{modifiers, key};
|
2021-04-30 06:48:37 +02:00
|
|
|
|
};
|
2019-09-08 09:19:41 +02:00
|
|
|
|
|
|
|
|
|
switch (c)
|
2019-09-07 07:34:58 +02:00
|
|
|
|
{
|
2019-12-14 20:03:48 +01:00
|
|
|
|
case '$':
|
2021-10-23 00:42:31 +02:00
|
|
|
|
if (private_mode == '?' and next_char() == 'y') // DECRPM
|
|
|
|
|
{
|
2021-10-30 14:21:51 +02:00
|
|
|
|
if (params[0][0] == 2026)
|
|
|
|
|
m_synchronized.supported = (params[1][0] == 1 or params[1][0] == 2);
|
2022-12-11 19:30:02 +01:00
|
|
|
|
return Key{Key::Invalid};
|
2021-10-23 00:42:31 +02:00
|
|
|
|
}
|
2021-10-30 14:21:51 +02:00
|
|
|
|
switch (params[0][0])
|
2019-12-14 20:03:48 +01:00
|
|
|
|
{
|
|
|
|
|
case 23: case 24:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return Key{Key::Modifiers::Shift, Key::F11 + params[0][0] - 23}; // rxvt style
|
2019-12-14 20:03:48 +01:00
|
|
|
|
}
|
|
|
|
|
return {};
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 'A': return masked_key(Key::Up);
|
|
|
|
|
case 'B': return masked_key(Key::Down);
|
|
|
|
|
case 'C': return masked_key(Key::Right);
|
|
|
|
|
case 'D': return masked_key(Key::Left);
|
2021-03-26 05:43:42 +01:00
|
|
|
|
case 'E': return masked_key('5'); // Numeric keypad 5
|
2019-12-11 12:50:48 +01:00
|
|
|
|
case 'F': return masked_key(Key::End); // PC/xterm style
|
|
|
|
|
case 'H': return masked_key(Key::Home); // PC/xterm style
|
2020-03-04 09:06:46 +01:00
|
|
|
|
case 'P': return masked_key(Key::F1);
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 'Q': return masked_key(Key::F2);
|
|
|
|
|
case 'R': return masked_key(Key::F3);
|
|
|
|
|
case 'S': return masked_key(Key::F4);
|
|
|
|
|
case '~':
|
2021-10-30 14:21:51 +02:00
|
|
|
|
switch (params[0][0])
|
2019-09-07 07:34:58 +02:00
|
|
|
|
{
|
2019-12-11 12:50:48 +01:00
|
|
|
|
case 1: return masked_key(Key::Home); // VT220/tmux style
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 2: return masked_key(Key::Insert);
|
|
|
|
|
case 3: return masked_key(Key::Delete);
|
2019-12-11 12:50:48 +01:00
|
|
|
|
case 4: return masked_key(Key::End); // VT220/tmux style
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 5: return masked_key(Key::PageUp);
|
|
|
|
|
case 6: return masked_key(Key::PageDown);
|
2019-12-11 12:50:48 +01:00
|
|
|
|
case 7: return masked_key(Key::Home); // rxvt style
|
|
|
|
|
case 8: return masked_key(Key::End); // rxvt style
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 11: case 12: case 13: case 14: case 15:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return masked_key(Key::F1 + params[0][0] - 11);
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 17: case 18: case 19: case 20: case 21:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return masked_key(Key::F6 + params[0][0] - 17);
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 23: case 24:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return masked_key(Key::F11 + params[0][0] - 23);
|
2019-12-14 23:17:34 +01:00
|
|
|
|
case 25: case 26:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return Key{Key::Modifiers::Shift, Key::F3 + params[0][0] - 25}; // rxvt style
|
2019-12-14 23:17:34 +01:00
|
|
|
|
case 28: case 29:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return Key{Key::Modifiers::Shift, Key::F5 + params[0][0] - 28}; // rxvt style
|
2019-12-14 23:17:34 +01:00
|
|
|
|
case 31: case 32:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return Key{Key::Modifiers::Shift, Key::F7 + params[0][0] - 31}; // rxvt style
|
2019-12-14 23:17:34 +01:00
|
|
|
|
case 33: case 34:
|
2021-10-30 14:21:51 +02:00
|
|
|
|
return Key{Key::Modifiers::Shift, Key::F9 + params[0][0] - 33}; // rxvt style
|
2022-12-11 19:30:02 +01:00
|
|
|
|
case 200:
|
2023-03-13 10:55:31 +01:00
|
|
|
|
m_paste_buffer = String{};
|
|
|
|
|
return Key{Key::Invalid};
|
2022-12-11 19:30:02 +01:00
|
|
|
|
case 201:
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (m_paste_buffer)
|
|
|
|
|
{
|
|
|
|
|
m_on_paste(*m_paste_buffer);
|
|
|
|
|
m_paste_buffer.reset();
|
|
|
|
|
}
|
|
|
|
|
return Key{Key::Invalid};
|
2019-09-07 07:34:58 +02:00
|
|
|
|
}
|
2019-09-08 09:19:41 +02:00
|
|
|
|
return {};
|
2019-11-04 11:51:03 +01:00
|
|
|
|
case 'u':
|
2023-06-17 14:53:44 +02:00
|
|
|
|
{
|
|
|
|
|
Codepoint key;
|
|
|
|
|
switch (params[0][0])
|
|
|
|
|
{
|
|
|
|
|
// Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here.
|
|
|
|
|
case 57414: key = Key::Return; break;
|
|
|
|
|
case 57417: key = Key::Left; break;
|
|
|
|
|
case 57418: key = Key::Right; break;
|
|
|
|
|
case 57419: key = Key::Up; break;
|
|
|
|
|
case 57420: key = Key::Down; break;
|
|
|
|
|
case 57421: key = Key::PageUp; break;
|
|
|
|
|
case 57422: key = Key::PageDown; break;
|
|
|
|
|
case 57423: key = Key::Home; break;
|
|
|
|
|
case 57424: key = Key::End; break;
|
|
|
|
|
case 57425: key = Key::Insert; break;
|
|
|
|
|
case 57426: key = Key::Delete; break;
|
|
|
|
|
default: key = convert(static_cast<Codepoint>(params[0][0])); break;
|
|
|
|
|
}
|
|
|
|
|
return masked_key(key, convert(static_cast<Codepoint>(params[0][1])));
|
|
|
|
|
}
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 'Z': return shift(Key::Tab);
|
|
|
|
|
case 'I': return {Key::FocusIn};
|
|
|
|
|
case 'O': return {Key::FocusOut};
|
2020-02-26 07:41:12 +01:00
|
|
|
|
case 'M': case 'm':
|
2019-09-08 09:19:41 +02:00
|
|
|
|
const bool sgr = private_mode == '<';
|
|
|
|
|
if (not sgr and c != 'M')
|
|
|
|
|
return {};
|
|
|
|
|
|
2021-10-30 14:21:51 +02:00
|
|
|
|
const Codepoint b = sgr ? params[0][0] : next_char() - 32;
|
|
|
|
|
const int x = (sgr ? params[1][0] : next_char() - 32) - 1;
|
|
|
|
|
const int y = (sgr ? params[2][0] : next_char() - 32) - 1;
|
2019-09-07 07:34:58 +02:00
|
|
|
|
auto coord = encode_coord({y - content_line_offset(), x});
|
2019-09-08 09:19:41 +02:00
|
|
|
|
Key::Modifiers mod = parse_mask((b >> 2) & 0x7);
|
2020-06-28 23:32:12 +02:00
|
|
|
|
switch (const int code = b & 0x43; code)
|
2019-09-07 07:34:58 +02:00
|
|
|
|
{
|
2020-06-28 11:48:55 +02:00
|
|
|
|
case 0: case 1: case 2:
|
|
|
|
|
return mouse_button(mod, Key::MouseButton{code}, coord, c == 'm');
|
2019-09-07 07:34:58 +02:00
|
|
|
|
case 3:
|
2019-09-08 09:19:41 +02:00
|
|
|
|
if (sgr)
|
|
|
|
|
return {};
|
2020-06-28 11:48:55 +02:00
|
|
|
|
else if (int guess = ffs(m_mouse_state) - 1; 0 <= guess and guess < 3)
|
|
|
|
|
return mouse_button(mod, Key::MouseButton{guess}, coord, true);
|
2019-09-07 07:34:58 +02:00
|
|
|
|
break;
|
2019-09-08 09:19:41 +02:00
|
|
|
|
case 64: return mouse_scroll(mod, false);
|
|
|
|
|
case 65: return mouse_scroll(mod, true);
|
2019-09-07 07:34:58 +02:00
|
|
|
|
}
|
|
|
|
|
return Key{Key::Modifiers::MousePos, coord};
|
|
|
|
|
}
|
2018-12-09 01:20:03 +01:00
|
|
|
|
return {};
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-11 16:19:15 +01:00
|
|
|
|
static auto parse_ss3 = []() -> Optional<Key> {
|
2021-03-26 05:43:42 +01:00
|
|
|
|
int raw_mask = 0;
|
|
|
|
|
char code = '0';
|
|
|
|
|
do {
|
|
|
|
|
raw_mask = raw_mask * 10 + (code - '0');
|
|
|
|
|
code = get_char().value_or((unsigned char)0xff);
|
|
|
|
|
} while (code >= '0' and code <= '9');
|
2021-04-30 06:48:37 +02:00
|
|
|
|
Key::Modifiers mod = parse_mask(std::max(raw_mask - 1, 0));
|
2021-03-26 05:43:42 +01:00
|
|
|
|
|
|
|
|
|
switch (code)
|
2019-09-22 01:16:08 +02:00
|
|
|
|
{
|
2021-12-10 22:12:08 +01:00
|
|
|
|
case ' ': return Key{mod, Key::Space};
|
2021-04-30 06:48:37 +02:00
|
|
|
|
case 'A': return Key{mod, Key::Up};
|
|
|
|
|
case 'B': return Key{mod, Key::Down};
|
|
|
|
|
case 'C': return Key{mod, Key::Right};
|
|
|
|
|
case 'D': return Key{mod, Key::Left};
|
|
|
|
|
case 'F': return Key{mod, Key::End};
|
|
|
|
|
case 'H': return Key{mod, Key::Home};
|
|
|
|
|
case 'I': return Key{mod, Key::Tab};
|
|
|
|
|
case 'M': return Key{mod, Key::Return};
|
|
|
|
|
case 'P': return Key{mod, Key::F1};
|
|
|
|
|
case 'Q': return Key{mod, Key::F2};
|
|
|
|
|
case 'R': return Key{mod, Key::F3};
|
|
|
|
|
case 'S': return Key{mod, Key::F4};
|
|
|
|
|
case 'X': return Key{mod, '='};
|
|
|
|
|
case 'j': return Key{mod, '*'};
|
|
|
|
|
case 'k': return Key{mod, '+'};
|
|
|
|
|
case 'l': return Key{mod, ','};
|
|
|
|
|
case 'm': return Key{mod, '-'};
|
|
|
|
|
case 'n': return Key{mod, '.'};
|
|
|
|
|
case 'o': return Key{mod, '/'};
|
|
|
|
|
case 'p': return Key{mod, '0'};
|
|
|
|
|
case 'q': return Key{mod, '1'};
|
|
|
|
|
case 'r': return Key{mod, '2'};
|
|
|
|
|
case 's': return Key{mod, '3'};
|
|
|
|
|
case 't': return Key{mod, '4'};
|
|
|
|
|
case 'u': return Key{mod, '5'};
|
|
|
|
|
case 'v': return Key{mod, '6'};
|
|
|
|
|
case 'w': return Key{mod, '7'};
|
|
|
|
|
case 'x': return Key{mod, '8'};
|
|
|
|
|
case 'y': return Key{mod, '9'};
|
2019-09-22 01:16:08 +02:00
|
|
|
|
default: return {};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (*c == 27)
|
2022-12-11 19:30:02 +01:00
|
|
|
|
{
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (auto next = get_char())
|
2022-12-11 19:30:02 +01:00
|
|
|
|
{
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (*next == '[') // potential CSI
|
|
|
|
|
return parse_csi().value_or(alt('['));
|
|
|
|
|
if (*next == 'O') // potential SS3
|
|
|
|
|
return parse_ss3().value_or(alt('O'));
|
|
|
|
|
return alt(parse_key(*next));
|
2022-12-11 19:30:02 +01:00
|
|
|
|
}
|
2023-03-13 10:55:31 +01:00
|
|
|
|
else if (not m_paste_buffer)
|
|
|
|
|
return Key{Key::Escape};
|
2022-12-11 19:30:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-13 10:55:31 +01:00
|
|
|
|
if (m_paste_buffer)
|
2012-02-16 15:25:16 +01:00
|
|
|
|
{
|
2023-03-13 18:48:11 +01:00
|
|
|
|
auto paste_convert = [](char c) {
|
|
|
|
|
switch (c)
|
|
|
|
|
{
|
|
|
|
|
case '\r': return '\n';
|
|
|
|
|
default: return c;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
m_paste_buffer->push_back(paste_convert(*c));
|
2023-03-13 10:55:31 +01:00
|
|
|
|
return Key{Key::Invalid};
|
2012-09-07 20:22:19 +02:00
|
|
|
|
}
|
2023-03-13 10:55:31 +01:00
|
|
|
|
return parse_key(*c);
|
2012-02-16 15:25:16 +01:00
|
|
|
|
}
|
|
|
|
|
|
2013-10-17 01:22:06 +02:00
|
|
|
|
template<typename T>
|
|
|
|
|
T div_round_up(T a, T b)
|
|
|
|
|
{
|
|
|
|
|
return (a - T(1)) / b + T(1);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::draw_menu()
|
2013-03-14 19:19:33 +01:00
|
|
|
|
{
|
2013-07-12 14:15:56 +02:00
|
|
|
|
// menu show may have not created the window if it did not fit.
|
|
|
|
|
// so be tolerant.
|
2015-09-11 00:39:19 +02:00
|
|
|
|
if (not m_menu)
|
2013-07-12 14:15:56 +02:00
|
|
|
|
return;
|
2013-03-14 19:19:33 +01:00
|
|
|
|
|
2016-02-27 18:22:31 +01:00
|
|
|
|
const int item_count = (int)m_menu.items.size();
|
2018-06-19 12:20:38 +02:00
|
|
|
|
if (m_menu.columns == 0)
|
|
|
|
|
{
|
2018-06-20 12:20:11 +02:00
|
|
|
|
const auto win_width = m_menu.size.column - 4;
|
2018-06-19 12:20:38 +02:00
|
|
|
|
kak_assert(m_menu.size.line == 1);
|
|
|
|
|
ColumnCount pos = 0;
|
|
|
|
|
|
2020-03-28 10:17:39 +01:00
|
|
|
|
m_menu.draw({0, 0}, DisplayAtom(m_menu.first_item > 0 ? "< " : ""), m_menu.bg);
|
2018-06-19 12:20:38 +02:00
|
|
|
|
|
|
|
|
|
int i = m_menu.first_item;
|
2018-06-20 12:20:11 +02:00
|
|
|
|
for (; i < item_count and pos < win_width; ++i)
|
2018-06-19 12:20:38 +02:00
|
|
|
|
{
|
|
|
|
|
const DisplayLine& item = m_menu.items[i];
|
2018-06-20 12:20:11 +02:00
|
|
|
|
const ColumnCount item_width = item.length();
|
2019-09-16 15:01:13 +02:00
|
|
|
|
auto& face = i == m_menu.selected_item ? m_menu.fg : m_menu.bg;
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_menu.draw({0, pos+2}, item.atoms(), face);
|
2020-03-28 10:17:39 +01:00
|
|
|
|
if (pos + item_width >= win_width)
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_menu.draw({0, win_width+2}, DisplayAtom("…"), m_menu.bg);
|
2018-06-20 12:20:11 +02:00
|
|
|
|
pos += item_width + 1;
|
2018-06-19 12:20:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-28 10:17:39 +01:00
|
|
|
|
if (i != item_count)
|
|
|
|
|
m_menu.draw({0, win_width+3}, DisplayAtom(">"), m_menu.bg);
|
2019-09-16 15:01:13 +02:00
|
|
|
|
|
2018-06-19 12:20:38 +02:00
|
|
|
|
m_dirty = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-27 18:22:31 +01:00
|
|
|
|
const LineCount menu_lines = div_round_up(item_count, m_menu.columns);
|
2018-06-03 01:08:02 +02:00
|
|
|
|
const LineCount win_height = m_menu.size.line;
|
2013-10-17 19:48:12 +02:00
|
|
|
|
kak_assert(win_height <= menu_lines);
|
2013-10-17 01:22:06 +02:00
|
|
|
|
|
2016-09-22 21:36:26 +02:00
|
|
|
|
const ColumnCount column_width = (m_menu.size.column - 1) / m_menu.columns;
|
2013-10-17 01:22:06 +02:00
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
const LineCount mark_height = min(div_round_up(sq(win_height), menu_lines),
|
|
|
|
|
win_height);
|
2018-07-20 19:30:47 +02:00
|
|
|
|
|
|
|
|
|
const int menu_cols = div_round_up(item_count, (int)m_menu.size.line);
|
|
|
|
|
const int first_col = m_menu.first_item / (int)m_menu.size.line;
|
|
|
|
|
|
2018-08-10 19:14:48 +02:00
|
|
|
|
const LineCount mark_line = (win_height - mark_height) * first_col / max(1, menu_cols - m_menu.columns);
|
2018-07-20 19:30:47 +02:00
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
for (auto line = 0_line; line < win_height; ++line)
|
2013-03-14 19:19:33 +01:00
|
|
|
|
{
|
2016-02-27 18:22:31 +01:00
|
|
|
|
for (int col = 0; col < m_menu.columns; ++col)
|
2013-03-14 19:19:33 +01:00
|
|
|
|
{
|
2018-08-10 19:14:48 +02:00
|
|
|
|
int item_idx = (first_col + col) * (int)m_menu.size.line + (int)line;
|
2019-09-16 15:01:13 +02:00
|
|
|
|
auto& face = item_idx < item_count and item_idx == m_menu.selected_item ? m_menu.fg : m_menu.bg;
|
2019-11-13 12:10:42 +01:00
|
|
|
|
auto atoms = item_idx < item_count ? m_menu.items[item_idx].atoms() : ConstArrayView<DisplayAtom>{};
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_menu.draw({line, col * column_width}, atoms, face);
|
2013-03-14 19:19:33 +01:00
|
|
|
|
}
|
2019-09-16 15:01:13 +02:00
|
|
|
|
const bool is_mark = line >= mark_line and line < mark_line + mark_height;
|
2020-03-28 02:54:49 +01:00
|
|
|
|
m_menu.draw({line, m_menu.size.column - 1}, DisplayAtom(is_mark ? "█" : "░"), m_menu.bg);
|
2013-03-14 19:19:33 +01:00
|
|
|
|
}
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = true;
|
2013-03-14 19:19:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-03 04:15:02 +02:00
|
|
|
|
static LineCount height_limit(MenuStyle style)
|
|
|
|
|
{
|
|
|
|
|
switch (style)
|
|
|
|
|
{
|
|
|
|
|
case MenuStyle::Inline: return 10_line;
|
|
|
|
|
case MenuStyle::Prompt: return 10_line;
|
|
|
|
|
case MenuStyle::Search: return 3_line;
|
|
|
|
|
}
|
|
|
|
|
kak_assert(false);
|
|
|
|
|
return 0_line;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::menu_show(ConstArrayView<DisplayLine> items,
|
2020-10-08 11:56:39 +02:00
|
|
|
|
DisplayCoord anchor, Face fg, Face bg,
|
|
|
|
|
MenuStyle style)
|
2012-08-30 21:14:28 +02:00
|
|
|
|
{
|
2018-06-03 04:06:29 +02:00
|
|
|
|
if (m_menu)
|
|
|
|
|
{
|
|
|
|
|
m_menu.destroy();
|
|
|
|
|
m_dirty = true;
|
|
|
|
|
}
|
2012-11-21 19:01:13 +01:00
|
|
|
|
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.fg = fg;
|
|
|
|
|
m_menu.bg = bg;
|
|
|
|
|
m_menu.style = style;
|
|
|
|
|
m_menu.anchor = anchor;
|
2013-04-04 13:53:47 +02:00
|
|
|
|
|
2018-03-19 19:54:19 +01:00
|
|
|
|
if (m_dimensions.column <= 2)
|
2013-05-16 21:44:58 +02:00
|
|
|
|
return;
|
2012-11-21 19:01:13 +01:00
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
const int item_count = items.size();
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.items.clear(); // make sure it is empty
|
|
|
|
|
m_menu.items.reserve(item_count);
|
2018-03-13 04:24:03 +01:00
|
|
|
|
const auto longest = accumulate(items | transform(&DisplayLine::length),
|
2018-03-13 04:14:16 +01:00
|
|
|
|
1_col, [](auto&& lhs, auto&& rhs) { return std::max(lhs, rhs); });
|
2015-11-18 21:12:16 +01:00
|
|
|
|
|
2018-06-03 01:08:02 +02:00
|
|
|
|
const ColumnCount max_width = m_dimensions.column - 1;
|
|
|
|
|
const bool is_inline = style == MenuStyle::Inline;
|
2018-06-19 12:20:38 +02:00
|
|
|
|
const bool is_search = style == MenuStyle::Search;
|
|
|
|
|
m_menu.columns = is_search ? 0 : (is_inline ? 1 : max((int)(max_width / (longest+1)), 1));
|
2018-06-03 01:08:02 +02:00
|
|
|
|
|
2018-06-03 04:15:02 +02:00
|
|
|
|
const LineCount max_height = min(height_limit(style), max(anchor.line, m_dimensions.line - anchor.line - 1));
|
2018-06-19 12:20:38 +02:00
|
|
|
|
const LineCount height = is_search ?
|
|
|
|
|
1 : (min<LineCount>(max_height, div_round_up(item_count, m_menu.columns)));
|
2015-11-18 21:12:16 +01:00
|
|
|
|
|
2022-09-16 17:51:34 +02:00
|
|
|
|
if (height == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
2018-03-13 05:18:06 +01:00
|
|
|
|
const ColumnCount maxlen = (m_menu.columns > 1 and item_count > 1) ?
|
2018-06-03 01:08:02 +02:00
|
|
|
|
max_width / m_menu.columns - 1 : max_width;
|
2015-11-18 21:12:16 +01:00
|
|
|
|
|
2013-10-17 19:48:12 +02:00
|
|
|
|
for (auto& item : items)
|
2012-08-31 14:14:16 +02:00
|
|
|
|
{
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.items.push_back(item);
|
2017-06-09 16:30:13 +02:00
|
|
|
|
m_menu.items.back().trim(0, maxlen);
|
2016-02-27 18:22:31 +01:00
|
|
|
|
kak_assert(m_menu.items.back().length() <= maxlen);
|
2012-08-31 14:14:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-03 04:06:29 +02:00
|
|
|
|
if (is_inline)
|
|
|
|
|
anchor.line += content_line_offset();
|
2018-03-19 19:54:19 +01:00
|
|
|
|
|
2018-02-22 12:04:04 +01:00
|
|
|
|
LineCount line = anchor.line + 1;
|
2018-06-19 12:20:38 +02:00
|
|
|
|
ColumnCount column = std::max(0_col, std::min(anchor.column, m_dimensions.column - longest - 1));
|
|
|
|
|
if (is_search)
|
|
|
|
|
{
|
|
|
|
|
line = m_status_on_top ? 0_line : m_dimensions.line;
|
|
|
|
|
column = m_dimensions.column / 2;
|
|
|
|
|
}
|
|
|
|
|
else if (not is_inline)
|
2018-02-22 12:04:04 +01:00
|
|
|
|
line = m_status_on_top ? 1_line : m_dimensions.line - height;
|
2020-11-11 07:22:35 +01:00
|
|
|
|
else if (line + height > m_dimensions.line and anchor.line >= height)
|
2018-02-22 12:04:04 +01:00
|
|
|
|
line = anchor.line - height;
|
2013-10-17 19:48:12 +02:00
|
|
|
|
|
2018-06-19 12:20:38 +02:00
|
|
|
|
const auto width = is_search ? m_dimensions.column - m_dimensions.column / 2
|
|
|
|
|
: (is_inline ? min(longest+1, m_dimensions.column)
|
|
|
|
|
: m_dimensions.column);
|
2018-03-13 05:18:06 +01:00
|
|
|
|
m_menu.create({line, column}, {height, width});
|
2018-02-22 12:04:04 +01:00
|
|
|
|
m_menu.selected_item = item_count;
|
2018-06-03 01:08:02 +02:00
|
|
|
|
m_menu.first_item = 0;
|
2018-02-22 12:04:04 +01:00
|
|
|
|
|
2013-03-14 19:19:33 +01:00
|
|
|
|
draw_menu();
|
2016-03-10 00:28:23 +01:00
|
|
|
|
|
|
|
|
|
if (m_info)
|
|
|
|
|
info_show(m_info.title, m_info.content,
|
|
|
|
|
m_info.anchor, m_info.face, m_info.style);
|
2012-08-30 21:53:22 +02:00
|
|
|
|
}
|
2012-08-30 21:14:28 +02:00
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::menu_select(int selected)
|
2012-08-30 21:53:22 +02:00
|
|
|
|
{
|
2016-02-27 18:22:31 +01:00
|
|
|
|
const int item_count = m_menu.items.size();
|
2013-10-17 19:48:12 +02:00
|
|
|
|
if (selected < 0 or selected >= item_count)
|
2012-08-30 21:14:28 +02:00
|
|
|
|
{
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.selected_item = -1;
|
2018-06-03 01:08:02 +02:00
|
|
|
|
m_menu.first_item = 0;
|
2012-08-30 21:14:28 +02:00
|
|
|
|
}
|
2018-06-19 12:20:38 +02:00
|
|
|
|
else if (m_menu.columns == 0) // Do not columnize
|
|
|
|
|
{
|
|
|
|
|
m_menu.selected_item = selected;
|
|
|
|
|
const ColumnCount width = m_menu.size.column - 3;
|
|
|
|
|
int first = 0;
|
|
|
|
|
ColumnCount item_col = 0;
|
|
|
|
|
for (int i = 0; i <= selected; ++i)
|
|
|
|
|
{
|
|
|
|
|
const ColumnCount item_width = m_menu.items[i].length() + 1;
|
|
|
|
|
if (item_col + item_width > width)
|
|
|
|
|
{
|
|
|
|
|
first = i;
|
|
|
|
|
item_col = item_width;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
item_col += item_width;
|
|
|
|
|
}
|
|
|
|
|
m_menu.first_item = first;
|
|
|
|
|
}
|
2012-09-05 19:02:06 +02:00
|
|
|
|
else
|
2013-03-14 19:19:33 +01:00
|
|
|
|
{
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.selected_item = selected;
|
2018-08-10 19:14:48 +02:00
|
|
|
|
const int menu_cols = div_round_up(item_count, (int)m_menu.size.line);
|
|
|
|
|
const int first_col = m_menu.first_item / (int)m_menu.size.line;
|
|
|
|
|
const int selected_col = m_menu.selected_item / (int)m_menu.size.line;
|
|
|
|
|
if (selected_col < first_col)
|
|
|
|
|
m_menu.first_item = selected_col * (int)m_menu.size.line;
|
|
|
|
|
if (selected_col >= first_col + m_menu.columns)
|
|
|
|
|
m_menu.first_item = min(selected_col, menu_cols - m_menu.columns) * (int)m_menu.size.line;
|
2013-03-14 19:19:33 +01:00
|
|
|
|
}
|
|
|
|
|
draw_menu();
|
2012-09-05 19:02:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::menu_hide()
|
2012-09-05 19:02:06 +02:00
|
|
|
|
{
|
2015-09-11 00:39:19 +02:00
|
|
|
|
if (not m_menu)
|
2012-09-05 19:02:06 +02:00
|
|
|
|
return;
|
2018-06-03 04:06:29 +02:00
|
|
|
|
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_menu.items.clear();
|
2015-09-11 00:39:19 +02:00
|
|
|
|
m_menu.destroy();
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = true;
|
2017-04-04 09:36:21 +02:00
|
|
|
|
|
|
|
|
|
// Recompute info as it does not have to avoid the menu anymore
|
|
|
|
|
if (m_info)
|
|
|
|
|
info_show(m_info.title, m_info.content, m_info.anchor, m_info.face, m_info.style);
|
2012-08-30 21:14:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-22 21:36:26 +02:00
|
|
|
|
static DisplayCoord compute_pos(DisplayCoord anchor, DisplayCoord size,
|
2019-11-16 11:27:06 +01:00
|
|
|
|
TerminalUI::Rect rect, TerminalUI::Rect to_avoid,
|
2019-09-04 16:04:27 +02:00
|
|
|
|
bool prefer_above)
|
2012-12-15 18:32:56 +01:00
|
|
|
|
{
|
2016-09-22 21:36:26 +02:00
|
|
|
|
DisplayCoord pos;
|
2014-11-10 14:28:06 +01:00
|
|
|
|
if (prefer_above)
|
|
|
|
|
{
|
2016-09-22 21:36:26 +02:00
|
|
|
|
pos = anchor - DisplayCoord{size.line};
|
2014-11-10 14:28:06 +01:00
|
|
|
|
if (pos.line < 0)
|
|
|
|
|
prefer_above = false;
|
|
|
|
|
}
|
2015-12-05 11:51:46 +01:00
|
|
|
|
auto rect_end = rect.pos + rect.size;
|
2014-11-10 14:28:06 +01:00
|
|
|
|
if (not prefer_above)
|
|
|
|
|
{
|
2016-09-22 21:36:26 +02:00
|
|
|
|
pos = anchor + DisplayCoord{1_line};
|
2020-03-20 10:31:09 +01:00
|
|
|
|
if (pos.line + size.line >= rect_end.line)
|
2015-12-05 11:51:46 +01:00
|
|
|
|
pos.line = max(rect.pos.line, anchor.line - size.line);
|
2014-11-10 14:28:06 +01:00
|
|
|
|
}
|
2020-03-20 10:31:09 +01:00
|
|
|
|
if (pos.column + size.column >= rect_end.column)
|
2015-12-05 11:51:46 +01:00
|
|
|
|
pos.column = max(rect.pos.column, rect_end.column - size.column);
|
2012-12-15 19:10:59 +01:00
|
|
|
|
|
2016-09-22 21:36:26 +02:00
|
|
|
|
if (to_avoid.size != DisplayCoord{})
|
2012-12-15 19:10:59 +01:00
|
|
|
|
{
|
2016-09-22 21:36:26 +02:00
|
|
|
|
DisplayCoord to_avoid_end = to_avoid.pos + to_avoid.size;
|
2012-12-15 19:10:59 +01:00
|
|
|
|
|
2016-09-22 21:36:26 +02:00
|
|
|
|
DisplayCoord end = pos + size;
|
2012-12-15 19:10:59 +01:00
|
|
|
|
|
|
|
|
|
// check intersection
|
2015-12-05 11:51:46 +01:00
|
|
|
|
if (not (end.line < to_avoid.pos.line or end.column < to_avoid.pos.column or
|
|
|
|
|
pos.line > to_avoid_end.line or pos.column > to_avoid_end.column))
|
2012-12-15 19:10:59 +01:00
|
|
|
|
{
|
2015-12-05 11:51:46 +01:00
|
|
|
|
pos.line = min(to_avoid.pos.line, anchor.line) - size.line;
|
2012-12-15 19:10:59 +01:00
|
|
|
|
// if above does not work, try below
|
|
|
|
|
if (pos.line < 0)
|
2015-12-05 11:51:46 +01:00
|
|
|
|
pos.line = max(to_avoid_end.line, anchor.line);
|
2012-12-15 19:10:59 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-12-15 18:32:56 +01:00
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 10:28:09 +01:00
|
|
|
|
static DisplayLineList wrap_lines(const DisplayLineList& lines, ColumnCount max_width)
|
|
|
|
|
{
|
|
|
|
|
DisplayLineList result;
|
|
|
|
|
for (auto line : lines)
|
|
|
|
|
{
|
|
|
|
|
ColumnCount column = 0;
|
|
|
|
|
for (auto it = line.begin(); it != line.end(); )
|
|
|
|
|
{
|
|
|
|
|
auto length = it->length();
|
|
|
|
|
column += length;
|
|
|
|
|
if (column > max_width)
|
|
|
|
|
{
|
|
|
|
|
auto content = it->content().substr(0, length - (column - max_width));
|
|
|
|
|
auto pos = find_if(content | reverse(), [](char c) { return not is_word(c); });
|
|
|
|
|
if (pos != content.rend())
|
|
|
|
|
content = {content.begin(), pos.base()};
|
|
|
|
|
|
|
|
|
|
if (not content.empty())
|
|
|
|
|
it = ++line.split(it, content.column_length());
|
|
|
|
|
result.push_back(AtomList(std::make_move_iterator(line.begin()),
|
|
|
|
|
std::make_move_iterator(it)));
|
|
|
|
|
it = line.erase(line.begin(), it);
|
|
|
|
|
column = 0;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
++it;
|
|
|
|
|
}
|
|
|
|
|
result.push_back(std::move(line));
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::info_show(const DisplayLine& title, const DisplayLineList& content,
|
2020-10-08 11:56:39 +02:00
|
|
|
|
DisplayCoord anchor, Face face, InfoStyle style)
|
2012-12-14 19:04:34 +01:00
|
|
|
|
{
|
2015-05-07 01:34:30 +02:00
|
|
|
|
info_hide();
|
2012-12-14 19:04:34 +01:00
|
|
|
|
|
2019-11-22 11:48:26 +01:00
|
|
|
|
m_info.title = title;
|
|
|
|
|
m_info.content = content;
|
2016-02-27 18:22:31 +01:00
|
|
|
|
m_info.anchor = anchor;
|
|
|
|
|
m_info.face = face;
|
|
|
|
|
m_info.style = style;
|
2015-10-13 14:58:24 +02:00
|
|
|
|
|
2020-01-03 10:28:09 +01:00
|
|
|
|
const bool framed = style == InfoStyle::Prompt or style == InfoStyle::Modal;
|
|
|
|
|
const bool assisted = style == InfoStyle::Prompt and m_assistant.size() != 0;
|
|
|
|
|
|
2019-11-24 01:39:33 +01:00
|
|
|
|
DisplayCoord max_size = m_dimensions;
|
|
|
|
|
if (style == InfoStyle::MenuDoc)
|
|
|
|
|
max_size.column = std::max(m_dimensions.column - (m_menu.pos.column + m_menu.size.column),
|
|
|
|
|
m_menu.pos.column);
|
|
|
|
|
else if (style != InfoStyle::Modal)
|
|
|
|
|
max_size.line -= m_menu.size.line;
|
|
|
|
|
|
2023-02-16 03:07:08 +01:00
|
|
|
|
const auto max_content_width = (m_info_max_width > 0 ? std::min(max_size.column, m_info_max_width) : max_size.column) -
|
2024-03-12 20:34:06 +01:00
|
|
|
|
(framed ? 4 : 0) -
|
2023-02-16 03:07:08 +01:00
|
|
|
|
(assisted ? m_assistant[0].column_length() : 0);
|
2020-01-03 10:28:09 +01:00
|
|
|
|
if (max_content_width <= 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto compute_size = [](const DisplayLineList& lines) -> DisplayCoord {
|
|
|
|
|
return {(int)lines.size(), accumulate(lines, 0_col, [](ColumnCount c, const DisplayLine& l) { return std::max(c, l.length()); })};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DisplayCoord content_size = compute_size(content);
|
|
|
|
|
const bool wrap = content_size.column > max_content_width;
|
|
|
|
|
DisplayLineList wrapped_content;
|
|
|
|
|
if (wrap)
|
|
|
|
|
{
|
|
|
|
|
wrapped_content = wrap_lines(content, max_content_width);
|
|
|
|
|
content_size = compute_size(wrapped_content);
|
|
|
|
|
}
|
|
|
|
|
const auto& lines = wrap ? wrapped_content : content;
|
|
|
|
|
|
|
|
|
|
DisplayCoord size{content_size.line, std::max(content_size.column, title.length() + (framed ? 2 : 0))};
|
2019-11-24 01:39:33 +01:00
|
|
|
|
if (framed)
|
|
|
|
|
size += {2, 4};
|
|
|
|
|
if (assisted)
|
|
|
|
|
size = {std::max(LineCount{(int)m_assistant.size()-1}, size.line), size.column + m_assistant[0].column_length()};
|
|
|
|
|
size = {std::min(max_size.line, size.line), std::min(max_size.column, size.column)};
|
|
|
|
|
|
2020-01-03 10:28:09 +01:00
|
|
|
|
if ((framed and size.line < 3) or size.line <= 0)
|
2019-11-24 01:39:33 +01:00
|
|
|
|
return;
|
|
|
|
|
|
2018-06-03 04:06:29 +02:00
|
|
|
|
const Rect rect = {content_line_offset(), m_dimensions};
|
2014-11-08 18:59:38 +01:00
|
|
|
|
if (style == InfoStyle::Prompt)
|
2014-04-30 20:39:52 +02:00
|
|
|
|
{
|
2018-03-18 15:23:32 +01:00
|
|
|
|
anchor = DisplayCoord{m_status_on_top ? 0 : m_dimensions.line, m_dimensions.column-1};
|
2019-11-24 01:39:33 +01:00
|
|
|
|
anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
|
2014-04-30 20:39:52 +02:00
|
|
|
|
}
|
2017-01-04 12:24:07 +01:00
|
|
|
|
else if (style == InfoStyle::Modal)
|
2014-11-20 14:56:37 +01:00
|
|
|
|
{
|
2018-03-18 15:23:32 +01:00
|
|
|
|
auto half = [](const DisplayCoord& c) { return DisplayCoord{c.line / 2, c.column / 2}; };
|
2019-11-24 01:39:33 +01:00
|
|
|
|
anchor = rect.pos + half(rect.size) - half(size);
|
2018-03-18 15:23:32 +01:00
|
|
|
|
}
|
|
|
|
|
else if (style == InfoStyle::MenuDoc)
|
|
|
|
|
{
|
|
|
|
|
const auto right_max_width = m_dimensions.column - (m_menu.pos.column + m_menu.size.column);
|
|
|
|
|
const auto left_max_width = m_menu.pos.column;
|
|
|
|
|
anchor.line = m_menu.pos.line;
|
2019-11-24 01:39:33 +01:00
|
|
|
|
if (size.column <= right_max_width or right_max_width >= left_max_width)
|
2018-03-18 15:23:32 +01:00
|
|
|
|
anchor.column = m_menu.pos.column + m_menu.size.column;
|
|
|
|
|
else
|
2019-11-24 01:39:33 +01:00
|
|
|
|
anchor.column = m_menu.pos.column - size.column;
|
2014-11-20 14:56:37 +01:00
|
|
|
|
}
|
2018-03-18 15:23:32 +01:00
|
|
|
|
else
|
2017-01-04 12:24:07 +01:00
|
|
|
|
{
|
2019-11-24 01:39:33 +01:00
|
|
|
|
anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
|
2018-06-03 04:06:29 +02:00
|
|
|
|
anchor.line += content_line_offset();
|
2017-01-04 12:24:07 +01:00
|
|
|
|
}
|
2012-12-14 19:04:34 +01:00
|
|
|
|
|
2019-11-24 01:39:33 +01:00
|
|
|
|
m_info.create(anchor, size);
|
|
|
|
|
for (auto line = 0_line; line < size.line; ++line)
|
2013-05-16 21:46:15 +02:00
|
|
|
|
{
|
2020-03-28 02:54:49 +01:00
|
|
|
|
auto draw_atoms = [&, this, pos=DisplayCoord{line}](auto&&... args) mutable {
|
|
|
|
|
auto draw = overload(
|
2020-03-28 10:17:39 +01:00
|
|
|
|
[&](ColumnCount padding) {
|
|
|
|
|
pos.column += padding;
|
|
|
|
|
},
|
2020-03-28 02:54:49 +01:00
|
|
|
|
[&](String str) {
|
|
|
|
|
auto len = str.column_length();
|
|
|
|
|
m_info.draw(pos, DisplayAtom{std::move(str)}, face);
|
|
|
|
|
pos.column += len;
|
|
|
|
|
},
|
|
|
|
|
[&](const DisplayLine& atoms) {
|
|
|
|
|
m_info.draw(pos, atoms.atoms(), face);
|
|
|
|
|
pos.column += atoms.length();
|
|
|
|
|
});
|
|
|
|
|
(draw(args), ...);
|
|
|
|
|
};
|
|
|
|
|
|
2019-11-24 01:39:33 +01:00
|
|
|
|
constexpr Codepoint dash{L'─'};
|
|
|
|
|
constexpr Codepoint dotted_dash{L'┄'};
|
|
|
|
|
if (assisted)
|
|
|
|
|
{
|
|
|
|
|
const auto assistant_top_margin = (size.line - m_assistant.size()+1) / 2;
|
|
|
|
|
StringView assistant_line = (line >= assistant_top_margin) ?
|
|
|
|
|
m_assistant[(int)min(line - assistant_top_margin, LineCount{(int)m_assistant.size()}-1)]
|
|
|
|
|
: m_assistant[(int)m_assistant.size()-1];
|
|
|
|
|
|
|
|
|
|
draw_atoms(assistant_line.str());
|
|
|
|
|
}
|
|
|
|
|
if (not framed)
|
2020-01-03 10:28:09 +01:00
|
|
|
|
draw_atoms(lines[(int)line]);
|
2019-11-24 01:39:33 +01:00
|
|
|
|
else if (line == 0)
|
|
|
|
|
{
|
2020-01-03 10:28:09 +01:00
|
|
|
|
if (title.atoms().empty() or content_size.column < 2)
|
|
|
|
|
draw_atoms("╭─" + String{dash, content_size.column} + "─╮");
|
2019-11-24 01:39:33 +01:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
auto trimmed_title = title;
|
2020-01-03 10:28:09 +01:00
|
|
|
|
trimmed_title.trim(0, content_size.column - 2);
|
|
|
|
|
auto dash_count = content_size.column - trimmed_title.length() - 2;
|
2019-11-24 01:39:33 +01:00
|
|
|
|
String left{dash, dash_count / 2};
|
|
|
|
|
String right{dash, dash_count - dash_count / 2};
|
|
|
|
|
draw_atoms("╭─" + left + "┤", trimmed_title, "├" + right +"─╮");
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-03 10:28:09 +01:00
|
|
|
|
else if (line < size.line - 1 and line <= lines.size())
|
2019-11-24 01:39:33 +01:00
|
|
|
|
{
|
2020-01-03 10:28:09 +01:00
|
|
|
|
auto info_line = lines[(int)line - 1];
|
|
|
|
|
const bool trimmed = info_line.trim(0, content_size.column);
|
|
|
|
|
const ColumnCount padding = content_size.column - info_line.length();
|
2020-03-28 10:17:39 +01:00
|
|
|
|
draw_atoms("│ ", info_line, padding, (trimmed ? "…│" : " │"));
|
2019-11-24 01:39:33 +01:00
|
|
|
|
}
|
2020-01-03 10:28:09 +01:00
|
|
|
|
else if (line == std::min<LineCount>((int)lines.size() + 1, size.line - 1))
|
2020-03-28 10:17:39 +01:00
|
|
|
|
draw_atoms("╰─", String(line > lines.size() ? dash : dotted_dash, content_size.column), "─╯");
|
2013-05-16 21:46:15 +02:00
|
|
|
|
}
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = true;
|
2012-12-14 19:04:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::info_hide()
|
2012-12-14 19:04:34 +01:00
|
|
|
|
{
|
2015-09-11 00:39:19 +02:00
|
|
|
|
if (not m_info)
|
2012-12-14 19:04:34 +01:00
|
|
|
|
return;
|
2015-09-11 00:39:19 +02:00
|
|
|
|
m_info.destroy();
|
2014-04-15 20:19:44 +02:00
|
|
|
|
m_dirty = true;
|
2012-12-14 19:04:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::set_on_key(OnKeyCallback callback)
|
2012-10-20 20:15:20 +02:00
|
|
|
|
{
|
2016-11-29 22:35:53 +01:00
|
|
|
|
m_on_key = std::move(callback);
|
2018-06-03 04:06:29 +02:00
|
|
|
|
EventManager::instance().force_signal(0);
|
2012-10-20 20:15:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-11 19:30:02 +01:00
|
|
|
|
void TerminalUI::set_on_paste(OnPasteCallback callback)
|
|
|
|
|
{
|
|
|
|
|
m_on_paste = std::move(callback);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
DisplayCoord TerminalUI::dimensions()
|
2013-01-11 19:17:21 +01:00
|
|
|
|
{
|
2018-06-19 12:20:38 +02:00
|
|
|
|
return m_dimensions;
|
2018-06-03 04:06:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
LineCount TerminalUI::content_line_offset() const
|
2018-06-03 04:06:29 +02:00
|
|
|
|
{
|
2018-06-19 12:20:38 +02:00
|
|
|
|
return m_status_on_top ? 1 : 0;
|
2013-01-11 19:17:21 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::set_resize_pending()
|
2018-06-08 10:54:11 +02:00
|
|
|
|
{
|
2018-06-19 12:20:38 +02:00
|
|
|
|
m_resize_pending = true;
|
2018-06-08 10:54:11 +02:00
|
|
|
|
EventManager::instance().force_signal(0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::setup_terminal()
|
2013-04-12 01:28:22 +02:00
|
|
|
|
{
|
2021-07-31 01:45:36 +02:00
|
|
|
|
write(STDOUT_FILENO,
|
|
|
|
|
"\033[?1049h" // enable alternative screen buffer
|
|
|
|
|
"\033[?1004h" // enable focus notify
|
|
|
|
|
"\033[>4;1m" // request CSI u style key reporting
|
2021-10-30 14:21:51 +02:00
|
|
|
|
"\033[>5u" // kitty progressive enhancement - report shifted key codes
|
2021-07-31 01:45:36 +02:00
|
|
|
|
"\033[22t" // save the current window title
|
|
|
|
|
"\033[?25l" // hide cursor
|
|
|
|
|
"\033=" // set application keypad mode, so the keypad keys send unique codes
|
2022-12-11 19:30:02 +01:00
|
|
|
|
"\033[?2004h" // force enable bracketed-paste events
|
2021-07-31 01:45:36 +02:00
|
|
|
|
);
|
2019-11-13 13:31:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::restore_terminal()
|
2019-11-13 13:31:24 +01:00
|
|
|
|
{
|
2021-07-31 01:45:36 +02:00
|
|
|
|
write(STDOUT_FILENO,
|
|
|
|
|
"\033>"
|
|
|
|
|
"\033[?25h"
|
|
|
|
|
"\033[23t"
|
2021-10-30 14:21:51 +02:00
|
|
|
|
"\033[<u"
|
2021-07-31 01:45:36 +02:00
|
|
|
|
"\033[>4;0m"
|
|
|
|
|
"\033[?1004l"
|
|
|
|
|
"\033[?1049l"
|
2022-12-11 19:30:02 +01:00
|
|
|
|
"\033[?2004l"
|
2021-07-31 01:45:36 +02:00
|
|
|
|
"\033[m" // set the terminal output back to default colours and style
|
|
|
|
|
);
|
2013-04-12 01:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::enable_mouse(bool enabled)
|
2015-10-02 14:52:41 +02:00
|
|
|
|
{
|
|
|
|
|
if (enabled == m_mouse_enabled)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
m_mouse_enabled = enabled;
|
|
|
|
|
if (enabled)
|
|
|
|
|
{
|
2021-07-31 01:45:36 +02:00
|
|
|
|
write(STDOUT_FILENO,
|
|
|
|
|
"\033[?1006h" // force SGR mode
|
|
|
|
|
"\033[?1000h" // enable mouse
|
|
|
|
|
"\033[?1002h" // force enable report mouse position
|
|
|
|
|
);
|
2015-10-02 14:52:41 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2021-07-31 01:45:36 +02:00
|
|
|
|
write(STDOUT_FILENO,
|
|
|
|
|
"\033[?1002l"
|
|
|
|
|
"\033[?1000l"
|
|
|
|
|
"\033[?1006l"
|
|
|
|
|
);
|
2015-10-02 14:52:41 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-16 11:27:06 +01:00
|
|
|
|
void TerminalUI::set_ui_options(const Options& options)
|
2014-11-11 00:29:16 +01:00
|
|
|
|
{
|
2021-07-09 15:43:14 +02:00
|
|
|
|
auto find = [&](StringView name) -> Optional<StringView> {
|
|
|
|
|
if (auto it = options.find(name); it != options.end())
|
|
|
|
|
return StringView{it->value};
|
|
|
|
|
return {};
|
2021-07-09 12:53:17 +02:00
|
|
|
|
};
|
2015-09-03 01:03:07 +02:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
auto assistant = find("terminal_assistant").value_or("clippy");
|
|
|
|
|
if (assistant == "clippy")
|
|
|
|
|
m_assistant = assistant_clippy;
|
|
|
|
|
else if (assistant == "cat")
|
|
|
|
|
m_assistant = assistant_cat;
|
|
|
|
|
else if (assistant == "dilbert")
|
|
|
|
|
m_assistant = assistant_dilbert;
|
|
|
|
|
else if (assistant == "none" or assistant == "off")
|
|
|
|
|
m_assistant = ConstArrayView<StringView>{};
|
2015-04-05 19:43:27 +02:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
auto to_bool = [](StringView s) { return s == "yes" or s == "true"; };
|
2018-04-11 12:29:35 +02:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
m_status_on_top = find("terminal_status_on_top").map(to_bool).value_or(false);
|
|
|
|
|
m_set_title = find("terminal_set_title").map(to_bool).value_or(true);
|
2021-10-23 00:42:31 +02:00
|
|
|
|
|
|
|
|
|
auto synchronized = find("terminal_synchronized").map(to_bool);
|
|
|
|
|
m_synchronized.set = (bool)synchronized;
|
|
|
|
|
m_synchronized.requested = synchronized.value_or(false);
|
2023-11-24 13:55:45 +01:00
|
|
|
|
if (not m_synchronized.queried and not m_synchronized.set) {
|
|
|
|
|
write(STDOUT_FILENO, "\033[?2026$p");
|
|
|
|
|
m_synchronized.queried = true;
|
|
|
|
|
}
|
2015-10-02 14:52:41 +02:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
m_shift_function_key = find("terminal_shift_function_key").map(str_to_int_ifp).value_or(default_shift_function_key);
|
2015-11-05 14:28:58 +01:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
enable_mouse(find("terminal_enable_mouse").map(to_bool).value_or(true));
|
|
|
|
|
m_wheel_scroll_amount = find("terminal_wheel_scroll_amount").map(str_to_int_ifp).value_or(3);
|
2021-04-05 20:21:48 +02:00
|
|
|
|
|
2021-07-09 15:43:14 +02:00
|
|
|
|
m_padding_char = find("terminal_padding_char").map([](StringView s) { return s.column_length() < 1 ? ' ' : s[0_char]; }).value_or(Codepoint{'~'});
|
|
|
|
|
m_padding_fill = find("terminal_padding_fill").map(to_bool).value_or(false);
|
2023-02-16 03:07:08 +01:00
|
|
|
|
|
|
|
|
|
m_info_max_width = find("terminal_info_max_width").map(str_to_int_ifp).value_or(0);
|
2014-11-11 00:29:16 +01:00
|
|
|
|
}
|
|
|
|
|
|
2012-02-16 15:25:16 +01:00
|
|
|
|
}
|