blob: 1c1ae8ff169481ca3c67113c0a2be944974a8b8e [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 <ctype.h>
#include "log.h"
#include "alert.h"
#include "cmd_queue.h"
#include "cmd_utils.h"
#include "cmd_clipboard.h"
#include "clipboard.h"
#include "brl_cmds.h"
#include "scr.h"
#include "routing.h"
#include "file.h"
#include "datafile.h"
#include "utf8.h"
#include "core.h"
typedef struct {
ClipboardObject *clipboard;
struct {
int column;
int row;
int offset;
} begin;
} ClipboardCommandData;
static wchar_t *
cpbReadScreen (ClipboardCommandData *ccd, size_t *length, int fromColumn, int fromRow, int toColumn, int toRow) {
wchar_t *newBuffer = NULL;
int columns = toColumn - fromColumn + 1;
int rows = toRow - fromRow + 1;
if ((columns >= 1) && (rows >= 1) && (ccd->begin.offset >= 0)) {
wchar_t fromBuffer[rows * columns];
if (readScreenText(fromColumn, fromRow, columns, rows, fromBuffer)) {
wchar_t toBuffer[rows * (columns + 1)];
wchar_t *toAddress = toBuffer;
const wchar_t *fromAddress = fromBuffer;
int row;
for (row=fromRow; row<=toRow; row+=1) {
int column;
for (column=fromColumn; column<=toColumn; column+=1) {
wchar_t character = *fromAddress++;
if (iswcntrl(character) || iswspace(character)) character = WC_C(' ');
*toAddress++ = character;
}
if (row != toRow) *toAddress++ = WC_C('\r');
}
/* make a new permanent buffer of just the right size */
{
size_t newLength = toAddress - toBuffer;
if ((newBuffer = allocateCharacters(newLength))) {
wmemcpy(newBuffer, toBuffer, (*length = newLength));
}
}
}
}
return newBuffer;
}
static int
cpbEndOperation (ClipboardCommandData *ccd, const wchar_t *characters, size_t length,
int insertCR) {
lockMainClipboard();
if (insertCR && ccd->begin.offset >= 1) {
size_t length;
const wchar_t *characters = getClipboardContent(ccd->clipboard, &length);
if (length > ccd->begin.offset) length = ccd->begin.offset;
while (length > 0) {
size_t last = length - 1;
if (characters[last] == WC_C('\r')) insertCR = 0;
if (characters[last] != WC_C(' ')) break;
length = last;
}
ccd->begin.offset = length;
}
if (ccd->begin.offset <= 0) insertCR = 0;
int truncated = truncateClipboardContent(ccd->clipboard, ccd->begin.offset);
if (insertCR) appendClipboardContent(ccd->clipboard, &(wchar_t){WC_C('\r')}, 1);
int appended = appendClipboardContent(ccd->clipboard, characters, length);
unlockMainClipboard();
if (truncated || appended) onMainClipboardUpdated();
if (!appended) return 0;
alert(ALERT_CLIPBOARD_END);
return 1;
}
static void
cpbBeginOperation (ClipboardCommandData *ccd, int column, int row) {
ccd->begin.column = column;
ccd->begin.row = row;
lockMainClipboard();
ccd->begin.offset = getClipboardContentLength(ccd->clipboard);
unlockMainClipboard();
alert(ALERT_CLIPBOARD_BEGIN);
}
static int
cpbRectangularCopy (ClipboardCommandData *ccd, int column, int row) {
int copied = 0;
size_t length;
wchar_t *buffer = cpbReadScreen(ccd, &length, ccd->begin.column, ccd->begin.row, column, row);
if (buffer) {
{
const wchar_t *from = buffer;
const wchar_t *end = from + length;
wchar_t *to = buffer;
int spaces = 0;
while (from != end) {
wchar_t character = *from++;
switch (character) {
case WC_C(' '):
spaces += 1;
continue;
case WC_C('\r'):
spaces = 0;
break;
default:
break;
}
while (spaces) {
*to++ = WC_C(' ');
spaces -= 1;
}
*to++ = character;
}
length = to - buffer;
}
if (cpbEndOperation(ccd, buffer, length, 1)) copied = 1;
free(buffer);
}
return copied;
}
static int
cpbLinearCopy (ClipboardCommandData *ccd, int column, int row) {
int copied = 0;
ScreenDescription screen;
describeScreen(&screen);
{
int rightColumn = screen.cols - 1;
size_t length;
wchar_t *buffer = cpbReadScreen(ccd, &length, 0, ccd->begin.row, rightColumn, row);
if (buffer) {
if (column < rightColumn) {
wchar_t *start = buffer + length;
while (start != buffer) {
if (*--start == WC_C('\r')) {
start += 1;
break;
}
}
{
int adjustment = (column + 1) - (buffer + length - start);
if (adjustment < 0) length += adjustment;
}
}
if (ccd->begin.column) {
wchar_t *start = wmemchr(buffer, WC_C('\r'), length);
if (!start) start = buffer + length;
if ((start - buffer) > ccd->begin.column) start = buffer + ccd->begin.column;
if (start != buffer) wmemmove(buffer, start, (length -= start - buffer));
}
{
const wchar_t *from = buffer;
const wchar_t *end = from + length;
wchar_t *to = buffer;
int spaces = 0;
int newlines = 0;
while (from != end) {
wchar_t character = *from++;
switch (character) {
case WC_C(' '):
spaces += 1;
continue;
case WC_C('\r'):
newlines += 1;
continue;
default:
break;
}
if (newlines) {
if ((newlines > 1) || (spaces > 0)) spaces = 1;
newlines = 0;
}
while (spaces) {
*to++ = WC_C(' ');
spaces -= 1;
}
*to++ = character;
}
if (spaces || newlines) *to++ = WC_C(' ');
length = to - buffer;
}
if (cpbEndOperation(ccd, buffer, length, 0)) copied = 1;
free(buffer);
}
}
return copied;
}
static int
pasteCharacters (const wchar_t *characters, size_t count) {
if (!characters) return 0;
if (!count) return 0;
if (!isMainScreen()) return 0;
if (isRouting()) return 0;
{
unsigned int i;
for (i=0; i<count; i+=1) {
if (!insertScreenKey(characters[i])) return 0;
}
}
return 1;
}
static int
cpbPaste (ClipboardCommandData *ccd, unsigned int index) {
int pasted;
lockMainClipboard();
const wchar_t *characters;
size_t length;
if (index) {
characters = getClipboardHistory(ccd->clipboard, index-1, &length);
} else {
characters = getClipboardContent(ccd->clipboard, &length);
}
while (length > 0) {
size_t last = length - 1;
if (characters[last] != WC_C(' ')) break;
length = last;
}
pasted = pasteCharacters(characters, length);
unlockMainClipboard();
return pasted;
}
static FILE *
cpbOpenFile (const char *mode) {
const char *file = "clipboard";
char *path = makeUpdatablePath(file);
if (path) {
FILE *stream = openDataFile(path, mode, 0);
free(path);
path = NULL;
if (stream) return stream;
}
return NULL;
}
static int
cpbSave (ClipboardCommandData *ccd) {
int ok = 0;
lockMainClipboard();
size_t length;
const wchar_t *characters = getClipboardContent(ccd->clipboard, &length);
if (length > 0) {
FILE *stream = cpbOpenFile("w");
if (stream) {
if (writeUtf8Characters(stream, characters, length)) {
ok = 1;
}
if (fclose(stream) == EOF) {
logSystemError("fclose");
ok = 0;
}
}
}
unlockMainClipboard();
return ok;
}
static int
cpbRestore (ClipboardCommandData *ccd) {
int ok = 0;
FILE *stream = cpbOpenFile("r");
if (stream) {
int wasUpdated = 0;
lockMainClipboard();
{
int isClear = 0;
if (isClipboardEmpty(ccd->clipboard)) {
isClear = 1;
} else if (clearClipboardContent(ccd->clipboard)) {
isClear = 1;
wasUpdated = 1;
}
if (isClear) {
ok = 1;
size_t size = 0X1000;
char buffer[size];
size_t length = 0;
do {
size_t count = fread(&buffer[length], 1, (size - length), stream);
int done = (length += count) < size;
if (ferror(stream)) {
logSystemError("fread");
ok = 0;
} else {
const char *next = buffer;
size_t left = length;
while (left > 0) {
const char *start = next;
wint_t wi = convertUtf8ToWchar(&next, &left);
if (wi == WEOF) {
length = next - start;
if (left > 0) {
logBytes(LOG_ERR, "invalid UTF-8 character", start, length);
ok = 0;
break;
}
memmove(buffer, start, length);
} else {
wchar_t wc = wi;
if (appendClipboardContent(ccd->clipboard, &wc, 1)) {
wasUpdated = 1;
} else {
ok = 0;
break;
}
}
}
}
if (done) break;
} while (ok);
}
}
unlockMainClipboard();
if (fclose(stream) == EOF) {
logSystemError("fclose");
ok = 0;
}
if (wasUpdated) onMainClipboardUpdated();
}
return ok;
}
static int
findCharacters (const wchar_t **address, size_t *length, const wchar_t *characters, size_t count) {
const wchar_t *ptr = *address;
size_t len = *length;
while (count <= len) {
const wchar_t *next = wmemchr(ptr, *characters, len);
if (!next) break;
len -= next - ptr;
if (wmemcmp((ptr = next), characters, count) == 0) {
*address = ptr;
*length = len;
return 1;
}
++ptr, --len;
}
return 0;
}
static int
handleClipboardCommands (int command, void *data) {
ClipboardCommandData *ccd = data;
switch (command & BRL_MSK_CMD) {
case BRL_CMD_PASTE:
if (!cpbPaste(ccd, 0)) alert(ALERT_COMMAND_REJECTED);
break;
case BRL_CMD_CLIP_SAVE:
alert(cpbSave(ccd)? ALERT_COMMAND_DONE: ALERT_COMMAND_REJECTED);
break;
case BRL_CMD_CLIP_RESTORE:
alert(cpbRestore(ccd)? ALERT_COMMAND_DONE: ALERT_COMMAND_REJECTED);
break;
{
int increment;
const wchar_t *cpbBuffer;
size_t cpbLength;
case BRL_CMD_PRSEARCH:
increment = -1;
goto doSearch;
case BRL_CMD_NXSEARCH:
increment = 1;
goto doSearch;
doSearch:
lockMainClipboard();
if ((cpbBuffer = getClipboardContent(ccd->clipboard, &cpbLength))) {
int found = 0;
size_t count = cpbLength;
if (count <= scr.cols) {
int line = ses->winy;
wchar_t buffer[scr.cols];
wchar_t characters[count];
{
unsigned int i;
for (i=0; i<count; i+=1) characters[i] = towlower(cpbBuffer[i]);
}
while ((line >= 0) && (line <= (int)(scr.rows - brl.textRows))) {
const wchar_t *address = buffer;
size_t length = scr.cols;
readScreenText(0, line, length, 1, buffer);
{
for (size_t i=0; i<length; i+=1) buffer[i] = towlower(buffer[i]);
}
if (line == ses->winy) {
if (increment < 0) {
int end = ses->winx + count - 1;
if (end < length) length = end;
} else {
int start = ses->winx + textCount;
if (start > length) start = length;
address += start;
length -= start;
}
}
if (findCharacters(&address, &length, characters, count)) {
if (increment < 0) {
while (findCharacters(&address, &length, characters, count)) {
++address, --length;
}
}
ses->winy = line;
ses->winx = (address - buffer) / textCount * textCount;
found = 1;
break;
}
line += increment;
}
}
if (!found) alert(ALERT_BOUNCE);
} else {
alert(ALERT_COMMAND_REJECTED);
}
unlockMainClipboard();
break;
}
default: {
int arg = command & BRL_MSK_ARG;
int ext = BRL_CODE_GET(EXT, command);
switch (command & BRL_MSK_BLK) {
{
int clear;
int column, row;
case BRL_CMD_BLK(CLIP_NEW):
clear = 1;
goto doClipBegin;
case BRL_CMD_BLK(CLIP_ADD):
clear = 0;
goto doClipBegin;
doClipBegin:
if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
if (clear) clearClipboardContent(ccd->clipboard);
cpbBeginOperation(ccd, column, row);
} else {
alert(ALERT_COMMAND_REJECTED);
}
break;
}
case BRL_CMD_BLK(COPY_RECT): {
int column, row;
if (getCharacterCoordinates(arg, &row, NULL, &column, 1))
if (cpbRectangularCopy(ccd, column, row))
break;
alert(ALERT_COMMAND_REJECTED);
break;
}
case BRL_CMD_BLK(COPY_LINE): {
int column, row;
if (getCharacterCoordinates(arg, &row, NULL, &column, 1))
if (cpbLinearCopy(ccd, column, row))
break;
alert(ALERT_COMMAND_REJECTED);
break;
}
{
int clear;
case BRL_CMD_BLK(CLIP_COPY):
clear = 1;
goto doCopy;
case BRL_CMD_BLK(CLIP_APPEND):
clear = 0;
goto doCopy;
doCopy:
if (ext > arg) {
int column1, row1;
if (getCharacterCoordinates(arg, &row1, &column1, NULL, 0)) {
int column2, row2;
if (getCharacterCoordinates(ext, &row2, NULL, &column2, 1)) {
if (clear) clearClipboardContent(ccd->clipboard);
cpbBeginOperation(ccd, column1, row1);
if (cpbLinearCopy(ccd, column2, row2)) break;
}
}
}
alert(ALERT_COMMAND_REJECTED);
break;
}
case BRL_CMD_BLK(PASTE_HISTORY):
if (!cpbPaste(ccd, arg)) alert(ALERT_COMMAND_REJECTED);
break;
default:
return 0;
}
break;
}
}
return 1;
}
static void
destroyClipboardCommandData (void *data) {
ClipboardCommandData *ccd = data;
free(ccd);
}
int
addClipboardCommands (void) {
ClipboardCommandData *ccd;
if ((ccd = malloc(sizeof(*ccd)))) {
memset(ccd, 0, sizeof(*ccd));
ccd->clipboard = getMainClipboard();
ccd->begin.column = 0;
ccd->begin.row = 0;
ccd->begin.offset = -1;
if (pushCommandHandler("clipboard", KTB_CTX_DEFAULT,
handleClipboardCommands, destroyClipboardCommandData, ccd)) {
return 1;
}
free(ccd);
} else {
logMallocError();
}
return 0;
}