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.
TermTCP/termtcp_cli.c

1531 lines
54 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 <time.h>
#include <ncurses.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#define VERSION "0.0.49"
#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;
/* ── 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;
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 */
/* ── 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;
/* ── 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;
ports_parse_mode = 0;
ports_line_len = 0;
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);
}
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 {
ports_parse_mode = 0; /* Non-matching line ends the list */
}
}
/* 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));
}
in_mon_frame = 1;
p = ff + 1;
} 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);
append_mon(p, seg_len, COLOR_PAIR(CP_GREEN));
}
if (fe) { 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 plain ASCII border (+, -, |) around window w.
* Title (if any) is written over the top border by the caller after this. */
static void ascii_box(WINDOW *w)
{
int rows, cols;
getmaxyx(w, rows, cols);
mvwaddch(w, 0, 0, '+');
mvwaddch(w, 0, cols-1, '+');
mvwaddch(w, rows-1, 0, '+');
mvwaddch(w, rows-1, cols-1, '+');
for (int c = 1; c < cols-1; c++) {
mvwaddch(w, 0, c, '-');
mvwaddch(w, rows-1, c, '-');
}
for (int r = 1; r < rows-1; r++) {
mvwaddch(w, r, 0, '|');
mvwaddch(w, r, cols-1, '|');
}
}
/* 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));
whline(w, '-', 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;
werase(win);
if (count == 0) { 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);
waddch(win, '\n');
}
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();
}
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); ascii_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);
touchwin(stdscr); refresh();
refresh_all();
}
/* ═══════════════════════════════════════════════════════════
* Monitor options (Ctrl+A -> M)
* ═══════════════════════════════════════════════════════════ */
static void monitor_options_menu(void)
{
/* Two pages: 0 = options, 1 = ports. Tab switches between them. */
static const char *opt_labels[] = {
"Monitor TX",
"Monitor Supervisor",
"Monitor Nodes",
"Monitor UI Only",
"Enable Colour",
"Show Monitor Pane",
};
int *opt_vals[] = {
&mon_tx, &mon_com, &mon_nodes, &mon_ui_only, &mon_colour, &mon_visible
};
const int N_OPTS = 6;
int mh = 14, mw = 46;
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 page = 0; /* 0=options 1=ports */
int cur = 0;
int running = 1;
while (running) {
werase(w); ascii_box(w);
if (page == 0) {
mvwprintw(w, 0, (mw-18)/2, " Monitor Options ");
mvwprintw(w, 1, 2, "[Options] Ports Tab=switch");
for (int i = 0; i < N_OPTS; i++) {
if (i == cur) wattron(w, A_REVERSE);
mvwprintw(w, 3+i, 3, "[%c] %s",
*opt_vals[i] ? 'X' : ' ', opt_labels[i]);
if (i == cur) wattroff(w, A_REVERSE);
}
mvwprintw(w, mh-3, 3, "Up/Down = navigate");
mvwprintw(w, mh-2, 3, "Space = toggle Esc/Q = close");
} else {
/* Build list of ports to show: detected ones, or 1-8 if none yet */
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;
/* Resize window if needed */
int need_h = 4 + n_ports + 1;
if (need_h < 8) need_h = 8;
if (need_h != mh) {
mh = need_h;
delwin(w);
my = (LINES-mh)/2; if (my<0) my=0;
mx = (COLS-mw)/2; if (mx<0) mx=0;
w = newwin(mh, mw, my, mx);
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
keypad(w, TRUE);
nodelay(w, FALSE);
werase(w); ascii_box(w);
}
mvwprintw(w, 0, (mw-16)/2, " Port Selection ");
mvwprintw(w, 1, 2, " Options [Ports] Tab=switch");
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, 3+i, 3, "[%c] %2d %-27s",
(port_mask >> (pn-1)) & 1 ? 'X' : ' ',
pn, port_names[pn-1]);
if (i == cur) wattroff(w, A_REVERSE);
}
mvwprintw(w, mh-2, 3, "Space = toggle Esc/Q = close");
}
wrefresh(w);
/* Build current port list for key handling */
int port_list2[32], n_ports2 = 0;
if (page == 1) {
for (int b = 0; b < 32; b++)
if (port_seen & (1 << b)) port_list2[n_ports2++] = b + 1;
if (n_ports2 == 0)
for (int b = 0; b < 8; b++) port_list2[n_ports2++] = b + 1;
}
int max_items = (page == 0) ? N_OPTS : n_ports2;
int ch = wgetch(w);
switch (ch) {
case KEY_UP:
cur = (cur - 1 + max_items) % max_items; break;
case KEY_DOWN:
cur = (cur + 1) % max_items; break;
case '\t':
page = 1 - page; cur = 0; break;
case ' ': case '\r': case '\n': case KEY_ENTER:
if (page == 0) {
if (cur == 5) { /* Show Monitor Pane — needs window rebuild */
mon_visible = !mon_visible;
delwin(w);
touchwin(stdscr); refresh();
create_windows();
refresh_all();
return;
}
*opt_vals[cur] = !(*opt_vals[cur]);
send_trace_options();
} else {
int pn = (cur < n_ports2) ? port_list2[cur] : cur + 1;
port_mask ^= (1 << (pn - 1));
send_trace_options();
}
break;
case 27: case 'Q': case 'q':
running = 0; break;
}
}
delwin(w);
touchwin(stdscr); refresh();
refresh_all();
}
/* ═══════════════════════════════════════════════════════════
* Help overlay
* ═══════════════════════════════════════════════════════════ */
static void show_help(void)
{
int mh = 18, mw = 52;
WINDOW *w = newwin(mh, mw, (LINES-mh)/2, (COLS-mw)/2);
wbkgd(w, COLOR_PAIR(CP_DEFAULT));
ascii_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 H This help");
mvwprintw(w, 9, 3, "Ctrl+A Q Quit");
mvwprintw(w, 11, 3, "Enter Send line to server");
mvwprintw(w, 12, 3, "Up / Down Input history");
mvwprintw(w, 13, 3, "PgUp / PgDn Scroll output window");
mvwprintw(w, 14, 3, "Backspace Delete char");
mvwprintw(w, mh-2, 3, "Press any key to close");
wrefresh(w);
nodelay(w, FALSE);
wgetch(w);
delwin(w);
touchwin(stdscr); refresh();
refresh_all();
}
/* ═══════════════════════════════════════════════════════════
* 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));
}
send_trace_options();
/* Ask the node for its port list; response is parsed in parse_session_line() */
tcp_send("PORTS\r", 6);
draw_status();
}
/* ═══════════════════════════════════════════════════════════
* 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));
ascii_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, "H - Help");
mvwprintw(w, 8, 4, "Q - Quit");
mvwprintw(w, mh-2, 4, "Esc = close");
wrefresh(w);
nodelay(w, FALSE);
int ch = wgetch(w);
delwin(w);
touchwin(stdscr); refresh();
refresh_all();
switch (ch | 0x20) {
case 'c':
if (num_hosts == 1) {
do_connect(0);
} else if (num_hosts > 1) {
int ph = num_hosts + 5, pw = 38;
WINDOW *hw = newwin(ph, pw, (LINES-ph)/2, (COLS-pw)/2);
wbkgd(hw, COLOR_PAIR(CP_DEFAULT));
box(hw, 0, 0);
mvwprintw(hw, 0, (pw-14)/2, " Select Host ");
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);
int hch = wgetch(hw);
delwin(hw);
touchwin(stdscr); refresh();
refresh_all();
if (hch >= '0' && hch < '0' + num_hosts)
do_connect(hch - '0');
}
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 '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 '\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) {
strncpy(input_buf, hist_save, INPUT_MAX - 1);
input_buf[INPUT_MAX - 1] = '\0';
} 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
* ═══════════════════════════════════════════════════════════ */
void interactive_loop(void)
{
fd_set readfds;
struct timeval tv;
char recv_buf[1024];
draw_input();
while (1) {
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();
initscr();
cbreak();
noecho();
set_escdelay(25); /* fast Escape key response */
init_colors();
curs_set(1);
create_windows();
draw_status();
{
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;
}

Powered by TurnKey Linux.