From b87fb2e22a9f28a84d2844cfd3f3a82ee996bb11 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Tue, 22 Dec 2020 18:42:27 +0300 Subject: [PATCH] remake: do it @ncurses & readline --- CMakeLists.txt | 13 +- Readme.md | 18 ++- cmdlnopts.c | 4 +- cmdlnopts.h | 1 + main.c | 83 ++++++----- ncurses_and_readline.c | 331 +++++++++++++++++++++++++++++++++++++++++ ncurses_and_readline.h | 39 +++++ tty.c | 99 ++++-------- tty.h | 3 +- 9 files changed, 477 insertions(+), 114 deletions(-) create mode 100644 ncurses_and_readline.c create mode 100644 ncurses_and_readline.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eb946aa..d88565a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,12 +33,23 @@ endif() ###### pkgconfig ###### # pkg-config modules (for pkg-check-modules) -set(MODULES usefull_macros) +set(MODULES ncurses readline usefull_macros) # find packages: find_package(PkgConfig REQUIRED) +find_package(Threads REQUIRED) pkg_check_modules(${PROJ} REQUIRED ${MODULES}) +find_package(Threads REQUIRED) +if(THREADS_HAVE_PTHREAD_ARG) + set_property(TARGET ${PROJ} PROPERTY COMPILE_OPTIONS "-pthread") + set_property(TARGET ${PROJ} PROPERTY INTERFACE_COMPILE_OPTIONS "-pthread") +endif() +if(CMAKE_THREAD_LIBS_INIT) + list(APPEND ${PROJ}_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}") +endif() + + ###### additional flags ###### #list(APPEND ${PROJ}_LIBRARIES "-lfftw3_threads") diff --git a/Readme.md b/Readme.md index 1115f3c..948413c 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,13 @@ Very simple terminal client =========================== - Usage: tty_term [args] - - Where args are: - - -d, --devname=arg serial device name - -e, --eol=arg end of line: n, r or rn - -h, --help show this help - -s, --speed=arg baudrate (default: 9600) + +Usage: tty_term [args] + + Where args are: + + -d, --devname=arg serial device name + -e, --eol=arg end of line: n (default), r, nr or rn + -h, --help show this help + -s, --speed=arg baudrate (default: 9600) + -t, --timeout=arg timeout for select() in ms (default: 100) diff --git a/cmdlnopts.c b/cmdlnopts.c index 12c9ab8..9e13a09 100644 --- a/cmdlnopts.c +++ b/cmdlnopts.c @@ -35,6 +35,7 @@ glob_pars const Gdefault = { .speed = 9600, .ttyname = "/dev/ttyUSB0", .eol = "n", + .tmoutms = 100, }; /* @@ -46,7 +47,8 @@ static myoption cmdlnopts[] = { {"help", NO_ARGS, NULL, 'h', arg_int, APTR(&help), _("show this help")}, {"speed", NEED_ARG, NULL, 's', arg_int, APTR(&G.speed), _("baudrate (default: 9600)")}, {"devname", NEED_ARG, NULL, 'd', arg_string, APTR(&G.ttyname), _("serial device name")}, - {"eol", NEED_ARG, NULL, 'e', arg_string, APTR(&G.eol), _("end of line: n, r or rn")}, + {"eol", NEED_ARG, NULL, 'e', arg_string, APTR(&G.eol), _("end of line: n (default), r, nr or rn")}, + {"timeout", NEED_ARG, NULL, 't', arg_int, APTR(&G.tmoutms), _("timeout for select() in ms (default: 100)")}, end_option }; diff --git a/cmdlnopts.h b/cmdlnopts.h index 2ffac8f..ee617fa 100644 --- a/cmdlnopts.h +++ b/cmdlnopts.h @@ -25,6 +25,7 @@ */ typedef struct{ int speed; // baudrate + int tmoutms; // timeout for select() in ms char *ttyname; // device name char *eol; // end of line: \r (CR), \rn (CR+LF) or \n (LF): "r", "rn", "n" } glob_pars; diff --git a/main.c b/main.c index fb51e2e..b0c252a 100644 --- a/main.c +++ b/main.c @@ -21,61 +21,72 @@ #include // strcmp #include #include "cmdlnopts.h" +#include "ncurses_and_readline.h" #include "tty.h" -// we don't need more than 64 bytes for TTY input buffer - USB CDC can't transmit more in one packet -#define BUFLEN 65 +#define BUFLEN 4096 -static TTY_descr *dev = NULL; +static ttyd dtty = {.dev = NULL, .mutex = PTHREAD_MUTEX_INITIALIZER}; void signals(int signo){ - restore_console(); - if(dev) close_tty(&dev); + if(dtty.dev){ + pthread_mutex_lock(&dtty.mutex); + close_tty(&dtty.dev); + } + deinit_ncurses(); + deinit_readline(); exit(signo); } -typedef enum{ - EOL_N = 0, - EOL_R, - EOL_RN -} eol_t; - int main(int argc, char **argv){ glob_pars *G = NULL; // default parameters see in cmdlnopts.c initial_setup(); G = parse_args(argc, argv); - dev = new_tty(G->ttyname, G->speed, BUFLEN); - if(!dev || !(dev = tty_open(dev, 1))) return 1; // open exclusively - eol_t eol = EOL_N; - if(strcmp(G->eol, "n")){ // change eol - if(strcmp(G->eol, "r") == 0) eol = EOL_R; - else if(strcmp(G->eol, "rn") == 0) eol = EOL_RN; - else ERRX("End of line should be \"r\", \"n\" or \"rn\""); + if(G->tmoutms < 0) ERRX("Timeout should be >= 0"); + dtty.dev = new_tty(G->ttyname, G->speed, BUFLEN); + if(!dtty.dev || !(dtty.dev = tty_open(dtty.dev, 1))){ + WARN("Can't open device %s", G->ttyname); + signals(1); } + init_ncurses(); + init_readline(); + const char *EOL = "\n"; + if(strcasecmp(G->eol, "n")){ + if(strcasecmp(G->eol, "r") == 0) EOL = "\r"; + else if(strcasecmp(G->eol, "rn") == 0) EOL = "\r\n"; + else if(strcasecmp(G->eol, "nr") == 0) EOL = "\n\r"; + else ERRX("End of line should be \"r\", \"n\" or \"rn\" or \"nr\""); + } + strcpy(dtty.eol, EOL); + int eollen = strlen(EOL); + dtty.eollen = eollen; signal(SIGTERM, signals); // kill (-15) - quit signal(SIGHUP, signals); // hup - quit signal(SIGINT, signals); // ctrl+C - quit signal(SIGQUIT, signals); // ctrl+\ - quit signal(SIGTSTP, SIG_IGN); // ignore ctrl+Z - setup_con(); - const char r = '\r'; + pthread_t writer; + if(pthread_create(&writer, NULL, cmdline, (void*)&dtty)) ERR("pthread_create()"); + settimeout(G->tmoutms); while(1){ - int b; - int l = read_ttyX(dev, &b); - if(l < 0) signals(9); - if(b > -1){ - char c = (char)b; - if(c == '\n' && eol != EOL_N){ // !\n - if(eol == EOL_R) c = '\r'; // \r - else if(write_tty(dev->comfd, &r, 1)) WARN("write_tty()"); // \r\n - } - if(write_tty(dev->comfd, &c, 1)) WARN("write_tty()"); - } - if(l){ - printf("%s", dev->buf); - fflush(stdout); - // if(fout) copy_buf_to_file(buff, &oldcmd); - } + //if(0 == pthread_mutex_lock(&dtty.mutex)){ + int l = Read_tty(dtty.dev); + if(l){ + char *buf = dtty.dev->buf; + char *eol = NULL, *estr = buf + l; + do{ + eol = strstr(buf, EOL); + if(eol){ + *eol = 0; + add_ttydata(buf); + buf = eol + eollen; + }else{ + add_ttydata(buf); + } + }while(eol && buf < estr); + }else if(l < 0) signals(9); + // pthread_mutex_unlock(&dtty.mutex); + //} } // never reached return 0; diff --git a/ncurses_and_readline.c b/ncurses_and_readline.c new file mode 100644 index 0000000..fe4c1ca --- /dev/null +++ b/ncurses_and_readline.c @@ -0,0 +1,331 @@ +/* + * This file is part of the ttyterm project. + * Copyright 2020 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +// based on https://stackoverflow.com/a/28709979/1965803 -> +// https://github.com/ulfalizer/readline-and-ncurses +// Copyright (c) 2015-2019, Ulf Magnusson +// SPDX-License-Identifier: ISC + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ncurses_and_readline.h" + +// Keeps track of the terminal mode so we can reset the terminal if needed on errors +static bool visual_mode = false; +// insert commands when true; roll upper screen when false +static bool insert_mode = true; +static bool should_exit = false; + +static ttyd *dtty = NULL; + +static void fail_exit(const char *msg){ + // Make sure endwin() is only called in visual mode. As a note, calling it + // twice does not seem to be supported and messed with the cursor position. + if(visual_mode) endwin(); + fprintf(stderr, "%s\n", msg); + exit(EXIT_FAILURE); +} + +static WINDOW *msg_win; // Message window +static WINDOW *sep_win; // Separator line above the command (readline) window +static WINDOW *cmd_win; // Command (readline) window + +// string list +typedef struct _Line{ + int Nline; + char *contents; + struct _Line *prev, *next; +} Line; +// head of list, current item and first line on screen +Line *head = NULL, *curr = NULL, *firstline = NULL; +int nr_lines = 0; // total anount of data portions @ input + +static unsigned char input; // Input character for readline + +// Used to signal "no more input" after feeding a character to readline +static bool input_avail = false; + +// Not bothering with 'input_avail' and just returning 0 here seems to do the +// right thing too, but this might be safer across readline versions +static int readline_input_avail(){ + return input_avail; +} + +static int readline_getc(__attribute__((__unused__)) FILE *dummy){ + input_avail = false; + return input; +} + +static void forward_to_readline(char c){ + input = c; + input_avail = true; + rl_callback_read_char(); +} + +static void msg_win_redisplay(bool for_resize){ + werase(msg_win); + Line *l = firstline; + int nlines = 0; // total amount of lines @ output + for(; l && (nlines < LINES - 2); l = l->next){ + size_t contlen = strlen(l->contents) + 128; + char *buf = malloc(contlen); + // don't add trailing '\n' (or last line will be empty with cursor) + contlen = snprintf(buf, contlen, "%s", l->contents); + int nlnext = (contlen - 1) / COLS + 1; + wmove(msg_win, nlines, 0); + if(nlines + nlnext < LINES-2){ // can put out the full line + waddstr(msg_win, buf); + //wprintw(msg_win, "%d (%d): %s -> %d", l->Nline, firstline->Nline, l->contents, nlnext); + nlines += nlnext; + }else{ // put only first part + int rest = LINES-2 - nlines; + waddnstr(msg_win, buf, rest *COLS); + break; + } + free(buf); + } + curs_set(0); + if(for_resize) wnoutrefresh(msg_win); + else wrefresh(msg_win); +} + +void add_ttydata(const char *text){ + if(!text) return; + if(!*text) text = " "; // empty string + Line *lp = malloc(sizeof(Line)); + lp->contents = strdup(text); + lp->prev = curr; + lp->next = NULL; + lp->Nline = nr_lines++; + if(!curr || !head){ + head = curr = firstline = lp; + }else + curr->next = lp; + curr = lp; + // roll back to show last input + if(curr->prev){ + firstline = curr; + int totalln = (strlen(firstline->contents) - 1)/COLS + 1; + while(firstline->prev){ + totalln += (strlen(firstline->prev->contents) - 1)/COLS + 1; + if(totalln > LINES-2) break; + firstline = firstline->prev; + } + } + msg_win_redisplay(false); +} + +static void cmd_win_redisplay(bool for_resize){ + int cursor_col = 2 + rl_point; // "> " width is 2 + werase(cmd_win); + int x = 0, maxw = COLS-2; + if(cursor_col > maxw){ + x = cursor_col - maxw; + cursor_col = maxw; + } + char abuf[4096]; + snprintf(abuf, 4096, "> %s", rl_line_buffer); + waddstr(cmd_win, abuf+x); + wmove(cmd_win, 0, cursor_col); + curs_set(2); + if(for_resize) wnoutrefresh(cmd_win); + else wrefresh(cmd_win); +} + +static void readline_redisplay(){ + cmd_win_redisplay(false); +} + +static void show_mode(bool for_resize){ + wclear(sep_win); + if(insert_mode) waddstr(sep_win, "INSERT (TAB to switch, ctrl+D to quit)"); + else waddstr(sep_win, "SCROLL (TAB to switch, q to quit)"); + if(for_resize) wnoutrefresh(sep_win); + else wrefresh(sep_win); + cmd_win_redisplay(for_resize); +} + +static void resize(){ + if(LINES > 2){ + wresize(msg_win, LINES - 2, COLS); + wresize(sep_win, 1, COLS); + wresize(cmd_win, 1, COLS); + mvwin(sep_win, LINES - 2, 0); + mvwin(cmd_win, LINES - 1, 0); + } + msg_win_redisplay(true); + show_mode(true); + doupdate(); +} + +void init_ncurses(){ + if (!initscr()) + fail_exit("Failed to initialize ncurses"); + visual_mode = true; + if(has_colors()){ + start_color(); + use_default_colors(); + } + cbreak(); + noecho(); + nonl(); + intrflush(NULL, FALSE); + keypad(cmd_win, 0); + curs_set(2); + if(LINES > 2){ + msg_win = newwin(LINES - 2, COLS, 0, 0); + sep_win = newwin(1, COLS, LINES - 2, 0); + cmd_win = newwin(1, COLS, LINES - 1, 0); + } + else{ + msg_win = newwin(1, COLS, 0, 0); + sep_win = newwin(1, COLS, 0, 0); + cmd_win = newwin(1, COLS, 0, 0); + } + if(!msg_win || !sep_win || !cmd_win) + fail_exit("Failed to allocate windows"); + if(has_colors()){ + init_pair(1, COLOR_WHITE, COLOR_BLUE); + wbkgd(sep_win, COLOR_PAIR(1)); + }else + wbkgd(sep_win, A_STANDOUT); + show_mode(false); + mousemask(BUTTON4_PRESSED|BUTTON5_PRESSED, NULL); +} + +void deinit_ncurses(){ + delwin(msg_win); + delwin(sep_win); + delwin(cmd_win); + endwin(); + visual_mode = false; +} + +static void got_command(char *line){ + if(!line) // Ctrl-D pressed on empty line + should_exit = true; + else{ + if(!*line) return; // zero length + add_history(line); + if(dtty && dtty->dev){ + + //if(0 == pthread_mutex_lock(&dtty->mutex)){ + if(write_tty(dtty->dev->comfd, line, strlen(line))) signals(9); + if(write_tty(dtty->dev->comfd, dtty->eol, dtty->eollen)) signals(9); + // pthread_mutex_unlock(&dtty->mutex); + //} + } + free(line); + } +} + +void init_readline(){ + rl_catch_signals = 0; + rl_catch_sigwinch = 0; + rl_deprep_term_function = NULL; + rl_prep_term_function = NULL; + rl_change_environment = 0; + rl_getc_function = readline_getc; + rl_input_available_hook = readline_input_avail; + rl_redisplay_function = readline_redisplay; + rl_callback_handler_install("> ", got_command); +} + +void deinit_readline(){ + rl_callback_handler_remove(); +} + +static void rolldown(){ + if(firstline && firstline->prev){ + firstline = firstline->prev; + msg_win_redisplay(false); + } +} + +static void rollup(){ + if(firstline && firstline->next){ + firstline = firstline->next; + msg_win_redisplay(false); + } +} + +void *cmdline(void* arg){ + MEVENT event; + dtty = (ttyd*)arg; + do{ + int c = wgetch(cmd_win); + bool processed = true; + switch(c){ + case KEY_MOUSE: + if(getmouse(&event) == OK){ + if(event.bstate & (BUTTON4_PRESSED)) rolldown(); // wheel up + else if(event.bstate & (BUTTON5_PRESSED)) rollup(); // wheel down + } + break; + case '\t': // tab switch between scroll and edit mode + keypad(cmd_win, insert_mode); // enable/disable reaction @ special characters + insert_mode = !insert_mode; + show_mode(false); + break; + case KEY_RESIZE: + resize(); + break; + default: + processed = false; + } + if(processed) continue; + if(insert_mode){ + forward_to_readline(c); + }else{ + switch(c){ + case KEY_UP: // roll down for one item + rolldown(); + break; + case KEY_DOWN: // roll up for one item + rollup(); + break; + case KEY_PPAGE: // PageUp: roll down for 10 items + for(int i = 0; i < 10; ++i){ + if(firstline && firstline->prev) firstline = firstline->prev; + else break; + } + msg_win_redisplay(false); + break; + case KEY_NPAGE: // PageUp: roll up for 10 items + for(int i = 0; i < 10; ++i){ + if(firstline && firstline->next) firstline = firstline->next; + else break; + } + msg_win_redisplay(false); + break; + default: + if(c == 'q' || c == 'Q') should_exit = true; // quit + } + } + }while(!should_exit); + signals(0); + return NULL; +} diff --git a/ncurses_and_readline.h b/ncurses_and_readline.h new file mode 100644 index 0000000..f7ef07f --- /dev/null +++ b/ncurses_and_readline.h @@ -0,0 +1,39 @@ +/* + * This file is part of the ttyterm project. + * Copyright 2020 Edward V. Emelianov . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once +#ifndef NCURSES_AND_READLINE_H__ +#define NCURSES_AND_READLINE_H__ + +#include +#include + +typedef struct{ + TTY_descr *dev; + pthread_mutex_t mutex; + char eol[3]; + int eollen; +} ttyd; + +void init_readline(); +void deinit_readline(); +void init_ncurses(); +void deinit_ncurses(); +void *cmdline(void* arg); +void add_ttydata(const char *text); + +#endif // NCURSES_AND_READLINE_H__ diff --git a/tty.c b/tty.c index e2a9990..315a766 100644 --- a/tty.c +++ b/tty.c @@ -20,78 +20,43 @@ #include #include "tty.h" -/** - * @brief read_ttyX- read data from TTY & console with 1ms timeout WITH disconnect detection - * @param buff (o) - buffer for data read - * @param length - buffer len - * @param rb (o) - byte read from console or -1 - * @return amount of bytes read on tty or -1 if disconnected - */ -int read_ttyX(TTY_descr *d, int *rb){ - if(!d || d->comfd < 0) return -1; - ssize_t L = 0; +static int sec = 0, usec = 100; // timeout + +void settimeout(int tmout){ + sec = 0; + if(tmout > 999){ + sec = tmout / 1000; + tmout -= sec * 1000; + } + usec = tmout * 1000L; +} + +int Read_tty(TTY_descr *d){ + if(d->comfd < 0) return 0; + size_t L = 0; + ssize_t l; + size_t length = d->bufsz; + char *ptr = d->buf; fd_set rfds; struct timeval tv; int retval; - if(rb) *rb = -1; - FD_ZERO(&rfds); - FD_SET(STDIN_FILENO, &rfds); - FD_SET(d->comfd, &rfds); - // wait for 1ms - tv.tv_sec = 0; tv.tv_usec = 1000; - retval = select(d->comfd + 1, &rfds, NULL, NULL, &tv); - if(!retval) return 0; - if(retval < 0){ - WARN("select()"); - return -1; - } - if(FD_ISSET(STDIN_FILENO, &rfds) && rb){ - *rb = getchar(); - } - if(FD_ISSET(d->comfd, &rfds)){ - if((L = read(d->comfd, d->buf, d->bufsz)) < 1){ - WARN("read()"); - return -1; // disconnect or other error - close TTY & die + do{ + l = 0; + FD_ZERO(&rfds); + FD_SET(d->comfd, &rfds); + tv.tv_sec = sec; tv.tv_usec = usec; + retval = select(d->comfd + 1, &rfds, NULL, NULL, &tv); + if (!retval) break; + if(FD_ISSET(d->comfd, &rfds)){ + l = read(d->comfd, ptr, length); + if(l < 0) return -1; + if(l == 0) break; + ptr += l; L += l; + length -= l; } - } - d->buflen = (size_t)L; + }while(l && length); + d->buflen = L; d->buf[L] = 0; return (int)L; } - -#if 0 -/** - * Copy line by line buffer buff to file removing cmd starting from newline - * @param buffer - data to put into file - * @param cmd - symbol to remove from line startint (if found, change *cmd to (-1) - * or NULL, (-1) if no command to remove - */ -void copy_buf_to_file(char *buffer, int *cmd){ - char *buff, *line, *ptr; - if(!cmd || *cmd < 0){ - fprintf(fout, "%s", buffer); - return; - } - buff = strdup(buffer), ptr = buff; - do{ - if(!*ptr) break; - if(ptr[0] == (char)*cmd){ - *cmd = -1; - ptr++; - if(ptr[0] == '\n') ptr++; - if(!*ptr) break; - } - line = ptr; - ptr = strchr(buff, '\n'); - if(ptr){ - *ptr++ = 0; - fprintf(fout, "%s\n", line); - }else - fprintf(fout, "%s", line); // no newline found in buffer -// fprintf(fout, "%s\n", line); - }while(ptr); - free(buff); -} - -#endif diff --git a/tty.h b/tty.h index fb00a0b..a119fc6 100644 --- a/tty.h +++ b/tty.h @@ -22,6 +22,7 @@ #include -int read_ttyX(TTY_descr *d, int *rb); +int Read_tty(TTY_descr *d); +void settimeout(int tms); #endif // TTY_H__