rc-tui/source/tui.cpp
2024-01-30 23:05:41 +01:00

519 lines
12 KiB
C++

#include "tui.hpp"
#include <time.h>
#include <algorithm>
#include <ncurses.h>
#include <menu.h>
#include <readline/readline.h>
#include "Service.hpp"
#include "search.hpp"
#include "config/colors.h"
#ifndef ESC
# define ESC '\033'
#endif
#ifndef ENTER
# define ENTER '\r'
#endif
#define TUI_BANNER "| Open-rc TUI |"
extern bool is_root;
template<typename T>
struct array {
T* elements;
int size;
array() {
}
array(int n) : size(n) {
elements = new T[n];
}
~array() {
delete[] this->elements;
}
T& operator[](int i) {
return elements[i];
}
};
static bool tui_running;
enum {
CUR_RUNLEVEL,
CUR_STATUS,
CUR_ENUM_END,
};
static size_t cursor = CUR_STATUS;
static size_t selection = 0;
enum {
STATE_INITIAL,
STATE_SEARCH,
STATE_CMD_MENU,
};
static size_t state = STATE_INITIAL;
static WINDOW * wmain;
static WINDOW * wmaind;
static WINDOW * wmenu;
static WINDOW * wmenud;
static WINDOW * whelpbar;
static WINDOW * wsearchbar;
static MENU * cmd_menu;
static array<ITEM*> runlevel_options;
static array<ITEM*> status_options[2];
static menu_callback_t menu_callback;
static inline size_t windoww(WINDOW* w){
return (w->_maxx+1 - w->_begx)+1;
}
static inline size_t windowh(WINDOW* w){
return (w->_maxy+1 - w->_begy+w->_yoffset)+1;
}
static size_t top = 0;
static int input_available = false;
static char input;
inline int option_list_to_items(ITEM** &items, const char * const * const &options) {
int n = 0;
while(options[n] != NULL) {
++n;
}
items = (ITEM**)malloc((n+1) * sizeof(ITEM*));
for (int i = 0; i < n; i++) {
items[i] = new_item(options[i], "");
}
items[n] = NULL;
return n;
}
bool tui_init(){
// Prerequizet info
for(auto i : services){
if(i->name.size() > SERVICE_MAX_NAME_LEN){
SERVICE_MAX_NAME_LEN = i->name.size();
}
}
runlevel_options.size = option_list_to_items(runlevel_options.elements, Service::runlevels);
status_options[0].size = option_list_to_items(status_options[0].elements, Service::cmd[0]);
status_options[1].size = option_list_to_items(status_options[1].elements, Service::cmd[1]);
// NCurses
initscr();
nonl();
noecho();
curs_set(0);
start_color();
easy_init_pair(STD);
easy_init_pair(SELECTION);
easy_init_pair(CURSOR);
easy_init_pair(HELP);
easy_init_pair(WARNING);
easy_init_pair(ERROR);
wmain = newwin(LINES-2, COLS, 0, 0);
wmaind = derwin(wmain, wmain->_maxy-1, wmain->_maxx-1, 1, 1);
whelpbar = newwin(1, COLS, LINES-1, 0);
wsearchbar = newwin(1, COLS, LINES-2, 0);
refresh();
wbkgd(wmain, COLOR_PAIR(COLOR_PAIR_STD));
wbkgd(wmaind, COLOR_PAIR(COLOR_PAIR_STD));
wbkgd(whelpbar, COLOR_PAIR(COLOR_PAIR_STD));
wbkgd(wsearchbar, COLOR_PAIR(COLOR_PAIR_STD));
// ReadLine
rl_bind_key('\t', rl_insert); // make tab insert itself
rl_catch_signals = 0; // do not install signal handlers
rl_catch_sigwinch = 0; // do not care about window change signals
rl_prep_term_function = NULL; // do not initialize the ternimal
rl_deprep_term_function = NULL; // do not clean up
rl_change_environment = 0; // ?!
rl_getc_function = +[]([[ maybe_unused ]] FILE* ignore){
input_available = false;
return (int)input;
};
rl_input_available_hook = +[]{
return input_available;
};
rl_redisplay_function = +[]{
wmove(wsearchbar, 0, 1);
wclrtoeol(wsearchbar);
waddstr(wsearchbar, rl_line_buffer);
wrefresh(wsearchbar);
return;
};
rl_callback_handler_install("", +[]([[ maybe_unused ]] char *line){
return;
});
tui_running = true;
return true;
}
void tui_quit(){
if(not tui_running){ return; }
delwin(wsearchbar);
delwin(whelpbar);
delwin(wmenud);
delwin(wmenu);
delwin(wmaind);
delwin(wmain);
endwin();
tui_running = false;
}
static char* tui_render_service(const Service* const s, const size_t &width){
char* r;
static const char l[] = " Locked";
auto l_spaceout = []() constexpr {
char* const r = (char*)malloc(sizeof(l));
memset(r, ' ', sizeof(l)-1);
r[sizeof(l)-1] = '\00';
return r;
};
const char* const lstr = (s->locked ? l : l_spaceout());
int err = asprintf(
&r,
"%-*s%s%*s%*s%*s", /* name,<locked>,<space_padding>,runlevel,status */
(int)SERVICE_MAX_NAME_LEN,
s->name.c_str(),
lstr,
(int)(width-
(SERVICE_MAX_NAME_LEN
+ (sizeof(l)-1)
+ SERVICE_MAX_RUNLEVEL_LEN
+ SERVICE_MAX_STATUS_LEN+1)
),
" ",
SERVICE_MAX_RUNLEVEL_LEN,
s->runlevel.c_str(),
SERVICE_MAX_STATUS_LEN+1,
s->status.c_str()
);
if (err == -1) {
abort();
}
return r;
}
static size_t tui_rendered_service_button_pos(const Service* const s, size_t width){
size_t starts[] = {
width-(s->status.size()+1 + s->runlevel.size()+1),
width - (s->status.size())
};
return starts[cursor];
}
static void tui_render_services(){
wmove(wmaind, 0, 0);
int winw = windoww(wmaind);
const size_t t = std::min(service_results.size() - top, (size_t)windowh(wmaind));
for(size_t i = top; i < (top + t); i++){
char* buf = tui_render_service(service_results[i], winw);
if(i == selection) [[ unlikely ]] {
size_t cur_start,
cur_len;
cur_start = tui_rendered_service_button_pos(service_results[i], winw);
switch(cursor){
case CUR_RUNLEVEL:
cur_len = service_results[i]->runlevel.size();
break;
case CUR_STATUS:
cur_len = service_results[i]->status.size();
break;
}
// Print until cursor
wattron(wmaind, A_BOLD | COLOR_PAIR(COLOR_PAIR_SELECTION));
waddnstr(wmaind, buf, cur_start);
wattroff(wmaind, COLOR_PAIR(COLOR_PAIR_SELECTION));
// Print cursor
wattron(wmaind, COLOR_PAIR(COLOR_PAIR_CURSOR));
waddnstr(wmaind, buf+cur_start, cur_len);
wattroff(wmaind, COLOR_PAIR(COLOR_PAIR_CURSOR));
// Print remainder
wattron(wmaind, COLOR_PAIR(COLOR_PAIR_SELECTION));
waddstr(wmaind, buf+cur_start+cur_len);
wattroff(wmaind, A_BOLD | COLOR_PAIR(COLOR_PAIR_SELECTION));
}else [[ likely ]] {
waddstr(wmaind, buf);
}
delete buf;
}
if ((service_results.size() - top) < windowh(wmaind)) {
wclrtobot(wmaind);
}
}
static const char NOT_ROOT_MSG[] = " WARNING: NOT RUNNING AS ROOT! ";
static const char * const * const HELP_MSG[] = {
[STATE_INITIAL] = (char const *[]){"Down [j]", "Up [k]", "Next [h]", "Previous [l]", "Modify [\\n]", "Search [/]", "Quit [q]", NULL},
[STATE_SEARCH] = (char const *[]){"Browse [\\n]", "Cancel [ESC]", NULL},
[STATE_CMD_MENU] = (char const *[]){"Down [j]", "Up [k]", "Select [\\n]", "Cancel [ESC]", NULL},
};
static void tui_render_help(){
werase(whelpbar);
for(int i = 0; HELP_MSG[state][i] != NULL; i++){
waddch(whelpbar, ' ');
wattron(whelpbar, COLOR_PAIR(COLOR_PAIR_HELP));
waddstr(whelpbar, HELP_MSG[state][i]);
wattroff(whelpbar, COLOR_PAIR(COLOR_PAIR_HELP));
}
if (not is_root) {
wattron(whelpbar, COLOR_PAIR(COLOR_PAIR_WARNING));
mvwaddstr(whelpbar, 0, COLS - sizeof(NOT_ROOT_MSG), NOT_ROOT_MSG);
wattroff(whelpbar, COLOR_PAIR(COLOR_PAIR_WARNING));
}
}
static inline void make_menu(const int w, int y, const int x, const int flip_above, array<ITEM*> &items, menu_callback_t callback) {
menu_callback = callback;
if (flip_above < items.size+2) {
y = y - (items.size+2) - 1;
}
wmenu = newwin(items.size+2, w, y, x);
wmenud = derwin(wmenu, items.size, w-2, 1, 1);
wbkgd(wmenu, COLOR_PAIR(COLOR_PAIR_STD));
wbkgd(wmenud, COLOR_PAIR(COLOR_PAIR_STD));
cmd_menu = new_menu(items.elements);
/* ITEM stores MENU information internally such as its position in the MENU;
* new_menu() will fail if it detects that the ITEMs passed in were already assigned;
* however, in the process it happens to reset the ITEMs;
* i assume this means the check is actually useless since it breaks the previous owner
* and does not create a new; someone should check
* yet, since it resets, it means a second call will succeed;
* we could statically alloc MENU, but we dont want to perserve cursor location
* and who knows what, so we dont
*/
if (not cmd_menu
&& errno == E_NOT_CONNECTED) {
cmd_menu = new_menu(items.elements);
}
set_menu_win(cmd_menu, wmenu);
set_menu_sub(cmd_menu, wmenud);
post_menu(cmd_menu);
}
static inline bool tui_control_search(const char &c){
switch(c) {
case ESC: {
state = STATE_INITIAL;
rl_end = 0;
rl_point = 0;
wmove(wsearchbar, 0, 0);
wclrtoeol(wsearchbar);
wrefresh(wsearchbar);
service_results.resize(services.size());
copy(services.begin(), services.end(), service_results.begin());
} return true;
case ENTER: {
state = STATE_INITIAL;
} return true;
default: {
input = c;
input_available = true;
int cache = rl_end;
rl_callback_read_char();
if (cache > rl_end) { // erasing occured
service_results.resize(services.size());
copy(services.begin(), services.end(), service_results.begin());
}
search(service_results, rl_line_buffer);
if (selection > service_results.size()-1) {
selection = service_results.size()-1;
}
} return true;
}
return false;
}
static inline bool tui_control_menu(const char &c){
switch(c) {
case 'j':
menu_driver(cmd_menu, REQ_DOWN_ITEM);
return true;
case 'k':
menu_driver(cmd_menu, REQ_UP_ITEM);
return true;
case ESC:
state = STATE_INITIAL;
delwin(wmenu);
tui_redraw();
return true;
case ENTER:
(services[selection]->*menu_callback)(item_index(current_item(cmd_menu)));
state = STATE_INITIAL;
free_menu(cmd_menu);
delwin(wmenud);
delwin(wmenu);
tui_redraw();
return true;
}
return false;
}
static inline bool tui_control_root(const char &c) {
switch(c){
case 'j':
if (selection < services.size()-1) {
++selection;
if (selection >= (top + windowh(wmaind))) {
++top;
}
}else{
selection = 0;
top = 0;
}
return true;
case 'k':
if (selection > 0) {
if (selection == top) {
--top;
}
--selection;
} else {
if (top > 0) {
--top;
} else {
selection = services.size()-1;
top = services.size() - windowh(wmaind);
}
}
return true;
case 'h':
if (cursor != 0) {
--cursor;
} else {
cursor = CUR_ENUM_END-1;
}
return true;
case 'l':
++cursor;
if (cursor == CUR_ENUM_END) {
cursor = 0;
}
return true;
case 'q':
abort();
case '/':
case '?':
state = STATE_SEARCH;
mvwaddch(wsearchbar, 0, 0, '/');
return true;
case '\r':
if (not is_root) {
wattron(whelpbar, COLOR_PAIR(COLOR_PAIR_ERROR));
mvwaddstr(whelpbar, 0, COLS - sizeof(NOT_ROOT_MSG), NOT_ROOT_MSG);
wattroff(whelpbar, COLOR_PAIR(COLOR_PAIR_ERROR));
wrefresh(whelpbar);
refresh();
auto tmp = (const struct timespec){.tv_sec = 0, .tv_nsec = 100'000'000};
nanosleep(&tmp, NULL);
flushinp();
return true;
}
state = STATE_CMD_MENU;
array<ITEM*> * options;
if (cursor == CUR_RUNLEVEL) {
options = &runlevel_options;
} else if (cursor == CUR_STATUS) {
options = &status_options[services[selection]->status != "[started]"];
}
const int mstartx = tui_rendered_service_button_pos(services[selection], windoww(wmaind));
const int mstarty = wmaind->_begy + selection+1;
const int mwidth = (
cursor
?
services[selection]->status
:
services[selection]->runlevel
).size() + 2;
const int lines_avail = windowh(wmaind) - (selection - top);
make_menu(mwidth, mstarty, mstartx, lines_avail, *options, &Service::change_status);
return true;
}
return false;
}
bool tui_control(const int &c){
if (c == KEY_RESIZE) {
tui_resize();
flushinp();
return true;
}
switch(state){
case STATE_INITIAL:
return tui_control_root(c);
case STATE_SEARCH:
return tui_control_search(c);
case STATE_CMD_MENU:
return tui_control_menu(c);
}
return false;
}
void tui_redraw(){
box(wmain, 0, 0);
mvwaddstr(wmain, 0, (COLS-(sizeof(TUI_BANNER)-1))/2, TUI_BANNER);
wrefresh(wmain);
tui_draw();
}
void tui_draw(){
tui_render_services();
wrefresh(wmaind);
tui_render_help();
wrefresh(whelpbar);
if(state == STATE_CMD_MENU){
wborder(wmenu, '|', '|', '=', '=', 'O', 'O', 'O', 'O');
wrefresh(wmenu);
wrefresh(wmenud);
}
if (state == STATE_SEARCH) {
wrefresh(wsearchbar);
}
refresh();
}
inline void tui_resize() {
tui_quit();
tui_init();
tui_redraw();
}