--- lbbs/src/screen.c 2025/06/17 13:18:55 1.100 +++ lbbs/src/screen.c 2025/12/18 10:43:48 1.136 @@ -1,43 +1,54 @@ -/*************************************************************************** - screen.c - description - ------------------- - Copyright : (C) 2004-2025 by Leaflet - Email : leaflet@leafok.com - ***************************************************************************/ - -/*************************************************************************** - * * - * 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. * - * * - ***************************************************************************/ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * screen + * - advanced telnet-based user interactive input / output features + * + * Copyright (C) 2004-2025 Leaflet + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif -#include "screen.h" #include "bbs.h" #include "common.h" -#include "str_process.h" -#include "log.h" -#include "io.h" #include "editor.h" #include "file_loader.h" -#include +#include "io.h" +#include "log.h" +#include "login.h" +#include "screen.h" +#include "str_process.h" #include +#include +#include #include -#include #include -#include -#include -#include +#include +#include #include -#include +#include +#include + +const char CTRL_SEQ_CLR_LINE[] = "\033[K"; + +static const int ACTIVE_BOARD_HEIGHT = 8; -#define ACTIVE_BOARD_HEIGHT 8 +static const int STR_TOP_LEFT_MAX_LEN = 80; +static const int STR_TOP_MIDDLE_MAX_LEN = 40; +static const int STR_TOP_RIGHT_MAX_LEN = 80; -#define STR_TOP_LEFT_MAX_LEN 40 -#define STR_TOP_MIDDLE_MAX_LEN 20 -#define STR_TOP_RIGHT_MAX_LEN 20 +static size_t get_time_str(char *s, size_t len) +{ + time_t curtime; + struct tm local_tm; + + time(&curtime); + localtime_r(&curtime, &local_tm); + size_t j = strftime(s, len, "%m/%d %H:%M %Z", &local_tm); + + return j; +} void moveto(int row, int col) { @@ -77,18 +88,35 @@ void clrtobot(int line_begin) void clearscr() { prints("\033[2J"); - moveto(0, 0); + moveto(1, 1); } -int press_any_key() +inline int press_any_key() { + return press_any_key_ex(" \033[1;33m按任意键继续...\033[m", 60); +} + +int press_any_key_ex(const char *msg, int sec) +{ + int ch = 0; + int duration = 0; + time_t t_begin = time(NULL); + moveto(SCREEN_ROWS, 0); clrtoeol(); - prints(" \033[1;33m...\033[0;37m"); + prints(msg); iflush(); - return igetch_t(MIN(MAX_DELAY_TIME, 60)); + igetch_reset(); + + do + { + ch = igetch_t(sec - duration); + duration = (int)(time(NULL) - t_begin); + } while (!SYS_server_exit && ch == 0 && duration < 60); + + return ch; } void set_input_echo(int echo) @@ -96,136 +124,487 @@ void set_input_echo(int echo) if (echo) { outc('\x83'); // ASCII code 131 - iflush(); } else { - // outc ('\x85'); // ASCII code 133 + // outc ('\x85'); // ASCII code 133 prints("\xff\xfb\x01\xff\xfb\x03"); - iflush(); - igetch(0); - igetch_reset(); } + iflush(); } -static int _str_input(char *buffer, int buf_size, int echo_mode) +static int _str_input(char *buffer, int buf_size, int max_display_len, enum io_echo_t echo_mode) { - int c; + int ch; int offset = 0; - int hz = 0; + int eol; + int display_len; + char input_str[5]; + int str_len = 0; + wchar_t wcs[2]; + char c; buffer[buf_size - 1] = '\0'; - for (offset = 0; offset < buf_size - 1 && buffer[offset] != '\0'; offset++) - ; + offset = split_line(buffer, max_display_len, &eol, &display_len, 0); igetch_reset(); while (!SYS_server_exit) { - c = igetch_t(MIN(MAX_DELAY_TIME, 60)); + ch = igetch_t(MIN(BBS_max_user_idle_time, 60)); - if (c == CR) + if (ch == CR) { - igetch_reset(); break; } - else if (c == KEY_TIMEOUT || c == KEY_NULL) // timeout or broken pipe + else if (ch == KEY_TIMEOUT || ch == KEY_NULL) // timeout or broken pipe { return -1; } - else if (c == LF || c == '\0') + else if (ch == LF || ch == '\0') { continue; } - else if (c == BACKSPACE) + else if (ch == BACKSPACE) { if (offset > 0) { offset--; - if (buffer[offset] < 0 || buffer[offset] > 127) + if (buffer[offset] < 0 || buffer[offset] > 127) // UTF8 { - prints("\033[D \033[D"); - offset--; - if (offset < 0) // should not happen + while (offset > 0 && (buffer[offset] & 0xc0) != 0xc0) { - log_error("Offset of buffer is negative\n"); - offset = 0; + offset--; } + display_len--; + prints("\033[D \033[D"); } buffer[offset] = '\0'; + display_len--; prints("\033[D \033[D"); iflush(); } continue; } - else if (c > 255 || iscntrl(c)) + else if (ch > 255 || iscntrl(ch)) { continue; } - else if (c > 127 && c <= 255) + else if ((ch & 0xff80) == 0x80) // head of multi-byte character { - if (!hz && offset + 2 > buf_size - 1) // No enough space for Chinese character + str_len = 0; + c = (char)(ch & 0xf0); + while (c & 0x80) + { + input_str[str_len] = (char)(ch - 256); + str_len++; + c = (c & 0x7f) << 1; + + if ((c & 0x80) == 0) // Input completed + { + break; + } + + // Expect additional bytes of input + ch = igetch(100); // 0.1 second + if (ch == KEY_NULL || ch == KEY_TIMEOUT) // Ignore received bytes if no futher input + { + log_debug("Ignore %d bytes of incomplete UTF8 character\n", str_len); + str_len = 0; + break; + } + } + input_str[str_len] = '\0'; + + if (str_len == 0) // Incomplete input + { + continue; + } + + if (mbstowcs(wcs, input_str, 1) == (size_t)-1) + { + log_error("mbstowcs() error\n"); + } + if (offset + str_len > buf_size - 1 || display_len + (UTF8_fixed_width ? 2 : wcwidth(wcs[0])) > max_display_len) // No enough space for Chinese character { - igetch(0); // Ignore 1 character outc('\a'); iflush(); continue; } - hz = (!hz); - } - if (offset + 1 > buf_size - 1) - { - outc('\a'); - iflush(); - continue; + memcpy(buffer + offset, input_str, (size_t)str_len); + offset += str_len; + buffer[offset] = '\0'; + display_len += 2; + + switch (echo_mode) + { + case DOECHO: + prints(input_str); + break; + case NOECHO: + prints("**"); + break; + } } + else if (ch >= 32 && ch < 127) // Printable character + { + if (offset + 1 > buf_size - 1 || display_len + 1 > max_display_len) + { + outc('\a'); + iflush(); + continue; + } - buffer[offset++] = (char)c; - buffer[offset] = '\0'; + buffer[offset++] = (char)ch; + buffer[offset] = '\0'; + display_len++; - switch (echo_mode) - { - case DOECHO: - outc((char)c); - break; - case NOECHO: - outc('*'); - break; + switch (echo_mode) + { + case DOECHO: + outc((char)ch); + break; + case NOECHO: + outc('*'); + break; + } } - if (!hz) + else // Invalid character { - iflush(); + continue; } + + iflush(); } return offset; } -int str_input(char *buffer, int buf_size, int echo_mode) +int str_input(char *buffer, int buf_size, enum io_echo_t echo_mode) { int len; buffer[0] = '\0'; - len = _str_input(buffer, buf_size, echo_mode); + len = _str_input(buffer, buf_size, buf_size, echo_mode); prints("\r\n"); iflush(); return len; -}; +} -int get_data(int row, int col, char *prompt, char *buffer, int buf_size, int echo_mode) +int get_data(int row, int col, char *prompt, char *buffer, int buf_size, int max_display_len) { - int len; + int len = 0; + int col_cur = 0; + int ch; + int offset = 0; + int eol; + int display_len; + char input_str[5]; + int str_len = 0; + wchar_t wcs[2]; + int wc_len; + char c; + + buffer[buf_size - 1] = '\0'; + offset = split_line(buffer, max_display_len, &eol, &display_len, 0); + buffer[offset] = '\0'; + len = offset; + col_cur = col + str_length(prompt, 1) + display_len; moveto(row, col); prints("%s", prompt); prints("%s", buffer); + prints("%*s", max_display_len - display_len, ""); + moveto(row, col_cur); iflush(); - len = _str_input(buffer, buf_size, echo_mode); + igetch_reset(); + + while (!SYS_server_exit) + { + ch = igetch_t(MIN(BBS_max_user_idle_time, 60)); + + if (ch == CR) + { + break; + } + else if (ch == KEY_TIMEOUT || ch == KEY_NULL) // timeout or broken pipe + { + return -1; + } + else if (ch == LF || ch == '\0') + { + continue; + } + else if (ch == BACKSPACE) + { + if (offset > 0) + { + str_len = 1; + offset--; + if (buffer[offset] < 0 || buffer[offset] > 127) // UTF8 + { + while (offset > 0 && (buffer[offset] & 0xc0) != 0xc0) + { + str_len++; + offset--; + } + + if (mbstowcs(wcs, buffer + offset, 1) == (size_t)-1) + { + log_error("mbstowcs() error\n"); + } + wc_len = (UTF8_fixed_width ? 2 : wcwidth(wcs[0])); + + if (wc_len == 2) + { + display_len--; + col_cur--; + } + } + + memmove(buffer + offset, buffer + offset + str_len, (size_t)(len - offset - str_len)); + len -= str_len; + buffer[len] = '\0'; + display_len--; + col_cur--; + + moveto(row, col_cur); + prints("%s", buffer + offset); + prints("%*s", max_display_len - display_len, ""); + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch == KEY_DEL) + { + if (offset < len) + { + if ((buffer[offset] & 0x80) == 0x80) // head of multi-byte character + { + str_len = 0; + c = (char)(buffer[offset] & 0xf0); + while (c & 0x80) + { + str_len++; + c = (c & 0x7f) << 1; + } + display_len--; + } + else + { + str_len = 1; + } + + memmove(buffer + offset, buffer + offset + str_len, (size_t)(len - offset - str_len)); + len -= str_len; + buffer[len] = '\0'; + display_len--; + + moveto(row, col_cur); + prints("%s", buffer + offset); + prints("%*s", max_display_len - display_len, ""); + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch == KEY_LEFT) + { + if (offset > 0) + { + str_len = 1; + offset--; + if (buffer[offset] < 0 || buffer[offset] > 127) // UTF8 + { + while (offset > 0 && (buffer[offset] & 0xc0) != 0xc0) + { + str_len++; + offset--; + } + + if (mbstowcs(wcs, buffer + offset, 1) == (size_t)-1) + { + log_error("mbstowcs() error\n"); + } + wc_len = (UTF8_fixed_width ? 2 : wcwidth(wcs[0])); + + if (wc_len == 2) + { + col_cur--; + } + } + col_cur--; + + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch == KEY_RIGHT) + { + if (offset < len) + { + str_len = 0; + if ((buffer[offset] & 0x80) == 0x80) // head of multi-byte character + { + c = (char)(buffer[offset] & 0xf0); + while (c & 0x80) + { + str_len++; + c = (c & 0x7f) << 1; + } + + if (mbstowcs(wcs, buffer + offset, 1) == (size_t)-1) + { + log_error("mbstowcs() error\n"); + } + wc_len = (UTF8_fixed_width ? 2 : wcwidth(wcs[0])); + + if (wc_len == 2) + { + col_cur++; + } + } + else + { + str_len = 1; + } + + col_cur++; + offset += str_len; + + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch == KEY_HOME || ch == KEY_UP) + { + if (offset > 0) + { + offset = 0; + col_cur = col + str_length(prompt, 1); + + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch == KEY_END || ch == KEY_DOWN) + { + if (offset < len) + { + offset = len; + col_cur = col + str_length(prompt, 1) + display_len; + + moveto(row, col_cur); + iflush(); + } + continue; + } + else if (ch > 255 || iscntrl(ch)) + { + continue; + } + else if ((ch & 0xff80) == 0x80) // head of multi-byte character + { + str_len = 0; + c = (char)(ch & 0xf0); + while (c & 0x80) + { + input_str[str_len] = (char)(ch - 256); + str_len++; + c = (c & 0x7f) << 1; + + if ((c & 0x80) == 0) // Input completed + { + break; + } + + // Expect additional bytes of input + ch = igetch(100); // 0.1 second + if (ch == KEY_NULL || ch == KEY_TIMEOUT) // Ignore received bytes if no futher input + { + log_debug("Ignore %d bytes of incomplete UTF8 character\n", str_len); + str_len = 0; + break; + } + } + input_str[str_len] = '\0'; + + if (str_len == 0) // Incomplete input + { + continue; + } + + if (mbstowcs(wcs, input_str, 1) == (size_t)-1) + { + log_error("mbstowcs() error\n"); + } + wc_len = (UTF8_fixed_width ? 2 : wcwidth(wcs[0])); + + if (len + str_len > buf_size - 1 || + display_len + wc_len > max_display_len) // No enough space for Chinese character + { + outc('\a'); + iflush(); + continue; + } + + memmove(buffer + offset + str_len, buffer + offset, (size_t)(len - offset)); + memcpy(buffer + offset, input_str, (size_t)str_len); + len += str_len; + buffer[len] = '\0'; + display_len += wc_len; + + moveto(row, col_cur); + prints("%s", buffer + offset); + prints("%*s", max_display_len - display_len, ""); + + col_cur += wc_len; + + moveto(row, col_cur); + iflush(); + + offset += str_len; + } + else if (ch >= 32 && ch < 127) // Printable character + { + if (len + 1 > buf_size - 1 || display_len + 1 > max_display_len) + { + outc('\a'); + iflush(); + continue; + } + + memmove(buffer + offset + 1, buffer + offset, (size_t)(len - offset)); + buffer[offset] = (char)ch; + len++; + buffer[len] = '\0'; + display_len++; + + moveto(row, col_cur); + prints("%s", buffer + offset); + prints("%*s", max_display_len - display_len, ""); + + col_cur++; + + moveto(row, col_cur); + iflush(); + + offset++; + } + else // Invalid character + { + continue; + } + } return len; } @@ -259,7 +638,7 @@ int display_data(const void *p_data, lon loop = 1; while (!SYS_server_exit && loop) { - if (eof_exit > 0 && line_current >= display_line_total && display_line_total <= screen_row_total) + if (eof_exit > 0 && line_current >= display_line_total) { if (eof_exit == 1) { @@ -293,7 +672,7 @@ int display_data(const void *p_data, lon ctx.line_bottom = MIN(line_current - (output_current_row - screen_begin_row) + screen_row_total, display_line_total); snprintf(buffer, sizeof(buffer), - "\033[1;44;33m\033[32m%ld\033[33m-\033[32m%ld\033[33m (\033[32m%ld%%\033[33m) %s", + "\033[1;44;33m第\033[32m%ld\033[33m-\033[32m%ld\033[33m行 (\033[32m%ld%%\033[33m) %s", ctx.line_top, ctx.line_bottom, percentile, @@ -314,9 +693,20 @@ int display_data(const void *p_data, lon input_ok = 0; while (!SYS_server_exit && !input_ok) { - ch = igetch_t(MAX_DELAY_TIME); + ch = igetch_t(BBS_max_user_idle_time); input_ok = 1; + if (ch != KEY_NULL && ch != KEY_TIMEOUT) + { + BBS_last_access_tm = time(NULL); + + // Refresh current action + if (user_online_update(NULL) < 0) + { + log_error("user_online_update(NULL) error\n"); + } + } + // extended key handler if (key_handler(&ch, &ctx) != 0) { @@ -326,7 +716,10 @@ int display_data(const void *p_data, lon switch (ch) { case KEY_NULL: + log_debug("KEY_NULL\n"); + goto cleanup; case KEY_TIMEOUT: + log_debug("User input timeout\n"); goto cleanup; case KEY_HOME: if (line_current - output_current_row < 0) // Reach begin @@ -360,8 +753,6 @@ int display_data(const void *p_data, lon output_end_row = SCREEN_ROWS - 1; // Legacy Fterm only works with this line break; case CR: - igetch_reset(); - case KEY_SPACE: case KEY_DOWN: if (line_current + (screen_row_total - (output_current_row - screen_begin_row)) >= display_line_total) // Reach end { @@ -372,7 +763,8 @@ int display_data(const void *p_data, lon output_end_row = SCREEN_ROWS - 1; moveto(SCREEN_ROWS, 0); clrtoeol(); - prints("\033[S"); // Scroll up 1 line + // prints("\033[S"); // Scroll up 1 line + prints("\n"); // Legacy Cterm only works with this line break; case KEY_PGUP: if (line_current - output_current_row < 0) // Reach begin @@ -388,6 +780,7 @@ int display_data(const void *p_data, lon output_end_row = SCREEN_ROWS - 1; clrline(output_current_row, SCREEN_ROWS); break; + case KEY_SPACE: case KEY_PGDN: if (line_current + screen_row_total - (output_current_row - screen_begin_row) >= display_line_total) // Reach end { @@ -428,8 +821,6 @@ int display_data(const void *p_data, lon input_ok = 0; break; } - - BBS_last_access_tm = time(NULL); } continue; @@ -463,15 +854,15 @@ cleanup: return ch; } -static int display_file_key_handler(int *p_key, DISPLAY_CTX *p_ctx) +int display_file_key_handler(int *p_key, DISPLAY_CTX *p_ctx) { switch (*p_key) { case 0: // Set msg snprintf(p_ctx->msg, sizeof(p_ctx->msg), - "| [\033[32m\033[33m,\033[32mESC\033[33m] | " - "ƶ[\033[32m\033[33m/\033[32m\033[33m/\033[32mPgUp\033[33m/\033[32mPgDn\033[33m] | " - "[\033[32mh\033[33m] |"); + "| 返回[\033[32m←\033[33m,\033[32mESC\033[33m] | " + "移动[\033[32m↑\033[33m/\033[32m↓\033[33m/\033[32mPgUp\033[33m/\033[32mPgDn\033[33m] | " + "帮助[\033[32mh\033[33m] |"); break; } @@ -481,7 +872,7 @@ static int display_file_key_handler(int int display_file(const char *filename, int eof_exit) { int ret; - const void *p_shm; + void *p_shm; size_t data_len; long line_total; const void *p_data; @@ -493,6 +884,11 @@ int display_file(const char *filename, i return KEY_NULL; } + if (user_online_update("VIEW_FILE") < 0) + { + log_error("user_online_update(VIEW_FILE) error\n"); + } + ret = display_data(p_data, line_total, p_line_offsets, eof_exit, display_file_key_handler, DATA_READ_HELP); if (detach_file_shm(p_shm) < 0) @@ -516,23 +912,24 @@ int show_top(const char *str_left, const strncpy(str_left_f, str_left, sizeof(str_left_f) - 1); str_left_f[sizeof(str_left_f) - 1] = '\0'; - len = split_line(str_left_f, STR_TOP_LEFT_MAX_LEN, &eol, &str_left_len, 1); + len = split_line(str_left_f, STR_TOP_LEFT_MAX_LEN / 2, &eol, &str_left_len, 1); str_left_f[len] = '\0'; strncpy(str_middle_f, str_middle, sizeof(str_middle_f) - 1); str_middle_f[sizeof(str_middle_f) - 1] = '\0'; - len = split_line(str_middle, STR_TOP_MIDDLE_MAX_LEN, &eol, &str_middle_len, 1); + len = split_line(str_middle, STR_TOP_MIDDLE_MAX_LEN / 2, &eol, &str_middle_len, 1); str_middle_f[len] = '\0'; strncpy(str_right_f, str_right, sizeof(str_right_f) - 1); str_right_f[sizeof(str_right_f) - 1] = '\0'; - len = split_line(str_right, STR_TOP_RIGHT_MAX_LEN, &eol, &str_right_len, 1); + len = split_line(str_right, STR_TOP_RIGHT_MAX_LEN / 2, &eol, &str_right_len, 1); str_right_f[len] = '\0'; moveto(1, 0); clrtoeol(); - prints("\033[1;44;33m%s\033[37m%*s\033[33m%*s\033[m", - str_left_f, 44 - str_left_len, str_middle_f, 36, str_right_f); + prints("\033[1;44;33m%s\033[37m%*s%s\033[33m%*s%s\033[m", + str_left_f, 44 - str_left_len - str_middle_len, "", + str_middle_f, 36 - str_right_len, "", str_right_f); return 0; } @@ -540,24 +937,26 @@ int show_top(const char *str_left, const int show_bottom(const char *msg) { char str_time[LINE_BUFFER_LEN]; + int len_str_time; time_t time_online; struct tm *tm_online; char msg_f[LINE_BUFFER_LEN]; int eol; - int msg_len; + int len_msg; int len; int len_username; char str_tm_online[LINE_BUFFER_LEN]; + int len_str_tm_online; - get_time_str(str_time, sizeof(str_time)); + len_str_time = (int)get_time_str(str_time, sizeof(str_time)); msg_f[0] = '\0'; - msg_len = 0; + len_msg = 0; if (msg != NULL) { strncpy(msg_f, msg, sizeof(msg_f) - 1); msg_f[sizeof(msg_f) - 1] = '\0'; - len = split_line(msg_f, 23, &eol, &msg_len, 1); + len = split_line(msg_f, 23, &eol, &len_msg, 1); msg_f[len] = '\0'; } @@ -568,20 +967,22 @@ int show_bottom(const char *msg) if (tm_online->tm_mday > 1) { snprintf(str_tm_online, sizeof(str_tm_online), - "\033[36m%2d\033[33m\033[36m%2d\033[33mʱ", - tm_online->tm_mday - 1, tm_online->tm_hour); + "\033[36m%d\033[33md \033[36m%d\033[33m:\033[36m%.2d", + tm_online->tm_mday - 1, tm_online->tm_hour, tm_online->tm_min); } else { snprintf(str_tm_online, sizeof(str_tm_online), - "\033[36m%2d\033[33mʱ\033[36m%2d\033[33m", + "\033[36m%d\033[33m:\033[36m%.2d", tm_online->tm_hour, tm_online->tm_min); } + len_str_tm_online = str_length(str_tm_online, 1); moveto(SCREEN_ROWS, 0); clrtoeol(); - prints("\033[1;44;33mʱ[\033[36m%s\033[33m]%s%*s \033[33mʺ[\033[36m%s\033[33m][%s\033[33m]\033[m", - str_time, msg_f, 38 - msg_len - len_username, "", BBS_username, str_tm_online); + prints("\033[1;44;33m时间[\033[36m%s\033[33m]%s%*s \033[33m用户[\033[36m%s\033[33m][%s\033[33m]\033[m", + str_time, msg_f, 65 - len_str_time - len_msg - len_username - len_str_tm_online, + "", BBS_username, str_tm_online); return 0; }