You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3030 lines
111 KiB
3030 lines
111 KiB
/*
|
|
* TermTCP CLI - Terminal TCP Client
|
|
*
|
|
* Layout:
|
|
* [status bar ]
|
|
* [output window - session data, scrolls]
|
|
* [--- Monitor ---------------------------] <- toggleable
|
|
* [monitor window - BPQ RF frames, scrolls]
|
|
* [╔═ Input ══════════════════════════════╗]
|
|
* [║ > text wraps here ║]
|
|
* [╚══════════════════════════════════════╝]
|
|
*
|
|
* BPQ protocol: 0xFF = monitor frame start, 0xFE = end.
|
|
* Auto-login: sends username\rpassword\rBPQTermTCP\r on connect.
|
|
* Menu: Ctrl+A
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <netdb.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <sys/select.h>
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
#include <locale.h>
|
|
#include <wchar.h>
|
|
#include <ncurses.h>
|
|
|
|
#ifndef PATH_MAX
|
|
#define PATH_MAX 4096
|
|
#endif
|
|
|
|
#define VERSION "0.0.53"
|
|
#define MAX_HOSTS 4
|
|
#define INPUT_MAX 512
|
|
#define CONF_FILE ".termtcprc"
|
|
#define INPUT_INNER 2 /* content rows inside input box */
|
|
#define INPUT_OUTER (INPUT_INNER + 2) /* total rows incl. border */
|
|
#define SCRL_MAX 500 /* scrollback ring buffer depth */
|
|
#define SCRL_W 512 /* max bytes per stored line */
|
|
#define HIST_MAX 100 /* input history ring depth */
|
|
|
|
/* ── Color pairs ─────────────────────────────────────────── */
|
|
#define CP_DEFAULT 1
|
|
#define CP_RED 2
|
|
#define CP_GREEN 3
|
|
#define CP_YELLOW 4
|
|
#define CP_CYAN 5
|
|
#define CP_MAGENTA 6
|
|
#define CP_STATUS 7 /* black on cyan */
|
|
#define CP_INPUT 8 /* white on blue */
|
|
#define CP_SEP 9 /* cyan on black */
|
|
|
|
/* ── Host config ─────────────────────────────────────────── */
|
|
typedef struct {
|
|
char host[100];
|
|
char port[10];
|
|
char username[80];
|
|
char password[80];
|
|
char name[50];
|
|
} HostEntry;
|
|
|
|
typedef struct { char data[SCRL_W]; int len; int base_attr; } ScrlLine;
|
|
|
|
HostEntry hosts[MAX_HOSTS];
|
|
int num_hosts = 0;
|
|
int current_host = -1;
|
|
|
|
/* ── Network ─────────────────────────────────────────────── */
|
|
int sock = -1;
|
|
int connected = 0;
|
|
time_t connect_time = 0;
|
|
|
|
/* ── Signal handling ─────────────────────────────────────── *
|
|
* SIGINT (Ctrl+C) is trapped rather than left at its default
|
|
* (immediate process kill) so the program can ask for confirmation
|
|
* instead of dropping a live session/unsent form with no warning.
|
|
* The handler only sets a flag - all actual UI/exit work happens
|
|
* in interactive_loop(), since signal handlers must stay minimal
|
|
* and ncurses calls are not safe to make from inside one. */
|
|
static volatile sig_atomic_t sigint_pending = 0;
|
|
|
|
static void on_sigint(int signum)
|
|
{
|
|
(void)signum;
|
|
sigint_pending = 1;
|
|
}
|
|
|
|
/* ── Logging ─────────────────────────────────────────────── */
|
|
int logging = 0;
|
|
FILE *log_file = NULL;
|
|
|
|
/* ── BPQ / monitor state ─────────────────────────────────── */
|
|
int mon_visible = 1;
|
|
int mon_tx = 1;
|
|
int mon_com = 1;
|
|
int mon_nodes = 0;
|
|
int mon_colour = 1;
|
|
int mon_ui_only = 1;
|
|
int port_mask = 0xFFFF;
|
|
int in_mon_frame = 0;
|
|
static char mon_fbuf[4096]; /* accumulates one complete 0xFF…0xFE monitor frame */
|
|
static int mon_flen = 0;
|
|
int port_seen = 0; /* bitmask: bit N-1 set when port N seen in monitor data */
|
|
|
|
char port_names[32][32]; /* names for ports 1-32, loaded from config or defaulted */
|
|
|
|
/* ── Session line parser (for PORTS response) ────────────── */
|
|
static char ports_line_buf[256];
|
|
static int ports_line_len = 0;
|
|
static int ports_parse_mode = 0; /* 1 while inside "} Ports" response */
|
|
static int ports_loaded = 0; /* set when PORTS list fully received */
|
|
|
|
/* ── Scrollback buffers ──────────────────────────────────── */
|
|
static ScrlLine out_sbuf[SCRL_MAX];
|
|
static int out_sbuf_head = 0, out_sbuf_count = 0;
|
|
static int out_scroll = 0; /* 0=live, >0=lines from bottom */
|
|
static char out_part[SCRL_W];
|
|
static int out_part_len = 0, out_part_base = 0, out_part_cur = 0;
|
|
|
|
static ScrlLine mon_sbuf[SCRL_MAX];
|
|
static int mon_sbuf_head = 0, mon_sbuf_count = 0;
|
|
static int mon_scroll = 0;
|
|
static char mon_part[SCRL_W];
|
|
static int mon_part_len = 0, mon_part_base = 0, mon_part_cur = 0;
|
|
|
|
/* ── Input history ───────────────────────────────────────── */
|
|
static char hist_buf[HIST_MAX][INPUT_MAX];
|
|
static int hist_head = 0; /* next write slot (ring) */
|
|
static int hist_count = 0; /* valid entries */
|
|
static int hist_pos = -1; /* -1=live, 0=most recent ... */
|
|
static char hist_save[INPUT_MAX]; /* saved input while browsing */
|
|
|
|
/* ── ncurses windows ─────────────────────────────────────── */
|
|
WINDOW *status_win = NULL;
|
|
WINDOW *out_win = NULL;
|
|
WINDOW *sep_win = NULL; /* "----- Monitor -----" between output and monitor */
|
|
WINDOW *mon_win = NULL;
|
|
WINDOW *sep2_win = NULL; /* "----- Input -----" above input area */
|
|
WINDOW *input_win = NULL;
|
|
|
|
/* ── Input buffer ────────────────────────────────────────── */
|
|
char input_buf[INPUT_MAX];
|
|
int input_len = 0;
|
|
|
|
/* ── ICS-213 General Message ─────────────────────────────── */
|
|
#define ICS_SBLINE_MAX 120 /* "SB <addressee>@<distribution>" line */
|
|
#define ICS_FIELD_MAX 80 /* To / From / Subject single-line fields */
|
|
#define ICS_DT_MAX 40 /* Date/Time field */
|
|
#define ICS_MSG_ROWS 12 /* visible rows in the message textarea */
|
|
#define ICS_MSG_COLS 80 /* wrap width sent to the BBS (also box width) */
|
|
#define ICS_MSG_LINES 60 /* max stored logical lines in the message body */
|
|
#define ICS_MSG_LINELEN 200 /* max chars per logical (unwrapped) line typed */
|
|
|
|
typedef struct {
|
|
char sb_line[ICS_SBLINE_MAX]; /* BBS command: "SB PKTNET@USA" — sent verbatim */
|
|
char subject[ICS_FIELD_MAX]; /* BBS title prompt + ICS field 4 (shared) */
|
|
char incident[ICS_FIELD_MAX]; /* ICS field 1 — Incident Name (optional) */
|
|
char to[ICS_FIELD_MAX]; /* ICS field 2 — To (Name and Position) */
|
|
char from[ICS_FIELD_MAX]; /* ICS field 3 — From (Name and Position) */
|
|
char date[ICS_FIELD_MAX]; /* ICS field 5 — Date (UTC) */
|
|
char time_z[ICS_FIELD_MAX]; /* ICS field 6 — Time UTC, Z appended on send */
|
|
char msg_lines[ICS_MSG_LINES][ICS_MSG_LINELEN]; /* ICS field 7 — Message body */
|
|
int msg_nlines;
|
|
char appr_name[ICS_FIELD_MAX]; /* ICS field 8 — Approved by: Name */
|
|
char appr_sig[ICS_FIELD_MAX]; /* ICS field 8 — Approved by: Signature */
|
|
char appr_pos[ICS_FIELD_MAX]; /* ICS field 8 — Approved by: Position/Title */
|
|
} IcsMessage;
|
|
|
|
/* ── PKTNET Packet Check-In form (https://vden.org/pktnet/check_in.html) ─── */
|
|
#define CHKIN_FIELD_MAX 80
|
|
#define CHKIN_COMMENT_MAX 500 /* form's own stated limit */
|
|
|
|
static const char *chkin_type_opts[] = { "EXERCISE", "REAL EVENT" };
|
|
static const int chkin_type_n = 2;
|
|
|
|
static const char *chkin_band_opts[] = { "AXIP", "HF", "VHF", "UHF", "SHF" };
|
|
static const int chkin_band_n = 5;
|
|
|
|
static const char *chkin_session_opts[] = {
|
|
"AXIP", "AX25 Packet", "Pactor", "Robust Packet",
|
|
"Ardop", "VARA HF", "VARA FM", "Mesh"
|
|
};
|
|
static const int chkin_session_n = 8;
|
|
|
|
typedef struct {
|
|
char sb_line[ICS_SBLINE_MAX]; /* defaults to "SB PKTNET@USA" */
|
|
char subject[CHKIN_FIELD_MAX];
|
|
char agency[CHKIN_FIELD_MAX]; /* optional agency/group name */
|
|
char date_time[ICS_DT_MAX]; /* 1a */
|
|
char to[CHKIN_FIELD_MAX]; /* 1b - fixed "PKTNET@USA" but editable */
|
|
char from[CHKIN_FIELD_MAX]; /* 1c */
|
|
char contact_name[CHKIN_FIELD_MAX]; /* 1d */
|
|
char operators[CHKIN_FIELD_MAX]; /* 1e */
|
|
int type_idx; /* 2a - index into chkin_type_opts */
|
|
int band_idx; /* 2c - index into chkin_band_opts */
|
|
int session_idx; /* 2d - index into chkin_session_opts */
|
|
char location[CHKIN_FIELD_MAX]; /* 3a */
|
|
char grid[CHKIN_FIELD_MAX]; /* 3b */
|
|
char comment_lines[ICS_MSG_LINES][ICS_MSG_LINELEN]; /* 4 - textarea-edited */
|
|
int comment_nlines;
|
|
} PktnetCheckin;
|
|
|
|
/* ── Forward declarations ────────────────────────────────── */
|
|
void draw_status(void);
|
|
void create_windows(void);
|
|
void append_mon(const char *text, int len, int attr);
|
|
void append_out(const char *text, int len, int attr);
|
|
void draw_input(void);
|
|
void send_trace_options(void);
|
|
void tcp_disconnect(void);
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Config
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
/* Config file is ~/.BPQTermTCP.ini — same format as the GTK GUI uses.
|
|
* Sections: [Session 1] for monitor/display settings, [Host1]-[Host4] for hosts.
|
|
* Simple key=value parser; we only write what we read to avoid clobbering
|
|
* unknown keys the GUI may have written. */
|
|
|
|
static char cfg_path[PATH_MAX];
|
|
|
|
static void cfg_path_init(void)
|
|
{
|
|
if (cfg_path[0]) return;
|
|
char *home = getenv("HOME");
|
|
if (!home) home = ".";
|
|
snprintf(cfg_path, PATH_MAX, "%s/.BPQTermTCP.ini", home);
|
|
}
|
|
|
|
/* Trim trailing whitespace/newline in place */
|
|
static void trim_nl(char *s)
|
|
{
|
|
int n = (int)strlen(s);
|
|
while (n > 0 && (s[n-1] == '\n' || s[n-1] == '\r' || s[n-1] == ' ')) s[--n] = '\0';
|
|
}
|
|
|
|
void load_config(void)
|
|
{
|
|
cfg_path_init();
|
|
FILE *fp = fopen(cfg_path, "r");
|
|
if (!fp) {
|
|
/* Try legacy ~/.termtcprc for migration */
|
|
char legacy[PATH_MAX];
|
|
char *home = getenv("HOME");
|
|
if (!home) home = ".";
|
|
snprintf(legacy, PATH_MAX, "%s/.termtcprc", home);
|
|
fp = fopen(legacy, "r");
|
|
if (!fp) return;
|
|
/* Parse old simple format */
|
|
char line[512];
|
|
num_hosts = 0;
|
|
while (fgets(line, sizeof(line), fp) && num_hosts < MAX_HOSTS) {
|
|
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
|
|
HostEntry *h = &hosts[num_hosts];
|
|
int n = sscanf(line, "%99s %9s %79s %79s %49[^\n]",
|
|
h->host, h->port, h->username, h->password, h->name);
|
|
if (n < 2) continue;
|
|
if (n == 3) { strncpy(h->name, h->username, 49); h->username[0] = h->password[0] = '\0'; }
|
|
if (n == 4) { strncpy(h->name, h->password, 49); h->password[0] = '\0'; }
|
|
num_hosts++;
|
|
}
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
|
|
char line[512];
|
|
char section[64] = "";
|
|
int host_idx = -1;
|
|
num_hosts = 0;
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
trim_nl(line);
|
|
if (line[0] == '#' || line[0] == ';' || line[0] == '\0') continue;
|
|
|
|
/* Section header */
|
|
if (line[0] == '[') {
|
|
char *e = strchr(line, ']');
|
|
if (e) { *e = '\0'; strncpy(section, line+1, 63); section[63] = '\0'; }
|
|
host_idx = -1;
|
|
if (strncmp(section, "Host", 4) == 0) {
|
|
int n = atoi(section + 4);
|
|
if (n >= 1 && n <= MAX_HOSTS) {
|
|
host_idx = n - 1;
|
|
if (host_idx >= num_hosts) {
|
|
/* ensure slot exists */
|
|
while (num_hosts <= host_idx) {
|
|
memset(&hosts[num_hosts], 0, sizeof(HostEntry));
|
|
num_hosts++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* key=value */
|
|
char *eq = strchr(line, '=');
|
|
if (!eq) continue;
|
|
*eq = '\0';
|
|
char *key = line;
|
|
char *val = eq + 1;
|
|
|
|
if (strcmp(section, "Session 1") == 0) {
|
|
if (strcmp(key,"MTX") == 0) mon_tx = atoi(val);
|
|
else if (strcmp(key,"MCOM") == 0) mon_com = atoi(val);
|
|
else if (strcmp(key,"MonNODES") == 0) mon_nodes = atoi(val);
|
|
else if (strcmp(key,"MONColour") == 0) mon_colour = atoi(val);
|
|
else if (strcmp(key,"PortMask") == 0) port_mask = atoi(val);
|
|
else if (strcmp(key,"MUIONLY") == 0) mon_ui_only = atoi(val);
|
|
else if (strcmp(key,"MonPorts") == 0) { /* informational */ }
|
|
/* Port names: PortName1=VHF etc. */
|
|
else if (strncmp(key,"PortName",8) == 0) {
|
|
int p = atoi(key + 8);
|
|
if (p >= 1 && p <= 32) strncpy(port_names[p-1], val, 31);
|
|
}
|
|
} else if (host_idx >= 0 && host_idx < MAX_HOSTS) {
|
|
HostEntry *h = &hosts[host_idx];
|
|
if (strcmp(key,"Host") == 0) strncpy(h->host, val, 99);
|
|
else if (strcmp(key,"Port") == 0) strncpy(h->port, val, 9);
|
|
else if (strcmp(key,"UserName") == 0) strncpy(h->username, val, 79);
|
|
else if (strcmp(key,"Password") == 0) strncpy(h->password, val, 79);
|
|
else if (strcmp(key,"Path") == 0) strncpy(h->name, val, 49);
|
|
}
|
|
}
|
|
fclose(fp);
|
|
|
|
/* Remove trailing empty hosts */
|
|
while (num_hosts > 0 && hosts[num_hosts-1].host[0] == '\0') num_hosts--;
|
|
}
|
|
|
|
void save_config(void)
|
|
{
|
|
cfg_path_init();
|
|
|
|
/* Read existing file so we preserve any keys we don't know about */
|
|
FILE *fp = fopen(cfg_path, "w");
|
|
if (!fp) return;
|
|
|
|
fprintf(fp, "[Session 1]\n");
|
|
fprintf(fp, "MTX=%d\n", mon_tx);
|
|
fprintf(fp, "MCOM=%d\n", mon_com);
|
|
fprintf(fp, "MonNODES=%d\n", mon_nodes);
|
|
fprintf(fp, "MONColour=%d\n", mon_colour);
|
|
fprintf(fp, "PortMask=%d\n", port_mask);
|
|
fprintf(fp, "MUIONLY=%d\n", mon_ui_only);
|
|
fprintf(fp, "MonPorts=%d\n", num_hosts > 0 ? 8 : 1);
|
|
/* Save non-default port names */
|
|
for (int i = 0; i < 32; i++) {
|
|
char def[32]; snprintf(def, 32, "Port %d", i+1);
|
|
if (strcmp(port_names[i], def) != 0)
|
|
fprintf(fp, "PortName%d=%s\n", i+1, port_names[i]);
|
|
}
|
|
|
|
for (int i = 0; i < num_hosts; i++) {
|
|
fprintf(fp, "\n[Host%d]\n", i+1);
|
|
fprintf(fp, "Host=%s\n", hosts[i].host);
|
|
fprintf(fp, "Port=%s\n", hosts[i].port);
|
|
fprintf(fp, "UserName=%s\n", hosts[i].username);
|
|
fprintf(fp, "Password=%s\n", hosts[i].password);
|
|
fprintf(fp, "Path=%s\n", hosts[i].name);
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Logging
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
void open_log(const char *prefix)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm *t = localtime(&now);
|
|
char filename[PATH_MAX];
|
|
snprintf(filename, PATH_MAX, "%s_%04d%02d%02d_%02d%02d%02d.log",
|
|
prefix, t->tm_year+1900, t->tm_mon+1, t->tm_mday,
|
|
t->tm_hour, t->tm_min, t->tm_sec);
|
|
log_file = fopen(filename, "w");
|
|
if (log_file) logging = 1;
|
|
}
|
|
|
|
void close_log(void)
|
|
{
|
|
if (log_file) { fclose(log_file); log_file = NULL; logging = 0; }
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Network
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
int tcp_connect(const char *host, const char *port)
|
|
{
|
|
struct addrinfo hints, *res = NULL;
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
if (getaddrinfo(host, port, &hints, &res) != 0 || !res) return -1;
|
|
|
|
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
|
if (fd < 0) { freeaddrinfo(res); return -1; }
|
|
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
connect(fd, res->ai_addr, res->ai_addrlen);
|
|
freeaddrinfo(res);
|
|
|
|
fd_set wfds;
|
|
struct timeval tv = {5, 0};
|
|
FD_ZERO(&wfds); FD_SET(fd, &wfds);
|
|
if (select(fd+1, NULL, &wfds, NULL, &tv) <= 0) { close(fd); return -1; }
|
|
|
|
int err = 0; socklen_t elen = sizeof(err);
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &elen);
|
|
if (err) { close(fd); return -1; }
|
|
|
|
flags = fcntl(fd, F_GETFL, 0);
|
|
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
|
|
return fd;
|
|
}
|
|
|
|
int tcp_send(const char *data, int len)
|
|
{
|
|
if (sock < 0 || len <= 0) return -1;
|
|
if (logging && log_file) { fwrite(data, 1, len, log_file); fflush(log_file); }
|
|
return send(sock, data, len, 0);
|
|
}
|
|
|
|
void tcp_disconnect(void)
|
|
{
|
|
if (sock >= 0) { close(sock); sock = -1; }
|
|
connected = 0;
|
|
connect_time = 0;
|
|
in_mon_frame = 0;
|
|
mon_flen = 0;
|
|
ports_parse_mode = 0;
|
|
ports_line_len = 0;
|
|
ports_loaded = 0;
|
|
port_seen = 0;
|
|
for (int i = 0; i < 32; i++) snprintf(port_names[i], 32, "Port %d", i + 1);
|
|
draw_status();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* BPQ protocol
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
void send_trace_options(void)
|
|
{
|
|
if (sock < 0) return;
|
|
char buf[80];
|
|
int len = snprintf(buf, sizeof(buf),
|
|
"\\\\\\\\%x %x %x %x %x %x\r",
|
|
port_mask, mon_tx, mon_com,
|
|
mon_nodes, mon_colour, mon_ui_only);
|
|
send(sock, buf, len, 0);
|
|
}
|
|
|
|
/* Same as send_trace_options but appends P8=1, which tells BPQ to call
|
|
* SendPortsForMonitor() — returns port list as 0xFF 0xFF N|num desc|... */
|
|
static void send_trace_options_with_ports(void)
|
|
{
|
|
if (sock < 0) return;
|
|
char buf[80];
|
|
int len = snprintf(buf, sizeof(buf),
|
|
"\\\\\\\\%x %x %x %x %x %x 0 1\r",
|
|
port_mask, mon_tx, mon_com,
|
|
mon_nodes, mon_colour, mon_ui_only);
|
|
send(sock, buf, len, 0);
|
|
}
|
|
|
|
/* Parse the 0xFF 0xFF port-info packet from SendPortsForMonitor.
|
|
* Format (after both 0xFF bytes): "N|portnum desc|portnum desc|..."
|
|
* N = total entries; portnum 0 = Mail Monitor (skipped). */
|
|
static void parse_portinfo(const char *data, int len)
|
|
{
|
|
const char *p = data, *e = data + len;
|
|
int nports = 0;
|
|
while (p < e && *p >= '0' && *p <= '9') nports = nports * 10 + (*p++ - '0');
|
|
if (p >= e || *p != '|') return;
|
|
p++;
|
|
for (int i = 0; i < nports && p < e; i++) {
|
|
int n = 0;
|
|
while (p < e && *p >= '0' && *p <= '9') n = n * 10 + (*p++ - '0');
|
|
if (p >= e || *p != ' ') break;
|
|
p++;
|
|
const char *desc = p;
|
|
while (p < e && *p != '|') p++;
|
|
int dlen = (int)(p - desc);
|
|
if (n >= 1 && n <= 32 && dlen > 0) {
|
|
int copy = dlen < 31 ? dlen : 31;
|
|
memcpy(port_names[n - 1], desc, copy);
|
|
port_names[n - 1][copy] = '\0';
|
|
port_seen |= (1u << (n - 1));
|
|
}
|
|
if (p < e && *p == '|') p++;
|
|
}
|
|
ports_loaded = 1;
|
|
}
|
|
|
|
static int bpq_color_attr(unsigned char color_byte)
|
|
{
|
|
/* Map BPQ Colours[] index (color_byte - 10) to nearest terminal color.
|
|
* Colours[]: 1-4 = dark blues, 5-20 = greens, 21-60 = reds, 61-100 = brighter reds */
|
|
int idx = (int)color_byte - 10;
|
|
if (idx < 0) return COLOR_PAIR(CP_DEFAULT);
|
|
if (idx < 4) return COLOR_PAIR(CP_CYAN); /* dark blues → cyan */
|
|
if (idx < 20) return COLOR_PAIR(CP_GREEN); /* greens */
|
|
if (idx < 40) return COLOR_PAIR(CP_RED); /* dark reds */
|
|
if (idx < 60) return COLOR_PAIR(CP_RED) | A_BOLD; /* medium reds */
|
|
if (idx < 80) return COLOR_PAIR(CP_YELLOW); /* bright reds → yellow so visible */
|
|
return COLOR_PAIR(CP_MAGENTA);
|
|
}
|
|
|
|
/* Scan monitor frame text for "Port=N" and record N in port_seen bitmask.
|
|
* BPQ monitor frames use "Port=N" format (e.g. "KB8PMY>APRS Port=2 <UI>").
|
|
* Port names come from the PORTS session command, not from monitor frames. */
|
|
static void detect_port_number(const char *text, int len)
|
|
{
|
|
/* Skip optional BPQ color prefix (0x1B + color_byte) */
|
|
const char *p = text;
|
|
if (len >= 2 && (unsigned char)p[0] == 0x1B) { p += 2; len -= 2; }
|
|
|
|
for (int i = 0; i + 5 <= len; i++) {
|
|
if ((p[i]=='P'||p[i]=='p') && (p[i+1]=='o'||p[i+1]=='O') &&
|
|
(p[i+2]=='r'||p[i+2]=='R') && (p[i+3]=='t'||p[i+3]=='T') &&
|
|
p[i+4]=='=') {
|
|
int n = 0, j = i + 5;
|
|
while (j < len && p[j] >= '0' && p[j] <= '9')
|
|
n = n * 10 + (p[j++] - '0');
|
|
if (n >= 1 && n <= 32)
|
|
port_seen |= (1 << (n-1));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Parse one complete session output line for the PORTS command response.
|
|
* Expects: "} Ports" → start of list
|
|
* " N name..." → port entry (leading 2 spaces + number + name) */
|
|
static void parse_session_line(const char *line, int len)
|
|
{
|
|
/* Trim trailing whitespace */
|
|
while (len > 0 && (unsigned char)line[len-1] <= ' ') len--;
|
|
if (len == 0) return;
|
|
|
|
if (!ports_parse_mode) {
|
|
/* Detect "} Ports" header (case-insensitive) */
|
|
for (int i = 0; i + 6 < len; i++) {
|
|
if (line[i] == '}' && line[i+1] == ' ' &&
|
|
(line[i+2]=='P'||line[i+2]=='p') &&
|
|
(line[i+3]=='o'||line[i+3]=='O') &&
|
|
(line[i+4]=='r'||line[i+4]=='R') &&
|
|
(line[i+5]=='t'||line[i+5]=='T') &&
|
|
(line[i+6]=='s'||line[i+6]=='S')) {
|
|
ports_parse_mode = 1;
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* In ports mode: parse " N name" lines */
|
|
if (len >= 4 && line[0]==' ' && line[1]==' ' && line[2]>='1' && line[2]<='9') {
|
|
int n = 0, j = 2;
|
|
while (j < len && line[j] >= '0' && line[j] <= '9')
|
|
n = n * 10 + (line[j++] - '0');
|
|
if (j < len && line[j] == ' ') j++;
|
|
/* Name: from j to len, trim trailing spaces */
|
|
int ne = len;
|
|
while (ne > j && (unsigned char)line[ne-1] <= ' ') ne--;
|
|
int nlen = ne - j;
|
|
if (n >= 1 && n <= 32 && nlen > 0) {
|
|
int copy = nlen < 31 ? nlen : 31;
|
|
memcpy(port_names[n-1], line + j, copy);
|
|
port_names[n-1][copy] = '\0';
|
|
port_seen |= (1 << (n-1));
|
|
}
|
|
} else {
|
|
if (ports_parse_mode) ports_loaded = 1; /* list ended cleanly */
|
|
ports_parse_mode = 0;
|
|
}
|
|
}
|
|
|
|
/* Feed one session output byte through the line accumulator. */
|
|
static void feed_session_byte(unsigned char c)
|
|
{
|
|
if (c == '\r' || c == '\n') {
|
|
if (ports_line_len > 0) {
|
|
parse_session_line(ports_line_buf, ports_line_len);
|
|
ports_line_len = 0;
|
|
}
|
|
} else if (c >= 32 && c < 128 && ports_line_len < 255) {
|
|
ports_line_buf[ports_line_len++] = (char)c;
|
|
}
|
|
}
|
|
|
|
void bpq_process(const char *buf, int len)
|
|
{
|
|
const char *p = buf;
|
|
const char *end = buf + len;
|
|
|
|
while (p < end) {
|
|
if (!in_mon_frame) {
|
|
const char *ff = memchr(p, 0xFF, end - p);
|
|
if (!ff) {
|
|
/* All remaining bytes are session output */
|
|
for (const char *q = p; q < end; q++)
|
|
feed_session_byte((unsigned char)*q);
|
|
append_out(p, (int)(end - p), COLOR_PAIR(CP_DEFAULT));
|
|
break;
|
|
}
|
|
if (ff > p) {
|
|
/* Session output before the next monitor frame */
|
|
for (const char *q = p; q < ff; q++)
|
|
feed_session_byte((unsigned char)*q);
|
|
append_out(p, (int)(ff - p), COLOR_PAIR(CP_DEFAULT));
|
|
}
|
|
/* 0xFF 0xFF = SendPortsForMonitor response; 0xFF 0x1B = monitor frame */
|
|
p = ff + 1;
|
|
if (p < end && (unsigned char)*p == 0xFF) {
|
|
/* Port info packet — parse in place, consume to end of buffer */
|
|
parse_portinfo(p + 1, (int)(end - p - 1));
|
|
p = end;
|
|
} else {
|
|
in_mon_frame = 1;
|
|
mon_flen = 0;
|
|
}
|
|
} else {
|
|
const char *fe = memchr(p, 0xFE, end - p);
|
|
int seg_len = fe ? (int)(fe - p) : (int)(end - p);
|
|
if (seg_len > 0) {
|
|
detect_port_number(p, seg_len);
|
|
/* Accumulate into the per-frame buffer */
|
|
int room = (int)sizeof(mon_fbuf) - 1 - mon_flen;
|
|
if (room > 0) {
|
|
int add = seg_len < room ? seg_len : room;
|
|
memcpy(mon_fbuf + mon_flen, p, add);
|
|
mon_flen += add;
|
|
}
|
|
}
|
|
if (fe) {
|
|
/* Frame complete: apply client-side NODES filter then display */
|
|
if (mon_flen > 0) {
|
|
mon_fbuf[mon_flen] = '\0';
|
|
/* Detect NODES broadcast by AX.25 destination ">NODES" in header */
|
|
int is_nodes = 0;
|
|
for (int i = 0; i <= mon_flen - 6 && !is_nodes; i++)
|
|
if (memcmp(mon_fbuf + i, ">NODES", 6) == 0) is_nodes = 1;
|
|
if (mon_nodes || !is_nodes)
|
|
append_mon(mon_fbuf, mon_flen, COLOR_PAIR(CP_GREEN));
|
|
}
|
|
mon_flen = 0;
|
|
in_mon_frame = 0;
|
|
p = fe + 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (logging && log_file) { fwrite(buf, 1, len, log_file); fflush(log_file); }
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* ncurses UI
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
void init_colors(void)
|
|
{
|
|
if (!has_colors()) return;
|
|
start_color();
|
|
use_default_colors();
|
|
init_pair(CP_DEFAULT, COLOR_WHITE, COLOR_BLACK);
|
|
init_pair(CP_RED, COLOR_RED, COLOR_BLACK);
|
|
init_pair(CP_GREEN, COLOR_GREEN, COLOR_BLACK);
|
|
init_pair(CP_YELLOW, COLOR_YELLOW, COLOR_BLACK);
|
|
init_pair(CP_CYAN, COLOR_CYAN, COLOR_BLACK);
|
|
init_pair(CP_MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
|
|
init_pair(CP_STATUS, COLOR_BLACK, COLOR_CYAN);
|
|
init_pair(CP_INPUT, COLOR_BLACK, COLOR_WHITE);
|
|
init_pair(CP_SEP, COLOR_CYAN, COLOR_BLACK);
|
|
}
|
|
|
|
|
|
/* Draw a box using Unicode cchar_t — bypasses ACS so it works on any UTF-8 terminal */
|
|
static void draw_box(WINDOW *w)
|
|
{
|
|
cchar_t vt, hz, ul, ur, ll, lr;
|
|
setcchar(&vt, L"│", 0, 0, NULL); /* │ */
|
|
setcchar(&hz, L"─", 0, 0, NULL); /* ─ */
|
|
setcchar(&ul, L"┌", 0, 0, NULL); /* ┌ */
|
|
setcchar(&ur, L"┐", 0, 0, NULL); /* ┐ */
|
|
setcchar(&ll, L"└", 0, 0, NULL); /* └ */
|
|
setcchar(&lr, L"┘", 0, 0, NULL); /* ┘ */
|
|
wborder_set(w, &vt, &vt, &hz, &hz, &ul, &ur, &ll, &lr);
|
|
}
|
|
|
|
/* Fill a 1-row window with plain '-' dashes and an optional centred label.
|
|
* No ACS — works on any terminal. */
|
|
static void fill_sep(WINDOW *w, const char *label)
|
|
{
|
|
if (!w) return;
|
|
werase(w);
|
|
wbkgd(w, COLOR_PAIR(CP_SEP));
|
|
wattron(w, COLOR_PAIR(CP_SEP));
|
|
cchar_t hz;
|
|
setcchar(&hz, L"─", 0, 0, NULL);
|
|
whline_set(w, &hz, COLS);
|
|
if (label && COLS > 4) {
|
|
int llen = (int)strlen(label);
|
|
int col = (COLS - llen) / 2;
|
|
if (col < 0) col = 0;
|
|
wattron(w, A_BOLD);
|
|
mvwaddstr(w, 0, col, label);
|
|
wattroff(w, A_BOLD);
|
|
}
|
|
wattroff(w, COLOR_PAIR(CP_SEP));
|
|
wrefresh(w);
|
|
}
|
|
|
|
void create_windows(void)
|
|
{
|
|
if (status_win) { delwin(status_win); status_win = NULL; }
|
|
if (out_win) { delwin(out_win); out_win = NULL; }
|
|
if (sep_win) { delwin(sep_win); sep_win = NULL; }
|
|
if (mon_win) { delwin(mon_win); mon_win = NULL; }
|
|
if (sep2_win) { delwin(sep2_win); sep2_win = NULL; }
|
|
if (input_win) { delwin(input_win); input_win = NULL; }
|
|
|
|
/* overhead = status(1) + sep2(1) + input(INPUT_OUTER)
|
|
* + sep(1) if monitor visible */
|
|
int overhead = 1 + 1 + INPUT_OUTER + (mon_visible ? 1 : 0);
|
|
int content_rows = LINES - overhead;
|
|
if (content_rows < 4) content_rows = 4;
|
|
|
|
int row = 0;
|
|
|
|
/* Status bar */
|
|
status_win = newwin(1, COLS, row, 0);
|
|
wbkgd(status_win, COLOR_PAIR(CP_STATUS));
|
|
leaveok(status_win, TRUE);
|
|
row++;
|
|
|
|
if (mon_visible) {
|
|
int mon_h = content_rows / 2;
|
|
int out_h = content_rows - mon_h;
|
|
|
|
mon_win = newwin(mon_h, COLS, row, 0);
|
|
wbkgd(mon_win, COLOR_PAIR(CP_DEFAULT));
|
|
scrollok(mon_win, TRUE);
|
|
leaveok(mon_win, TRUE);
|
|
row += mon_h;
|
|
|
|
sep_win = newwin(1, COLS, row, 0);
|
|
leaveok(sep_win, TRUE);
|
|
fill_sep(sep_win, " Output ");
|
|
row++;
|
|
|
|
out_win = newwin(out_h, COLS, row, 0);
|
|
wbkgd(out_win, COLOR_PAIR(CP_DEFAULT));
|
|
scrollok(out_win, TRUE);
|
|
leaveok(out_win, TRUE);
|
|
row += out_h;
|
|
} else {
|
|
out_win = newwin(content_rows, COLS, row, 0);
|
|
wbkgd(out_win, COLOR_PAIR(CP_DEFAULT));
|
|
scrollok(out_win, TRUE);
|
|
leaveok(out_win, TRUE);
|
|
row += content_rows;
|
|
}
|
|
|
|
/* Separator above input */
|
|
sep2_win = newwin(1, COLS, row, 0);
|
|
leaveok(sep2_win, TRUE);
|
|
fill_sep(sep2_win, " Input ");
|
|
row++;
|
|
|
|
/* Input area — no border, just open lines */
|
|
input_win = newwin(INPUT_OUTER, COLS, row, 0);
|
|
wbkgd(input_win, COLOR_PAIR(CP_INPUT));
|
|
leaveok(input_win, FALSE);
|
|
keypad(input_win, TRUE);
|
|
nodelay(input_win, TRUE);
|
|
}
|
|
|
|
void draw_status(void)
|
|
{
|
|
if (!status_win) return;
|
|
werase(status_win);
|
|
wattron(status_win, COLOR_PAIR(CP_STATUS) | A_BOLD);
|
|
|
|
if (connected && current_host >= 0) {
|
|
int elapsed = (int)(time(NULL) - connect_time);
|
|
mvwprintw(status_win, 0, 0,
|
|
" TermTCP v%s | %s | %02d:%02d | Mon:%s%s | Ctrl+A=Menu",
|
|
VERSION,
|
|
hosts[current_host].name,
|
|
elapsed / 60, elapsed % 60,
|
|
mon_visible ? "ON" : "OFF",
|
|
logging ? " | LOG" : "");
|
|
} else {
|
|
mvwprintw(status_win, 0, 0,
|
|
" TermTCP v%s | Not connected | Mon:%s | Ctrl+A=Menu",
|
|
VERSION, mon_visible ? "ON" : "OFF");
|
|
}
|
|
|
|
wattroff(status_win, COLOR_PAIR(CP_STATUS) | A_BOLD);
|
|
wrefresh(status_win);
|
|
}
|
|
|
|
/* Render stored lines from a ring buffer into an ncurses window.
|
|
* scroll_off=0 shows the most recent lines (live); >0 scrolls back. */
|
|
static void scrl_draw_window(WINDOW *win, ScrlLine *buf, int count, int head,
|
|
int scroll_off)
|
|
{
|
|
if (!win) return;
|
|
scrollok(win, FALSE); /* disable scroll during batch redraw — prevents losing lines */
|
|
werase(win);
|
|
if (count == 0) { scrollok(win, TRUE); wrefresh(win); return; }
|
|
|
|
int win_h, win_w;
|
|
getmaxyx(win, win_h, win_w);
|
|
(void)win_w;
|
|
|
|
int last = count - 1 - scroll_off;
|
|
if (last < 0) { wrefresh(win); return; }
|
|
int first = last - win_h + 1;
|
|
if (first < 0) first = 0;
|
|
|
|
for (int i = first; i <= last; i++) {
|
|
int slot = (head - count + i + SCRL_MAX) % SCRL_MAX;
|
|
const ScrlLine *sl = &buf[slot];
|
|
const char *p = sl->data;
|
|
const char *end = p + sl->len;
|
|
int cur = sl->base_attr;
|
|
wattron(win, cur);
|
|
while (p < end) {
|
|
unsigned char c = (unsigned char)*p;
|
|
if (c == 0x1B && (p + 1) < end) {
|
|
wattroff(win, cur);
|
|
cur = bpq_color_attr((unsigned char)p[1]);
|
|
wattron(win, cur);
|
|
p += 2;
|
|
} else if (c >= 32 && c < 127) { waddch(win, c); p++; }
|
|
else { p++; }
|
|
}
|
|
wattroff(win, cur);
|
|
if (i < last) waddch(win, '\n');
|
|
}
|
|
/* Move cursor to start of next blank row so live appends begin on a fresh line */
|
|
int drawn = last - first + 1;
|
|
if (drawn < win_h) wmove(win, drawn, 0);
|
|
scrollok(win, TRUE); /* re-enable for live appends */
|
|
wrefresh(win);
|
|
}
|
|
|
|
void append_mon(const char *text, int len, int attr)
|
|
{
|
|
if (!mon_visible || len <= 0) return;
|
|
|
|
const char *p = text;
|
|
const char *end = text + len;
|
|
|
|
mon_part_cur = attr;
|
|
if (mon_part_len == 0) mon_part_base = attr;
|
|
|
|
if (mon_win && mon_scroll == 0)
|
|
wattron(mon_win, mon_part_cur);
|
|
|
|
while (p < end) {
|
|
unsigned char c = (unsigned char)*p;
|
|
if (c == 0x1B && (p + 1) < end) {
|
|
int na = bpq_color_attr((unsigned char)p[1]);
|
|
if (mon_part_len + 2 < SCRL_W) {
|
|
mon_part[mon_part_len++] = (char)0x1B;
|
|
mon_part[mon_part_len++] = p[1];
|
|
}
|
|
if (mon_win && mon_scroll == 0) {
|
|
wattroff(mon_win, mon_part_cur);
|
|
wattron(mon_win, na);
|
|
}
|
|
mon_part_cur = na;
|
|
p += 2;
|
|
} else if (c == '\r' || c == '\n') {
|
|
ScrlLine *sl = &mon_sbuf[mon_sbuf_head];
|
|
memcpy(sl->data, mon_part, mon_part_len);
|
|
sl->len = mon_part_len;
|
|
sl->base_attr = mon_part_base;
|
|
mon_sbuf_head = (mon_sbuf_head + 1) % SCRL_MAX;
|
|
if (mon_sbuf_count < SCRL_MAX) mon_sbuf_count++;
|
|
mon_part_len = 0;
|
|
mon_part_base = mon_part_cur;
|
|
if (mon_win && mon_scroll == 0)
|
|
waddch(mon_win, '\n');
|
|
p++;
|
|
} else if (c >= 32 && c < 127) {
|
|
if (mon_part_len < SCRL_W - 1)
|
|
mon_part[mon_part_len++] = (char)c;
|
|
if (mon_win && mon_scroll == 0)
|
|
waddch(mon_win, c);
|
|
p++;
|
|
} else { p++; }
|
|
}
|
|
|
|
if (mon_win && mon_scroll == 0) {
|
|
wattroff(mon_win, mon_part_cur);
|
|
wrefresh(mon_win);
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
}
|
|
|
|
void append_out(const char *text, int len, int attr)
|
|
{
|
|
if (len <= 0) return;
|
|
|
|
const char *p = text;
|
|
const char *end = text + len;
|
|
|
|
out_part_cur = attr;
|
|
if (out_part_len == 0) out_part_base = attr;
|
|
|
|
if (out_win && out_scroll == 0)
|
|
wattron(out_win, out_part_cur);
|
|
|
|
while (p < end) {
|
|
unsigned char c = (unsigned char)*p;
|
|
if (c == 0x1B && (p + 1) < end) {
|
|
int na = bpq_color_attr((unsigned char)p[1]);
|
|
if (out_part_len + 2 < SCRL_W) {
|
|
out_part[out_part_len++] = (char)0x1B;
|
|
out_part[out_part_len++] = p[1];
|
|
}
|
|
if (out_win && out_scroll == 0) {
|
|
wattroff(out_win, out_part_cur);
|
|
wattron(out_win, na);
|
|
}
|
|
out_part_cur = na;
|
|
p += 2;
|
|
} else if (c == '\r' || c == '\n') {
|
|
ScrlLine *sl = &out_sbuf[out_sbuf_head];
|
|
memcpy(sl->data, out_part, out_part_len);
|
|
sl->len = out_part_len;
|
|
sl->base_attr = out_part_base;
|
|
out_sbuf_head = (out_sbuf_head + 1) % SCRL_MAX;
|
|
if (out_sbuf_count < SCRL_MAX) out_sbuf_count++;
|
|
out_part_len = 0;
|
|
out_part_base = out_part_cur;
|
|
if (out_win && out_scroll == 0)
|
|
waddch(out_win, '\n');
|
|
p++;
|
|
} else if (c >= 32 && c < 127) {
|
|
if (out_part_len < SCRL_W - 1)
|
|
out_part[out_part_len++] = (char)c;
|
|
if (out_win && out_scroll == 0)
|
|
waddch(out_win, c);
|
|
p++;
|
|
} else { p++; }
|
|
}
|
|
|
|
if (out_win && out_scroll == 0) {
|
|
wattroff(out_win, out_part_cur);
|
|
wrefresh(out_win);
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
}
|
|
|
|
void draw_input(void)
|
|
{
|
|
if (!input_win) return;
|
|
|
|
/* Refresh separator above */
|
|
if (sep2_win) fill_sep(sep2_win, " Input ");
|
|
|
|
werase(input_win);
|
|
wattron(input_win, COLOR_PAIR(CP_INPUT));
|
|
|
|
/* Prompt on first row */
|
|
mvwaddstr(input_win, 0, 0, "> ");
|
|
|
|
/* Buffer content — wraps at COLS, '> ' prefix is 2 chars */
|
|
for (int i = 0; i < input_len; i++) {
|
|
int pos = 2 + i;
|
|
int r = pos / COLS;
|
|
int c = pos % COLS;
|
|
if (r >= INPUT_OUTER) break;
|
|
mvwaddch(input_win, r, c, (unsigned char)input_buf[i]);
|
|
}
|
|
|
|
/* Cursor position */
|
|
int cpos = 2 + input_len;
|
|
int cr = cpos / COLS;
|
|
int cc = cpos % COLS;
|
|
if (cr < INPUT_OUTER) wmove(input_win, cr, cc);
|
|
|
|
wattroff(input_win, COLOR_PAIR(CP_INPUT));
|
|
wrefresh(input_win);
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* In-program config editor (Ctrl+A -> O)
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void scrl_redraw_out(void)
|
|
{
|
|
scrl_draw_window(out_win, out_sbuf, out_sbuf_count, out_sbuf_head, out_scroll);
|
|
}
|
|
|
|
static void scrl_redraw_mon(void)
|
|
{
|
|
if (!mon_visible) return;
|
|
scrl_draw_window(mon_win, mon_sbuf, mon_sbuf_count, mon_sbuf_head, mon_scroll);
|
|
}
|
|
|
|
static void update_sep_labels(void)
|
|
{
|
|
if (sep_win) {
|
|
if (out_scroll > 0)
|
|
fill_sep(sep_win, " Output [scrolled - PgDn=live] ");
|
|
else
|
|
fill_sep(sep_win, " Output ");
|
|
}
|
|
}
|
|
|
|
static void refresh_all(void)
|
|
{
|
|
draw_status();
|
|
update_sep_labels();
|
|
if (sep2_win) fill_sep(sep2_win, " Input ");
|
|
scrl_redraw_out();
|
|
scrl_redraw_mon();
|
|
draw_input();
|
|
}
|
|
|
|
/* Full background repaint using redrawwin() — safe alternative to
|
|
* clearok(curscr) which resets ACS mode and breaks box() borders. */
|
|
static void force_repaint(void)
|
|
{
|
|
if (status_win) redrawwin(status_win);
|
|
if (sep_win) redrawwin(sep_win);
|
|
if (out_win) redrawwin(out_win);
|
|
if (mon_win) redrawwin(mon_win);
|
|
if (sep2_win) redrawwin(sep2_win);
|
|
if (input_win) redrawwin(input_win);
|
|
refresh_all();
|
|
}
|
|
|
|
static void config_menu(void)
|
|
{
|
|
int mh = 18, mw = 64;
|
|
int my = (LINES - mh) / 2;
|
|
int mx = (COLS - mw) / 2;
|
|
if (my < 0) my = 0;
|
|
if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
|
|
const char *labels[] = { "Host:", "Port:", "User:", "Pass:", "Name:" };
|
|
const int field_y[] = { 4, 6, 8, 10, 12 };
|
|
const int max_len[] = { 99, 9, 79, 79, 49 };
|
|
char fields[5][100];
|
|
|
|
int cur_host = (current_host >= 0 && current_host < num_hosts) ? current_host : 0;
|
|
int cur_field = 0;
|
|
|
|
if (num_hosts == 0) { memset(&hosts[0], 0, sizeof(HostEntry)); num_hosts = 1; }
|
|
|
|
#define CF_COMMIT() do { \
|
|
strncpy(hosts[cur_host].host, fields[0], 99); hosts[cur_host].host[99] =0; \
|
|
strncpy(hosts[cur_host].port, fields[1], 9); hosts[cur_host].port[9] =0; \
|
|
strncpy(hosts[cur_host].username, fields[2], 79); hosts[cur_host].username[79]=0; \
|
|
strncpy(hosts[cur_host].password, fields[3], 79); hosts[cur_host].password[79]=0; \
|
|
strncpy(hosts[cur_host].name, fields[4], 49); hosts[cur_host].name[49] =0; \
|
|
} while(0)
|
|
|
|
int running = 1;
|
|
while (running) {
|
|
if (cur_host >= num_hosts) cur_host = num_hosts - 1;
|
|
if (cur_host < 0) cur_host = 0;
|
|
|
|
strncpy(fields[0], hosts[cur_host].host, 99); fields[0][99]=0;
|
|
strncpy(fields[1], hosts[cur_host].port, 9); fields[1][9] =0;
|
|
strncpy(fields[2], hosts[cur_host].username, 79); fields[2][79]=0;
|
|
strncpy(fields[3], hosts[cur_host].password, 79); fields[3][79]=0;
|
|
strncpy(fields[4], hosts[cur_host].name, 49); fields[4][49]=0;
|
|
|
|
werase(w); draw_box(w);
|
|
mvwprintw(w, 0, (mw-14)/2, " Config Hosts ");
|
|
mvwprintw(w, 2, 2, "Host %d of %d PgUp/PgDn=switch N=new D=delete",
|
|
cur_host+1, num_hosts);
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
mvwprintw(w, field_y[i], 2, "%-6s", labels[i]);
|
|
if (i == cur_field) wattron(w, A_REVERSE);
|
|
mvwprintw(w, field_y[i], 9, "%-52s", fields[i]);
|
|
if (i == cur_field) wattroff(w, A_REVERSE);
|
|
}
|
|
|
|
mvwprintw(w, mh-4, 2, "Tab / Up / Down = move between fields");
|
|
mvwprintw(w, mh-3, 2, "Enter / F1 = save & close Esc / Q = cancel");
|
|
mvwprintw(w, mh-2, 2, "Backspace = delete char");
|
|
|
|
int flen = (int)strlen(fields[cur_field]);
|
|
wmove(w, field_y[cur_field], 9 + flen);
|
|
wrefresh(w);
|
|
|
|
int ch = wgetch(w);
|
|
switch (ch) {
|
|
case KEY_PPAGE:
|
|
CF_COMMIT();
|
|
if (cur_host > 0) { cur_host--; cur_field = 0; }
|
|
break;
|
|
case KEY_NPAGE:
|
|
CF_COMMIT();
|
|
if (cur_host < num_hosts-1) { cur_host++; cur_field = 0; }
|
|
break;
|
|
case 'N': case 'n':
|
|
if (num_hosts < MAX_HOSTS) {
|
|
CF_COMMIT();
|
|
memset(&hosts[num_hosts], 0, sizeof(HostEntry));
|
|
num_hosts++; cur_host = num_hosts-1; cur_field = 0;
|
|
}
|
|
break;
|
|
case 'D': case 'd':
|
|
if (num_hosts > 1) {
|
|
for (int i = cur_host; i < num_hosts-1; i++) hosts[i] = hosts[i+1];
|
|
num_hosts--;
|
|
if (cur_host >= num_hosts) cur_host = num_hosts-1;
|
|
}
|
|
break;
|
|
case '\t': case KEY_DOWN:
|
|
CF_COMMIT(); cur_field = (cur_field + 1) % 5;
|
|
break;
|
|
case KEY_UP:
|
|
CF_COMMIT(); cur_field = (cur_field + 4) % 5;
|
|
break;
|
|
case '\r': case '\n': case KEY_ENTER: case KEY_F(1):
|
|
CF_COMMIT(); save_config(); running = 0;
|
|
break;
|
|
case 27: case 'Q': case 'q':
|
|
running = 0;
|
|
break;
|
|
case KEY_BACKSPACE: case 127: case '\b': {
|
|
int fl = (int)strlen(fields[cur_field]);
|
|
if (fl > 0) { fields[cur_field][fl-1] = '\0'; CF_COMMIT(); }
|
|
break;
|
|
}
|
|
default:
|
|
if (ch >= 32 && ch < 127) {
|
|
int fl = (int)strlen(fields[cur_field]);
|
|
if (fl < max_len[cur_field]-1) {
|
|
fields[cur_field][fl] = (char)ch;
|
|
fields[cur_field][fl+1] = '\0';
|
|
CF_COMMIT();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
#undef CF_COMMIT
|
|
|
|
delwin(w);
|
|
force_repaint();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Multi-line textarea editor (used by the General Message form)
|
|
*
|
|
* Operates on an IcsMessage's msg_lines[]/msg_nlines directly.
|
|
* Enter splits/creates a new logical line, arrows move the cursor,
|
|
* Backspace joins with the previous line at column 0.
|
|
* w must already be sized at least ICS_MSG_ROWS+2 x ICS_MSG_COLS+2
|
|
* (the box border eats one row/col on each side).
|
|
* Returns when the caller's exit key (Tab/Esc handled by caller via
|
|
* the return value) is pressed; ch_out receives that key.
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
typedef struct {
|
|
char (*lines)[ICS_MSG_LINELEN];
|
|
int *nlines;
|
|
int cur_line;
|
|
int cur_col;
|
|
int top_line; /* first visible line (vertical scroll) */
|
|
} TextArea;
|
|
|
|
static void ta_clamp(TextArea *ta, int rows)
|
|
{
|
|
if (*ta->nlines < 1) *ta->nlines = 1;
|
|
if (ta->cur_line >= *ta->nlines) ta->cur_line = *ta->nlines - 1;
|
|
if (ta->cur_line < 0) ta->cur_line = 0;
|
|
int llen = (int)strlen(ta->lines[ta->cur_line]);
|
|
if (ta->cur_col > llen) ta->cur_col = llen;
|
|
if (ta->cur_col < 0) ta->cur_col = 0;
|
|
if (ta->cur_line < ta->top_line) ta->top_line = ta->cur_line;
|
|
if (ta->cur_line >= ta->top_line + rows) ta->top_line = ta->cur_line - rows + 1;
|
|
if (ta->top_line < 0) ta->top_line = 0;
|
|
}
|
|
|
|
/* Draws the textarea inside window w at the given inner origin/size
|
|
* (caller already drew the surrounding box/labels). */
|
|
static void ta_draw(WINDOW *w, TextArea *ta, int oy, int ox, int rows, int cols)
|
|
{
|
|
for (int r = 0; r < rows; r++) {
|
|
int line_idx = ta->top_line + r;
|
|
mvwprintw(w, oy + r, ox, "%-*s", cols, ""); /* clear row */
|
|
if (line_idx < *ta->nlines) {
|
|
const char *s = ta->lines[line_idx];
|
|
int slen = (int)strlen(s);
|
|
int show = slen < cols ? slen : cols;
|
|
mvwaddnstr(w, oy + r, ox, s, show);
|
|
}
|
|
}
|
|
int crow = oy + (ta->cur_line - ta->top_line);
|
|
int ccol = ox + ta->cur_col;
|
|
wmove(w, crow, ccol);
|
|
}
|
|
|
|
/* Handles one keystroke. Returns 0 to keep editing, or the raw key
|
|
* code that the caller should treat as an "exit/switch focus" key
|
|
* (currently: '\t' and 27/Esc; caller decides what those mean).
|
|
* cols = visible display width of the textarea; used for auto word-wrap. */
|
|
static int ta_handle_key(TextArea *ta, int ch, int rows, int cols)
|
|
{
|
|
switch (ch) {
|
|
case '\t':
|
|
case 27:
|
|
return ch;
|
|
|
|
case KEY_UP:
|
|
ta->cur_line--;
|
|
break;
|
|
case KEY_DOWN:
|
|
ta->cur_line++;
|
|
break;
|
|
case KEY_LEFT:
|
|
if (ta->cur_col > 0) ta->cur_col--;
|
|
else if (ta->cur_line > 0) {
|
|
ta->cur_line--;
|
|
ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
|
|
}
|
|
break;
|
|
case KEY_RIGHT: {
|
|
int llen = (int)strlen(ta->lines[ta->cur_line]);
|
|
if (ta->cur_col < llen) ta->cur_col++;
|
|
else if (ta->cur_line < *ta->nlines - 1) {
|
|
ta->cur_line++;
|
|
ta->cur_col = 0;
|
|
}
|
|
break;
|
|
}
|
|
case KEY_HOME:
|
|
ta->cur_col = 0;
|
|
break;
|
|
case KEY_END:
|
|
ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
|
|
break;
|
|
|
|
case '\r': case '\n': case KEY_ENTER: {
|
|
if (*ta->nlines >= ICS_MSG_LINES) break; /* full */
|
|
/* Split current line at cursor into a new line below */
|
|
char *cur = ta->lines[ta->cur_line];
|
|
char tail[ICS_MSG_LINELEN];
|
|
strncpy(tail, cur + ta->cur_col, ICS_MSG_LINELEN - 1);
|
|
tail[ICS_MSG_LINELEN - 1] = '\0';
|
|
cur[ta->cur_col] = '\0';
|
|
for (int i = *ta->nlines; i > ta->cur_line + 1; i--)
|
|
memcpy(ta->lines[i], ta->lines[i-1], ICS_MSG_LINELEN);
|
|
strncpy(ta->lines[ta->cur_line + 1], tail, ICS_MSG_LINELEN - 1);
|
|
ta->lines[ta->cur_line + 1][ICS_MSG_LINELEN - 1] = '\0';
|
|
(*ta->nlines)++;
|
|
ta->cur_line++;
|
|
ta->cur_col = 0;
|
|
break;
|
|
}
|
|
|
|
case KEY_BACKSPACE: case 127: case '\b':
|
|
if (ta->cur_col > 0) {
|
|
char *cur = ta->lines[ta->cur_line];
|
|
int len = (int)strlen(cur);
|
|
memmove(cur + ta->cur_col - 1, cur + ta->cur_col, len - ta->cur_col + 1);
|
|
ta->cur_col--;
|
|
} else if (ta->cur_line > 0) {
|
|
/* Join with previous line */
|
|
char *prev = ta->lines[ta->cur_line - 1];
|
|
char *cur = ta->lines[ta->cur_line];
|
|
int plen = (int)strlen(prev);
|
|
int clen = (int)strlen(cur);
|
|
if (plen + clen < ICS_MSG_LINELEN - 1) {
|
|
memcpy(prev + plen, cur, clen + 1);
|
|
for (int i = ta->cur_line; i < *ta->nlines - 1; i++)
|
|
memcpy(ta->lines[i], ta->lines[i+1], ICS_MSG_LINELEN);
|
|
(*ta->nlines)--;
|
|
ta->cur_line--;
|
|
ta->cur_col = plen;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DC: { /* Delete key: remove char at cursor */
|
|
char *cur = ta->lines[ta->cur_line];
|
|
int len = (int)strlen(cur);
|
|
if (ta->cur_col < len) {
|
|
memmove(cur + ta->cur_col, cur + ta->cur_col + 1, len - ta->cur_col);
|
|
} else if (ta->cur_line < *ta->nlines - 1) {
|
|
char *next = ta->lines[ta->cur_line + 1];
|
|
int nlen = (int)strlen(next);
|
|
if (len + nlen < ICS_MSG_LINELEN - 1) {
|
|
memcpy(cur + len, next, nlen + 1);
|
|
for (int i = ta->cur_line + 1; i < *ta->nlines - 1; i++)
|
|
memcpy(ta->lines[i], ta->lines[i+1], ICS_MSG_LINELEN);
|
|
(*ta->nlines)--;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
if (ch >= 32 && ch < 256) {
|
|
char *cur = ta->lines[ta->cur_line];
|
|
int len = (int)strlen(cur);
|
|
/* Auto word-wrap when the visible line width is reached */
|
|
if (len >= cols - 1 && *ta->nlines < ICS_MSG_LINES) {
|
|
int brk = len - 1;
|
|
while (brk > 0 && cur[brk] != ' ') brk--;
|
|
if (brk == 0) brk = len; /* no space: hard break at end */
|
|
int skip = (brk < len && cur[brk] == ' ') ? 1 : 0;
|
|
char tail[ICS_MSG_LINELEN];
|
|
strncpy(tail, cur + brk + skip, ICS_MSG_LINELEN - 1);
|
|
tail[ICS_MSG_LINELEN - 1] = '\0';
|
|
cur[brk] = '\0';
|
|
for (int i = *ta->nlines; i > ta->cur_line + 1; i--)
|
|
memcpy(ta->lines[i], ta->lines[i-1], ICS_MSG_LINELEN);
|
|
strncpy(ta->lines[ta->cur_line + 1], tail, ICS_MSG_LINELEN - 1);
|
|
ta->lines[ta->cur_line + 1][ICS_MSG_LINELEN - 1] = '\0';
|
|
(*ta->nlines)++;
|
|
ta->cur_line++;
|
|
ta->cur_col = (int)strlen(ta->lines[ta->cur_line]);
|
|
cur = ta->lines[ta->cur_line];
|
|
}
|
|
len = (int)strlen(cur);
|
|
if (len < ICS_MSG_LINELEN - 1 && ta->cur_col <= len) {
|
|
memmove(cur + ta->cur_col + 1, cur + ta->cur_col, len - ta->cur_col + 1);
|
|
cur[ta->cur_col] = (char)ch;
|
|
ta->cur_col++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
ta_clamp(ta, rows);
|
|
return 0;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Monitor options (Ctrl+A -> M)
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
/* Returns 'P' to switch to port page, 0 to close */
|
|
static int mon_options_page(void)
|
|
{
|
|
static const char *labels[] = {
|
|
"Monitor TX",
|
|
"Monitor Supervisor",
|
|
"Monitor Nodes",
|
|
"Monitor UI Only",
|
|
"Enable Colour",
|
|
"Show Monitor Pane",
|
|
};
|
|
int *vals[] = { &mon_tx, &mon_com, &mon_nodes, &mon_ui_only, &mon_colour, &mon_visible };
|
|
const int N = 6;
|
|
int mh = N + 5, mw = 46;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
force_repaint();
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
|
|
int cur = 0;
|
|
for (;;) {
|
|
werase(w);
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 18) / 2, " Monitor Options ");
|
|
for (int i = 0; i < N; i++) {
|
|
if (i == cur) wattron(w, A_REVERSE);
|
|
mvwprintw(w, 2 + i, 3, "[%c] %s", *vals[i] ? 'X' : ' ', labels[i]);
|
|
if (i == cur) wattroff(w, A_REVERSE);
|
|
}
|
|
mvwprintw(w, mh - 3, 3, "Up/Down=move Space=toggle");
|
|
mvwprintw(w, mh - 2, 3, "Tab=Port Select Esc/Q=close");
|
|
wrefresh(w);
|
|
|
|
int ch = wgetch(w);
|
|
switch (ch) {
|
|
case KEY_UP: cur = (cur - 1 + N) % N; break;
|
|
case KEY_DOWN: cur = (cur + 1) % N; break;
|
|
case '\t':
|
|
delwin(w);
|
|
return 'P';
|
|
case ' ': case '\r': case '\n': case KEY_ENTER:
|
|
if (cur == 5) {
|
|
mon_visible = !mon_visible;
|
|
save_config();
|
|
delwin(w);
|
|
create_windows();
|
|
force_repaint();
|
|
return 0;
|
|
}
|
|
*vals[cur] = !(*vals[cur]);
|
|
send_trace_options();
|
|
save_config();
|
|
break;
|
|
case 27: case 'Q': case 'q':
|
|
delwin(w);
|
|
force_repaint();
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Returns 'O' to switch to options page, 0 to close */
|
|
static int mon_ports_page(void)
|
|
{
|
|
int port_list[32], n_ports = 0;
|
|
for (int b = 0; b < 32; b++)
|
|
if (port_seen & (1 << b)) port_list[n_ports++] = b + 1;
|
|
if (n_ports == 0)
|
|
for (int b = 0; b < 8; b++) port_list[n_ports++] = b + 1;
|
|
|
|
int mh = n_ports + 5; if (mh < 10) mh = 10;
|
|
int mw = 54;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
force_repaint();
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
|
|
int cur = 0;
|
|
for (;;) {
|
|
werase(w);
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 16) / 2, " Port Selection ");
|
|
if (port_seen == 0)
|
|
mvwprintw(w, 2, 3, "(connect to auto-detect ports)");
|
|
for (int i = 0; i < n_ports; i++) {
|
|
int pn = port_list[i];
|
|
if (i == cur) wattron(w, A_REVERSE);
|
|
mvwprintw(w, 2 + i, 3, "[%c] %2d %-32s",
|
|
(port_mask >> (pn - 1)) & 1 ? 'X' : ' ',
|
|
pn, port_names[pn - 1]);
|
|
if (i == cur) wattroff(w, A_REVERSE);
|
|
}
|
|
mvwprintw(w, mh - 3, 3, "Up/Down=move Space=toggle");
|
|
mvwprintw(w, mh - 2, 3, "Tab=Monitor Opts Esc/Q=close");
|
|
wrefresh(w);
|
|
|
|
int ch = wgetch(w);
|
|
switch (ch) {
|
|
case KEY_UP: cur = (cur - 1 + n_ports) % n_ports; break;
|
|
case KEY_DOWN: cur = (cur + 1) % n_ports; break;
|
|
case '\t':
|
|
delwin(w);
|
|
return 'O';
|
|
case ' ': case '\r': case '\n': case KEY_ENTER:
|
|
port_mask ^= (1 << (port_list[cur] - 1));
|
|
send_trace_options();
|
|
save_config();
|
|
break;
|
|
case 27: case 'Q': case 'q':
|
|
delwin(w);
|
|
force_repaint();
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void monitor_options_menu(void)
|
|
{
|
|
int next = 'O';
|
|
while (next) {
|
|
if (next == 'O') next = mon_options_page();
|
|
else next = mon_ports_page();
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Help overlay
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void show_help(void)
|
|
{
|
|
int mh = 19, mw = 52;
|
|
WINDOW *w = newwin(mh, mw, (LINES-mh)/2, (COLS-mw)/2);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw-8)/2, " Help ");
|
|
|
|
mvwprintw(w, 2, 3, "Ctrl+A Open menu");
|
|
mvwprintw(w, 3, 3, "Ctrl+A C Connect");
|
|
mvwprintw(w, 4, 3, "Ctrl+A D Disconnect");
|
|
mvwprintw(w, 5, 3, "Ctrl+A O Config / host editor");
|
|
mvwprintw(w, 6, 3, "Ctrl+A M Monitor options");
|
|
mvwprintw(w, 7, 3, "Ctrl+A L Toggle log");
|
|
mvwprintw(w, 8, 3, "Ctrl+A F Forms & Check-Ins");
|
|
mvwprintw(w, 9, 3, "Ctrl+A H This help");
|
|
mvwprintw(w, 10, 3, "Ctrl+A Q Quit");
|
|
mvwprintw(w, 12, 3, "Enter Send line to server");
|
|
mvwprintw(w, 13, 3, "Up / Down Input history");
|
|
mvwprintw(w, 14, 3, "PgUp / PgDn Scroll output window");
|
|
mvwprintw(w, 15, 3, "Home / End Scroll monitor window");
|
|
mvwprintw(w, 16, 3, "Backspace Delete char");
|
|
mvwprintw(w, mh-2, 3, "Press any key to close");
|
|
|
|
wrefresh(w);
|
|
nodelay(w, FALSE);
|
|
wgetch(w);
|
|
delwin(w);
|
|
force_repaint();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* ICS-213 General Message (Ctrl+A -> G)
|
|
*
|
|
* F2 always sends over a dedicated background connection (see
|
|
* "Background send connection" below) rather than the main
|
|
* session's socket, so a send never disturbs whatever is happening
|
|
* in the live monitor/output windows.
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
#define BG_PROMPT_TIMEOUT_MS 10000 /* max ms to wait for BBS to stop talking */
|
|
#define BG_QUIET_MS 300 /* silence this long after data = BBS at prompt */
|
|
|
|
/* ── Background send connection ──────────────────────────────
|
|
* F2 always opens its OWN socket for sending, completely separate
|
|
* from the main interactive session's global `sock`/`connected`.
|
|
* This means a send never disturbs whatever you're doing in the
|
|
* main window - no shared connection state, no risk of interleaving
|
|
* a send with whatever you're typing or watching scroll by.
|
|
*
|
|
* Deliberately does NOT call bpq_process(): that function's monitor
|
|
* frame parser keeps state in globals (in_mon_frame, mon_flen,
|
|
* ports_parse_mode, etc.) shared with the main session. Running two
|
|
* sockets' data through one shared parser would corrupt both. The
|
|
* background connection instead does a much simpler raw drain: BPQ
|
|
* monitor frames are bounded by 0xFF/0xFE bytes, so they're just
|
|
* stripped out wholesale rather than decoded, since the background
|
|
* send has no monitor window of its own to display them in anyway.
|
|
*
|
|
* All progress/results are echoed into the same out_win as the main
|
|
* session, prefixed "[BG]" so it's visually distinct from anything
|
|
* happening on the live connection.
|
|
* ──────────────────────────────────────────────────────────── */
|
|
|
|
/* Strips BPQ monitor-frame markers (0xFF ... 0xFE) from raw bytes
|
|
* before treating the rest as plain session text, since the
|
|
* background connection has no monitor window to decode them into.
|
|
* Writes the surviving plain-text bytes into out (caller-sized) and
|
|
* returns how many bytes were written. */
|
|
static int bg_strip_monitor_frames(const char *buf, int len, char *out, int outcap)
|
|
{
|
|
int outlen = 0;
|
|
int in_frame = 0;
|
|
for (int i = 0; i < len && outlen < outcap; i++) {
|
|
unsigned char c = (unsigned char)buf[i];
|
|
if (!in_frame) {
|
|
if (c == 0xFF) { in_frame = 1; continue; }
|
|
out[outlen++] = (char)c;
|
|
} else {
|
|
if (c == 0xFE) in_frame = 0;
|
|
/* else: still inside a monitor frame, discard the byte */
|
|
}
|
|
}
|
|
return outlen;
|
|
}
|
|
|
|
/* Wait for the BBS to finish talking and reach a prompt.
|
|
* Strategy: keep reading until BG_QUIET_MS of silence after the last
|
|
* received byte — that silence means the BBS is waiting for input.
|
|
* Hard timeout at BG_PROMPT_TIMEOUT_MS in case the server never responds.
|
|
* Returns 1 if a BBS error keyword was seen, 0 otherwise. */
|
|
static int bg_wait_for_prompt(int fd)
|
|
{
|
|
char rbuf[2048], stripped[2048], lc[2048];
|
|
struct timeval start, now, last_rx;
|
|
int got_data = 0;
|
|
gettimeofday(&start, NULL);
|
|
|
|
for (;;) {
|
|
gettimeofday(&now, NULL);
|
|
long total = (now.tv_sec - start.tv_sec) * 1000L
|
|
+ (now.tv_usec - start.tv_usec) / 1000L;
|
|
if (total >= BG_PROMPT_TIMEOUT_MS) return 0;
|
|
|
|
/* Once data has arrived, return as soon as it's been quiet long enough */
|
|
if (got_data) {
|
|
long quiet = (now.tv_sec - last_rx.tv_sec) * 1000L
|
|
+ (now.tv_usec - last_rx.tv_usec) / 1000L;
|
|
if (quiet >= BG_QUIET_MS) return 0;
|
|
}
|
|
|
|
fd_set rfds;
|
|
FD_ZERO(&rfds); FD_SET(fd, &rfds);
|
|
struct timeval tv = { 0, 100000L }; /* poll every 100ms */
|
|
int r = select(fd + 1, &rfds, NULL, NULL, &tv);
|
|
if (r > 0 && FD_ISSET(fd, &rfds)) {
|
|
int n = recv(fd, rbuf, sizeof(rbuf) - 1, 0);
|
|
if (n <= 0) return 0;
|
|
gettimeofday(&last_rx, NULL);
|
|
got_data = 1;
|
|
int slen = bg_strip_monitor_frames(rbuf, n, stripped, sizeof(stripped) - 1);
|
|
if (slen > 0) {
|
|
stripped[slen] = '\0';
|
|
char echo[2200];
|
|
int elen = snprintf(echo, sizeof(echo), "[BG] %s", stripped);
|
|
if (elen > 0) append_out(echo, elen, COLOR_PAIR(CP_MAGENTA));
|
|
/* Lowercase copy for keyword scan so we don't clobber the echo */
|
|
memcpy(lc, stripped, (size_t)(slen + 1));
|
|
for (int i = 0; i < slen; i++)
|
|
if (lc[i] >= 'A' && lc[i] <= 'Z') lc[i] = (char)(lc[i] - 'A' + 'a');
|
|
if (strstr(lc, "invalid") || strstr(lc, "unknown command") ||
|
|
strstr(lc, "not allowed") || strstr(lc, "rejected")) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void bg_send_line(int fd, const char *text, int *aborted)
|
|
{
|
|
if (*aborted || fd < 0) return;
|
|
|
|
int len = (int)strlen(text);
|
|
send(fd, text, len, 0);
|
|
send(fd, "\r", 1, 0);
|
|
|
|
char echo[ICS_MSG_LINELEN + 16];
|
|
int elen = snprintf(echo, sizeof(echo), "[BG] > %s\n", text);
|
|
append_out(echo, elen, COLOR_PAIR(CP_MAGENTA) | A_BOLD);
|
|
|
|
/* Wait for BBS to go quiet (prompt ready) before caller sends next line */
|
|
if (bg_wait_for_prompt(fd)) *aborted = 1;
|
|
}
|
|
|
|
static void bg_send_block(int fd, char lines[][ICS_MSG_LINELEN], int n, int *aborted)
|
|
{
|
|
if (*aborted || fd < 0 || n <= 0) return;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
int len = (int)strlen(lines[i]);
|
|
send(fd, lines[i], len, 0);
|
|
send(fd, "\r", 1, 0);
|
|
|
|
char echo[ICS_MSG_LINELEN + 16];
|
|
int elen = snprintf(echo, sizeof(echo), "[BG] > %s\n", lines[i]);
|
|
append_out(echo, elen, COLOR_PAIR(CP_MAGENTA) | A_BOLD);
|
|
}
|
|
|
|
if (bg_wait_for_prompt(fd)) *aborted = 1;
|
|
}
|
|
|
|
/* Opens a new socket to hosts[idx], logs in if credentials are saved,
|
|
* and drains the login banner briefly. Returns the connected fd, or
|
|
* -1 on failure (with a message already echoed to out_win). This is
|
|
* intentionally separate from do_connect()/sock/connected - it never
|
|
* touches the main session's connection state. */
|
|
static int bg_connect_and_login(int idx)
|
|
{
|
|
if (idx < 0 || idx >= num_hosts) return -1;
|
|
|
|
char msg[160];
|
|
snprintf(msg, sizeof(msg), "[BG] Connecting to %s:%s...\n",
|
|
hosts[idx].host, hosts[idx].port);
|
|
append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_MAGENTA));
|
|
|
|
int fd = tcp_connect(hosts[idx].host, hosts[idx].port);
|
|
if (fd < 0) {
|
|
append_out("[BG] Connection failed.\n", 24, COLOR_PAIR(CP_RED));
|
|
return -1;
|
|
}
|
|
|
|
if (hosts[idx].username[0]) {
|
|
char login[256];
|
|
int llen = snprintf(login, sizeof(login), "%s\r%s\rBPQTermTCP\r",
|
|
hosts[idx].username, hosts[idx].password);
|
|
send(fd, login, llen, 0);
|
|
}
|
|
|
|
/* Wait for login banner to finish — node is at a prompt when it goes quiet */
|
|
bg_wait_for_prompt(fd);
|
|
|
|
/* Enter BBS — BPQ drops you at the node prompt after login */
|
|
send(fd, "bbs\r", 4, 0);
|
|
append_out("[BG] > bbs\n", 11, COLOR_PAIR(CP_MAGENTA) | A_BOLD);
|
|
|
|
/* Wait for BBS greeting to finish before sending the message */
|
|
bg_wait_for_prompt(fd);
|
|
|
|
return fd;
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Host picker (shared by Connect and the background send flow)
|
|
* Returns the chosen host index, or -1 if cancelled / no hosts.
|
|
* Picks automatically with no popup if exactly one host exists.
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
static int pick_host(const char *title)
|
|
{
|
|
if (num_hosts <= 0) return -1;
|
|
if (num_hosts == 1) return 0;
|
|
|
|
int pw = 20;
|
|
for (int i = 0; i < num_hosts; i++) {
|
|
int len = snprintf(NULL, 0, "%d - %s (%s:%s)", i,
|
|
hosts[i].name, hosts[i].host, hosts[i].port) + 6;
|
|
if (len > pw) pw = len;
|
|
}
|
|
int hint_w = snprintf(NULL, 0, "0-%d = pick Esc = cancel", num_hosts-1) + 6;
|
|
if (hint_w > pw) pw = hint_w;
|
|
if (pw > COLS) pw = COLS;
|
|
int ph = num_hosts + 5;
|
|
|
|
WINDOW *hw = newwin(ph, pw, (LINES-ph)/2, (COLS-pw)/2);
|
|
wbkgd(hw, COLOR_PAIR(CP_DEFAULT));
|
|
draw_box(hw);
|
|
int tlen = (int)strlen(title) + 2;
|
|
mvwprintw(hw, 0, (pw - tlen) / 2 > 0 ? (pw - tlen) / 2 : 0, " %s ", title);
|
|
for (int i = 0; i < num_hosts; i++)
|
|
mvwprintw(hw, 2+i, 3, "%d - %s (%s:%s)", i,
|
|
hosts[i].name, hosts[i].host, hosts[i].port);
|
|
mvwprintw(hw, ph-2, 3, "0-%d = pick Esc = cancel", num_hosts-1);
|
|
wrefresh(hw);
|
|
nodelay(hw, FALSE);
|
|
keypad(hw, TRUE);
|
|
int hch = wgetch(hw);
|
|
delwin(hw);
|
|
force_repaint();
|
|
|
|
if (hch >= '0' && hch < '0' + num_hosts) return hch - '0';
|
|
return -1;
|
|
}
|
|
|
|
/* Word-wraps src into dst_lines (each up to width chars), up to max_lines.
|
|
* Returns the number of wrapped lines produced. A blank src line produces
|
|
* one blank output line, preserving paragraph breaks the user typed. */
|
|
static int ics_wrap_line(const char *src, char dst_lines[][ICS_MSG_LINELEN],
|
|
int max_lines, int width)
|
|
{
|
|
int n = 0;
|
|
int slen = (int)strlen(src);
|
|
if (slen == 0) {
|
|
if (n < max_lines) dst_lines[n++][0] = '\0';
|
|
return n;
|
|
}
|
|
int pos = 0;
|
|
while (pos < slen && n < max_lines) {
|
|
int remaining = slen - pos;
|
|
int take = remaining < width ? remaining : width;
|
|
if (take < remaining) {
|
|
/* try to break on the last space within the window */
|
|
int brk = take;
|
|
while (brk > 0 && src[pos + brk] != ' ') brk--;
|
|
if (brk > 0) take = brk;
|
|
}
|
|
memcpy(dst_lines[n], src + pos, take);
|
|
dst_lines[n][take] = '\0';
|
|
n++;
|
|
pos += take;
|
|
while (pos < slen && src[pos] == ' ') pos++; /* skip the break space */
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Builds the full SB exchange and sends it over its OWN background
|
|
* connection (see "Background send connection" above) - the main
|
|
* session's connection, if any, is never touched. Echoes progress/
|
|
* result into out_win, prefixed "[BG]". Returns 1 on apparent
|
|
* success, 0 if cancelled/failed (no host picked, connect failed,
|
|
* or a BBS error keyword was detected). */
|
|
static int ics_send_message(const IcsMessage *msg)
|
|
{
|
|
int idx = pick_host("Send via host");
|
|
if (idx < 0) {
|
|
append_out("Send cancelled - no host selected.\n", 37,
|
|
COLOR_PAIR(CP_YELLOW));
|
|
return 0;
|
|
}
|
|
|
|
int fd = bg_connect_and_login(idx);
|
|
if (fd < 0) return 0;
|
|
|
|
append_out("[BG] --- Sending ICS-213 General Message ---\n", 47,
|
|
COLOR_PAIR(CP_MAGENTA) | A_BOLD);
|
|
|
|
int aborted = 0;
|
|
|
|
/* Step 1: SB line (addressee/distribution) */
|
|
bg_send_line(fd, msg->sb_line, &aborted);
|
|
|
|
/* Step 2: Subject, in reply to the BBS's subject prompt */
|
|
if (!aborted) bg_send_line(fd, msg->subject, &aborted);
|
|
|
|
/* Step 3: Body — ICS-213 numbered format, assembled then sent as one block. */
|
|
if (!aborted) {
|
|
/* Build raw (pre-wrap) lines */
|
|
char raw[100][ICS_MSG_LINELEN];
|
|
int rn = 0;
|
|
|
|
#define RAW(fmt, ...) \
|
|
if (rn < 100) snprintf(raw[rn++], ICS_MSG_LINELEN, fmt, ##__VA_ARGS__)
|
|
|
|
RAW("GENERAL MESSAGE (ICS 213)");
|
|
RAW("");
|
|
RAW("1. Incident Name (Optional): %s", msg->incident);
|
|
RAW("");
|
|
RAW("2. To (Name and Position): %s", msg->to);
|
|
RAW("");
|
|
RAW("3. From (Name and Position): %s", msg->from);
|
|
RAW("");
|
|
RAW("4. Subject: %s", msg->subject);
|
|
RAW("");
|
|
RAW("5. Date: %s", msg->date);
|
|
RAW("");
|
|
RAW("6. Time: %sZ", msg->time_z);
|
|
RAW("");
|
|
/* Field 7: first message line shares the "7. Message:" prefix */
|
|
if (rn < 100) {
|
|
const char *first = (msg->msg_nlines > 0) ? msg->msg_lines[0] : "";
|
|
snprintf(raw[rn++], ICS_MSG_LINELEN, "7. Message: %s", first);
|
|
}
|
|
for (int i = 1; i < msg->msg_nlines && rn < 100; i++) {
|
|
strncpy(raw[rn], msg->msg_lines[i], ICS_MSG_LINELEN - 1);
|
|
raw[rn++][ICS_MSG_LINELEN - 1] = '\0';
|
|
}
|
|
RAW("");
|
|
RAW("8. Approved by: Name: %s Signature: %s Position/Title: %s",
|
|
msg->appr_name, msg->appr_sig, msg->appr_pos);
|
|
RAW("");
|
|
RAW("9. Reply:");
|
|
RAW("");
|
|
RAW("10. Replied by: Name: Signature: Position/Title:");
|
|
|
|
#undef RAW
|
|
|
|
/* Word-wrap each raw line and collect into the send buffer */
|
|
char body[ICS_MSG_LINES + 60][ICS_MSG_LINELEN];
|
|
int bn = 0;
|
|
for (int i = 0; i < rn; i++) {
|
|
char wrapped[ICS_MSG_LINES][ICS_MSG_LINELEN];
|
|
int nw = ics_wrap_line(raw[i], wrapped, ICS_MSG_LINES, ICS_MSG_COLS);
|
|
for (int j = 0; j < nw && bn < (int)(sizeof(body)/sizeof(body[0])); j++) {
|
|
strncpy(body[bn], wrapped[j], ICS_MSG_LINELEN - 1);
|
|
body[bn][ICS_MSG_LINELEN - 1] = '\0';
|
|
bn++;
|
|
}
|
|
}
|
|
|
|
bg_send_block(fd, body, bn, &aborted);
|
|
}
|
|
|
|
/* Step 4: terminator */
|
|
if (!aborted) bg_send_line(fd, "/EX", &aborted);
|
|
|
|
close(fd);
|
|
|
|
if (aborted) {
|
|
append_out("[BG] --- BBS reported an error - message send aborted ---\n", 60,
|
|
COLOR_PAIR(CP_RED) | A_BOLD);
|
|
return 0;
|
|
}
|
|
|
|
append_out("[BG] --- General Message sent ---\n", 35,
|
|
COLOR_PAIR(CP_GREEN) | A_BOLD);
|
|
return 1;
|
|
}
|
|
|
|
/* ── Form UI ──────────────────────────────────────────────── */
|
|
|
|
/* Single-line field editor shared by several form rows. Returns the
|
|
* key that ended editing (Tab/Up/Down/Esc/Enter) so the caller can
|
|
* move focus. Operates in-place on buf (NUL-terminated, max maxlen-1). */
|
|
static int ics_edit_field(WINDOW *w, int y, int x, int width,
|
|
char *buf, int maxlen)
|
|
{
|
|
int len = (int)strlen(buf);
|
|
int col = len;
|
|
|
|
for (;;) {
|
|
mvwprintw(w, y, x, "%-*s", width, "");
|
|
mvwaddnstr(w, y, x, buf, (int)strlen(buf));
|
|
wmove(w, y, x + col);
|
|
wrefresh(w);
|
|
|
|
int ch = wgetch(w);
|
|
switch (ch) {
|
|
case '\t': case KEY_DOWN: case KEY_UP: case 27:
|
|
case '\r': case '\n': case KEY_ENTER: case KEY_F(2):
|
|
return ch;
|
|
case KEY_LEFT:
|
|
if (col > 0) col--;
|
|
break;
|
|
case KEY_RIGHT:
|
|
if (col < len) col++;
|
|
break;
|
|
case KEY_HOME:
|
|
col = 0;
|
|
break;
|
|
case KEY_END:
|
|
col = len;
|
|
break;
|
|
case KEY_BACKSPACE: case 127: case '\b':
|
|
if (col > 0) {
|
|
memmove(buf + col - 1, buf + col, len - col + 1);
|
|
col--; len--;
|
|
}
|
|
break;
|
|
case KEY_DC:
|
|
if (col < len) {
|
|
memmove(buf + col, buf + col + 1, len - col);
|
|
len--;
|
|
}
|
|
break;
|
|
default:
|
|
if (ch >= 32 && ch < 256 && len < maxlen - 1) {
|
|
memmove(buf + col + 1, buf + col, len - col + 1);
|
|
buf[col] = (char)ch;
|
|
col++; len++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ics_default_datetime(char *out, int outlen)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm *t = localtime(&now);
|
|
snprintf(out, outlen, "%02d/%02d/%04d %02d:%02d",
|
|
t->tm_mon + 1, t->tm_mday, t->tm_year + 1900,
|
|
t->tm_hour, t->tm_min);
|
|
}
|
|
|
|
static void ics_default_date(char *out, int outlen)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm *t = gmtime(&now);
|
|
snprintf(out, outlen, "%04d-%02d-%02d",
|
|
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday);
|
|
}
|
|
|
|
static void ics_default_time(char *out, int outlen)
|
|
{
|
|
time_t now = time(NULL);
|
|
struct tm *t = gmtime(&now);
|
|
snprintf(out, outlen, "%02d:%02d", t->tm_hour, t->tm_min);
|
|
}
|
|
|
|
enum {
|
|
GM_SB = 0, GM_INCIDENT, GM_TO, GM_FROM, GM_SUBJECT,
|
|
GM_DATE, GM_TIME, GM_MSG,
|
|
GM_APPR_NAME, GM_APPR_SIG, GM_APPR_POS,
|
|
GM_COUNT
|
|
};
|
|
|
|
static void general_message_form(void)
|
|
{
|
|
static IcsMessage msg;
|
|
static int initialized = 0;
|
|
|
|
if (!initialized) {
|
|
memset(&msg, 0, sizeof(msg));
|
|
strncpy(msg.sb_line, "SB PKTNET@USA", ICS_SBLINE_MAX - 1);
|
|
ics_default_date(msg.date, ICS_FIELD_MAX);
|
|
ics_default_time(msg.time_z, ICS_FIELD_MAX);
|
|
msg.msg_nlines = 1;
|
|
initialized = 1;
|
|
} else {
|
|
ics_default_date(msg.date, ICS_FIELD_MAX);
|
|
ics_default_time(msg.time_z, ICS_FIELD_MAX);
|
|
}
|
|
|
|
int mh = LINES - 2;
|
|
if (mh > 30) mh = 30;
|
|
if (mh < 20) mh = 20;
|
|
int mw = COLS - 4;
|
|
if (mw > ICS_MSG_COLS + 6) mw = ICS_MSG_COLS + 6;
|
|
if (mw < 60) mw = 60;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
curs_set(1);
|
|
|
|
/* Row layout:
|
|
* 0 border+title
|
|
* 1 blank
|
|
* 2 SB Line
|
|
* 3 1. Incident
|
|
* 4 2. To
|
|
* 5 3. From
|
|
* 6 4. Subject (= BBS title + ICS field 4)
|
|
* 7 5. Date
|
|
* 8 6. Time
|
|
* 9 -------- 7. Message -------- (top border of textarea)
|
|
* 10..9+msg_rows textarea
|
|
* 10+msg_rows --------------- (bottom border)
|
|
* 11+msg_rows 8. Name
|
|
* 12+msg_rows Sig
|
|
* 13+msg_rows Position
|
|
* 14+msg_rows hint
|
|
* 15+msg_rows border (= mh-1)
|
|
*/
|
|
const int label_x = 2;
|
|
const int field_x = 16;
|
|
const int field_w = mw - field_x - 3;
|
|
|
|
const int y_sb = 2;
|
|
const int y_incident = 3;
|
|
const int y_to = 4;
|
|
const int y_from = 5;
|
|
const int y_subject = 6;
|
|
const int y_date = 7;
|
|
const int y_time = 8;
|
|
const int y_msgtop = 9; /* top '-' border doubles as "7. Message" label */
|
|
|
|
int msg_rows = mh - 16;
|
|
if (msg_rows > ICS_MSG_ROWS) msg_rows = ICS_MSG_ROWS;
|
|
if (msg_rows < 4) msg_rows = 4;
|
|
|
|
int y_msgbot = y_msgtop + msg_rows + 1;
|
|
int y_apprname = y_msgbot + 1;
|
|
int y_apprsig = y_msgbot + 2;
|
|
int y_apprpos = y_msgbot + 3;
|
|
int y_hint = y_msgbot + 4;
|
|
|
|
int ta_cols = (ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w;
|
|
|
|
TextArea ta;
|
|
ta.lines = msg.msg_lines;
|
|
ta.nlines = &msg.msg_nlines;
|
|
ta.cur_line = 0;
|
|
ta.cur_col = 0;
|
|
ta.top_line = 0;
|
|
|
|
int focus = GM_SB;
|
|
int running = 1;
|
|
int send_failed = 0;
|
|
|
|
while (running) {
|
|
werase(w);
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 27) / 2, " General Message (ICS-213) ");
|
|
|
|
/* Labels */
|
|
mvwprintw(w, y_sb, label_x, "SB Line:");
|
|
mvwprintw(w, y_incident, label_x, "1. Incident:");
|
|
mvwprintw(w, y_to, label_x, "2. To:");
|
|
mvwprintw(w, y_from, label_x, "3. From:");
|
|
mvwprintw(w, y_subject, label_x, "4. Subject:");
|
|
mvwprintw(w, y_date, label_x, "5. Date:");
|
|
mvwprintw(w, y_time, label_x, "6. Time:");
|
|
mvwprintw(w, y_apprname, label_x, "8. Name:");
|
|
mvwprintw(w, y_apprsig, label_x, " Sig:");
|
|
mvwprintw(w, y_apprpos, label_x, " Position:");
|
|
|
|
/* Field values */
|
|
mvwprintw(w, y_sb, field_x, "%-*s", field_w, msg.sb_line);
|
|
mvwprintw(w, y_subject, field_x, "%-*s", field_w, msg.subject);
|
|
mvwprintw(w, y_incident, field_x, "%-*s", field_w, msg.incident);
|
|
mvwprintw(w, y_to, field_x, "%-*s", field_w, msg.to);
|
|
mvwprintw(w, y_from, field_x, "%-*s", field_w, msg.from);
|
|
mvwprintw(w, y_date, field_x, "%-*s", field_w, msg.date);
|
|
mvwprintw(w, y_time, field_x, "%-*s", field_w, msg.time_z);
|
|
/* Show static "Z" suffix after the time value */
|
|
mvwprintw(w, y_time, field_x + (int)strlen(msg.time_z), "Z");
|
|
mvwprintw(w, y_apprname, field_x, "%-*s", field_w, msg.appr_name);
|
|
mvwprintw(w, y_apprsig, field_x, "%-*s", field_w, msg.appr_sig);
|
|
mvwprintw(w, y_apprpos, field_x, "%-*s", field_w, msg.appr_pos);
|
|
|
|
/* Message section borders with "7. Message" label centred in the top bar */
|
|
for (int c = label_x; c < mw - label_x && c < mw - 1; c++) {
|
|
mvwaddch(w, y_msgtop, c, '-');
|
|
mvwaddch(w, y_msgbot, c, '-');
|
|
}
|
|
mvwprintw(w, y_msgtop, (mw - 13) / 2, " 7. Message ");
|
|
|
|
ta_draw(w, &ta, y_msgtop + 1, label_x + 1, msg_rows, ta_cols);
|
|
|
|
if (send_failed)
|
|
mvwprintw(w, y_hint, label_x, "%-*s", mw - label_x - 2,
|
|
"Send failed - see output. F2=retry Esc=Close");
|
|
else
|
|
mvwprintw(w, y_hint, label_x, "%-*s", mw - label_x - 2,
|
|
"Tab/Up/Down=move F2=Send Esc=Close (Time=UTC, Z auto)");
|
|
|
|
int ch;
|
|
switch (focus) {
|
|
case GM_SB: ch = ics_edit_field(w, y_sb, field_x, field_w, msg.sb_line, ICS_SBLINE_MAX); break;
|
|
case GM_SUBJECT: ch = ics_edit_field(w, y_subject, field_x, field_w, msg.subject, ICS_FIELD_MAX); break;
|
|
case GM_INCIDENT: ch = ics_edit_field(w, y_incident, field_x, field_w, msg.incident, ICS_FIELD_MAX); break;
|
|
case GM_TO: ch = ics_edit_field(w, y_to, field_x, field_w, msg.to, ICS_FIELD_MAX); break;
|
|
case GM_FROM: ch = ics_edit_field(w, y_from, field_x, field_w, msg.from, ICS_FIELD_MAX); break;
|
|
case GM_DATE: ch = ics_edit_field(w, y_date, field_x, field_w, msg.date, ICS_FIELD_MAX); break;
|
|
case GM_TIME: ch = ics_edit_field(w, y_time, field_x, field_w, msg.time_z, ICS_FIELD_MAX); break;
|
|
case GM_APPR_NAME: ch = ics_edit_field(w, y_apprname, field_x, field_w, msg.appr_name, ICS_FIELD_MAX); break;
|
|
case GM_APPR_SIG: ch = ics_edit_field(w, y_apprsig, field_x, field_w, msg.appr_sig, ICS_FIELD_MAX); break;
|
|
case GM_APPR_POS: ch = ics_edit_field(w, y_apprpos, field_x, field_w, msg.appr_pos, ICS_FIELD_MAX); break;
|
|
case GM_MSG:
|
|
default: {
|
|
wmove(w, y_msgtop + 1 + (ta.cur_line - ta.top_line), label_x + 1 + ta.cur_col);
|
|
wrefresh(w);
|
|
int raw = wgetch(w);
|
|
if (raw == KEY_F(2)) { ch = KEY_F(2); break; }
|
|
int r = ta_handle_key(&ta, raw, msg_rows, ta_cols);
|
|
if (r == '\t' || r == 27) ch = r;
|
|
else continue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
send_failed = 0;
|
|
|
|
if (ch == KEY_F(2)) {
|
|
if (ics_send_message(&msg)) { running = 0; break; }
|
|
redrawwin(w); wrefresh(w);
|
|
send_failed = 1;
|
|
continue;
|
|
}
|
|
|
|
if (ch == 27) { running = 0; break; }
|
|
|
|
if (ch == '\t' || ch == KEY_DOWN || ch == '\r' || ch == '\n' || ch == KEY_ENTER) {
|
|
focus++;
|
|
if (focus >= GM_COUNT) focus = GM_SB;
|
|
} else if (ch == KEY_UP) {
|
|
focus--;
|
|
if (focus < GM_SB) focus = GM_APPR_POS;
|
|
}
|
|
}
|
|
|
|
delwin(w);
|
|
curs_set(1);
|
|
force_repaint();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* PKTNET Packet Check-In (Ctrl+A -> P)
|
|
* Mirrors https://vden.org/pktnet/check_in.html
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
/* Small modal picker: shows a bordered list of options, highlights
|
|
* *cur_idx, lets Up/Down move, Enter/Space confirms, Esc cancels
|
|
* (leaving *cur_idx unchanged). Mirrors the existing mon_options_page
|
|
* style of list UI already used elsewhere in this program. */
|
|
static void chkin_pick(const char *title, const char **opts, int n, int *cur_idx)
|
|
{
|
|
int mh = n + 4; if (mh < 6) mh = 6;
|
|
int mw = 30;
|
|
for (int i = 0; i < n; i++) {
|
|
int l = (int)strlen(opts[i]) + 6;
|
|
if (l > mw) mw = l;
|
|
}
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
|
|
int sel = (*cur_idx >= 0 && *cur_idx < n) ? *cur_idx : 0;
|
|
|
|
for (;;) {
|
|
werase(w);
|
|
draw_box(w);
|
|
int tlen = (int)strlen(title) + 2;
|
|
mvwprintw(w, 0, (mw - tlen) / 2 > 0 ? (mw - tlen) / 2 : 0, " %s ", title);
|
|
for (int i = 0; i < n; i++) {
|
|
if (i == sel) wattron(w, A_REVERSE);
|
|
mvwprintw(w, 2 + i, 3, "%-*s", mw - 5, opts[i]);
|
|
if (i == sel) wattroff(w, A_REVERSE);
|
|
}
|
|
mvwprintw(w, mh - 1, 2, "Enter=pick Esc=cancel");
|
|
wrefresh(w);
|
|
|
|
int ch = wgetch(w);
|
|
switch (ch) {
|
|
case KEY_UP: sel = (sel - 1 + n) % n; break;
|
|
case KEY_DOWN: sel = (sel + 1) % n; break;
|
|
case ' ': case '\r': case '\n': case KEY_ENTER:
|
|
*cur_idx = sel;
|
|
delwin(w);
|
|
force_repaint();
|
|
return;
|
|
case 27: case 'Q': case 'q':
|
|
delwin(w);
|
|
force_repaint();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Sends the check-in over its OWN background connection (same as
|
|
* ics_send_message) - the main session's connection is never touched. */
|
|
static int chkin_send(const PktnetCheckin *f)
|
|
{
|
|
int idx = pick_host("Send via host");
|
|
if (idx < 0) {
|
|
append_out("Send cancelled - no host selected.\n", 37,
|
|
COLOR_PAIR(CP_YELLOW));
|
|
return 0;
|
|
}
|
|
|
|
int fd = bg_connect_and_login(idx);
|
|
if (fd < 0) return 0;
|
|
|
|
append_out("[BG] --- Sending PKTNET Packet Check-In ---\n", 46,
|
|
COLOR_PAIR(CP_MAGENTA) | A_BOLD);
|
|
|
|
int aborted = 0;
|
|
|
|
bg_send_line(fd, f->sb_line, &aborted);
|
|
if (!aborted) bg_send_line(fd, f->subject, &aborted);
|
|
|
|
if (!aborted) {
|
|
char raw[40][ICS_MSG_LINELEN];
|
|
int n = 0;
|
|
|
|
strncpy(raw[n], "PACKET CHECK-IN", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
if (f->agency[0]) {
|
|
strncpy(raw[n], f->agency, ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
}
|
|
raw[n++][0] = '\0';
|
|
|
|
strncpy(raw[n], "1. STATION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "a. Date/Time: %s", f->date_time);
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "b. To: %s", f->to);
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "c. From: %s", f->from);
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "d. Station Contact Name: %s", f->contact_name);
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "e. Initial Operator(s): %s", f->operators);
|
|
raw[n++][0] = '\0';
|
|
|
|
strncpy(raw[n], "2. SESSION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "a. Type: %s", chkin_type_opts[f->type_idx]);
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "b. Service: AMATEUR");
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "c. Band: %s", chkin_band_opts[f->band_idx]);
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "d. Session: %s", chkin_session_opts[f->session_idx]);
|
|
raw[n++][0] = '\0';
|
|
raw[n++][0] = '\0';
|
|
|
|
strncpy(raw[n], "3. LOCATION", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "a. Location: %s", f->location);
|
|
raw[n++][0] = '\0';
|
|
snprintf(raw[n++], ICS_MSG_LINELEN, "b. GRID SQUARE: %s", f->grid);
|
|
raw[n++][0] = '\0';
|
|
raw[n++][0] = '\0';
|
|
|
|
/* "4. COMMENTS:" label, then the comment text on its own line(s) so a
|
|
* full-length first comment line can never be truncated by the label. */
|
|
strncpy(raw[n], "4. COMMENTS:", ICS_MSG_LINELEN - 1); raw[n][ICS_MSG_LINELEN-1]=0; n++;
|
|
for (int i = 0; i < f->comment_nlines && n < 40; i++) {
|
|
strncpy(raw[n], f->comment_lines[i], ICS_MSG_LINELEN - 1);
|
|
raw[n][ICS_MSG_LINELEN-1] = 0;
|
|
n++;
|
|
}
|
|
|
|
/* Word-wrap everything into one flat block, then send it all at
|
|
* once with no per-line pacing. */
|
|
char body[ICS_MSG_LINES][ICS_MSG_LINELEN];
|
|
int bn = 0;
|
|
for (int i = 0; i < n && bn < ICS_MSG_LINES; i++) {
|
|
char wrapped[ICS_MSG_LINES][ICS_MSG_LINELEN];
|
|
int nwrapped = ics_wrap_line(raw[i], wrapped, ICS_MSG_LINES, ICS_MSG_COLS);
|
|
for (int j = 0; j < nwrapped && bn < ICS_MSG_LINES; j++) {
|
|
strncpy(body[bn], wrapped[j], ICS_MSG_LINELEN - 1);
|
|
body[bn][ICS_MSG_LINELEN - 1] = '\0';
|
|
bn++;
|
|
}
|
|
}
|
|
|
|
bg_send_block(fd, body, bn, &aborted);
|
|
}
|
|
|
|
if (!aborted) bg_send_line(fd, "/EX", &aborted);
|
|
|
|
close(fd);
|
|
|
|
if (aborted) {
|
|
append_out("[BG] --- BBS reported an error - check-in send aborted ---\n", 61,
|
|
COLOR_PAIR(CP_RED) | A_BOLD);
|
|
return 0;
|
|
}
|
|
|
|
append_out("[BG] --- Packet Check-In sent ---\n", 35,
|
|
COLOR_PAIR(CP_GREEN) | A_BOLD);
|
|
return 1;
|
|
}
|
|
|
|
enum {
|
|
CK_SB = 0, CK_SUBJECT, CK_AGENCY, CK_DATETIME, CK_TO, CK_FROM,
|
|
CK_CONTACT, CK_OPERATORS, CK_TYPE, CK_BAND, CK_SESSION,
|
|
CK_LOCATION, CK_GRID, CK_COMMENTS, CK_COUNT
|
|
};
|
|
|
|
static void pktnet_checkin_form(void)
|
|
{
|
|
static PktnetCheckin f;
|
|
static int initialized = 0;
|
|
|
|
if (!initialized) {
|
|
memset(&f, 0, sizeof(f));
|
|
strncpy(f.sb_line, "SB PKTNET@USA", ICS_SBLINE_MAX - 1);
|
|
strncpy(f.to, "PKTNET@USA", CHKIN_FIELD_MAX - 1);
|
|
if (current_host >= 0 && hosts[current_host].username[0])
|
|
strncpy(f.from, hosts[current_host].username, CHKIN_FIELD_MAX - 1);
|
|
f.type_idx = 0; /* EXERCISE */
|
|
f.band_idx = 2; /* VHF */
|
|
f.session_idx = 1; /* AX25 Packet */
|
|
f.comment_nlines = 1;
|
|
initialized = 1;
|
|
}
|
|
ics_default_datetime(f.date_time, ICS_DT_MAX);
|
|
|
|
int mh = LINES - 2; if (mh > 30) mh = 30;
|
|
int mw = COLS - 4; if (mw > ICS_MSG_COLS + 6) mw = ICS_MSG_COLS + 6;
|
|
if (mh < 26) mh = 26;
|
|
if (mw < 60) mw = 60;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
keypad(w, TRUE);
|
|
nodelay(w, FALSE);
|
|
curs_set(1);
|
|
|
|
const int label_x = 2;
|
|
const int field_x = 18;
|
|
const int field_w = mw - field_x - 3;
|
|
|
|
/* Row layout */
|
|
const int y_sb = 2;
|
|
const int y_subject = 3;
|
|
const int y_agency = 4;
|
|
const int y_datetime = 6;
|
|
const int y_to = 7;
|
|
const int y_from = 8;
|
|
const int y_contact = 9;
|
|
const int y_operators= 10;
|
|
const int y_type = 12;
|
|
const int y_band = 13;
|
|
const int y_session = 14;
|
|
const int y_location = 16;
|
|
const int y_grid = 17;
|
|
const int y_commentlbl = 19;
|
|
const int y_commentbox = 20;
|
|
int comment_rows = mh - y_commentbox - 4;
|
|
if (comment_rows > 6) comment_rows = 6;
|
|
if (comment_rows < 3) comment_rows = 3;
|
|
int ck_ta_cols = (ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w;
|
|
|
|
TextArea ta;
|
|
ta.lines = f.comment_lines;
|
|
ta.nlines = &f.comment_nlines;
|
|
ta.cur_line = 0;
|
|
ta.cur_col = 0;
|
|
ta.top_line = 0;
|
|
|
|
int focus = CK_SB;
|
|
int running = 1;
|
|
int just_sent = 0;
|
|
|
|
while (running) {
|
|
werase(w);
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 22) / 2, " PKTNET Packet Check-In ");
|
|
|
|
mvwprintw(w, y_sb, label_x, "SB Line:");
|
|
mvwprintw(w, y_subject, label_x, "Subject:");
|
|
mvwprintw(w, y_agency, label_x, "Agency/Group:");
|
|
mvwprintw(w, y_datetime, label_x, "1a Date/Time:");
|
|
mvwprintw(w, y_to, label_x, "1b To:");
|
|
mvwprintw(w, y_from, label_x, "1c From:");
|
|
mvwprintw(w, y_contact, label_x, "1d Contact:");
|
|
mvwprintw(w, y_operators, label_x, "1e Operators:");
|
|
mvwprintw(w, y_type, label_x, "2a Type:");
|
|
mvwprintw(w, y_band, label_x, "2c Band:");
|
|
mvwprintw(w, y_session, label_x, "2d Session:");
|
|
mvwprintw(w, y_location, label_x, "3a Location:");
|
|
mvwprintw(w, y_grid, label_x, "3b Grid Sq:");
|
|
mvwprintw(w, y_commentlbl, label_x, "4. Comments:");
|
|
|
|
mvwprintw(w, y_sb, field_x, "%-*s", field_w, f.sb_line);
|
|
mvwprintw(w, y_subject, field_x, "%-*s", field_w, f.subject);
|
|
mvwprintw(w, y_agency, field_x, "%-*s", field_w, f.agency);
|
|
mvwprintw(w, y_datetime, field_x, "%-*s", field_w, f.date_time);
|
|
mvwprintw(w, y_to, field_x, "%-*s", field_w, f.to);
|
|
mvwprintw(w, y_from, field_x, "%-*s", field_w, f.from);
|
|
mvwprintw(w, y_contact, field_x, "%-*s", field_w, f.contact_name);
|
|
mvwprintw(w, y_operators, field_x, "%-*s", field_w, f.operators);
|
|
|
|
if (focus == CK_TYPE) wattron(w, A_REVERSE);
|
|
mvwprintw(w, y_type, field_x, "%-20s", chkin_type_opts[f.type_idx]);
|
|
if (focus == CK_TYPE) wattroff(w, A_REVERSE);
|
|
|
|
mvwprintw(w, y_type, field_x + 22, "(Service: AMATEUR)");
|
|
|
|
if (focus == CK_BAND) wattron(w, A_REVERSE);
|
|
mvwprintw(w, y_band, field_x, "%-20s", chkin_band_opts[f.band_idx]);
|
|
if (focus == CK_BAND) wattroff(w, A_REVERSE);
|
|
|
|
if (focus == CK_SESSION) wattron(w, A_REVERSE);
|
|
mvwprintw(w, y_session, field_x, "%-20s", chkin_session_opts[f.session_idx]);
|
|
if (focus == CK_SESSION) wattroff(w, A_REVERSE);
|
|
|
|
mvwprintw(w, y_location, field_x, "%-*s", field_w, f.location);
|
|
mvwprintw(w, y_grid, field_x, "%-*s", field_w, f.grid);
|
|
|
|
for (int c = label_x; c < label_x + ICS_MSG_COLS + 2 && c < mw - 1; c++) {
|
|
mvwaddch(w, y_commentbox, c, '-');
|
|
mvwaddch(w, y_commentbox + comment_rows + 1, c, '-');
|
|
}
|
|
ta_draw(w, &ta, y_commentbox + 1, label_x + 1, comment_rows,
|
|
(ICS_MSG_COLS < field_w) ? ICS_MSG_COLS : field_w);
|
|
|
|
mvwprintw(w, mh - 3, label_x,
|
|
"Tab/Up/Down move fields Enter/Space picks on Type/Band/Session");
|
|
mvwprintw(w, mh - 2, label_x,
|
|
"F2=Send Esc=Cancel/Close");
|
|
if (just_sent)
|
|
mvwprintw(w, mh - 4, label_x, "Sent. Edit and F2 to send again, or Esc to close.");
|
|
wrefresh(w);
|
|
|
|
int ch;
|
|
switch (focus) {
|
|
case CK_SB: ch = ics_edit_field(w, y_sb, field_x, field_w, f.sb_line, ICS_SBLINE_MAX); break;
|
|
case CK_SUBJECT: ch = ics_edit_field(w, y_subject, field_x, field_w, f.subject, CHKIN_FIELD_MAX); break;
|
|
case CK_AGENCY: ch = ics_edit_field(w, y_agency, field_x, field_w, f.agency, CHKIN_FIELD_MAX); break;
|
|
case CK_DATETIME: ch = ics_edit_field(w, y_datetime, field_x, field_w, f.date_time, ICS_DT_MAX); break;
|
|
case CK_TO: ch = ics_edit_field(w, y_to, field_x, field_w, f.to, CHKIN_FIELD_MAX); break;
|
|
case CK_FROM: ch = ics_edit_field(w, y_from, field_x, field_w, f.from, CHKIN_FIELD_MAX); break;
|
|
case CK_CONTACT: ch = ics_edit_field(w, y_contact, field_x, field_w, f.contact_name, CHKIN_FIELD_MAX); break;
|
|
case CK_OPERATORS: ch = ics_edit_field(w, y_operators,field_x, field_w, f.operators, CHKIN_FIELD_MAX); break;
|
|
case CK_LOCATION: ch = ics_edit_field(w, y_location, field_x, field_w, f.location, CHKIN_FIELD_MAX); break;
|
|
case CK_GRID: ch = ics_edit_field(w, y_grid, field_x, field_w, f.grid, CHKIN_FIELD_MAX); break;
|
|
|
|
case CK_TYPE:
|
|
wmove(w, y_type, field_x);
|
|
ch = wgetch(w);
|
|
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
|
|
chkin_pick("Type", chkin_type_opts, chkin_type_n, &f.type_idx);
|
|
break;
|
|
case CK_BAND:
|
|
wmove(w, y_band, field_x);
|
|
ch = wgetch(w);
|
|
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
|
|
chkin_pick("Band", chkin_band_opts, chkin_band_n, &f.band_idx);
|
|
break;
|
|
case CK_SESSION:
|
|
wmove(w, y_session, field_x);
|
|
ch = wgetch(w);
|
|
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == KEY_ENTER)
|
|
chkin_pick("Session", chkin_session_opts, chkin_session_n, &f.session_idx);
|
|
break;
|
|
|
|
case CK_COMMENTS:
|
|
default: {
|
|
wmove(w, y_commentbox + 1 + (ta.cur_line - ta.top_line), label_x + 1 + ta.cur_col);
|
|
wrefresh(w);
|
|
int raw = wgetch(w);
|
|
if (raw == KEY_F(2)) { ch = KEY_F(2); break; }
|
|
int r = ta_handle_key(&ta, raw, comment_rows, ck_ta_cols);
|
|
if (r == '\t' || r == 27) ch = r;
|
|
else continue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
just_sent = 0;
|
|
|
|
if (ch == KEY_F(2)) { /* Send */
|
|
if (chkin_send(&f)) {
|
|
running = 0; /* success: behave like Esc was pressed */
|
|
break;
|
|
}
|
|
/* See general_message_form()'s F2 handler for why this is here. */
|
|
redrawwin(w);
|
|
wrefresh(w);
|
|
just_sent = 1; /* failed/aborted: stay open, show status */
|
|
continue;
|
|
}
|
|
if (ch == 27) { /* Esc closes the form */
|
|
running = 0;
|
|
break;
|
|
}
|
|
|
|
if (ch == '\t' || ch == KEY_DOWN || ch == '\r' || ch == '\n' || ch == KEY_ENTER) {
|
|
focus++;
|
|
if (focus > CK_COMMENTS) focus = CK_SB;
|
|
} else if (ch == KEY_UP) {
|
|
focus--;
|
|
if (focus < CK_SB) focus = CK_COMMENTS;
|
|
}
|
|
}
|
|
|
|
delwin(w);
|
|
curs_set(1);
|
|
force_repaint();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Connect helper
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void do_connect(int idx)
|
|
{
|
|
if (idx < 0 || idx >= num_hosts) return;
|
|
if (connected) tcp_disconnect();
|
|
|
|
char msg[160];
|
|
snprintf(msg, sizeof(msg), "Connecting to %s:%s...\n",
|
|
hosts[idx].host, hosts[idx].port);
|
|
append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_YELLOW));
|
|
|
|
sock = tcp_connect(hosts[idx].host, hosts[idx].port);
|
|
if (sock < 0) {
|
|
append_out("Connection failed.\n", 19, COLOR_PAIR(CP_RED));
|
|
return;
|
|
}
|
|
|
|
connected = 1;
|
|
current_host = idx;
|
|
connect_time = time(NULL);
|
|
|
|
snprintf(msg, sizeof(msg), "Connected to %s\n", hosts[idx].name);
|
|
append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_GREEN) | A_BOLD);
|
|
|
|
if (hosts[idx].username[0]) {
|
|
char login[256];
|
|
int llen = snprintf(login, sizeof(login), "%s\r%s\rBPQTermTCP\r",
|
|
hosts[idx].username, hosts[idx].password);
|
|
tcp_send(login, llen);
|
|
char info[120];
|
|
int ilen = snprintf(info, sizeof(info), "[Auto-login: %s]\n",
|
|
hosts[idx].username);
|
|
append_out(info, ilen, COLOR_PAIR(CP_YELLOW));
|
|
} else {
|
|
append_out("[No credentials - manual login required]\n", 41,
|
|
COLOR_PAIR(CP_YELLOW));
|
|
}
|
|
|
|
/* P8=1 triggers BPQ's SendPortsForMonitor → arrives as 0xFF 0xFF packet */
|
|
send_trace_options_with_ports();
|
|
|
|
/* Drain incoming data for up to 2 seconds waiting for PORTS response.
|
|
* A single recv() often gets only the login banner; loop until we have
|
|
* the full port list (ports_loaded) or the deadline passes. */
|
|
{
|
|
char rbuf[4096];
|
|
struct timeval t0, now;
|
|
gettimeofday(&t0, NULL);
|
|
for (;;) {
|
|
gettimeofday(&now, NULL);
|
|
long elapsed = (now.tv_sec - t0.tv_sec) * 1000000L
|
|
+ (now.tv_usec - t0.tv_usec);
|
|
if (elapsed >= 2000000L) break;
|
|
if (ports_loaded) break;
|
|
long remain = 2000000L - elapsed;
|
|
fd_set rfds;
|
|
struct timeval tv = { remain / 1000000L, remain % 1000000L };
|
|
FD_ZERO(&rfds); FD_SET(sock, &rfds);
|
|
if (select(sock + 1, &rfds, NULL, NULL, &tv) <= 0) break;
|
|
int n = recv(sock, rbuf, sizeof(rbuf) - 1, 0);
|
|
if (n <= 0) break;
|
|
bpq_process(rbuf, n);
|
|
}
|
|
}
|
|
|
|
/* Safety-net resend: the very first send_trace_options_with_ports()
|
|
* call above can race with BPQ still processing login - if BPQ wasn't
|
|
* ready to parse it yet, the port mask/monitor flags silently fall
|
|
* back to BPQ's own defaults with no error on either side (seen as
|
|
* "port shows enabled here but no traffic flows until re-toggled").
|
|
* Re-sending once the initial drain has settled costs one extra
|
|
* trace command and catches that race without needing to detect it. */
|
|
if (connected && sock >= 0) {
|
|
send_trace_options_with_ports();
|
|
char rbuf[4096];
|
|
struct timeval tv2 = { 1, 0 };
|
|
fd_set rfds2;
|
|
FD_ZERO(&rfds2); FD_SET(sock, &rfds2);
|
|
if (select(sock + 1, &rfds2, NULL, NULL, &tv2) > 0) {
|
|
int n = recv(sock, rbuf, sizeof(rbuf) - 1, 0);
|
|
if (n > 0) bpq_process(rbuf, n);
|
|
}
|
|
}
|
|
|
|
draw_status();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Forms submenu (Ctrl+A -> F)
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void forms_menu(void)
|
|
{
|
|
int mh = 8, mw = 40;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 9) / 2, " Forms ");
|
|
mvwprintw(w, 2, 4, "G - General Message (ICS-213)");
|
|
mvwprintw(w, 3, 4, "P - PKTNET Packet Check-In");
|
|
mvwprintw(w, mh - 2, 4, "Esc = close");
|
|
wrefresh(w);
|
|
|
|
nodelay(w, FALSE);
|
|
int ch = wgetch(w);
|
|
delwin(w);
|
|
force_repaint();
|
|
|
|
switch (ch | 0x20) {
|
|
case 'g':
|
|
general_message_form();
|
|
break;
|
|
case 'p':
|
|
pktnet_checkin_form();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Ctrl+A main menu
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void ctrl_a_menu(void)
|
|
{
|
|
int mh = 13, mw = 40;
|
|
int my = (LINES-mh)/2; if (my<0) my=0;
|
|
int mx = (COLS-mw)/2; if (mx<0) mx=0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw-14)/2, " TermTCP Menu ");
|
|
mvwprintw(w, 2, 4, "C - Connect");
|
|
mvwprintw(w, 3, 4, "D - Disconnect");
|
|
mvwprintw(w, 4, 4, "O - Config / Host Editor");
|
|
mvwprintw(w, 5, 4, "M - Monitor Options");
|
|
mvwprintw(w, 6, 4, "L - Toggle Log");
|
|
mvwprintw(w, 7, 4, "F - Forms & Check-Ins");
|
|
mvwprintw(w, 8, 4, "H - Help");
|
|
mvwprintw(w, 9, 4, "Q - Quit");
|
|
mvwprintw(w, mh-2, 4, "Esc = close");
|
|
wrefresh(w);
|
|
|
|
nodelay(w, FALSE);
|
|
int ch = wgetch(w);
|
|
delwin(w);
|
|
force_repaint();
|
|
|
|
switch (ch | 0x20) {
|
|
case 'c': {
|
|
int idx = pick_host("Select Host");
|
|
if (idx >= 0) do_connect(idx);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'd':
|
|
if (connected) {
|
|
tcp_disconnect();
|
|
append_out("*** Disconnected\n", 17, COLOR_PAIR(CP_RED));
|
|
}
|
|
break;
|
|
|
|
case 'o':
|
|
config_menu();
|
|
break;
|
|
|
|
case 'm':
|
|
monitor_options_menu();
|
|
break;
|
|
|
|
case 'l':
|
|
if (logging) {
|
|
close_log();
|
|
append_out("[Logging OFF]\n", 14, COLOR_PAIR(CP_YELLOW));
|
|
} else if (connected && current_host >= 0) {
|
|
char prefix[120];
|
|
snprintf(prefix, sizeof(prefix), "%s_%s",
|
|
hosts[current_host].host, hosts[current_host].port);
|
|
open_log(prefix);
|
|
append_out("[Logging ON]\n", 13, COLOR_PAIR(CP_YELLOW));
|
|
} else {
|
|
append_out("Connect first to start logging.\n", 32, COLOR_PAIR(CP_YELLOW));
|
|
}
|
|
draw_status();
|
|
break;
|
|
|
|
case 'f':
|
|
forms_menu();
|
|
break;
|
|
|
|
case 'h':
|
|
show_help();
|
|
break;
|
|
|
|
case 'q':
|
|
if (connected) tcp_disconnect();
|
|
if (logging) close_log();
|
|
endwin();
|
|
exit(0);
|
|
}
|
|
|
|
draw_input();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Key handler
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
static void handle_key(int ch)
|
|
{
|
|
switch (ch) {
|
|
case 1: /* Ctrl+A */
|
|
ctrl_a_menu();
|
|
return;
|
|
|
|
case KEY_RESIZE:
|
|
endwin(); refresh(); clear();
|
|
create_windows();
|
|
refresh_all();
|
|
return;
|
|
|
|
case KEY_PPAGE: /* PgUp — scroll output back */
|
|
if (out_win) {
|
|
int page = getmaxy(out_win);
|
|
out_scroll += page;
|
|
int max_scroll = out_sbuf_count > 0 ? out_sbuf_count - 1 : 0;
|
|
if (out_scroll > max_scroll) out_scroll = max_scroll;
|
|
scrl_redraw_out();
|
|
update_sep_labels();
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
return;
|
|
|
|
case KEY_NPAGE: /* PgDn — scroll output forward / return to live */
|
|
if (out_win && out_scroll > 0) {
|
|
int page = getmaxy(out_win);
|
|
out_scroll -= page;
|
|
if (out_scroll < 0) out_scroll = 0;
|
|
scrl_redraw_out();
|
|
update_sep_labels();
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
return;
|
|
|
|
case KEY_HOME: /* Home — scroll monitor back */
|
|
if (mon_win && mon_visible) {
|
|
int page = getmaxy(mon_win);
|
|
mon_scroll += page;
|
|
int max_scroll = mon_sbuf_count > 0 ? mon_sbuf_count - 1 : 0;
|
|
if (mon_scroll > max_scroll) mon_scroll = max_scroll;
|
|
scrl_redraw_mon();
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
return;
|
|
|
|
case KEY_END: /* End — scroll monitor forward / return to live */
|
|
if (mon_win && mon_visible && mon_scroll > 0) {
|
|
int page = getmaxy(mon_win);
|
|
mon_scroll -= page;
|
|
if (mon_scroll < 0) mon_scroll = 0;
|
|
scrl_redraw_mon();
|
|
if (input_win) wrefresh(input_win);
|
|
}
|
|
return;
|
|
|
|
case '\r': case '\n': case KEY_ENTER:
|
|
if (input_len == 0) return;
|
|
input_buf[input_len] = '\0';
|
|
/* Save to history */
|
|
strncpy(hist_buf[hist_head], input_buf, INPUT_MAX - 1);
|
|
hist_buf[hist_head][INPUT_MAX - 1] = '\0';
|
|
hist_head = (hist_head + 1) % HIST_MAX;
|
|
if (hist_count < HIST_MAX) hist_count++;
|
|
hist_pos = -1;
|
|
if (connected) {
|
|
tcp_send(input_buf, input_len);
|
|
tcp_send("\r", 1);
|
|
char echo[INPUT_MAX + 4];
|
|
int elen = snprintf(echo, sizeof(echo), "> %s\n", input_buf);
|
|
append_out(echo, elen, COLOR_PAIR(CP_CYAN) | A_BOLD);
|
|
} else {
|
|
append_out("Not connected -- Ctrl+A to open menu.\n", 38,
|
|
COLOR_PAIR(CP_YELLOW));
|
|
}
|
|
input_len = 0;
|
|
break;
|
|
|
|
case KEY_UP: /* history: go back */
|
|
if (hist_count == 0) break;
|
|
if (hist_pos == -1) {
|
|
strncpy(hist_save, input_buf, INPUT_MAX - 1);
|
|
hist_save[INPUT_MAX - 1] = '\0';
|
|
hist_pos = 0;
|
|
} else if (hist_pos < hist_count - 1) {
|
|
hist_pos++;
|
|
}
|
|
{
|
|
int idx = (hist_head - 1 - hist_pos + HIST_MAX * 2) % HIST_MAX;
|
|
strncpy(input_buf, hist_buf[idx], INPUT_MAX - 1);
|
|
input_buf[INPUT_MAX - 1] = '\0';
|
|
input_len = (int)strlen(input_buf);
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN: /* history: go forward */
|
|
if (hist_pos < 0) break;
|
|
hist_pos--;
|
|
if (hist_pos < 0) {
|
|
input_buf[0] = '\0'; /* always end with blank line */
|
|
} else {
|
|
int idx = (hist_head - 1 - hist_pos + HIST_MAX * 2) % HIST_MAX;
|
|
strncpy(input_buf, hist_buf[idx], INPUT_MAX - 1);
|
|
input_buf[INPUT_MAX - 1] = '\0';
|
|
}
|
|
input_len = (int)strlen(input_buf);
|
|
break;
|
|
|
|
case KEY_BACKSPACE: case 127: case '\b':
|
|
if (input_len > 0) input_len--;
|
|
break;
|
|
|
|
default:
|
|
if (ch >= 32 && ch < 256 && input_len < INPUT_MAX - 1)
|
|
input_buf[input_len++] = (char)ch;
|
|
break;
|
|
}
|
|
|
|
draw_input();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* Main event loop
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
/* Shown when Ctrl+C (SIGINT) is caught. Returns 1 if the user
|
|
* confirms quitting, 0 to resume exactly where they were. */
|
|
static int confirm_quit_popup(void)
|
|
{
|
|
int mh = 7, mw = 40;
|
|
int my = (LINES - mh) / 2; if (my < 0) my = 0;
|
|
int mx = (COLS - mw) / 2; if (mx < 0) mx = 0;
|
|
|
|
WINDOW *w = newwin(mh, mw, my, mx);
|
|
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
|
|
draw_box(w);
|
|
mvwprintw(w, 0, (mw - 8) / 2, " Quit? ");
|
|
mvwprintw(w, 2, 3, "Ctrl+C pressed.");
|
|
if (connected) mvwprintw(w, 3, 3, "This will close the current session.");
|
|
mvwprintw(w, mh - 2, 3, "Y = quit N / Esc = resume");
|
|
wrefresh(w);
|
|
|
|
nodelay(w, FALSE);
|
|
keypad(w, TRUE);
|
|
int ch = wgetch(w);
|
|
delwin(w);
|
|
force_repaint();
|
|
|
|
return (ch == 'y' || ch == 'Y');
|
|
}
|
|
|
|
void interactive_loop(void)
|
|
{
|
|
fd_set readfds;
|
|
struct timeval tv;
|
|
char recv_buf[1024];
|
|
|
|
draw_input();
|
|
|
|
while (1) {
|
|
if (sigint_pending) {
|
|
sigint_pending = 0;
|
|
if (confirm_quit_popup()) {
|
|
if (connected) tcp_disconnect();
|
|
if (logging) close_log();
|
|
endwin();
|
|
exit(0);
|
|
}
|
|
draw_input();
|
|
}
|
|
|
|
draw_status();
|
|
|
|
FD_ZERO(&readfds);
|
|
FD_SET(STDIN_FILENO, &readfds);
|
|
if (connected && sock >= 0) FD_SET(sock, &readfds);
|
|
|
|
tv.tv_sec = 1;
|
|
tv.tv_usec = 0;
|
|
int maxfd = (connected && sock > STDIN_FILENO) ? sock : STDIN_FILENO;
|
|
int ready = select(maxfd + 1, &readfds, NULL, NULL, &tv);
|
|
if (ready < 0) continue;
|
|
|
|
/* Network data */
|
|
if (connected && sock >= 0 && FD_ISSET(sock, &readfds)) {
|
|
int n = recv(sock, recv_buf, sizeof(recv_buf)-1, 0);
|
|
if (n > 0) {
|
|
bpq_process(recv_buf, n);
|
|
draw_input();
|
|
} else if (n == 0 || (n < 0 && errno != EAGAIN)) {
|
|
append_out("\n*** Server closed connection ***\n", 33,
|
|
COLOR_PAIR(CP_RED));
|
|
tcp_disconnect();
|
|
}
|
|
}
|
|
|
|
/* Keyboard */
|
|
if (FD_ISSET(STDIN_FILENO, &readfds)) {
|
|
int k;
|
|
while ((k = wgetch(input_win)) != ERR)
|
|
handle_key(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════
|
|
* main()
|
|
* ═══════════════════════════════════════════════════════════ */
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
int auto_connect = -1;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--connect") == 0)
|
|
&& i+1 < argc)
|
|
auto_connect = atoi(argv[++i]);
|
|
else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
printf("Usage: termtcp [-c N]\n -c N auto-connect to host N\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Set default port names before load_config so INI PortName keys override them */
|
|
for (int i = 0; i < 32; i++)
|
|
snprintf(port_names[i], 32, "Port %d", i + 1);
|
|
|
|
load_config();
|
|
|
|
setlocale(LC_ALL, ""); /* enable UTF-8 so ncursesw renders box chars correctly */
|
|
initscr();
|
|
cbreak();
|
|
noecho();
|
|
set_escdelay(25); /* fast Escape key response */
|
|
init_colors();
|
|
curs_set(1);
|
|
create_windows();
|
|
draw_status();
|
|
|
|
{
|
|
/* Trap Ctrl+C so it asks for confirmation instead of killing the
|
|
* program outright. SA_RESTART deliberately omitted: select() in
|
|
* interactive_loop() should return EINTR so the pending flag is
|
|
* noticed on the very next loop iteration rather than after the
|
|
* current 1-second poll silently restarts. */
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_handler = on_sigint;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
sigaction(SIGINT, &sa, NULL);
|
|
}
|
|
|
|
{
|
|
const char *banner = "TermTCP v" VERSION " - Ctrl+A opens the menu\n";
|
|
append_out(banner, (int)strlen(banner), COLOR_PAIR(CP_CYAN) | A_BOLD);
|
|
}
|
|
|
|
if (num_hosts > 0) {
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg),
|
|
"Loaded %d host(s). Ctrl+A then C to connect.\n", num_hosts);
|
|
append_out(msg, (int)strlen(msg), COLOR_PAIR(CP_DEFAULT));
|
|
} else {
|
|
append_out("No hosts configured. Ctrl+A then O to edit config.\n", 51,
|
|
COLOR_PAIR(CP_YELLOW));
|
|
}
|
|
|
|
if (auto_connect >= 0) do_connect(auto_connect);
|
|
|
|
interactive_loop();
|
|
|
|
if (connected) tcp_disconnect();
|
|
if (logging) close_log();
|
|
endwin();
|
|
return 0;
|
|
}
|