679 lines
15 KiB
C
679 lines
15 KiB
C
/*
|
|
* ui.c - hlogin(1) user interface functions
|
|
*
|
|
* Copyleft (C) 2022 ~keith <keith@keithhacks.cyou>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "config.h"
|
|
#include "ui.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <errno.h>
|
|
|
|
#include <locale.h>
|
|
#include "gettext.h"
|
|
|
|
#include <unistd.h>
|
|
#include <limits.h>
|
|
#include <time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
|
|
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;
|
|
}
|