blob: 31b54c279cdb4ac6ffc1c5636fe56b5e1958b6ba [file] [log] [blame] [edit]
/*
* 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 <stdio.h>
#include <string.h>
#include "log.h"
#include "strfmt.h"
#include "alert.h"
#include "prefs.h"
#include "unicode.h"
#include "ktb.h"
#include "ktb_internal.h"
#include "ktb_inspect.h"
#include "brl_types.h"
#include "brl_cmds.h"
#include "cmd.h"
#include "cmd_enqueue.h"
#include "async_alarm.h"
#include "hostcmd.h"
#define BRL_CMD_ALERT(alert) BRL_CMD_ARG(ALERT, ALERT_##alert)
ASYNC_ALARM_CALLBACK(handleKeyAutoreleaseAlarm) {
KeyTable *table = parameters->data;
asyncDiscardHandle(table->autorelease.alarm);
table->autorelease.alarm = NULL;
for (unsigned int index=0; index<table->pressedKeys.count; index+=1) {
const KeyValue *kv = &table->pressedKeys.table[index];
char key[0X40];
STR_BEGIN(key, sizeof(key));
STR_FORMAT(formatKeyName, table, kv);
STR_PRINTF(" (Grp:%u Num:%u)", kv->group, kv->number);
STR_END;
logMessage(LOG_WARNING, "autoreleasing key: %s", key);
}
resetKeyTable(table);
alert(ALERT_KEYS_AUTORELEASED);
}
static void
cancelAutoreleaseAlarm (KeyTable *table) {
if (table->autorelease.alarm) {
asyncCancelRequest(table->autorelease.alarm);
table->autorelease.alarm = NULL;
}
}
static void
setAutoreleaseAlarm (KeyTable *table) {
if (!table->autorelease.time || !table->pressedKeys.count) {
cancelAutoreleaseAlarm(table);
} else if (table->autorelease.alarm) {
asyncResetAlarmIn(table->autorelease.alarm, table->autorelease.time);
} else {
asyncNewRelativeAlarm(&table->autorelease.alarm, table->autorelease.time,
handleKeyAutoreleaseAlarm, table);
}
}
void
setKeyAutoreleaseTime (KeyTable *table, unsigned char setting) {
table->autorelease.time = setting? (5000 << (setting - 1)): 0;
setAutoreleaseAlarm(table);
}
static int
sortModifierKeys (const void *element1, const void *element2) {
const KeyValue *modifier1 = element1;
const KeyValue *modifier2 = element2;
return compareKeyValues(modifier1, modifier2);
}
static int
searchKeyBinding (const void *target, const void *element) {
const KeyBinding *reference = target;
const KeyBinding *binding = element;
return compareKeyBindings(reference, binding);
}
static const KeyBinding *
findKeyBinding (KeyTable *table, unsigned char context, const KeyValue *immediate, int *isIncomplete) {
const KeyContext *ctx = getKeyContext(table, context);
if (!ctx) return NULL;
if (!ctx->keyBindings.table) return NULL;
if (table->pressedKeys.count > MAX_MODIFIERS_PER_COMBINATION) return NULL;
KeyBinding target = {
.keyCombination.modifierCount = table->pressedKeys.count
};
if (immediate) {
target.keyCombination.immediateKey = *immediate;
target.keyCombination.flags |= KCF_IMMEDIATE_KEY;
}
while (1) {
unsigned int all = (1 << table->pressedKeys.count) - 1;
for (unsigned int bits=0; bits<=all; bits+=1) {
{
unsigned int index;
unsigned int bit;
for (index=0, bit=1; index<table->pressedKeys.count; index+=1, bit<<=1) {
KeyValue *modifier = &target.keyCombination.modifierKeys[index];
*modifier = table->pressedKeys.table[index];
if (bits & bit) modifier->number = KTB_KEY_ANY;
}
}
qsort(
target.keyCombination.modifierKeys, table->pressedKeys.count,
sizeof(*target.keyCombination.modifierKeys), sortModifierKeys
);
{
const KeyBinding *binding = bsearch(&target, ctx->keyBindings.table,
ctx->keyBindings.count,
sizeof(*ctx->keyBindings.table),
searchKeyBinding);
if (binding) {
if (binding->primaryCommand.value != EOF) return binding;
*isIncomplete = 1;
}
}
}
if (!(target.keyCombination.flags & KCF_IMMEDIATE_KEY)) break;
if (target.keyCombination.immediateKey.number == KTB_KEY_ANY) break;
target.keyCombination.immediateKey.number = KTB_KEY_ANY;
}
return NULL;
}
static int
searchHotkeyEntry (const void *target, const void *element) {
const HotkeyEntry *reference = target;
const HotkeyEntry *hotkey = element;
return compareHotkeyEntries(reference, hotkey);
}
static const HotkeyEntry *
findHotkeyEntry (KeyTable *table, unsigned char context, const KeyValue *keyValue) {
const KeyContext *ctx = getKeyContext(table, context);
if (!ctx) return NULL;
if (!ctx->hotkeys.table) return NULL;
HotkeyEntry target = {
.keyValue = *keyValue
};
return bsearch(&target, ctx->hotkeys.table, ctx->hotkeys.count,
sizeof(*ctx->hotkeys.table), searchHotkeyEntry);
}
static int
searchMappedKeyEntry (const void *target, const void *element) {
const MappedKeyEntry *reference = target;
const MappedKeyEntry *map = element;
return compareMappedKeyEntries(reference, map);
}
static const MappedKeyEntry *
findMappedKeyEntry (const KeyContext *ctx, const KeyValue *keyValue) {
if (!ctx) return NULL;
if (!ctx->mappedKeys.table) return NULL;
MappedKeyEntry target = {
.keyValue = *keyValue
};
return bsearch(&target, ctx->mappedKeys.table, ctx->mappedKeys.count,
sizeof(*ctx->mappedKeys.table), searchMappedKeyEntry);
}
static int
makeKeyboardCommand (KeyTable *table, unsigned char context, int allowChords) {
const KeyContext *ctx;
if ((ctx = getKeyContext(table, context))) {
int bits = 0;
for (unsigned int pressedIndex=0; pressedIndex<table->pressedKeys.count; pressedIndex+=1) {
const KeyValue *keyValue = &table->pressedKeys.table[pressedIndex];
const MappedKeyEntry *map = findMappedKeyEntry(ctx, keyValue);
if (!map) return EOF;
bits |= map->keyboardFunction->bit;
}
{
int space = bits & BRL_DOTC;
int dots = bits & BRL_ALL_DOTS;
if (!allowChords) {
if (!space == !dots) return EOF;
bits &= ~BRL_DOTC;
}
if (dots) bits |= ctx->mappedKeys.superimpose;
}
return BRL_CMD_BLK(PASSDOTS) | bits;
}
return EOF;
}
static int
findPressedKey (KeyTable *table, const KeyValue *value, unsigned int *position) {
return findKeyValue(table->pressedKeys.table, table->pressedKeys.count, value, position);
}
static int
insertPressedKey (KeyTable *table, const KeyValue *value, unsigned int position) {
return insertKeyValue(&table->pressedKeys.table, &table->pressedKeys.count, &table->pressedKeys.size, value, position);
}
static void
removePressedKey (KeyTable *table, unsigned int position) {
removeKeyValue(table->pressedKeys.table, &table->pressedKeys.count, position);
}
static inline void
deleteExplicitKeyValue (KeyValue *values, unsigned int *count, const KeyValue *value) {
if (value->number != KTB_KEY_ANY) deleteKeyValue(values, count, value);
}
static int
sortKeyOffsets (const void *element1, const void *element2) {
const KeyValue *value1 = element1;
const KeyValue *value2 = element2;
if (value1->number < value2->number) return -1;
if (value1->number > value2->number) return 1;
return 0;
}
static void
addCommandArguments (KeyTable *table, int *command, const CommandEntry *entry, const KeyBinding *binding) {
if (entry->isOffset | entry->isColumn | entry->isRow | entry->isRange | entry->isKeyboard) {
unsigned int keyCount = table->pressedKeys.count;
KeyValue keyValues[keyCount];
copyKeyValues(keyValues, table->pressedKeys.table, keyCount);
{
int index;
for (index=0; index<binding->keyCombination.modifierCount; index+=1) {
deleteExplicitKeyValue(keyValues, &keyCount, &binding->keyCombination.modifierKeys[index]);
}
}
if (binding->keyCombination.flags & KCF_IMMEDIATE_KEY) {
deleteExplicitKeyValue(keyValues, &keyCount, &binding->keyCombination.immediateKey);
}
if (keyCount > 0) {
if (keyCount > 1) {
qsort(keyValues, keyCount, sizeof(*keyValues), sortKeyOffsets);
if (entry->isRange) *command |= BRL_EXT_PUT(keyValues[1].number);
}
*command += keyValues[0].number;
} else if (entry->isColumn) {
if (!entry->isRouting) *command |= BRL_MSK_ARG;
}
}
}
static int
isInputKey (BRL_Key key) {
switch (key) {
case BRL_KEY_BACKSPACE:
case BRL_KEY_DELETE:
case BRL_KEY_ESCAPE:
case BRL_KEY_TAB:
case BRL_KEY_ENTER:
return 1;
default:
return 0;
}
}
static int
processCommand (KeyTable *table, int command) {
int isInput = 0;
switch (command) {
default: {
int arg = command & BRL_MSK_ARG;
switch (command & BRL_MSK_BLK) {
case BRL_CMD_BLK(CONTEXT): {
unsigned char context = KTB_CTX_DEFAULT + arg;
const KeyContext *ctx = getKeyContext(table, context);
if (ctx) {
table->context.next = context;
if (isTemporaryKeyContext(table, ctx)) {
command = BRL_CMD_ALERT(CONTEXT_TEMPORARY);
} else {
table->context.persistent = context;
command =
(context == KTB_CTX_DEFAULT)?
BRL_CMD_ALERT(CONTEXT_DEFAULT):
BRL_CMD_ALERT(CONTEXT_PERSISTENT);
}
if (prefs.speakKeyContext) {
if (ctx->title) {
speakAlertText(ctx->title);
} else {
const wchar_t *name = ctx->name;
const wchar_t *from = name;
wchar_t text[(wcslen(name) * 2) + 1];
wchar_t *to = text;
while (*from) {
wchar_t character = *from++;
if (iswupper(character)) {
if (to != text) {
*to++ = WC_C(' ');
}
}
*to++ = character;
}
*to = 0;
speakAlertText(text);
}
} else if (!enqueueCommand(command)) {
return 0;
}
command = BRL_CMD_NOOP;
}
break;
}
case BRL_CMD_BLK(MACRO): {
if (arg < table->commandMacros.count) {
const CommandMacro *macro = &table->commandMacros.table[arg];
const BoundCommand *cmd = macro->commands;
const BoundCommand *end = cmd + macro->count;
while (cmd < end) {
if (!processCommand(table, (cmd++)->value)) return 0;
}
}
command = BRL_CMD_NOOP;
break;
}
case BRL_CMD_BLK(HOSTCMD): {
if (arg < table->hostCommands.count) {
const HostCommand *hc = &table->hostCommands.table[arg];
HostCommandOptions options;
initializeHostCommandOptions(&options);
options.asynchronous = 1;
runHostCommand((const char *const *)hc->arguments, &options);
}
command = BRL_CMD_NOOP;
break;
}
case BRL_CMD_BLK(PASSDOTS):
switch (prefs.brailleTypingMode) {
case BRL_TYPING_DOTS: {
wchar_t character = UNICODE_BRAILLE_ROW | arg;
int flags = command & BRL_MSK_FLG;
command = BRL_CMD_BLK(PASSCHAR) | BRL_ARG_SET(character) | flags;
break;
}
}
isInput = 1;
break;
case BRL_CMD_BLK(PASSCHAR):
isInput = 1;
break;
case BRL_CMD_BLK(PASSKEY):
if (isInputKey(arg)) isInput = 1;
break;
default:
break;
}
break;
}
}
if (isInput) {
if (table->options.keyboardEnabledFlag && !*table->options.keyboardEnabledFlag) {
command = BRL_CMD_ALERT(COMMAND_REJECTED);
}
}
return enqueueCommand(command);
}
static void
logKeyEvent (
KeyTable *table, const char *action,
unsigned char context, const KeyValue *keyValue, int command
) {
if (table->options.logKeyEventsFlag && *table->options.logKeyEventsFlag) {
char buffer[0X100];
STR_BEGIN(buffer, sizeof(buffer));
if (table->options.logLabel) STR_PRINTF("%s ", table->options.logLabel);
STR_PRINTF("key %s: ", action);
STR_FORMAT(formatKeyName, table, keyValue);
STR_PRINTF(" (Ctx:%u Grp:%u Num:%u)", context, keyValue->group, keyValue->number);
if (command != EOF) {
const CommandEntry *cmd = findCommandEntry(command);
const char *name = cmd? cmd->name: "?";
STR_PRINTF(" -> %s (Cmd:%06X)", name, command);
}
STR_END;
logMessage(categoryLogLevel, "%s", buffer);
}
}
static void setLongPressAlarm (KeyTable *table, unsigned char when);
ASYNC_ALARM_CALLBACK(handleLongPressAlarm) {
KeyTable *table = parameters->data;
int command = table->longPress.command;
asyncDiscardHandle(table->longPress.alarm);
table->longPress.alarm = NULL;
logKeyEvent(table, table->longPress.keyAction,
table->longPress.keyContext,
&table->longPress.keyValue,
command);
if (table->longPress.repeat) {
table->longPress.keyAction = "repeat";
setLongPressAlarm(table, prefs.autorepeatInterval);
}
table->release.command = BRL_CMD_NOOP;
processCommand(table, command);
}
static void
setLongPressAlarm (KeyTable *table, unsigned char when) {
asyncNewRelativeAlarm(&table->longPress.alarm, PREFS2MSECS(when),
handleLongPressAlarm, table);
}
static int
isRepeatableCommand (int command) {
if (prefs.autorepeatEnabled) {
switch (command & BRL_MSK_BLK) {
case BRL_CMD_BLK(PASSCHAR):
case BRL_CMD_BLK(PASSDOTS):
return 1;
default:
switch (command & BRL_MSK_CMD) {
case BRL_CMD_LNUP:
case BRL_CMD_LNDN:
case BRL_CMD_PRDIFLN:
case BRL_CMD_NXDIFLN:
case BRL_CMD_CHRLT:
case BRL_CMD_CHRRT:
case BRL_CMD_MENU_PREV_ITEM:
case BRL_CMD_MENU_NEXT_ITEM:
case BRL_CMD_MENU_PREV_SETTING:
case BRL_CMD_MENU_NEXT_SETTING:
case BRL_CMD_KEY(BACKSPACE):
case BRL_CMD_KEY(DELETE):
case BRL_CMD_KEY(PAGE_UP):
case BRL_CMD_KEY(PAGE_DOWN):
case BRL_CMD_KEY(CURSOR_UP):
case BRL_CMD_KEY(CURSOR_DOWN):
case BRL_CMD_KEY(CURSOR_LEFT):
case BRL_CMD_KEY(CURSOR_RIGHT):
case BRL_CMD_SPEAK_PREV_CHAR:
case BRL_CMD_SPEAK_NEXT_CHAR:
case BRL_CMD_SPEAK_PREV_WORD:
case BRL_CMD_SPEAK_NEXT_WORD:
case BRL_CMD_SPEAK_PREV_LINE:
case BRL_CMD_SPEAK_NEXT_LINE:
return 1;
case BRL_CMD_FWINLT:
case BRL_CMD_FWINRT:
if (prefs.autorepeatPanning) return 1;
default:
break;
}
break;
}
}
return 0;
}
static int
getPressedKeysCommand (
KeyTable *table, unsigned char context,
const KeyValue *key, unsigned int position,
const KeyBinding **binding, int *wasInserted,
int *isIncomplete, int *isImmediate
) {
*binding = findKeyBinding(table, context, key, isIncomplete);
*wasInserted = insertPressedKey(table, key, position);
if (*binding) {
*isImmediate = 1;
return (*binding)->primaryCommand.value;
}
if ((*binding = findKeyBinding(table, context, NULL, isIncomplete))) {
*isImmediate = 0;
return (*binding)->primaryCommand.value;
}
return EOF;
}
KeyTableState
processKeyEvent (
KeyTable *table, unsigned char context,
KeyGroup keyGroup, KeyNumber keyNumber, int press
) {
const KeyValue keyValue = {
.group = keyGroup,
.number = keyNumber
};
KeyTableState state = KTS_UNBOUND;
int command = EOF;
const HotkeyEntry *hotkey;
if (press && !table->pressedKeys.count) {
table->context.current = table->context.next;
table->context.next = table->context.persistent;
}
if (context == KTB_CTX_DEFAULT) context = table->context.current;
if (!(hotkey = findHotkeyEntry(table, context, &keyValue))) {
const KeyValue anyKey = {
.group = keyValue.group,
.number = KTB_KEY_ANY
};
hotkey = findHotkeyEntry(table, context, &anyKey);
}
if (hotkey) {
const BoundCommand *cmd = press? &hotkey->pressCommand: &hotkey->releaseCommand;
if (cmd->value != BRL_CMD_NOOP) processCommand(table, (command = cmd->value));
state = KTS_HOTKEY;
} else {
int isImmediate = 1;
unsigned int keyPosition;
int wasPressed = findPressedKey(table, &keyValue, &keyPosition);
if (wasPressed) removePressedKey(table, keyPosition);
if (press) {
const KeyBinding *binding;
int isIncomplete = 0;
int wasInserted;
int command = getPressedKeysCommand(
table, context,
&keyValue, keyPosition,
&binding, &wasInserted,
&isIncomplete, &isImmediate
);
if (command == EOF) {
if ((command = makeKeyboardCommand(table, context, 0)) != EOF) {
isImmediate = 0;
} ;
}
if (command == EOF) {
int tryDefaultContext = wasInserted && (context != KTB_CTX_DEFAULT);
if (tryDefaultContext) {
const KeyContext *ctx = getKeyContext(table, context);
if (ctx && ctx->isIsolated) {
tryDefaultContext = 0;
command = BRL_CMD_NOOP;
}
}
if (tryDefaultContext) {
removePressedKey(table, keyPosition);
command = getPressedKeysCommand(
table, KTB_CTX_DEFAULT,
&keyValue, keyPosition,
&binding, &wasInserted,
&isIncomplete, &isImmediate
);
if (command != EOF) {
switch (command & BRL_MSK_BLK) {
case BRL_CMD_BLK(PASSDOTS): {
command = BRL_CMD_ALERT(COMMAND_REJECTED);
break;
}
}
}
}
}
if (prefs.brailleQuickSpace) {
int cmd = makeKeyboardCommand(table, context, 1);
if (cmd != EOF) {
command = cmd;
isImmediate = 0;
}
}
if (command == EOF) {
command = BRL_CMD_NOOP;
if (isIncomplete) state = KTS_MODIFIERS;
} else {
state = KTS_COMMAND;
}
if (!wasPressed) {
int secondaryCommand = BRL_CMD_NOOP;
resetLongPressData(table);
table->release.command = BRL_CMD_NOOP;
if (binding) {
addCommandArguments(table, &command, binding->primaryCommand.entry, binding);
secondaryCommand = binding->secondaryCommand.value;
addCommandArguments(table, &secondaryCommand, binding->secondaryCommand.entry, binding);
}
if (context == KTB_CTX_WAITING) {
table->release.command = BRL_CMD_NOOP;
} else {
if (secondaryCommand == BRL_CMD_NOOP) {
if (isRepeatableCommand(command)) {
secondaryCommand = command;
}
}
if (isImmediate) {
table->release.command = BRL_CMD_NOOP;
} else {
table->release.command = command;
command = BRL_CMD_NOOP;
}
if (secondaryCommand != BRL_CMD_NOOP) {
table->longPress.command = secondaryCommand;
table->longPress.repeat = isRepeatableCommand(secondaryCommand);
table->longPress.keyAction = "long";
table->longPress.keyContext = context;
table->longPress.keyValue = keyValue;
setLongPressAlarm(table, prefs.longPressTime);
}
}
processCommand(table, command);
}
} else {
resetLongPressData(table);
if (prefs.onFirstRelease || (table->pressedKeys.count == 0)) {
int *cmd = &table->release.command;
if (*cmd != BRL_CMD_NOOP) {
processCommand(table, (command = *cmd));
*cmd = BRL_CMD_NOOP;
}
}
}
setAutoreleaseAlarm(table);
}
logKeyEvent(table, (press? "press": "release"), context, &keyValue, command);
return state;
}
void
releaseAllKeys (KeyTable *table) {
while (table->pressedKeys.count) {
const KeyValue *kv = &table->pressedKeys.table[0];
processKeyEvent(table, KTB_CTX_DEFAULT, kv->group, kv->number, 0);
}
}
void
setKeyTableLogLabel (KeyTable *table, const char *label) {
table->options.logLabel = label;
}
void
setLogKeyEventsFlag (KeyTable *table, const unsigned char *flag) {
table->options.logKeyEventsFlag = flag;
}
void
setKeyboardEnabledFlag (KeyTable *table, const unsigned char *flag) {
table->options.keyboardEnabledFlag = flag;
}
void
getKeyGroupCommands (KeyTable *table, KeyGroup group, int *commands, unsigned int size) {
const KeyContext *ctx = getKeyContext(table, KTB_CTX_DEFAULT);
if (ctx) {
unsigned int i;
for (i=0; i<size; i+=1) {
commands[i] = BRL_CMD_NOOP;
}
for (i=0; i<ctx->keyBindings.count; i+=1) {
const KeyBinding *binding = &ctx->keyBindings.table[i];
const KeyCombination *combination = &binding->keyCombination;
const KeyValue *key;
if (combination->flags & KCF_IMMEDIATE_KEY) {
if (combination->modifierCount != 0) continue;
key = &combination->immediateKey;
} else {
if (combination->modifierCount != 1) continue;
key = &combination->modifierKeys[0];
}
if (key->group == group) {
if (key->number != KTB_KEY_ANY) {
if (key->number < size) {
int command = binding->primaryCommand.value;
if (command != BRL_CMD_NOOP) {
commands[key->number] = command;
}
}
}
}
}
}
}
typedef struct {
int *array;
unsigned int size;
unsigned int count;
} AddCommandData;
static int
addCommand (AddCommandData *acd, int command) {
if (command == EOF) return 1;
int blk = command & BRL_MSK_BLK;
command &= blk? BRL_MSK_BLK: BRL_MSK_CMD;
if (command == BRL_CMD_NOOP) return 1;
if (acd->count == acd->size) {
unsigned int newSize = acd->size? (acd->size << 1): 0X100;
int *newArray = realloc(acd->array, ARRAY_SIZE(newArray, newSize));
if (!newArray) {
logMallocError();
return 0;
}
acd->array = newArray;
acd->size = newSize;
}
acd->array[acd->count++] = command;
return 1;
}
static int
addBoundCommand (AddCommandData *acd, const BoundCommand *cmd) {
return addCommand(acd, cmd->value);
}
static int
sortCommands (const void *element1, const void *element2) {
const int *command1 = element1;
const int *command2 = element2;
if (*command1 < *command2) return -1;
if (*command1 > *command2) return 1;
return 0;
}
int *
getBoundCommands (KeyTable *table, unsigned int *count) {
AddCommandData acd = {
.array = NULL,
.size = 0,
.count = 0
};
for (unsigned int context=0; context<table->keyContexts.count; context+=1) {
const KeyContext *ctx = getKeyContext(table, context);
if (ctx) {
{
const KeyBinding *binding = ctx->keyBindings.table;
const KeyBinding *end = binding + ctx->keyBindings.count;
while (binding < end) {
if (!addBoundCommand(&acd, &binding->primaryCommand)) goto error;
if (!addBoundCommand(&acd, &binding->secondaryCommand)) goto error;
binding += 1;
}
}
{
const HotkeyEntry *hotkey = ctx->hotkeys.table;
const HotkeyEntry *end = hotkey + ctx->hotkeys.count;
while (hotkey < end) {
if (!addBoundCommand(&acd, &hotkey->pressCommand)) goto error;
if (!addBoundCommand(&acd, &hotkey->releaseCommand)) goto error;
hotkey += 1;
}
}
if (ctx->mappedKeys.count) {
if (!addCommand(&acd, BRL_CMD_BLK(PASSDOTS))) {
goto error;
}
}
}
}
if (acd.count > 1) {
qsort(acd.array, acd.count, sizeof(*acd.array), sortCommands);
int *to = acd.array;
const int *from = to;
const int *end = from + acd.count;
while (from < end) {
if ((from == acd.array) || (*from != *(from - 1))) {
if (from != to) *to = *from;
to += 1;
}
from += 1;
}
acd.count = to - acd.array;
}
{
int *newArray = realloc(acd.array, ARRAY_SIZE(newArray, acd.count));
if (newArray) acd.array = newArray;
}
*count = acd.count;
return acd.array;
error:
if (acd.array) free(acd.array);
return NULL;
}