/* * 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-2021 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 . */ #include "prologue.h" #include #include #include "log.h" #include "strfmt.h" #include "utf8.h" #include "cmd.h" #include "brl_cmds.h" #include "ktb.h" #include "ktb_list.h" #include "ktb_cmds.h" struct CommandGroupHookDataStruct { ListGenerationData *const lgd; const KeyContext *const ctx; int ok; }; static inline void listCommandSubgroup ( int (*list) (ListGenerationData *lgd, const KeyContext *ctx), CommandGroupHookData *cgh ) { cgh->ok = list(cgh->lgd, cgh->ctx); } static int handleCommandGroupHook (CommandGroupHook *handler, CommandGroupHookData *cgh) { if (!handler) return 1; cgh->ok = 0; handler(cgh); return cgh->ok; } static void * getMethodData (ListGenerationData *lgd) { return lgd->list.internal? lgd: lgd->list.data; } static int writeHeader (ListGenerationData *lgd, const wchar_t *text, int level) { return lgd->list.methods->writeHeader(text, level, getMethodData(lgd)); } static int writeLine (ListGenerationData *lgd, const wchar_t *line) { return lgd->list.writeLine(line, lgd->list.data); } static int writeBlankLine (ListGenerationData *lgd) { return writeLine(lgd, WS_C("")); } static int addCharacters (ListGenerationData *lgd, const wchar_t *characters, size_t count) { size_t newLength = lgd->line.length + count; if (newLength > lgd->line.size) { size_t newSize = (newLength | 0X3F) + 1; wchar_t *newCharacters = realloc(lgd->line.characters, ARRAY_SIZE(newCharacters, newSize)); if (!newCharacters) { logSystemError("realloc"); return 0; } lgd->line.characters = newCharacters; lgd->line.size = newSize; } wmemcpy(&lgd->line.characters[lgd->line.length], characters, count); lgd->line.length = newLength; return 1; } static int putCharacters (ListGenerationData *lgd, const wchar_t *characters, size_t count) { if (lgd->line.length == 0) { if (lgd->list.elementLevel > 0) { const unsigned int indent = 2; const unsigned int count = indent * lgd->list.elementLevel; wchar_t characters[count]; wmemset(characters, WC_C(' '), count); characters[count - indent] = lgd->list.elementBullet; lgd->list.elementBullet = WC_C(' '); if (!addCharacters(lgd, characters, count)) return 0; } } return addCharacters(lgd, characters, count); } static int putCharacter (ListGenerationData *lgd, wchar_t character) { return putCharacters(lgd, &character, 1); } static int putCharacterString (ListGenerationData *lgd, const wchar_t *string) { return putCharacters(lgd, string, wcslen(string)); } static int putUtf8String (ListGenerationData *lgd, const char *string) { size_t size = strlen(string) + 1; wchar_t characters[size]; wchar_t *character = characters; convertUtf8ToWchars(&string, &character, size); return putCharacters(lgd, characters, character-characters); } static void clearLine (ListGenerationData *lgd) { lgd->line.length = 0; } static void trimLine (ListGenerationData *lgd) { while (lgd->line.length > 0) { size_t last = lgd->line.length - 1; if (lgd->line.characters[last] != WC_C(' ')) break; lgd->line.length = last; } } static int finishLine (ListGenerationData *lgd) { trimLine(lgd); if (!putCharacter(lgd, 0)) return 0; return 1; } static int endLine (ListGenerationData *lgd) { if (lgd->topicHeader) { if (!writeHeader(lgd, lgd->topicHeader, 1)) return 0; lgd->topicHeader = NULL; } if (lgd->listHeader) { const char *string = lgd->listHeader; lgd->listHeader = NULL; size_t size = strlen(string) + 1; wchar_t characters[size]; wchar_t *character = characters; convertUtf8ToWchars(&string, &character, size); if (!writeHeader(lgd, characters, 2)) return 0; } if (!finishLine(lgd)) return 0; if (!writeLine(lgd, lgd->line.characters)) return 0; clearLine(lgd); return 1; } static int beginList (ListGenerationData *lgd, const char *header) { lgd->listHeader = header; return 1; } static int endList (ListGenerationData *lgd) { return lgd->list.methods->endList(getMethodData(lgd)); } static int beginElement (ListGenerationData *lgd, unsigned int level) { return lgd->list.methods->beginElement(level, getMethodData(lgd)); } static int searchKeyNameEntry (const void *target, const void *element) { const KeyValue *value = target; const KeyNameEntry *const *kne = element; return compareKeyValues(value, &(*kne)->value); } const KeyNameEntry * findKeyNameEntry (KeyTable *table, const KeyValue *value) { const KeyNameEntry *const *array = table->keyNames.table; unsigned int count = table->keyNames.count; const KeyNameEntry *const *kne = bsearch(value, array, count, sizeof(*array), searchKeyNameEntry); if (!kne) return NULL; while (kne > array) { if (compareKeyValues(value, &(*--kne)->value) != 0) { kne += 1; break; } } return *kne; } STR_BEGIN_FORMATTER(formatKeyName, KeyTable *table, const KeyValue *value) const KeyNameEntry *kne = findKeyNameEntry(table, value); if (kne) { STR_PRINTF("%s", kne->name); } else if (value->number != KTB_KEY_ANY) { const KeyValue anyKey = { .group = value->group, .number = KTB_KEY_ANY }; if ((kne = findKeyNameEntry(table, &anyKey))) { STR_PRINTF("%s.%u", kne->name, value->number+1); } } if (STR_LENGTH == 0) STR_PRINTF("?"); STR_END_FORMATTER static int putKeyName (ListGenerationData *lgd, const KeyValue *value) { char string[0X100]; formatKeyName(string, sizeof(string), lgd->keyTable, value); return putUtf8String(lgd, string); } STR_BEGIN_FORMATTER(formatKeyCombination, KeyTable *table, const KeyCombination *combination) char keyDelimiter = 0; unsigned char dotCount = 0; const char *dotPrefix = "dot"; const size_t dotPrefixLength = strlen(dotPrefix); const size_t dotNameLength = dotPrefixLength + 1; for (unsigned char index=0; indexmodifierCount; index+=1) { char keyName[0X100]; formatKeyName(keyName, sizeof(keyName), table, &combination->modifierKeys[combination->modifierPositions[index]]); if (strlen(keyName) == dotNameLength) { if (strncasecmp(keyName, dotPrefix, dotPrefixLength) == 0) { char dotNumber = keyName[dotPrefixLength]; if ((dotNumber >= '1') && (dotNumber <= '8')) { if (++dotCount == 1) goto FIRST_DOT; if (dotCount == 2) { char firstDot = *STR_POP(); STR_PRINTF("s%c", firstDot); } STR_PRINTF("%c", dotNumber); continue; } } } dotCount = 0; FIRST_DOT: if (keyDelimiter) { STR_PRINTF("%c", keyDelimiter); } else { keyDelimiter = '+'; } STR_PRINTF("%s", keyName); } if (combination->flags & KCF_IMMEDIATE_KEY) { if (keyDelimiter) STR_PRINTF("%c", keyDelimiter); STR_FORMAT(formatKeyName, table, &combination->immediateKey); } STR_END_FORMATTER static int putKeyCombination (ListGenerationData *lgd, const KeyCombination *combination) { char string[0X100]; formatKeyCombination(string, sizeof(string), lgd->keyTable, combination); return putUtf8String(lgd, string); } static int putCommandDescription (ListGenerationData *lgd, const BoundCommand *cmd, int details) { char description[0X60]; describeCommand(description, sizeof(description), cmd->value, (details? (CDO_IncludeOperand | CDO_DefaultOperand): 0)); return putUtf8String(lgd, description); } static int putKeyboardFunction (ListGenerationData *lgd, const KeyboardFunction *kbf) { if (!beginElement(lgd, 1)) return 0; if (!putCharacterString(lgd, WS_C("braille keyboard "))) return 0; if (!putUtf8String(lgd, kbf->name)) return 0; if (!putCharacterString(lgd, WS_C(": "))) return 0; return 1; } static int listKeyboardFunctions (ListGenerationData *lgd, const KeyContext *ctx) { if (ctx->mappedKeys.count > 0) { for (unsigned int index=0; indexmappedKeys.count; index+=1) { const MappedKeyEntry *map = &ctx->mappedKeys.table[index]; if (!(map->flags & MKF_HIDDEN)) { const KeyboardFunction *kbf = map->keyboardFunction; if (!putKeyboardFunction(lgd, kbf)) return 0; if (!putKeyName(lgd, &map->keyValue)) return 0; if (!endLine(lgd)) return 0; } } { const KeyboardFunction *kbf = keyboardFunctionTable; const KeyboardFunction *end = kbf + keyboardFunctionCount; while (kbf < end) { if (ctx->mappedKeys.superimpose & kbf->bit) { if (!putKeyboardFunction(lgd, kbf)) return 0; if (!putCharacterString(lgd, WS_C("superimposed"))) return 0; if (!endLine(lgd)) return 0; } kbf += 1; } } } return 1; } void commandGroupHook_keyboardFunctions (CommandGroupHookData *cgh) { listCommandSubgroup(listKeyboardFunctions, cgh); } static int listHotkeyEvent (ListGenerationData *lgd, const KeyValue *keyValue, const char *event, const BoundCommand *cmd) { if (cmd->value != BRL_CMD_NOOP) { if (!beginElement(lgd, 1)) return 0; if ((cmd->value & BRL_MSK_BLK) == BRL_CMD_BLK(CONTEXT)) { const KeyContext *ctx = getKeyContext(lgd->keyTable, (KTB_CTX_DEFAULT + (cmd->value & BRL_MSK_ARG))); if (!ctx) return 0; if (!putUtf8String(lgd, "switch to ")) return 0; if (!putCharacterString(lgd, ctx->title)) return 0; } else { if (!putCommandDescription(lgd, cmd, (keyValue->number != KTB_KEY_ANY))) return 0; } if (!putCharacterString(lgd, WS_C(": "))) return 0; if (!putUtf8String(lgd, event)) return 0; if (!putCharacter(lgd, WC_C(' '))) return 0; if (!putKeyName(lgd, keyValue)) return 0; if (!endLine(lgd)) return 0; } return 1; } static int listHotkeys (ListGenerationData *lgd, const KeyContext *ctx) { const HotkeyEntry *hotkey = ctx->hotkeys.table; unsigned int count = ctx->hotkeys.count; while (count) { if (!(hotkey->flags & HKF_HIDDEN)) { if (!listHotkeyEvent(lgd, &hotkey->keyValue, "press", &hotkey->pressCommand)) return 0; if (!listHotkeyEvent(lgd, &hotkey->keyValue, "release", &hotkey->releaseCommand)) return 0; } hotkey += 1, count -= 1; } return 1; } void commandGroupHook_hotkeys (CommandGroupHookData *cgh) { listCommandSubgroup(listHotkeys, cgh); } static int saveBindingLine ( ListGenerationData *lgd, size_t keysOffset, const BoundCommand *command, const KeyBinding *binding ) { if (lgd->binding.count == lgd->binding.size) { size_t newSize = lgd->binding.size? (lgd->binding.size << 1): 0X10; BindingLine **newLines = realloc(lgd->binding.lines, ARRAY_SIZE(newLines, newSize)); if (!newLines) { logMallocError(); return 0; } lgd->binding.lines = newLines; while (lgd->binding.size < newSize) { lgd->binding.lines[lgd->binding.size++] = NULL; } } { BindingLine *line; size_t size = sizeof(*line) + (sizeof(line->text[0]) * lgd->line.length); if (!(line = malloc(size))) { logMallocError(); return 0; } line->command = command; line->keyCombination = &binding->keyCombination; line->keysOffset = keysOffset; wmemcpy(line->text, lgd->line.characters, (line->length = lgd->line.length)); lgd->binding.lines[lgd->binding.count++] = line; } clearLine(lgd); return 1; } static void removeBindingLine (ListGenerationData *lgd, int index) { { BindingLine *bl = lgd->binding.lines[index]; free(bl); } if (--lgd->binding.count > index) { memmove(&lgd->binding.lines[index], &lgd->binding.lines[index+1], ((lgd->binding.count - index) * sizeof(*lgd->binding.lines))); } } static void removeBindingLines (ListGenerationData *lgd) { size_t *count = &lgd->binding.count; while (*count > 0) removeBindingLine(lgd, (*count - 1)); } static int sortBindingLines (const void *element1, const void *element2) { const BindingLine *const *line1 = element1; const BindingLine *const *line2 = element2; int command1 = (*line1)->command->value; int command2 = (*line2)->command->value; int cmd1 = command1 & BRL_MSK_CMD; int cmd2 = command2 & BRL_MSK_CMD; if (cmd1 < cmd2) return -1; if (cmd1 > cmd2) return 1; if (command1 < command2) return -1; if (command1 > command2) return 1; const KeyCombination *combination1 = (*line1)->keyCombination; const KeyCombination *combination2 = (*line2)->keyCombination; if (combination1->anyKeyCount < combination2->anyKeyCount) return -1; if (combination1->anyKeyCount > combination2->anyKeyCount) return 1; if (combination1 < combination2) return -1; if (combination1 > combination2) return 1; return 0; } static int listBindingLine (ListGenerationData *lgd, int index, int *isSame) { const BindingLine *bl = lgd->binding.lines[index]; int asList = *isSame; if (*isSame) { *isSame = 0; } else { if (!beginElement(lgd, 1)) return 0; if (!putCharacters(lgd, bl->text, bl->keysOffset)) return 0; } { int next = index + 1; if (next < lgd->binding.count) { const BindingLine *nl = lgd->binding.lines[next]; if (bl->command->value == nl->command->value) { if (bl->keyCombination->anyKeyCount == nl->keyCombination->anyKeyCount) { if (!asList) { if (!endLine(lgd)) return 0; } asList = 1; *isSame = 1; } } } } if (asList) { if (!beginElement(lgd, 2)) return 0; } if (!putCharacters(lgd, &bl->text[bl->keysOffset], (bl->length - bl->keysOffset))) return 0; if (!endLine(lgd)) return 0; removeBindingLine(lgd, index); return 1; } static int listBindingLines (ListGenerationData *lgd, const KeyContext *ctx) { if (lgd->binding.count > 0) { qsort(lgd->binding.lines, lgd->binding.count, sizeof(*lgd->binding.lines), sortBindingLines); { const CommandGroupEntry *grp = commandGroupTable; const CommandGroupEntry *grpEnd = grp + commandGroupCount; while (grp < grpEnd) { const CommandListEntry *cmd = grp->commands.table; const CommandListEntry *cmdEnd = cmd + grp->commands.count; CommandGroupHookData cgh = { .lgd = lgd, .ctx = ctx }; if (!beginList(lgd, grp->name)) return 0; if (!handleCommandGroupHook(grp->before, &cgh)) return 0; while (cmd < cmdEnd) { int first = 0; int last = lgd->binding.count - 1; while (first <= last) { int current = (first + last) / 2; const BindingLine *bl = lgd->binding.lines[current]; int command = bl->command->value & BRL_MSK_CMD; if (command < cmd->code) { first = current + 1; } else { last = current - 1; } } int isSame = 0; int current = cmd->code; // Binding lines are removed as they're processed, so, even though // first isn't being incremented, the count will be decremented. while (first < lgd->binding.count) { const BindingLine *bl = lgd->binding.lines[first]; int next = bl->command->value; if ((next & BRL_MSK_CMD) != current) { int blk = next & BRL_MSK_BLK; if (!blk) break; if (blk != (current & BRL_MSK_BLK)) break; } if (!listBindingLine(lgd, first, &isSame)) return 0; } cmd += 1; } if (!handleCommandGroupHook(grp->after, &cgh)) return 0; if (!endList(lgd)) return 0; grp += 1; } { int isSame = 0; if (!beginList(lgd, "Uncategorized Bindings")) return 0; while (lgd->binding.count > 0) { if (!listBindingLine(lgd, 0, &isSame)) return 0; } if (!endList(lgd)) return 0; } } } return 1; } static int listKeyBindings (ListGenerationData *lgd, const KeyContext *ctx, const wchar_t *keysPrefix); static int listKeyBinding (ListGenerationData *lgd, const KeyBinding *binding, int longPress, const wchar_t *keysPrefix) { const BoundCommand *cmd = longPress? &binding->secondaryCommand: &binding->primaryCommand; if (cmd->value == BRL_CMD_NOOP) return 1; if (!putCommandDescription(lgd, cmd, !binding->keyCombination.anyKeyCount)) return 0; if (!putCharacterString(lgd, WS_C(": "))) return 0; size_t keysOffset = lgd->line.length; if (keysPrefix) { if (!putCharacterString(lgd, keysPrefix)) return 0; if (!putCharacterString(lgd, WS_C(", "))) return 0; } if (longPress) { if (!putCharacterString(lgd, WS_C("long "))) return 0; } if (!putKeyCombination(lgd, &binding->keyCombination)) return 0; if ((cmd->value & BRL_MSK_BLK) == BRL_CMD_BLK(CONTEXT)) { const KeyContext *ctx = getKeyContext(lgd->keyTable, (KTB_CTX_DEFAULT + (cmd->value & BRL_MSK_ARG))); if (!ctx) return 0; { size_t length = lgd->line.length - keysOffset; wchar_t keys[length + 1]; wmemcpy(keys, &lgd->line.characters[keysOffset], length); keys[length] = 0; clearLine(lgd); if (isTemporaryKeyContext(lgd->keyTable, ctx)) { if (!listKeyBindings(lgd, ctx, keys)) return 0; } else { if (!putCharacterString(lgd, WS_C("switch to "))) return 0; if (!putCharacterString(lgd, ctx->title)) return 0; if (!putCharacterString(lgd, WS_C(": "))) return 0; keysOffset = lgd->line.length; if (!putCharacterString(lgd, keys)) return 0; if (!saveBindingLine(lgd, keysOffset, cmd, binding)) return 0; } } } else { if (!saveBindingLine(lgd, keysOffset, cmd, binding)) return 0; } return 1; } static int listKeyBindings (ListGenerationData *lgd, const KeyContext *ctx, const wchar_t *keysPrefix) { const KeyBinding *binding = ctx->keyBindings.table; unsigned int count = ctx->keyBindings.count; while (count) { if (!(binding->flags & KBF_HIDDEN)) { if (!listKeyBinding(lgd, binding, 0, keysPrefix)) return 0; if (!listKeyBinding(lgd, binding, 1, keysPrefix)) return 0; } binding += 1, count -= 1; } return 1; } static int listKeyContext (ListGenerationData *lgd, const KeyContext *ctx) { lgd->topicHeader = ctx->title; if (!listKeyBindings(lgd, ctx, NULL)) return 0; if (!listBindingLines(lgd, ctx)) return 0; return 1; } static int listSpecialKeyContexts (ListGenerationData *lgd) { static const unsigned char contexts[] = { KTB_CTX_DEFAULT, KTB_CTX_MENU }; const unsigned char *context = contexts; unsigned int count = ARRAY_COUNT(contexts); while (count) { const KeyContext *ctx = getKeyContext(lgd->keyTable, *context); if (ctx) { if (!listKeyContext(lgd, ctx)) return 0; } context += 1, count -= 1; } return 1; } static int listPersistentKeyContexts (ListGenerationData *lgd) { for (unsigned int context=KTB_CTX_DEFAULT+1; contextkeyTable->keyContexts.count; context+=1) { const KeyContext *ctx = getKeyContext(lgd->keyTable, context); if (ctx && !isTemporaryKeyContext(lgd->keyTable, ctx)) { if (!listKeyContext(lgd, ctx)) return 0; } } return 1; } static int listKeyTableTitle (ListGenerationData *lgd) { if (!putUtf8String(lgd, gettext("Key Table"))) return 0; if (lgd->keyTable->title) { if (!putCharacterString(lgd, WS_C(": "))) return 0; if (!putCharacterString(lgd, lgd->keyTable->title)) return 0; if (!finishLine(lgd)) return 0; if (!writeHeader(lgd, lgd->line.characters, 0)) return 0; clearLine(lgd); } return 1; } static int listKeyTableNotes (ListGenerationData *lgd) { unsigned int noteIndex; if (!beginList(lgd, "Notes")) return 0; for (noteIndex=0; noteIndexkeyTable->notes.count; noteIndex+=1) { const wchar_t *line = lgd->keyTable->notes.table[noteIndex]; unsigned int level; int prefixed; if (*line == WC_C('*')) { level = 0; prefixed = 1; } else if (*line == WC_C('+')) { level = 2; prefixed = 1; } else { level = 1; prefixed = 0; } if (level > 0) { if (!beginElement(lgd, level)) return 0; } if (prefixed) { line += 1; while (iswspace(*line)) line += 1; } if (!putCharacterString(lgd, line)) return 0; if (!endLine(lgd)) return 0; } if (!endList(lgd)) return 0; return 1; } static int listKeyTableSections (ListGenerationData *lgd) { typedef int Lister (ListGenerationData *lgd); static Lister *const listerTable[] = { listKeyTableTitle, listKeyTableNotes, listSpecialKeyContexts, listPersistentKeyContexts }; Lister *const *lister = listerTable; Lister *const *end = lister + ARRAY_COUNT(listerTable); while (lister < end) { if (!(*lister)(lgd)) return 0; lister += 1; } return 1; } static int internalWriteHeader (const wchar_t *text, unsigned int level, void *data) { static const wchar_t characters[] = {WC_C('='), WC_C('-')}; ListGenerationData *lgd = data; if (!writeLine(lgd, text)) return 0; if (level < ARRAY_COUNT(characters)) { size_t length = wcslen(text); wchar_t underline[length + 1]; wmemset(underline, characters[level], length); underline[length] = 0; if (!writeLine(lgd, underline)) return 0; if (!writeBlankLine(lgd)) return 0; } return 1; } static int internalBeginElement (unsigned int level, void *data) { ListGenerationData *lgd = data; static const wchar_t bullets[] = { WC_C('*'), WC_C('+'), WC_C('-') }; lgd->list.elementLevel = level; lgd->list.elementBullet = bullets[level - 1]; return 1; } static int internalEndList (void *data) { ListGenerationData *lgd = data; if (lgd->list.elementLevel > 0) { lgd->list.elementLevel = 0; lgd->listHeader = NULL; if (!writeBlankLine(lgd)) return 0; } return 1; } int listKeyTable (KeyTable *table, const KeyTableListMethods *methods, KeyTableWriteLineMethod *writeLine, void *data) { static const KeyTableListMethods internalMethods = { .writeHeader = internalWriteHeader, .beginElement = internalBeginElement, .endList = internalEndList }; ListGenerationData lgd = { .keyTable = table, .topicHeader = NULL, .listHeader = NULL, .line = { .characters = NULL, .size = 0, .length = 0, }, .list = { .methods = methods? methods: &internalMethods, .writeLine = writeLine, .data = data, .internal = !methods, .elementLevel = 0 }, .binding = { .lines = NULL, .size = 0, .count = 0 } }; int result = listKeyTableSections(&lgd); if (lgd.binding.lines) { removeBindingLines(&lgd); free(lgd.binding.lines); } if (lgd.line.characters) free(lgd.line.characters); return result; } int forEachKeyName (KEY_NAME_TABLES_REFERENCE keys, KeyNameEntryHandler *handleKeyNameEntry, void *data) { const KeyNameEntry *const *knt = keys; while (*knt) { const KeyNameEntry *kne = *knt; if (knt != keys) { if (!handleKeyNameEntry(NULL, data)) { return 0; } } while (kne->name) { if (!handleKeyNameEntry(kne, data)) return 0; kne += 1; } knt += 1; } return 1; } typedef struct { KeyTableWriteLineMethod *const writeLine; void *const data; } ListKeyNameData; static int listKeyName (const KeyNameEntry *kne, void *data) { const ListKeyNameData *lkn = data; const char *name = kne? kne->name: ""; size_t size = strlen(name) + 1; wchar_t characters[size]; wchar_t *character = characters; convertUtf8ToWchars(&name, &character, size); return lkn->writeLine(characters, lkn->data); } int listKeyNames (KEY_NAME_TABLES_REFERENCE keys, KeyTableWriteLineMethod *writeLine, void *data) { ListKeyNameData lkn = { .writeLine = writeLine, .data = data }; return forEachKeyName(keys, listKeyName, &lkn); }