/* * Copyright (C) 1984-2024 Mark Nudelman * * You may distribute under the terms of either the GNU General Public * License or the Less License, as specified in the README file. * * For more information, see the README file. */ /* * High level routines dealing with the output to the screen. */ #include "less.h" #if MSDOS_COMPILER==WIN32C #include "windows.h" #ifndef COMMON_LVB_UNDERSCORE #define COMMON_LVB_UNDERSCORE 0x8000 #endif #endif public int errmsgs; /* Count of messages displayed by error() */ public int need_clr; public int final_attr; public int at_prompt; extern int sigs; extern int sc_width; extern int so_s_width, so_e_width; extern int is_tty; extern int oldbot; extern int utf_mode; extern char intr_char; #if MSDOS_COMPILER==WIN32C || MSDOS_COMPILER==BORLANDC || MSDOS_COMPILER==DJGPPC extern int ctldisp; extern int nm_fg_color, nm_bg_color; extern int bo_fg_color, bo_bg_color; extern int ul_fg_color, ul_bg_color; extern int so_fg_color, so_bg_color; extern int bl_fg_color, bl_bg_color; extern int sgr_mode; #if MSDOS_COMPILER==WIN32C extern int vt_enabled; #endif #endif /* * Display the line which is in the line buffer. */ public void put_line(void) { int c; size_t i; int a; if (ABORT_SIGS()) { /* * Don't output if a signal is pending. */ screen_trashed(); return; } final_attr = AT_NORMAL; for (i = 0; (c = gline(i, &a)) != '\0'; i++) { at_switch(a); final_attr = a; if (c == '\b') putbs(); else putchr(c); } at_exit(); } /* * win_flush has at least one non-critical issue when an escape sequence * begins at the last char of the buffer, and possibly more issues. * as a temporary measure to reduce likelyhood of encountering end-of-buffer * issues till the SGR parser is replaced, OUTBUF_SIZE is 8K on Windows. */ static char obuf[OUTBUF_SIZE]; static char *ob = obuf; static int outfd = 2; /* stderr */ #if MSDOS_COMPILER==WIN32C || MSDOS_COMPILER==BORLANDC || MSDOS_COMPILER==DJGPPC typedef unsigned t_attr; #define A_BOLD (1u<<0) #define A_ITALIC (1u<<1) #define A_UNDERLINE (1u<<2) #define A_BLINK (1u<<3) #define A_INVERSE (1u<<4) #define A_CONCEAL (1u<<5) /* long is guaranteed 32 bits, and we reserve bits for type + RGB */ typedef unsigned long t_color; #define T_DEFAULT 0ul #define T_ANSI 1ul /* colors 0-7 */ #define CGET_ANSI(c) ((c) & 0x7) #define C_DEFAULT (T_DEFAULT <<24) /* 0 */ #define C_ANSI(c) ((T_ANSI <<24) | (c)) /* attr/fg/bg/all 0 is the default attr/fg/bg/all, respectively */ typedef struct t_sgr { t_attr attr; t_color fg; t_color bg; } t_sgr; static constant t_sgr SGR_DEFAULT; /* = {0} */ /* returns 0 on success, non-0 on unknown SGR code */ static int update_sgr(t_sgr *sgr, long code) { switch (code) { case 0: *sgr = SGR_DEFAULT; break; case 1: sgr->attr |= A_BOLD; break; case 22: sgr->attr &= ~A_BOLD; break; case 3: sgr->attr |= A_ITALIC; break; case 23: sgr->attr &= ~A_ITALIC; break; case 4: sgr->attr |= A_UNDERLINE; break; case 24: sgr->attr &= ~A_UNDERLINE; break; case 6: /* fast-blink, fallthrough */ case 5: sgr->attr |= A_BLINK; break; case 25: sgr->attr &= ~A_BLINK; break; case 7: sgr->attr |= A_INVERSE; break; case 27: sgr->attr &= ~A_INVERSE; break; case 8: sgr->attr |= A_CONCEAL; break; case 28: sgr->attr &= ~A_CONCEAL; break; case 39: sgr->fg = C_DEFAULT; break; case 49: sgr->bg = C_DEFAULT; break; case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: sgr->fg = C_ANSI(code - 30); break; case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: sgr->bg = C_ANSI(code - 40); break; default: return 1; } return 0; } static void set_win_colors(t_sgr *sgr) { #if MSDOS_COMPILER==WIN32C /* Screen colors used by 3x and 4x SGR commands. */ static unsigned char screen_color[] = { 0, /* BLACK */ FOREGROUND_RED, FOREGROUND_GREEN, FOREGROUND_RED|FOREGROUND_GREEN, FOREGROUND_BLUE, FOREGROUND_BLUE|FOREGROUND_RED, FOREGROUND_BLUE|FOREGROUND_GREEN, FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED }; #else static enum COLORS screen_color[] = { BLACK, RED, GREEN, BROWN, BLUE, MAGENTA, CYAN, LIGHTGRAY }; #endif int fg, bg, tmp; /* Windows colors */ /* Not "SGR mode": apply -D to default fg+bg with one attribute */ if (!sgr_mode && sgr->fg == C_DEFAULT && sgr->bg == C_DEFAULT) { switch (sgr->attr) { case A_BOLD: WIN32setcolors(bo_fg_color, bo_bg_color); return; case A_UNDERLINE: WIN32setcolors(ul_fg_color, ul_bg_color); return; case A_BLINK: WIN32setcolors(bl_fg_color, bl_bg_color); return; case A_INVERSE: WIN32setcolors(so_fg_color, so_bg_color); return; /* * There's no -Di so italic should not be here, but to * preserve legacy behavior, apply -Ds to italic too. */ case A_ITALIC: WIN32setcolors(so_fg_color, so_bg_color); return; } } /* generic application of the SGR state as Windows colors */ fg = sgr->fg == C_DEFAULT ? nm_fg_color : screen_color[CGET_ANSI(sgr->fg)]; bg = sgr->bg == C_DEFAULT ? nm_bg_color : screen_color[CGET_ANSI(sgr->bg)]; if (sgr->attr & A_BOLD) fg |= 8; if (sgr->attr & (A_BLINK | A_UNDERLINE)) bg |= 8; /* TODO: can be illegible */ if (sgr->attr & (A_INVERSE | A_ITALIC)) { tmp = fg; fg = bg; bg = tmp; } if (sgr->attr & A_CONCEAL) fg = bg ^ 8; WIN32setcolors(fg, bg); } /* like is_ansi_end, but doesn't assume c != 0 (returns 0 for c == 0) */ static int is_ansi_end_0(char c) { return c && is_ansi_end((unsigned char)c); } static void win_flush(void) { if (ctldisp != OPT_ONPLUS #if MSDOS_COMPILER==WIN32C || (vt_enabled && sgr_mode) #endif ) WIN32textout(obuf, ptr_diff(ob, obuf)); else { /* * Digest text, apply embedded SGR sequences as Windows-colors. * By default - when -Da ("SGR mode") is unset - also apply * translation of -D command-line options (at set_win_colors) */ char *anchor, *p, *p_next; static t_sgr sgr; /* when unsupported SGR value is encountered, like 38/48 for * 256/true colors, then we abort processing this sequence, * because it may expect followup values, but we don't know * how many, so we've lost sync of this sequence parsing. * Without VT enabled it's OK because we can't do much anyway, * but with VT enabled we choose to passthrough this sequence * to the terminal - which can handle it better than us. * however, this means that our "sgr" var is no longer in sync * with the actual terminal state, which can lead to broken * colors with future sequences which we _can_ fully parse. * in such case, once it happens, we keep passthrough sequences * until we know we're in sync again - on a valid reset. */ static int sgr_bad_sync; for (anchor = p_next = obuf; (p_next = memchr(p_next, ESC, ob - p_next)) != NULL; ) { p = p_next; if (p[1] == '[') /* "ESC-[" sequence */ { /* * unknown SGR code ignores the rest of the seq, * and allows ignoring sequences such as * ^[[38;5;123m or ^[[38;2;5;6;7m * (prior known codes at the same seq do apply) */ int bad_code = 0; if (p > anchor) { /* * If some chars seen since * the last escape sequence, * write them out to the screen. */ WIN32textout(anchor, ptr_diff(p, anchor)); anchor = p; } p += 2; /* Skip the "ESC-[" */ if (is_ansi_end_0(*p)) { /* * Handle null escape sequence * "ESC[m" as if it was "ESC[0m" */ p++; anchor = p_next = p; update_sgr(&sgr, 0); set_win_colors(&sgr); sgr_bad_sync = 0; continue; } p_next = p; /* * Parse and apply SGR values to the SGR state * based on the escape sequence. */ while (!is_ansi_end_0(*p)) { char *q; long code = strtol(p, &q, 10); if (*q == '\0') { /* * Incomplete sequence. * Leave it unprocessed * in the buffer. */ size_t slop = ptr_diff(q, anchor); memmove(obuf, anchor, slop); ob = &obuf[slop]; return; } if (q == p || (!is_ansi_end_0(*q) && *q != ';')) { /* * can't parse. passthrough * till the end of the buffer */ p_next = q; break; } if (*q == ';') q++; if (!bad_code) bad_code = update_sgr(&sgr, code); if (bad_code) sgr_bad_sync = 1; else if (code == 0) sgr_bad_sync = 0; p = q; } if (!is_ansi_end_0(*p) || p == p_next) break; if (sgr_bad_sync && vt_enabled) { /* this or a prior sequence had unknown * SGR value. passthrough all sequences * until we're in-sync again */ WIN32textout(anchor, ptr_diff(p+1, anchor)); } else { set_win_colors(&sgr); } p_next = anchor = p + 1; } else p_next++; } /* Output what's left in the buffer. */ WIN32textout(anchor, ptr_diff(ob, anchor)); } ob = obuf; } #endif /* * Flush buffered output. * * If we haven't displayed any file data yet, * output messages on error output (file descriptor 2), * otherwise output on standard output (file descriptor 1). * * This has the desirable effect of producing all * error messages on error output if standard output * is directed to a file. It also does the same if * we never produce any real output; for example, if * the input file(s) cannot be opened. If we do * eventually produce output, code in edit() makes * sure these messages can be seen before they are * overwritten or scrolled away. */ public void flush(void) { size_t n; n = ptr_diff(ob, obuf); if (n == 0) return; ob = obuf; #if MSDOS_COMPILER==MSOFTC if (interactive()) { obuf[n] = '\0'; _outtext(obuf); return; } #else #if MSDOS_COMPILER==WIN32C || MSDOS_COMPILER==BORLANDC || MSDOS_COMPILER==DJGPPC if (interactive()) { ob = obuf + n; *ob = '\0'; win_flush(); return; } #endif #endif if (write(outfd, obuf, n) != n) screen_trashed(); } /* * Set the output file descriptor (1=stdout or 2=stderr). */ public void set_output(int fd) { flush(); outfd = fd; } /* * Output a character. * ch is int for compatibility with tputs. */ public int putchr(int ch) { char c = (char) ch; #if 0 /* fake UTF-8 output for testing */ extern int utf_mode; if (utf_mode) { static char ubuf[MAX_UTF_CHAR_LEN]; static int ubuf_len = 0; static int ubuf_index = 0; if (ubuf_len == 0) { ubuf_len = utf_len(c); ubuf_index = 0; } ubuf[ubuf_index++] = c; if (ubuf_index < ubuf_len) return c; c = get_wchar(ubuf) & 0xFF; ubuf_len = 0; } #endif clear_bot_if_needed(); #if MSDOS_COMPILER if (c == '\n' && is_tty) { /* remove_top(1); */ putchr('\r'); } #else #ifdef _OSK if (c == '\n' && is_tty) /* In OS-9, '\n' == 0x0D */ putchr(0x0A); #endif #endif /* * Some versions of flush() write to *ob, so we must flush * when we are still one char from the end of obuf. */ if (ob >= &obuf[sizeof(obuf)-1]) flush(); *ob++ = c; at_prompt = 0; return (c); } public void clear_bot_if_needed(void) { if (!need_clr) return; need_clr = 0; clear_bot(); } /* * Output a string. */ public void putstr(constant char *s) { while (*s != '\0') putchr(*s++); } /* * Convert an integral type to a string. */ #define TYPE_TO_A_FUNC(funcname, type) \ void funcname(type num, char *buf, int radix) \ { \ int neg = (num < 0); \ char tbuf[INT_STRLEN_BOUND(num)+2]; \ char *s = tbuf + sizeof(tbuf); \ if (neg) num = -num; \ *--s = '\0'; \ do { \ *--s = "0123456789ABCDEF"[num % radix]; \ } while ((num /= radix) != 0); \ if (neg) *--s = '-'; \ strcpy(buf, s); \ } TYPE_TO_A_FUNC(postoa, POSITION) TYPE_TO_A_FUNC(linenumtoa, LINENUM) TYPE_TO_A_FUNC(inttoa, int) /* * Convert a string to an integral type. Return ((type) -1) on overflow. */ #define STR_TO_TYPE_FUNC(funcname, cfuncname, type) \ type cfuncname(constant char *buf, constant char **ebuf, int radix) \ { \ type val = 0; \ lbool v = 0; \ for (;; buf++) { \ char c = *buf; \ int digit = (c >= '0' && c <= '9') ? c - '0' : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; \ if (digit < 0 || digit >= radix) break; \ v = v || ckd_mul(&val, val, radix); \ v = v || ckd_add(&val, val, digit); \ } \ if (ebuf != NULL) *ebuf = buf; \ return v ? (type)(-1) : val; \ } \ type funcname(char *buf, char **ebuf, int radix) \ { \ constant char *cbuf = buf; \ type r = cfuncname(cbuf, &cbuf, radix); \ if (ebuf != NULL) *ebuf = (char *) cbuf; /*{{const-issue}}*/ \ return r; \ } STR_TO_TYPE_FUNC(lstrtopos, lstrtoposc, POSITION) STR_TO_TYPE_FUNC(lstrtoi, lstrtoic, int) STR_TO_TYPE_FUNC(lstrtoul, lstrtoulc, unsigned long) /* * Print an integral type. */ #define IPRINT_FUNC(funcname, type, typetoa) \ static int funcname(type num, int radix) \ { \ char buf[INT_STRLEN_BOUND(num)]; \ typetoa(num, buf, radix); \ putstr(buf); \ return (int) strlen(buf); \ } IPRINT_FUNC(iprint_int, int, inttoa) IPRINT_FUNC(iprint_linenum, LINENUM, linenumtoa) /* * This function implements printf-like functionality * using a more portable argument list mechanism than printf's. * * {{ This paranoia about the portability of printf dates from experiences * with systems in the 1980s and is of course no longer necessary. }} */ public int less_printf(constant char *fmt, PARG *parg) { constant char *s; constant char *es; int col; col = 0; while (*fmt != '\0') { if (*fmt != '%') { putchr(*fmt++); col++; } else { ++fmt; switch (*fmt++) { case 's': s = parg->p_string; es = s + strlen(s); parg++; while (*s != '\0') { LWCHAR ch = step_charc(&s, +1, es); constant char *ps = utf_mode ? prutfchar(ch) : prchar(ch); while (*ps != '\0') { putchr(*ps++); col++; } } break; case 'd': col += iprint_int(parg->p_int, 10); parg++; break; case 'x': col += iprint_int(parg->p_int, 16); parg++; break; case 'n': col += iprint_linenum(parg->p_linenum, 10); parg++; break; case 'c': s = prchar((LWCHAR) parg->p_char); parg++; while (*s != '\0') { putchr(*s++); col++; } break; case '%': putchr('%'); break; } } } return (col); } /* * Get a RETURN. * If some other non-trivial char is pressed, unget it, so it will * become the next command. */ public void get_return(void) { int c; #if ONLY_RETURN while ((c = getchr()) != '\n' && c != '\r') bell(); #else c = getchr(); if (c != '\n' && c != '\r' && c != ' ' && c != READ_INTR) ungetcc((char) c); #endif } /* * Output a message in the lower left corner of the screen * and wait for carriage return. */ public void error(constant char *fmt, PARG *parg) { int col = 0; static char return_to_continue[] = " (press RETURN)"; errmsgs++; if (!interactive()) { less_printf(fmt, parg); putchr('\n'); return; } if (!oldbot) squish_check(); at_exit(); clear_bot(); at_enter(AT_STANDOUT|AT_COLOR_ERROR); col += so_s_width; col += less_printf(fmt, parg); putstr(return_to_continue); at_exit(); col += (int) sizeof(return_to_continue) + so_e_width; get_return(); lower_left(); clear_eol(); if (col >= sc_width) /* * Printing the message has probably scrolled the screen. * {{ Unless the terminal doesn't have auto margins, * in which case we just hammered on the right margin. }} */ screen_trashed(); flush(); } /* * Output a message in the lower left corner of the screen * and don't wait for carriage return. * Usually used to warn that we are beginning a potentially * time-consuming operation. */ static void ierror_suffix(constant char *fmt, PARG *parg, constant char *suffix1, constant char *suffix2, constant char *suffix3) { at_exit(); clear_bot(); at_enter(AT_STANDOUT|AT_COLOR_ERROR); (void) less_printf(fmt, parg); putstr(suffix1); putstr(suffix2); putstr(suffix3); at_exit(); flush(); need_clr = 1; } public void ierror(constant char *fmt, PARG *parg) { ierror_suffix(fmt, parg, "... (interrupt to abort)", "", ""); } public void ixerror(constant char *fmt, PARG *parg) { if (!supports_ctrl_x()) ierror(fmt, parg); else { char ichar[MAX_PRCHAR_LEN+1]; strcpy(ichar, prchar((LWCHAR) intr_char)); ierror_suffix(fmt, parg, "... (", ichar, " or interrupt to abort)"); } } /* * Output a message in the lower left corner of the screen * and return a single-character response. */ public int query(constant char *fmt, PARG *parg) { int c; int col = 0; if (interactive()) clear_bot(); (void) less_printf(fmt, parg); c = getchr(); if (interactive()) { lower_left(); if (col >= sc_width) screen_trashed(); flush(); } else { putchr('\n'); } if (c == 'Q') quit(QUIT_OK); return (c); }