diff --git a/termtcp_gui.cpp b/termtcp_gui.cpp index 1a35f0f..cb9c055 100644 --- a/termtcp_gui.cpp +++ b/termtcp_gui.cpp @@ -1,11 +1,10 @@ /* termtcp_gui.cpp — FLTK GUI for TermTCP - * BPQTermTCP protocol client for BPQ32 packet radio nodes. - * Compile: see Makefile target 'gui' + * Layout matches the ncurses CLI: status bar top, output pane, monitor pane, + * input line, function key bar bottom. */ #include #include -#include #include #include #include @@ -13,9 +12,11 @@ #include #include #include +#include #include #include #include +#include extern "C" { #include @@ -34,6 +35,12 @@ extern "C" { #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 { @@ -69,37 +76,50 @@ 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 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[] = { - { 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 */ + { 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 cb_timer(void *); +static void do_connect(int idx); -/* ── Config I/O ──────────────────────────────────────────── */ +/* ── Config loading ──────────────────────────────────────── */ static void load_config(void) { @@ -113,33 +133,24 @@ static void load_config(void) 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; - } + hi = (strncmp(line,"[Host",5)==0 && num_hosts= 0) memset(&hosts[hi], 0, sizeof(HostEntry)); continue; } - char *eq = strchr(line, '='); if (!eq) continue; *eq = '\0'; - const char *key = line, *val = eq + 1; - + 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); } + 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); @@ -149,8 +160,7 @@ static void load_config(void) 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); + if (pn>=1 && pn<=32) snprintf(port_names[pn-1],32,"%s",val); } } } @@ -163,50 +173,39 @@ 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; + 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 (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'; /* dark blue → cyan */ - else if (idx < 20) cur = 'B'; /* green */ - else if (idx < 60) cur = 'C'; /* reds */ - else cur = 'D'; /* bright → yellow */ + 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++; /* skip bare CR */ + i++; } else { tmp[ti] = (char)c; sty[ti] = cur; - ti++; - i++; + ti++; i++; } } - tmp[ti] = '\0'; - sty[ti] = '\0'; - + tmp[ti] = '\0'; sty[ti] = '\0'; tbuf->append(tmp); sbuf->append(sty); - - free(tmp); - free(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); } @@ -237,86 +236,74 @@ static void send_trace_options(bool with_ports) static void parse_portinfo(const char *data, int len) { - const char *p = data, *e = data + len; + const char *p = data, *e = data+len; int nports = 0; - while (p < e && *p >= '0' && *p <= '9') nports = nports*10 + (*p++ - '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'); + 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; + 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++; + if (p < e && *p=='|') p++; } } static void bpq_process(const char *buf, int len) { - const char *p = buf; - const char *end = buf + 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; + 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)); + parse_portinfo(p+1, (int)(end-p-1)); p = end; } else { - g_in_mon = 1; - g_mon_flen = 0; + 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); + 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 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; + 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); + mon_append(g_mon_fbuf, g_mon_flen+1); } } - g_mon_flen = 0; - g_in_mon = 0; - p = fe + 1; - } else { - break; - } + g_mon_flen = 0; g_in_mon = 0; p = fe+1; + } else break; } } } /* ── Socket handling ─────────────────────────────────────── */ -static void on_socket_ready(int /*fd*/, void * /*data*/) +static void on_socket_ready(int, void *) { char buf[RECV_SZ]; - int n = recv(g_sock, buf, sizeof(buf) - 1, 0); + 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)) { @@ -325,40 +312,25 @@ static void on_socket_ready(int /*fd*/, void * /*data*/) } } -static int tcp_connect_to(int idx) +static int tcp_connect_host(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; - + 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; - } + 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; + 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(); } @@ -372,25 +344,24 @@ static void tcp_send(const char *text, int len) static void update_status(void) { - static char buf[160]; + static char buf[200]; if (!g_connected || g_host_idx < 0) { - snprintf(buf, sizeof(buf), " Not connected"); + snprintf(buf, sizeof(buf), " TermTCP %s — Not connected", GUI_VERSION); } 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 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), - " %s (%s:%s) %02d:%02d:%02d %02d:%02d UTC", - hosts[g_host_idx].name, + " 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); + h, m, s, utc->tm_hour, utc->tm_min); } - status_lbl->copy_label(buf); - status_lbl->redraw(); + status_bar->copy_label(buf); + status_bar->redraw(); } static void cb_timer(void *) @@ -401,29 +372,21 @@ static void cb_timer(void *) /* ── 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 int g_pick = -1; -static void cb_dlg_cancel(Fl_Widget *w, void *) +static void cb_pick_ok(Fl_Widget *w, void *br) { - g_dlg_result = -1; + 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_dlg_dblclick(Fl_Widget *w, void *) +static void cb_pick_dbl(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(); - } + if (br->value() > 0) { g_pick = br->value()-1; br->window()->hide(); } } static int show_connect_dialog(void) @@ -432,255 +395,299 @@ static int show_connect_dialog(void) 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"); + Fl_Window *dlg = new Fl_Window(340, 250, "Connect to Host"); dlg->begin(); - - Fl_Hold_Browser *br = new Fl_Hold_Browser(10, 10, 300, 170); + 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)", + 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; + 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 result = g_dlg_result; + int r = g_pick; delete dlg; - return result; + return r; } -/* ── Menu callbacks ──────────────────────────────────────── */ +/* ── 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_to(idx); + 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); - + 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 *) +/* F-key callbacks */ +static void cb_f1(Fl_Widget *, void *) /* Connect / Disconnect */ { - int idx = show_connect_dialog(); - if (idx >= 0) do_connect(idx); + 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_disconnect(Fl_Widget *, void *) +static void cb_f3(Fl_Widget *, void *) /* Toggle monitor */ { - if (!g_connected) { fl_message("Not connected."); return; } - out_append("*** Disconnected\n", 17, 'C'); - tcp_disconnect(); + mon_visible = !mon_visible; + out_append(mon_visible ? "[Monitor ON]\n" : "[Monitor OFF]\n", 14, 'D'); } -static void cb_quit(Fl_Widget *, void *) +static void cb_f4(Fl_Widget *, void *) /* Config — placeholder */ { - if (g_connected) tcp_disconnect(); - exit(0); + fl_message("Host config editor coming soon.\nEdit %s manually for now.", cfg_path); } -static void cb_clear_out(Fl_Widget *, void *) +static void cb_f5(Fl_Widget *, void *) /* Log — placeholder */ { - out_buf->remove(0, out_buf->length()); - out_styl->remove(0, out_styl->length()); + fl_message("Session logging coming soon."); } -static void cb_clear_mon(Fl_Widget *, void *) +static void cb_f6(Fl_Widget *, void *) /* Forms — placeholder */ { - mon_buf->remove(0, mon_buf->length()); - mon_styl->remove(0, mon_styl->length()); + fl_message("Forms coming soon."); } -static void cb_toggle_mon(Fl_Widget *, void *) +static void cb_f7(Fl_Widget *, void *) /* New session — placeholder */ { - mon_visible = !mon_visible; - if (mon_visible) - out_append("[Monitor ON]\n", 13, 'D'); - else - out_append("[Monitor OFF]\n", 14, 'D'); + fl_message("Multiple sessions coming soon."); } -/* ── Input send ──────────────────────────────────────────── */ +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] == '\0') return; + if (!text || !text[0]) return; if (!g_connected) { - out_append("Not connected.\n", 15, 'C'); - inp->value(""); - return; + out_append("Not connected — use F1 to connect.\n", 36, 'C'); + inp->value(""); return; } - - /* Echo locally */ char echo[514]; - int elen = snprintf(echo, sizeof(echo), "> %s\n", text); + int elen = snprintf(echo, sizeof(echo), "> %s\n", text); out_append(echo, elen, 'D'); - - /* Send to server with CR */ - int tlen = (int)strlen(text); + 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); + 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 } -}; +/* ── Build window ────────────────────────────────────────── */ 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 */ + 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(); - 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); + 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); - 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); + 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_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); + mon_grp->resizable(mon_disp); + mon_grp->end(); tile->end(); - - /* Input row */ - int inp_y = tile_y + tile_h; - inp = new Fl_Input(0, inp_y, W - SEND_W, INPUT_H); + 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->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); + 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) { - /* Init port names */ for (int i = 0; i < 32; i++) snprintf(port_names[i], 32, "Port %d", i+1); @@ -688,25 +695,21 @@ int main(int argc, char **argv) Fl::scheme("gtk+"); build_window(); + Fl::add_handler(event_handler); 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"; + " — F1 to connect\n"; out_append(banner, (int)strlen(banner), 'E'); - /* Auto-connect if exactly one host and -c flag or single host */ + /* 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) + for (int i = 1; i < argc; i++) + if ((strcmp(argv[i],"-c")==0||strcmp(argv[i],"--connect")==0) && i+1= 0 && auto_idx < num_hosts) do_connect(auto_idx);