[C++, Разработка под Linux, Разработка под MacOS, Разработка под Windows] Пишем автодополнение для ваших CLI проектов
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Приветствие
Всем привет! Хочу поделиться своим опытом написания кроссплатформенного проекта на C++ для интеграции автодополнения в CLI приложения, усаживайтесь поудобнее.
Формулировка задания
- Приложение должно работать на Linux, macOS, Windows
- Необходима возможность задавать правила для автодополнения
- Предусмотреть наличие опечаток
- Предусмотреть смену подсказок стрелками клавиатуры
Приготовления
Сразу предупрежу, использовать будем C++17
Предлагаю перейти к делу. Очевидно, так как наш проект кроссплатформенный, необходимо написать простенький макрос для определения текущей платформы.
#if defined(_WIN32) || defined(_WIN64)
#define OS_WINDOWS
#elif defined(__APPLE__) || defined(__unix__) || defined(__unix)
#define OS_POSIX
#else
#error unsupported platform
#endif
Также сделаем небольшую заготовку:
#if defined(OS_WINDOWS)
#define ENTER 13
#define BACKSPACE 8
#define CTRL_C 3
#define LEFT 75
#define RIGHT 77
#define DEL 83
#define UP 72
#define DOWN 80
#define SPACE 32
#elif defined(OS_POSIX)
#define ENTER 10
#define BACKSPACE 127
#define SPACE 32
#define LEFT 68
#define RIGHT 67
#define UP 65
#define DOWN 66
#define DEL 51
#endif
#define TAB 9
Так как мы нацелены на CLI проекты, и терминалы Linux и macOS имеют одинаковый API, объединим их в один define OS_POSIX. Windows, как всегда, стоит в стороне, вынесем для нее отдельный define OS_WINDOWS.
Следующим шагом мы должны понять, как будут выглядеть автодополнения. С самого начала я был вдохновлен Redis CLI, поэтому будем просто выводить посказки другим нейтральным цветом. Но это не помешает нам в итоге использовать любые цвета.
Следовательно, требуется написать функцию установки нужного цвета для вывода в консоль:
/**
* Sets the console color.
*
* @param color System code of target color.
* @return Input parameter os.
*/
#if defined(OS_WINDOWS)
std::string set_console_color(uint16_t color) {
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
return "";
#elif defined(OS_POSIX)
std::string set_console_color(std::string color) {
return "\033[" + color + "m";
#endif
}
Опять таки из-за разницы API приходится искать компромисс, будем всегда возвращать строку для того, чтобы можно было использовать функцию после оператора вывода << для повышения читаемости кода.
Для тех, кому интересно, как именно работает API для цвета в Posix и Windows, и какие цветовые профили вообще бывают, предлагаю почитать ответы добрых людей на stackoverflow:
- Windows: https://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c#answer-4053879
- Posix: https://stackoverflow.com/questions/2616906/how-do-i-output-coloured-text-to-a-linux-terminal#answer-45300654
Так как нам придется постоянно перерисовывать строку из-за подсказок, необходимо написать функцию для "стирания" строки.
/**
* Get count of terminal cols.
*
* @return Width of terminal.
*/
#if defined(OS_WINDOWS)
size_t console_width() {
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
short width = --info.dwSize.X;
return size_t((width < 0) ? 0 : width);
}
#endif
/**
* Clear terminal line.
*
* @param os Output stream.
* @return input parameter os.
*/
std::ostream& clear_line(std::ostream& os) {
#if defined(OS_WINDOWS)
size_t width = console_width();
os << '\r' << std::string(width, ' ');
#elif defined(OS_POSIX)
std::cout << "\033[2K";
#endif
return os;
}
На Posix платформах все просто, достаточно вывести в консоль \033[2K, но естественно в Windows нет аналогов, конкретно я не смог найти, приходится писать свою реализацию.
Осталось понять, как осуществлять ввод символов пользователем. Обычный cin явно не подойдет, в процессе считывания не получится выводить предсказания.
Тут приходит на ум функция _getch(), доступная в Windows, которая получает код символа нажатой клавиши на клавиатуре — это именно то, что нам надо. Но в этот раз с Posix платформами все плохо, увы, но придется писать свою реализацию.
#if defined(OS_POSIX)
/**
* Read key without press ENTER.
*
* @return Code of key on keyboard.
*/
int _getch() {
int ch;
struct termios old_termios, new_termios;
tcgetattr( STDIN_FILENO, &old_termios );
new_termios = old_termios;
new_termios.c_lflag &= ~(ICANON | ECHO );
tcsetattr( STDIN_FILENO, TCSANOW, &new_termios );
ch = getchar();
tcsetattr( STDIN_FILENO, TCSANOW, &old_termios );
return ch;
}
#endif
Правила автодополнения
Отлично. Теперь придумаем, как мы будем задавать правила для автодополнения. Предлагаю получать их из текстового файла следующей структуры:
git
config
--global
user.name
"[name]"
user.email
"[email]"
user.name
"[name]"
user.email
"[email]"
init
[repository name]
clone
[url]
Идея такая. За каждым словом могут идти слова на расстоянии 1 табуляции от него. Т.е. после слова git могут идти слова config, init и global. После слова config могут идти слова --global, user.name и user.email и т.д. Также введем возможность указывать опциональные слова, в моем случае это слова внутри символов [] (вместо этих слов пользователь должен вводить свои данные).
Хранить правила будем в ассоциативном массиве, где ключ будет выступать строкой, а значения — вектор слов, которые могут идти после ключа-строки.
typedef std::map<std::string, std::vector<std::string>> Dictionary;
Давайте напишем функцию для парсинга файла с правилами.
/**
* Parse config file to dictionary.
*
* @param file_path The path to the configuration file.
* @return Tuple of dictionary with autocomplete rules, status of parsing and message.
*/
std::tuple<Dictionary, bool, std::string>
parse_config_file(const std::string& file_path) {
Dictionary dict; // Словарь с правилами автозаполнения
std::map<int, std::string> // Массив для запоминания корневого слова
root_words_by_tabsize; // для определенной длины табуляции
std::string line; // Строка для чтения
std::string token; // Полученное слово из строки
std::string root_word; // Корневое слово для вставки в словарь как ключ
long tab_size = 0; // Базовая длина табуляции (пробелов)
long tab_count = 0; // Колличество табуляций в строке
// Открытие файла конфигураций
std::ifstream config_file(file_path);
// Возвращаем сообщение об ошибке, если файл не был открыт
if (!config_file.is_open()) {
return std::make_tuple(
dict,
false,
"Error! Can't open " + file_path + " file."
);
}
// Считываем все строки
while (std::getline(config_file, line)) {
// Пропускаем строку если она пустая
if (line.empty()) {
continue;
}
// Если в файле обнаружен символ табуляции, возвращаем сообщение о ошибке
if (std::count(line.begin(), line.end(), '\t') != 0) {
return std::make_tuple(
dict,
false,
"Error! Use a sequence of spaces instead of a tab character."
);
}
// Получение количества пробелов в начале строки
auto spaces = std::count(
line.begin(),
line.begin() + line.find_first_not_of(" "),
' '
);
// Устанавливаем базовый размер табуляции, если
// была найдена строка с пробелами в начале
if (spaces != 0 && tab_size == 0) {
tab_size = spaces;
}
// Получаем слово из строки
token = trim(line);
// Проверка длины табуляции
if (tab_size != 0 && spaces % tab_size != 0) {
return std::make_tuple(
dict,
false,
"Error! Tab length error was made.\nPossibly in line: " + line
);
}
// Получаем количество табуляций
tab_count = (tab_size == 0) ? 0 : (spaces / tab_size);
// Запоминаем корневое слово для заданного количества табуляций
root_words_by_tabsize[tab_count] = token;
// Получаем корневое слово для текущего токена
root_word = (tab_count == 0) ? "" : root_words_by_tabsize[tab_count - 1];
// Вставка токена в словарь, если его там нет
if (std::count(dict[root_word].begin(), dict[root_word].end(), token) == 0) {
dict[root_word].push_back(token);
}
}
// Закрываем файл
config_file.close();
// Если все ОК возвращаем готовый словарь
return std::make_tuple(
dict,
true,
"Success. The rule dictionary has been created."
);
}
Разберемся с накопившимися вопросами.
- Функция возвращает кортеж, так как по моему использование исключений не очень удачный вариант.
- Почему использование символа \t в файле запрещено? Потому что будем привыкать к хорошей практике использования последовательности пробелов вместо табуляции.
- Откуда взялась функция trim, и что она делает? Сейчас покажу ее простую реализацию.
/**
* Remove extra spaces to the left and right of the string.
*
* @param str Source string.
* @return Line without spaces on the left and right.
*/
std::string trim(std::string_view str) {
std::string result(str);
result.erase(0, result.find_first_not_of(" \n\r\t"));
result.erase(result.find_last_not_of(" \n\r\t") + 1);
return result;
}
Функция просто отрезает лишнее пространство слева и справа у строки
Автодополнение
Хорошо. У нас есть словарь с правилами, а что дальше? Осталось сделать само автодополнение.
Представим, что пользователь вводит что-то с клавиатуры. Что мы имеем? Одно или несколько введенных слов.
Давайте научимся получать последнее слово из строки.
/**
* Get the position of the beginning of the last word.
*
* @param str String with words.
* @return Position of the beginning of the last word.
*/
size_t get_last_word_pos(std::string_view str) {
// Вернуть 0 если строка состоит только из пробелов
if (std::count(str.begin(), str.end(), ' ') == str.length()) {
return 0;
}
// Получаем позицию последнего пробела
auto last_word_pos = str.rfind(' ');
// Вернуть 0, если пробел не найден, иначе вернуть позицию + 1
return (last_word_pos == std::string::npos) ? 0 : last_word_pos + 1;
}
/**
* Get the last word in string.
*
* @param str String with words.
* @return Pair Position of the beginning of the
* last word and the last word in string.
*/
std::pair<size_t, std::string> get_last_word(std::string_view str) {
// Поулчаем позицию
size_t last_word_pos = get_last_word_pos(str);
// Получаем последнее слово из строки
auto last_word = str.substr(last_word_pos);
// Возвращаем пару из слова и позиции слова в строке (для удобства)
return std::make_pair(last_word_pos, last_word.data());
}
Но давайте вспомним, чтобы предугадать, что хочет пользователь, нам надо знать не только последнее слово, которое мы пытаемся угадать, но и то, что шло до него.
Давайте научимся получать предпоследнее слово из строки.
// Не использовал std::min из-за странного
// поведения MSVC компилятора
/**
* Get the minimum of two numbers.
*
* @param a First value.
* @param b Second value.
* @return Minimum of two numbers.
*/
size_t min_of(size_t a, size_t b) {
return (a < b) ? a : b;
}
/**
* Get the penultimate words.
*
* @param str String with words.
* @return Pair Position of the beginning of the penultimate
* word and the penultimate word in string.
*/
std::pair<size_t, std::string> get_penult_word(std::string_view str) {
// Находим правую границу поиска
size_t end_pos = min_of(str.find_last_not_of(' ') + 2, str.length());
// Получаем позицию начала последнего слова
size_t last_word = get_last_word_pos(str.substr(0, end_pos));
size_t penult_word_pos = 0;
std::string penult_word = "";
// Находим предпоследнее слово если позиция
// начала последнего была найдена
if (last_word != 0) {
// Находим начало предпоследнего слова
penult_word_pos = str.find_last_of(' ', last_word - 2);
// Находим предпоследнее слово если позиция начала найдена
if (penult_word_pos != std::string::npos) {
penult_word = str.substr(penult_word_pos, last_word - penult_word_pos - 1);
}
// Иначе предпоследнее слово - все, что дошло до последнего слова
else {
penult_word = str.substr(0, last_word - 1);
}
}
// Обрезаем строку
penult_word = trim(penult_word);
// Возвращаем пару из позиции и слова (для удобства)
return std::make_pair(penult_word_pos, penult_word);
}
Нахождение слов для автодополнения
Что же мы забыли? Функцию для нахождения слов, которые начинаются также, как и последнее слово в строке.
/**
* Find strings in vector starts with substring.
*
* @param substr String with which the word should begin.
* @param penult_word Penultimate word in user-entered line.
* @param dict Vector of words.
* @param optional_brackets String with symbols for optional values.
* @return Vector with words starts with substring.
*/
std::vector<std::string>
words_starts_with(std::string_view substr, std::string_view penult_word,
Dictionary& dict, std::string_view optional_brackets) {
std::vector<std::string> result;
// Выход если нет ключа равного penult_word или
// substr имеет символы для опциональных слов
if (!dict.count(penult_word.data()) ||
substr.find_first_of(optional_brackets) != std::string::npos)
{
return result;
}
// Возвращаем все слова, которые могут быть
// после last_word, если substr пуста
if (substr.empty()) {
return dict[penult_word.data()];
}
// Находим строки, начинающиеся с substr
std::vector<std::string> candidates_list = dict[penult_word.data()];
for (size_t i = 0 ; i < candidates_list.size(); i++) {
if (candidates_list[i].find(substr) == 0) {
result.push_back(dict[penult_word.data()][i]);
}
}
return result;
}
А что по поводу проверки орфографии? Мы же хотели ее добавить? Давайте сделаем это.
/**
* Find strings in vector similar to a substring (max 1 error).
*
* @param substr String with which the word should begin.
* @param penult_word Penultimate word in user-entered line.
* @param dict Vector of words.
* @param optional_brackets String with symbols for optional values.
* @return Vector with words similar to a substring.
*/
std::vector<std::string>
words_similar_to(std::string_view substr, std::string_view penult_word,
Dictionary& dict, std::string_view optional_brackets) {
std::vector<std::string> result;
// Выход, если строка пустая
if (substr.empty()) {
return result;
}
std::vector<std::string> candidates_list = dict[penult_word.data()];
for (size_t i = 0 ; i < candidates_list.size(); i++) {
int errors = 0;
// Получаем кандидата
std::string candidate = candidates_list[i];
// Посимвольная проверка кандидата
for (size_t j = 0; j < substr.length(); j++) {
// Пропуск, если кандидат содержит символы для опциональных слов
if (optional_brackets.find_first_of(candidate[j]) != std::string::npos) {
errors = 2;
break;
}
if (substr[j] != candidate[j]) {
errors += 1;
}
if (errors > 1) {
break;
}
}
// Добавляем кандидата, если максимум одна ошибка
if (errors <= 1) {
result.push_back(candidate);
}
}
return result;
}
Теперь у нас есть все, чтобы предсказать слово по введенной пользователем строке.
Давайте решим эту задачу.
/**
* Get the word-prediction by the index.
*
* @param buffer String with user input.
* @param dict Dictionary with rules.
* @param number Index of word-prediction.
* @param optional_brackets String with symbols for optional values.
* @return Tuple of word-prediction, phrase for output, substring of buffer
* preceding before phrase, start position of last word.
*/
std::tuple<std::string, std::string, std::string, size_t>
get_prediction(std::string_view buffer, Dictionary& dict, size_t number,
std::string_view optional_brackets) {
// Получаем информацию о последнем слове
auto [last_word_pos, last_word] = get_last_word(buffer);
// Получаем информацию о предпоследнем слове
auto [_, penult_word] = get_penult_word(buffer);
std::string prediction; // предсказание
std::string phrase; // фраза для вывода
std::string prefix; // подстрока буфера, предшествующая фразе
// Ищем предсказания
std::vector<std::string> starts_with = words_starts_with(
last_word, penult_word, dict, optional_brackets
);
// Устанавливаем значения, если предсказания были найдены
if (!starts_with.empty()) {
prediction = starts_with[number % starts_with.size()];
phrase = prediction;
prefix = buffer.substr(0, last_word_pos);
}
// Если слова не были найдены
else {
// Ищем слова с учетом орфографии
std::vector<std::string> similar = words_similar_to(
last_word, penult_word, dict, optional_brackets
);
// Устанавливаем значения, если предсказания были найдены
if (!similar.empty()) {
prediction = similar[number % similar.size()];
phrase = " maybe you mean " + prediction + "?";
prefix = buffer;
}
}
// Возвращаем необходимые данные
return std::make_tuple(prediction, phrase, prefix, last_word_pos);
}
Ввод пользователя с клавиатуры
Осталось одно из самых сложных заданий. Написать саму функцию ввода с клавиатуры.
/**
* Gets current terminal cursor position.
*
* @return Y position of terminal cursor.
*/
short cursor_y_pos() {
#if defined(OS_WINDOWS)
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
return info.dwCursorPosition.Y;
#elif defined(OS_POSIX)
struct termios term, restore;
char ch, buf[30] = {0};
short i = 0, pow = 1, y = 0;
tcgetattr(0, &term);
tcgetattr(0, &restore);
term.c_lflag &= ~(ICANON|ECHO);
tcsetattr(0, TCSANOW, &term);
write(1, "\033[6n", 4);
for (ch = 0; ch != 'R'; i++) {
read(0, &ch, 1);
buf[i] = ch;
}
i -= 2;
while (buf[i] != ';') {
i -= 1;
}
i -= 1;
while (buf[i] != '[') {
y = y + ( buf[i] - '0' ) * pow;
pow *= 10;
i -= 1;
}
tcsetattr(0, TCSANOW, &restore);
return y;
#endif
}
/**
* Move terminal cursor at position x and y.
*
* @param x X position to move.
* @param x Y position to move.
* @return Void.
*/
void goto_xy(short x, short y) {
#if defined(OS_WINDOWS)
COORD xy {--x, y};
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy);
#elif defined(OS_POSIX)
printf("\033[%d;%dH", y, x);
#endif
}
/**
* Printing user input with prompts.
*
* @param buffer String - User input.
* @param dict Vector of words.
* @param line_title Line title of CLI when entering command.
* @param number Hint number.
* @param optional_brackets String with symbols for optional values.
* @param title_color System code of title color (line title color).
* @param predict_color System code of predict color (prediction color).
* @param default_color System code of default color (user input color).
* @return Void.
*/
#if defined(OS_WINDOWS)
void print_with_prompts(std::string_view buffer, Dictionary& dict,
std::string_view line_title, size_t number,
std::string_view optional_brackets,
uint16_t title_color, uint16_t predict_color,
uint16_t default_color) {
#else
void print_with_prompts(std::string_view buffer, Dictionary& dict,
std::string_view line_title, size_t number,
std::string_view optional_brackets,
std::string title_color, std::string predict_color,
std::string default_color) {
#endif
// Получить прогнозируемую фразу и часть буфера, предшествующую фразе
auto [_, phrase, prefix, __] =
get_prediction(buffer, dict, number, optional_brackets);
std::string delimiter = line_title.empty() ? "" : " ";
std::cout << clear_line;
std::cout << '\r' << set_console_color(title_color) << line_title
<< set_console_color(default_color) << delimiter << prefix
<< set_console_color(predict_color) << phrase;
std::cout << '\r' << set_console_color(title_color) << line_title
<< set_console_color(default_color) << delimiter << buffer;
}
/**
* Reading user input with autocomplete.
*
* @param dict Vector of words.
* @param optional_brackets String with symbols for optional values.
* @param title_color System code of title color (line title color).
* @param predict_color System code of predict color (prediction color).
* @param default_color System code of default color (user input color).
* @return User input.
*/
#if defined(OS_WINDOWS)
std::string input(Dictionary& dict, std::string_view line_title,
std::string_view optional_brackets, uint16_t title_color,
uint16_t predict_color, uint16_t default_color) {
#else
std::string input(Dictionary& dict, std::string_view line_title,
std::string_view optional_brackets, std::string title_color,
std::string predict_color, std::string default_color) {
#endif
std::string buffer; // Буфер
size_t offset = 0; // Смещение курсора от конца буфера
size_t number = 0; // Номер (индекс) посдказки, для переключения
short y = cursor_y_pos(); // Получаем позицию курсора по оси Y в терминале
// Игнорируемые символы
#if defined(OS_WINDOWS)
std::vector<int> ignore_keys({1, 2, 19, 24, 26});
#elif defined(OS_POSIX)
std::vector<int> ignore_keys({1, 2, 4, 24});
#endif
while (true) {
// Выводим строку пользователя с предсказанием
print_with_prompts(buffer, dict, line_title, number, optional_brackets,
title_color, predict_color, default_color);
// Перемещаем курсор в нужную позицию
short x = short(
buffer.length() + line_title.length() + !line_title.empty() + 1 - offset
);
goto_xy(x, y);
// Считываем очередной символ
int ch = _getch();
// Возвращаем буфер, если нажат Enter
if (ch == ENTER) {
return buffer;
}
// Обработка выхода из CLI в Windows
#if defined(OS_WINDOWS)
else if (ch == CTRL_C) {
exit(0);
}
#endif
// Изменение буфера при нажатии BACKSPACE
else if (ch == BACKSPACE) {
if (!buffer.empty() && buffer.length() - offset >= 1) {
buffer.erase(buffer.length() - offset - 1, 1);
}
}
// Применение подсказки при нажатии TAB
else if (ch == TAB) {
// Получаем необходимую информацию
auto [prediction, _, __, last_word_pos] =
get_prediction(buffer, dict, number, optional_brackets);
// Дописываем предсказание, если имеется
if (!prediction.empty() &&
prediction.find_first_of(optional_brackets) == std::string::npos) {
buffer = buffer.substr(0, last_word_pos) + prediction + " ";
}
// Очищаем индекс подсказки и смещение
offset = 0;
number = 0;
}
// Обработка стрелок
#if defined(OS_WINDOWS)
else if (ch == 0 || ch == 224)
#elif defined(OS_POSIX)
else if (ch == 27 && _getch() == 91)
#endif
switch (_getch()) {
case LEFT:
// Увеличьте смещение, если нажата левая клавиша
offset = (offset < buffer.length())
? offset + 1
: buffer.length();
break;
case RIGHT:
// Уменьшить смещение, если нажата правая клавиша
offset = (offset > 0) ? offset - 1 : 0;
break;
case UP:
// Увеличить индекс подсказки
number = number + 1;
std::cout << clear_line;
break;
case DOWN:
// Уменьшить индекс подсказки
number = number - 1;
std::cout << clear_line;
break;
case DEL:
// Изменение буфера, при нажатии DELETE
#if defined(OS_POSIX)
if (_getch() == 126)
#endif
{
if (!buffer.empty() && offset != 0) {
buffer.erase(buffer.length() - offset, 1);
offset -= 1;
}
}
default:
break;
}
// Добавить символ в буфер с учетом смещения
// при нажатии любой другой клавиши
else if (!std::count(ignore_keys.begin(), ignore_keys.end(), ch)) {
buffer.insert(buffer.end() - offset, (char)ch);
if (ch == SPACE) {
number = 0;
}
}
}
}
В принципе, все готово. Давайте проверим наш код в деле.
Пример использования
#include <iostream>
#include <string>
#include "../include/autocomplete.h"
int main() {
// Расположение файла конфигурации
std::string config_file_path = "../config.txt";
// Символы, с которых начинаются опциональные
// значения (необязательный параметр)
std::string optional_brackets = "[";
// Возможность задать цвет
#if defined(OS_WINDOWS)
uint16_t title_color = 160; // by default 10
uint16_t predict_color = 8; // by default 8
uint16_t default_color = 7; // by default 7
#elif defined(OS_POSIX)
// Set the value that goes between \033 and m ( \033{your_value}m )
std::string title_color = "0;30;102"; // by default 92
std::string predict_color = "90"; // by default 90
std::string default_color = "0"; // by default 90
#endif
// Перменная для заголовка строки
size_t command_counter = 0;
// Получаем словарь
auto [dict, status, message] = parse_config_file(config_file_path);
// Если получение словаря успешно
if (status) {
std::cerr << "Attention! Please run the executable file only" << std::endl
<< "through the command line!\n\n";
std::cerr << "- To switch the prompts press UP or DOWN arrow." << std::endl;
std::cerr << "- To move cursor press LEFT or RIGHT arrow." << std::endl;
std::cerr << "- To edit input press DELETE or BACKSPACE key." << std::endl;
std::cerr << "- To apply current prompt press TAB key.\n\n";
// Начинаем слушать
while (true) {
// Заготавливаем заголовок строки
std::string line_title = "git [" + std::to_string(command_counter) + "]:";
// Ожидаем ввода пользователя с отображением подсказок
std::string command = input(dict, line_title, optional_brackets,
title_color, predict_color, default_color);
// Делаем что-нибудь с полученной строкой
std::cout << std::endl << command << std::endl << std::endl;
command_counter++;
}
}
// Вывод сообщения, если файл конфигурации не был считан
else {
std::cerr << message << std::endl;
}
return 0;
}
Код был проверен на macOS, Linux, Windows. Все работает отлично.
Заключение:
Как вы видите, писать кроссплатформенный код довольно не просто (в нашем случае пришлось писать, то что есть на Windows из коробки для Linux вручную и наоборот), однако это очень интересно и сам факт, что это все работает на всех трех ОС, крайне доставляет.
Надеюсь, я был кому-нибудь полезен. Если вам есть что дополнить, буду внимательно слушать в комментариях.
Исходный код можно взять тут.
Пользуйтесь на здоровье.
===========
Источник:
habr.com
===========
Похожие новости:
- [Интернет-маркетинг, Контекстная реклама, Повышение конверсии] Как digital-агентству привлекать лиды из контекста: 10 лайфхаков
- [Системное администрирование, Серверное администрирование, DevOps, Kubernetes] CRI-O как замена Docker в качестве исполняемой среды для Kubernetes: настройка на CentOS 8
- [Работа с 3D-графикой, Дизайн, Умный дом, Интернет вещей, DIY или Сделай сам] Электронные часы в духе Cronixie
- [Open source, Программирование, Rust] Закладывая фундамент будущего Rust (перевод)
- [Настройка Linux, *nix, Разработка под Linux] Конфигурация i3 под ноутбук: как свести производительность на 100%?
- [Информационная безопасность, Open source, Разработка под Linux] Kali Linux получил графический интерфейс для подсистемы Windows для Linux (WSL2). Инструкция по установке
- [Программирование, C++, Параллельное программирование] Немного об ускорении программы: распараллеливание (ручное или автоматическое) на базе сверхоптимистичных вычислений
- [Системное администрирование, Серверное администрирование, DevOps, Kubernetes] Что такое Docker: краткий экскурс в историю и основные абстракции
- [SQL, Big Data] Clickhouse — оконные функции, которых нет…
- [Высокая производительность, Анализ и проектирование систем, C++] STL, allocator, его разделяемая память и её особенности
Теги для поиска: #_c++, #_razrabotka_pod_linux (Разработка под Linux), #_razrabotka_pod_macos (Разработка под MacOS), #_razrabotka_pod_windows (Разработка под Windows), #_c++, #_crossplatform, #_autocomplete, #_terminal, #_cli, #_c++, #_razrabotka_pod_linux (
Разработка под Linux
), #_razrabotka_pod_macos (
Разработка под MacOS
), #_razrabotka_pod_windows (
Разработка под Windows
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:44
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Приветствие Всем привет! Хочу поделиться своим опытом написания кроссплатформенного проекта на C++ для интеграции автодополнения в CLI приложения, усаживайтесь поудобнее. Формулировка задания
Приготовления Сразу предупрежу, использовать будем C++17 Предлагаю перейти к делу. Очевидно, так как наш проект кроссплатформенный, необходимо написать простенький макрос для определения текущей платформы. #if defined(_WIN32) || defined(_WIN64)
#define OS_WINDOWS #elif defined(__APPLE__) || defined(__unix__) || defined(__unix) #define OS_POSIX #else #error unsupported platform #endif Также сделаем небольшую заготовку: #if defined(OS_WINDOWS)
#define ENTER 13 #define BACKSPACE 8 #define CTRL_C 3 #define LEFT 75 #define RIGHT 77 #define DEL 83 #define UP 72 #define DOWN 80 #define SPACE 32 #elif defined(OS_POSIX) #define ENTER 10 #define BACKSPACE 127 #define SPACE 32 #define LEFT 68 #define RIGHT 67 #define UP 65 #define DOWN 66 #define DEL 51 #endif #define TAB 9 Так как мы нацелены на CLI проекты, и терминалы Linux и macOS имеют одинаковый API, объединим их в один define OS_POSIX. Windows, как всегда, стоит в стороне, вынесем для нее отдельный define OS_WINDOWS. Следующим шагом мы должны понять, как будут выглядеть автодополнения. С самого начала я был вдохновлен Redis CLI, поэтому будем просто выводить посказки другим нейтральным цветом. Но это не помешает нам в итоге использовать любые цвета. Следовательно, требуется написать функцию установки нужного цвета для вывода в консоль: /**
* Sets the console color. * * @param color System code of target color. * @return Input parameter os. */ #if defined(OS_WINDOWS) std::string set_console_color(uint16_t color) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color); return ""; #elif defined(OS_POSIX) std::string set_console_color(std::string color) { return "\033[" + color + "m"; #endif } Опять таки из-за разницы API приходится искать компромисс, будем всегда возвращать строку для того, чтобы можно было использовать функцию после оператора вывода << для повышения читаемости кода. Для тех, кому интересно, как именно работает API для цвета в Posix и Windows, и какие цветовые профили вообще бывают, предлагаю почитать ответы добрых людей на stackoverflow:
Так как нам придется постоянно перерисовывать строку из-за подсказок, необходимо написать функцию для "стирания" строки. /**
* Get count of terminal cols. * * @return Width of terminal. */ #if defined(OS_WINDOWS) size_t console_width() { CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); short width = --info.dwSize.X; return size_t((width < 0) ? 0 : width); } #endif /** * Clear terminal line. * * @param os Output stream. * @return input parameter os. */ std::ostream& clear_line(std::ostream& os) { #if defined(OS_WINDOWS) size_t width = console_width(); os << '\r' << std::string(width, ' '); #elif defined(OS_POSIX) std::cout << "\033[2K"; #endif return os; } На Posix платформах все просто, достаточно вывести в консоль \033[2K, но естественно в Windows нет аналогов, конкретно я не смог найти, приходится писать свою реализацию. Осталось понять, как осуществлять ввод символов пользователем. Обычный cin явно не подойдет, в процессе считывания не получится выводить предсказания. Тут приходит на ум функция _getch(), доступная в Windows, которая получает код символа нажатой клавиши на клавиатуре — это именно то, что нам надо. Но в этот раз с Posix платформами все плохо, увы, но придется писать свою реализацию. #if defined(OS_POSIX)
/** * Read key without press ENTER. * * @return Code of key on keyboard. */ int _getch() { int ch; struct termios old_termios, new_termios; tcgetattr( STDIN_FILENO, &old_termios ); new_termios = old_termios; new_termios.c_lflag &= ~(ICANON | ECHO ); tcsetattr( STDIN_FILENO, TCSANOW, &new_termios ); ch = getchar(); tcsetattr( STDIN_FILENO, TCSANOW, &old_termios ); return ch; } #endif Правила автодополнения Отлично. Теперь придумаем, как мы будем задавать правила для автодополнения. Предлагаю получать их из текстового файла следующей структуры: git
config --global user.name "[name]" user.email "[email]" user.name "[name]" user.email "[email]" init [repository name] clone [url] Идея такая. За каждым словом могут идти слова на расстоянии 1 табуляции от него. Т.е. после слова git могут идти слова config, init и global. После слова config могут идти слова --global, user.name и user.email и т.д. Также введем возможность указывать опциональные слова, в моем случае это слова внутри символов [] (вместо этих слов пользователь должен вводить свои данные). Хранить правила будем в ассоциативном массиве, где ключ будет выступать строкой, а значения — вектор слов, которые могут идти после ключа-строки. typedef std::map<std::string, std::vector<std::string>> Dictionary;
Давайте напишем функцию для парсинга файла с правилами. /**
* Parse config file to dictionary. * * @param file_path The path to the configuration file. * @return Tuple of dictionary with autocomplete rules, status of parsing and message. */ std::tuple<Dictionary, bool, std::string> parse_config_file(const std::string& file_path) { Dictionary dict; // Словарь с правилами автозаполнения std::map<int, std::string> // Массив для запоминания корневого слова root_words_by_tabsize; // для определенной длины табуляции std::string line; // Строка для чтения std::string token; // Полученное слово из строки std::string root_word; // Корневое слово для вставки в словарь как ключ long tab_size = 0; // Базовая длина табуляции (пробелов) long tab_count = 0; // Колличество табуляций в строке // Открытие файла конфигураций std::ifstream config_file(file_path); // Возвращаем сообщение об ошибке, если файл не был открыт if (!config_file.is_open()) { return std::make_tuple( dict, false, "Error! Can't open " + file_path + " file." ); } // Считываем все строки while (std::getline(config_file, line)) { // Пропускаем строку если она пустая if (line.empty()) { continue; } // Если в файле обнаружен символ табуляции, возвращаем сообщение о ошибке if (std::count(line.begin(), line.end(), '\t') != 0) { return std::make_tuple( dict, false, "Error! Use a sequence of spaces instead of a tab character." ); } // Получение количества пробелов в начале строки auto spaces = std::count( line.begin(), line.begin() + line.find_first_not_of(" "), ' ' ); // Устанавливаем базовый размер табуляции, если // была найдена строка с пробелами в начале if (spaces != 0 && tab_size == 0) { tab_size = spaces; } // Получаем слово из строки token = trim(line); // Проверка длины табуляции if (tab_size != 0 && spaces % tab_size != 0) { return std::make_tuple( dict, false, "Error! Tab length error was made.\nPossibly in line: " + line ); } // Получаем количество табуляций tab_count = (tab_size == 0) ? 0 : (spaces / tab_size); // Запоминаем корневое слово для заданного количества табуляций root_words_by_tabsize[tab_count] = token; // Получаем корневое слово для текущего токена root_word = (tab_count == 0) ? "" : root_words_by_tabsize[tab_count - 1]; // Вставка токена в словарь, если его там нет if (std::count(dict[root_word].begin(), dict[root_word].end(), token) == 0) { dict[root_word].push_back(token); } } // Закрываем файл config_file.close(); // Если все ОК возвращаем готовый словарь return std::make_tuple( dict, true, "Success. The rule dictionary has been created." ); } Разберемся с накопившимися вопросами.
/**
* Remove extra spaces to the left and right of the string. * * @param str Source string. * @return Line without spaces on the left and right. */ std::string trim(std::string_view str) { std::string result(str); result.erase(0, result.find_first_not_of(" \n\r\t")); result.erase(result.find_last_not_of(" \n\r\t") + 1); return result; } Функция просто отрезает лишнее пространство слева и справа у строки Автодополнение Хорошо. У нас есть словарь с правилами, а что дальше? Осталось сделать само автодополнение. Представим, что пользователь вводит что-то с клавиатуры. Что мы имеем? Одно или несколько введенных слов. Давайте научимся получать последнее слово из строки. /**
* Get the position of the beginning of the last word. * * @param str String with words. * @return Position of the beginning of the last word. */ size_t get_last_word_pos(std::string_view str) { // Вернуть 0 если строка состоит только из пробелов if (std::count(str.begin(), str.end(), ' ') == str.length()) { return 0; } // Получаем позицию последнего пробела auto last_word_pos = str.rfind(' '); // Вернуть 0, если пробел не найден, иначе вернуть позицию + 1 return (last_word_pos == std::string::npos) ? 0 : last_word_pos + 1; } /** * Get the last word in string. * * @param str String with words. * @return Pair Position of the beginning of the * last word and the last word in string. */ std::pair<size_t, std::string> get_last_word(std::string_view str) { // Поулчаем позицию size_t last_word_pos = get_last_word_pos(str); // Получаем последнее слово из строки auto last_word = str.substr(last_word_pos); // Возвращаем пару из слова и позиции слова в строке (для удобства) return std::make_pair(last_word_pos, last_word.data()); } Но давайте вспомним, чтобы предугадать, что хочет пользователь, нам надо знать не только последнее слово, которое мы пытаемся угадать, но и то, что шло до него. Давайте научимся получать предпоследнее слово из строки. // Не использовал std::min из-за странного
// поведения MSVC компилятора /** * Get the minimum of two numbers. * * @param a First value. * @param b Second value. * @return Minimum of two numbers. */ size_t min_of(size_t a, size_t b) { return (a < b) ? a : b; } /** * Get the penultimate words. * * @param str String with words. * @return Pair Position of the beginning of the penultimate * word and the penultimate word in string. */ std::pair<size_t, std::string> get_penult_word(std::string_view str) { // Находим правую границу поиска size_t end_pos = min_of(str.find_last_not_of(' ') + 2, str.length()); // Получаем позицию начала последнего слова size_t last_word = get_last_word_pos(str.substr(0, end_pos)); size_t penult_word_pos = 0; std::string penult_word = ""; // Находим предпоследнее слово если позиция // начала последнего была найдена if (last_word != 0) { // Находим начало предпоследнего слова penult_word_pos = str.find_last_of(' ', last_word - 2); // Находим предпоследнее слово если позиция начала найдена if (penult_word_pos != std::string::npos) { penult_word = str.substr(penult_word_pos, last_word - penult_word_pos - 1); } // Иначе предпоследнее слово - все, что дошло до последнего слова else { penult_word = str.substr(0, last_word - 1); } } // Обрезаем строку penult_word = trim(penult_word); // Возвращаем пару из позиции и слова (для удобства) return std::make_pair(penult_word_pos, penult_word); } Нахождение слов для автодополнения Что же мы забыли? Функцию для нахождения слов, которые начинаются также, как и последнее слово в строке. /**
* Find strings in vector starts with substring. * * @param substr String with which the word should begin. * @param penult_word Penultimate word in user-entered line. * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @return Vector with words starts with substring. */ std::vector<std::string> words_starts_with(std::string_view substr, std::string_view penult_word, Dictionary& dict, std::string_view optional_brackets) { std::vector<std::string> result; // Выход если нет ключа равного penult_word или // substr имеет символы для опциональных слов if (!dict.count(penult_word.data()) || substr.find_first_of(optional_brackets) != std::string::npos) { return result; } // Возвращаем все слова, которые могут быть // после last_word, если substr пуста if (substr.empty()) { return dict[penult_word.data()]; } // Находим строки, начинающиеся с substr std::vector<std::string> candidates_list = dict[penult_word.data()]; for (size_t i = 0 ; i < candidates_list.size(); i++) { if (candidates_list[i].find(substr) == 0) { result.push_back(dict[penult_word.data()][i]); } } return result; } А что по поводу проверки орфографии? Мы же хотели ее добавить? Давайте сделаем это. /**
* Find strings in vector similar to a substring (max 1 error). * * @param substr String with which the word should begin. * @param penult_word Penultimate word in user-entered line. * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @return Vector with words similar to a substring. */ std::vector<std::string> words_similar_to(std::string_view substr, std::string_view penult_word, Dictionary& dict, std::string_view optional_brackets) { std::vector<std::string> result; // Выход, если строка пустая if (substr.empty()) { return result; } std::vector<std::string> candidates_list = dict[penult_word.data()]; for (size_t i = 0 ; i < candidates_list.size(); i++) { int errors = 0; // Получаем кандидата std::string candidate = candidates_list[i]; // Посимвольная проверка кандидата for (size_t j = 0; j < substr.length(); j++) { // Пропуск, если кандидат содержит символы для опциональных слов if (optional_brackets.find_first_of(candidate[j]) != std::string::npos) { errors = 2; break; } if (substr[j] != candidate[j]) { errors += 1; } if (errors > 1) { break; } } // Добавляем кандидата, если максимум одна ошибка if (errors <= 1) { result.push_back(candidate); } } return result; } Теперь у нас есть все, чтобы предсказать слово по введенной пользователем строке. Давайте решим эту задачу. /**
* Get the word-prediction by the index. * * @param buffer String with user input. * @param dict Dictionary with rules. * @param number Index of word-prediction. * @param optional_brackets String with symbols for optional values. * @return Tuple of word-prediction, phrase for output, substring of buffer * preceding before phrase, start position of last word. */ std::tuple<std::string, std::string, std::string, size_t> get_prediction(std::string_view buffer, Dictionary& dict, size_t number, std::string_view optional_brackets) { // Получаем информацию о последнем слове auto [last_word_pos, last_word] = get_last_word(buffer); // Получаем информацию о предпоследнем слове auto [_, penult_word] = get_penult_word(buffer); std::string prediction; // предсказание std::string phrase; // фраза для вывода std::string prefix; // подстрока буфера, предшествующая фразе // Ищем предсказания std::vector<std::string> starts_with = words_starts_with( last_word, penult_word, dict, optional_brackets ); // Устанавливаем значения, если предсказания были найдены if (!starts_with.empty()) { prediction = starts_with[number % starts_with.size()]; phrase = prediction; prefix = buffer.substr(0, last_word_pos); } // Если слова не были найдены else { // Ищем слова с учетом орфографии std::vector<std::string> similar = words_similar_to( last_word, penult_word, dict, optional_brackets ); // Устанавливаем значения, если предсказания были найдены if (!similar.empty()) { prediction = similar[number % similar.size()]; phrase = " maybe you mean " + prediction + "?"; prefix = buffer; } } // Возвращаем необходимые данные return std::make_tuple(prediction, phrase, prefix, last_word_pos); } Ввод пользователя с клавиатуры Осталось одно из самых сложных заданий. Написать саму функцию ввода с клавиатуры. /**
* Gets current terminal cursor position. * * @return Y position of terminal cursor. */ short cursor_y_pos() { #if defined(OS_WINDOWS) CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); return info.dwCursorPosition.Y; #elif defined(OS_POSIX) struct termios term, restore; char ch, buf[30] = {0}; short i = 0, pow = 1, y = 0; tcgetattr(0, &term); tcgetattr(0, &restore); term.c_lflag &= ~(ICANON|ECHO); tcsetattr(0, TCSANOW, &term); write(1, "\033[6n", 4); for (ch = 0; ch != 'R'; i++) { read(0, &ch, 1); buf[i] = ch; } i -= 2; while (buf[i] != ';') { i -= 1; } i -= 1; while (buf[i] != '[') { y = y + ( buf[i] - '0' ) * pow; pow *= 10; i -= 1; } tcsetattr(0, TCSANOW, &restore); return y; #endif } /** * Move terminal cursor at position x and y. * * @param x X position to move. * @param x Y position to move. * @return Void. */ void goto_xy(short x, short y) { #if defined(OS_WINDOWS) COORD xy {--x, y}; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy); #elif defined(OS_POSIX) printf("\033[%d;%dH", y, x); #endif } /** * Printing user input with prompts. * * @param buffer String - User input. * @param dict Vector of words. * @param line_title Line title of CLI when entering command. * @param number Hint number. * @param optional_brackets String with symbols for optional values. * @param title_color System code of title color (line title color). * @param predict_color System code of predict color (prediction color). * @param default_color System code of default color (user input color). * @return Void. */ #if defined(OS_WINDOWS) void print_with_prompts(std::string_view buffer, Dictionary& dict, std::string_view line_title, size_t number, std::string_view optional_brackets, uint16_t title_color, uint16_t predict_color, uint16_t default_color) { #else void print_with_prompts(std::string_view buffer, Dictionary& dict, std::string_view line_title, size_t number, std::string_view optional_brackets, std::string title_color, std::string predict_color, std::string default_color) { #endif // Получить прогнозируемую фразу и часть буфера, предшествующую фразе auto [_, phrase, prefix, __] = get_prediction(buffer, dict, number, optional_brackets); std::string delimiter = line_title.empty() ? "" : " "; std::cout << clear_line; std::cout << '\r' << set_console_color(title_color) << line_title << set_console_color(default_color) << delimiter << prefix << set_console_color(predict_color) << phrase; std::cout << '\r' << set_console_color(title_color) << line_title << set_console_color(default_color) << delimiter << buffer; } /** * Reading user input with autocomplete. * * @param dict Vector of words. * @param optional_brackets String with symbols for optional values. * @param title_color System code of title color (line title color). * @param predict_color System code of predict color (prediction color). * @param default_color System code of default color (user input color). * @return User input. */ #if defined(OS_WINDOWS) std::string input(Dictionary& dict, std::string_view line_title, std::string_view optional_brackets, uint16_t title_color, uint16_t predict_color, uint16_t default_color) { #else std::string input(Dictionary& dict, std::string_view line_title, std::string_view optional_brackets, std::string title_color, std::string predict_color, std::string default_color) { #endif std::string buffer; // Буфер size_t offset = 0; // Смещение курсора от конца буфера size_t number = 0; // Номер (индекс) посдказки, для переключения short y = cursor_y_pos(); // Получаем позицию курсора по оси Y в терминале // Игнорируемые символы #if defined(OS_WINDOWS) std::vector<int> ignore_keys({1, 2, 19, 24, 26}); #elif defined(OS_POSIX) std::vector<int> ignore_keys({1, 2, 4, 24}); #endif while (true) { // Выводим строку пользователя с предсказанием print_with_prompts(buffer, dict, line_title, number, optional_brackets, title_color, predict_color, default_color); // Перемещаем курсор в нужную позицию short x = short( buffer.length() + line_title.length() + !line_title.empty() + 1 - offset ); goto_xy(x, y); // Считываем очередной символ int ch = _getch(); // Возвращаем буфер, если нажат Enter if (ch == ENTER) { return buffer; } // Обработка выхода из CLI в Windows #if defined(OS_WINDOWS) else if (ch == CTRL_C) { exit(0); } #endif // Изменение буфера при нажатии BACKSPACE else if (ch == BACKSPACE) { if (!buffer.empty() && buffer.length() - offset >= 1) { buffer.erase(buffer.length() - offset - 1, 1); } } // Применение подсказки при нажатии TAB else if (ch == TAB) { // Получаем необходимую информацию auto [prediction, _, __, last_word_pos] = get_prediction(buffer, dict, number, optional_brackets); // Дописываем предсказание, если имеется if (!prediction.empty() && prediction.find_first_of(optional_brackets) == std::string::npos) { buffer = buffer.substr(0, last_word_pos) + prediction + " "; } // Очищаем индекс подсказки и смещение offset = 0; number = 0; } // Обработка стрелок #if defined(OS_WINDOWS) else if (ch == 0 || ch == 224) #elif defined(OS_POSIX) else if (ch == 27 && _getch() == 91) #endif switch (_getch()) { case LEFT: // Увеличьте смещение, если нажата левая клавиша offset = (offset < buffer.length()) ? offset + 1 : buffer.length(); break; case RIGHT: // Уменьшить смещение, если нажата правая клавиша offset = (offset > 0) ? offset - 1 : 0; break; case UP: // Увеличить индекс подсказки number = number + 1; std::cout << clear_line; break; case DOWN: // Уменьшить индекс подсказки number = number - 1; std::cout << clear_line; break; case DEL: // Изменение буфера, при нажатии DELETE #if defined(OS_POSIX) if (_getch() == 126) #endif { if (!buffer.empty() && offset != 0) { buffer.erase(buffer.length() - offset, 1); offset -= 1; } } default: break; } // Добавить символ в буфер с учетом смещения // при нажатии любой другой клавиши else if (!std::count(ignore_keys.begin(), ignore_keys.end(), ch)) { buffer.insert(buffer.end() - offset, (char)ch); if (ch == SPACE) { number = 0; } } } } В принципе, все готово. Давайте проверим наш код в деле. Пример использования #include <iostream>
#include <string> #include "../include/autocomplete.h" int main() { // Расположение файла конфигурации std::string config_file_path = "../config.txt"; // Символы, с которых начинаются опциональные // значения (необязательный параметр) std::string optional_brackets = "["; // Возможность задать цвет #if defined(OS_WINDOWS) uint16_t title_color = 160; // by default 10 uint16_t predict_color = 8; // by default 8 uint16_t default_color = 7; // by default 7 #elif defined(OS_POSIX) // Set the value that goes between \033 and m ( \033{your_value}m ) std::string title_color = "0;30;102"; // by default 92 std::string predict_color = "90"; // by default 90 std::string default_color = "0"; // by default 90 #endif // Перменная для заголовка строки size_t command_counter = 0; // Получаем словарь auto [dict, status, message] = parse_config_file(config_file_path); // Если получение словаря успешно if (status) { std::cerr << "Attention! Please run the executable file only" << std::endl << "through the command line!\n\n"; std::cerr << "- To switch the prompts press UP or DOWN arrow." << std::endl; std::cerr << "- To move cursor press LEFT or RIGHT arrow." << std::endl; std::cerr << "- To edit input press DELETE or BACKSPACE key." << std::endl; std::cerr << "- To apply current prompt press TAB key.\n\n"; // Начинаем слушать while (true) { // Заготавливаем заголовок строки std::string line_title = "git [" + std::to_string(command_counter) + "]:"; // Ожидаем ввода пользователя с отображением подсказок std::string command = input(dict, line_title, optional_brackets, title_color, predict_color, default_color); // Делаем что-нибудь с полученной строкой std::cout << std::endl << command << std::endl << std::endl; command_counter++; } } // Вывод сообщения, если файл конфигурации не был считан else { std::cerr << message << std::endl; } return 0; } Код был проверен на macOS, Linux, Windows. Все работает отлично. Заключение: Как вы видите, писать кроссплатформенный код довольно не просто (в нашем случае пришлось писать, то что есть на Windows из коробки для Linux вручную и наоборот), однако это очень интересно и сам факт, что это все работает на всех трех ОС, крайне доставляет. Надеюсь, я был кому-нибудь полезен. Если вам есть что дополнить, буду внимательно слушать в комментариях. Исходный код можно взять тут. Пользуйтесь на здоровье. =========== Источник: habr.com =========== Похожие новости:
Разработка под Linux ), #_razrabotka_pod_macos ( Разработка под MacOS ), #_razrabotka_pod_windows ( Разработка под Windows ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:44
Часовой пояс: UTC + 5