summaryrefslogtreecommitdiff
path: root/stacks/terminal/max25_ui.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-07 18:25:13 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-07 18:25:13 +0000
commit04d965d67a7264a1c7c211494aebda1953df7603 (patch)
tree0ebd700a6e219f84a26a656f4bee8778bc75da7b /stacks/terminal/max25_ui.c
Initial push
Diffstat (limited to 'stacks/terminal/max25_ui.c')
-rw-r--r--stacks/terminal/max25_ui.c407
1 files changed, 407 insertions, 0 deletions
diff --git a/stacks/terminal/max25_ui.c b/stacks/terminal/max25_ui.c
new file mode 100644
index 0000000..91cc977
--- /dev/null
+++ b/stacks/terminal/max25_ui.c
@@ -0,0 +1,407 @@
+#include "max25_ui.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef MAX25_HAVE_NCURSES
+#include <ncurses.h>
+#endif
+
+struct max25_ui {
+ int menu_open;
+ char rx_lines[MAX25_UI_RX_LINES][MAX25_LINE_MAX];
+ int rx_count;
+ int rx_head;
+#ifdef MAX25_HAVE_NCURSES
+ int ncurses_on;
+#endif
+};
+
+#ifdef MAX25_HAVE_NCURSES
+static int term_ok_for_ncurses(void)
+{
+ const char *term = getenv("TERM");
+
+ if (term == NULL || term[0] == '\0') {
+ return 0;
+ }
+ if (strcmp(term, "dumb") == 0) {
+ return 0;
+ }
+ if (strncmp(term, "unknown", 7) == 0) {
+ return 0;
+ }
+ return 1;
+}
+#endif
+
+int max25_ui_init(max25_ui_t **ui)
+{
+ max25_ui_t *u;
+
+ if (ui == NULL) {
+ return -1;
+ }
+
+ u = calloc(1, sizeof(*u));
+ if (u == NULL) {
+ return -1;
+ }
+
+#ifdef MAX25_HAVE_NCURSES
+ if (isatty(STDOUT_FILENO) && isatty(STDIN_FILENO) && term_ok_for_ncurses()
+ && initscr() != NULL) {
+ cbreak();
+ noecho();
+ keypad(stdscr, TRUE);
+ nodelay(stdscr, TRUE);
+ if (has_colors()) {
+ start_color();
+ init_pair(1, COLOR_WHITE, COLOR_BLACK);
+ }
+ bkgd(COLOR_PAIR(1));
+ curs_set(0);
+ clear();
+ refresh();
+ u->ncurses_on = 1;
+ }
+#endif
+
+ *ui = u;
+ return 0;
+}
+
+void max25_ui_shutdown(max25_ui_t *ui)
+{
+ if (ui == NULL) {
+ return;
+ }
+#ifdef MAX25_HAVE_NCURSES
+ if (ui->ncurses_on) {
+ endwin();
+ }
+#endif
+ free(ui);
+}
+
+void max25_ui_apply_palette(void)
+{
+ fputs(MAX25_TERM_SGR, stdout);
+ fflush(stdout);
+}
+
+void max25_ui_clear_screen(void)
+{
+ fputs(MAX25_TERM_CLEAR, stdout);
+ fputs(MAX25_TERM_SGR, stdout);
+ fflush(stdout);
+}
+
+int max25_ui_has_ncurses(const max25_ui_t *ui)
+{
+#ifdef MAX25_HAVE_NCURSES
+ return ui != NULL && ui->ncurses_on;
+#else
+ (void)ui;
+ return 0;
+#endif
+}
+
+void max25_ui_reset_rx(max25_ui_t *ui)
+{
+ if (ui == NULL) {
+ return;
+ }
+ ui->rx_count = 0;
+ ui->rx_head = 0;
+ memset(ui->rx_lines, 0, sizeof(ui->rx_lines));
+}
+
+void max25_ui_append_rx(max25_ui_t *ui, const char *text)
+{
+ int slot;
+
+ if (ui == NULL || text == NULL) {
+ return;
+ }
+
+ slot = ui->rx_head;
+ strncpy(ui->rx_lines[slot], text, MAX25_LINE_MAX - 1);
+ ui->rx_lines[slot][MAX25_LINE_MAX - 1] = '\0';
+ ui->rx_head = (ui->rx_head + 1) % MAX25_UI_RX_LINES;
+ if (ui->rx_count < MAX25_UI_RX_LINES) {
+ ui->rx_count++;
+ }
+}
+
+static void draw_header_plain(const max25_status_t *status)
+{
+ printf("%sMAX25 Terminal DEVICE: %-12s CALLERID: %-9s CALLID: %-9s ax25-ui: %s connected: %s F10=Menu\n",
+ MAX25_TERM_SGR,
+ status->device[0] != '\0' ? status->device : "-",
+ status->callerid, status->callid,
+ status->ax25_ui ? "on" : "off",
+ status->connected ? "yes" : "no");
+ fflush(stdout);
+}
+
+static void draw_rx_plain(const max25_ui_t *ui)
+{
+ int i;
+ int start;
+ int idx;
+
+ if (ui == NULL) {
+ return;
+ }
+
+ start = ui->rx_count < MAX25_UI_RX_LINES ? 0 : ui->rx_head;
+ for (i = 0; i < ui->rx_count; i++) {
+ idx = (start + i) % MAX25_UI_RX_LINES;
+ printf("%s%s\n", MAX25_TERM_SGR, ui->rx_lines[idx]);
+ }
+ fflush(stdout);
+}
+
+void max25_ui_draw_screen(max25_ui_t *ui, const max25_status_t *status,
+ const char *input_line)
+{
+ int i;
+ int start;
+ int idx;
+ int rows;
+ int y;
+
+ if (ui == NULL || status == NULL) {
+ return;
+ }
+
+#ifdef MAX25_HAVE_NCURSES
+ if (ui->ncurses_on && !ui->menu_open) {
+ getmaxyx(stdscr, rows, y);
+ (void)y;
+ if (rows < 4) {
+ rows = 24;
+ }
+
+ attron(COLOR_PAIR(1));
+ mvprintw(0, 0,
+ "MAX25 Terminal DEVICE: %-12s CALLERID: %-9s CALLID: %-9s ax25-ui: %s connected: %s F10=Menu",
+ status->device[0] != '\0' ? status->device : "-",
+ status->callerid, status->callid,
+ status->ax25_ui ? "on" : "off",
+ status->connected ? "yes" : "no");
+ clrtoeol();
+
+ start = ui->rx_count < MAX25_UI_RX_LINES ? 0 : ui->rx_head;
+ for (i = 0; i < ui->rx_count && i + 2 < rows - 2; i++) {
+ idx = (start + i) % MAX25_UI_RX_LINES;
+ mvprintw(i + 2, 0, "%.*s", (int)(rows > 0 ? 200 : 80), ui->rx_lines[idx]);
+ clrtoeol();
+ }
+
+ mvprintw(rows - 2, 0, "%s", "--------------------------------------------------------");
+ clrtoeol();
+ mvprintw(rows - 1, 0, "> %s", input_line != NULL ? input_line : "");
+ clrtoeol();
+ attroff(COLOR_PAIR(1));
+ refresh();
+ return;
+ }
+#endif
+
+ if (!ui->menu_open) {
+ fputs(MAX25_TERM_CLEAR, stdout);
+ fputs(MAX25_TERM_SGR, stdout);
+ draw_header_plain(status);
+ draw_rx_plain(ui);
+ printf("%s> %s\n", MAX25_TERM_SGR, input_line != NULL ? input_line : "");
+ fflush(stdout);
+ }
+}
+
+static void draw_menu_plain(const max25_status_t *status)
+{
+ puts("");
+ puts("+- MAX25 Terminal ---------------------+");
+ printf("| DEVICE: %-12s |\n",
+ status->device[0] != '\0' ? status->device : "-");
+ printf("| CALLERID: %-9s CALLID: %-9s |\n",
+ status->callerid, status->callid);
+ puts("+---------------------------------------+");
+ puts("| 1 Change CALLERID (live) |");
+ puts("| 2 Change CALLID (live) |");
+ puts("| 3 Status |");
+ puts("| 4 Send line |");
+ puts("| 5 RX only (Monitor) |");
+ puts("| 6 Connection on/off |");
+ puts("| 7 Change DEVICE (TX target) |");
+ puts("| 8 List DEVICES (max25d) |");
+ puts("| 0 Quit client |");
+ puts("+---------------------------------------+");
+ puts("Pick a number - F10 closes");
+ fflush(stdout);
+}
+
+#ifdef MAX25_HAVE_NCURSES
+static void draw_menu_ncurses(const max25_status_t *status, int y0)
+{
+ attron(COLOR_PAIR(1));
+ mvprintw(y0, 0, "+- MAX25 Terminal ---------------------+");
+ mvprintw(y0 + 1, 0, "| DEVICE: %-12s |",
+ status->device[0] != '\0' ? status->device : "-");
+ mvprintw(y0 + 2, 0, "| CALLERID: %-9s CALLID: %-9s |",
+ status->callerid, status->callid);
+ mvprintw(y0 + 3, 0, "+---------------------------------------+");
+ mvprintw(y0 + 4, 0, "| 1 Change CALLERID (live) |");
+ mvprintw(y0 + 5, 0, "| 2 Change CALLID (live) |");
+ mvprintw(y0 + 6, 0, "| 3 Status |");
+ mvprintw(y0 + 7, 0, "| 4 Send line |");
+ mvprintw(y0 + 8, 0, "| 5 RX only (Monitor) |");
+ mvprintw(y0 + 9, 0, "| 6 Connection on/off |");
+ mvprintw(y0 + 10, 0, "| 7 Change DEVICE (TX target) |");
+ mvprintw(y0 + 11, 0, "| 8 List DEVICES (max25d) |");
+ mvprintw(y0 + 12, 0, "| 0 Quit client |");
+ mvprintw(y0 + 13, 0, "+---------------------------------------+");
+ mvprintw(y0 + 14, 0, "Pick a number - F10 closes");
+ attroff(COLOR_PAIR(1));
+}
+#endif
+
+void max25_ui_show_menu(max25_ui_t *ui, const max25_status_t *status)
+{
+ if (ui == NULL || status == NULL) {
+ return;
+ }
+ ui->menu_open = 1;
+
+#ifdef MAX25_HAVE_NCURSES
+ if (ui->ncurses_on) {
+ clear();
+ bkgd(COLOR_PAIR(1));
+ draw_menu_ncurses(status, 2);
+ refresh();
+ return;
+ }
+#endif
+ fputs(MAX25_TERM_CLEAR, stdout);
+ fputs(MAX25_TERM_SGR, stdout);
+ draw_menu_plain(status);
+}
+
+void max25_ui_hide_menu(max25_ui_t *ui)
+{
+ if (ui == NULL) {
+ return;
+ }
+ ui->menu_open = 0;
+#ifdef MAX25_HAVE_NCURSES
+ if (ui->ncurses_on) {
+ clear();
+ bkgd(COLOR_PAIR(1));
+ refresh();
+ }
+#endif
+}
+
+int max25_ui_menu_visible(const max25_ui_t *ui)
+{
+ return ui != NULL && ui->menu_open;
+}
+
+max25_menu_action_t max25_ui_menu_pick(max25_ui_t *ui, int ch)
+{
+ (void)ui;
+ switch (ch) {
+ case '1':
+ return MAX25_MENU_CALLERID;
+ case '2':
+ return MAX25_MENU_CALLID;
+ case '3':
+ return MAX25_MENU_STATUS;
+ case '4':
+ return MAX25_MENU_SEND;
+ case '5':
+ return MAX25_MENU_MONITOR;
+ case '6':
+ return MAX25_MENU_CONNECT;
+ case '7':
+ return MAX25_MENU_DEVICE;
+ case '8':
+ return MAX25_MENU_DEVICES;
+ case '0':
+ return MAX25_MENU_QUIT;
+ default:
+ return MAX25_MENU_NONE;
+ }
+}
+
+int max25_ui_prompt(max25_ui_t *ui, const char *label, char *buf, size_t buf_sz)
+{
+ if (label == NULL || buf == NULL || buf_sz == 0) {
+ return -1;
+ }
+
+#ifdef MAX25_HAVE_NCURSES
+ if (ui != NULL && ui->ncurses_on) {
+ int ch;
+ size_t len = 0;
+ int rows;
+
+ getmaxyx(stdscr, rows, ch);
+ (void)rows;
+ echo();
+ curs_set(1);
+ attron(COLOR_PAIR(1));
+ mvprintw(14, 0, "%s: ", label);
+ clrtoeol();
+ refresh();
+ buf[0] = '\0';
+
+ nodelay(stdscr, FALSE);
+ while (len + 1 < buf_sz) {
+ ch = getch();
+ if (ch == '\n' || ch == KEY_ENTER || ch == '\r') {
+ break;
+ }
+ if (ch == 27) {
+ nodelay(stdscr, TRUE);
+ noecho();
+ curs_set(0);
+ return -1;
+ }
+ if (ch == KEY_BACKSPACE || ch == 127 || ch == '\b') {
+ if (len > 0) {
+ len--;
+ buf[len] = '\0';
+ mvprintw(14, (int)strlen(label) + 2, "%s ", buf);
+ clrtoeol();
+ refresh();
+ }
+ continue;
+ }
+ if (ch >= 32 && ch < 127) {
+ buf[len++] = (char)ch;
+ buf[len] = '\0';
+ mvprintw(14, (int)strlen(label) + 2, "%s", buf);
+ refresh();
+ }
+ }
+ nodelay(stdscr, TRUE);
+ noecho();
+ curs_set(0);
+ attroff(COLOR_PAIR(1));
+ return (int)len;
+ }
+#endif
+
+ printf("%s%s: ", MAX25_TERM_SGR, label);
+ fflush(stdout);
+ if (fgets(buf, (int)buf_sz, stdin) == NULL) {
+ return -1;
+ }
+ buf[strcspn(buf, "\r\n")] = '\0';
+ return (int)strlen(buf);
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com