From 8bf8ae80ecac290d3f792e59f6ff946e53d94682 Mon Sep 17 00:00:00 2001 From: Ryan Collier Date: Mon, 29 Jun 2026 08:23:08 -0400 Subject: [PATCH] Session architecture: bundle per-connection state into Session struct All per-connection state (socket, buffers, logging, port tracking, input history, scrollback) moved from global variables into a Session struct. sessions[MAX_SESSIONS=4] holds the pool; S points to the active one. CLI mode runs with S=&sessions[0] unchanged. This is the foundation for multiple simultaneous sessions in the GUI. Co-Authored-By: Claude Sonnet 4.6 --- termtcp.c | 520 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 273 insertions(+), 247 deletions(-) diff --git a/termtcp.c b/termtcp.c index 78ad052..5abab4a 100644 --- a/termtcp.c +++ b/termtcp.c @@ -70,14 +70,69 @@ typedef struct { typedef struct { char data[SCRL_W]; int len; int base_attr; } ScrlLine; -HostEntry hosts[MAX_HOSTS]; -int num_hosts = 0; -int current_host = -1; +/* ── Session ────────────────────────────────────────────────── + * All per-connection state in one struct so the program can run + * multiple simultaneous sessions (one per tab in the GUI, or one + * in the CLI). S always points to the currently active session. */ +#define MAX_SESSIONS 4 +#define SESSION_LABEL 32 + +typedef struct { + int active; /* slot is in use */ + int host_idx; /* index into hosts[] */ + char label[SESSION_LABEL]; /* display name / tab title */ + + /* Connection */ + int sock; + int connected; + time_t connect_time; + + /* Logging */ + int logging; + FILE *log_file; + + /* BPQ monitor frame accumulator */ + int in_mon_frame; + char mon_fbuf[4096]; + int mon_flen; + + /* Port tracking */ + int port_seen; + char ports_line_buf[256]; + int ports_line_len; + int ports_parse_mode; + int ports_loaded; + + /* Output scrollback */ + ScrlLine out_sbuf[SCRL_MAX]; + int out_sbuf_head, out_sbuf_count; + int out_scroll; + char out_part[SCRL_W]; + int out_part_len, out_part_base, out_part_cur; + + /* Monitor scrollback */ + ScrlLine mon_sbuf[SCRL_MAX]; + int mon_sbuf_head, mon_sbuf_count; + int mon_scroll; + char mon_part[SCRL_W]; + int mon_part_len, mon_part_base, mon_part_cur; + + /* Input */ + char input_buf[INPUT_MAX]; + int input_len; + + /* Input history */ + char hist_buf[HIST_MAX][INPUT_MAX]; + int hist_head, hist_count, hist_pos; + char hist_save[INPUT_MAX]; + +} Session; + +static Session sessions[MAX_SESSIONS]; +static Session *S; /* active (displayed) session */ -/* ── Network ─────────────────────────────────────────────── */ -int sock = -1; -int connected = 0; -time_t connect_time = 0; +HostEntry hosts[MAX_HOSTS]; +int num_hosts = 0; /* ── Signal handling ─────────────────────────────────────── * * SIGINT (Ctrl+C) is trapped rather than left at its default @@ -94,11 +149,7 @@ static void on_sigint(int signum) sigint_pending = 1; } -/* ── Logging ─────────────────────────────────────────────── */ -int logging = 0; -FILE *log_file = NULL; - -/* ── BPQ / monitor state ─────────────────────────────────── */ +/* ── BPQ / monitor state (shared across all sessions) ─────── */ int mon_visible = 1; int mon_tx = 1; int mon_com = 1; @@ -106,38 +157,8 @@ 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 */ + +char port_names[32][32]; /* port 1-32 names, loaded from config */ /* ── ncurses windows ─────────────────────────────────────── */ WINDOW *status_win = NULL; @@ -148,10 +169,6 @@ WINDOW *sep2_win = NULL; /* separator above input area */ WINDOW *input_win = NULL; WINDOW *fkey_win = NULL; /* function key hint bar at bottom */ -/* ── Input buffer ────────────────────────────────────────── */ -char input_buf[INPUT_MAX]; -int input_len = 0; - /* ── ICS-213 General Message ─────────────────────────────── */ #define ICS_SBLINE_MAX 120 /* "SB @" line */ #define ICS_FIELD_MAX 80 /* To / From / Subject single-line fields */ @@ -385,13 +402,13 @@ void open_log(const char *prefix) 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; + S->log_file = fopen(filename, "w"); + if (S->log_file) S->logging = 1; } void close_log(void) { - if (log_file) { fclose(log_file); log_file = NULL; logging = 0; } + if (S->log_file) { fclose(S->log_file); S->log_file = NULL; S->logging = 0; } } /* ═══════════════════════════════════════════════════════════ @@ -431,22 +448,22 @@ int tcp_connect(const char *host, const char *port) 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); + if (S->sock < 0 || len <= 0) return -1; + if (S->logging && S->log_file) { fwrite(data, 1, len, S->log_file); fflush(S->log_file); } + return send(S->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; + if (S->sock >= 0) { close(S->sock); S->sock = -1; } + S->connected = 0; + S->connect_time = 0; + S->in_mon_frame = 0; + S->mon_flen = 0; + S->ports_parse_mode = 0; + S->ports_line_len = 0; + S->ports_loaded = 0; + S->port_seen = 0; for (int i = 0; i < 32; i++) snprintf(port_names[i], 32, "Port %d", i + 1); draw_status(); } @@ -457,26 +474,26 @@ void tcp_disconnect(void) void send_trace_options(void) { - if (sock < 0) return; + if (S->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); + send(S->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; + if (S->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); + send(S->sock, buf, len, 0); } /* Parse the 0xFF 0xFF port-info packet from SendPortsForMonitor. @@ -501,11 +518,11 @@ static void parse_portinfo(const char *data, int len) int copy = dlen < 31 ? dlen : 31; memcpy(port_names[n - 1], desc, copy); port_names[n - 1][copy] = '\0'; - port_seen |= (1u << (n - 1)); + S->port_seen |= (1u << (n - 1)); } if (p < e && *p == '|') p++; } - ports_loaded = 1; + S->ports_loaded = 1; } static int bpq_color_attr(unsigned char color_byte) @@ -522,7 +539,7 @@ static int bpq_color_attr(unsigned char color_byte) return COLOR_PAIR(CP_MAGENTA); } -/* Scan monitor frame text for "Port=N" and record N in port_seen bitmask. +/* Scan monitor frame text for "Port=N" and record N in S->port_seen bitmask. * BPQ monitor frames use "Port=N" format (e.g. "KB8PMY>APRS Port=2 "). * Port names come from the PORTS session command, not from monitor frames. */ static void detect_port_number(const char *text, int len) @@ -539,7 +556,7 @@ static void detect_port_number(const char *text, int len) 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)); + S->port_seen |= (1 << (n-1)); } } } @@ -553,7 +570,7 @@ static void parse_session_line(const char *line, int len) while (len > 0 && (unsigned char)line[len-1] <= ' ') len--; if (len == 0) return; - if (!ports_parse_mode) { + if (!S->ports_parse_mode) { /* Detect "} Ports" header (case-insensitive) */ for (int i = 0; i + 6 < len; i++) { if (line[i] == '}' && line[i+1] == ' ' && @@ -562,7 +579,7 @@ static void parse_session_line(const char *line, int len) (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; + S->ports_parse_mode = 1; return; } } @@ -583,11 +600,11 @@ static void parse_session_line(const char *line, int len) 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)); + S->port_seen |= (1 << (n-1)); } } else { - if (ports_parse_mode) ports_loaded = 1; /* list ended cleanly */ - ports_parse_mode = 0; + if (S->ports_parse_mode) S->ports_loaded = 1; /* list ended cleanly */ + S->ports_parse_mode = 0; } } @@ -595,12 +612,12 @@ static void parse_session_line(const char *line, int len) 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; + if (S->ports_line_len > 0) { + parse_session_line(S->ports_line_buf, S->ports_line_len); + S->ports_line_len = 0; } - } else if (c >= 32 && c < 128 && ports_line_len < 255) { - ports_line_buf[ports_line_len++] = (char)c; + } else if (c >= 32 && c < 128 && S->ports_line_len < 255) { + S->ports_line_buf[S->ports_line_len++] = (char)c; } } @@ -610,7 +627,7 @@ void bpq_process(const char *buf, int len) const char *end = buf + len; while (p < end) { - if (!in_mon_frame) { + if (!S->in_mon_frame) { const char *ff = memchr(p, 0xFF, end - p); if (!ff) { /* All remaining bytes are session output */ @@ -632,8 +649,8 @@ void bpq_process(const char *buf, int len) parse_portinfo(p + 1, (int)(end - p - 1)); p = end; } else { - in_mon_frame = 1; - mon_flen = 0; + S->in_mon_frame = 1; + S->mon_flen = 0; } } else { const char *fe = memchr(p, 0xFE, end - p); @@ -641,26 +658,26 @@ void bpq_process(const char *buf, int len) 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; + int room = (int)sizeof(S->mon_fbuf) - 1 - S->mon_flen; if (room > 0) { int add = seg_len < room ? seg_len : room; - memcpy(mon_fbuf + mon_flen, p, add); - mon_flen += add; + memcpy(S->mon_fbuf + S->mon_flen, p, add); + S->mon_flen += add; } } if (fe) { /* Frame complete: apply client-side NODES filter then display */ - if (mon_flen > 0) { - mon_fbuf[mon_flen] = '\0'; + if (S->mon_flen > 0) { + S->mon_fbuf[S->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; + for (int i = 0; i <= S->mon_flen - 6 && !is_nodes; i++) + if (memcmp(S->mon_fbuf + i, ">NODES", 6) == 0) is_nodes = 1; if (mon_nodes || !is_nodes) - append_mon(mon_fbuf, mon_flen, COLOR_PAIR(CP_GREEN)); + append_mon(S->mon_fbuf, S->mon_flen, COLOR_PAIR(CP_GREEN)); } - mon_flen = 0; - in_mon_frame = 0; + S->mon_flen = 0; + S->in_mon_frame = 0; p = fe + 1; } else { break; @@ -668,7 +685,7 @@ void bpq_process(const char *buf, int len) } } - if (logging && log_file) { fwrite(buf, 1, len, log_file); fflush(log_file); } + if (S->logging && S->log_file) { fwrite(buf, 1, len, S->log_file); fflush(S->log_file); } } /* ═══════════════════════════════════════════════════════════ @@ -813,10 +830,10 @@ void draw_status(void) /* Center: connection info */ char center[80]; - if (connected && current_host >= 0) { - int elapsed = (int)(time(NULL) - connect_time); + if (S->connected && S->host_idx >= 0) { + int elapsed = (int)(time(NULL) - S->connect_time); snprintf(center, sizeof(center), "%s [%02d:%02d]", - hosts[current_host].name, elapsed / 60, elapsed % 60); + hosts[S->host_idx].name, elapsed / 60, elapsed % 60); } else { snprintf(center, sizeof(center), "Not Connected"); } @@ -828,7 +845,7 @@ void draw_status(void) struct tm *t = gmtime(&now); char right[32]; snprintf(right, sizeof(right), "%s%02d:%02d UTC ", - logging ? "LOG " : "", t->tm_hour, t->tm_min); + S->logging ? "LOG " : "", t->tm_hour, t->tm_min); int rx = COLS - (int)strlen(right); if (rx > 0) mvwprintw(status_win, 0, rx, "%s", right); @@ -923,49 +940,49 @@ void append_mon(const char *text, int len, int attr) const char *p = text; const char *end = text + len; - mon_part_cur = attr; - if (mon_part_len == 0) mon_part_base = attr; + S->mon_part_cur = attr; + if (S->mon_part_len == 0) S->mon_part_base = attr; - if (mon_win && mon_scroll == 0) - wattron(mon_win, mon_part_cur); + if (mon_win && S->mon_scroll == 0) + wattron(mon_win, S->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 (S->mon_part_len + 2 < SCRL_W) { + S->mon_part[S->mon_part_len++] = (char)0x1B; + S->mon_part[S->mon_part_len++] = p[1]; } - if (mon_win && mon_scroll == 0) { - wattroff(mon_win, mon_part_cur); + if (mon_win && S->mon_scroll == 0) { + wattroff(mon_win, S->mon_part_cur); wattron(mon_win, na); } - mon_part_cur = na; + S->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) + ScrlLine *sl = &S->mon_sbuf[S->mon_sbuf_head]; + memcpy(sl->data, S->mon_part, S->mon_part_len); + sl->len = S->mon_part_len; + sl->base_attr = S->mon_part_base; + S->mon_sbuf_head = (S->mon_sbuf_head + 1) % SCRL_MAX; + if (S->mon_sbuf_count < SCRL_MAX) S->mon_sbuf_count++; + S->mon_part_len = 0; + S->mon_part_base = S->mon_part_cur; + if (mon_win && S->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) + if (S->mon_part_len < SCRL_W - 1) + S->mon_part[S->mon_part_len++] = (char)c; + if (mon_win && S->mon_scroll == 0) waddch(mon_win, c); p++; } else { p++; } } - if (mon_win && mon_scroll == 0) { - wattroff(mon_win, mon_part_cur); + if (mon_win && S->mon_scroll == 0) { + wattroff(mon_win, S->mon_part_cur); wrefresh(mon_win); if (input_win) wrefresh(input_win); } @@ -978,49 +995,49 @@ void append_out(const char *text, int len, int attr) const char *p = text; const char *end = text + len; - out_part_cur = attr; - if (out_part_len == 0) out_part_base = attr; + S->out_part_cur = attr; + if (S->out_part_len == 0) S->out_part_base = attr; - if (out_win && out_scroll == 0) - wattron(out_win, out_part_cur); + if (out_win && S->out_scroll == 0) + wattron(out_win, S->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 (S->out_part_len + 2 < SCRL_W) { + S->out_part[S->out_part_len++] = (char)0x1B; + S->out_part[S->out_part_len++] = p[1]; } - if (out_win && out_scroll == 0) { - wattroff(out_win, out_part_cur); + if (out_win && S->out_scroll == 0) { + wattroff(out_win, S->out_part_cur); wattron(out_win, na); } - out_part_cur = na; + S->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) + ScrlLine *sl = &S->out_sbuf[S->out_sbuf_head]; + memcpy(sl->data, S->out_part, S->out_part_len); + sl->len = S->out_part_len; + sl->base_attr = S->out_part_base; + S->out_sbuf_head = (S->out_sbuf_head + 1) % SCRL_MAX; + if (S->out_sbuf_count < SCRL_MAX) S->out_sbuf_count++; + S->out_part_len = 0; + S->out_part_base = S->out_part_cur; + if (out_win && S->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) + if (S->out_part_len < SCRL_W - 1) + S->out_part[S->out_part_len++] = (char)c; + if (out_win && S->out_scroll == 0) waddch(out_win, c); p++; } else { p++; } } - if (out_win && out_scroll == 0) { - wattroff(out_win, out_part_cur); + if (out_win && S->out_scroll == 0) { + wattroff(out_win, S->out_part_cur); wrefresh(out_win); if (input_win) wrefresh(input_win); } @@ -1040,16 +1057,16 @@ void draw_input(void) mvwaddstr(input_win, 0, 0, "> "); /* Buffer content — wraps at COLS, '> ' prefix is 2 chars */ - for (int i = 0; i < input_len; i++) { + for (int i = 0; i < S->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]); + mvwaddch(input_win, r, c, (unsigned char)S->input_buf[i]); } /* Cursor position */ - int cpos = 2 + input_len; + int cpos = 2 + S->input_len; int cr = cpos / COLS; int cc = cpos % COLS; if (cr < INPUT_OUTER) wmove(input_win, cr, cc); @@ -1064,19 +1081,19 @@ void draw_input(void) static void scrl_redraw_out(void) { - scrl_draw_window(out_win, out_sbuf, out_sbuf_count, out_sbuf_head, out_scroll); + scrl_draw_window(out_win, S->out_sbuf, S->out_sbuf_count, S->out_sbuf_head, S->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); + scrl_draw_window(mon_win, S->mon_sbuf, S->mon_sbuf_count, S->mon_sbuf_head, S->mon_scroll); } static void update_sep_labels(void) { if (sep_win) { - if (out_scroll > 0) + if (S->out_scroll > 0) fill_sep(sep_win, " Output [scrolled - PgDn=live] "); else fill_sep(sep_win, " Output "); @@ -1126,7 +1143,7 @@ static void config_menu(void) 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_host = (S->host_idx >= 0 && S->host_idx < num_hosts) ? S->host_idx : 0; int cur_field = 0; if (num_hosts == 0) { memset(&hosts[0], 0, sizeof(HostEntry)); num_hosts = 1; } @@ -1492,7 +1509,7 @@ 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 (S->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; @@ -1513,7 +1530,7 @@ static int mon_ports_page(void) werase(w); draw_box(w); mvwprintw(w, 0, (mw - 16) / 2, " Port Selection "); - if (port_seen == 0) + if (S->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]; @@ -1605,14 +1622,14 @@ static void show_help(void) /* ── Background send connection ────────────────────────────── * F2 always opens its OWN socket for sending, completely separate - * from the main interactive session's global `sock`/`connected`. + * from the main interactive session's global `S->sock`/`S->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 + * frame parser keeps state in globals (S->in_mon_frame, S->mon_flen, + * S->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 @@ -1733,9 +1750,9 @@ static void bg_send_block(int fd, char lines[][ICS_MSG_LINELEN], int n, int *abo } /* Opens a new socket to hosts[idx], logs in if credentials are saved, - * and drains the login banner briefly. Returns the connected fd, or + * and drains the login banner briefly. Returns the S->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 + * intentionally separate from do_connect()/S->sock/S->connected - it never * touches the main session's connection state. */ static int bg_connect_and_login(int idx) { @@ -2393,8 +2410,8 @@ static void pktnet_checkin_form(void) 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); + if (S->host_idx >= 0 && hosts[S->host_idx].username[0]) + strncpy(f.from, hosts[S->host_idx].username, CHKIN_FIELD_MAX - 1); f.type_idx = 0; /* EXERCISE */ f.band_idx = 2; /* VHF */ f.session_idx = 1; /* AX25 Packet */ @@ -2597,22 +2614,22 @@ static void pktnet_checkin_form(void) static void do_connect(int idx) { if (idx < 0 || idx >= num_hosts) return; - if (connected) tcp_disconnect(); + if (S->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) { + S->sock = tcp_connect(hosts[idx].host, hosts[idx].port); + if (S->sock < 0) { append_out("Connection failed.\n", 19, COLOR_PAIR(CP_RED)); return; } - connected = 1; - current_host = idx; - connect_time = time(NULL); + S->connected = 1; + S->host_idx = idx; + S->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); @@ -2636,7 +2653,7 @@ static void do_connect(int idx) /* 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. */ + * the full port list (S->ports_loaded) or the deadline passes. */ { char rbuf[4096]; struct timeval t0, now; @@ -2646,13 +2663,13 @@ static void do_connect(int idx) long elapsed = (now.tv_sec - t0.tv_sec) * 1000000L + (now.tv_usec - t0.tv_usec); if (elapsed >= 2000000L) break; - if (ports_loaded) break; + if (S->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); + FD_ZERO(&rfds); FD_SET(S->sock, &rfds); + if (select(S->sock + 1, &rfds, NULL, NULL, &tv) <= 0) break; + int n = recv(S->sock, rbuf, sizeof(rbuf) - 1, 0); if (n <= 0) break; bpq_process(rbuf, n); } @@ -2665,14 +2682,14 @@ static void do_connect(int idx) * "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) { + if (S->connected && S->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); + FD_ZERO(&rfds2); FD_SET(S->sock, &rfds2); + if (select(S->sock + 1, &rfds2, NULL, NULL, &tv2) > 0) { + int n = recv(S->sock, rbuf, sizeof(rbuf) - 1, 0); if (n > 0) bpq_process(rbuf, n); } } @@ -2753,7 +2770,7 @@ static void ctrl_a_menu(void) break; case 'd': - if (connected) { + if (S->connected) { tcp_disconnect(); append_out("*** Disconnected\n", 17, COLOR_PAIR(CP_RED)); } @@ -2768,17 +2785,17 @@ static void ctrl_a_menu(void) break; case 'l': - if (logging) { + if (S->logging) { close_log(); append_out("[Logging OFF]\n", 14, COLOR_PAIR(CP_YELLOW)); - } else if (connected && current_host >= 0) { + } else if (S->connected && S->host_idx >= 0) { char prefix[120]; snprintf(prefix, sizeof(prefix), "%s_%s", - hosts[current_host].host, hosts[current_host].port); + hosts[S->host_idx].host, hosts[S->host_idx].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)); + append_out("Connect first to start S->logging.\n", 32, COLOR_PAIR(CP_YELLOW)); } draw_status(); break; @@ -2792,8 +2809,8 @@ static void ctrl_a_menu(void) break; case 'q': - if (connected) tcp_disconnect(); - if (logging) close_log(); + if (S->connected) tcp_disconnect(); + if (S->logging) close_log(); endwin(); exit(0); } @@ -2823,7 +2840,7 @@ static void handle_key(int ch) } case KEY_F(4): - if (connected) { + if (S->connected) { tcp_disconnect(); append_out("*** Disconnected\n", 17, COLOR_PAIR(CP_RED)); draw_status(); @@ -2835,24 +2852,24 @@ static void handle_key(int ch) return; case KEY_F(6): - if (logging) { + if (S->logging) { close_log(); append_out("[Logging OFF]\n", 14, COLOR_PAIR(CP_YELLOW)); - } else if (connected && current_host >= 0) { + } else if (S->connected && S->host_idx >= 0) { char prefix[120]; snprintf(prefix, sizeof(prefix), "%s_%s", - hosts[current_host].host, hosts[current_host].port); + hosts[S->host_idx].host, hosts[S->host_idx].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)); + append_out("Connect first to start S->logging.\n", 32, COLOR_PAIR(CP_YELLOW)); } draw_status(); return; case KEY_F(10): - if (connected) tcp_disconnect(); - if (logging) close_log(); + if (S->connected) tcp_disconnect(); + if (S->logging) close_log(); endwin(); exit(0); @@ -2865,9 +2882,9 @@ static void handle_key(int ch) 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; + S->out_scroll += page; + int max_scroll = S->out_sbuf_count > 0 ? S->out_sbuf_count - 1 : 0; + if (S->out_scroll > max_scroll) S->out_scroll = max_scroll; scrl_redraw_out(); update_sep_labels(); if (input_win) wrefresh(input_win); @@ -2875,10 +2892,10 @@ static void handle_key(int ch) return; case KEY_NPAGE: /* PgDn — scroll output forward / return to live */ - if (out_win && out_scroll > 0) { + if (out_win && S->out_scroll > 0) { int page = getmaxy(out_win); - out_scroll -= page; - if (out_scroll < 0) out_scroll = 0; + S->out_scroll -= page; + if (S->out_scroll < 0) S->out_scroll = 0; scrl_redraw_out(); update_sep_labels(); if (input_win) wrefresh(input_win); @@ -2888,83 +2905,83 @@ static void handle_key(int ch) 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; + S->mon_scroll += page; + int max_scroll = S->mon_sbuf_count > 0 ? S->mon_sbuf_count - 1 : 0; + if (S->mon_scroll > max_scroll) S->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) { + if (mon_win && mon_visible && S->mon_scroll > 0) { int page = getmaxy(mon_win); - mon_scroll -= page; - if (mon_scroll < 0) mon_scroll = 0; + S->mon_scroll -= page; + if (S->mon_scroll < 0) S->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'; + if (S->input_len == 0) return; + S->input_buf[S->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); + strncpy(S->hist_buf[S->hist_head], S->input_buf, INPUT_MAX - 1); + S->hist_buf[S->hist_head][INPUT_MAX - 1] = '\0'; + S->hist_head = (S->hist_head + 1) % HIST_MAX; + if (S->hist_count < HIST_MAX) S->hist_count++; + S->hist_pos = -1; + if (S->connected) { + tcp_send(S->input_buf, S->input_len); tcp_send("\r", 1); char echo[INPUT_MAX + 4]; - int elen = snprintf(echo, sizeof(echo), "> %s\n", input_buf); + int elen = snprintf(echo, sizeof(echo), "> %s\n", S->input_buf); append_out(echo, elen, COLOR_PAIR(CP_CYAN) | A_BOLD); } else { - append_out("Not connected -- Ctrl+A to open menu.\n", 38, + append_out("Not S->connected -- Ctrl+A to open menu.\n", 38, COLOR_PAIR(CP_YELLOW)); } - input_len = 0; + S->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++; + if (S->hist_count == 0) break; + if (S->hist_pos == -1) { + strncpy(S->hist_save, S->input_buf, INPUT_MAX - 1); + S->hist_save[INPUT_MAX - 1] = '\0'; + S->hist_pos = 0; + } else if (S->hist_pos < S->hist_count - 1) { + S->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); + int idx = (S->hist_head - 1 - S->hist_pos + HIST_MAX * 2) % HIST_MAX; + strncpy(S->input_buf, S->hist_buf[idx], INPUT_MAX - 1); + S->input_buf[INPUT_MAX - 1] = '\0'; + S->input_len = (int)strlen(S->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 */ + if (S->hist_pos < 0) break; + S->hist_pos--; + if (S->hist_pos < 0) { + S->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'; + int idx = (S->hist_head - 1 - S->hist_pos + HIST_MAX * 2) % HIST_MAX; + strncpy(S->input_buf, S->hist_buf[idx], INPUT_MAX - 1); + S->input_buf[INPUT_MAX - 1] = '\0'; } - input_len = (int)strlen(input_buf); + S->input_len = (int)strlen(S->input_buf); break; case KEY_BACKSPACE: case 127: case '\b': - if (input_len > 0) input_len--; + if (S->input_len > 0) S->input_len--; break; default: - if (ch >= 32 && ch < 256 && input_len < INPUT_MAX - 1) - input_buf[input_len++] = (char)ch; + if (ch >= 32 && ch < 256 && S->input_len < INPUT_MAX - 1) + S->input_buf[S->input_len++] = (char)ch; break; } @@ -2988,7 +3005,7 @@ static int confirm_quit_popup(void) 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."); + if (S->connected) mvwprintw(w, 3, 3, "This will close the current session."); mvwprintw(w, mh - 2, 3, "Y = quit N / Esc = resume"); wrefresh(w); @@ -3013,8 +3030,8 @@ void interactive_loop(void) if (sigint_pending) { sigint_pending = 0; if (confirm_quit_popup()) { - if (connected) tcp_disconnect(); - if (logging) close_log(); + if (S->connected) tcp_disconnect(); + if (S->logging) close_log(); endwin(); exit(0); } @@ -3025,17 +3042,17 @@ void interactive_loop(void) FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); - if (connected && sock >= 0) FD_SET(sock, &readfds); + if (S->connected && S->sock >= 0) FD_SET(S->sock, &readfds); tv.tv_sec = 1; tv.tv_usec = 0; - int maxfd = (connected && sock > STDIN_FILENO) ? sock : STDIN_FILENO; + int maxfd = (S->connected && S->sock > STDIN_FILENO) ? S->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 (S->connected && S->sock >= 0 && FD_ISSET(S->sock, &readfds)) { + int n = recv(S->sock, recv_buf, sizeof(recv_buf)-1, 0); if (n > 0) { bpq_process(recv_buf, n); draw_input(); @@ -3073,6 +3090,15 @@ int main(int argc, char *argv[]) } } + /* Initialise session 0 — the one and only session in CLI mode */ + memset(sessions, 0, sizeof(sessions)); + sessions[0].active = 1; + sessions[0].sock = -1; + sessions[0].host_idx = -1; + sessions[0].hist_pos = -1; + snprintf(sessions[0].label, SESSION_LABEL, "Session 1"); + S = &sessions[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); @@ -3122,8 +3148,8 @@ int main(int argc, char *argv[]) interactive_loop(); - if (connected) tcp_disconnect(); - if (logging) close_log(); + if (S->connected) tcp_disconnect(); + if (S->logging) close_log(); endwin(); return 0; }