#include "tui.hpp" #include #include #include #include #include #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 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 runlevel_options; static array 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,,,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 &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 * 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(); }