[C++, Разработка под Linux, Разработка под MacOS, Разработка под Windows] Пишем автодополнение для ваших CLI проектов

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
20-Авг-2020 16:32

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

Разберемся с накопившимися вопросами.
  • Функция возвращает кортеж, так как по моему использование исключений не очень удачный вариант.
  • Почему использование символа \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
===========

Похожие новости: Теги для поиска: #_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