/* * 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 "dbg.h" #include "ttysocket.h" #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 chardevice *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); free(buf); break; } free(buf); } curs_set(0); if(for_resize) wnoutrefresh(msg_win); else wrefresh(msg_win); } 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); char buf[128]; if(insert_mode){ if(dtty){ switch(dtty->type){ case DEV_NETSOCKET: snprintf(buf, 127, "INSERT (TAB to switch, ctrl+D to quit) HOST: %s, ENDLINE: %s, PORT: %s", dtty->name, dtty->seol, dtty->port); break; case DEV_UNIXSOCKET: snprintf(buf, 127, "INSERT (TAB to switch, ctrl+D to quit) HOST: %s, ENDLINE: %s, PATH: %s", dtty->name, dtty->seol, dtty->port); break; case DEV_TTY: snprintf(buf, 127, "INSERT (TAB to switch, ctrl+D to quit) DEV: %s, ENDLINE: %s, SPEED: %d", dtty->name, dtty->seol, dtty->speed); break; default: break; }}else{ snprintf(buf, 127, "INSERT (TAB to switch, ctrl+D to quit) NOT INITIALIZED"); } }else{ snprintf(buf, 127, "SCROLL (TAB to switch, q to quit) ENDLINE: %s", dtty?dtty->seol:"n"); } wprintw(sep_win, "%s", buf); if(for_resize) wnoutrefresh(sep_win); else wrefresh(sep_win); cmd_win_redisplay(for_resize); } /** * @brief ShowData - show string on display * @param text - text string */ void ShowData(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(true); show_mode(true); doupdate(); } 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(){ visual_mode = false; delwin(msg_win); delwin(sep_win); delwin(cmd_win); endwin(); } 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(SendData(dtty, line) == -1){ ERRX("Device disconnected"); } 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); } } /** * @brief cmdline - console reading process; runs as separate thread * @param arg - tty/socket device to write strings entered by user * @return NULL */ void *cmdline(void* arg){ MEVENT event; dtty = (chardevice*)arg; show_mode(false); 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); if(insert_mode) curs_set(2); else curs_set(0); 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; }