New Version 52 all issues fix with better menu's and borders along with port names.

gui-redesign
Ryan Collier 4 weeks ago
parent 4ba7d18ea9
commit fb58f3089b

@ -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

@ -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**

Binary file not shown.

@ -27,14 +27,17 @@
#include <fcntl.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <time.h>
#include <locale.h>
#include <wchar.h>
#include <ncurses.h>
#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();

Binary file not shown.
Loading…
Cancel
Save

Powered by TurnKey Linux.