/* * ui.c - hlogin(1) user interface functions * * Copyleft (C) 2022 ~keith * * This file is part of hlogin. * * hlogin is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, version 3. * * hlogin is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License * along with hlogin. If not, see . */ #include "config.h" #include "ui.h" #include #include #include #include #include #include "gettext.h" #include #include #include #include #include #include int term_rows, term_cols; WINDOW *back_w; PANEL *back_p; WINDOW *login_w; PANEL *login_p; WINDOW *login_shadow_w; PANEL *login_shadow_p; WINDOW *error_w; PANEL *error_p; WINDOW *error_shadow_w; PANEL *error_shadow_p; const int login_rows = 8; const int login_cols = 48; int error_rows, error_cols; int error_curx; char *input_buf; size_t input_buflen; int input_pos; int input_scroll; char *msg; bool hide_input; int btn_focus = 0; int btn_count = 2; char *hostname; char *domain; char *unknown_str = "(unknown)"; char *xgethostname() { size_t len = HOST_NAME_MAX; char *buf = malloc(sizeof(char) * (len + 1)); if (!buf) return unknown_str; while (gethostname(buf, len) != 0) { if (errno != EINVAL && errno != ENAMETOOLONG) { free(buf); return unknown_str; } len *= 2; buf = realloc(buf, sizeof(char) * (len + 1)); if (!buf) return unknown_str; } buf[len] = '\0'; return buf; } char *xgetdomainname(char *host) { struct addrinfo hints, *info = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_CANONNAME; if ((getaddrinfo(host, NULL, &hints, &info) != 0) || !info || !info->ai_canonname) goto FAILURE; // skip the first part of the canonname (the hostname) char *domain = strchr(info->ai_canonname, '.'); if (!domain || (strlen(domain) <= 1)) goto FAILURE; domain++; // copy into a new buffer & return char *buf = malloc(sizeof(char) * (strlen(domain) + 1)); strcpy(buf, domain); freeaddrinfo(info); return buf; FAILURE: if (info) freeaddrinfo(info); return unknown_str; } void safe_free(void *ptr, size_t size) { memset(ptr, (int)'\0', size); free(ptr); } void *safe_realloc(void *ptr, size_t size, size_t new_size) { void *new_ptr = malloc(new_size); if (new_ptr == NULL) return NULL; memcpy(new_ptr, ptr, size); safe_free(ptr, size); return new_ptr; } /* char *xgetnisdomainname() { size_t len = HOST_NAME_MAX; char *buf = malloc(sizeof(char) * (len + 1)); if (!buf) return unknown_str; while (getdomainname(buf, len) != 0) { if (errno != EINVAL && errno != ENAMETOOLONG) { free(buf); return unknown_str; } len *= 2; buf = realloc(buf, sizeof(char) * (len + 1)); if (!buf) return unknown_str; } buf[len] = '\0'; return buf; } */ char erase_ch; void ui_setup_dialog(char *prompt, bool password) { memset(input_buf, (int)'\0', sizeof(char) * input_buflen); input_pos = 0; input_scroll = 0; btn_focus = 0; btn_count = 2; msg = prompt; hide_input = password; hide_panel(error_p); hide_panel(error_shadow_p); show_panel(login_shadow_p); top_panel(login_p); wclear(login_w); draw_outline(login_w, login_rows, login_cols, 0, 0, false); paint_login(); } void ui_setup_message(char *text) { btn_focus = 0; btn_count = 1; int tcols = login_cols - 4; int trows = 1; char **msg_lines = word_wrap(text, tcols, tcols / 2, &trows); if (trows == 1) tcols = strlen(msg_lines[0]); if (tcols < 16) tcols = 16; error_rows = trows + 4; error_cols = tcols + 4; hide_panel(login_shadow_p); wresize(error_w, error_rows, error_cols); wresize(error_shadow_w, error_rows, error_cols); replace_panel(error_shadow_p, error_shadow_w); show_panel(error_shadow_p); move_panel(error_shadow_p, ((term_rows - error_rows) / 2) + 1, ((term_cols - error_cols) / 2) + 1); replace_panel(error_p, error_w); show_panel(error_p); move_panel(error_p, (term_rows - error_rows) / 2, (term_cols - error_cols) / 2); wbkgd(error_shadow_w, ' ' | COLOR_PAIR(4)); wclear(error_shadow_w); touchwin(error_shadow_w); wclear(error_w); draw_outline(error_w, error_rows, error_cols, 0, 0, false); // draw text wattron(error_w, COLOR_PAIR(2)); for (int i = 0; i < trows; i++) { wmove(error_w, i + 1, 2); waddstr(error_w, msg_lines[i]); free(msg_lines[i]); } free(msg_lines); // draw button wmove(error_w, error_rows - 2, 2); wattron(error_w, A_REVERSE); wprintw(error_w, "< %s >", _("OK")); error_curx = error_w->_curx - 2; wattroff(error_w, A_REVERSE); } /** ui_init - initialize the UI subsystem */ void ui_init() { initscr(); noecho(); cbreak(); halfdelay(2); start_color(); getmaxyx(stdscr, term_rows, term_cols); clear(); erase_ch = erasechar(); // set up input buffer input_pos = 0; input_buflen = 256; input_buf = malloc(input_buflen * sizeof(char)); if (!input_buf) exit(EXIT_FAILURE); input_buf[0] = '\0'; // get hostname & domain hostname = xgethostname(); domain = xgetdomainname(hostname); // set up colours init_pair(1, COLOR_WHITE, COLOR_BLUE); init_pair(2, COLOR_BLACK, COLOR_WHITE); init_pair(3, COLOR_WHITE, COLOR_WHITE); init_pair(4, COLOR_BLACK, COLOR_BLACK); // set up background window back_w = newwin(term_rows, term_cols, 0, 0); back_p = new_panel(back_w); wattron(back_w, COLOR_PAIR(1)); wbkgd(back_w, ' ' | COLOR_PAIR(1)); wclear(back_w); paint_back(); // set up login shadow window login_shadow_w = newwin(login_rows, login_cols, ((term_rows - login_rows) / 2) + 1, ((term_cols - login_cols) / 2) + 1); login_shadow_p = new_panel(login_shadow_w); wbkgd(login_shadow_w, ' ' | COLOR_PAIR(4)); wclear(login_shadow_w); // set up login window login_w = newwin(login_rows, login_cols, (term_rows - login_rows) / 2, (term_cols - login_cols) / 2); login_p = new_panel(login_w); wattron(login_w, COLOR_PAIR(2)); wbkgd(login_w, ' ' | COLOR_PAIR(2)); wclear(login_w); draw_outline(login_w, login_rows, login_cols, 0, 0, false); paint_login(); // set up error shadow window error_shadow_w = newwin(2, 2, 1, 1); error_shadow_p = new_panel(error_shadow_w); wbkgd(error_shadow_w, ' ' | COLOR_PAIR(4)); wclear(error_shadow_w); hide_panel(error_shadow_p); // set up error window error_w = newwin(1, 1, 0, 0); error_p = new_panel(error_w); wbkgd(error_w, ' ' | COLOR_PAIR(2)); wclear(error_w); hide_panel(error_p); // enable keypad mode keypad(login_w, true); keypad(error_w, true); } void ui_end() { safe_free(input_buf, input_buflen); del_panel(back_p); del_panel(login_p); del_panel(login_shadow_p); del_panel(error_p); del_panel(error_shadow_p); delwin(back_w); delwin(login_w); delwin(login_shadow_w); delwin(error_w); delwin(error_shadow_w); endwin(); } /** paint_back - redraw info displayed on background */ void paint_back() { wmove(back_w, 0, 0); wclrtoeol(back_w); wattron(back_w, A_BOLD); wprintw(back_w, _("Welcome to %s.%s"), hostname, domain); wattroff(back_w, A_BOLD); char time_buf[256]; time_t now = time(NULL); if (strftime(time_buf, 256, "%c", localtime(&now)) == 0) time_buf[0] = '\0'; wmove(back_w, 0, term_cols - strlen(time_buf)); wprintw(back_w, "%s", time_buf); } /** paint_login - redraw login dialog */ void paint_login() { // Field outline draw_outline(login_w, 3, login_cols - 4, 2, 2, true); // Field label wmove(login_w, 1, 2); wattron(login_w, A_BOLD | COLOR_PAIR(2)); waddstr(login_w, msg); wattroff(login_w, A_BOLD); // Field text wmove(login_w, 3, 3); whline(login_w, ' ', login_cols - 6); if (!hide_input) waddnstr(login_w, input_buf + input_scroll, login_cols - 6); else { int num_dots = strlen(input_buf) - input_scroll; if (num_dots > login_cols - 6) num_dots = login_cols - 6; whline(login_w, '*', num_dots); } // Buttons wmove(login_w, login_rows - 2, 2); if (btn_focus == 0) wattron(login_w, A_REVERSE); wprintw(login_w, "< %s >", _("OK")); wattroff(login_w, A_REVERSE); wmove(login_w, login_rows - 2, login_w->_curx + 2); if (btn_focus == 1) wattron(login_w, A_REVERSE); wprintw(login_w, "< %s >", _("Cancel")); wattroff(login_w, A_REVERSE); } int ui_update() { paint_back(); wmove(login_w, 3, 3 + input_pos - input_scroll); update_panels(); doupdate(); int ch = wgetch(login_w); if (ch == ERR) return -1; if (ch >= ' ' && ch < 0x7F) input_add_char(ch); else if (ch == 0x08 || ch == KEY_BACKSPACE || ch == erase_ch) input_backdel_char(); else if (ch == 0x7F || ch == KEY_DC) input_del_char(); else if (ch == KEY_LEFT && input_pos > 0) { input_pos--; if (input_pos < input_scroll) input_scroll--; } else if (ch == KEY_RIGHT && input_pos < strlen(input_buf)) { input_pos++; if (input_pos >= (input_scroll + login_cols - 6)) input_scroll++; } else if (ch == '\t') btn_focus = (btn_focus + 1) % btn_count; else if (ch == 0x1B) // escape return -2; else if (ch == KEY_ENTER || ch == '\r' || ch == '\n') return btn_focus; paint_login(); return -1; } int ui_error_update() { paint_back(); wmove(error_w, error_rows - 2, error_curx); update_panels(); doupdate(); int ch = wgetch(error_w); if (ch == ERR) return -1; if (ch == 0x1B) return -2; else if (ch == KEY_ENTER || ch == '\r' || ch == '\n') return btn_focus; return -1; } int ui_run() { int result; while ((result = ui_update()) == -1); btn_focus = -1; paint_back(); paint_login(); update_panels(); doupdate(); return result; } int ui_message_run() { int result; while ((result = ui_error_update()) == -1); btn_focus = -1; paint_back(); wmove(error_w, error_rows - 2, 2); wattron(error_w, COLOR_PAIR(2)); wprintw(error_w, "< %s >", _("OK")); wmove(error_w, error_rows - 2, error_curx); update_panels(); doupdate(); hide_panel(error_p); hide_panel(error_shadow_p); show_panel(login_shadow_p); top_panel(login_p); clear(); return result; } char *ui_get_text() { unsigned int len = strlen(input_buf) + 1; char *buf = malloc(len * sizeof(char)); strcpy(buf, input_buf); memset(input_buf, (int)'\0', input_buflen * sizeof(char)); return buf; } void input_add_char(char ch) { // resize if necessary if ((strlen(input_buf) + 2) > input_buflen) { input_buf = safe_realloc(input_buf, input_buflen * sizeof(char), input_buflen * 2 * sizeof(char)); input_buflen *= 2; } // insert new char & shift everything over for (int i = (input_pos++); i < input_buflen; i++) { char temp = input_buf[i]; input_buf[i] = ch; ch = temp; if (input_buf[i] == '\0') break; } if (input_pos >= (input_scroll + login_cols - 6)) input_scroll++; } void input_backdel_char() { if (input_pos <= 0) return; input_pos--; input_del_char(); } void input_del_char() { if (input_buf[input_pos] == '\0') return; for (int i = input_pos; i < input_buflen - 1; i++) { input_buf[i] = input_buf[i + 1]; if (input_buf[i] == '\0') break; } if ((input_scroll + login_cols - 6) > strlen(input_buf) && input_scroll > 0) input_scroll--; } const attr_t light = A_BOLD | COLOR_PAIR(3); const attr_t shadow = COLOR_PAIR(2); void draw_outline(WINDOW *win, int height, int width, int y, int x, bool inset) { wattron(win, A_ALTCHARSET); wattron(win, inset ? shadow : light); mvwhline(win, y, x + 1, ACS_HLINE, width - 2); mvwvline(win, y + 1, x, ACS_VLINE, height - 2); mvwaddch(win, y, x, ACS_ULCORNER); mvwaddch(win, y + height - 1, x, ACS_LLCORNER); wattroff(win, inset ? shadow : light); wattron(win, inset ? light : shadow); mvwhline(win, y + height - 1, x + 1, ACS_HLINE, width - 2); mvwvline(win, y + 1, x + width - 1, ACS_VLINE, height - 2); mvwaddch(win, y, x + width - 1, ACS_URCORNER); mvwaddch(win, y + height - 1, x + width - 1, ACS_LRCORNER); wattroff(win, inset ? light : shadow); wattroff(win, A_ALTCHARSET); } /** output_word - used in word_wrap */ void output_word(char *word, size_t len, char ***out, int *line_pos, int *lines, int width) { int i = 0; while (i < len) { int space = width - (*line_pos); int remain = len - i; if (space > remain) { // can fit on line memcpy((*out)[(*lines) - 1] + (*line_pos), word + i, sizeof(char) * remain); i += remain; *line_pos += remain; } else { // copy part of word memcpy((*out)[(*lines) - 1] + (*line_pos), word + i, sizeof(char) * space); i += space; *line_pos += space; // add newline (*out)[(*lines) - 1][(*line_pos)] = '\0'; *line_pos = 0; (*lines)++; *out = realloc(*out, sizeof(char *) * (*lines)); (*out)[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } } } char **word_wrap(char *text, int width, int max_word, int *lines) { // output lines *lines = 1; char **out = malloc(sizeof(char *) * (*lines)); out[0] = malloc(sizeof(char) * (width + 1)); // word buffer char *word = malloc(sizeof(char) * max_word); int word_len = 0; int line_pos = 0; for (char ch; (ch = *text) != '\0'; text++) { switch (ch) { case '\n': case ' ': if (word[0] == '\0') word_len = 0; if (word_len > 0) { // do we have to wrap to a new line? if (line_pos + word_len > width) { out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; out = realloc(out, sizeof(char *) * (*lines)); out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } memcpy(out[(*lines) - 1] + line_pos, word, sizeof(char) * word_len); line_pos += word_len; word_len = 0; } // output \n if (ch == '\n' || line_pos >= width) { out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; out = realloc(out, sizeof(char *) * (*lines)); out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } else { out[(*lines) - 1][line_pos++] = ' '; } break; default: // word longer than word buffer if (word_len >= max_word) { if (word[0] != '\0') { // write what we have output_word(word, word_len, &out, &line_pos, lines, width); word[0] = '\0'; } // write char and maybe newline out[(*lines) - 1][line_pos++] = ch; if (line_pos >= width) { out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; out = realloc(out, sizeof(char *) * (*lines)); out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } } else word[word_len++] = ch; } } if (word_len > 0 && word[0] != '\0') { // do we have to wrap to a new line? if (line_pos + word_len > width) { out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; out = realloc(out, sizeof(char *) * (*lines)); out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } memcpy(out[(*lines) - 1] + line_pos, word, sizeof(char) * word_len); line_pos += word_len; word_len = 0; } free(word); if (line_pos == 0) // remove last line free(out[--(*lines)]); else // non-empty line, add null terminator out[(*lines) - 1][line_pos] = '\0'; return out; }