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 }