File indexing completed on 2024-04-28 05:50:42

0001 /*
0002     This source file is part of Konsole, a terminal emulator.
0003 
0004     SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "KeyboardTranslatorReader.h"
0010 
0011 // Qt
0012 #include <QBuffer>
0013 #include <QKeySequence>
0014 #include <QRegularExpression>
0015 
0016 // KDE
0017 #include <KLocalizedString>
0018 
0019 using namespace Konsole;
0020 
0021 // each line of the keyboard translation file is one of:
0022 //
0023 // - keyboard "name"
0024 // - key KeySequence : "characters"
0025 // - key KeySequence : CommandName
0026 //
0027 // KeySequence begins with the name of the key ( taken from the Qt::Key enum )
0028 // and is followed by the keyboard modifiers and state flags ( with + or - in front
0029 // of each modifier or flag to indicate whether it is required ).  All keyboard modifiers
0030 // and flags are optional, if a particular modifier or state is not specified it is
0031 // assumed not to be a part of the sequence.  The key sequence may contain whitespace
0032 //
0033 // eg:  "key Up+Shift : scrollLineUp"
0034 //      "key PgDown-Shift : "\E[6~"
0035 //
0036 // (lines containing only whitespace are ignored, parseLine assumes that comments have
0037 // already been removed)
0038 //
0039 
0040 KeyboardTranslatorReader::KeyboardTranslatorReader(QIODevice *source)
0041     : _source(source)
0042     , _description(QString())
0043     , _nextEntry()
0044     , _hasNext(false)
0045 {
0046     // read input until we find the description
0047     while (_description.isEmpty() && !source->atEnd()) {
0048         QList<Token> tokens = tokenize(QString::fromLocal8Bit(source->readLine()));
0049         if (!tokens.isEmpty() && tokens.first().type == Token::TitleKeyword) {
0050             _description = i18n(tokens[1].text.toUtf8().constData());
0051         }
0052     }
0053     // read first entry (if any)
0054     readNext();
0055 }
0056 
0057 KeyboardTranslatorReader::~KeyboardTranslatorReader() = default;
0058 
0059 void KeyboardTranslatorReader::readNext()
0060 {
0061     // find next entry
0062     while (!_source->atEnd()) {
0063         const QList<Token> &tokens = tokenize(QString::fromLocal8Bit(_source->readLine()));
0064         if (!tokens.isEmpty() && tokens.first().type == Token::KeyKeyword) {
0065             KeyboardTranslator::States flags = KeyboardTranslator::NoState;
0066             KeyboardTranslator::States flagMask = KeyboardTranslator::NoState;
0067             Qt::KeyboardModifiers modifiers = Qt::NoModifier;
0068             Qt::KeyboardModifiers modifierMask = Qt::NoModifier;
0069 
0070             int keyCode = Qt::Key_unknown;
0071 
0072             decodeSequence(tokens[1].text.toLower(), keyCode, modifiers, modifierMask, flags, flagMask);
0073 
0074             KeyboardTranslator::Command command = KeyboardTranslator::NoCommand;
0075             QByteArray text;
0076 
0077             // get text or command
0078             if (tokens[2].type == Token::OutputText) {
0079                 text = tokens[2].text.toLocal8Bit();
0080             } else if (tokens[2].type == Token::Command) {
0081                 // identify command
0082                 if (!parseAsCommand(tokens[2].text, command)) {
0083                     qCDebug(KonsoleKeyTrDebug) << "Key" << tokens[1].text << ", Command" << tokens[2].text << "not understood. ";
0084                 }
0085             }
0086 
0087             KeyboardTranslator::Entry newEntry;
0088             newEntry.setKeyCode(keyCode);
0089             newEntry.setState(flags);
0090             newEntry.setStateMask(flagMask);
0091             newEntry.setModifiers(modifiers);
0092             newEntry.setModifierMask(modifierMask);
0093             newEntry.setText(text);
0094             newEntry.setCommand(command);
0095 
0096             _nextEntry = newEntry;
0097 
0098             _hasNext = true;
0099 
0100             return;
0101         }
0102     }
0103 
0104     _hasNext = false;
0105 }
0106 
0107 bool KeyboardTranslatorReader::parseAsCommand(const QString &text, KeyboardTranslator::Command &command)
0108 {
0109     if (text.compare(QLatin1String("erase"), Qt::CaseInsensitive) == 0) {
0110         command = KeyboardTranslator::EraseCommand;
0111     } else if (text.compare(QLatin1String("scrollpageup"), Qt::CaseInsensitive) == 0) {
0112         command = KeyboardTranslator::ScrollPageUpCommand;
0113     } else if (text.compare(QLatin1String("scrollpagedown"), Qt::CaseInsensitive) == 0) {
0114         command = KeyboardTranslator::ScrollPageDownCommand;
0115     } else if (text.compare(QLatin1String("scrolllineup"), Qt::CaseInsensitive) == 0) {
0116         command = KeyboardTranslator::ScrollLineUpCommand;
0117     } else if (text.compare(QLatin1String("scrolllinedown"), Qt::CaseInsensitive) == 0) {
0118         command = KeyboardTranslator::ScrollLineDownCommand;
0119     } else if (text.compare(QLatin1String("scrolluptotop"), Qt::CaseInsensitive) == 0) {
0120         command = KeyboardTranslator::ScrollUpToTopCommand;
0121     } else if (text.compare(QLatin1String("scrolldowntobottom"), Qt::CaseInsensitive) == 0) {
0122         command = KeyboardTranslator::ScrollDownToBottomCommand;
0123     } else if (text.compare(QLatin1String("scrollpromptup"), Qt::CaseInsensitive) == 0) {
0124         command = KeyboardTranslator::ScrollPromptUpCommand;
0125     } else if (text.compare(QLatin1String("scrollpromptdown"), Qt::CaseInsensitive) == 0) {
0126         command = KeyboardTranslator::ScrollPromptDownCommand;
0127     } else {
0128         return false;
0129     }
0130 
0131     return true;
0132 }
0133 
0134 bool KeyboardTranslatorReader::decodeSequence(const QString &text,
0135                                               int &keyCode,
0136                                               Qt::KeyboardModifiers &modifiers,
0137                                               Qt::KeyboardModifiers &modifierMask,
0138                                               KeyboardTranslator::States &flags,
0139                                               KeyboardTranslator::States &flagMask)
0140 {
0141     bool isWanted = true;
0142     QString buffer;
0143 
0144     Qt::KeyboardModifiers tempModifiers = modifiers;
0145     Qt::KeyboardModifiers tempModifierMask = modifierMask;
0146     KeyboardTranslator::States tempFlags = flags;
0147     KeyboardTranslator::States tempFlagMask = flagMask;
0148 
0149     for (int i = 0; i < text.length(); i++) {
0150         const QChar &ch = text[i];
0151         const bool isFirstLetter = (i == 0);
0152         const bool isLastLetter = (i == text.length() - 1);
0153         bool endOfItem = true;
0154         if (ch.isLetterOrNumber()) {
0155             endOfItem = false;
0156             buffer.append(ch);
0157         } else if (isFirstLetter) {
0158             buffer.append(ch);
0159         }
0160 
0161         if ((endOfItem || isLastLetter) && !buffer.isEmpty()) {
0162             Qt::KeyboardModifier itemModifier = Qt::NoModifier;
0163             int itemKeyCode = 0;
0164             KeyboardTranslator::State itemFlag = KeyboardTranslator::NoState;
0165 
0166             if (parseAsModifier(buffer, itemModifier)) {
0167                 tempModifierMask |= itemModifier;
0168 
0169                 if (isWanted) {
0170                     tempModifiers |= itemModifier;
0171                 }
0172             } else if (parseAsStateFlag(buffer, itemFlag)) {
0173                 tempFlagMask |= itemFlag;
0174 
0175                 if (isWanted) {
0176                     tempFlags |= itemFlag;
0177                 }
0178             } else if (parseAsKeyCode(buffer, itemKeyCode)) {
0179                 keyCode = itemKeyCode;
0180             } else {
0181                 qCDebug(KonsoleKeyTrDebug) << "Unable to parse key binding item:" << buffer;
0182             }
0183 
0184             buffer.clear();
0185         }
0186 
0187         // check if this is a wanted / not-wanted flag and update the
0188         // state ready for the next item
0189         if (ch == QLatin1Char('+')) {
0190             isWanted = true;
0191         } else if (ch == QLatin1Char('-')) {
0192             isWanted = false;
0193         }
0194     }
0195 
0196     modifiers = tempModifiers;
0197     modifierMask = tempModifierMask;
0198     flags = tempFlags;
0199     flagMask = tempFlagMask;
0200 
0201     return true;
0202 }
0203 
0204 bool KeyboardTranslatorReader::parseAsModifier(const QString &item, Qt::KeyboardModifier &modifier)
0205 {
0206     if (item == QLatin1String("shift")) {
0207         modifier = Qt::ShiftModifier;
0208     } else if (item == QLatin1String("ctrl") || item == QLatin1String("control")) {
0209         modifier = Qt::ControlModifier;
0210     } else if (item == QLatin1String("alt")) {
0211         modifier = Qt::AltModifier;
0212     } else if (item == QLatin1String("meta")) {
0213         modifier = Qt::MetaModifier;
0214     } else if (item == QLatin1String("keypad")) {
0215         modifier = Qt::KeypadModifier;
0216     } else {
0217         return false;
0218     }
0219 
0220     return true;
0221 }
0222 
0223 bool KeyboardTranslatorReader::parseAsStateFlag(const QString &item, KeyboardTranslator::State &flag)
0224 {
0225     if (item == QLatin1String("appcukeys") || item == QLatin1String("appcursorkeys")) {
0226         flag = KeyboardTranslator::CursorKeysState;
0227     } else if (item == QLatin1String("ansi")) {
0228         flag = KeyboardTranslator::AnsiState;
0229     } else if (item == QLatin1String("newline")) {
0230         flag = KeyboardTranslator::NewLineState;
0231     } else if (item == QLatin1String("appscreen")) {
0232         flag = KeyboardTranslator::AlternateScreenState;
0233     } else if (item == QLatin1String("anymod") || item == QLatin1String("anymodifier")) {
0234         flag = KeyboardTranslator::AnyModifierState;
0235     } else if (item == QLatin1String("appkeypad")) {
0236         flag = KeyboardTranslator::ApplicationKeypadState;
0237     } else {
0238         return false;
0239     }
0240 
0241     return true;
0242 }
0243 
0244 bool KeyboardTranslatorReader::parseAsKeyCode(const QString &item, int &keyCode)
0245 {
0246     const QKeySequence sequence = QKeySequence::fromString(item);
0247     if (!sequence.isEmpty()) {
0248         keyCode = sequence[0].toCombined();
0249         if (sequence.count() > 1) {
0250             qCDebug(KonsoleKeyTrDebug) << "Unhandled key codes in sequence: " << item;
0251         }
0252     } else {
0253         return false;
0254     }
0255 
0256     return true;
0257 }
0258 
0259 QString KeyboardTranslatorReader::description() const
0260 {
0261     return _description;
0262 }
0263 
0264 bool KeyboardTranslatorReader::hasNextEntry() const
0265 {
0266     return _hasNext;
0267 }
0268 
0269 KeyboardTranslator::Entry KeyboardTranslatorReader::createEntry(const QString &condition, const QString &result)
0270 {
0271     QString entryString(QStringLiteral("keyboard \"temporary\"\nkey "));
0272     entryString.append(condition);
0273     entryString.append(QLatin1String(" : "));
0274 
0275     // if 'result' is the name of a command then the entry result will be that command,
0276     // otherwise the result will be treated as a string to echo when the key sequence
0277     // specified by 'condition' is pressed
0278     KeyboardTranslator::Command command;
0279     if (parseAsCommand(result, command)) {
0280         entryString.append(result);
0281     } else {
0282         entryString.append(QLatin1Char('\"') + result + QLatin1Char('\"'));
0283     }
0284 
0285     QByteArray array = entryString.toUtf8();
0286     QBuffer buffer(&array);
0287     buffer.open(QIODevice::ReadOnly);
0288     KeyboardTranslatorReader reader(&buffer);
0289 
0290     KeyboardTranslator::Entry entry;
0291     if (reader.hasNextEntry()) {
0292         entry = reader.nextEntry();
0293     }
0294 
0295     return entry;
0296 }
0297 
0298 KeyboardTranslator::Entry KeyboardTranslatorReader::nextEntry()
0299 {
0300     Q_ASSERT(_hasNext);
0301     KeyboardTranslator::Entry entry = _nextEntry;
0302     readNext();
0303     return entry;
0304 }
0305 
0306 bool KeyboardTranslatorReader::parseError()
0307 {
0308     return false;
0309 }
0310 
0311 QList<KeyboardTranslatorReader::Token> KeyboardTranslatorReader::tokenize(const QString &line)
0312 {
0313     QString text = line;
0314 
0315     // remove comments
0316     bool inQuotes = false;
0317     int commentPos = -1;
0318     for (int i = text.length() - 1; i >= 0; i--) {
0319         QChar ch = text[i];
0320         if (ch == QLatin1Char('\"')) {
0321             inQuotes = !inQuotes;
0322         } else if (ch == QLatin1Char('#') && !inQuotes) {
0323             commentPos = i;
0324         }
0325     }
0326     if (commentPos != -1) {
0327         text.remove(commentPos, text.length());
0328     }
0329 
0330     text = text.simplified();
0331 
0332     QList<Token> list;
0333 
0334     if (text.isEmpty()) {
0335         return list;
0336     }
0337 
0338     // Example:
0339     // keyboard "Default (XFree 4)"
0340     static const QLatin1String prefix("keyboard");
0341     if (text.startsWith(prefix)) {
0342         text.remove(0, prefix.size()).remove(QLatin1Char('"'));
0343         text = text.simplified();
0344         if (!text.isEmpty()) {
0345             Token titleToken = {Token::TitleKeyword, QString()};
0346             Token textToken = {Token::TitleText, text};
0347             list << titleToken << textToken;
0348         }
0349 
0350         return list;
0351     }
0352 
0353     // Examples:
0354     // key Enter-NewLine                 : "\r"
0355     // key Home        -AnyMod-AppCuKeys : "\E[H"
0356     static const QRegularExpression key(QStringLiteral(R"(key\s+(.+?)\s*:\s*(\"(.*)\"|\w+))"));
0357 
0358     QRegularExpressionMatch keyMatch(key.match(text));
0359     if (!keyMatch.hasMatch()) {
0360         qCDebug(KonsoleKeyTrDebug) << "Line in keyboard translator file could not be parsed:" << text;
0361         return list;
0362     }
0363 
0364     Token keyToken = {Token::KeyKeyword, QString()};
0365     QString sequenceTokenString = keyMatch.captured(1);
0366     Token sequenceToken = {Token::KeySequence, sequenceTokenString.remove(QLatin1Char(' '))};
0367 
0368     list << keyToken << sequenceToken;
0369 
0370     // capturedTexts().at(3) is the output string
0371     const QStringView outText = keyMatch.capturedView(3);
0372     if (!outText.isEmpty()) {
0373         Token outputToken = {Token::OutputText, outText.toString()};
0374         list << outputToken;
0375     } else {
0376         // capturedTexts().at(2) is a command
0377         Token commandToken = {Token::Command, keyMatch.captured(2)};
0378         list << commandToken;
0379     }
0380 
0381     return list;
0382 }