Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
3c0453320f
commit
c6450a4c92
@ -1,714 +0,0 @@
|
||||
/* termtcp_gui.cpp — FLTK GUI for TermTCP
|
||||
* BPQTermTCP protocol client for BPQ32 packet radio nodes.
|
||||
* Compile: see Makefile target 'gui'
|
||||
*/
|
||||
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Double_Window.H>
|
||||
#include <FL/Fl_Menu_Bar.H>
|
||||
#include <FL/Fl_Text_Display.H>
|
||||
#include <FL/Fl_Text_Buffer.H>
|
||||
#include <FL/Fl_Input.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
#include <FL/Fl_Return_Button.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Tile.H>
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <FL/Fl_Window.H>
|
||||
#include <FL/fl_ask.H>
|
||||
|
||||
extern "C" {
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
}
|
||||
|
||||
#define GUI_VERSION "0.0.60"
|
||||
#define MAX_HOSTS 10
|
||||
#define RECV_SZ 4096
|
||||
#define MON_FBUF_SZ 4096
|
||||
|
||||
/* ── Config ──────────────────────────────────────────────── */
|
||||
|
||||
struct HostEntry {
|
||||
char host[100];
|
||||
char port[10];
|
||||
char username[80];
|
||||
char password[80];
|
||||
char name[50];
|
||||
};
|
||||
|
||||
static HostEntry hosts[MAX_HOSTS];
|
||||
static int num_hosts = 0;
|
||||
static char cfg_path[512];
|
||||
|
||||
static int mon_visible = 1;
|
||||
static int mon_tx = 1;
|
||||
static int mon_com = 1;
|
||||
static int mon_nodes = 0;
|
||||
static int mon_colour = 1;
|
||||
static int mon_ui_only = 1;
|
||||
static int port_mask = 0xFFFF;
|
||||
static char port_names[32][32];
|
||||
|
||||
/* ── Session state ───────────────────────────────────────── */
|
||||
|
||||
static int g_sock = -1;
|
||||
static int g_connected = 0;
|
||||
static int g_host_idx = -1;
|
||||
static time_t g_conn_time = 0;
|
||||
static int g_in_mon = 0;
|
||||
static char g_mon_fbuf[MON_FBUF_SZ];
|
||||
static int g_mon_flen = 0;
|
||||
|
||||
/* ── GUI widgets ─────────────────────────────────────────── */
|
||||
|
||||
static Fl_Double_Window *main_win = nullptr;
|
||||
static Fl_Text_Display *out_disp = nullptr;
|
||||
static Fl_Text_Display *mon_disp = nullptr;
|
||||
static Fl_Text_Buffer *out_buf = nullptr;
|
||||
static Fl_Text_Buffer *out_styl = nullptr;
|
||||
static Fl_Text_Buffer *mon_buf = nullptr;
|
||||
static Fl_Text_Buffer *mon_styl = nullptr;
|
||||
static Fl_Input *inp = nullptr;
|
||||
static Fl_Box *status_lbl = nullptr;
|
||||
static Fl_Menu_Bar *menu_bar = nullptr;
|
||||
|
||||
/* Style table — index from 'A'.
|
||||
* A=default B=green(monitor) C=red(error)
|
||||
* D=yellow(status) E=cyan(info) F=magenta */
|
||||
static const Fl_Text_Display::Style_Table_Entry STYLES[] = {
|
||||
{ FL_FOREGROUND_COLOR, FL_COURIER, 13 }, /* A */
|
||||
{ fl_rgb_color(0,180,0), FL_COURIER, 13 }, /* B */
|
||||
{ FL_RED, FL_COURIER, 13 }, /* C */
|
||||
{ fl_rgb_color(180,140,0), FL_COURIER, 13 }, /* D */
|
||||
{ fl_rgb_color(0,190,210), FL_COURIER, 13 }, /* E */
|
||||
{ FL_MAGENTA, FL_COURIER, 13 }, /* F */
|
||||
};
|
||||
#define N_STYLES 6
|
||||
|
||||
/* ── Forward declarations ────────────────────────────────── */
|
||||
|
||||
static void tcp_disconnect(void);
|
||||
static void update_status(void);
|
||||
static void cb_timer(void *);
|
||||
|
||||
/* ── Config I/O ──────────────────────────────────────────── */
|
||||
|
||||
static void load_config(void)
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
snprintf(cfg_path, sizeof(cfg_path), "%s/.BPQTermTCP.ini", home);
|
||||
|
||||
FILE *fp = fopen(cfg_path, "r");
|
||||
if (!fp) return;
|
||||
|
||||
char line[256];
|
||||
int hi = -1;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
/* strip trailing newline */
|
||||
int l = (int)strlen(line);
|
||||
while (l > 0 && (line[l-1]=='\n'||line[l-1]=='\r'||line[l-1]==' '))
|
||||
line[--l] = '\0';
|
||||
|
||||
if (line[0] == '[') {
|
||||
/* section header */
|
||||
if (strncmp(line, "[Host", 5) == 0 && num_hosts < MAX_HOSTS) {
|
||||
hi = num_hosts++;
|
||||
memset(&hosts[hi], 0, sizeof(HostEntry));
|
||||
} else {
|
||||
hi = -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
*eq = '\0';
|
||||
const char *key = line, *val = eq + 1;
|
||||
|
||||
if (hi >= 0) {
|
||||
if (!strcmp(key,"Host")) { strncpy(hosts[hi].host, val, 99); }
|
||||
else if (!strcmp(key,"Port")) { strncpy(hosts[hi].port, val, 9); }
|
||||
else if (!strcmp(key,"Username")) { strncpy(hosts[hi].username, val, 79); }
|
||||
else if (!strcmp(key,"Password")) { strncpy(hosts[hi].password, val, 79); }
|
||||
else if (!strcmp(key,"Name")) { strncpy(hosts[hi].name, val, 49); }
|
||||
} else {
|
||||
if (!strcmp(key,"MonTX")) mon_tx = atoi(val);
|
||||
else if (!strcmp(key,"MonCom")) mon_com = atoi(val);
|
||||
else if (!strcmp(key,"MonNodes")) mon_nodes = atoi(val);
|
||||
else if (!strcmp(key,"MonUIOnly")) mon_ui_only = atoi(val);
|
||||
else if (!strcmp(key,"MonVisible")) mon_visible = atoi(val);
|
||||
else if (!strcmp(key,"PortMask")) port_mask = atoi(val);
|
||||
else if (strncmp(key,"PortName",8)==0) {
|
||||
int pn = atoi(key+8);
|
||||
if (pn >= 1 && pn <= 32)
|
||||
snprintf(port_names[pn-1], 32, "%s", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/* ── Text display helpers ────────────────────────────────── */
|
||||
|
||||
static void append_styled(Fl_Text_Buffer *tbuf, Fl_Text_Buffer *sbuf,
|
||||
const char *text, int len, char style)
|
||||
{
|
||||
if (len <= 0) return;
|
||||
|
||||
/* Build a style string the same length as text (minus any stripped
|
||||
* BPQ 0x1B colour-escape pairs which we skip). */
|
||||
char *tmp = (char *)malloc(len + 1);
|
||||
char *sty = (char *)malloc(len + 1);
|
||||
if (!tmp || !sty) { free(tmp); free(sty); return; }
|
||||
|
||||
int ti = 0;
|
||||
char cur = style;
|
||||
for (int i = 0; i < len; ) {
|
||||
unsigned char c = (unsigned char)text[i];
|
||||
if (c == 0x1B && i + 1 < len) {
|
||||
/* BPQ colour byte — map to style char */
|
||||
unsigned char cb = (unsigned char)text[i+1];
|
||||
int idx = (int)cb - 10;
|
||||
if (idx < 0) cur = 'A';
|
||||
else if (idx < 4) cur = 'E'; /* dark blue → cyan */
|
||||
else if (idx < 20) cur = 'B'; /* green */
|
||||
else if (idx < 60) cur = 'C'; /* reds */
|
||||
else cur = 'D'; /* bright → yellow */
|
||||
i += 2;
|
||||
} else if (c == '\r') {
|
||||
i++; /* skip bare CR */
|
||||
} else {
|
||||
tmp[ti] = (char)c;
|
||||
sty[ti] = cur;
|
||||
ti++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
tmp[ti] = '\0';
|
||||
sty[ti] = '\0';
|
||||
|
||||
tbuf->append(tmp);
|
||||
sbuf->append(sty);
|
||||
|
||||
free(tmp);
|
||||
free(sty);
|
||||
}
|
||||
|
||||
static void out_append(const char *text, int len, char style = 'A')
|
||||
{
|
||||
append_styled(out_buf, out_styl, text, len, style);
|
||||
/* scroll to bottom */
|
||||
out_disp->scroll(out_buf->count_lines(0, out_buf->length()), 0);
|
||||
}
|
||||
|
||||
static void mon_append(const char *text, int len, char style = 'B')
|
||||
{
|
||||
if (!mon_visible) return;
|
||||
append_styled(mon_buf, mon_styl, text, len, style);
|
||||
mon_disp->scroll(mon_buf->count_lines(0, mon_buf->length()), 0);
|
||||
}
|
||||
|
||||
/* ── BPQ protocol ────────────────────────────────────────── */
|
||||
|
||||
static void send_trace_options(bool with_ports)
|
||||
{
|
||||
if (g_sock < 0) return;
|
||||
char buf[80];
|
||||
int len;
|
||||
if (with_ports)
|
||||
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);
|
||||
else
|
||||
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(g_sock, buf, len, 0);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
if (p < e && *p == '|') p++;
|
||||
}
|
||||
}
|
||||
|
||||
static void bpq_process(const char *buf, int len)
|
||||
{
|
||||
const char *p = buf;
|
||||
const char *end = buf + len;
|
||||
|
||||
while (p < end) {
|
||||
if (!g_in_mon) {
|
||||
const char *ff = (const char *)memchr(p, 0xFF, end - p);
|
||||
if (!ff) {
|
||||
out_append(p, (int)(end - p));
|
||||
break;
|
||||
}
|
||||
if (ff > p)
|
||||
out_append(p, (int)(ff - p));
|
||||
p = ff + 1;
|
||||
if (p < end && (unsigned char)*p == 0xFF) {
|
||||
parse_portinfo(p + 1, (int)(end - p - 1));
|
||||
p = end;
|
||||
} else {
|
||||
g_in_mon = 1;
|
||||
g_mon_flen = 0;
|
||||
}
|
||||
} else {
|
||||
const char *fe = (const char *)memchr(p, 0xFE, end - p);
|
||||
int seg_len = fe ? (int)(fe - p) : (int)(end - p);
|
||||
if (seg_len > 0) {
|
||||
int room = MON_FBUF_SZ - 1 - g_mon_flen;
|
||||
int add = seg_len < room ? seg_len : room;
|
||||
memcpy(g_mon_fbuf + g_mon_flen, p, add);
|
||||
g_mon_flen += add;
|
||||
}
|
||||
if (fe) {
|
||||
if (g_mon_flen > 0) {
|
||||
/* NODES filter */
|
||||
int is_nodes = 0;
|
||||
for (int i = 0; i <= g_mon_flen - 6 && !is_nodes; i++)
|
||||
if (memcmp(g_mon_fbuf + i, ">NODES", 6) == 0) is_nodes = 1;
|
||||
if (mon_nodes || !is_nodes) {
|
||||
g_mon_fbuf[g_mon_flen] = '\n';
|
||||
mon_append(g_mon_fbuf, g_mon_flen + 1);
|
||||
}
|
||||
}
|
||||
g_mon_flen = 0;
|
||||
g_in_mon = 0;
|
||||
p = fe + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Socket handling ─────────────────────────────────────── */
|
||||
|
||||
static void on_socket_ready(int /*fd*/, void * /*data*/)
|
||||
{
|
||||
char buf[RECV_SZ];
|
||||
int n = recv(g_sock, buf, sizeof(buf) - 1, 0);
|
||||
if (n > 0) {
|
||||
bpq_process(buf, n);
|
||||
} else if (n == 0 || (n < 0 && errno != EAGAIN)) {
|
||||
out_append("\n*** Connection closed by server ***\n", 37, 'C');
|
||||
tcp_disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static int tcp_connect_to(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= num_hosts) return -1;
|
||||
|
||||
struct addrinfo hints, *res = nullptr;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
if (getaddrinfo(hosts[idx].host, hosts[idx].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; }
|
||||
|
||||
if (connect(fd, res->ai_addr, res->ai_addrlen) != 0) {
|
||||
close(fd); freeaddrinfo(res); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void tcp_disconnect(void)
|
||||
{
|
||||
if (g_sock >= 0) {
|
||||
Fl::remove_fd(g_sock);
|
||||
close(g_sock);
|
||||
g_sock = -1;
|
||||
}
|
||||
g_connected = 0;
|
||||
g_host_idx = -1;
|
||||
g_conn_time = 0;
|
||||
g_in_mon = 0;
|
||||
g_mon_flen = 0;
|
||||
update_status();
|
||||
}
|
||||
|
||||
static void tcp_send(const char *text, int len)
|
||||
{
|
||||
if (g_sock < 0 || !g_connected) return;
|
||||
send(g_sock, text, len, 0);
|
||||
}
|
||||
|
||||
/* ── Status bar ──────────────────────────────────────────── */
|
||||
|
||||
static void update_status(void)
|
||||
{
|
||||
static char buf[160];
|
||||
if (!g_connected || g_host_idx < 0) {
|
||||
snprintf(buf, sizeof(buf), " Not connected");
|
||||
} else {
|
||||
time_t elapsed = time(nullptr) - g_conn_time;
|
||||
int h = (int)(elapsed / 3600);
|
||||
int m = (int)((elapsed % 3600) / 60);
|
||||
int s = (int)(elapsed % 60);
|
||||
time_t now = time(nullptr);
|
||||
struct tm *utc = gmtime(&now);
|
||||
snprintf(buf, sizeof(buf),
|
||||
" %s (%s:%s) %02d:%02d:%02d %02d:%02d UTC",
|
||||
hosts[g_host_idx].name,
|
||||
hosts[g_host_idx].host, hosts[g_host_idx].port,
|
||||
h, m, s,
|
||||
utc->tm_hour, utc->tm_min);
|
||||
}
|
||||
status_lbl->copy_label(buf);
|
||||
status_lbl->redraw();
|
||||
}
|
||||
|
||||
static void cb_timer(void *)
|
||||
{
|
||||
update_status();
|
||||
Fl::repeat_timeout(1.0, cb_timer, nullptr);
|
||||
}
|
||||
|
||||
/* ── Connect dialog ──────────────────────────────────────── */
|
||||
|
||||
static int g_dlg_result = -1;
|
||||
|
||||
static void cb_dlg_connect(Fl_Widget *w, void *browser_ptr)
|
||||
{
|
||||
Fl_Hold_Browser *br = (Fl_Hold_Browser *)browser_ptr;
|
||||
int sel = br->value();
|
||||
g_dlg_result = (sel > 0) ? sel - 1 : -1;
|
||||
w->window()->hide();
|
||||
}
|
||||
|
||||
static void cb_dlg_cancel(Fl_Widget *w, void *)
|
||||
{
|
||||
g_dlg_result = -1;
|
||||
w->window()->hide();
|
||||
}
|
||||
|
||||
static void cb_dlg_dblclick(Fl_Widget *w, void *)
|
||||
{
|
||||
Fl_Hold_Browser *br = (Fl_Hold_Browser *)w;
|
||||
if (br->value() > 0) {
|
||||
g_dlg_result = br->value() - 1;
|
||||
br->window()->hide();
|
||||
}
|
||||
}
|
||||
|
||||
static int show_connect_dialog(void)
|
||||
{
|
||||
if (num_hosts == 0) {
|
||||
fl_message("No hosts configured.\nEdit %s to add BPQ nodes.", cfg_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Fl_Window *dlg = new Fl_Window(320, 240, "Connect to Host");
|
||||
dlg->begin();
|
||||
|
||||
Fl_Hold_Browser *br = new Fl_Hold_Browser(10, 10, 300, 170);
|
||||
for (int i = 0; i < num_hosts; i++) {
|
||||
char lbl[140];
|
||||
snprintf(lbl, sizeof(lbl), "%s (%s:%s)",
|
||||
hosts[i].name[0] ? hosts[i].name : hosts[i].host,
|
||||
hosts[i].host, hosts[i].port);
|
||||
br->add(lbl);
|
||||
}
|
||||
br->select(1);
|
||||
br->callback(cb_dlg_dblclick);
|
||||
br->when(FL_WHEN_CHANGED); /* double-click triggers when() */
|
||||
|
||||
Fl_Return_Button *btn_ok = new Fl_Return_Button(130, 195, 90, 30, "Connect");
|
||||
btn_ok->callback(cb_dlg_connect, br);
|
||||
|
||||
Fl_Button *btn_cancel = new Fl_Button(230, 195, 80, 30, "Cancel");
|
||||
btn_cancel->callback(cb_dlg_cancel);
|
||||
|
||||
dlg->end();
|
||||
dlg->set_modal();
|
||||
g_dlg_result = -1;
|
||||
dlg->show();
|
||||
while (dlg->shown()) Fl::wait();
|
||||
int result = g_dlg_result;
|
||||
delete dlg;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── Menu callbacks ──────────────────────────────────────── */
|
||||
|
||||
static void do_connect(int idx)
|
||||
{
|
||||
if (g_connected) tcp_disconnect();
|
||||
|
||||
char msg[160];
|
||||
snprintf(msg, sizeof(msg), "Connecting to %s (%s:%s)...\n",
|
||||
hosts[idx].name[0] ? hosts[idx].name : hosts[idx].host,
|
||||
hosts[idx].host, hosts[idx].port);
|
||||
out_append(msg, (int)strlen(msg), 'E');
|
||||
|
||||
int fd = tcp_connect_to(idx);
|
||||
if (fd < 0) {
|
||||
snprintf(msg, sizeof(msg), "Connection failed: %s\n", strerror(errno));
|
||||
out_append(msg, (int)strlen(msg), 'C');
|
||||
return;
|
||||
}
|
||||
|
||||
g_sock = fd;
|
||||
g_connected = 1;
|
||||
g_host_idx = idx;
|
||||
g_conn_time = time(nullptr);
|
||||
|
||||
Fl::add_fd(g_sock, FL_READ, on_socket_ready, nullptr);
|
||||
send_trace_options(true);
|
||||
update_status();
|
||||
|
||||
snprintf(msg, sizeof(msg), "Connected.\n");
|
||||
out_append(msg, (int)strlen(msg), 'E');
|
||||
}
|
||||
|
||||
static void cb_connect(Fl_Widget *, void *)
|
||||
{
|
||||
int idx = show_connect_dialog();
|
||||
if (idx >= 0) do_connect(idx);
|
||||
}
|
||||
|
||||
static void cb_disconnect(Fl_Widget *, void *)
|
||||
{
|
||||
if (!g_connected) { fl_message("Not connected."); return; }
|
||||
out_append("*** Disconnected\n", 17, 'C');
|
||||
tcp_disconnect();
|
||||
}
|
||||
|
||||
static void cb_quit(Fl_Widget *, void *)
|
||||
{
|
||||
if (g_connected) tcp_disconnect();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static void cb_clear_out(Fl_Widget *, void *)
|
||||
{
|
||||
out_buf->remove(0, out_buf->length());
|
||||
out_styl->remove(0, out_styl->length());
|
||||
}
|
||||
|
||||
static void cb_clear_mon(Fl_Widget *, void *)
|
||||
{
|
||||
mon_buf->remove(0, mon_buf->length());
|
||||
mon_styl->remove(0, mon_styl->length());
|
||||
}
|
||||
|
||||
static void cb_toggle_mon(Fl_Widget *, void *)
|
||||
{
|
||||
mon_visible = !mon_visible;
|
||||
if (mon_visible)
|
||||
out_append("[Monitor ON]\n", 13, 'D');
|
||||
else
|
||||
out_append("[Monitor OFF]\n", 14, 'D');
|
||||
}
|
||||
|
||||
/* ── Input send ──────────────────────────────────────────── */
|
||||
|
||||
static void cb_send(Fl_Widget *, void *)
|
||||
{
|
||||
const char *text = inp->value();
|
||||
if (!text || text[0] == '\0') return;
|
||||
if (!g_connected) {
|
||||
out_append("Not connected.\n", 15, 'C');
|
||||
inp->value("");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Echo locally */
|
||||
char echo[514];
|
||||
int elen = snprintf(echo, sizeof(echo), "> %s\n", text);
|
||||
out_append(echo, elen, 'D');
|
||||
|
||||
/* Send to server with CR */
|
||||
int tlen = (int)strlen(text);
|
||||
char *tbuf = (char *)malloc(tlen + 2);
|
||||
if (tbuf) {
|
||||
memcpy(tbuf, text, tlen);
|
||||
tbuf[tlen] = '\r';
|
||||
tbuf[tlen+1] = '\0';
|
||||
tcp_send(tbuf, tlen + 1);
|
||||
free(tbuf);
|
||||
}
|
||||
inp->value("");
|
||||
}
|
||||
|
||||
/* ── Main window ─────────────────────────────────────────── */
|
||||
|
||||
static Fl_Menu_Item menu_items[] = {
|
||||
{ "&File", 0, nullptr, nullptr, FL_SUBMENU },
|
||||
{ "&Connect...", FL_CTRL+'k', cb_connect },
|
||||
{ "&Disconnect", FL_CTRL+'d', cb_disconnect },
|
||||
{ "Clear output", 0, cb_clear_out },
|
||||
{ "Clear monitor", 0, cb_clear_mon },
|
||||
{ "E&xit", FL_CTRL+'q', cb_quit, nullptr, FL_MENU_DIVIDER },
|
||||
{ nullptr },
|
||||
{ "&Monitor", 0, nullptr, nullptr, FL_SUBMENU },
|
||||
{ "Toggle monitor pane", FL_CTRL+'m', cb_toggle_mon },
|
||||
{ nullptr },
|
||||
{ "&Help", 0, nullptr, nullptr, FL_SUBMENU },
|
||||
{ "About", 0, [](Fl_Widget*, void*){
|
||||
fl_message("TermTCP GUI v" GUI_VERSION "\n"
|
||||
"BPQTermTCP client for BPQ32 packet radio nodes.\n"
|
||||
"Config: ~/.BPQTermTCP.ini");
|
||||
}},
|
||||
{ nullptr },
|
||||
{ nullptr }
|
||||
};
|
||||
|
||||
static void build_window(void)
|
||||
{
|
||||
const int W = 900, H = 650;
|
||||
const int MENU_H = 25;
|
||||
const int STATUS_H = 22;
|
||||
const int INPUT_H = 32;
|
||||
const int SEND_W = 70;
|
||||
const int MON_FRAC = 3; /* monitor gets 1/3 of inner height */
|
||||
|
||||
main_win = new Fl_Double_Window(W, H, "TermTCP " GUI_VERSION);
|
||||
main_win->begin();
|
||||
main_win->color(FL_WHITE);
|
||||
|
||||
/* Menu bar */
|
||||
menu_bar = new Fl_Menu_Bar(0, 0, W, MENU_H);
|
||||
menu_bar->menu(menu_items);
|
||||
menu_bar->color(fl_rgb_color(230,230,230));
|
||||
|
||||
/* Resizable split area */
|
||||
int tile_y = MENU_H;
|
||||
int tile_h = H - MENU_H - INPUT_H - STATUS_H;
|
||||
int out_h = tile_h - tile_h / MON_FRAC;
|
||||
int mon_h = tile_h / MON_FRAC;
|
||||
|
||||
Fl_Tile *tile = new Fl_Tile(0, tile_y, W, tile_h);
|
||||
|
||||
/* Output display */
|
||||
out_disp = new Fl_Text_Display(0, tile_y, W, out_h);
|
||||
out_buf = new Fl_Text_Buffer();
|
||||
out_styl = new Fl_Text_Buffer();
|
||||
out_disp->buffer(out_buf);
|
||||
out_disp->highlight_data(out_styl, STYLES, N_STYLES, 'A', nullptr, 0);
|
||||
out_disp->textfont(FL_COURIER);
|
||||
out_disp->textsize(13);
|
||||
out_disp->color(fl_rgb_color(20, 20, 20));
|
||||
out_disp->textcolor(FL_WHITE);
|
||||
out_disp->cursor_color(FL_WHITE);
|
||||
out_disp->scrollbar_width(14);
|
||||
out_disp->wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 0);
|
||||
|
||||
/* Monitor display */
|
||||
mon_disp = new Fl_Text_Display(0, tile_y + out_h, W, mon_h);
|
||||
mon_buf = new Fl_Text_Buffer();
|
||||
mon_styl = new Fl_Text_Buffer();
|
||||
mon_disp->buffer(mon_buf);
|
||||
mon_disp->highlight_data(mon_styl, STYLES, N_STYLES, 'B', nullptr, 0);
|
||||
mon_disp->textfont(FL_COURIER);
|
||||
mon_disp->textsize(12);
|
||||
mon_disp->color(fl_rgb_color(10, 30, 10));
|
||||
mon_disp->textcolor(fl_rgb_color(0, 220, 0));
|
||||
mon_disp->cursor_color(fl_rgb_color(0,220,0));
|
||||
mon_disp->scrollbar_width(14);
|
||||
|
||||
tile->end();
|
||||
|
||||
/* Input row */
|
||||
int inp_y = tile_y + tile_h;
|
||||
inp = new Fl_Input(0, inp_y, W - SEND_W, INPUT_H);
|
||||
inp->textfont(FL_COURIER);
|
||||
inp->textsize(13);
|
||||
inp->color(fl_rgb_color(30,30,30));
|
||||
inp->textcolor(FL_WHITE);
|
||||
inp->cursor_color(FL_WHITE);
|
||||
inp->when(FL_WHEN_ENTER_KEY_ALWAYS);
|
||||
inp->callback(cb_send);
|
||||
|
||||
Fl_Return_Button *send_btn = new Fl_Return_Button(W - SEND_W, inp_y, SEND_W, INPUT_H, "Send");
|
||||
send_btn->callback(cb_send);
|
||||
|
||||
/* Status bar */
|
||||
int st_y = inp_y + INPUT_H;
|
||||
status_lbl = new Fl_Box(0, st_y, W, STATUS_H, " Not connected");
|
||||
status_lbl->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
|
||||
status_lbl->box(FL_FLAT_BOX);
|
||||
status_lbl->color(fl_rgb_color(0, 0, 140));
|
||||
status_lbl->labelcolor(FL_WHITE);
|
||||
status_lbl->labelfont(FL_HELVETICA);
|
||||
status_lbl->labelsize(12);
|
||||
|
||||
main_win->resizable(tile);
|
||||
main_win->end();
|
||||
main_win->callback([](Fl_Widget *, void *) {
|
||||
if (g_connected) tcp_disconnect();
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── main ────────────────────────────────────────────────── */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
/* Init port names */
|
||||
for (int i = 0; i < 32; i++)
|
||||
snprintf(port_names[i], 32, "Port %d", i+1);
|
||||
|
||||
load_config();
|
||||
Fl::scheme("gtk+");
|
||||
|
||||
build_window();
|
||||
main_win->show(argc, argv);
|
||||
|
||||
/* Status timer */
|
||||
Fl::add_timeout(1.0, cb_timer, nullptr);
|
||||
|
||||
/* Welcome banner */
|
||||
const char *banner = "TermTCP GUI v" GUI_VERSION
|
||||
" — File > Connect to get started\n";
|
||||
out_append(banner, (int)strlen(banner), 'E');
|
||||
|
||||
/* Auto-connect if exactly one host and -c flag or single host */
|
||||
int auto_idx = -1;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--connect") == 0)
|
||||
&& i+1 < argc)
|
||||
auto_idx = atoi(argv[++i]);
|
||||
}
|
||||
if (auto_idx < 0 && num_hosts == 1) auto_idx = 0;
|
||||
|
||||
if (auto_idx >= 0 && auto_idx < num_hosts)
|
||||
do_connect(auto_idx);
|
||||
|
||||
return Fl::run();
|
||||
}
|
||||
Loading…
Reference in new issue