Пишем автодополнение для ваших CLI проектов

от автора

Приветствие

Всем привет! Хочу поделиться своим опытом написания кроссплатформенного проекта на 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:

Так как нам придется постоянно перерисовывать строку из-за подсказок, необходимо написать функцию для "стирания" строки.

/**  * 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."     ); }

Разберемся с накопившимися вопросами.

  1. Функция возвращает кортеж, так как по моему использование исключений не очень удачный вариант.
  2. Почему использование символа \t в файле запрещено? Потому что будем привыкать к хорошей практике использования последовательности пробелов вместо табуляции.
  3. Откуда взялась функция 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 вручную и наоборот), однако это очень интересно и сам факт, что это все работает на всех трех ОС, крайне доставляет.

Надеюсь, я был кому-нибудь полезен. Если вам есть что дополнить, буду внимательно слушать в комментариях.

Исходный код можно взять тут.
Пользуйтесь на здоровье.

ссылка на оригинал статьи https://habr.com/ru/post/515946/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *