diff --git a/sbin/devd/Makefile b/sbin/devd/Makefile --- a/sbin/devd/Makefile +++ b/sbin/devd/Makefile @@ -31,6 +31,8 @@ DEVD+= zfs.conf .endif +DEVD+= accessibility.conf + PROG_CXX=devd SRCS= devd.cc token.l parse.y y.tab.h MAN= devd.8 devd.conf.5 diff --git a/sbin/devd/accessibility.conf b/sbin/devd/accessibility.conf new file mode 100644 --- /dev/null +++ b/sbin/devd/accessibility.conf @@ -0,0 +1,9 @@ +# $FreeBSD$ + +# Use espeak by default using UTF-8 +notify 0 { + match "system" "VT"; + match "subsystem" "ACCESSIBILITY"; + match "type" "SPEAK"; + action "rtprio 8 /usr/local/bin/espeak -- $text"; +}; diff --git a/sys/conf/files b/sys/conf/files --- a/sys/conf/files +++ b/sys/conf/files @@ -3465,6 +3465,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_accessibility.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 @@ -84,6 +84,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) @@ -168,6 +170,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) @@ -240,6 +244,7 @@ int vtbuf_get_marked_len(struct vt_buf *vb); void vtbuf_extract_marked(struct vt_buf *vb, term_char_t *buf, int sz); #endif +void vtbuf_extract_accessibility(char *, size_t lines, size_t chars); #define VTB_MARK_NONE 0 #define VTB_MARK_END 1 diff --git a/sys/dev/vt/vt_accessibility.c b/sys/dev/vt/vt_accessibility.c new file mode 100644 --- /dev/null +++ b/sys/dev/vt/vt_accessibility.c @@ -0,0 +1,159 @@ +/*- + * 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 + +static SYSCTL_NODE(_kern_vt, OID_AUTO, accessibility, + CTLFLAG_RW | CTLFLAG_MPSAFE, 0, "Accessibility parameters"); + +static int vt_access_enable(SYSCTL_HANDLER_ARGS); + +static unsigned int vt_access_enabled; /* default is disabled */ +static struct sx vt_access_sx; +static struct callout vt_access_callout; + +SX_SYSINIT(vt_access_sx, &vt_access_sx, "VT accessibility"); + +#define VT_ACCESS_LOCK() sx_xlock(&vt_access_sx) +#define VT_ACCESS_UNLOCK() sx_xunlock(&vt_access_sx) + +static unsigned vt_lines_per_interval = 1; /* default value */ + +SYSCTL_UINT(_kern_vt_accessibility, OID_AUTO, lines_per_interval, CTLFLAG_RWTUN, + &vt_lines_per_interval, 0, "Number of lines the VT accessibility module will feed per interval, between 1 and 1000"); + +static unsigned vt_interval_ms = 1000; /* default value */ + +SYSCTL_UINT(_kern_vt_accessibility, OID_AUTO, interval_ms, CTLFLAG_RWTUN, + &vt_interval_ms, 0, "Interval between accessibility requests in milliseconds, between 10ms and 60000ms"); + +SYSCTL_PROC(_kern_vt_accessibility, OID_AUTO, enable, + CTLTYPE_UINT | CTLFLAG_RWTUN | CTLFLAG_MPSAFE, &vt_access_enabled, + 0, vt_access_enable, "IU", "Set if VT accessibility is enabled"); + +static void +vt_access_worker_callout(void *arg) +{ + char buffer[512]; /* XXX the devctl buffer is not so big */ + unsigned max_ln; + unsigned max_iv; + size_t len; + + if (devctl_queue_empty()) { + max_ln = vt_lines_per_interval; + if (max_ln < 1) + max_ln = 1; + else if (max_ln > 1000) + max_ln = 1000; + + max_iv = vt_interval_ms; + if (max_iv < 10) + max_iv = 10; + else if (max_iv > 60000) + max_iv = 60000; + + snprintf(buffer, sizeof(buffer) - 2, "text=\""); + len = strlen(buffer); + vtbuf_extract_accessibility(buffer + len, max_ln, sizeof(buffer) - 2 - len); + len = strlen(buffer); + + /* Check if the buffer was filled */ + if (buffer[len - 1] != '"') { + buffer[len++] = '"'; + buffer[len++] = 0; + + MPASS(len <= sizeof(buffer)); + + devctl_notify("VT", "ACCESSIBILITY", "SPEAK", buffer); + } + } + + /* Restart timer. */ + if (vt_access_enabled != 0) + callout_reset(&vt_access_callout, (max_iv * hz) / 1000, &vt_access_worker_callout, arg); +} + +static void +vt_access_start(void) +{ + callout_init(&vt_access_callout, 1); + + /* kick the callout */ + vt_access_worker_callout(NULL); +} + +static void +vt_access_stop(void) +{ + callout_drain(&vt_access_callout); +} + +static int +vt_access_enable(SYSCTL_HANDLER_ARGS) +{ + unsigned val; + int error; + + VT_ACCESS_LOCK(); + + /* Copy current value as required */ + val = *(unsigned *)arg1; + + error = SYSCTL_OUT(req, &val, sizeof(unsigned)); + if (error || !req->newptr) + goto done; + + error = SYSCTL_IN(req, &val, sizeof(unsigned)); + if (error) + goto done; + + /* Range check input value */ + val = (val != 0) ? 1 : 0; + + if (*(unsigned *)arg1 != val) { + *(unsigned *)arg1 = val; + + if (val) + vt_access_start(); + else + vt_access_stop(); + } +done: + VT_ACCESS_UNLOCK(); + return (error); +} 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,266 @@ } } +static bool +vtbuf_extract_utf8(term_char_t ch, char *buffer, size_t *ppos, size_t size) +{ + char temp[32]; + size_t len; + + ch = TCHAR_CHARACTER(ch); + + switch (ch) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '0' ... '9': + temp[0] = (char)ch; + temp[1] = 0; + break; + case '~': + strlcpy(temp, " tilde ", sizeof(temp)); + break; + case '#': + strlcpy(temp, " hash ", sizeof(temp)); + break; + case '+': + strlcpy(temp, " pluss ", sizeof(temp)); + break; + case '-': + strlcpy(temp, " minus ", sizeof(temp)); + break; + case '_': + strlcpy(temp, " underscore ", sizeof(temp)); + break; + case '*': + strlcpy(temp, " multiply ", sizeof(temp)); + break; + case '/': + strlcpy(temp, " divide ", sizeof(temp)); + break; + case '\\': + strlcpy(temp, " backslash ", sizeof(temp)); + break; + case '=': + strlcpy(temp, " equal ", sizeof(temp)); + break; + case '@': + strlcpy(temp, " at ", sizeof(temp)); + break; + case '^': + strlcpy(temp, " xor ", sizeof(temp)); + break; + case '|': + strlcpy(temp, " pipe ", sizeof(temp)); + break; + case '&': + strlcpy(temp, " and ", sizeof(temp)); + break; + case '(': + strlcpy(temp, " opening parenthesis ", sizeof(temp)); + break; + case ')': + strlcpy(temp, " closing parenthesis ", sizeof(temp)); + break; + case '[': + strlcpy(temp, " opening bracket ", sizeof(temp)); + break; + case ']': + strlcpy(temp, " closing bracket ", sizeof(temp)); + break; + case '{': + strlcpy(temp, " opening curlybracket ", sizeof(temp)); + break; + case '}': + strlcpy(temp, " closing curlybracket ", sizeof(temp)); + break; + case '.': + strlcpy(temp, " period ", sizeof(temp)); + break; + case ',': + strlcpy(temp, " comma ", sizeof(temp)); + break; + case ':': + strlcpy(temp, " colon ", sizeof(temp)); + break; + case ';': + strlcpy(temp, " semicolon ", sizeof(temp)); + break; + case '!': + strlcpy(temp, " not ", sizeof(temp)); + break; + case '$': + strlcpy(temp, " dollar ", sizeof(temp)); + break; + case '?': + strlcpy(temp, " questionmark ", sizeof(temp)); + break; + case '>': + strlcpy(temp, " greaterthan ", sizeof(temp)); + break; + case '<': + strlcpy(temp, " lessthan ", sizeof(temp)); + break; + case '"': + strlcpy(temp, " quote ", sizeof(temp)); + break; + case '%': + strlcpy(temp, " percent ", sizeof(temp)); + break; + default: + if (tchar_is_word_separator(ch)) { + temp[0] = ' '; + temp[1] = 0; + } else { + snprintf(temp, sizeof(temp), " uc %u ", (uint32_t)ch); + } + break; + } + + len = strlen(temp) + 1; + + if (*ppos + len > size) + return (false); + + memcpy(buffer + *ppos, temp, len - 1); + *ppos += len - 1; + return (true); +} + +static bool +tchar_is_extended_word_separator(term_char_t ch) +{ + if (tchar_is_word_separator(ch)) { + return (true); + } else { + char temp[2]; + size_t pos = 0; + return (!vtbuf_extract_utf8(ch, temp, &pos, sizeof(temp))); + } +} + +static const char vt_accessibility_overflow[] = { "Line cannot be spoken" }; + +void +vtbuf_extract_accessibility(char *buffer, size_t lines, size_t sz) +{ + struct vt_device *vd; + struct vt_window *vw; + struct vt_buf *vb; + term_pos_t max; + term_char_t ch; + size_t pos; + size_t last_sep_off; + size_t last_line_off; + int last_sep_col; + int c; + int i; + int r; + + MPASS(sz != 0); + + vd = main_vd; + vw = vd->vd_curwindow; + vb = &vw->vw_buf; + max = vb->vb_scr_size; + pos = 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_ACCESSIBILITY(ch) != 0 || + TCHAR_CHARACTER(ch) == 0) + continue; + if (tchar_is_extended_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_extended_word_separator(ch)) + break; + } + break; + } + + if (c == max.tp_col) + continue; + + lines--; + last_sep_col = c; + last_sep_off = pos; + last_line_off = pos; + + /* 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_extended_word_separator(ch)) { + if (vtbuf_extract_utf8(ch, buffer, &pos, sz) == false) + break; + /* Keep track of sensible stop locations. */ + last_sep_col = c + 1; + last_sep_off = pos; + } else { + if (vtbuf_extract_utf8(ch, buffer, &pos, sz) == 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. */ + pos = last_line_off; + c = max.tp_col; + + for (size_t i = 0; vt_accessibility_overflow[i]; i++) { + if (vtbuf_extract_utf8(vt_accessibility_overflow[i], + buffer, &pos, sz) == false) + break; + } + } else { + /* Stop at last separator, if any. */ + pos = last_sep_off; + c = last_sep_col; + } + + /* Don't output this line again. */ + while (c--) + vb->vb_rows[r][c] |= TACCESSIBILITY; + break; + } else { + /* Don't output this line again. */ + while (c--) + vb->vb_rows[r][c] |= TACCESSIBILITY; + + /* Insert a word separator, just in case. */ + if (vtbuf_extract_utf8(' ', buffer, &pos, sz)) + break; + } + } + VTBUF_UNLOCK(vb); + + /* Remove trailing white space. */ + while (pos != 0 && buffer[pos - 1] == ' ') + pos--; + + /* Put the terminating character there. */ + buffer[pos] = 0; +} + +#ifndef SC_NO_CUTPASTE void vtbuf_extract_marked(struct vt_buf *vb, term_char_t *buf, int sz) { 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"); diff --git a/sys/kern/subr_bus.c b/sys/kern/subr_bus.c --- a/sys/kern/subr_bus.c +++ b/sys/kern/subr_bus.c @@ -674,6 +674,18 @@ pgsigio(&devsoftc.sigio, SIGIO, 0); } +bool +devctl_queue_empty(void) +{ + bool retval; + + mtx_lock(&devsoftc.mtx); + retval = STAILQ_EMPTY(&devsoftc.devq); + mtx_unlock(&devsoftc.mtx); + + return (retval); +} + /** * @brief Send a 'notification' to userland, using standard ways */ diff --git a/sys/sys/devctl.h b/sys/sys/devctl.h --- a/sys/sys/devctl.h +++ b/sys/sys/devctl.h @@ -13,6 +13,7 @@ * hook to send the message. */ bool devctl_process_running(void); +bool devctl_queue_empty(void); void devctl_notify(const char *__system, const char *__subsystem, const char *__type, const char *__data); struct sbuf; 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 accessibility 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_ACCESSIBILITY(c) (((c) >> 32) & 0x1) + +/* bit used by VT accessibility layer */ +#define TACCESSIBILITY ((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)