diff --git a/usr.bin/Makefile b/usr.bin/Makefile --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -168,6 +168,7 @@ unvis \ vis \ vmstat \ + vtspeakd \ w \ wall \ wc \ diff --git a/usr.bin/vtspeakd/Makefile b/usr.bin/vtspeakd/Makefile new file mode 100644 --- /dev/null +++ b/usr.bin/vtspeakd/Makefile @@ -0,0 +1,6 @@ +# $FreeBSD$ + +PROG= vtspeakd +MAN= vtspeakd.1 + +.include diff --git a/usr.bin/vtspeakd/vtspeakd.1 b/usr.bin/vtspeakd/vtspeakd.1 new file mode 100644 --- /dev/null +++ b/usr.bin/vtspeakd/vtspeakd.1 @@ -0,0 +1,78 @@ +.\" $FreeBSD$ +.\" +.\" 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. +.\" +.Dd October 5, 2022 +.Dt VTSPEAKD 1 +.Os +.Sh NAME +.Nm vtspeakd +.Nd daemon to speak the console +.Sh SYNOPSIS +.Nm +.Op Fl c +.Op Fl k +.Op Fl l Ar language +.Op Fl S Ar program_name +.Op Fl B +.Op Fl v +.Op Fl h +.Sh DESCRIPTION +The +.Nm +daemon is used to feed the contents of the current +.Xr vt 4 +console to an external program, for example espeak. +The +.Nm +program responds to Control-L and Control-B by either redrawing the +screen or terminating the currently spoken text, respectivly. +.Pp +The options are as follows: +.Bl -tag -width "xxxxx" +.It Fl c +Be chatty when speaking the console. +.It Fl k +Don't allow kernel to hangup speak program on bell. +.It Fl l +Select language to use. +.It Fl S +Select speak program to use. +Default is bin/espeak in the local base directory. +.It Fl B +Run program in the backround. +.It Fl v +Print text strings for the speak program to standard output. +.It Fl h +Show usage and exit. +.El +.Sh EXAMPLES +Start +.Nm : +.Pp +.Dl vtspeakd -B +.Sh SEE ALSO +.Xr espeak 1 diff --git a/usr.bin/vtspeakd/vtspeakd.c b/usr.bin/vtspeakd/vtspeakd.c new file mode 100644 --- /dev/null +++ b/usr.bin/vtspeakd/vtspeakd.c @@ -0,0 +1,361 @@ +/*- + * 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 WRAP(str) (char []){str} + +static const char *speak_prog = _PATH_LOCALBASE "/bin/espeak"; +static char *speak_lang; +static bool background; +static bool chatty; +static bool verbose; +static bool nohangup; +static uint16_t last_row = 0xffff; +static uint16_t last_col = 0xffff; + +static void +chatty_char(char ch, char *buffer, size_t *ppos) +{ + char temp[32]; + size_t len; + + 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, " plus ", 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, " apostrophe ", sizeof(temp)); + break; + case '%': + strlcpy(temp, " percent ", sizeof(temp)); + break; + case ' ': + temp[0] = ' '; + temp[1] = 0; + break; + default: + snprintf(temp, sizeof(temp), " byte %u ", (uint8_t)ch); + break; + } + + len = strlen(temp); + + if (buffer != NULL) + memcpy(buffer + *ppos, temp, len); + *ppos += len; +} + +static char * +chatty_buffer(char *data, size_t off) +{ + size_t pos; + char *ndata; + + /* Compute size of chatty buffer: */ + for (size_t x = pos = 0; data[x + off] != 0; x++) + chatty_char(data[x + off], NULL, &pos); + + if (pos != 0) { + ndata = malloc(off + pos + 1); + for (size_t x = pos = 0; data[x + off] != 0; x++) + chatty_char(data[x + off], ndata + off, &pos); + memcpy(ndata, data, off); + ndata[off + pos] = 0; /* zero terminate string buffer */ + free(data); + return (ndata); + } else { + return (data); + } +} + +static void +exit_function(void) +{ + int pid; + + pid = 0; + sysctlbyname("kern.vt.screen_reader.hangup_pid", NULL, NULL, &pid, sizeof(pid)); +} + +static void +sync_execv(const char *path, char *const argv[]) +{ + int pid; + + pid = fork(); + if (pid == 0) { + if (!nohangup) { + pid = getpid(); + sysctlbyname("kern.vt.screen_reader.hangup_pid", NULL, NULL, &pid, sizeof(pid)); + atexit(&exit_function); + } + execv(path, argv); + exit(0); + } else if (pid != -1) { + waitpid(pid, NULL, 0); + } +} + +static void +feed(void) +{ + unsigned tmp = 1; + sysctlbyname("kern.vt.screen_reader.feed", NULL, NULL, &tmp, sizeof(tmp)); +} + +static bool +speak(void) +{ + char *buffer; + char *data; + char *argv[8]; + + size_t len; + uint16_t row = 0; + uint16_t col = 0; + int err; + + unsigned i = 0; + + bool retval; + + len = sizeof(row); + sysctlbyname("kern.vt.screen_reader.row", &row, &len, NULL, 0); + + len = sizeof(col); + sysctlbyname("kern.vt.screen_reader.col", &col, &len, NULL, 0); + + /* Check if anything changed */ + retval = (row != last_row || col != last_col); + + last_row = row; + last_col = col; + + len = 0; + sysctlbyname("kern.vt.screen_reader.text_utf8", NULL, &len, NULL, 0); + if (len == 0) + return (retval); + + data = malloc(len + 1); + memset(data, 0, len + 1); + sysctlbyname("kern.vt.screen_reader.text_utf8", data, &len, NULL, 0); + + if (chatty) { + data = chatty_buffer(data, 0); + if (col == 0) + err = asprintf(&buffer, "row %u %s", row, data); + else + err = asprintf(&buffer, "row %u column %u %s", row, col, data); + } else { + if (col == 0) + err = asprintf(&buffer, "%u %s", row, data); + else + err = asprintf(&buffer, "%u column %u %s", row, col, data); + } + free(data); + + if (err < 0) + return (true); + if (verbose) + printf("%s\n", buffer); + + argv[i++] = WRAP("espeak"); + if (speak_lang != NULL) { + argv[i++] = WRAP("-v"); + argv[i++] = speak_lang; + } + argv[i++] = WRAP("--"); + argv[i++] = buffer; + argv[i++] = NULL; + + sync_execv(speak_prog, argv); + + free(buffer); + + return (true); +} + +static void +usage(void) +{ + fprintf(stderr, "Usage: %s [parameters]\n" + "\t" "-c Be chatty\n" + "\t" "-k Bell does not hangup the speak program\n" + "\t" "-l \n" + "\t" "-S # Default: %s\n" + "\t" "-B Run in background\n" + "\t" "-v Be verbose\n" + "\t" "-h Show usage\n", + getprogname(), speak_prog); + exit(1); +} + +int +main(int argc, char **argv) +{ + int c; + + while ((c = getopt(argc, argv, "Bckl:hS:v")) != -1) { + switch (c) { + case 'B': + background = true; + break; + case 'c': + chatty = true; + break; + case 'k': + nohangup = true; + break; + case 'l': + speak_lang = optarg; + break; + case 'S': + speak_prog = optarg; + break; + case 'v': + verbose = true; + break; + default: + usage(); + break; + } + } + + if (background && daemon(0, 0) != 0) + errx(1, "daemon(0,0) failed"); + + for (;;) { + bool retval; + + retval = speak(); + feed(); + if (retval == false) + usleep(500000); + } + return (0); +}