b298e01390
Merge attributes as well, and reuse an existing function instead of reimplementing the same logic again. Closes #1684
1082 lines
34 KiB
C++
1082 lines
34 KiB
C++
#include "ncurses_ui.hh"
|
||
|
||
#include "display_buffer.hh"
|
||
#include "event_manager.hh"
|
||
#include "keys.hh"
|
||
#include "ranges.hh"
|
||
#include "string_utils.hh"
|
||
|
||
#include <algorithm>
|
||
|
||
#define NCURSES_OPAQUE 0
|
||
#define NCURSES_INTERNALS
|
||
|
||
#include <ncurses.h>
|
||
|
||
#include <fcntl.h>
|
||
#include <csignal>
|
||
#include <sys/ioctl.h>
|
||
#include <termios.h>
|
||
#include <unistd.h>
|
||
|
||
constexpr char control(char c) { return c & 037; }
|
||
|
||
namespace Kakoune
|
||
{
|
||
|
||
using std::min;
|
||
using std::max;
|
||
|
||
struct NCursesWin : WINDOW {};
|
||
|
||
static constexpr StringView assistant_cat[] =
|
||
{ R"( ___ )",
|
||
R"( (__ \ )",
|
||
R"( / / ╭)",
|
||
R"( .' '·. │)",
|
||
R"( ' ” │)",
|
||
R"( ╰ /\_/| │)",
|
||
R"( | . \ │)",
|
||
R"( ╰_J` | | | ╯)",
|
||
R"( ' \__- _/ )",
|
||
R"( \_\ \_\ )",
|
||
R"( )"};
|
||
|
||
static constexpr StringView assistant_clippy[] =
|
||
{ " ╭──╮ ",
|
||
" │ │ ",
|
||
" @ @ ╭",
|
||
" ││ ││ │",
|
||
" ││ ││ ╯",
|
||
" │╰─╯│ ",
|
||
" ╰───╯ ",
|
||
" " };
|
||
|
||
static constexpr StringView assistant_dilbert[] =
|
||
{ R"( დოოოოოდ )",
|
||
R"( | | )",
|
||
R"( | | ╭)",
|
||
R"( |-ᱛ ᱛ-| │)",
|
||
R"( Ͼ ∪ Ͽ │)",
|
||
R"( | | ╯)",
|
||
R"( ˏ`-.ŏ.-´ˎ )",
|
||
R"( @ )",
|
||
R"( @ )",
|
||
R"( )"};
|
||
|
||
static void set_attribute(WINDOW* window, int attribute, bool on)
|
||
{
|
||
if (on)
|
||
wattron(window, attribute);
|
||
else
|
||
wattroff(window, attribute);
|
||
}
|
||
|
||
template<typename T> T sq(T x) { return x * x; }
|
||
|
||
constexpr struct { unsigned char r, g, b; } builtin_colors[] = {
|
||
{0x00,0x00,0x00}, {0x80,0x00,0x00}, {0x00,0x80,0x00}, {0x80,0x80,0x00},
|
||
{0x00,0x00,0x80}, {0x80,0x00,0x80}, {0x00,0x80,0x80}, {0xc0,0xc0,0xc0},
|
||
{0x80,0x80,0x80}, {0xff,0x00,0x00}, {0x00,0xff,0x00}, {0xff,0xff,0x00},
|
||
{0x00,0x00,0xff}, {0xff,0x00,0xff}, {0x00,0xff,0xff}, {0xff,0xff,0xff},
|
||
{0x00,0x00,0x00}, {0x00,0x00,0x5f}, {0x00,0x00,0x87}, {0x00,0x00,0xaf},
|
||
{0x00,0x00,0xd7}, {0x00,0x00,0xff}, {0x00,0x5f,0x00}, {0x00,0x5f,0x5f},
|
||
{0x00,0x5f,0x87}, {0x00,0x5f,0xaf}, {0x00,0x5f,0xd7}, {0x00,0x5f,0xff},
|
||
{0x00,0x87,0x00}, {0x00,0x87,0x5f}, {0x00,0x87,0x87}, {0x00,0x87,0xaf},
|
||
{0x00,0x87,0xd7}, {0x00,0x87,0xff}, {0x00,0xaf,0x00}, {0x00,0xaf,0x5f},
|
||
{0x00,0xaf,0x87}, {0x00,0xaf,0xaf}, {0x00,0xaf,0xd7}, {0x00,0xaf,0xff},
|
||
{0x00,0xd7,0x00}, {0x00,0xd7,0x5f}, {0x00,0xd7,0x87}, {0x00,0xd7,0xaf},
|
||
{0x00,0xd7,0xd7}, {0x00,0xd7,0xff}, {0x00,0xff,0x00}, {0x00,0xff,0x5f},
|
||
{0x00,0xff,0x87}, {0x00,0xff,0xaf}, {0x00,0xff,0xd7}, {0x00,0xff,0xff},
|
||
{0x5f,0x00,0x00}, {0x5f,0x00,0x5f}, {0x5f,0x00,0x87}, {0x5f,0x00,0xaf},
|
||
{0x5f,0x00,0xd7}, {0x5f,0x00,0xff}, {0x5f,0x5f,0x00}, {0x5f,0x5f,0x5f},
|
||
{0x5f,0x5f,0x87}, {0x5f,0x5f,0xaf}, {0x5f,0x5f,0xd7}, {0x5f,0x5f,0xff},
|
||
{0x5f,0x87,0x00}, {0x5f,0x87,0x5f}, {0x5f,0x87,0x87}, {0x5f,0x87,0xaf},
|
||
{0x5f,0x87,0xd7}, {0x5f,0x87,0xff}, {0x5f,0xaf,0x00}, {0x5f,0xaf,0x5f},
|
||
{0x5f,0xaf,0x87}, {0x5f,0xaf,0xaf}, {0x5f,0xaf,0xd7}, {0x5f,0xaf,0xff},
|
||
{0x5f,0xd7,0x00}, {0x5f,0xd7,0x5f}, {0x5f,0xd7,0x87}, {0x5f,0xd7,0xaf},
|
||
{0x5f,0xd7,0xd7}, {0x5f,0xd7,0xff}, {0x5f,0xff,0x00}, {0x5f,0xff,0x5f},
|
||
{0x5f,0xff,0x87}, {0x5f,0xff,0xaf}, {0x5f,0xff,0xd7}, {0x5f,0xff,0xff},
|
||
{0x87,0x00,0x00}, {0x87,0x00,0x5f}, {0x87,0x00,0x87}, {0x87,0x00,0xaf},
|
||
{0x87,0x00,0xd7}, {0x87,0x00,0xff}, {0x87,0x5f,0x00}, {0x87,0x5f,0x5f},
|
||
{0x87,0x5f,0x87}, {0x87,0x5f,0xaf}, {0x87,0x5f,0xd7}, {0x87,0x5f,0xff},
|
||
{0x87,0x87,0x00}, {0x87,0x87,0x5f}, {0x87,0x87,0x87}, {0x87,0x87,0xaf},
|
||
{0x87,0x87,0xd7}, {0x87,0x87,0xff}, {0x87,0xaf,0x00}, {0x87,0xaf,0x5f},
|
||
{0x87,0xaf,0x87}, {0x87,0xaf,0xaf}, {0x87,0xaf,0xd7}, {0x87,0xaf,0xff},
|
||
{0x87,0xd7,0x00}, {0x87,0xd7,0x5f}, {0x87,0xd7,0x87}, {0x87,0xd7,0xaf},
|
||
{0x87,0xd7,0xd7}, {0x87,0xd7,0xff}, {0x87,0xff,0x00}, {0x87,0xff,0x5f},
|
||
{0x87,0xff,0x87}, {0x87,0xff,0xaf}, {0x87,0xff,0xd7}, {0x87,0xff,0xff},
|
||
{0xaf,0x00,0x00}, {0xaf,0x00,0x5f}, {0xaf,0x00,0x87}, {0xaf,0x00,0xaf},
|
||
{0xaf,0x00,0xd7}, {0xaf,0x00,0xff}, {0xaf,0x5f,0x00}, {0xaf,0x5f,0x5f},
|
||
{0xaf,0x5f,0x87}, {0xaf,0x5f,0xaf}, {0xaf,0x5f,0xd7}, {0xaf,0x5f,0xff},
|
||
{0xaf,0x87,0x00}, {0xaf,0x87,0x5f}, {0xaf,0x87,0x87}, {0xaf,0x87,0xaf},
|
||
{0xaf,0x87,0xd7}, {0xaf,0x87,0xff}, {0xaf,0xaf,0x00}, {0xaf,0xaf,0x5f},
|
||
{0xaf,0xaf,0x87}, {0xaf,0xaf,0xaf}, {0xaf,0xaf,0xd7}, {0xaf,0xaf,0xff},
|
||
{0xaf,0xd7,0x00}, {0xaf,0xd7,0x5f}, {0xaf,0xd7,0x87}, {0xaf,0xd7,0xaf},
|
||
{0xaf,0xd7,0xd7}, {0xaf,0xd7,0xff}, {0xaf,0xff,0x00}, {0xaf,0xff,0x5f},
|
||
{0xaf,0xff,0x87}, {0xaf,0xff,0xaf}, {0xaf,0xff,0xd7}, {0xaf,0xff,0xff},
|
||
{0xd7,0x00,0x00}, {0xd7,0x00,0x5f}, {0xd7,0x00,0x87}, {0xd7,0x00,0xaf},
|
||
{0xd7,0x00,0xd7}, {0xd7,0x00,0xff}, {0xd7,0x5f,0x00}, {0xd7,0x5f,0x5f},
|
||
{0xd7,0x5f,0x87}, {0xd7,0x5f,0xaf}, {0xd7,0x5f,0xd7}, {0xd7,0x5f,0xff},
|
||
{0xd7,0x87,0x00}, {0xd7,0x87,0x5f}, {0xd7,0x87,0x87}, {0xd7,0x87,0xaf},
|
||
{0xd7,0x87,0xd7}, {0xd7,0x87,0xff}, {0xd7,0xaf,0x00}, {0xd7,0xaf,0x5f},
|
||
{0xd7,0xaf,0x87}, {0xd7,0xaf,0xaf}, {0xd7,0xaf,0xd7}, {0xd7,0xaf,0xff},
|
||
{0xd7,0xd7,0x00}, {0xd7,0xd7,0x5f}, {0xd7,0xd7,0x87}, {0xd7,0xd7,0xaf},
|
||
{0xd7,0xd7,0xd7}, {0xd7,0xd7,0xff}, {0xd7,0xff,0x00}, {0xd7,0xff,0x5f},
|
||
{0xd7,0xff,0x87}, {0xd7,0xff,0xaf}, {0xd7,0xff,0xd7}, {0xd7,0xff,0xff},
|
||
{0xff,0x00,0x00}, {0xff,0x00,0x5f}, {0xff,0x00,0x87}, {0xff,0x00,0xaf},
|
||
{0xff,0x00,0xd7}, {0xff,0x00,0xff}, {0xff,0x5f,0x00}, {0xff,0x5f,0x5f},
|
||
{0xff,0x5f,0x87}, {0xff,0x5f,0xaf}, {0xff,0x5f,0xd7}, {0xff,0x5f,0xff},
|
||
{0xff,0x87,0x00}, {0xff,0x87,0x5f}, {0xff,0x87,0x87}, {0xff,0x87,0xaf},
|
||
{0xff,0x87,0xd7}, {0xff,0x87,0xff}, {0xff,0xaf,0x00}, {0xff,0xaf,0x5f},
|
||
{0xff,0xaf,0x87}, {0xff,0xaf,0xaf}, {0xff,0xaf,0xd7}, {0xff,0xaf,0xff},
|
||
{0xff,0xd7,0x00}, {0xff,0xd7,0x5f}, {0xff,0xd7,0x87}, {0xff,0xd7,0xaf},
|
||
{0xff,0xd7,0xd7}, {0xff,0xd7,0xff}, {0xff,0xff,0x00}, {0xff,0xff,0x5f},
|
||
{0xff,0xff,0x87}, {0xff,0xff,0xaf}, {0xff,0xff,0xd7}, {0xff,0xff,0xff},
|
||
{0x08,0x08,0x08}, {0x12,0x12,0x12}, {0x1c,0x1c,0x1c}, {0x26,0x26,0x26},
|
||
{0x30,0x30,0x30}, {0x3a,0x3a,0x3a}, {0x44,0x44,0x44}, {0x4e,0x4e,0x4e},
|
||
{0x58,0x58,0x58}, {0x60,0x60,0x60}, {0x66,0x66,0x66}, {0x76,0x76,0x76},
|
||
{0x80,0x80,0x80}, {0x8a,0x8a,0x8a}, {0x94,0x94,0x94}, {0x9e,0x9e,0x9e},
|
||
{0xa8,0xa8,0xa8}, {0xb2,0xb2,0xb2}, {0xbc,0xbc,0xbc}, {0xc6,0xc6,0xc6},
|
||
{0xd0,0xd0,0xd0}, {0xda,0xda,0xda}, {0xe4,0xe4,0xe4}, {0xee,0xee,0xee},
|
||
};
|
||
|
||
int NCursesUI::get_color(Color color)
|
||
{
|
||
auto it = m_colors.find(color);
|
||
if (it != m_colors.end())
|
||
return it->value;
|
||
else if (m_change_colors and can_change_color() and COLORS > 16)
|
||
{
|
||
kak_assert(color.color == Color::RGB);
|
||
if (m_next_color > COLORS)
|
||
m_next_color = 16;
|
||
init_color(m_next_color,
|
||
color.r * 1000 / 255,
|
||
color.g * 1000 / 255,
|
||
color.b * 1000 / 255);
|
||
m_colors[color] = m_next_color;
|
||
return m_next_color++;
|
||
}
|
||
else
|
||
{
|
||
kak_assert(color.color == Color::RGB);
|
||
int lowestDist = INT_MAX;
|
||
int closestCol = -1;
|
||
for (int i = 0; i < std::min(256, COLORS); ++i)
|
||
{
|
||
auto& col = builtin_colors[i];
|
||
int dist = sq(color.r - col.r)
|
||
+ sq(color.g - col.g)
|
||
+ sq(color.b - col.b);
|
||
if (dist < lowestDist)
|
||
{
|
||
lowestDist = dist;
|
||
closestCol = i;
|
||
}
|
||
}
|
||
return closestCol;
|
||
}
|
||
}
|
||
|
||
int NCursesUI::get_color_pair(const Face& face)
|
||
{
|
||
ColorPair colors{face.fg, face.bg};
|
||
auto it = m_colorpairs.find(colors);
|
||
if (it != m_colorpairs.end())
|
||
return it->value;
|
||
else
|
||
{
|
||
init_pair(m_next_pair, get_color(face.fg), get_color(face.bg));
|
||
m_colorpairs[colors] = m_next_pair;
|
||
return m_next_pair++;
|
||
}
|
||
}
|
||
|
||
void NCursesUI::set_face(NCursesWin* window, Face face, const Face& default_face)
|
||
{
|
||
if (m_active_pair != -1)
|
||
wattroff(window, COLOR_PAIR(m_active_pair));
|
||
|
||
face = merge_faces(default_face, face);
|
||
|
||
if (face.fg != Color::Default or face.bg != Color::Default)
|
||
{
|
||
m_active_pair = get_color_pair(face);
|
||
wattron(window, COLOR_PAIR(m_active_pair));
|
||
}
|
||
|
||
set_attribute(window, A_UNDERLINE, face.attributes & Attribute::Underline);
|
||
set_attribute(window, A_REVERSE, face.attributes & Attribute::Reverse);
|
||
set_attribute(window, A_BLINK, face.attributes & Attribute::Blink);
|
||
set_attribute(window, A_BOLD, face.attributes & Attribute::Bold);
|
||
set_attribute(window, A_DIM, face.attributes & Attribute::Dim);
|
||
#if defined(A_ITALIC)
|
||
set_attribute(window, A_ITALIC, face.attributes & Attribute::Italic);
|
||
#endif
|
||
}
|
||
|
||
static sig_atomic_t resize_pending = 0;
|
||
|
||
void on_term_resize(int)
|
||
{
|
||
resize_pending = 1;
|
||
EventManager::instance().force_signal(0);
|
||
}
|
||
|
||
static const std::initializer_list<HashMap<Kakoune::Color, int>::Item>
|
||
default_colors = {
|
||
{ Color::Default, -1 },
|
||
{ Color::Black, 0 },
|
||
{ Color::Red, 1 },
|
||
{ Color::Green, 2 },
|
||
{ Color::Yellow, 3 },
|
||
{ Color::Blue, 4 },
|
||
{ Color::Magenta, 5 },
|
||
{ Color::Cyan, 6 },
|
||
{ Color::White, 7 },
|
||
{ Color::BrightBlack, 8 },
|
||
{ Color::BrightRed, 9 },
|
||
{ Color::BrightGreen, 10 },
|
||
{ Color::BrightYellow, 11 },
|
||
{ Color::BrightBlue, 12 },
|
||
{ Color::BrightMagenta, 13 },
|
||
{ Color::BrightCyan, 14 },
|
||
{ Color::BrightWhite, 15 },
|
||
};
|
||
|
||
NCursesUI::NCursesUI()
|
||
: m_stdin_watcher{0, FdEvents::Read,
|
||
[this](FDWatcher&, FdEvents, EventMode mode) {
|
||
if (not m_on_key)
|
||
return;
|
||
|
||
while (auto key = get_next_key())
|
||
m_on_key(*key);
|
||
}},
|
||
m_assistant(assistant_clippy),
|
||
m_colors{default_colors},
|
||
m_cursor{CursorMode::Buffer, {}}
|
||
{
|
||
initscr();
|
||
raw();
|
||
noecho();
|
||
nonl();
|
||
curs_set(0);
|
||
start_color();
|
||
use_default_colors();
|
||
set_escdelay(25);
|
||
|
||
enable_mouse(true);
|
||
|
||
set_signal_handler(SIGWINCH, on_term_resize);
|
||
set_signal_handler(SIGCONT, on_term_resize);
|
||
|
||
check_resize(true);
|
||
|
||
redraw();
|
||
}
|
||
|
||
NCursesUI::~NCursesUI()
|
||
{
|
||
enable_mouse(false);
|
||
if (can_change_color()) // try to reset palette
|
||
{
|
||
fputs("\033]104;\007", stdout);
|
||
fflush(stdout);
|
||
}
|
||
endwin();
|
||
set_signal_handler(SIGWINCH, SIG_DFL);
|
||
set_signal_handler(SIGCONT, SIG_DFL);
|
||
}
|
||
|
||
void NCursesUI::Window::create(const DisplayCoord& p, const DisplayCoord& s)
|
||
{
|
||
pos = p;
|
||
size = s;
|
||
win = (NCursesWin*)newpad((int)size.line, (int)size.column);
|
||
}
|
||
|
||
void NCursesUI::Window::destroy()
|
||
{
|
||
delwin(win);
|
||
win = nullptr;
|
||
pos = DisplayCoord{};
|
||
size = DisplayCoord{};
|
||
}
|
||
|
||
void NCursesUI::Window::refresh()
|
||
{
|
||
if (not win)
|
||
return;
|
||
|
||
DisplayCoord max_pos = pos + size - DisplayCoord{1,1};
|
||
pnoutrefresh(win, 0, 0, (int)pos.line, (int)pos.column,
|
||
(int)max_pos.line, (int)max_pos.column);
|
||
}
|
||
|
||
void NCursesUI::redraw()
|
||
{
|
||
pnoutrefresh(m_window, 0, 0, 0, 0,
|
||
(int)m_dimensions.line + 1, (int)m_dimensions.column);
|
||
|
||
m_menu.refresh();
|
||
m_info.refresh();
|
||
|
||
if (m_cursor.mode == CursorMode::Prompt)
|
||
wmove(newscr, m_status_on_top ? 0 : (int)m_dimensions.line,
|
||
(int)m_cursor.coord.column);
|
||
else
|
||
wmove(newscr, (int)m_cursor.coord.line + (m_status_on_top ? 1 : 0),
|
||
(int)m_cursor.coord.column);
|
||
|
||
doupdate();
|
||
}
|
||
|
||
void NCursesUI::set_cursor(CursorMode mode, DisplayCoord coord)
|
||
{
|
||
m_cursor = Cursor{ mode, coord };
|
||
}
|
||
|
||
void NCursesUI::refresh(bool force)
|
||
{
|
||
if (force)
|
||
redrawwin(m_window);
|
||
|
||
if (m_dirty or force)
|
||
redraw();
|
||
m_dirty = false;
|
||
}
|
||
|
||
void add_str(WINDOW* win, StringView str)
|
||
{
|
||
waddnstr(win, str.begin(), (int)str.length());
|
||
}
|
||
|
||
void NCursesUI::draw_line(NCursesWin* window, const DisplayLine& line,
|
||
ColumnCount col_index, ColumnCount max_column,
|
||
const Face& default_face)
|
||
{
|
||
for (const DisplayAtom& atom : line)
|
||
{
|
||
set_face(window, atom.face, default_face);
|
||
|
||
StringView content = atom.content();
|
||
if (content.empty())
|
||
continue;
|
||
|
||
const auto remaining_columns = max_column - col_index;
|
||
if (content.back() == '\n' and
|
||
content.column_length() - 1 < remaining_columns)
|
||
{
|
||
add_str(window, content.substr(0, content.length()-1));
|
||
waddch(window, ' ');
|
||
}
|
||
else
|
||
{
|
||
content = content.substr(0_col, remaining_columns);
|
||
add_str(window, content);
|
||
col_index += content.column_length();
|
||
}
|
||
}
|
||
}
|
||
|
||
static const DisplayLine empty_line = String(" ");
|
||
|
||
void NCursesUI::draw(const DisplayBuffer& display_buffer,
|
||
const Face& default_face,
|
||
const Face& padding_face)
|
||
{
|
||
wbkgdset(m_window, COLOR_PAIR(get_color_pair(default_face)));
|
||
|
||
check_resize();
|
||
|
||
LineCount line_index = m_status_on_top ? 1 : 0;
|
||
for (const DisplayLine& line : display_buffer.lines())
|
||
{
|
||
wmove(m_window, (int)line_index, 0);
|
||
wclrtoeol(m_window);
|
||
draw_line(m_window, line, 0, m_dimensions.column, default_face);
|
||
++line_index;
|
||
}
|
||
|
||
wbkgdset(m_window, COLOR_PAIR(get_color_pair(padding_face)));
|
||
set_face(m_window, padding_face, default_face);
|
||
|
||
while (line_index < m_dimensions.line + (m_status_on_top ? 1 : 0))
|
||
{
|
||
wmove(m_window, (int)line_index++, 0);
|
||
wclrtoeol(m_window);
|
||
waddch(m_window, '~');
|
||
}
|
||
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::draw_status(const DisplayLine& status_line,
|
||
const DisplayLine& mode_line,
|
||
const Face& default_face)
|
||
{
|
||
const int status_line_pos = m_status_on_top ? 0 : (int)m_dimensions.line;
|
||
wmove(m_window, status_line_pos, 0);
|
||
|
||
wbkgdset(m_window, COLOR_PAIR(get_color_pair(default_face)));
|
||
wclrtoeol(m_window);
|
||
|
||
draw_line(m_window, status_line, 0, m_dimensions.column, default_face);
|
||
|
||
const auto mode_len = mode_line.length();
|
||
const auto remaining = m_dimensions.column - status_line.length();
|
||
if (mode_len < remaining)
|
||
{
|
||
ColumnCount col = m_dimensions.column - mode_len;
|
||
wmove(m_window, status_line_pos, (int)col);
|
||
draw_line(m_window, mode_line, col, m_dimensions.column, default_face);
|
||
}
|
||
else if (remaining > 2)
|
||
{
|
||
DisplayLine trimmed_mode_line = mode_line;
|
||
trimmed_mode_line.trim(mode_len + 2 - remaining, remaining - 2);
|
||
trimmed_mode_line.insert(trimmed_mode_line.begin(), { "…" });
|
||
kak_assert(trimmed_mode_line.length() == remaining - 1);
|
||
|
||
ColumnCount col = m_dimensions.column - remaining + 1;
|
||
wmove(m_window, status_line_pos, (int)col);
|
||
draw_line(m_window, trimmed_mode_line, col, m_dimensions.column, default_face);
|
||
}
|
||
|
||
if (m_set_title)
|
||
{
|
||
constexpr char suffix[] = " - Kakoune\007";
|
||
char buf[4 + 511 + 2] = "\033]2;";
|
||
// Fill title escape sequence buffer, removing non ascii characters
|
||
auto buf_it = &buf[4], buf_end = &buf[4 + 511 - (sizeof(suffix) - 2)];
|
||
for (auto& atom : mode_line)
|
||
{
|
||
const auto str = atom.content();
|
||
for (auto it = str.begin(), end = str.end();
|
||
it != end and buf_it != buf_end; utf8::to_next(it, end))
|
||
*buf_it++ = (*it >= 0x20 and *it <= 0x7e) ? *it : '?';
|
||
}
|
||
for (auto c : suffix)
|
||
*buf_it++ = c;
|
||
|
||
fputs(buf, stdout);
|
||
fflush(stdout);
|
||
}
|
||
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::check_resize(bool force)
|
||
{
|
||
if (not force and not resize_pending)
|
||
return;
|
||
|
||
resize_pending = 0;
|
||
|
||
const int fd = open("/dev/tty", O_RDWR);
|
||
auto close_fd = on_scope_end([fd]{ close(fd); });
|
||
winsize ws;
|
||
if (ioctl(fd, TIOCGWINSZ, (void*)&ws) == 0)
|
||
{
|
||
const bool info = (bool)m_info;
|
||
const bool menu = (bool)m_menu;
|
||
if (m_window) delwin(m_window);
|
||
if (info) m_info.destroy();
|
||
if (menu) m_menu.destroy();
|
||
|
||
resize_term(ws.ws_row, ws.ws_col);
|
||
|
||
m_window = (NCursesWin*)newpad(ws.ws_row, ws.ws_col);
|
||
intrflush(m_window, false);
|
||
keypad(m_window, true);
|
||
meta(m_window, true);
|
||
|
||
m_dimensions = DisplayCoord{ws.ws_row-1, ws.ws_col};
|
||
|
||
if (char* csr = tigetstr((char*)"csr"))
|
||
putp(tparm(csr, 0, ws.ws_row));
|
||
|
||
if (menu)
|
||
{
|
||
auto items = std::move(m_menu.items);
|
||
menu_show(items, m_menu.anchor, m_menu.fg, m_menu.bg, m_menu.style);
|
||
}
|
||
if (info)
|
||
info_show(m_info.title, m_info.content, m_info.anchor, m_info.face, m_info.style);
|
||
}
|
||
else
|
||
kak_assert(false);
|
||
|
||
ungetch(KEY_RESIZE);
|
||
clearok(curscr, true);
|
||
werase(curscr);
|
||
}
|
||
|
||
void NCursesUI::on_sighup()
|
||
{
|
||
set_signal_handler(SIGWINCH, SIG_DFL);
|
||
set_signal_handler(SIGCONT, SIG_DFL);
|
||
|
||
m_window = nullptr;
|
||
}
|
||
|
||
Optional<Key> NCursesUI::get_next_key()
|
||
{
|
||
if (not m_window)
|
||
return {};
|
||
|
||
check_resize();
|
||
|
||
wtimeout(m_window, 0);
|
||
const int c = wgetch(m_window);
|
||
wtimeout(m_window, -1);
|
||
|
||
if (c == KEY_MOUSE)
|
||
{
|
||
MEVENT ev;
|
||
if (getmouse(&ev) == OK)
|
||
{
|
||
auto get_modifiers = [this](mmask_t mask) {
|
||
Key::Modifiers res{};
|
||
|
||
if (mask & BUTTON_CTRL)
|
||
res |= Key::Modifiers::Control;
|
||
if (mask & BUTTON_ALT)
|
||
res |= Key::Modifiers::Alt;
|
||
|
||
if (BUTTON_PRESS(mask, 1))
|
||
return res | Key::Modifiers::MousePress;
|
||
if (BUTTON_RELEASE(mask, 1))
|
||
return res | Key::Modifiers::MouseRelease;
|
||
if (BUTTON_PRESS(mask, m_wheel_down_button))
|
||
return res | Key::Modifiers::MouseWheelDown;
|
||
if (BUTTON_PRESS(mask, m_wheel_up_button))
|
||
return res | Key::Modifiers::MouseWheelUp;
|
||
return res | Key::Modifiers::MousePos;
|
||
};
|
||
|
||
return Key{ get_modifiers(ev.bstate),
|
||
encode_coord({ ev.y - (m_status_on_top ? 1 : 0), ev.x }) };
|
||
}
|
||
}
|
||
|
||
auto parse_key = [this](int c) -> Optional<Key> {
|
||
if (c == ERR)
|
||
return {};
|
||
|
||
switch (c)
|
||
{
|
||
case KEY_BACKSPACE: case 127: return {Key::Backspace};
|
||
case KEY_DC: return {Key::Delete};
|
||
case KEY_UP: return {Key::Up};
|
||
case KEY_DOWN: return {Key::Down};
|
||
case KEY_LEFT: return {Key::Left};
|
||
case KEY_RIGHT: return {Key::Right};
|
||
case KEY_PPAGE: return {Key::PageUp};
|
||
case KEY_NPAGE: return {Key::PageDown};
|
||
case KEY_HOME: return {Key::Home};
|
||
case KEY_END: return {Key::End};
|
||
case KEY_BTAB: return {Key::BackTab};
|
||
case KEY_RESIZE: return resize(m_dimensions);
|
||
}
|
||
|
||
if (c > 0 and c < 27)
|
||
{
|
||
if (c == control('m') or c == control('j'))
|
||
return {Key::Return};
|
||
if (c == control('i'))
|
||
return {Key::Tab};
|
||
if (c == control('h'))
|
||
return {Key::Backspace};
|
||
if (c == control('z'))
|
||
{
|
||
raise(SIGTSTP);
|
||
return {};
|
||
}
|
||
return ctrl(Codepoint(c) - 1 + 'a');
|
||
}
|
||
|
||
for (int i = 0; i < 12; ++i)
|
||
{
|
||
if (c == KEY_F(i+1))
|
||
return {Key::F1 + i};
|
||
}
|
||
|
||
if (c >= 0 and c < 256)
|
||
{
|
||
ungetch(c);
|
||
struct getch_iterator
|
||
{
|
||
getch_iterator(WINDOW* win) : window(win) {}
|
||
int operator*() { return wgetch(window); }
|
||
getch_iterator& operator++() { return *this; }
|
||
getch_iterator& operator++(int) { return *this; }
|
||
bool operator== (const getch_iterator&) const { return false; }
|
||
|
||
WINDOW* window;
|
||
};
|
||
return Key{utf8::codepoint(getch_iterator{m_window},
|
||
getch_iterator{m_window})};
|
||
}
|
||
return {};
|
||
};
|
||
|
||
if (c == 27)
|
||
{
|
||
wtimeout(m_window, 0);
|
||
const int new_c = wgetch(m_window);
|
||
if (new_c == '[') // potential CSI
|
||
{
|
||
const Codepoint csi_val = wgetch(m_window);
|
||
switch (csi_val)
|
||
{
|
||
case 'I': return {Key::FocusIn};
|
||
case 'O': return {Key::FocusOut};
|
||
default: break; // nothing
|
||
}
|
||
}
|
||
wtimeout(m_window, -1);
|
||
|
||
if (auto key = parse_key(new_c))
|
||
return alt(*key);
|
||
else
|
||
return {Key::Escape};
|
||
}
|
||
return parse_key(c);
|
||
}
|
||
|
||
template<typename T>
|
||
T div_round_up(T a, T b)
|
||
{
|
||
return (a - T(1)) / b + T(1);
|
||
}
|
||
|
||
void NCursesUI::draw_menu()
|
||
{
|
||
// menu show may have not created the window if it did not fit.
|
||
// so be tolerant.
|
||
if (not m_menu)
|
||
return;
|
||
|
||
const auto menu_bg = get_color_pair(m_menu.bg);
|
||
wattron(m_menu.win, COLOR_PAIR(menu_bg));
|
||
wbkgdset(m_menu.win, COLOR_PAIR(menu_bg));
|
||
|
||
const int item_count = (int)m_menu.items.size();
|
||
const LineCount menu_lines = div_round_up(item_count, m_menu.columns);
|
||
const LineCount& win_height = m_menu.size.line;
|
||
kak_assert(win_height <= menu_lines);
|
||
|
||
const ColumnCount column_width = (m_menu.size.column - 1) / m_menu.columns;
|
||
|
||
const LineCount mark_height = min(div_round_up(sq(win_height), menu_lines),
|
||
win_height);
|
||
const LineCount mark_line = (win_height - mark_height) * m_menu.top_line /
|
||
max(1_line, menu_lines - win_height);
|
||
for (auto line = 0_line; line < win_height; ++line)
|
||
{
|
||
wmove(m_menu.win, (int)line, 0);
|
||
for (int col = 0; col < m_menu.columns; ++col)
|
||
{
|
||
const int item_idx = (int)(m_menu.top_line + line) * m_menu.columns
|
||
+ col;
|
||
if (item_idx >= item_count)
|
||
break;
|
||
|
||
const DisplayLine& item = m_menu.items[item_idx];
|
||
draw_line(m_menu.win, item, 0, column_width,
|
||
item_idx == m_menu.selected_item ? m_menu.fg : m_menu.bg);
|
||
const ColumnCount pad = column_width - item.length();
|
||
add_str(m_menu.win, String{' ', pad});
|
||
}
|
||
const bool is_mark = line >= mark_line and
|
||
line < mark_line + mark_height;
|
||
wclrtoeol(m_menu.win);
|
||
wmove(m_menu.win, (int)line, (int)m_menu.size.column - 1);
|
||
wattron(m_menu.win, COLOR_PAIR(menu_bg));
|
||
add_str(m_menu.win, is_mark ? "█" : "░");
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::menu_show(ConstArrayView<DisplayLine> items,
|
||
DisplayCoord anchor, Face fg, Face bg,
|
||
MenuStyle style)
|
||
{
|
||
menu_hide();
|
||
|
||
m_menu.fg = fg;
|
||
m_menu.bg = bg;
|
||
m_menu.style = style;
|
||
m_menu.anchor = anchor;
|
||
|
||
if (style == MenuStyle::Prompt)
|
||
anchor = DisplayCoord{m_status_on_top ? 0_line : m_dimensions.line, 0};
|
||
else if (m_status_on_top)
|
||
anchor.line += 1;
|
||
|
||
DisplayCoord maxsize = m_dimensions;
|
||
maxsize.column -= anchor.column;
|
||
if (maxsize.column <= 2)
|
||
return;
|
||
|
||
const int item_count = items.size();
|
||
m_menu.items.clear(); // make sure it is empty
|
||
m_menu.items.reserve(item_count);
|
||
ColumnCount longest = 1;
|
||
for (auto& item : items)
|
||
longest = max(longest, item.length());
|
||
|
||
const bool is_prompt = style == MenuStyle::Prompt;
|
||
m_menu.columns = is_prompt ? max((int)((maxsize.column-1) / (longest+1)), 1) : 1;
|
||
|
||
ColumnCount maxlen = maxsize.column-1;
|
||
if (m_menu.columns > 1 and item_count > 1)
|
||
maxlen = maxlen / m_menu.columns - 1;
|
||
|
||
for (auto& item : items)
|
||
{
|
||
m_menu.items.push_back(item);
|
||
m_menu.items.back().trim(0, maxlen);
|
||
kak_assert(m_menu.items.back().length() <= maxlen);
|
||
}
|
||
|
||
int height = min(10, div_round_up(item_count, m_menu.columns));
|
||
|
||
int line = (int)anchor.line + 1;
|
||
if (line + height >= (int)maxsize.line)
|
||
line = (int)anchor.line - height;
|
||
m_menu.selected_item = item_count;
|
||
m_menu.top_line = 0;
|
||
|
||
auto width = is_prompt ? maxsize.column : min(longest+1, maxsize.column);
|
||
m_menu.create({line, anchor.column}, {height, width});
|
||
draw_menu();
|
||
|
||
if (m_info)
|
||
info_show(m_info.title, m_info.content,
|
||
m_info.anchor, m_info.face, m_info.style);
|
||
}
|
||
|
||
void NCursesUI::menu_select(int selected)
|
||
{
|
||
const int item_count = m_menu.items.size();
|
||
const LineCount menu_lines = div_round_up(item_count, m_menu.columns);
|
||
if (selected < 0 or selected >= item_count)
|
||
{
|
||
m_menu.selected_item = -1;
|
||
m_menu.top_line = 0;
|
||
}
|
||
else
|
||
{
|
||
m_menu.selected_item = selected;
|
||
const LineCount selected_line = m_menu.selected_item / m_menu.columns;
|
||
const LineCount win_height = m_menu.size.line;
|
||
kak_assert(menu_lines >= win_height);
|
||
if (selected_line < m_menu.top_line)
|
||
m_menu.top_line = selected_line;
|
||
if (selected_line >= m_menu.top_line + win_height)
|
||
m_menu.top_line = min(selected_line, menu_lines - win_height);
|
||
}
|
||
draw_menu();
|
||
}
|
||
|
||
void NCursesUI::menu_hide()
|
||
{
|
||
if (not m_menu)
|
||
return;
|
||
m_menu.items.clear();
|
||
mark_dirty(m_menu);
|
||
m_menu.destroy();
|
||
m_dirty = true;
|
||
|
||
// 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);
|
||
}
|
||
|
||
static DisplayCoord compute_pos(DisplayCoord anchor, DisplayCoord size,
|
||
NCursesUI::Rect rect, NCursesUI::Rect to_avoid,
|
||
bool prefer_above)
|
||
{
|
||
DisplayCoord pos;
|
||
if (prefer_above)
|
||
{
|
||
pos = anchor - DisplayCoord{size.line};
|
||
if (pos.line < 0)
|
||
prefer_above = false;
|
||
}
|
||
auto rect_end = rect.pos + rect.size;
|
||
if (not prefer_above)
|
||
{
|
||
pos = anchor + DisplayCoord{1_line};
|
||
if (pos.line + size.line > rect_end.line)
|
||
pos.line = max(rect.pos.line, anchor.line - size.line);
|
||
}
|
||
if (pos.column + size.column > rect_end.column)
|
||
pos.column = max(rect.pos.column, rect_end.column - size.column);
|
||
|
||
if (to_avoid.size != DisplayCoord{})
|
||
{
|
||
DisplayCoord to_avoid_end = to_avoid.pos + to_avoid.size;
|
||
|
||
DisplayCoord end = pos + size;
|
||
|
||
// check intersection
|
||
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))
|
||
{
|
||
pos.line = min(to_avoid.pos.line, anchor.line) - size.line;
|
||
// if above does not work, try below
|
||
if (pos.line < 0)
|
||
pos.line = max(to_avoid_end.line, anchor.line);
|
||
}
|
||
}
|
||
|
||
return pos;
|
||
}
|
||
|
||
Vector<String> make_info_box(StringView title, StringView message, ColumnCount max_width,
|
||
ConstArrayView<StringView> assistant)
|
||
{
|
||
DisplayCoord assistant_size;
|
||
if (not assistant.empty())
|
||
assistant_size = { (int)assistant.size(), assistant[0].column_length() };
|
||
|
||
Vector<String> result;
|
||
|
||
const ColumnCount max_bubble_width = max_width - assistant_size.column - 6;
|
||
if (max_bubble_width < 4)
|
||
return result;
|
||
|
||
Vector<StringView> lines = wrap_lines(message, max_bubble_width);
|
||
|
||
ColumnCount bubble_width = title.column_length() + 2;
|
||
for (auto& line : lines)
|
||
bubble_width = max(bubble_width, line.column_length());
|
||
|
||
auto line_count = max(assistant_size.line-1,
|
||
LineCount{(int)lines.size()} + 2);
|
||
const auto assistant_top_margin = (line_count - assistant_size.line+1) / 2;
|
||
for (LineCount i = 0; i < line_count; ++i)
|
||
{
|
||
String line;
|
||
constexpr Codepoint dash{L'─'};
|
||
if (not assistant.empty())
|
||
{
|
||
if (i >= assistant_top_margin)
|
||
line += assistant[(int)min(i - assistant_top_margin, assistant_size.line-1)];
|
||
else
|
||
line += assistant[(int)assistant_size.line-1];
|
||
}
|
||
if (i == 0)
|
||
{
|
||
if (title.empty())
|
||
line += "╭─" + String{dash, bubble_width} + "─╮";
|
||
else
|
||
{
|
||
auto dash_count = bubble_width - title.column_length() - 2;
|
||
String left{dash, dash_count / 2};
|
||
String right{dash, dash_count - dash_count / 2};
|
||
line += "╭─" + left + "┤" + title +"├" + right +"─╮";
|
||
}
|
||
}
|
||
else if (i < lines.size() + 1)
|
||
{
|
||
auto& info_line = lines[(int)i - 1];
|
||
const ColumnCount padding = bubble_width - info_line.column_length();
|
||
line += "│ " + info_line + String{' ', padding} + " │";
|
||
}
|
||
else if (i == lines.size() + 1)
|
||
line += "╰─" + String(dash, bubble_width) + "─╯";
|
||
|
||
result.push_back(std::move(line));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void NCursesUI::info_show(StringView title, StringView content,
|
||
DisplayCoord anchor, Face face, InfoStyle style)
|
||
{
|
||
info_hide();
|
||
|
||
m_info.title = title.str();
|
||
m_info.content = content.str();
|
||
m_info.anchor = anchor;
|
||
m_info.face = face;
|
||
m_info.style = style;
|
||
|
||
Vector<String> info_box;
|
||
if (style == InfoStyle::Prompt)
|
||
{
|
||
info_box = make_info_box(m_info.title, m_info.content,
|
||
m_dimensions.column, m_assistant);
|
||
anchor = DisplayCoord{m_status_on_top ? 0 : m_dimensions.line,
|
||
m_dimensions.column-1};
|
||
}
|
||
else if (style == InfoStyle::Modal)
|
||
info_box = make_info_box(m_info.title, m_info.content,
|
||
m_dimensions.column, {});
|
||
else
|
||
{
|
||
if (m_status_on_top)
|
||
anchor.line += 1;
|
||
ColumnCount col = anchor.column;
|
||
if (style == InfoStyle::MenuDoc and m_menu)
|
||
col = m_menu.pos.column + m_menu.size.column;
|
||
|
||
const ColumnCount max_width = m_dimensions.column - col;
|
||
if (max_width < 4)
|
||
return;
|
||
|
||
for (auto& line : wrap_lines(m_info.content, max_width))
|
||
info_box.push_back(line.str());
|
||
}
|
||
|
||
const DisplayCoord size{(int)info_box.size(),
|
||
accumulate(info_box | transform(std::mem_fn(&String::column_length)), 0_col,
|
||
[](ColumnCount lhs, ColumnCount rhs){ return lhs < rhs ? rhs : lhs; })};
|
||
const Rect rect = {m_status_on_top ? 1_line : 0_line, m_dimensions};
|
||
DisplayCoord pos;
|
||
if (style == InfoStyle::MenuDoc and m_menu)
|
||
pos = m_menu.pos + DisplayCoord{0_line, m_menu.size.column};
|
||
else if (style == InfoStyle::Modal)
|
||
{
|
||
auto half = [](const DisplayCoord& c) { return DisplayCoord{c.line / 2, c.column / 2}; };
|
||
pos = rect.pos + half(rect.size) - half(size);
|
||
}
|
||
else
|
||
pos = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
|
||
|
||
// The info box does not fit
|
||
if (pos < rect.pos or pos + size > rect.pos + rect.size)
|
||
return;
|
||
|
||
m_info.create(pos, size);
|
||
|
||
wbkgd(m_info.win, COLOR_PAIR(get_color_pair(face)));
|
||
for (size_t line = 0; line < info_box.size(); ++line)
|
||
{
|
||
wmove(m_info.win, line, 0);
|
||
add_str(m_info.win, info_box[line]);
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::info_hide()
|
||
{
|
||
if (not m_info)
|
||
return;
|
||
mark_dirty(m_info);
|
||
m_info.destroy();
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::mark_dirty(const Window& win)
|
||
{
|
||
wredrawln(m_window, (int)win.pos.line, (int)win.size.line);
|
||
}
|
||
|
||
void NCursesUI::set_on_key(OnKeyCallback callback)
|
||
{
|
||
m_on_key = std::move(callback);
|
||
}
|
||
|
||
DisplayCoord NCursesUI::dimensions()
|
||
{
|
||
return m_dimensions;
|
||
}
|
||
|
||
void NCursesUI::abort()
|
||
{
|
||
endwin();
|
||
}
|
||
|
||
void NCursesUI::enable_mouse(bool enabled)
|
||
{
|
||
if (enabled == m_mouse_enabled)
|
||
return;
|
||
|
||
m_mouse_enabled = enabled;
|
||
if (enabled)
|
||
{
|
||
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
|
||
mouseinterval(0);
|
||
// force enable report mouse position
|
||
fputs("\033[?1002h", stdout);
|
||
// force enable report focus events
|
||
fputs("\033[?1004h", stdout);
|
||
}
|
||
else
|
||
{
|
||
mousemask(0, nullptr);
|
||
fputs("\033[?1004l", stdout);
|
||
fputs("\033[?1002l", stdout);
|
||
}
|
||
fflush(stdout);
|
||
}
|
||
|
||
void NCursesUI::set_ui_options(const Options& options)
|
||
{
|
||
{
|
||
auto it = options.find("ncurses_assistant"_sv);
|
||
if (it == options.end() or it->value == "clippy")
|
||
m_assistant = assistant_clippy;
|
||
else if (it->value == "cat")
|
||
m_assistant = assistant_cat;
|
||
else if (it->value == "dilbert")
|
||
m_assistant = assistant_dilbert;
|
||
else if (it->value == "none" or it->value == "off")
|
||
m_assistant = ConstArrayView<StringView>{};
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_status_on_top"_sv);
|
||
m_status_on_top = it != options.end() and
|
||
(it->value == "yes" or it->value == "true");
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_set_title"_sv);
|
||
m_set_title = it == options.end() or
|
||
(it->value == "yes" or it->value == "true");
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_change_colors"_sv);
|
||
auto value = it == options.end() or
|
||
(it->value == "yes" or it->value == "true");
|
||
|
||
if (can_change_color() and m_change_colors != value)
|
||
{
|
||
fputs("\033]104;\007", stdout); // try to reset palette
|
||
fflush(stdout);
|
||
m_colorpairs.clear();
|
||
m_colors = default_colors;
|
||
m_next_color = 16;
|
||
m_next_pair = 1;
|
||
m_active_pair = -1;
|
||
}
|
||
m_change_colors = value;
|
||
}
|
||
|
||
{
|
||
auto enable_mouse_it = options.find("ncurses_enable_mouse"_sv);
|
||
enable_mouse(enable_mouse_it == options.end() or
|
||
enable_mouse_it->value == "yes" or
|
||
enable_mouse_it->value == "true");
|
||
|
||
auto wheel_up_it = options.find("ncurses_wheel_up_button"_sv);
|
||
m_wheel_up_button = wheel_up_it != options.end() ?
|
||
str_to_int_ifp(wheel_up_it->value).value_or(4) : 4;
|
||
|
||
auto wheel_down_it = options.find("ncurses_wheel_down_button"_sv);
|
||
m_wheel_down_button = wheel_down_it != options.end() ?
|
||
str_to_int_ifp(wheel_down_it->value).value_or(5) : 5;
|
||
}
|
||
}
|
||
|
||
}
|