jcs
/subtext
/amendments
/97
user_settings: Add user settings menu
Currently allows changing passwords and renegotiating the terminal
jcs made amendment 97 over 2 years ago
--- ansi.c Mon Feb 21 15:24:00 2022
+++ ansi.c Thu Apr 28 14:03:36 2022
@@ -248,4 +248,50 @@ ansi_strip(char *inp, char **outp)
}
return olen;
-}
+}
+
+void
+ansi_probe_screen_size(struct session *s)
+{
+ short cols, rows;
+ size_t count = 0;
+ char c[2] = { 0 };
+
+ if (!s->vt100)
+ return;
+
+ /* save cursor position */
+ session_clear_input(s);
+ session_output_string(s, "\33[s");
+ session_flush(s);
+
+ /* go to the (hopefully) edge of the screen */
+ session_output_string(s, "\33[200B\33[200C");
+ session_flush(s);
+
+ /* report cursor position */
+ session_clear_input(s);
+ session_output_string(s, "\33[6n");
+ session_flush(s);
+
+ session_wait_for_chars(s, 1500, 100);
+
+ /* \e[12;34R */
+ if (s->ibuf[0] == ESC && s->ibuf[1] == '[' &&
+ sscanf((char *)&s->ibuf + 2, "%d;%d%[R]", &rows, &cols, &c) == 3 &&
+ c[0] == 'R') {
+ if (cols >= MIN_TERMINAL_COLUMNS && rows >= MIN_TERMINAL_LINES) {
+ s->terminal_columns = cols;
+ s->terminal_lines = rows;
+ session_log(s, "Probed terminal size of %dx%d", cols, rows);
+ } else {
+ session_log(s, "Bogus terminal size probe response: %dx%d",
+ cols, rows);
+ }
+ }
+
+ /* restore saved cursor position */
+ session_clear_input(s);
+ session_output_string(s, "\33[u");
+ session_flush(s);
+}
--- ansi.h Mon Feb 21 15:24:45 2022
+++ ansi.h Thu Apr 28 13:22:14 2022
@@ -17,6 +17,8 @@
#ifndef __ANSI_H__
#define __ANSI_H__
+#define ESC 0x1b
+
enum {
ANSI_RESET = 1,
ANSI_BOLD,
@@ -45,5 +47,6 @@ enum {
char *ansi(struct session *s, ...);
size_t ansi_strip(char *inp, char **outp);
+void ansi_probe_screen_size(struct session *s);
#endif /* __ANSI_H__ */
--- console.c Thu Apr 21 17:04:17 2022
+++ console.c Thu Apr 28 13:40:14 2022
@@ -18,6 +18,7 @@
#include <string.h>
#include "subtext.h"
+#include "ansi.h"
#include "console.h"
#include "focusable.h"
#include "session.h"
@@ -514,9 +515,9 @@ console_shift_chars(struct console *console, short sta
count);
if (start % console->ncolumns != 0)
- panic("TODO partial console_shift_chars");
+ warn("TODO partial console_shift_chars");
if (offset % console->ncolumns != 0)
- panic("TODO partial console_shift_chars");
+ warn("TODO partial console_shift_chars");
rgn = NewRgn();
mover.top = console->win->portRect.top + CONSOLE_PADDING +
@@ -539,6 +540,7 @@ console_parse_csi(struct console *console)
short param1 = -1, param2 = -1;
short parambuflen;
short start, count, offset;
+ struct session *session;
char parambuf[4];
unsigned char c;
@@ -567,6 +569,7 @@ console_parse_csi(struct console *console)
case 'T':
case 'd':
case 'g':
+ case 'n':
case 's':
case 'u':
case '7':
@@ -846,11 +849,42 @@ console_parse_csi(struct console *console)
case 'n': /* DSR - device status report */
switch (param1) {
case 5: /* terminal is ready */
- session_output(console->session, "\33[0n", 4);
+ session = console->session;
+ if (session->ibuflen >= sizeof(session->ibuf) - 4)
+ break;
+ session->ibuf[session->ibuflen++] = ESC;
+ session->ibuf[session->ibuflen++] = '[';
+ session->ibuf[session->ibuflen++] = '0';
+ session->ibuf[session->ibuflen++] = 'n';
break;
case 6: /* CPR - report cursor position */
- session_printf(console->session, "\33[%d;%dR",
- console->cursor_line + 1, console->cursor_column + 1);
+ session = console->session;
+ if (session->ibuflen >= sizeof(session->ibuf) - 10)
+ break;
+ session->ibuf[session->ibuflen++] = ESC;
+ session->ibuf[session->ibuflen++] = '[';
+
+ if (console->cursor_line >= 99)
+ session->ibuf[session->ibuflen++] = '0' +
+ ((console->cursor_line + 1) / 100);
+ if (console->cursor_line >= 9)
+ session->ibuf[session->ibuflen++] = '0' +
+ (((console->cursor_line + 1) % 100) / 10);
+ session->ibuf[session->ibuflen++] = '0' +
+ ((console->cursor_line + 1) % 10);
+
+ session->ibuf[session->ibuflen++] = ';';
+
+ if (console->cursor_column >= 99)
+ session->ibuf[session->ibuflen++] = '0' +
+ ((console->cursor_column + 1) / 100);
+ if (console->cursor_column >= 9)
+ session->ibuf[session->ibuflen++] = '0' +
+ (((console->cursor_column + 1) % 100) / 10);
+ session->ibuf[session->ibuflen++] = '0' +
+ ((console->cursor_column + 1) % 10);
+
+ session->ibuf[session->ibuflen++] = 'R';
break;
}
break;
--- mail.c Mon Feb 21 13:50:29 2022
+++ mail.c Tue Apr 26 20:55:15 2022
@@ -75,11 +75,12 @@ mail_free_message_strings(struct private_message *msg)
void
mail_menu(struct session *s)
{
- char arg[32];
+ char l[2], arg[32];
size_t count, nmsgs, id;
bool done = false;
char *field;
unsigned long *mail_ids = NULL;
+ short ret;
if (!s->user) {
session_output_string(s, "Mail is not available to guests.\r\n"
@@ -105,15 +106,18 @@ mail_menu(struct session *s)
session_output(s, "\r\n", 2);
session_flush(s);
- if (sscanf(field, "c %s%n", &arg, &count) == 1 && count > 1)
+ if (sscanf(field, "%[Cc] %s", &l, &arg) == 2)
mail_compose(s, arg, NULL, NULL);
- else if (strcmp(field, "c") == 0)
+ else if (sscanf(field, "%[Cc]", &l) == 1 && field[1] == '\0')
mail_compose(s, NULL, NULL, NULL);
- else if (sscanf(field, "d %ld%n", &id, &count) == 1 && count > 1)
+ else if (sscanf(field, "%[Dd] %ld%n", &l, &id, &count) == 2 &&
+ field[count] == '\0')
mail_delete(s, id);
- else if (sscanf(field, "u %ld%n", &id, &count) == 1 && count > 1)
+ else if (sscanf(field, "%[Uu] %ld%n", &l, &id, &count) == 2 &&
+ field[count] == '\0')
mail_mark_unread(s, id);
- else if (sscanf(field, "%ld%n", &id, &count) == 1 && count > 1) {
+ else if (sscanf(field, "%ld%n", &id, &count) == 1 &&
+ field[count] == '\0') {
if (id < 1 || id > nmsgs) {
session_output_string(s, "Invalid message id\r\n");
session_flush(s);
@@ -121,11 +125,11 @@ mail_menu(struct session *s)
}
mail_read(s, mail_ids[id - 1]);
}
- else if (strcmp(field, "l") == 0)
+ else if (sscanf(field, "%[Ll]", &l) == 1 && field[1] == '\0')
mail_list(s, false, &mail_ids, &nmsgs);
- else if (strcmp(field, "q") == 0 || strcmp(field, "x") == 0)
+ else if (sscanf(field, "%[QqXx]", &l) == 1 && field[1] == '\0')
done = true;
- else if (strcmp(field, "?") == 0 || strcmp(field, "h") == 0)
+ else if (sscanf(field, "%[?Hh]", &l) == 1 && field[1] == '\0')
mail_help(s);
else if (field[0] != '\0') {
session_output_template(s,
@@ -141,12 +145,12 @@ void
mail_help(struct session *s)
{
session_output_template(s,
- "{{B}}l{{/}}: List mail messages\r\n"
+ "{{B}}L{{/}}: List mail messages\r\n"
"{{B}}#{{/}}: Read mail message [#]\r\n"
- "{{B}}d #{{/}}: Delete mail message [#]\r\n"
- "{{B}}u #{{/}}: Mark mail message [#] unread\r\n"
- "{{B}}c{{/}}: Compose new mail message\r\n"
- "{{B}}q{{/}}: Return to main menu\r\n");
+ "{{B}}D #{{/}}: Delete mail message [#]\r\n"
+ "{{B}}U #{{/}}: Mark mail message [#] unread\r\n"
+ "{{B}}C{{/}}: Compose new mail message\r\n"
+ "{{B}}Q{{/}}: Return to main menu\r\n");
session_flush(s);
}
--- session.c Tue Apr 26 15:46:59 2022
+++ session.c Wed Apr 27 17:41:47 2022
@@ -187,8 +187,11 @@ get_another_char:
break;
case '?':
/* short menu */
- if (!session_output_view(s, DB_TEXT_SHORTMENU_ID))
- session_output_string(s, "\r\n[ Short menu missing! ]\r\n\r\n");
+ if (!session_output_view(s, DB_TEXT_SHORTMENU_ID)) {
+ if (!session_output_view(s, DB_TEXT_MENU_ID))
+ session_output_string(s, "\r\n[ Short and long menu "
+ "missing! ]\r\n\r\n");
+ }
session_flush(s);
break;
default:
@@ -376,32 +379,54 @@ session_output_template(struct session *session, const
return size;
}
-unsigned short
-session_input_char(struct session *session)
+bool
+session_wait_for_chars(struct session *session, unsigned short timeout_ms,
+ unsigned short num_chars)
{
- unsigned short ret;
- short waiting_for = 1;
- short consumed = 0;
-
+ unsigned long expire = 0;
+
+ if (timeout_ms)
+ expire = Ticks + (timeout_ms / ((double)1000 / (double)60));
+
wait_for_char:
- while (session->ibuflen < waiting_for) {
+ while (session->ibuflen < num_chars) {
session->node_funcs->input(session);
if (session->ending)
- return 0;
+ return false;
if (session->abort_input)
- return 0;
- if (session->ibuflen >= waiting_for)
- break;
+ return false;
if (session->obuflen != 0 && session->chatting)
- return 0;
+ return false;
+ if (expire && Ticks > expire)
+ return false;
if (Time - session->last_input_at >
(db->config.max_idle_minutes * 60))
- goto idled_out;
+ return false;
+ if (session->ibuflen >= num_chars)
+ break;
uthread_yield();
}
session->last_input_at = Time;
+ return true;
+}
+
+unsigned short
+session_input_char(struct session *session)
+{
+ unsigned short ret;
+ short waiting_for = 1;
+ short consumed = 0;
+wait_for_char:
+ if (!session_wait_for_chars(session, 0, waiting_for)) {
+ if (Time - session->last_input_at >
+ (db->config.max_idle_minutes * 60))
+ goto idled_out;
+
+ return 0;
+ }
+
if (session->ibuf[0] == '\33') {
/* look for a VT100 escape sequence */
if (session->ibuflen == 1) {
@@ -465,6 +490,17 @@ idled_out:
session_flush(session);
session->ending = 1;
return 0;
+}
+
+void
+session_clear_input(struct session *session)
+{
+ session->ibuflen = 0;
+
+ /*
+ * TODO: provide a node-specific way to do this since nodes might
+ * have their own input buffers
+ */
}
/*
--- session.h Tue Apr 26 15:45:15 2022
+++ session.h Thu Apr 28 14:02:58 2022
@@ -30,6 +30,8 @@ enum session_input_state {
#define DEFAULT_TERMINAL_COLUMNS 80
#define DEFAULT_TERMINAL_LINES 24
+#define MIN_TERMINAL_COLUMNS 20
+#define MIN_TERMINAL_LINES 4
#define CONTROL_C 3
#define CONTROL_D 4
@@ -99,7 +101,10 @@ struct session * session_create(char *node, char *via,
struct node_funcs *node_funcs);
void session_close(struct session *session);
void session_idle(struct session *session);
+bool session_wait_for_chars(struct session *session,
+ unsigned short timeout_ms, unsigned short num_chars);
unsigned short session_input_char(struct session *session);
+void session_clear_input(struct session *session);
void session_flush(struct session *session);
size_t session_log(struct session *session, const char *format, ...);
size_t session_output(struct session *session, const char *str, size_t len);
--- user_settings.c Thu Apr 28 14:13:17 2022
+++ user_settings.c Thu Apr 28 14:13:17 2022
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2022 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "subtext.h"
+#include "ansi.h"
+#include "session.h"
+#include "user.h"
+#include "user_settings.h"
+#include "util.h"
+
+void user_settings_help(struct session *s);
+void user_settings_renegotiate(struct session *s);
+void user_settings_password(struct session *s);
+
+void
+user_settings_help(struct session *s)
+{
+ session_output_template(s,
+ "{{B}}R{{/}}: Renegotiate terminal\r\n");
+
+ if (s->user)
+ session_output_template(s, "{{B}}P{{/}}: Change password\r\n");
+
+ session_output_template(s,
+ "{{B}}Q{{/}}: Return to main menu\r\n");
+ session_flush(s);
+}
+
+void
+user_settings_renegotiate(struct session *s)
+{
+ unsigned short c;
+ short cols, lines;
+ char *tsize;
+
+ session_output_template(s,
+ "Does this terminal support VT100 escape sequences? [Y/n] ");
+ session_flush(s);
+
+try_again:
+ c = session_input_char(s);
+ if (s->ending)
+ return;
+ if (c == 0 || c > 255)
+ goto try_again;
+ if (c == 'n' || c == 'N') {
+ session_printf(s, "%c\r\n", c == 'N' ? 'N' : 'n');
+ session_flush(s);
+ s->vt100 = false;
+ } else if (c == '\r' || c == 'Y' || c == 'y') {
+ session_printf(s, "%c\r\n", c == 'Y' ? 'Y' : 'y');
+ session_flush(s);
+ s->vt100 = true;
+ }
+
+ if (!s->vt100) {
+ session_output_template(s,
+ "Does this terminal support VT52 escape sequences? [Y/n] ");
+ session_flush(s);
+
+try_again2:
+ c = session_input_char(s);
+ if (s->ending)
+ return;
+ if (c == 0 || c > 255)
+ goto try_again2;
+ if (c == 'n' || c == 'N') {
+ session_printf(s, "%c\r\n", c == 'N' ? 'N' : 'n');
+ session_flush(s);
+ s->vt52 = false;
+ } else if (c == '\r' || c == 'Y' || c == 'y') {
+ session_printf(s, "%c\r\n", c == 'Y' ? 'Y' : 'y');
+ session_flush(s);
+ s->vt52 = true;
+ }
+ }
+
+ if (s->vt100)
+ ansi_probe_screen_size(s);
+
+get_terminal_size:
+ session_printf(s,
+ "Terminal size (columns x rows) [%dx%d] ", s->terminal_columns,
+ s->terminal_lines);
+ session_flush(s);
+
+ tsize = session_field_input(s, 10, 10, NULL, 0);
+ if (tsize == NULL)
+ return;
+ session_output(s, "\r\n", 2);
+ session_flush(s);
+ if (tsize[0] == '\0')
+ return;
+
+ if (sscanf(tsize, "%dx%d", &cols, &lines) != 2) {
+ session_printf(s, "Invalid response, must be in format %dx%d\r\n",
+ s->terminal_columns, s->terminal_lines);
+ session_flush(s);
+ goto get_terminal_size;
+ }
+
+ if (cols < MIN_TERMINAL_COLUMNS || lines < MIN_TERMINAL_LINES) {
+ session_printf(s, "Terminal too small, must be at least "
+ "%dx%d\r\n", MIN_TERMINAL_COLUMNS, MIN_TERMINAL_LINES);
+ session_flush(s);
+ goto get_terminal_size;
+ }
+
+ s->terminal_columns = cols;
+ s->terminal_lines = lines;
+ session_log(s, "Changed terminal size to %dx%d", cols, lines);
+}
+
+void
+user_settings_password(struct session *s)
+{
+ char *password = NULL, *password_confirm = NULL;
+
+ if (!s->user) {
+ session_output_template(s, "{{B}}Error{{/}}: Guest accounts "
+ "cannot change passwords\r\n");
+ return;
+ }
+
+ for (;;) {
+ session_output_template(s, "{{B}}New Password:{{/}} ");
+ session_flush(s);
+ password = session_field_input(s, 64, 64, NULL, '*');
+ session_output(s, "\r\n", 2);
+ session_flush(s);
+
+ if (password == NULL || s->ending)
+ break;
+
+ if (password[0] == '\0') {
+ session_output_template(s, "{{B}}Error:{{/}} "
+ "Password cannot be blank\r\n");
+ free(password);
+ continue;
+ }
+
+ session_output_template(s, "{{B}}New Password (again):{{/}} ");
+ session_flush(s);
+ password_confirm = session_field_input(s, 64, 64, NULL, '*');
+ session_output(s, "\r\n", 2);
+ session_flush(s);
+
+ if (password_confirm == NULL || s->ending)
+ break;
+
+ if (strcmp(password_confirm, password) != 0) {
+ session_output_template(s, "{{B}}Error:{{/}} "
+ "Passwords do not match\r\n");
+ free(password);
+ free(password_confirm);
+ continue;
+ }
+
+ user_set_password(s->user, password);
+ user_save(s->user);
+ session_output_template(s, "{{B}}Your password has been "
+ "changed{{/}}\r\n");
+ break;
+ }
+
+ if (password)
+ free(password);
+ if (password_confirm)
+ free(password_confirm);
+}
+
+void
+user_settings_menu(struct session *s)
+{
+ bool done = false;
+ unsigned short c;
+
+ session_log(s, "User settings menu");
+
+ session_output_template(s, "{{B}}Settings{{/}}\r\n"
+ "{{B}}--------{{/}}\r\n");
+ user_settings_help(s);
+
+ while (!done) {
+ session_output_template(s, "{{B}}Settings>{{/}} ");
+ session_flush(s);
+
+get_another_char:
+ c = session_input_char(s);
+ if (s->ending)
+ break;
+ if (c == '\r' || c == 0 || c > 255)
+ goto get_another_char;
+
+ session_printf(s, "%c\r\n", c);
+ session_flush(s);
+
+ switch (c) {
+ case 'p':
+ case 'P':
+ user_settings_password(s);
+ break;
+ case 'q':
+ case 'Q':
+ case 'x':
+ case 'X':
+ done = true;
+ break;
+ case 'r':
+ case 'R':
+ user_settings_renegotiate(s);
+ break;
+ case '?':
+ case 'H':
+ case 'h':
+ user_settings_help(s);
+ break;
+ default:
+ session_output_string(s, "Invalid option\r\n");
+ session_flush(s);
+ break;
+ }
+ }
+}
--- user_settings.h Fri Apr 22 12:28:20 2022
+++ user_settings.h Fri Apr 22 12:28:20 2022
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 joshua stein <jcs@jcs.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __USER_SETTINGS_H__
+#define __USER_SETTINGS_H__
+
+#include "session.h"
+#include "user.h"
+
+void user_settings_menu(struct session *s);
+
+#endif