diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -3473,6 +3473,7 @@ dev/vt/hw/vga/vt_vga.c optional vt vt_vga dev/vt/logo/logo_freebsd.c optional vt splash dev/vt/logo/logo_beastie.c optional vt splash +dev/vt/vt_screen_reader.c optional vt dev/vt/vt_buf.c optional vt dev/vt/vt_consolectl.c optional vt dev/vt/vt_core.c optional vt diff --git a/sys/dev/vt/vt.h b/sys/dev/vt/vt.h --- a/sys/dev/vt/vt.h +++ b/sys/dev/vt/vt.h @@ -85,6 +85,8 @@ #endif #define ISSIGVALID(sig) ((sig) > 0 && (sig) < NSIG) +SYSCTL_DECL(_kern_vt); + #define VT_SYSCTL_INT(_name, _default, _descr) \ int vt_##_name = (_default); \ SYSCTL_INT(_kern_vt, OID_AUTO, _name, CTLFLAG_RWTUN, &vt_##_name, 0, _descr) @@ -169,6 +171,8 @@ term_color_t *vd_drawnbg; /* (?) Most recent bg color drawn. */ }; +extern struct vt_device *main_vd; + #define VD_PASTEBUF(vd) ((vd)->vd_pastebuf.vpb_buf) #define VD_PASTEBUFSZ(vd) ((vd)->vd_pastebuf.vpb_bufsz) #define VD_PASTEBUFLEN(vd) ((vd)->vd_pastebuf.vpb_len) @@ -241,6 +245,7 @@ int vtbuf_get_marked_len(struct vt_buf *vb); void vtbuf_extract_marked(struct vt_buf *vb, term_char_t *buf, int sz, int mark); #endif +int vtbuf_extract_screen_reader(char *buf, size_t *, term_pos_t *, size_t lines, size_t bufsz); #define VTB_MARK_NONE 0 #define VTB_MARK_END 1 @@ -450,4 +455,7 @@ void vtterm_draw_cpu_logos(struct vt_device *); +/* Prototypes for screen reader layer. */ +void vt_screen_reader_hup(void); + #endif /* !_DEV_VT_VT_H_ */ diff --git a/sys/dev/vt/vt_buf.c b/sys/dev/vt/vt_buf.c --- a/sys/dev/vt/vt_buf.c +++ b/sys/dev/vt/vt_buf.c @@ -746,6 +746,7 @@ return (sz * sizeof(term_char_t)); } +#endif static bool tchar_is_word_separator(term_char_t ch) @@ -770,6 +771,204 @@ } } +static bool +vtbuf_extract_utf8(term_char_t ch, char *buffer, size_t *ppos, size_t size) +{ + char temp[4]; + size_t len; + + ch = TCHAR_CHARACTER(ch); + + /* Convert to UTF-8. */ + + if (ch < 0x80) { + temp[0] = ch; + len = 1; + } else if (ch < 0x800) { + temp[0] = 0xc0 | (ch >> 6); + temp[1] = 0x80 | (ch & 0x3f); + len = 2; + } else if (ch < 0x10000) { + temp[0] = 0xe0 | (ch >> 12); + temp[1] = 0x80 | ((ch >> 6) & 0x3f); + temp[2] = 0x80 | (ch & 0x3f); + len = 3; + } else { + temp[0] = 0xf0 | (ch >> 18); + temp[1] = 0x80 | ((ch >> 12) & 0x3f); + temp[2] = 0x80 | ((ch >> 6) & 0x3f); + temp[3] = 0x80 | (ch & 0x3f); + len = 4; + } + + /* + * Make sure everything fits into the buffer. + */ + if (*ppos + len > size) + return (false); + + memcpy(buffer + *ppos, temp, len); + *ppos += len; + return (true); +} + +static const char vt_access_overflow[] = { "Line is not accessible" }; + +int +vtbuf_extract_screen_reader(char *buffer, size_t *ppos, term_pos_t *cursor, + size_t lines, size_t bufsz) +{ + struct vt_device *vd; + struct vt_window *vw; + struct vt_buf *vb; + term_pos_t max; + term_char_t ch; + size_t lines_old; + size_t last_sep_off; + size_t last_line_off; + int last_sep_col; + int c; + int i; + int r; + + MPASS(bufsz != 0); + MPASS(lines != 0); + + vd = main_vd; + if (vd == NULL) + return (EINVAL); + vw = vd->vd_curwindow; + if (vw == NULL) + return (EINVAL); + vb = &vw->vw_buf; + max = vb->vb_scr_size; + lines_old = lines; + + /* set default cursor */ + cursor->tp_row = 0; + cursor->tp_col = 0; + + VTBUF_LOCK(vb); + for (i = 0; lines != 0 && i != max.tp_row; i++) { + /* Get real row number in VT buffer. */ + r = (vb->vb_roffset + i) % vb->vb_history_size; + + /* Find first character, get whole word, if any. */ + for (c = 0; c != max.tp_col; c++) { + ch = vb->vb_rows[r][c]; + + if (TCHAR_SCREEN_READER(ch) != 0 || + TCHAR_CHARACTER(ch) == 0) + continue; + if (tchar_is_word_separator(ch)) + break; + /* Go back a bit to get context, if any. */ + while (c != 0) { + c--; + ch = vb->vb_rows[r][c]; + if (TCHAR_CHARACTER(ch) == 0) + continue; + if (tchar_is_word_separator(ch)) + break; + } + break; + } + + /* Skip initial word separators, if any. */ + for (; c != max.tp_col; c++) { + ch = vb->vb_rows[r][c]; + + if (TCHAR_CHARACTER(ch) == 0 || + tchar_is_word_separator(ch)) + continue; + break; + } + + if (c == max.tp_col) + continue; + + /* Check for first line and store screen position. */ + if (lines == lines_old) { + cursor->tp_row = i; + cursor->tp_col = c; + } + + lines--; + last_sep_col = c; + last_sep_off = *ppos; + last_line_off = *ppos; + + /* Try to output line. */ + for( ; c != max.tp_col; c++) { + ch = vb->vb_rows[r][c]; + + if (TCHAR_CHARACTER(ch) == 0) + continue; + + if (tchar_is_word_separator(ch)) { + if (vtbuf_extract_utf8(ch, buffer, ppos, bufsz) == false) + break; + /* Keep track of sensible stop locations. */ + last_sep_col = c + 1; + last_sep_off = *ppos; + } else { + if (vtbuf_extract_utf8(ch, buffer, ppos, bufsz) == false) + break; + } + } + + /* Check if buffer is full (whole line was not consumed). */ + if (c != max.tp_col) { + /* Check if this is the first line. */ + if (last_line_off == 0) { + /* XXX Pretend line was consumed. */ + *ppos = last_line_off; + c = max.tp_col; + + for (size_t i = 0; vt_access_overflow[i]; i++) { + if (vtbuf_extract_utf8(vt_access_overflow[i], + buffer, ppos, bufsz) == false) + break; + } + } else { + /* Stop at last separator, if any. */ + *ppos = last_sep_off; + c = last_sep_col; + } + + /* Don't output this part again. */ + while (c--) + vb->vb_rows[r][c] |= TSCREEN_READER; + break; + } else { + /* Don't output this line again. */ + while (c--) + vb->vb_rows[r][c] |= TSCREEN_READER; + + /* Remove trailing white space. */ + while (*ppos != last_line_off && buffer[*ppos - 1] == ' ') + (*ppos)--; + + if (*ppos == last_line_off) { + /* Grab another line, this one is empty. */ + lines++; + } else { + /* Insert a word separator, just in case. */ + if (vtbuf_extract_utf8(' ', buffer, ppos, bufsz) == false) + break; + } + } + } + VTBUF_UNLOCK(vb); + + /* Remove trailing white space from last line. */ + while (*ppos != 0 && buffer[*ppos - 1] == ' ') + (*ppos)--; + + return (lines != lines_old ? 0 : ENOENT); +} + +#ifndef SC_NO_CUTPASTE void vtbuf_extract_marked(struct vt_buf *vb, term_char_t *buf, int sz, int mark) { diff --git a/sys/dev/vt/vt_core.c b/sys/dev/vt/vt_core.c --- a/sys/dev/vt/vt_core.c +++ b/sys/dev/vt/vt_core.c @@ -126,7 +126,7 @@ #define VT_UNIT(vw) ((vw)->vw_device->vd_unit * VT_MAXWINDOWS + \ (vw)->vw_number) -static SYSCTL_NODE(_kern, OID_AUTO, vt, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, +SYSCTL_NODE(_kern, OID_AUTO, vt, CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "vt(9) parameters"); static VT_SYSCTL_INT(enable_altgr, 1, "Enable AltGr key (Do not assume R.Alt as Alt)"); static VT_SYSCTL_INT(enable_bell, 0, "Enable bell"); @@ -1109,6 +1109,9 @@ struct vt_window *vw = tm->tm_softc; struct vt_device *vd = vw->vw_device; + /* piggyback screen reader hangup on the bell event */ + vt_screen_reader_hup(); + vtterm_devctl(vt_enable_bell, vd->vd_flags & VDF_QUIET_BELL, vw->vw_bell_pitch, vw->vw_bell_duration); diff --git a/sys/dev/vt/vt_screen_reader.c b/sys/dev/vt/vt_screen_reader.c new file mode 100644 --- /dev/null +++ b/sys/dev/vt/vt_screen_reader.c @@ -0,0 +1,170 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2022 Hans Petter Selasky + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define VT_ACCESS_UTF8_DATA_MAX 1000 /* bytes */ + +static MALLOC_DEFINE(M_VTACCESS, "vt_screen_reader", "VT screen reader"); + +static SYSCTL_NODE(_kern_vt, OID_AUTO, screen_reader, + CTLFLAG_RW | CTLFLAG_MPSAFE, 0, "Screen reader parameters"); + +static int vt_screen_reader_text_utf8(SYSCTL_HANDLER_ARGS); +static int vt_screen_reader_feed(SYSCTL_HANDLER_ARGS); + +static term_pos_t vt_screen_reader_cursor; + +static char *vt_screen_reader_utf8_data; +static size_t vt_screen_reader_utf8_length; + +static struct sx vt_screen_reader_sx; + +static void vt_screen_reader_hup_callback(void *, int); + +static struct task vt_screen_reader_hup_task = + TASK_INITIALIZER(0, &vt_screen_reader_hup_callback, NULL); + +SX_SYSINIT(vt_screen_reader_sx, &vt_screen_reader_sx, "VT screen reader"); + +#define VT_ACCESS_LOCK() sx_xlock(&vt_screen_reader_sx) +#define VT_ACCESS_UNLOCK() sx_xunlock(&vt_screen_reader_sx) + +static unsigned vt_screen_reader_lines = 1; /* default value */ +static unsigned vt_screen_reader_hup_pid; /* send HUP signal to this PID, if non-zero */ + +SYSCTL_UINT(_kern_vt_screen_reader, OID_AUTO, lines, CTLFLAG_RWTUN | CTLFLAG_MPSAFE, + &vt_screen_reader_lines, 0, "Number of lines the VT screen reader module will feed, between 1 and 1000"); + +SYSCTL_UINT(_kern_vt_screen_reader, OID_AUTO, hangup_pid, CTLFLAG_RW | CTLFLAG_MPSAFE, + &vt_screen_reader_hup_pid, 0, "Send hangup signal to this PID, if non-zero"); + +SYSCTL_U16(_kern_vt_screen_reader, OID_AUTO, row, CTLFLAG_RD | CTLFLAG_MPSAFE, + &vt_screen_reader_cursor.tp_row, 0, "Current VT screen reader row number, between 0 and 65535 inclusivly"); + +SYSCTL_U16(_kern_vt_screen_reader, OID_AUTO, col, CTLFLAG_RD | CTLFLAG_MPSAFE, + &vt_screen_reader_cursor.tp_col, 0, "Current VT screen reader column number, between 0 and 65535 inclusivly"); + +SYSCTL_PROC(_kern_vt_screen_reader, OID_AUTO, text_utf8, CTLTYPE_STRING | CTLFLAG_RD | + CTLFLAG_MPSAFE, NULL, 0, &vt_screen_reader_text_utf8, "A", "Current VT screen reader buffer in UTF-8 format"); + +SYSCTL_PROC(_kern_vt_screen_reader, OID_AUTO, feed, + CTLTYPE_UINT | CTLFLAG_RWTUN | CTLFLAG_MPSAFE, NULL, + 0, &vt_screen_reader_feed, "IU", "Set to non-zero to feed a new VT screen reader buffer"); + +static int +vt_screen_reader_text_utf8(SYSCTL_HANDLER_ARGS) +{ + int error; + + VT_ACCESS_LOCK(); + if (vt_screen_reader_utf8_data != NULL) { + error = SYSCTL_OUT(req, vt_screen_reader_utf8_data, + vt_screen_reader_utf8_length); + } else { + error = 0; + } + VT_ACCESS_UNLOCK(); + + return (error); +} + +static int +vt_screen_reader_feed(SYSCTL_HANDLER_ARGS) +{ + unsigned max_ln; + unsigned val; + int error; + + VT_ACCESS_LOCK(); + + val = 0; + error = SYSCTL_OUT(req, &val, sizeof(val)); + if (error || !req->newptr) + goto done; + + error = SYSCTL_IN(req, &val, sizeof(val)); + if (error || val == 0) + goto done; + + if (vt_screen_reader_utf8_data == NULL) { + vt_screen_reader_utf8_data = + malloc(VT_ACCESS_UTF8_DATA_MAX, M_VTACCESS, + M_WAITOK | M_ZERO); + } else { + /* Re-use the UTF-8 buffer */ + } + + max_ln = vt_screen_reader_lines; + if (max_ln < 1) + max_ln = 1; + else if (max_ln > 1000) + max_ln = 1000; + + vt_screen_reader_utf8_length = 0; + + /* Update the screen reader buffer: */ + if (vtbuf_extract_screen_reader(vt_screen_reader_utf8_data, + &vt_screen_reader_utf8_length, &vt_screen_reader_cursor, max_ln, + VT_ACCESS_UTF8_DATA_MAX) != 0) + vt_screen_reader_utf8_length = 0; +done: + VT_ACCESS_UNLOCK(); + return (error); +} + +static void +vt_screen_reader_hup_callback(void *arg, int pending) +{ + struct proc *p; + pid_t pid; + + pid = atomic_swap_int(&vt_screen_reader_hup_pid, 0); + if (pid <= 0) + return; + p = pfind(pid); + if (p == NULL) + return; + kern_psignal(p, SIGHUP); + PROC_UNLOCK(p); +} + +void +vt_screen_reader_hup(void) +{ + taskqueue_enqueue(taskqueue_thread, &vt_screen_reader_hup_task); +} diff --git a/sys/sys/terminal.h b/sys/sys/terminal.h --- a/sys/sys/terminal.h +++ b/sys/sys/terminal.h @@ -69,19 +69,25 @@ * 21-25: Bold, underline, blink, reverse, right part of CJK fullwidth character * 26-28: Foreground color * 29-31: Background color + * 32: Used by VT screen reader layer + * 33-63: Unused */ -typedef uint32_t term_char_t; +typedef uint64_t term_char_t; #define TCHAR_CHARACTER(c) ((c) & 0x1fffff) #define TCHAR_FORMAT(c) (((c) >> 21) & 0x1f) #define TCHAR_FGCOLOR(c) (((c) >> 26) & 0x7) #define TCHAR_BGCOLOR(c) (((c) >> 29) & 0x7) +#define TCHAR_SCREEN_READER(c) (((c) >> 32) & 0x1) + +/* bit used by VT screen reader layer */ +#define TSCREEN_READER ((term_char_t)1 << 32) typedef teken_attr_t term_attr_t; typedef teken_color_t term_color_t; #define TCOLOR_FG(c) (((c) & 0x7) << 26) -#define TCOLOR_BG(c) (((c) & 0x7) << 29) +#define TCOLOR_BG(c) ((term_char_t)((c) & 0x7) << 29) #define TCOLOR_LIGHT(c) ((c) | 0x8) #define TCOLOR_DARK(c) ((c) & ~0x8)