diff --git a/Makefile_CLI b/Makefile_CLI index 117f154..8aeb06d 100644 --- a/Makefile_CLI +++ b/Makefile_CLI @@ -2,8 +2,8 @@ # Simple command-line TCP client CC = gcc -CFLAGS = -Wall -Wextra -O2 -std=gnu99 -LDFLAGS = -lm -lncurses +CFLAGS = -Wall -Wextra -O2 -std=gnu99 -D_XOPEN_SOURCE_EXTENDED +LDFLAGS = -lm -lncursesw TARGET = termtcp SOURCE = termtcp_cli.c diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5afa57 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# TermTCP CLI + +A terminal TCP client for BPQ32 packet radio nodes, written in C with an ncurses TUI. Connects via BPQTermTCP protocol, displays split output/monitor panes, and supports real-time RF frame monitoring with port filtering. + +## Features + +- ncurses TUI with split output and monitor panes +- BPQ packet radio node support (BPQTermTCP protocol) +- RF monitor with per-port and frame-type filtering +- Auto-populates port names from the BPQ server on connect +- Scrollable output and monitor windows with history +- Input history (Up/Down arrows) +- Session logging to timestamped files +- Multiple host configuration with quick connect +- Unicode box-drawing borders on all menus + +## Dependencies + +- GCC +- ncurses (wide character support — `libncursesw6`) + +### Install dependencies on Debian/Ubuntu + +```bash +sudo apt install gcc libncurses-dev +``` + +## Build + +```bash +make -f Makefile_CLI +``` + +The binary is `./termtcp`. + +### Other make targets + +``` +make -f Makefile_CLI clean # Remove build artifacts +make -f Makefile_CLI debug # Build with debug symbols +make -f Makefile_CLI install # Install to /usr/local/bin/ +``` + +### Manual compile + +```bash +gcc -Wall -Wextra -O2 -std=gnu99 -D_XOPEN_SOURCE_EXTENDED \ + -o termtcp termtcp_cli.c -lm -lncursesw +``` + +## Configuration + +On first run, `~/.termtcprc` is created automatically. Edit it to add BPQ nodes: + +```ini +[Host0] +Host=10.0.2.7 +Port=17023 +Username=kb8pmy +Password=yourpassword +Name=Home Node + +[Host1] +Host=10.121.15.68 +Port=17023 +Username=kb8pmy +Password=yourpassword +Name=Water Tower +``` + +Port names and monitor settings are also saved here automatically. + +## Usage + +```bash +./termtcp +``` + +### Key bindings + +| Key | Action | +|-----|--------| +| Ctrl+A | Open menu | +| Ctrl+A C | Connect | +| Ctrl+A D | Disconnect | +| Ctrl+A O | Config / host editor | +| Ctrl+A M | Monitor options | +| Ctrl+A L | Toggle session log | +| Ctrl+A H | Help | +| Ctrl+A Q | Quit | +| Up / Down | Input history | +| PgUp / PgDn | Scroll output window | +| Home / End | Scroll monitor window | +| Enter | Send line to server | + +### Monitor options (Ctrl+A M) + +- Toggle TX, supervisor, nodes, and UI-only filtering +- Toggle monitor pane visibility +- Select which RF ports to monitor (Tab to switch to port selection) + +Port names are auto-populated from the BPQ server when you connect. + +## BPQ Protocol Notes + +- Connects using BPQTermTCP protocol (same as QtTermTCP) +- Sends trace options on connect to enable RF monitoring +- Port list is requested automatically via P8=1 parameter +- Monitor frames delimited by `0xFF` / `0xFE` byte markers +- NODES broadcasts filtered client-side via `>NODES` detection in AX.25 header + +## Version + +Current: **0.0.52** diff --git a/termtcp b/termtcp index f3db602..43e7c64 100755 Binary files a/termtcp and b/termtcp differ diff --git a/termtcp_cli.c b/termtcp_cli.c index ef9fcbd..4b125bb 100644 --- a/termtcp_cli.c +++ b/termtcp_cli.c @@ -27,14 +27,17 @@ #include #include #include +#include #include +#include +#include #include #ifndef PATH_MAX #define PATH_MAX 4096 #endif -#define VERSION "0.0.49" +#define VERSION "0.0.52" #define MAX_HOSTS 4 #define INPUT_MAX 512 #define CONF_FILE ".termtcprc" @@ -88,6 +91,8 @@ int mon_colour = 1; int mon_ui_only = 1; int port_mask = 0xFFFF; int in_mon_frame = 0; +static char mon_fbuf[4096]; /* accumulates one complete 0xFF…0xFE monitor frame */ +static int mon_flen = 0; int port_seen = 0; /* bitmask: bit N-1 set when port N seen in monitor data */ char port_names[32][32]; /* names for ports 1-32, loaded from config or defaulted */ @@ -96,6 +101,7 @@ char port_names[32][32]; /* names for ports 1-32, loaded from config or defa static char ports_line_buf[256]; static int ports_line_len = 0; static int ports_parse_mode = 0; /* 1 while inside "} Ports" response */ +static int ports_loaded = 0; /* set when PORTS list fully received */ /* ── Scrollback buffers ──────────────────────────────────── */ static ScrlLine out_sbuf[SCRL_MAX]; @@ -361,8 +367,12 @@ void tcp_disconnect(void) connected = 0; connect_time = 0; in_mon_frame = 0; + mon_flen = 0; ports_parse_mode = 0; ports_line_len = 0; + ports_loaded = 0; + port_seen = 0; + for (int i = 0; i < 32; i++) snprintf(port_names[i], 32, "Port %d", i + 1); draw_status(); } @@ -381,6 +391,48 @@ void send_trace_options(void) send(sock, buf, len, 0); } +/* Same as send_trace_options but appends P8=1, which tells BPQ to call + * SendPortsForMonitor() — returns port list as 0xFF 0xFF N|num desc|... */ +static void send_trace_options_with_ports(void) +{ + if (sock < 0) return; + char buf[80]; + int 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); + send(sock, buf, len, 0); +} + +/* Parse the 0xFF 0xFF port-info packet from SendPortsForMonitor. + * Format (after both 0xFF bytes): "N|portnum desc|portnum desc|..." + * N = total entries; portnum 0 = Mail Monitor (skipped). */ +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'; + port_seen |= (1u << (n - 1)); + } + if (p < e && *p == '|') p++; + } + ports_loaded = 1; +} + static int bpq_color_attr(unsigned char color_byte) { /* Map BPQ Colours[] index (color_byte - 10) to nearest terminal color. @@ -459,7 +511,8 @@ static void parse_session_line(const char *line, int len) port_seen |= (1 << (n-1)); } } else { - ports_parse_mode = 0; /* Non-matching line ends the list */ + if (ports_parse_mode) ports_loaded = 1; /* list ended cleanly */ + ports_parse_mode = 0; } } @@ -497,17 +550,46 @@ void bpq_process(const char *buf, int len) feed_session_byte((unsigned char)*q); append_out(p, (int)(ff - p), COLOR_PAIR(CP_DEFAULT)); } - in_mon_frame = 1; + /* 0xFF 0xFF = SendPortsForMonitor response; 0xFF 0x1B = monitor frame */ p = ff + 1; + if (p < end && (unsigned char)*p == 0xFF) { + /* Port info packet — parse in place, consume to end of buffer */ + parse_portinfo(p + 1, (int)(end - p - 1)); + p = end; + } else { + in_mon_frame = 1; + mon_flen = 0; + } } else { const char *fe = memchr(p, 0xFE, end - p); int seg_len = fe ? (int)(fe - p) : (int)(end - p); if (seg_len > 0) { detect_port_number(p, seg_len); - append_mon(p, seg_len, COLOR_PAIR(CP_GREEN)); + /* Accumulate into the per-frame buffer */ + int room = (int)sizeof(mon_fbuf) - 1 - mon_flen; + if (room > 0) { + int add = seg_len < room ? seg_len : room; + memcpy(mon_fbuf + mon_flen, p, add); + mon_flen += add; + } + } + if (fe) { + /* Frame complete: apply client-side NODES filter then display */ + if (mon_flen > 0) { + mon_fbuf[mon_flen] = '\0'; + /* Detect NODES broadcast by AX.25 destination ">NODES" in header */ + int is_nodes = 0; + for (int i = 0; i <= mon_flen - 6 && !is_nodes; i++) + if (memcmp(mon_fbuf + i, ">NODES", 6) == 0) is_nodes = 1; + if (mon_nodes || !is_nodes) + append_mon(mon_fbuf, mon_flen, COLOR_PAIR(CP_GREEN)); + } + mon_flen = 0; + in_mon_frame = 0; + p = fe + 1; + } else { + break; } - if (fe) { in_mon_frame = 0; p = fe + 1; } - else { break; } } } @@ -534,27 +616,20 @@ void init_colors(void) init_pair(CP_SEP, COLOR_CYAN, COLOR_BLACK); } -/* Draw a plain ASCII border (+, -, |) around window w. - * Title (if any) is written over the top border by the caller after this. */ -static void ascii_box(WINDOW *w) + +/* Draw a box using Unicode cchar_t — bypasses ACS so it works on any UTF-8 terminal */ +static void draw_box(WINDOW *w) { - int rows, cols; - getmaxyx(w, rows, cols); - mvwaddch(w, 0, 0, '+'); - mvwaddch(w, 0, cols-1, '+'); - mvwaddch(w, rows-1, 0, '+'); - mvwaddch(w, rows-1, cols-1, '+'); - for (int c = 1; c < cols-1; c++) { - mvwaddch(w, 0, c, '-'); - mvwaddch(w, rows-1, c, '-'); - } - for (int r = 1; r < rows-1; r++) { - mvwaddch(w, r, 0, '|'); - mvwaddch(w, r, cols-1, '|'); - } + cchar_t vt, hz, ul, ur, ll, lr; + setcchar(&vt, L"│", 0, 0, NULL); /* │ */ + setcchar(&hz, L"─", 0, 0, NULL); /* ─ */ + setcchar(&ul, L"┌", 0, 0, NULL); /* ┌ */ + setcchar(&ur, L"┐", 0, 0, NULL); /* ┐ */ + setcchar(&ll, L"└", 0, 0, NULL); /* └ */ + setcchar(&lr, L"┘", 0, 0, NULL); /* ┘ */ + wborder_set(w, &vt, &vt, &hz, &hz, &ul, &ur, &ll, &lr); } - /* Fill a 1-row window with plain '-' dashes and an optional centred label. * No ACS — works on any terminal. */ static void fill_sep(WINDOW *w, const char *label) @@ -563,7 +638,9 @@ static void fill_sep(WINDOW *w, const char *label) werase(w); wbkgd(w, COLOR_PAIR(CP_SEP)); wattron(w, COLOR_PAIR(CP_SEP)); - whline(w, '-', COLS); + cchar_t hz; + setcchar(&hz, L"─", 0, 0, NULL); + whline_set(w, &hz, COLS); if (label && COLS > 4) { int llen = (int)strlen(label); int col = (COLS - llen) / 2; @@ -672,8 +749,9 @@ static void scrl_draw_window(WINDOW *win, ScrlLine *buf, int count, int head, int scroll_off) { if (!win) return; + scrollok(win, FALSE); /* disable scroll during batch redraw — prevents losing lines */ werase(win); - if (count == 0) { wrefresh(win); return; } + if (count == 0) { scrollok(win, TRUE); wrefresh(win); return; } int win_h, win_w; getmaxyx(win, win_h, win_w); @@ -702,8 +780,12 @@ static void scrl_draw_window(WINDOW *win, ScrlLine *buf, int count, int head, else { p++; } } wattroff(win, cur); - waddch(win, '\n'); + if (i < last) waddch(win, '\n'); } + /* Move cursor to start of next blank row so live appends begin on a fresh line */ + int drawn = last - first + 1; + if (drawn < win_h) wmove(win, drawn, 0); + scrollok(win, TRUE); /* re-enable for live appends */ wrefresh(win); } @@ -884,6 +966,19 @@ static void refresh_all(void) draw_input(); } +/* Full background repaint using redrawwin() — safe alternative to + * clearok(curscr) which resets ACS mode and breaks box() borders. */ +static void force_repaint(void) +{ + if (status_win) redrawwin(status_win); + if (sep_win) redrawwin(sep_win); + if (out_win) redrawwin(out_win); + if (mon_win) redrawwin(mon_win); + if (sep2_win) redrawwin(sep2_win); + if (input_win) redrawwin(input_win); + refresh_all(); +} + static void config_menu(void) { int mh = 18, mw = 64; @@ -926,7 +1021,7 @@ static void config_menu(void) strncpy(fields[3], hosts[cur_host].password, 79); fields[3][79]=0; strncpy(fields[4], hosts[cur_host].name, 49); fields[4][49]=0; - werase(w); ascii_box(w); + werase(w); draw_box(w); mvwprintw(w, 0, (mw-14)/2, " Config Hosts "); mvwprintw(w, 2, 2, "Host %d of %d PgUp/PgDn=switch N=new D=delete", cur_host+1, num_hosts); @@ -1002,18 +1097,17 @@ static void config_menu(void) #undef CF_COMMIT delwin(w); - touchwin(stdscr); refresh(); - refresh_all(); + force_repaint(); } /* ═══════════════════════════════════════════════════════════ * Monitor options (Ctrl+A -> M) * ═══════════════════════════════════════════════════════════ */ -static void monitor_options_menu(void) +/* Returns 'P' to switch to port page, 0 to close */ +static int mon_options_page(void) { - /* Two pages: 0 = options, 1 = ports. Tab switches between them. */ - static const char *opt_labels[] = { + static const char *labels[] = { "Monitor TX", "Monitor Supervisor", "Monitor Nodes", @@ -1021,120 +1115,125 @@ static void monitor_options_menu(void) "Enable Colour", "Show Monitor Pane", }; - int *opt_vals[] = { - &mon_tx, &mon_com, &mon_nodes, &mon_ui_only, &mon_colour, &mon_visible - }; - const int N_OPTS = 6; + int *vals[] = { &mon_tx, &mon_com, &mon_nodes, &mon_ui_only, &mon_colour, &mon_visible }; + const int N = 6; + int mh = N + 5, mw = 46; + int my = (LINES - mh) / 2; if (my < 0) my = 0; + int mx = (COLS - mw) / 2; if (mx < 0) mx = 0; - int mh = 14, mw = 46; - int my = (LINES-mh)/2; if (my<0) my=0; - int mx = (COLS-mw)/2; if (mx<0) mx=0; + force_repaint(); WINDOW *w = newwin(mh, mw, my, mx); wbkgd(w, COLOR_PAIR(CP_DEFAULT)); keypad(w, TRUE); nodelay(w, FALSE); - int page = 0; /* 0=options 1=ports */ - int cur = 0; - int running = 1; + int cur = 0; + for (;;) { + werase(w); + draw_box(w); + mvwprintw(w, 0, (mw - 18) / 2, " Monitor Options "); + for (int i = 0; i < N; i++) { + if (i == cur) wattron(w, A_REVERSE); + mvwprintw(w, 2 + i, 3, "[%c] %s", *vals[i] ? 'X' : ' ', labels[i]); + if (i == cur) wattroff(w, A_REVERSE); + } + mvwprintw(w, mh - 3, 3, "Up/Down=move Space=toggle"); + mvwprintw(w, mh - 2, 3, "Tab=Port Select Esc/Q=close"); + wrefresh(w); - while (running) { - werase(w); ascii_box(w); - - if (page == 0) { - mvwprintw(w, 0, (mw-18)/2, " Monitor Options "); - mvwprintw(w, 1, 2, "[Options] Ports Tab=switch"); - for (int i = 0; i < N_OPTS; i++) { - if (i == cur) wattron(w, A_REVERSE); - mvwprintw(w, 3+i, 3, "[%c] %s", - *opt_vals[i] ? 'X' : ' ', opt_labels[i]); - if (i == cur) wattroff(w, A_REVERSE); - } - mvwprintw(w, mh-3, 3, "Up/Down = navigate"); - mvwprintw(w, mh-2, 3, "Space = toggle Esc/Q = close"); - } else { - /* Build list of ports to show: detected ones, or 1-8 if none yet */ - int port_list[32], n_ports = 0; - for (int b = 0; b < 32; b++) - if (port_seen & (1 << b)) port_list[n_ports++] = b + 1; - if (n_ports == 0) - for (int b = 0; b < 8; b++) port_list[n_ports++] = b + 1; - - /* Resize window if needed */ - int need_h = 4 + n_ports + 1; - if (need_h < 8) need_h = 8; - if (need_h != mh) { - mh = need_h; + int ch = wgetch(w); + switch (ch) { + case KEY_UP: cur = (cur - 1 + N) % N; break; + case KEY_DOWN: cur = (cur + 1) % N; break; + case '\t': + delwin(w); + return 'P'; + case ' ': case '\r': case '\n': case KEY_ENTER: + if (cur == 5) { + mon_visible = !mon_visible; delwin(w); - my = (LINES-mh)/2; if (my<0) my=0; - mx = (COLS-mw)/2; if (mx<0) mx=0; - w = newwin(mh, mw, my, mx); - wbkgd(w, COLOR_PAIR(CP_DEFAULT)); - keypad(w, TRUE); - nodelay(w, FALSE); - werase(w); ascii_box(w); + create_windows(); + force_repaint(); + return 0; } + *vals[cur] = !(*vals[cur]); + send_trace_options(); + break; + case 27: case 'Q': case 'q': + delwin(w); + force_repaint(); + return 0; + } + } +} - mvwprintw(w, 0, (mw-16)/2, " Port Selection "); - mvwprintw(w, 1, 2, " Options [Ports] Tab=switch"); - if (port_seen == 0) - mvwprintw(w, 2, 3, "(connect to auto-detect ports)"); - for (int i = 0; i < n_ports; i++) { - int pn = port_list[i]; - if (i == cur) wattron(w, A_REVERSE); - mvwprintw(w, 3+i, 3, "[%c] %2d %-27s", - (port_mask >> (pn-1)) & 1 ? 'X' : ' ', - pn, port_names[pn-1]); - if (i == cur) wattroff(w, A_REVERSE); - } - mvwprintw(w, mh-2, 3, "Space = toggle Esc/Q = close"); +/* Returns 'O' to switch to options page, 0 to close */ +static int mon_ports_page(void) +{ + int port_list[32], n_ports = 0; + for (int b = 0; b < 32; b++) + if (port_seen & (1 << b)) port_list[n_ports++] = b + 1; + if (n_ports == 0) + for (int b = 0; b < 8; b++) port_list[n_ports++] = b + 1; + + int mh = n_ports + 5; if (mh < 10) mh = 10; + int mw = 54; + int my = (LINES - mh) / 2; if (my < 0) my = 0; + int mx = (COLS - mw) / 2; if (mx < 0) mx = 0; + + force_repaint(); + + WINDOW *w = newwin(mh, mw, my, mx); + wbkgd(w, COLOR_PAIR(CP_DEFAULT)); + keypad(w, TRUE); + nodelay(w, FALSE); + + int cur = 0; + for (;;) { + werase(w); + draw_box(w); + mvwprintw(w, 0, (mw - 16) / 2, " Port Selection "); + if (port_seen == 0) + mvwprintw(w, 2, 3, "(connect to auto-detect ports)"); + for (int i = 0; i < n_ports; i++) { + int pn = port_list[i]; + if (i == cur) wattron(w, A_REVERSE); + mvwprintw(w, 2 + i, 3, "[%c] %2d %-32s", + (port_mask >> (pn - 1)) & 1 ? 'X' : ' ', + pn, port_names[pn - 1]); + if (i == cur) wattroff(w, A_REVERSE); } + mvwprintw(w, mh - 3, 3, "Up/Down=move Space=toggle"); + mvwprintw(w, mh - 2, 3, "Tab=Monitor Opts Esc/Q=close"); wrefresh(w); - /* Build current port list for key handling */ - int port_list2[32], n_ports2 = 0; - if (page == 1) { - for (int b = 0; b < 32; b++) - if (port_seen & (1 << b)) port_list2[n_ports2++] = b + 1; - if (n_ports2 == 0) - for (int b = 0; b < 8; b++) port_list2[n_ports2++] = b + 1; - } - int max_items = (page == 0) ? N_OPTS : n_ports2; int ch = wgetch(w); switch (ch) { - case KEY_UP: - cur = (cur - 1 + max_items) % max_items; break; - case KEY_DOWN: - cur = (cur + 1) % max_items; break; + case KEY_UP: cur = (cur - 1 + n_ports) % n_ports; break; + case KEY_DOWN: cur = (cur + 1) % n_ports; break; case '\t': - page = 1 - page; cur = 0; break; + delwin(w); + return 'O'; case ' ': case '\r': case '\n': case KEY_ENTER: - if (page == 0) { - if (cur == 5) { /* Show Monitor Pane — needs window rebuild */ - mon_visible = !mon_visible; - delwin(w); - touchwin(stdscr); refresh(); - create_windows(); - refresh_all(); - return; - } - *opt_vals[cur] = !(*opt_vals[cur]); - send_trace_options(); - } else { - int pn = (cur < n_ports2) ? port_list2[cur] : cur + 1; - port_mask ^= (1 << (pn - 1)); - send_trace_options(); - } + port_mask ^= (1 << (port_list[cur] - 1)); + send_trace_options(); break; case 27: case 'Q': case 'q': - running = 0; break; + delwin(w); + force_repaint(); + return 0; } } +} - delwin(w); - touchwin(stdscr); refresh(); - refresh_all(); +static void monitor_options_menu(void) +{ + int next = 'O'; + while (next) { + if (next == 'O') next = mon_options_page(); + else next = mon_ports_page(); + } } /* ═══════════════════════════════════════════════════════════ @@ -1143,10 +1242,10 @@ static void monitor_options_menu(void) static void show_help(void) { - int mh = 18, mw = 52; + int mh = 19, mw = 52; WINDOW *w = newwin(mh, mw, (LINES-mh)/2, (COLS-mw)/2); wbkgd(w, COLOR_PAIR(CP_DEFAULT)); - ascii_box(w); + draw_box(w); mvwprintw(w, 0, (mw-8)/2, " Help "); mvwprintw(w, 2, 3, "Ctrl+A Open menu"); @@ -1160,15 +1259,15 @@ static void show_help(void) mvwprintw(w, 11, 3, "Enter Send line to server"); mvwprintw(w, 12, 3, "Up / Down Input history"); mvwprintw(w, 13, 3, "PgUp / PgDn Scroll output window"); - mvwprintw(w, 14, 3, "Backspace Delete char"); + mvwprintw(w, 14, 3, "Home / End Scroll monitor window"); + mvwprintw(w, 15, 3, "Backspace Delete char"); mvwprintw(w, mh-2, 3, "Press any key to close"); wrefresh(w); nodelay(w, FALSE); wgetch(w); delwin(w); - touchwin(stdscr); refresh(); - refresh_all(); + force_repaint(); } /* ═══════════════════════════════════════════════════════════ @@ -1212,9 +1311,33 @@ static void do_connect(int idx) COLOR_PAIR(CP_YELLOW)); } - send_trace_options(); - /* Ask the node for its port list; response is parsed in parse_session_line() */ - tcp_send("PORTS\r", 6); + /* P8=1 triggers BPQ's SendPortsForMonitor → arrives as 0xFF 0xFF packet */ + send_trace_options_with_ports(); + + /* Drain incoming data for up to 2 seconds waiting for PORTS response. + * A single recv() often gets only the login banner; loop until we have + * the full port list (ports_loaded) or the deadline passes. */ + { + char rbuf[4096]; + struct timeval t0, now; + gettimeofday(&t0, NULL); + for (;;) { + gettimeofday(&now, NULL); + long elapsed = (now.tv_sec - t0.tv_sec) * 1000000L + + (now.tv_usec - t0.tv_usec); + if (elapsed >= 2000000L) break; + if (ports_loaded) break; + long remain = 2000000L - elapsed; + fd_set rfds; + struct timeval tv = { remain / 1000000L, remain % 1000000L }; + FD_ZERO(&rfds); FD_SET(sock, &rfds); + if (select(sock + 1, &rfds, NULL, NULL, &tv) <= 0) break; + int n = recv(sock, rbuf, sizeof(rbuf) - 1, 0); + if (n <= 0) break; + bpq_process(rbuf, n); + } + } + draw_status(); } @@ -1230,7 +1353,7 @@ static void ctrl_a_menu(void) WINDOW *w = newwin(mh, mw, my, mx); wbkgd(w, COLOR_PAIR(CP_DEFAULT)); - ascii_box(w); + draw_box(w); mvwprintw(w, 0, (mw-14)/2, " TermTCP Menu "); mvwprintw(w, 2, 4, "C - Connect"); mvwprintw(w, 3, 4, "D - Disconnect"); @@ -1245,18 +1368,27 @@ static void ctrl_a_menu(void) nodelay(w, FALSE); int ch = wgetch(w); delwin(w); - touchwin(stdscr); refresh(); - refresh_all(); + force_repaint(); switch (ch | 0x20) { case 'c': if (num_hosts == 1) { do_connect(0); } else if (num_hosts > 1) { - int ph = num_hosts + 5, pw = 38; + /* Auto-size width to fit the longest entry */ + int pw = 20; + for (int i = 0; i < num_hosts; i++) { + int len = snprintf(NULL, 0, "%d - %s (%s:%s)", i, + hosts[i].name, hosts[i].host, hosts[i].port) + 6; + if (len > pw) pw = len; + } + int hint_w = snprintf(NULL, 0, "0-%d = pick Esc = cancel", num_hosts-1) + 6; + if (hint_w > pw) pw = hint_w; + if (pw > COLS) pw = COLS; + int ph = num_hosts + 5; WINDOW *hw = newwin(ph, pw, (LINES-ph)/2, (COLS-pw)/2); wbkgd(hw, COLOR_PAIR(CP_DEFAULT)); - box(hw, 0, 0); + draw_box(hw); mvwprintw(hw, 0, (pw-14)/2, " Select Host "); for (int i = 0; i < num_hosts; i++) mvwprintw(hw, 2+i, 3, "%d - %s (%s:%s)", i, @@ -1266,8 +1398,7 @@ static void ctrl_a_menu(void) nodelay(hw, FALSE); int hch = wgetch(hw); delwin(hw); - touchwin(stdscr); refresh(); - refresh_all(); + force_repaint(); if (hch >= '0' && hch < '0' + num_hosts) do_connect(hch - '0'); } @@ -1358,6 +1489,27 @@ static void handle_key(int ch) } return; + case KEY_HOME: /* Home — scroll monitor back */ + if (mon_win && mon_visible) { + int page = getmaxy(mon_win); + mon_scroll += page; + int max_scroll = mon_sbuf_count > 0 ? mon_sbuf_count - 1 : 0; + if (mon_scroll > max_scroll) mon_scroll = max_scroll; + scrl_redraw_mon(); + if (input_win) wrefresh(input_win); + } + return; + + case KEY_END: /* End — scroll monitor forward / return to live */ + if (mon_win && mon_visible && mon_scroll > 0) { + int page = getmaxy(mon_win); + mon_scroll -= page; + if (mon_scroll < 0) mon_scroll = 0; + scrl_redraw_mon(); + if (input_win) wrefresh(input_win); + } + return; + case '\r': case '\n': case KEY_ENTER: if (input_len == 0) return; input_buf[input_len] = '\0'; @@ -1401,8 +1553,7 @@ static void handle_key(int ch) if (hist_pos < 0) break; hist_pos--; if (hist_pos < 0) { - strncpy(input_buf, hist_save, INPUT_MAX - 1); - input_buf[INPUT_MAX - 1] = '\0'; + input_buf[0] = '\0'; /* always end with blank line */ } else { int idx = (hist_head - 1 - hist_pos + HIST_MAX * 2) % HIST_MAX; strncpy(input_buf, hist_buf[idx], INPUT_MAX - 1); @@ -1495,6 +1646,7 @@ int main(int argc, char *argv[]) load_config(); + setlocale(LC_ALL, ""); /* enable UTF-8 so ncursesw renders box chars correctly */ initscr(); cbreak(); noecho(); diff --git a/termtcp_cli.o b/termtcp_cli.o index a993530..83492ac 100644 Binary files a/termtcp_cli.o and b/termtcp_cli.o differ