From e36cff11d472471073a7a3c3f762fa2470a2c675 Mon Sep 17 00:00:00 2001 From: Ryan Collier Date: Mon, 29 Jun 2026 09:52:22 -0400 Subject: [PATCH] Add FLTK GUI frontend (termtcp-gui) Single-session GUI with dark-themed output + green monitor panes, input field, connect dialog, BPQ protocol parser, and status bar. Auto-connects if only one host is configured. Build with: make gui Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 + Makefile | 36 ++- termtcp_gui.cpp | 714 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 744 insertions(+), 8 deletions(-) create mode 100644 termtcp_gui.cpp diff --git a/.gitignore b/.gitignore index 39a8ce9..0bc6b97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ termtcp +termtcp-gui termtcp_cli.o termtcp.o +termtcp_gui.o *.log diff --git a/Makefile b/Makefile index 5acd1ea..65c2326 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,21 @@ # TermTCP - Makefile -CC = gcc -CFLAGS = -Wall -Wextra -O2 -std=gnu99 -D_XOPEN_SOURCE_EXTENDED -LDFLAGS = -lm -lncursesw +CC = gcc +CXX = g++ +CFLAGS = -Wall -Wextra -O2 -std=gnu99 -D_XOPEN_SOURCE_EXTENDED +CXXFLAGS = -Wall -Wextra -O2 -std=c++11 +LDFLAGS = -lm -lncursesw -TARGET = termtcp -SOURCE = termtcp.c -OBJECT = $(SOURCE:.c=.o) +# FLTK +FLTK_CXXFLAGS = $(shell fltk-config --cxxflags) +FLTK_LDFLAGS = $(shell fltk-config --ldflags) + +TARGET = termtcp +GUI_TARGET = termtcp-gui +SOURCE = termtcp.c +GUI_SOURCE = termtcp_gui.cpp +OBJECT = $(SOURCE:.c=.o) +GUI_OBJECT = $(GUI_SOURCE:.cpp=.o) # Default target all: $(TARGET) @@ -21,6 +30,17 @@ $(TARGET): $(OBJECT) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ +# GUI target +gui: $(GUI_TARGET) + +$(GUI_TARGET): $(GUI_OBJECT) + $(CXX) $(CXXFLAGS) -o $@ $^ $(FLTK_LDFLAGS) + @echo "Build complete: $(GUI_TARGET)" + @ls -lh $(GUI_TARGET) + +$(GUI_OBJECT): $(GUI_SOURCE) + $(CXX) $(CXXFLAGS) $(FLTK_CXXFLAGS) -c $< -o $@ + # Debug build debug: CFLAGS += -g -O0 -DDEBUG debug: clean $(TARGET) @@ -32,7 +52,7 @@ run: $(TARGET) # Clean clean: - rm -f $(OBJECT) $(TARGET) + rm -f $(OBJECT) $(GUI_OBJECT) $(TARGET) $(GUI_TARGET) @echo "Clean complete" # Install to system @@ -61,4 +81,4 @@ help: @echo " make lint - Run static analysis" @echo " make help - Show this message" -.PHONY: all debug run clean install uninstall lint help +.PHONY: all gui debug run clean install uninstall lint help diff --git a/termtcp_gui.cpp b/termtcp_gui.cpp new file mode 100644 index 0000000..4db6852 --- /dev/null +++ b/termtcp_gui.cpp @@ -0,0 +1,714 @@ +/* termtcp_gui.cpp — FLTK GUI for TermTCP + * BPQTermTCP protocol client for BPQ32 packet radio nodes. + * Compile: see Makefile target 'gui' + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +} + +#define GUI_VERSION "0.0.53" +#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(); +}