866 lines
27 KiB
C++
866 lines
27 KiB
C++
#include "ncurses_ui.hh"
|
||
|
||
#include "containers.hh"
|
||
#include "display_buffer.hh"
|
||
#include "event_manager.hh"
|
||
#include "keys.hh"
|
||
#include "register_manager.hh"
|
||
#include "utf8_iterator.hh"
|
||
|
||
#include <map>
|
||
#include <algorithm>
|
||
|
||
#define NCURSES_OPAQUE 0
|
||
#define NCURSES_INTERNALS
|
||
|
||
#include <ncurses.h>
|
||
|
||
#include <fcntl.h>
|
||
#include <signal.h>
|
||
#include <sys/ioctl.h>
|
||
#include <termios.h>
|
||
#include <unistd.h>
|
||
|
||
namespace Kakoune
|
||
{
|
||
|
||
using std::min;
|
||
using std::max;
|
||
|
||
struct NCursesWin : WINDOW {};
|
||
|
||
static constexpr StringView assistant_cat[] =
|
||
{ R"( ___ )",
|
||
R"( / __) )",
|
||
R"( \ \ ╭)",
|
||
R"( .·' '. │)",
|
||
R"( ” ' ╯)",
|
||
R"( |\_/\ ╯ )",
|
||
R"( / . | )",
|
||
R"( | | | ’l_╯ )",
|
||
R"( \_ -__/ ' )",
|
||
R"( /_/ /_/ )",
|
||
R"( )"};
|
||
|
||
static constexpr StringView assistant_clippy[] =
|
||
{ " ╭──╮ ",
|
||
" │ │ ",
|
||
" @ @ ╭",
|
||
" ││ ││ │",
|
||
" ││ ││ ╯",
|
||
" │╰─╯│ ",
|
||
" ╰───╯ ",
|
||
" " };
|
||
|
||
static void set_attribute(WINDOW* window, int attribute, bool on)
|
||
{
|
||
if (on)
|
||
wattron(window, attribute);
|
||
else
|
||
wattroff(window, attribute);
|
||
}
|
||
|
||
static bool operator<(Color lhs, Color rhs)
|
||
{
|
||
if (lhs.color == rhs.color and lhs.color == Colors::RGB)
|
||
return lhs.r == rhs.r ? (lhs.g == rhs.g ? lhs.b < rhs.b
|
||
: lhs.g < rhs.g)
|
||
: lhs.r < rhs.r;
|
||
return lhs.color < rhs.color;
|
||
}
|
||
|
||
template<typename T> T sq(T x) { return x * x; }
|
||
|
||
static int nc_color(Color color)
|
||
{
|
||
static std::map<Color, int> colors = {
|
||
{ Colors::Default, -1 },
|
||
{ Colors::Black, COLOR_BLACK },
|
||
{ Colors::Red, COLOR_RED },
|
||
{ Colors::Green, COLOR_GREEN },
|
||
{ Colors::Yellow, COLOR_YELLOW },
|
||
{ Colors::Blue, COLOR_BLUE },
|
||
{ Colors::Magenta, COLOR_MAGENTA },
|
||
{ Colors::Cyan, COLOR_CYAN },
|
||
{ Colors::White, COLOR_WHITE },
|
||
};
|
||
static int next_color = 8;
|
||
|
||
auto it = colors.find(color);
|
||
if (it != colors.end())
|
||
return it->second;
|
||
else if (can_change_color() and COLORS > 8)
|
||
{
|
||
kak_assert(color.color == Colors::RGB);
|
||
if (next_color > COLORS)
|
||
next_color = 8;
|
||
init_color(next_color,
|
||
color.r * 1000 / 255,
|
||
color.g * 1000 / 255,
|
||
color.b * 1000 / 255);
|
||
colors[color] = next_color;
|
||
return next_color++;
|
||
}
|
||
else
|
||
{
|
||
kak_assert(color.color == Colors::RGB);
|
||
static const 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 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;
|
||
}
|
||
}
|
||
|
||
static int get_color_pair(const Face& face)
|
||
{
|
||
using ColorPair = std::pair<Color, Color>;
|
||
static UnorderedMap<ColorPair, int> colorpairs;
|
||
static int next_pair = 1;
|
||
|
||
ColorPair colors{face.fg, face.bg};
|
||
auto it = colorpairs.find(colors);
|
||
if (it != colorpairs.end())
|
||
return it->second;
|
||
else
|
||
{
|
||
init_pair(next_pair, nc_color(face.fg), nc_color(face.bg));
|
||
colorpairs[colors] = next_pair;
|
||
return next_pair++;
|
||
}
|
||
}
|
||
|
||
static void set_face(WINDOW* window, Face face)
|
||
{
|
||
static int current_pair = -1;
|
||
|
||
if (current_pair != -1)
|
||
wattroff(window, COLOR_PAIR(current_pair));
|
||
|
||
if (face.fg != Colors::Default or face.bg != Colors::Default)
|
||
{
|
||
current_pair = get_color_pair(face);
|
||
wattron(window, COLOR_PAIR(current_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);
|
||
}
|
||
|
||
static sig_atomic_t resize_pending = 0;
|
||
|
||
void on_term_resize(int)
|
||
{
|
||
resize_pending = 1;
|
||
EventManager::instance().force_signal(0);
|
||
}
|
||
|
||
void on_sigint(int)
|
||
{
|
||
// do nothing
|
||
}
|
||
|
||
NCursesUI::NCursesUI()
|
||
: m_stdin_watcher{0, [this](FDWatcher&, EventMode mode) {
|
||
if (m_input_callback)
|
||
m_input_callback(mode);
|
||
}},
|
||
m_assistant(assistant_clippy)
|
||
{
|
||
initscr();
|
||
raw();
|
||
noecho();
|
||
nonl();
|
||
intrflush(stdscr, false);
|
||
keypad(stdscr, true);
|
||
curs_set(0);
|
||
start_color();
|
||
use_default_colors();
|
||
set_escdelay(25);
|
||
|
||
signal(SIGWINCH, on_term_resize);
|
||
signal(SIGINT, on_sigint);
|
||
|
||
update_dimensions();
|
||
|
||
wrefresh(stdscr);
|
||
}
|
||
|
||
NCursesUI::~NCursesUI()
|
||
{
|
||
endwin();
|
||
signal(SIGWINCH, SIG_DFL);
|
||
signal(SIGINT, SIG_DFL);
|
||
}
|
||
|
||
void NCursesUI::redraw()
|
||
{
|
||
redrawwin(m_window);
|
||
wnoutrefresh(m_window);
|
||
if (m_menu_win)
|
||
{
|
||
redrawwin(m_menu_win);
|
||
wnoutrefresh(m_menu_win);
|
||
}
|
||
if (m_info_win)
|
||
{
|
||
redrawwin(m_info_win);
|
||
wnoutrefresh(m_info_win);
|
||
}
|
||
doupdate();
|
||
}
|
||
|
||
void NCursesUI::refresh()
|
||
{
|
||
if (m_dirty)
|
||
redraw();
|
||
m_dirty = false;
|
||
}
|
||
|
||
using Utf8Policy = utf8::InvalidPolicy::Pass;
|
||
using Utf8Iterator = utf8::iterator<const char*, Utf8Policy>;
|
||
void addutf8str(WINDOW* win, Utf8Iterator begin, Utf8Iterator end)
|
||
{
|
||
waddstr(win, StringView(begin.base(), end.base()).zstr());
|
||
}
|
||
|
||
static CharCoord window_size(WINDOW* win)
|
||
{
|
||
CharCoord size;
|
||
getmaxyx(win, (int&)size.line, (int&)size.column);
|
||
return size;
|
||
}
|
||
|
||
static CharCoord window_pos(WINDOW* win)
|
||
{
|
||
CharCoord pos;
|
||
getbegyx(win, (int&)pos.line, (int&)pos.column);
|
||
return pos;
|
||
}
|
||
|
||
void NCursesUI::update_dimensions()
|
||
{
|
||
m_dimensions = window_size(stdscr);
|
||
|
||
if (m_window)
|
||
delwin(m_window);
|
||
m_window = (NCursesWin*)newwin((int)m_dimensions.line, (int)m_dimensions.column, 0, 0);
|
||
|
||
--m_dimensions.line;
|
||
}
|
||
|
||
void NCursesUI::draw_line(const DisplayLine& line, CharCount col_index) const
|
||
{
|
||
for (const DisplayAtom& atom : line)
|
||
{
|
||
set_face(m_window, atom.face);
|
||
|
||
StringView content = atom.content();
|
||
if (content.empty())
|
||
continue;
|
||
|
||
if (content[content.length()-1] == '\n' and
|
||
content.char_length() - 1 < m_dimensions.column - col_index)
|
||
{
|
||
addutf8str(m_window, Utf8Iterator{content.begin()},
|
||
Utf8Iterator{content.end()}-1);
|
||
waddch(m_window, ' ');
|
||
}
|
||
else
|
||
{
|
||
Utf8Iterator begin{content.begin()}, end{content.end()};
|
||
if (end - begin > m_dimensions.column - col_index)
|
||
end = begin + (m_dimensions.column - col_index);
|
||
addutf8str(m_window, begin, end);
|
||
col_index += end - begin;
|
||
}
|
||
}
|
||
}
|
||
|
||
void NCursesUI::draw(const DisplayBuffer& display_buffer,
|
||
const DisplayLine& status_line,
|
||
const DisplayLine& mode_line)
|
||
{
|
||
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(line, 0);
|
||
++line_index;
|
||
}
|
||
|
||
set_face(m_window, { Colors::Blue, Colors::Default });
|
||
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, '~');
|
||
}
|
||
|
||
int status_line_pos = m_status_on_top ? 0 : (int)m_dimensions.line;
|
||
wmove(m_window, status_line_pos, 0);
|
||
wclrtoeol(m_window);
|
||
draw_line(status_line, 0);
|
||
CharCount status_len = mode_line.length();
|
||
// only draw mode_line if it does not overlap one status line
|
||
if (m_dimensions.column - status_line.length() > status_len + 1)
|
||
{
|
||
CharCount col = m_dimensions.column - status_len;
|
||
wmove(m_window, status_line_pos, (int)col);
|
||
draw_line(mode_line, col);
|
||
}
|
||
|
||
const char* tsl = tigetstr((char*)"tsl");
|
||
const char* fsl = tigetstr((char*)"fsl");
|
||
if (tsl != 0 and (ptrdiff_t)tsl != -1 and
|
||
fsl != 0 and (ptrdiff_t)fsl != -1)
|
||
{
|
||
String title;
|
||
for (auto& atom : mode_line)
|
||
title += atom.content();
|
||
title += " - Kakoune";
|
||
printf("%s%s%s", tsl, title.c_str(), fsl);
|
||
}
|
||
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::check_resize()
|
||
{
|
||
if (resize_pending)
|
||
{
|
||
int fd = open("/dev/tty", O_RDWR);
|
||
winsize ws;
|
||
if (ioctl(fd, TIOCGWINSZ, (void*)&ws) == 0)
|
||
{
|
||
close(fd);
|
||
resizeterm(ws.ws_row, ws.ws_col);
|
||
update_dimensions();
|
||
}
|
||
ungetch(KEY_RESIZE);
|
||
m_dirty = true;
|
||
resize_pending = false;
|
||
}
|
||
}
|
||
|
||
bool NCursesUI::is_key_available()
|
||
{
|
||
check_resize();
|
||
|
||
timeout(0);
|
||
const int c = getch();
|
||
if (c != ERR)
|
||
ungetch(c);
|
||
timeout(-1);
|
||
return c != ERR;
|
||
}
|
||
|
||
Key NCursesUI::get_key()
|
||
{
|
||
check_resize();
|
||
|
||
const int c = getch();
|
||
if (c > 0 and c < 27)
|
||
{
|
||
if (c == CTRL('l'))
|
||
redrawwin(stdscr);
|
||
if (c == CTRL('z'))
|
||
{
|
||
raise(SIGTSTP);
|
||
return Key::Invalid;
|
||
}
|
||
return ctrl(Codepoint(c) - 1 + 'a');
|
||
}
|
||
else if (c == 27)
|
||
{
|
||
timeout(0);
|
||
const Codepoint new_c = getch();
|
||
timeout(-1);
|
||
if (new_c != ERR)
|
||
{
|
||
if (new_c > 0 and new_c < 27)
|
||
return ctrlalt(Codepoint(new_c) - 1 + 'a');
|
||
return alt(new_c);
|
||
}
|
||
else
|
||
return Key::Escape;
|
||
}
|
||
else 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;
|
||
}
|
||
|
||
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
|
||
{
|
||
int operator*() { return getch(); }
|
||
getch_iterator& operator++() { return *this; }
|
||
getch_iterator& operator++(int) { return *this; }
|
||
bool operator== (const getch_iterator&) const { return false; }
|
||
};
|
||
return utf8::codepoint(getch_iterator{}, getch_iterator{});
|
||
}
|
||
return Key::Invalid;
|
||
}
|
||
|
||
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_win)
|
||
return;
|
||
|
||
const auto menu_fg = get_color_pair(m_menu_fg);
|
||
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_items.size();
|
||
const LineCount menu_lines = div_round_up(item_count, m_menu_columns);
|
||
const CharCoord win_size = window_size(m_menu_win);
|
||
const LineCount& win_height = win_size.line;
|
||
kak_assert(win_height <= menu_lines);
|
||
|
||
const CharCount column_width = (win_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;
|
||
if (item_idx == m_selected_item)
|
||
wattron(m_menu_win, COLOR_PAIR(menu_fg));
|
||
|
||
StringView item = m_items[item_idx];
|
||
auto begin = item.begin();
|
||
auto end = utf8::advance(begin, item.end(), column_width);
|
||
addutf8str(m_menu_win, begin, end);
|
||
const CharCount pad = column_width - utf8::distance(begin, end);
|
||
waddstr(m_menu_win, String{' ' COMMA pad}.c_str());
|
||
wattron(m_menu_win, COLOR_PAIR(menu_bg));
|
||
}
|
||
const bool is_mark = line >= mark_line and
|
||
line < mark_line + mark_height;
|
||
wclrtoeol(m_menu_win);
|
||
wmove(m_menu_win, (int)line, (int)win_size.column - 1);
|
||
wattron(m_menu_win, COLOR_PAIR(menu_bg));
|
||
waddstr(m_menu_win, is_mark ? "█" : "░");
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::menu_show(ConstArrayView<String> items,
|
||
CharCoord anchor, Face fg, Face bg,
|
||
MenuStyle style)
|
||
{
|
||
if (m_menu_win)
|
||
delwin(m_menu_win);
|
||
m_items.clear();
|
||
|
||
m_menu_fg = fg;
|
||
m_menu_bg = bg;
|
||
|
||
if (style == MenuStyle::Prompt)
|
||
anchor = CharCoord{m_status_on_top ? 0_line : m_dimensions.line, 0};
|
||
else if (m_status_on_top)
|
||
anchor.line += 1;
|
||
|
||
CharCoord maxsize = window_size(stdscr);
|
||
maxsize.column -= anchor.column;
|
||
if (maxsize.column <= 2)
|
||
return;
|
||
|
||
const int item_count = items.size();
|
||
m_items.reserve(item_count);
|
||
CharCount longest = 0;
|
||
const CharCount maxlen = min((int)maxsize.column-2, 200);
|
||
for (auto& item : items)
|
||
{
|
||
m_items.push_back(item.substr(0_char, maxlen).str());
|
||
longest = max(longest, m_items.back().char_length());
|
||
}
|
||
longest += 1;
|
||
|
||
const bool is_prompt = style == MenuStyle::Prompt;
|
||
m_menu_columns = is_prompt ? (int)((maxsize.column - 1) / longest) : 1;
|
||
|
||
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_selected_item = item_count;
|
||
m_menu_top_line = 0;
|
||
|
||
int width = is_prompt ? (int)maxsize.column : (int)longest;
|
||
m_menu_win = (NCursesWin*)newwin(height, width, line, (int)anchor.column);
|
||
draw_menu();
|
||
}
|
||
|
||
void NCursesUI::menu_select(int selected)
|
||
{
|
||
const int item_count = m_items.size();
|
||
const LineCount menu_lines = div_round_up(item_count, m_menu_columns);
|
||
if (selected < 0 or selected >= item_count)
|
||
{
|
||
m_selected_item = -1;
|
||
m_menu_top_line = 0;
|
||
}
|
||
else
|
||
{
|
||
m_selected_item = selected;
|
||
const LineCount selected_line = m_selected_item / m_menu_columns;
|
||
const LineCount win_height = window_size(m_menu_win).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_win)
|
||
return;
|
||
m_items.clear();
|
||
delwin(m_menu_win);
|
||
m_menu_win = nullptr;
|
||
m_dirty = true;
|
||
}
|
||
|
||
static CharCoord compute_needed_size(StringView str)
|
||
{
|
||
CharCoord res{1,0};
|
||
CharCount line_len = 0;
|
||
for (Utf8Iterator begin{str.begin()}, end{str.end()};
|
||
begin != end; ++begin)
|
||
{
|
||
if (*begin == '\n')
|
||
{
|
||
// ignore last '\n', no need to show an empty line
|
||
if (begin+1 == end)
|
||
break;
|
||
|
||
res.column = max(res.column, line_len);
|
||
line_len = 0;
|
||
++res.line;
|
||
}
|
||
else
|
||
{
|
||
++line_len;
|
||
res.column = max(res.column, line_len);
|
||
}
|
||
}
|
||
return res;
|
||
}
|
||
|
||
static CharCoord compute_pos(CharCoord anchor, CharCoord size,
|
||
WINDOW* opt_window_to_avoid = nullptr,
|
||
bool prefer_above = false)
|
||
{
|
||
CharCoord scrsize = window_size(stdscr);
|
||
CharCoord pos;
|
||
if (prefer_above)
|
||
{
|
||
pos = anchor - CharCoord{size.line};
|
||
if (pos.line < 0)
|
||
prefer_above = false;
|
||
}
|
||
if (not prefer_above)
|
||
{
|
||
pos = anchor + CharCoord{1_line};
|
||
if (pos.line + size.line >= scrsize.line)
|
||
pos.line = max(0_line, anchor.line - size.line);
|
||
}
|
||
if (pos.column + size.column >= scrsize.column)
|
||
pos.column = max(0_char, scrsize.column - size.column);
|
||
|
||
if (opt_window_to_avoid)
|
||
{
|
||
CharCoord winbeg = window_pos(opt_window_to_avoid);
|
||
CharCoord winend = winbeg + window_size(opt_window_to_avoid);
|
||
|
||
CharCoord end = pos + size;
|
||
|
||
// check intersection
|
||
if (not (end.line < winbeg.line or end.column < winbeg.column or
|
||
pos.line > winend.line or pos.column > winend.column))
|
||
{
|
||
pos.line = min(winbeg.line, anchor.line) - size.line;
|
||
// if above does not work, try below
|
||
if (pos.line < 0)
|
||
pos.line = max(winend.line, anchor.line);
|
||
}
|
||
}
|
||
|
||
return pos;
|
||
}
|
||
|
||
String make_info_box(StringView title, StringView message, CharCount max_width,
|
||
ConstArrayView<StringView> assistant)
|
||
{
|
||
CharCoord assistant_size;
|
||
if (not assistant.empty())
|
||
assistant_size = { (int)assistant.size(), assistant[0].char_length() };
|
||
|
||
const CharCount max_bubble_width = max_width - assistant_size.column - 6;
|
||
Vector<StringView> lines = wrap_lines(message, max_bubble_width);
|
||
|
||
CharCount bubble_width = title.char_length() + 2;
|
||
for (auto& line : lines)
|
||
bubble_width = max(bubble_width, line.char_length());
|
||
|
||
String result;
|
||
auto line_count = max(assistant_size.line-1,
|
||
LineCount{(int)lines.size()} + 2);
|
||
for (LineCount i = 0; i < line_count; ++i)
|
||
{
|
||
constexpr Codepoint dash{L'─'};
|
||
if (not assistant.empty())
|
||
result += assistant[min((int)i, (int)assistant_size.line-1)];
|
||
if (i == 0)
|
||
{
|
||
if (title.empty())
|
||
result += "╭─" + String{dash, bubble_width} + "─╮";
|
||
else
|
||
{
|
||
auto dash_count = bubble_width - title.char_length() - 2;
|
||
String left{dash, dash_count / 2};
|
||
String right{dash, dash_count - dash_count / 2};
|
||
result += "╭─" + left + "┤" + title +"├" + right +"─╮";
|
||
}
|
||
}
|
||
else if (i < lines.size() + 1)
|
||
{
|
||
auto& line = lines[(int)i - 1];
|
||
const CharCount padding = bubble_width - line.char_length();
|
||
result += "│ " + line + String{' ', padding} + " │";
|
||
}
|
||
else if (i == lines.size() + 1)
|
||
result += "╰─" + String(dash, bubble_width) + "─╯";
|
||
|
||
result += "\n";
|
||
}
|
||
return result;
|
||
}
|
||
|
||
void NCursesUI::info_show(StringView title, StringView content,
|
||
CharCoord anchor, Face face, InfoStyle style)
|
||
{
|
||
if (m_info_win)
|
||
delwin(m_info_win);
|
||
|
||
StringView info_box = content;
|
||
String fancy_info_box;
|
||
if (style == InfoStyle::Prompt)
|
||
{
|
||
fancy_info_box = make_info_box(title, content, m_dimensions.column,
|
||
m_assistant);
|
||
info_box = fancy_info_box;
|
||
anchor = CharCoord{m_status_on_top ? 0 : m_dimensions.line,
|
||
m_dimensions.column-1};
|
||
}
|
||
else
|
||
{
|
||
if (m_status_on_top)
|
||
anchor.line += 1;
|
||
CharCount col = anchor.column;
|
||
if (style == InfoStyle::MenuDoc and m_menu_win)
|
||
col = window_pos(m_menu_win).column + window_size(m_menu_win).column;
|
||
|
||
for (auto& line : wrap_lines(content, m_dimensions.column - col))
|
||
fancy_info_box += line + "\n";
|
||
info_box = fancy_info_box;
|
||
}
|
||
|
||
CharCoord size = compute_needed_size(info_box);
|
||
CharCoord pos;
|
||
if (style == InfoStyle::MenuDoc and m_menu_win)
|
||
pos = window_pos(m_menu_win) +
|
||
CharCoord{0_line, window_size(m_menu_win).column};
|
||
else
|
||
pos = compute_pos(anchor, size, m_menu_win, style == InfoStyle::InlineAbove);
|
||
|
||
m_info_win = (NCursesWin*)newwin((int)size.line, (int)size.column,
|
||
(int)pos.line, (int)pos.column);
|
||
|
||
wbkgd(m_info_win, COLOR_PAIR(get_color_pair(face)));
|
||
int line = 0;
|
||
auto it = info_box.begin(), end = info_box.end();
|
||
while (true)
|
||
{
|
||
wmove(m_info_win, line++, 0);
|
||
auto eol = std::find_if(it, end, [](char c) { return c == '\n'; });
|
||
addutf8str(m_info_win, Utf8Iterator(it), Utf8Iterator(eol));
|
||
if (eol == end)
|
||
break;
|
||
it = eol + 1;
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::info_hide()
|
||
{
|
||
if (not m_info_win)
|
||
return;
|
||
delwin(m_info_win);
|
||
m_info_win = nullptr;
|
||
m_dirty = true;
|
||
}
|
||
|
||
CharCoord NCursesUI::dimensions()
|
||
{
|
||
return m_dimensions;
|
||
}
|
||
|
||
void NCursesUI::set_input_callback(InputCallback callback)
|
||
{
|
||
m_input_callback = std::move(callback);
|
||
}
|
||
|
||
void NCursesUI::abort()
|
||
{
|
||
endwin();
|
||
}
|
||
|
||
void NCursesUI::set_ui_options(const Options& options)
|
||
{
|
||
{
|
||
auto it = options.find("ncurses_assistant");
|
||
if (it != options.end())
|
||
{
|
||
if (it->second == "cat")
|
||
m_assistant = assistant_cat;
|
||
else if (it->second == "clippy")
|
||
m_assistant = assistant_clippy;
|
||
else if (it->second == "none" or it->second == "off")
|
||
m_assistant = ConstArrayView<StringView>{};
|
||
}
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_status_on_top");
|
||
if (it != options.end())
|
||
m_status_on_top = it->second == "yes" or it->second == "true";
|
||
}
|
||
}
|
||
|
||
}
|