hlogin/ui.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;
}