blob: e4fd916c5b4bc05c834c249f222bfd1229269d4e [file] [log] [blame]
/*
* BRLTTY - A background process providing access to the console screen (when in
* text mode) for a blind person using a refreshable braille display.
*
* Copyright (C) 1995-2023 by The BRLTTY Developers.
*
* BRLTTY comes with ABSOLUTELY NO WARRANTY.
*
* This is free software, placed under the terms of the
* GNU Lesser General Public License, as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any
* later version. Please see the file LICENSE-LGPL for details.
*
* Web Page: http://brltty.app/
*
* This software is maintained by Dave Mielke <dave@mielke.cc>.
*/
#include "prologue.h"
#include <string.h>
#include "embed.h"
#include "log.h"
#include "log_history.h"
#include "message.h"
#include "defaults.h"
#include "async_wait.h"
#include "async_task.h"
#include "utf8.h"
#include "brl_utils.h"
#include "brl_cmds.h"
#include "spk.h"
#include "ktb_types.h"
#include "update.h"
#include "cmd_queue.h"
#include "api_control.h"
#include "core.h"
int messageHoldTimeout = DEFAULT_MESSAGE_HOLD_TIMEOUT;
typedef struct {
const char *mode;
MessageOptions options;
unsigned presented:1;
unsigned deallocate:1;
char text[0];
} MessageParameters;
typedef struct {
const wchar_t *start;
size_t length;
} MessageSegment;
typedef struct {
const MessageParameters *parameters;
struct {
const MessageSegment *first;
const MessageSegment *current;
const MessageSegment *last;
} segments;
int timeout;
unsigned endWait:1;
unsigned hold:1;
unsigned touch:1;
} MessageData;
ASYNC_CONDITION_TESTER(testEndMessageWait) {
const MessageData *mgd = data;
return mgd->endWait;
}
static int
handleMessageCommands (int command, void *data) {
MessageData *mgd = data;
switch (command & BRL_MSK_CMD) {
case BRL_CMD_LNUP:
case BRL_CMD_PRDIFLN:
case BRL_CMD_FWINLTSKIP:
case BRL_CMD_FWINLT: {
if (mgd->segments.current > mgd->segments.first) {
mgd->segments.current -= 1;
mgd->endWait = 1;
}
mgd->hold = 1;
return 1;
}
case BRL_CMD_LNDN:
case BRL_CMD_NXDIFLN:
case BRL_CMD_FWINRTSKIP:
case BRL_CMD_FWINRT: {
if ((mgd->hold = mgd->segments.current < mgd->segments.last)) {
mgd->segments.current += 1;
}
break;
}
default: {
int arg = command & BRL_MSK_ARG;
switch (command & BRL_MSK_BLK) {
case BRL_CMD_BLK(TOUCH_AT):
if ((mgd->touch = arg != BRL_MSK_ARG)) return 1;
mgd->timeout = 1000;
break;
default:
mgd->hold = 0;
break;
}
break;
}
}
mgd->endWait = 1;
return 1;
}
ASYNC_TASK_CALLBACK(presentMessage) {
MessageParameters *mgp = data;
#ifdef ENABLE_SPEECH_SUPPORT
if (!(mgp->options & MSG_SILENT)) {
if (isAutospeakActive()) {
sayString(&spk, mgp->text, SAY_OPT_MUTE_FIRST);
}
}
#endif /* ENABLE_SPEECH_SUPPORT */
if (canBraille()) {
MessageData mgd = {
.hold = 0,
.touch = 0,
.parameters = mgp
};
const size_t characterCount = countUtf8Characters(mgp->text);
MessageSegment messageSegments[characterCount];
wchar_t characters[characterCount + 1];
makeWcharsFromUtf8(mgp->text, characters, ARRAY_COUNT(characters));
const size_t brailleSize = textCount * brl.textRows;
wchar_t brailleBuffer[brailleSize];
{
const wchar_t *character = characters;
const wchar_t *const end = character + characterCount;
MessageSegment *segment = messageSegments;
mgd.segments.current = mgd.segments.first = segment;
while (*character) {
/* strip leading spaces */
while ((character < end) && iswspace(*character)) character += 1;
const size_t charactersLeft = end - character;
if (!charactersLeft) break;
segment->start = character;
segment->length = MIN(charactersLeft, brailleSize);
character += segment->length;
segment += 1;
}
mgd.segments.last = segment - 1;
}
int wasLinked = api.isServerLinked();
if (wasLinked) api.unlinkServer();
suspendUpdates();
pushCommandEnvironment("message", NULL, NULL);
pushCommandHandler("message", KTB_CTX_WAITING,
handleMessageCommands, NULL, &mgd);
while (1) {
const MessageSegment *segment = mgd.segments.current;
size_t cellCount = segment->length;
int lastSegment = segment == mgd.segments.last;
wmemcpy(brailleBuffer, segment->start, cellCount);
brl.cursor = BRL_NO_CURSOR;
if (!writeBrailleCharacters(mgp->mode, brailleBuffer, cellCount)) {
mgp->presented = 0;
break;
}
mgd.timeout = messageHoldTimeout - brl.writeDelay;
drainBrailleOutput(&brl, 0);
if (!mgd.hold && lastSegment && (mgp->options & MSG_NODELAY)) break;
mgd.timeout = MAX(mgd.timeout, 0);
while (1) {
int timeout = mgd.timeout;
mgd.timeout = -1;
mgd.endWait = 0;
int timedOut = !asyncAwaitCondition(timeout, testEndMessageWait, &mgd);
if (mgd.segments.current != segment) break;
if (mgd.hold || mgd.touch) {
mgd.timeout = 1000000;
} else if (timedOut) {
if (lastSegment) goto DONE;
mgd.segments.current += 1;
mgd.timeout = messageHoldTimeout;
break;
} else if (mgd.timeout < 0) {
goto DONE;
}
}
}
DONE:
popCommandEnvironment();
resumeUpdates(1);
if (wasLinked) api.linkServer();
}
if (mgp->deallocate) free(mgp);
}
int
message (const char *mode, const char *text, MessageOptions options) {
if (options & MSG_LOG) pushLogMessage(text);
int presented = 0;
MessageParameters *mgp;
size_t size = sizeof(*mgp) + strlen(text) + 1;
if ((mgp = malloc(size))) {
memset(mgp, 0, size);
mgp->mode = mode? mode: "";
mgp->options = options;
mgp->presented = 1;
strcpy(mgp->text, text);
if (mgp->options & MSG_SYNC) {
mgp->deallocate = 0;
presentMessage(mgp);
if (mgp->presented) presented = 1;
} else {
mgp->deallocate = 1;
if (asyncAddTask(NULL, presentMessage, mgp)) return 1;
}
free(mgp);
}
return presented;
}
void
showMessage (const char *text) {
message(NULL, text, 0);
}