c68f85659f
The previous handling code was at a pretty random location and broke terminals that could send <c-z> encoded in a non pure ascii way. See #4238
1436 lines
48 KiB
C++
1436 lines
48 KiB
C++
#include "ncurses_ui.hh"
|
||
|
||
#include "display_buffer.hh"
|
||
#include "event_manager.hh"
|
||
#include "exception.hh"
|
||
#include "file.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 <unistd.h>
|
||
#include <strings.h>
|
||
|
||
constexpr char control(char c) { return c & 037; }
|
||
|
||
namespace Kakoune
|
||
{
|
||
|
||
using std::min;
|
||
using std::max;
|
||
|
||
struct NCursesWin : WINDOW {};
|
||
|
||
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(bool force)
|
||
{
|
||
if (not win)
|
||
return;
|
||
|
||
if (force)
|
||
redrawwin(win);
|
||
|
||
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::Window::move_cursor(DisplayCoord coord)
|
||
{
|
||
wmove(win, (int)coord.line, (int)coord.column);
|
||
}
|
||
|
||
void NCursesUI::Window::draw(Palette& palette, ConstArrayView<DisplayAtom> atoms,
|
||
const Face& default_face)
|
||
{
|
||
auto add_str = [&](StringView str) { waddnstr(win, str.begin(), (int)str.length()); };
|
||
|
||
auto set_face = [&](Face face) {
|
||
if (m_active_pair != -1)
|
||
wattroff(win, COLOR_PAIR(m_active_pair));
|
||
|
||
face = merge_faces(default_face, face);
|
||
|
||
if (face.fg != Color::Default or face.bg != Color::Default)
|
||
{
|
||
m_active_pair = palette.get_color_pair(face);
|
||
wattron(win, COLOR_PAIR(m_active_pair));
|
||
}
|
||
|
||
auto set_attribute = [&](Attribute attr, int nc_attr) {
|
||
(face.attributes & attr) ? wattron(win, nc_attr) : wattroff(win, nc_attr);
|
||
};
|
||
|
||
set_attribute(Attribute::Underline, A_UNDERLINE);
|
||
set_attribute(Attribute::Reverse, A_REVERSE);
|
||
set_attribute(Attribute::Blink, A_BLINK);
|
||
set_attribute(Attribute::Bold, A_BOLD);
|
||
set_attribute(Attribute::Dim, A_DIM);
|
||
#if defined(A_ITALIC)
|
||
set_attribute(Attribute::Italic, A_ITALIC);
|
||
#endif
|
||
};
|
||
|
||
wbkgdset(win, COLOR_PAIR(palette.get_color_pair(default_face)));
|
||
|
||
ColumnCount column = getcurx(win);
|
||
for (const DisplayAtom& atom : atoms)
|
||
{
|
||
StringView content = atom.content();
|
||
if (content.empty())
|
||
continue;
|
||
|
||
set_face(atom.face);
|
||
if (content.back() == '\n')
|
||
{
|
||
add_str(content.substr(0, content.length()-1));
|
||
waddch(win, ' ');
|
||
}
|
||
else
|
||
add_str(content);
|
||
column += content.column_length();
|
||
}
|
||
|
||
if (column < size.column)
|
||
wclrtoeol(win);
|
||
}
|
||
|
||
constexpr int NCursesUI::default_shift_function_key;
|
||
|
||
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"( )"};
|
||
|
||
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},
|
||
};
|
||
|
||
const std::initializer_list<HashMap<Kakoune::Color, int>::Item>
|
||
NCursesUI::Palette::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 },
|
||
};
|
||
|
||
int NCursesUI::Palette::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.isRGB());
|
||
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.isRGB());
|
||
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::Palette::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++;
|
||
}
|
||
}
|
||
|
||
bool NCursesUI::Palette::set_change_colors(bool change_colors)
|
||
{
|
||
bool reset = false;
|
||
if (can_change_color() and m_change_colors != change_colors)
|
||
{
|
||
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;
|
||
reset = true;
|
||
}
|
||
m_change_colors = change_colors;
|
||
return reset;
|
||
}
|
||
|
||
static sig_atomic_t resize_pending = 0;
|
||
static sig_atomic_t stdin_closed = 0;
|
||
|
||
template<sig_atomic_t* signal_flag>
|
||
static void signal_handler(int)
|
||
{
|
||
*signal_flag = 1;
|
||
EventManager::instance().force_signal(0);
|
||
}
|
||
|
||
NCursesUI::NCursesUI()
|
||
: m_cursor{CursorMode::Buffer, {}},
|
||
m_stdin_watcher{STDIN_FILENO, FdEvents::Read, EventMode::Urgent,
|
||
[this](FDWatcher&, FdEvents, EventMode) {
|
||
if (not m_on_key)
|
||
return;
|
||
|
||
while (auto key = get_next_key())
|
||
{
|
||
if (key == ctrl('z'))
|
||
kill(0, SIGTSTP); // We suspend at this line
|
||
else
|
||
m_on_key(*key);
|
||
}
|
||
}},
|
||
m_assistant(assistant_clippy)
|
||
{
|
||
if (not isatty(1))
|
||
throw runtime_error("stdout is not a tty");
|
||
|
||
tcgetattr(STDIN_FILENO, &m_original_termios);
|
||
|
||
initscr();
|
||
curs_set(0);
|
||
start_color();
|
||
use_default_colors();
|
||
|
||
set_terminal_mode();
|
||
enable_mouse(true);
|
||
|
||
set_signal_handler(SIGWINCH, &signal_handler<&resize_pending>);
|
||
set_signal_handler(SIGHUP, &signal_handler<&stdin_closed>);
|
||
set_signal_handler(SIGTSTP, [](int){ NCursesUI::instance().suspend(); });
|
||
|
||
check_resize(true);
|
||
redraw(false);
|
||
}
|
||
|
||
NCursesUI::~NCursesUI()
|
||
{
|
||
enable_mouse(false);
|
||
m_palette.set_change_colors(false);
|
||
restore_terminal_mode();
|
||
endwin();
|
||
set_signal_handler(SIGWINCH, SIG_DFL);
|
||
set_signal_handler(SIGHUP, SIG_DFL);
|
||
set_signal_handler(SIGTSTP, SIG_DFL);
|
||
}
|
||
|
||
void NCursesUI::suspend()
|
||
{
|
||
bool mouse_enabled = m_mouse_enabled;
|
||
enable_mouse(false);
|
||
bool change_color_enabled = m_palette.get_change_colors();
|
||
m_palette.set_change_colors(false);
|
||
endwin();
|
||
|
||
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);
|
||
restore_terminal_mode();
|
||
|
||
raise(SIGTSTP); // suspend here
|
||
|
||
tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
|
||
set_signal_handler(SIGTSTP, current);
|
||
sigprocmask(SIG_SETMASK, &old_mask, nullptr);
|
||
|
||
doupdate();
|
||
check_resize(true);
|
||
set_terminal_mode();
|
||
m_palette.set_change_colors(change_color_enabled);
|
||
enable_mouse(mouse_enabled);
|
||
}
|
||
|
||
void NCursesUI::set_terminal_mode() const
|
||
{
|
||
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;
|
||
|
||
tcsetattr(STDIN_FILENO, TCSANOW, &attr);
|
||
fputs("\033=", stdout);
|
||
// force enable report focus events
|
||
fputs("\033[?1004h", stdout);
|
||
// request CSI u style key reporting
|
||
fputs("\033[>4;1m", stdout);
|
||
fflush(stdout);
|
||
}
|
||
|
||
void NCursesUI::restore_terminal_mode() const
|
||
{
|
||
tcsetattr(STDIN_FILENO, TCSAFLUSH, &m_original_termios);
|
||
fputs("\033>", stdout);
|
||
fputs("\033[?1004l", stdout);
|
||
fputs("\033[>4;0m", stdout);
|
||
fflush(stdout);
|
||
}
|
||
|
||
void NCursesUI::redraw(bool force)
|
||
{
|
||
m_window.refresh(force);
|
||
|
||
if (m_menu.columns != 0 or m_menu.pos.column > m_status_len)
|
||
m_menu.refresh(false);
|
||
|
||
m_info.refresh(false);
|
||
|
||
Window screen{{}, static_cast<NCursesWin*>(newscr)};
|
||
if (m_cursor.mode == CursorMode::Prompt)
|
||
screen.move_cursor({m_status_on_top ? 0 : m_dimensions.line, m_cursor.coord.column});
|
||
else
|
||
screen.move_cursor(m_cursor.coord + content_line_offset());
|
||
|
||
doupdate();
|
||
}
|
||
|
||
void NCursesUI::set_cursor(CursorMode mode, DisplayCoord coord)
|
||
{
|
||
m_cursor = Cursor{mode, coord};
|
||
}
|
||
|
||
void NCursesUI::refresh(bool force)
|
||
{
|
||
if (m_dirty or force)
|
||
redraw(force);
|
||
m_dirty = false;
|
||
}
|
||
|
||
static const DisplayLine empty_line = { String(" "), {} };
|
||
|
||
void NCursesUI::draw(const DisplayBuffer& display_buffer,
|
||
const Face& default_face,
|
||
const Face& padding_face)
|
||
{
|
||
check_resize();
|
||
|
||
const DisplayCoord dim = dimensions();
|
||
const LineCount line_offset = content_line_offset();
|
||
LineCount line_index = line_offset;
|
||
for (const DisplayLine& line : display_buffer.lines())
|
||
{
|
||
m_window.move_cursor(line_index++);
|
||
m_window.draw(m_palette, line.atoms(), default_face);
|
||
}
|
||
|
||
auto face = merge_faces(default_face, padding_face);
|
||
while (line_index < dim.line + line_offset)
|
||
{
|
||
m_window.move_cursor(line_index++);
|
||
if (m_padding_fill)
|
||
{
|
||
ColumnCount column_index = 0;
|
||
while (column_index < dim.column)
|
||
{
|
||
m_window.draw(m_palette, m_padding_char, face);
|
||
column_index += m_padding_char.length();
|
||
}
|
||
}
|
||
else
|
||
m_window.draw(m_palette, m_padding_char, face);
|
||
}
|
||
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::draw_status(const DisplayLine& status_line,
|
||
const DisplayLine& mode_line,
|
||
const Face& default_face)
|
||
{
|
||
const LineCount status_line_pos = m_status_on_top ? 0 : m_dimensions.line;
|
||
m_window.move_cursor(status_line_pos);
|
||
|
||
m_window.draw(m_palette, status_line.atoms(), default_face);
|
||
|
||
const auto mode_len = mode_line.length();
|
||
m_status_len = status_line.length();
|
||
const auto remaining = m_dimensions.column - m_status_len;
|
||
if (mode_len < remaining)
|
||
{
|
||
ColumnCount col = m_dimensions.column - mode_len;
|
||
m_window.move_cursor({status_line_pos, col});
|
||
m_window.draw(m_palette, mode_line.atoms(), 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;
|
||
m_window.move_cursor({status_line_pos, col});
|
||
m_window.draw(m_palette, trimmed_mode_line.atoms(), 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);
|
||
if (fd < 0)
|
||
return;
|
||
auto close_fd = on_scope_end([fd]{ ::close(fd); });
|
||
|
||
winsize ws;
|
||
if (::ioctl(fd, TIOCGWINSZ, &ws) != 0)
|
||
return;
|
||
|
||
const bool info = (bool)m_info;
|
||
const bool menu = (bool)m_menu;
|
||
if (m_window) m_window.destroy();
|
||
if (info) m_info.destroy();
|
||
if (menu) m_menu.destroy();
|
||
|
||
resize_term(ws.ws_row, ws.ws_col);
|
||
|
||
m_window.create({0, 0}, {ws.ws_row, ws.ws_col});
|
||
kak_assert(m_window);
|
||
|
||
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)
|
||
menu_show(Vector<DisplayLine>(std::move(m_menu.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);
|
||
|
||
set_resize_pending();
|
||
wclear(curscr);
|
||
}
|
||
|
||
Optional<Key> NCursesUI::get_next_key()
|
||
{
|
||
if (stdin_closed)
|
||
{
|
||
set_signal_handler(SIGWINCH, SIG_DFL);
|
||
set_signal_handler(SIGHUP, SIG_DFL);
|
||
if (m_window)
|
||
m_window.destroy();
|
||
m_stdin_watcher.disable();
|
||
return {};
|
||
}
|
||
|
||
check_resize();
|
||
|
||
if (m_resize_pending)
|
||
{
|
||
m_resize_pending = false;
|
||
return resize(dimensions());
|
||
}
|
||
|
||
static auto get_char = []() -> Optional<unsigned char> {
|
||
if (not fd_readable(STDIN_FILENO))
|
||
return {};
|
||
|
||
if (unsigned char c = 0; read(STDIN_FILENO, &c, 1) == 1)
|
||
return c;
|
||
|
||
stdin_closed = 1;
|
||
return {};
|
||
};
|
||
|
||
const auto c = get_char();
|
||
if (not c)
|
||
return {};
|
||
|
||
const cc_t erase = m_original_termios.c_cc[VERASE];
|
||
auto convert = [erase](Codepoint c) -> Codepoint {
|
||
if (c == control('m') or c == control('j'))
|
||
return Key::Return;
|
||
if (c == control('i'))
|
||
return Key::Tab;
|
||
if (c == erase)
|
||
return Key::Backspace;
|
||
if (c == 127) // when it's not backspace
|
||
return Key::Delete;
|
||
if (c == 27)
|
||
return Key::Escape;
|
||
return c;
|
||
};
|
||
auto parse_key = [&convert](unsigned char c) -> Key {
|
||
if (Codepoint cp = convert(c); cp > 255)
|
||
return Key{cp};
|
||
// 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)
|
||
return ctrl(' ');
|
||
// Represent Ctrl-letter combinations in lower-case, to be clear
|
||
// that Shift is not involved.
|
||
if (c < 27)
|
||
return ctrl(c - 1 + 'a');
|
||
// 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');
|
||
|
||
struct Sentinel{};
|
||
struct CharIterator
|
||
{
|
||
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; }
|
||
Optional<unsigned char> c;
|
||
};
|
||
return Key{utf8::codepoint(CharIterator{c}, Sentinel{})};
|
||
};
|
||
|
||
auto parse_mask = [](int mask) {
|
||
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;
|
||
};
|
||
|
||
auto parse_csi = [this, &convert, &parse_mask]() -> Optional<Key> {
|
||
auto next_char = [] { return get_char().value_or((unsigned char)0xff); };
|
||
int params[16] = {};
|
||
auto c = next_char();
|
||
char private_mode = 0;
|
||
if (c == '?' or c == '<' or c == '=' or c == '>')
|
||
{
|
||
private_mode = c;
|
||
c = next_char();
|
||
}
|
||
for (int count = 0; count < 16 and c >= 0x30 && c <= 0x3f; c = next_char())
|
||
{
|
||
if (isdigit(c))
|
||
params[count] = params[count] * 10 + c - '0';
|
||
else if (c == ';')
|
||
++count;
|
||
else
|
||
return {};
|
||
}
|
||
if (c != '$' and (c < 0x40 or c > 0x7e))
|
||
return {};
|
||
|
||
auto mouse_button = [this](Key::Modifiers mod, Key::MouseButton button, Codepoint coord, bool release) {
|
||
auto mask = 1 << (int)button;
|
||
if (not release)
|
||
{
|
||
mod |= (m_mouse_state & mask) ? Key::Modifiers::MousePos : Key::Modifiers::MousePress;
|
||
m_mouse_state |= mask;
|
||
}
|
||
else
|
||
{
|
||
mod |= Key::Modifiers::MouseRelease;
|
||
m_mouse_state &= ~mask;
|
||
}
|
||
return Key{mod | Key::to_modifier(button), coord};
|
||
};
|
||
|
||
auto mouse_scroll = [this](Key::Modifiers mod, bool down) -> Key {
|
||
return {mod | Key::Modifiers::Scroll,
|
||
(Codepoint)((down ? 1 : -1) * m_wheel_scroll_amount)};
|
||
};
|
||
|
||
auto masked_key = [&](Codepoint key) {
|
||
int mask = std::max(params[1] - 1, 0);
|
||
Key::Modifiers modifiers = parse_mask(mask);
|
||
if (is_basic_alpha(key) and (modifiers & Key::Modifiers::Shift))
|
||
{
|
||
modifiers &= ~Key::Modifiers::Shift;
|
||
key = to_upper(key);
|
||
}
|
||
return Key{modifiers, key};
|
||
};
|
||
|
||
switch (c)
|
||
{
|
||
case '$':
|
||
switch (params[0])
|
||
{
|
||
case 23: case 24:
|
||
return Key{Key::Modifiers::Shift, Key::F11 + params[0] - 23}; // rxvt style
|
||
}
|
||
return {};
|
||
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);
|
||
case 'E': return masked_key('5'); // Numeric keypad 5
|
||
case 'F': return masked_key(Key::End); // PC/xterm style
|
||
case 'H': return masked_key(Key::Home); // PC/xterm style
|
||
case 'P': return masked_key(Key::F1);
|
||
case 'Q': return masked_key(Key::F2);
|
||
case 'R': return masked_key(Key::F3);
|
||
case 'S': return masked_key(Key::F4);
|
||
case '~':
|
||
switch (params[0])
|
||
{
|
||
case 1: return masked_key(Key::Home); // VT220/tmux style
|
||
case 2: return masked_key(Key::Insert);
|
||
case 3: return masked_key(Key::Delete);
|
||
case 4: return masked_key(Key::End); // VT220/tmux style
|
||
case 5: return masked_key(Key::PageUp);
|
||
case 6: return masked_key(Key::PageDown);
|
||
case 7: return masked_key(Key::Home); // rxvt style
|
||
case 8: return masked_key(Key::End); // rxvt style
|
||
case 11: case 12: case 13: case 14: case 15:
|
||
return masked_key(Key::F1 + params[0] - 11);
|
||
case 17: case 18: case 19: case 20: case 21:
|
||
return masked_key(Key::F6 + params[0] - 17);
|
||
case 23: case 24:
|
||
return masked_key(Key::F11 + params[0] - 23);
|
||
case 25: case 26:
|
||
return Key{Key::Modifiers::Shift, Key::F3 + params[0] - 25}; // rxvt style
|
||
case 28: case 29:
|
||
return Key{Key::Modifiers::Shift, Key::F5 + params[0] - 28}; // rxvt style
|
||
case 31: case 32:
|
||
return Key{Key::Modifiers::Shift, Key::F7 + params[0] - 31}; // rxvt style
|
||
case 33: case 34:
|
||
return Key{Key::Modifiers::Shift, Key::F9 + params[0] - 33}; // rxvt style
|
||
}
|
||
return {};
|
||
case 'u':
|
||
return masked_key(convert(static_cast<Codepoint>(params[0])));
|
||
case 'Z': return shift(Key::Tab);
|
||
case 'I': return {Key::FocusIn};
|
||
case 'O': return {Key::FocusOut};
|
||
case 'M': case 'm':
|
||
const bool sgr = private_mode == '<';
|
||
if (not sgr and c != 'M')
|
||
return {};
|
||
|
||
const Codepoint b = sgr ? params[0] : next_char() - 32;
|
||
const int x = (sgr ? params[1] : next_char() - 32) - 1;
|
||
const int y = (sgr ? params[2] : next_char() - 32) - 1;
|
||
auto coord = encode_coord({y - content_line_offset(), x});
|
||
Key::Modifiers mod = parse_mask((b >> 2) & 0x7);
|
||
switch (const int code = b & 0x43; code)
|
||
{
|
||
case 0: case 1: case 2:
|
||
return mouse_button(mod, Key::MouseButton{code}, coord, c == 'm');
|
||
case 3:
|
||
if (sgr)
|
||
return {};
|
||
else if (int guess = ffs(m_mouse_state) - 1; 0 <= guess and guess < 3)
|
||
return mouse_button(mod, Key::MouseButton{guess}, coord, true);
|
||
break;
|
||
case 64: return mouse_scroll(mod, false);
|
||
case 65: return mouse_scroll(mod, true);
|
||
}
|
||
return Key{Key::Modifiers::MousePos, coord};
|
||
}
|
||
return {};
|
||
};
|
||
|
||
auto parse_ss3 = [&parse_mask]() -> Optional<Key> {
|
||
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');
|
||
Key::Modifiers mod = parse_mask(std::max(raw_mask - 1, 0));
|
||
|
||
switch (code)
|
||
{
|
||
case ' ': return Key{mod, ' '};
|
||
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'};
|
||
default: return {};
|
||
}
|
||
};
|
||
|
||
if (*c != 27)
|
||
return parse_key(*c);
|
||
|
||
if (auto next = get_char())
|
||
{
|
||
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));
|
||
}
|
||
return Key{Key::Escape};
|
||
}
|
||
|
||
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 int item_count = (int)m_menu.items.size();
|
||
if (m_menu.columns == 0)
|
||
{
|
||
const auto win_width = m_menu.size.column - 4;
|
||
kak_assert(m_menu.size.line == 1);
|
||
ColumnCount pos = 0;
|
||
|
||
m_menu.move_cursor({0, 0});
|
||
m_menu.draw(m_palette, DisplayAtom(m_menu.first_item > 0 ? "< " : " "), m_menu.bg);
|
||
|
||
int i = m_menu.first_item;
|
||
for (; i < item_count and pos < win_width; ++i)
|
||
{
|
||
const DisplayLine& item = m_menu.items[i];
|
||
const ColumnCount item_width = item.length();
|
||
auto& face = i == m_menu.selected_item ? m_menu.fg : m_menu.bg;
|
||
m_menu.draw(m_palette, item.atoms(), face);
|
||
if (pos + item_width < win_width)
|
||
m_menu.draw(m_palette, DisplayAtom(" "), m_menu.bg);
|
||
else
|
||
{
|
||
m_menu.move_cursor({0, win_width+2});
|
||
m_menu.draw(m_palette, DisplayAtom("…"), m_menu.bg);
|
||
}
|
||
pos += item_width + 1;
|
||
}
|
||
|
||
m_menu.move_cursor({0, win_width+3});
|
||
m_menu.draw(m_palette, DisplayAtom(i == item_count ? " " : ">"), m_menu.bg);
|
||
|
||
m_dirty = true;
|
||
return;
|
||
}
|
||
|
||
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 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 LineCount mark_line = (win_height - mark_height) * first_col / max(1, menu_cols - m_menu.columns);
|
||
|
||
for (auto line = 0_line; line < win_height; ++line)
|
||
{
|
||
for (int col = 0; col < m_menu.columns; ++col)
|
||
{
|
||
m_menu.move_cursor({line, col * column_width});
|
||
int item_idx = (first_col + col) * (int)m_menu.size.line + (int)line;
|
||
auto& face = item_idx < item_count and item_idx == m_menu.selected_item ? m_menu.fg : m_menu.bg;
|
||
auto atoms = item_idx < item_count ? m_menu.items[item_idx].atoms() : ConstArrayView<DisplayAtom>{};
|
||
m_menu.draw(m_palette, atoms, face);
|
||
}
|
||
const bool is_mark = line >= mark_line and line < mark_line + mark_height;
|
||
m_menu.move_cursor({line, m_menu.size.column - 1});
|
||
m_menu.draw(m_palette, DisplayAtom(is_mark ? "█" : "░"), m_menu.bg);
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void NCursesUI::menu_show(ConstArrayView<DisplayLine> items,
|
||
DisplayCoord anchor, Face fg, Face bg,
|
||
MenuStyle style)
|
||
{
|
||
if (m_menu)
|
||
{
|
||
m_menu.destroy();
|
||
m_dirty = true;
|
||
}
|
||
|
||
m_menu.fg = fg;
|
||
m_menu.bg = bg;
|
||
m_menu.style = style;
|
||
m_menu.anchor = anchor;
|
||
|
||
if (m_dimensions.column <= 2)
|
||
return;
|
||
|
||
const int item_count = items.size();
|
||
m_menu.items.clear(); // make sure it is empty
|
||
m_menu.items.reserve(item_count);
|
||
const auto longest = accumulate(items | transform(&DisplayLine::length),
|
||
1_col, [](auto&& lhs, auto&& rhs) { return std::max(lhs, rhs); });
|
||
|
||
const ColumnCount max_width = m_dimensions.column - 1;
|
||
const bool is_inline = style == MenuStyle::Inline;
|
||
const bool is_search = style == MenuStyle::Search;
|
||
m_menu.columns = is_search ? 0 : (is_inline ? 1 : max((int)(max_width / (longest+1)), 1));
|
||
|
||
const LineCount max_height = min(height_limit(style), max(anchor.line, m_dimensions.line - anchor.line - 1));
|
||
const LineCount height = is_search ?
|
||
1 : (min<LineCount>(max_height, div_round_up(item_count, m_menu.columns)));
|
||
|
||
const ColumnCount maxlen = (m_menu.columns > 1 and item_count > 1) ?
|
||
max_width / m_menu.columns - 1 : max_width;
|
||
|
||
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);
|
||
}
|
||
|
||
if (is_inline)
|
||
anchor.line += content_line_offset();
|
||
|
||
LineCount line = anchor.line + 1;
|
||
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)
|
||
line = m_status_on_top ? 1_line : m_dimensions.line - height;
|
||
else if (line + height > m_dimensions.line)
|
||
line = anchor.line - height;
|
||
|
||
const auto width = is_search ? m_dimensions.column - m_dimensions.column / 2
|
||
: (is_inline ? min(longest+1, m_dimensions.column)
|
||
: m_dimensions.column);
|
||
m_menu.create({line, column}, {height, width});
|
||
m_menu.selected_item = item_count;
|
||
m_menu.first_item = 0;
|
||
|
||
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();
|
||
if (selected < 0 or selected >= item_count)
|
||
{
|
||
m_menu.selected_item = -1;
|
||
m_menu.first_item = 0;
|
||
}
|
||
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;
|
||
}
|
||
else
|
||
{
|
||
m_menu.selected_item = selected;
|
||
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;
|
||
}
|
||
draw_menu();
|
||
}
|
||
|
||
void NCursesUI::menu_hide()
|
||
{
|
||
if (not m_menu)
|
||
return;
|
||
|
||
m_menu.items.clear();
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void NCursesUI::info_show(const DisplayLine& title, const DisplayLineList& content,
|
||
DisplayCoord anchor, Face face, InfoStyle style)
|
||
{
|
||
info_hide();
|
||
|
||
m_info.title = title;
|
||
m_info.content = content;
|
||
m_info.anchor = anchor;
|
||
m_info.face = face;
|
||
m_info.style = style;
|
||
|
||
const bool framed = style == InfoStyle::Prompt or style == InfoStyle::Modal;
|
||
const bool assisted = style == InfoStyle::Prompt and m_assistant.size() != 0;
|
||
|
||
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;
|
||
|
||
const auto max_content_width = max_size.column - (framed ? 4 : 2) - (assisted ? m_assistant[0].column_length() : 0);
|
||
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))};
|
||
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)};
|
||
|
||
if ((framed and size.line < 3) or size.line <= 0)
|
||
return;
|
||
|
||
const Rect rect = {content_line_offset(), m_dimensions};
|
||
if (style == InfoStyle::Prompt)
|
||
{
|
||
anchor = DisplayCoord{m_status_on_top ? 0 : m_dimensions.line, m_dimensions.column-1};
|
||
anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
|
||
}
|
||
else if (style == InfoStyle::Modal)
|
||
{
|
||
auto half = [](const DisplayCoord& c) { return DisplayCoord{c.line / 2, c.column / 2}; };
|
||
anchor = rect.pos + half(rect.size) - half(size);
|
||
}
|
||
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;
|
||
if (size.column <= right_max_width or right_max_width >= left_max_width)
|
||
anchor.column = m_menu.pos.column + m_menu.size.column;
|
||
else
|
||
anchor.column = m_menu.pos.column - size.column;
|
||
}
|
||
else
|
||
{
|
||
anchor = compute_pos(anchor, size, rect, m_menu, style == InfoStyle::InlineAbove);
|
||
anchor.line += content_line_offset();
|
||
}
|
||
|
||
m_info.create(anchor, size);
|
||
auto draw_atoms = [&](auto&&... args) {
|
||
auto draw = overload(
|
||
[&](String str) { m_info.draw(m_palette, DisplayAtom{std::move(str)}, face); },
|
||
[&](const DisplayLine& atoms) { m_info.draw(m_palette, atoms.atoms(), face); });
|
||
|
||
(draw(args), ...);
|
||
};
|
||
|
||
for (auto line = 0_line; line < size.line; ++line)
|
||
{
|
||
constexpr Codepoint dash{L'─'};
|
||
constexpr Codepoint dotted_dash{L'┄'};
|
||
m_info.move_cursor(line);
|
||
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)
|
||
draw_atoms(lines[(int)line]);
|
||
else if (line == 0)
|
||
{
|
||
if (title.atoms().empty() or content_size.column < 2)
|
||
draw_atoms("╭─" + String{dash, content_size.column} + "─╮");
|
||
else
|
||
{
|
||
auto trimmed_title = title;
|
||
trimmed_title.trim(0, content_size.column - 2);
|
||
auto dash_count = content_size.column - trimmed_title.length() - 2;
|
||
String left{dash, dash_count / 2};
|
||
String right{dash, dash_count - dash_count / 2};
|
||
draw_atoms("╭─" + left + "┤", trimmed_title, "├" + right +"─╮");
|
||
}
|
||
}
|
||
else if (line < size.line - 1 and line <= lines.size())
|
||
{
|
||
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();
|
||
draw_atoms("│ ", info_line, String{' ', padding} + (trimmed ? "…│" : " │"));
|
||
}
|
||
else if (line == std::min<LineCount>((int)lines.size() + 1, size.line - 1))
|
||
draw_atoms("╰─" + String(line > lines.size() ? dash : dotted_dash, content_size.column) + "─╯");
|
||
}
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::info_hide()
|
||
{
|
||
if (not m_info)
|
||
return;
|
||
m_info.destroy();
|
||
m_dirty = true;
|
||
}
|
||
|
||
void NCursesUI::set_on_key(OnKeyCallback callback)
|
||
{
|
||
m_on_key = std::move(callback);
|
||
EventManager::instance().force_signal(0);
|
||
}
|
||
|
||
DisplayCoord NCursesUI::dimensions()
|
||
{
|
||
return m_dimensions;
|
||
}
|
||
|
||
LineCount NCursesUI::content_line_offset() const
|
||
{
|
||
return m_status_on_top ? 1 : 0;
|
||
}
|
||
|
||
void NCursesUI::set_resize_pending()
|
||
{
|
||
m_resize_pending = true;
|
||
EventManager::instance().force_signal(0);
|
||
}
|
||
|
||
void NCursesUI::abort()
|
||
{
|
||
endwin();
|
||
}
|
||
|
||
void NCursesUI::enable_mouse(bool enabled)
|
||
{
|
||
if (enabled == m_mouse_enabled)
|
||
return;
|
||
|
||
m_mouse_enabled = enabled;
|
||
if (enabled)
|
||
{
|
||
// force SGR mode
|
||
fputs("\033[?1006h", stdout);
|
||
// enable mouse
|
||
fputs("\033[?1000h", stdout);
|
||
// force enable report mouse position
|
||
fputs("\033[?1002h", stdout);
|
||
}
|
||
else
|
||
{
|
||
fputs("\033[?1002l", stdout);
|
||
fputs("\033[?1000l", stdout);
|
||
fputs("\033[?1006l", 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_shift_function_key"_sv);
|
||
m_shift_function_key = it != options.end() ?
|
||
str_to_int_ifp(it->value).value_or(default_shift_function_key)
|
||
: default_shift_function_key;
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_change_colors"_sv);
|
||
if (m_palette.set_change_colors(it == options.end() or
|
||
(it->value == "yes" or it->value == "true")))
|
||
{
|
||
m_window.m_active_pair = -1;
|
||
m_menu.m_active_pair = -1;
|
||
m_info.m_active_pair = -1;
|
||
}
|
||
}
|
||
|
||
{
|
||
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;
|
||
|
||
auto wheel_scroll_amount_it = options.find("ncurses_wheel_scroll_amount"_sv);
|
||
m_wheel_scroll_amount = wheel_scroll_amount_it != options.end() ?
|
||
str_to_int_ifp(wheel_scroll_amount_it->value).value_or(3) : 3;
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_padding_char"_sv);
|
||
if (it == options.end())
|
||
// Defaults to tilde.
|
||
m_padding_char = DisplayAtom("~");
|
||
else if (it->value.column_length() < 1)
|
||
// Do not allow empty string, use space instead.
|
||
m_padding_char = DisplayAtom(" ");
|
||
else
|
||
m_padding_char = DisplayAtom(it->value);
|
||
}
|
||
|
||
{
|
||
auto it = options.find("ncurses_padding_fill"_sv);
|
||
m_padding_fill = it != options.end() and
|
||
(it->value == "yes" or it->value == "true");
|
||
}
|
||
}
|
||
|
||
}
|