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_gui.cpp

718 lines
24 KiB

/* termtcp_gui.cpp — FLTK GUI for TermTCP
* Layout matches the ncurses CLI: status bar top, output pane, monitor pane,
* input line, function key bar bottom.
*/
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.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_Group.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Window.H>
#include <FL/fl_ask.H>
#include <FL/fl_draw.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
/* ── Layout constants (pixels) ───────────────────────────── */
static const int STATUS_H = 22;
static const int SEP_H = 16;
static const int INPUT_H = 26;
static const int FKEY_H = 26;
/* ── 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_Box *status_bar = 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_Button *fkey_btn[9]; /* F1 F3 F4 F5 F6 F7 F8 F9 F10 */
/* Colors matching the CLI color pairs */
static const Fl_Color COL_STATUS_BG = fl_rgb_color(0, 0, 160);
static const Fl_Color COL_STATUS_FG = FL_WHITE;
static const Fl_Color COL_OUT_BG = fl_rgb_color(10, 10, 10);
static const Fl_Color COL_OUT_FG = FL_WHITE;
static const Fl_Color COL_MON_BG = fl_rgb_color(0, 20, 0);
static const Fl_Color COL_MON_FG = fl_rgb_color(0, 220, 0);
static const Fl_Color COL_SEP_BG = fl_rgb_color(0, 140, 140);
static const Fl_Color COL_SEP_FG = FL_WHITE;
static const Fl_Color COL_INP_BG = fl_rgb_color(20, 20, 20);
static const Fl_Color COL_INP_FG = FL_WHITE;
static const Fl_Color COL_FKEY_BG = fl_rgb_color(0, 0, 100);
static const Fl_Color COL_FKEY_CHIP = fl_rgb_color(200, 200, 255);
static const Fl_Color COL_FKEY_TEXT = fl_rgb_color(0, 0, 80);
/* Style table for Fl_Text_Display highlighting.
* Index from 'A': A=default B=green C=red D=yellow E=cyan F=magenta */
static const Fl_Text_Display::Style_Table_Entry STYLES[] = {
{ COL_OUT_FG, FL_COURIER, 13 }, /* A default */
{ fl_rgb_color(0,220,0), FL_COURIER, 13 }, /* B green */
{ fl_rgb_color(220,60,60), FL_COURIER, 13 }, /* C red */
{ fl_rgb_color(220,200,0), FL_COURIER, 13 }, /* D yellow */
{ fl_rgb_color(0,210,220), FL_COURIER, 13 }, /* E cyan */
{ fl_rgb_color(200,80,200),FL_COURIER, 13 }, /* F magenta */
};
#define N_STYLES 6
/* ── Forward declarations ────────────────────────────────── */
static void tcp_disconnect(void);
static void update_status(void);
static void do_connect(int idx);
/* ── Config loading ──────────────────────────────────────── */
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)) {
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] == '[') {
hi = (strncmp(line,"[Host",5)==0 && num_hosts<MAX_HOSTS) ? num_hosts++ : -1;
if (hi >= 0) memset(&hosts[hi], 0, sizeof(HostEntry));
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;
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 */
int idx = (int)(unsigned char)text[i+1] - 10;
if (idx < 0) cur = 'A';
else if (idx < 4) cur = 'E';
else if (idx < 20) cur = 'B';
else if (idx < 60) cur = 'C';
else cur = 'D';
i += 2;
} else if (c == '\r') {
i++;
} 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);
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, *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 = fe ? (int)(fe-p) : (int)(end-p);
if (seg > 0) {
int room = MON_FBUF_SZ-1-g_mon_flen;
int add = seg<room?seg:room;
memcpy(g_mon_fbuf+g_mon_flen, p, add);
g_mon_flen += add;
}
if (fe) {
if (g_mon_flen > 0) {
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, void *)
{
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_host(int idx)
{
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[200];
if (!g_connected || g_host_idx < 0) {
snprintf(buf, sizeof(buf), " TermTCP %s — Not connected", GUI_VERSION);
} else {
time_t el = time(nullptr) - g_conn_time;
int h = (int)(el/3600), m = (int)((el%3600)/60), s = (int)(el%60);
time_t now = time(nullptr);
struct tm *utc = gmtime(&now);
const char *nm = hosts[g_host_idx].name[0]
? hosts[g_host_idx].name : hosts[g_host_idx].host;
snprintf(buf, sizeof(buf),
" TermTCP %s | %s (%s:%s) | %02d:%02d:%02d | %02d:%02dz",
GUI_VERSION, nm,
hosts[g_host_idx].host, hosts[g_host_idx].port,
h, m, s, utc->tm_hour, utc->tm_min);
}
status_bar->copy_label(buf);
status_bar->redraw();
}
static void cb_timer(void *)
{
update_status();
Fl::repeat_timeout(1.0, cb_timer, nullptr);
}
/* ── Connect dialog ──────────────────────────────────────── */
static int g_pick = -1;
static void cb_pick_ok(Fl_Widget *w, void *br)
{
int v = ((Fl_Hold_Browser *)br)->value();
g_pick = (v > 0) ? v-1 : -1;
w->window()->hide();
}
static void cb_pick_cancel(Fl_Widget *w, void *)
{ g_pick = -1; w->window()->hide(); }
static void cb_pick_dbl(Fl_Widget *w, void *)
{
Fl_Hold_Browser *br = (Fl_Hold_Browser *)w;
if (br->value() > 0) { g_pick = 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(340, 250, "Connect to Host");
dlg->begin();
Fl_Hold_Browser *br = new Fl_Hold_Browser(10, 10, 320, 180);
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->textfont(FL_COURIER); br->textsize(13);
br->callback(cb_pick_dbl);
Fl_Return_Button *ok = new Fl_Return_Button(150, 205, 90, 28, "Connect");
ok->callback(cb_pick_ok, br);
Fl_Button *cancel = new Fl_Button(250, 205, 80, 28, "Cancel");
cancel->callback(cb_pick_cancel);
dlg->end(); dlg->set_modal();
g_pick = -1;
dlg->show();
while (dlg->shown()) Fl::wait();
int r = g_pick;
delete dlg;
return r;
}
/* ── Actions ─────────────────────────────────────────────── */
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_host(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');
}
/* F-key callbacks */
static void cb_f1(Fl_Widget *, void *) /* Connect / Disconnect */
{
if (g_connected) {
out_append("*** Disconnected\n", 17, 'C');
tcp_disconnect();
} else {
int idx = show_connect_dialog();
if (idx >= 0) do_connect(idx);
}
}
static void cb_f3(Fl_Widget *, void *) /* Toggle monitor */
{
mon_visible = !mon_visible;
out_append(mon_visible ? "[Monitor ON]\n" : "[Monitor OFF]\n", 14, 'D');
}
static void cb_f4(Fl_Widget *, void *) /* Config — placeholder */
{
fl_message("Host config editor coming soon.\nEdit %s manually for now.", cfg_path);
}
static void cb_f5(Fl_Widget *, void *) /* Log — placeholder */
{
fl_message("Session logging coming soon.");
}
static void cb_f6(Fl_Widget *, void *) /* Forms — placeholder */
{
fl_message("Forms coming soon.");
}
static void cb_f7(Fl_Widget *, void *) /* New session — placeholder */
{
fl_message("Multiple sessions coming soon.");
}
static void cb_f8(Fl_Widget *, void *) {} /* Prev session — future */
static void cb_f9(Fl_Widget *, void *) {} /* Next session — future */
static void cb_f10(Fl_Widget *, void *) /* Quit */
{
if (g_connected) tcp_disconnect();
exit(0);
}
/* Send input line */
static void cb_send(Fl_Widget *, void *)
{
const char *text = inp->value();
if (!text || !text[0]) return;
if (!g_connected) {
out_append("Not connected — use F1 to connect.\n", 36, 'C');
inp->value(""); return;
}
char echo[514];
int elen = snprintf(echo, sizeof(echo), "> %s\n", text);
out_append(echo, elen, 'D');
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("");
}
/* ── Build window ────────────────────────────────────────── */
static void build_window(void)
{
const int W = 920;
const int H = 650;
/* Fixed heights */
const int FIXED = STATUS_H + SEP_H + INPUT_H + SEP_H + FKEY_H;
const int TILE_H = H - FIXED; /* height for output+sep+monitor tile */
const int OUT_H = TILE_H * 62 / 100; /* ~62% output */
const int MON_H = TILE_H - OUT_H; /* ~38% monitor */
main_win = new Fl_Double_Window(W, H, "TermTCP " GUI_VERSION);
main_win->color(fl_rgb_color(10,10,10));
main_win->begin();
int y = 0;
/* ── Status bar (top, blue) ── */
status_bar = new Fl_Box(0, y, W, STATUS_H);
status_bar->box(FL_FLAT_BOX);
status_bar->color(COL_STATUS_BG);
status_bar->labelcolor(COL_STATUS_FG);
status_bar->labelfont(FL_HELVETICA_BOLD);
status_bar->labelsize(12);
status_bar->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
status_bar->copy_label(" TermTCP " GUI_VERSION " — Not connected");
y += STATUS_H;
/* ── Resizable tile: output group + monitor group ── */
Fl_Tile *tile = new Fl_Tile(0, y, W, TILE_H);
tile->begin();
/* Output group: display + "Output" separator at bottom */
Fl_Group *out_grp = new Fl_Group(0, y, W, OUT_H);
out_grp->begin();
out_disp = new Fl_Text_Display(0, y, W, OUT_H - SEP_H);
out_disp->box(FL_FLAT_BOX);
out_disp->color(COL_OUT_BG);
out_disp->textcolor(COL_OUT_FG);
out_disp->textfont(FL_COURIER);
out_disp->textsize(13);
out_disp->cursor_color(COL_OUT_FG);
out_disp->scrollbar_width(12);
out_disp->wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 0);
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);
Fl_Box *out_sep = new Fl_Box(0, y + OUT_H - SEP_H, W, SEP_H, "Output");
out_sep->box(FL_FLAT_BOX);
out_sep->color(COL_SEP_BG);
out_sep->labelcolor(COL_SEP_FG);
out_sep->labelfont(FL_HELVETICA_BOLD);
out_sep->labelsize(11);
out_grp->resizable(out_disp);
out_grp->end();
/* Monitor group: display fills group, "Monitor" in its top label area */
Fl_Group *mon_grp = new Fl_Group(0, y + OUT_H, W, MON_H);
mon_grp->begin();
mon_disp = new Fl_Text_Display(0, y + OUT_H, W, MON_H);
mon_disp->box(FL_FLAT_BOX);
mon_disp->color(COL_MON_BG);
mon_disp->textcolor(COL_MON_FG);
mon_disp->textfont(FL_COURIER);
mon_disp->textsize(12);
mon_disp->cursor_color(COL_MON_FG);
mon_disp->scrollbar_width(12);
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_grp->resizable(mon_disp);
mon_grp->end();
tile->end();
tile->resizable(tile);
y += TILE_H;
/* ── Input separator ── */
Fl_Box *inp_sep = new Fl_Box(0, y, W, SEP_H, "Input");
inp_sep->box(FL_FLAT_BOX);
inp_sep->color(COL_SEP_BG);
inp_sep->labelcolor(COL_SEP_FG);
inp_sep->labelfont(FL_HELVETICA_BOLD);
inp_sep->labelsize(11);
y += SEP_H;
/* ── Input row: "> " prompt + input field ── */
Fl_Box *prompt = new Fl_Box(0, y, 22, INPUT_H, "> ");
prompt->box(FL_FLAT_BOX);
prompt->color(COL_INP_BG);
prompt->labelcolor(COL_INP_FG);
prompt->labelfont(FL_COURIER_BOLD);
prompt->labelsize(13);
inp = new Fl_Input(22, y, W-22, INPUT_H);
inp->box(FL_FLAT_BOX);
inp->color(COL_INP_BG);
inp->textcolor(COL_INP_FG);
inp->cursor_color(COL_INP_FG);
inp->textfont(FL_COURIER);
inp->textsize(13);
inp->when(FL_WHEN_ENTER_KEY_ALWAYS);
inp->callback(cb_send);
y += INPUT_H;
/* ── Function key bar ── */
struct { const char *label; Fl_Callback *cb; } fkeys[] = {
{ "F1 Conn", cb_f1 },
{ "F3 Mon", cb_f3 },
{ "F4 Cfg", cb_f4 },
{ "F5 Log", cb_f5 },
{ "F6 Forms", cb_f6 },
{ "F7 New", cb_f7 },
{ "F8 Prev", cb_f8 },
{ "F9 Next", cb_f9 },
{ "F10 Quit", cb_f10 },
};
const int NKEYS = 9;
int btn_w = W / NKEYS;
Fl_Group *fkey_grp = new Fl_Group(0, y, W, FKEY_H);
fkey_grp->begin();
fkey_grp->box(FL_FLAT_BOX);
fkey_grp->color(COL_FKEY_BG);
for (int i = 0; i < NKEYS; i++) {
int bx = i * btn_w;
int bw = (i == NKEYS-1) ? W - bx : btn_w; /* last button fills remainder */
fkey_btn[i] = new Fl_Button(bx, y, bw, FKEY_H, fkeys[i].label);
fkey_btn[i]->box(FL_FLAT_BOX);
fkey_btn[i]->color(COL_FKEY_CHIP);
fkey_btn[i]->labelcolor(COL_FKEY_TEXT);
fkey_btn[i]->labelfont(FL_HELVETICA_BOLD);
fkey_btn[i]->labelsize(11);
fkey_btn[i]->callback(fkeys[i].cb);
}
fkey_grp->end();
main_win->resizable(tile);
main_win->end();
/* Wire up keyboard F-keys on the main window */
main_win->callback([](Fl_Widget *, void *) {
if (g_connected) tcp_disconnect();
exit(0);
});
}
/* Handle F-keys pressed anywhere in the window */
static int event_handler(int event)
{
if (event != FL_SHORTCUT) return 0;
switch (Fl::event_key()) {
case FL_F+1: cb_f1(nullptr,nullptr); return 1;
case FL_F+3: cb_f3(nullptr,nullptr); return 1;
case FL_F+4: cb_f4(nullptr,nullptr); return 1;
case FL_F+5: cb_f5(nullptr,nullptr); return 1;
case FL_F+6: cb_f6(nullptr,nullptr); return 1;
case FL_F+7: cb_f7(nullptr,nullptr); return 1;
case FL_F+8: cb_f8(nullptr,nullptr); return 1;
case FL_F+9: cb_f9(nullptr,nullptr); return 1;
case FL_F+10: cb_f10(nullptr,nullptr); return 1;
}
return 0;
}
/* ── main ────────────────────────────────────────────────── */
int main(int argc, char **argv)
{
for (int i = 0; i < 32; i++)
snprintf(port_names[i], 32, "Port %d", i+1);
load_config();
Fl::scheme("gtk+");
build_window();
Fl::add_handler(event_handler);
main_win->show(argc, argv);
Fl::add_timeout(1.0, cb_timer, nullptr);
const char *banner = "TermTCP GUI v" GUI_VERSION
" — F1 to connect\n";
out_append(banner, (int)strlen(banner), 'E');
/* Auto-connect if only one host configured */
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();
}

Powered by TurnKey Linux.