Пишем терминальный сервер для микроконтроллера на С

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Всем привет! В процессе работы над гексаподом появилась потребность в каком-нибудь интерфейсе для общения с ним. В результате тесной работы с Linux я подумал, а почему бы не использовать терминал для гексапода? Я был удивлен, что по запросу "STM32 terminal" не было готовых решений. Ну раз нет готовых, то напишем свой терминальный сервер для использования в микроконтроллерах. Сделаем это без использования динамической памяти и прочих опасных радостей, быстро, просто и понятно.

Github: https://github.com/NeoProg2013/terminal-server

Синхронизируем понятия

Терминал, консоль — клиентская часть (KiTTY, PuTTY и прочие). Выполняется на стороне клиента (PC). Возьмем готовую реализацию в виде KiTTY.

Терминальный сервер — удаленная часть, выполняется на микроконтроллере. Именно эту часть я и буду делать.

Аппаратные тонкости

В качестве тестового микроконтроллера я буду использовать STM32F030. Общаться с MCU мы будем через USB-USART преобразователь CH340E.

CH340E
CH340E

Почему именно она? Для корректной работы терминального сервера нужно уметь отслеживать текущее состояние подключения, т.е. нам нужен аналог TCP протокола в железном варианте и эта микросхема отлично для этого подходит — тут есть DTR. Просто берем этот вывод и заводим его на ногу микроконтроллера, тем самым мы сможем определить подключение.

  • DTR=Low — пользователь открыл терминал;

  • DTR=High — пользователь закрыл терминал;

Зачем нам вообще следить за подключением? Дело в том, что терминальному серверу необходимо знать когда "забыть" текущую сессию и обнулить все буферы. Можно закрыть терминал в середине набора команды и при следующем подключением можно обнаружить неадекватное поведение. Так же нам нужно знать когда вывести приглашение в консоль пользователя, всё должно быть по канону :)

В итоге, для работы требуется всего 4 провода: RX, TX, DTR, GND. Мне кажется это хорошим началом — минимум проводов. На этом аппаратные детали закончились.

Управляющие последовательности (УП) и символы (УС)

Что это и зачем они нужны? Вся вот эта красивая разноцветная консолька это "клиент-сервер", неважно SSH там или провод между ними. Нужно каким-то образом сообщать серверу или клиенту о каких-либо действиях — для этого используются УП и УС.

Начнем с управляющих последовательностей (УП)

Для простоты можно рассматривать УП как некие события, которые посылает клиент в сторону сервера и наоборот. Если нужно на стороне клиента переместить курсор влево (поправить опечатку, например), то мы должны как-то сообщить серверу об этом. Для этого мы отправляем ему событие в виде УП.

УП начинаются со специального кода ESC (0х1В), после которого идут несколько байт, определяющие тип события. Например, событие перемещения курсора влево будет иметь следующий вид: [0x1B 0x5B 0x44].

Управляющие символы (УС)

В отличие от УП они имеют размерность 1 байт. В качестве примера можно взять всем известные \r (0x0D) и \n (0x0A). Они находятся в самом начале ANSII таблицы [0x00 - 0x31, 0x7F] и в целом с ними нет никаких проблем.

Когда вы хотите прервать команду, то нажимаете Ctrl+C. В итоге это превращается в управляющий символ 0x03 (End of text). Для закрытия сессии Ctrl+D, это превращается в 0x04 (End of transmission) и т.д.

Программные тонкости

Ниже я приведу несколько УП и УС, которые хочу реализовать на этом этапе. Более обширный их список можно найти тут

  • "\x0D" — Carriage Return (CR, возврат каретки), он же \r;

  • "\x0A" — Line Feed (LF, перевод каретки), он же \n;

  • "\x0D" — TAB button (ну куда же без авто завершения команд);

  • "\x7F" и "\x08" — BACKSPACE button;

  • "\x1B[3~" — DEL button;

  • "\x1B[A" и "\x1B[B" — arrow up \ down;

  • "\x1B[D" и "\x1B[C" — arrow left \ right;

  • "\x1B[4~" — END button;

  • "\x1B[1~" — HOME button;

Этого списка достаточно для простого терминального сервера. В дальнейшем новые УП и УС будут добавляться по мере необходимости.

Есть еще две хитрых УП, которые я буду посылать клиенту. При посылке любого символа клиенту на его стороне не просто отображается символ, но и перемещается курсор. Использование этих двух УП позволяет убрать слежение за положением курсора в некоторых местах. Это сильно упростит манипуляции с клиентской частью и сократит трафик по USART.

  • "\x1B[s" — Запомнить положение курсора;

  • "\x1B[u" — Восстановить положение курсора;

Закинем их в отдельные функции:

static inline void save_cursor_pos() { send_data("\x1B[s", 3); }
static inline void load_cursor_pos() { send_data("\x1B[u", 3); }

Важно: не все терминалы поддерживают эти команды, нужно иметь это ввиду.

Ну давайте код попишем

Начнем с простого — драйвер USART. Вжух и драйвер готов:

int main() {
    system_init(); // Инициализация источников тактирования, FLASH latency и т.п.
   
    // Запуск тактирования USART1
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    while ((RCC->APB2ENR & RCC_APB2ENR_USART1EN) == 0);
    
    // Настройка TX вывода
    gpio_set_mode        (GPIOA, USART_TX_PIN, GPIO_MODE_AF); // Передаем управление пином переферии 
    gpio_set_output_speed(GPIOA, USART_TX_PIN, GPIO_SPEED_HIGH); // Ну конечно же максимальная скорость пина
    gpio_set_pull        (GPIOA, USART_TX_PIN, GPIO_PULL_NO); // Без подтягивающих регисторов обойдемся
    gpio_set_af          (GPIOA, USART_TX_PIN, 1); // Какой периферии мы передаем пин, в данном случае USART1
    
    // Настройка RX вывода
    gpio_set_mode        (GPIOA, USART_RX_PIN, GPIO_MODE_AF);
    gpio_set_output_speed(GPIOA, USART_RX_PIN, GPIO_SPEED_HIGH);
    gpio_set_pull        (GPIOA, USART_RX_PIN, GPIO_PULL_UP);
    gpio_set_af          (GPIOA, USART_RX_PIN, 1);

    // Конфигурация USART: 8N1, 115200
    RCC->APB2RSTR |= RCC_APB2RSTR_USART1RST;
    RCC->APB2RSTR &= ~RCC_APB2RSTR_USART1RST;
    USART1->BRR = SYSTEM_CLOCK_FREQUENCY / 115200;

    // Запускаем USART1
    USART1->CR1 |= USART_CR1_UE;
    USART1->CR1 |= USART_CR1_TE | USART_CR1_RE;
    
    while (true) {
        if (USART1->ISR & USART_ISR_RXNE) {
            uint8_t data = USART1->RDR;
        }
    }
}

void send_data(const char* data, uint8_t size) {
    while (size) {
        while ((USART1->ISR & USART_ISR_TXE) != USART_ISR_TXE);
        USART1->TDR = *data;
        ++data;
        --size;
    }
}

Я не буду детально останавливаться на каждой строчке, расскажу общую идею. Драйвер блокирующий (polling) без использования прерываний и DMA. Сделано это в угоду простоты и скорости реализации. В главном цикле мы читаем данные пока в никуда, так же есть отдельная функция для передачи данных. Я планирую её передавать терминальному серверу в виде указателя на функцию, чтобы тот мог передавать данные. Это позволит отвязать сервер от периферии (абстракция и все такое).

Первые шаги

"Нажать кнопку и увидеть символ в консоли" звучит просто, но не все так гладко. При нажатии клавиши клиент передает серверу её скан-код, а тот уже решает что с этим делать. Он может послать скан-код обратно клиенту для отображения в консоли или выполнить какое-либо действие, если это УП.

УП и УС я решил обрабатывать как обычные пользовательские команды, по сути то они ни чем не отличаются. Соответственно их обработчики будут внутри кода сервера и будут храниться в отдельной переменной.

Для выполнения команд нам нужно где-то буферизировать вводимые символы, при этом УП и УС должны попадать в отдельный буфер. Еще нам нужно где-то хранить список пользовательских команд и иметь возможность указывать их через интерфейс. И нельзя забывать про историю команд. Появляются следующие структуры данных:

typedef struct {
    char cmd[MAX_COMMAND_LENGTH];
    uint16_t length;
} cmd_info_t;


static term_srv_cmd_t esc_seq_list[ESCAPE_SEQUENCES_COUNT] = {
    { .cmd = "\x0D",    .len = 1, .handler = esc_return_handler,    },
    { .cmd = "\x0A",    .len = 1, .handler = esc_return_handler,    },
    { .cmd = "\x7F",    .len = 1, .handler = esc_backspace_handler, },
    { .cmd = "\x08",    .len = 1, .handler = esc_backspace_handler, },
    { .cmd = "\x1B[3~", .len = 4, .handler = esc_del_handler,       },
    { .cmd = "\x1B[A",  .len = 3, .handler = esc_up_handler,        },
    { .cmd = "\x1B[B",  .len = 3, .handler = esc_down_handler,      },
    { .cmd = "\x1B[D",  .len = 3, .handler = esc_left_handler,      },
    { .cmd = "\x1B[C",  .len = 3, .handler = esc_right_handler,     },
    { .cmd = "\x1B[4~", .len = 4, .handler = esc_end_handler,       },
    { .cmd = "\x1B[1~", .len = 4, .handler = esc_home_handler,      }
};

static void(*send_data)(const char* data, uint16_t) = NULL; // Указатель на функцию для передачи данных клиенту
static term_srv_cmd_t* ext_cmd_list = NULL; // Указатель на список пользовательских команд
static uint8_t ext_cmd_count = 0; // Количество пользовательских команд

static uint16_t cursor_pos = 0; // Текущая позиция курсора
static cmd_info_t current_cmd = {0}; // Информация о вводимой в данный момент команде
static char esc_seq_buffer[10] = {0}; // Буфер для управляющих последовательностей
static uint16_t esc_seq_length = 0; // Длина управляющей последовательности на данный момент

static cmd_info_t history_elements[MAX_COMMAND_HISTORY_LENGTH] = {0}; // Элементы истории. Вместо динамического выделения памяти
static cmd_info_t* history[MAX_COMMAND_HISTORY_LENGTH] = {0}; // Указатели на элементы. Всегда отсортированы по времени
static int16_t history_len = 0; // Длина истории (количество команд в ней)
static int16_t history_pos = -1; // Текущая позиция в истории

esc_seq_list это те самые УП и УС, которые я буду обрабатывать. В этой структуре есть сигнатура, её длина и функция-обработчик. При совпадении сигнатуры будет вызываться функция-обработчик и выполняться какое-либо действие (перемещение курсора, удаление символа и т.п.)

Теперь нам нужен внешний интерфейс для работы с сервером (заголовочный файл):

#ifndef _TERM_SRV_H_
#define _TERM_SRV_H_
#include <stdint.h>

// Settings
#define MAX_COMMAND_HISTORY_LENGTH          (3)     // Command count in history
#define MAX_COMMAND_LENGTH                  (64)    // Command length in bytes
#define GREETING_STRING                     ("\x1B[36mroot@hexapod-AIWM: \x1B[0m")
#define UNKNOWN_COMMAND_STRING              ("\x1B[31m - command not found\x1B[0m")


typedef struct {
    char* cmd;
    uint16_t len;
    void(*handler)(const char* cmd);
} term_srv_cmd_t;


extern void term_srv_init(void(*_send_data)(const char*, uint16_t), term_srv_cmd_t* _ext_cmd_list, uint8_t _ext_cmd_count);
extern void term_srv_attach(void);
extern void term_srv_detach(void);
extern void term_srv_process(char symbol);


#endif // _TERM_SRV_H_

term_srv_init инициализирует терминальный сервер. В нее передается указатель на функцию для передачи данных клиенту и список пользовательских команд в виде массива структур. В самой структуре содержится команда, её длина и функция-обработчик, которая будет вызываться сервером при получении команды.

term_srv_process вызывается для обработки принятого символа. В нее просто передается принятый символ. Её мы добавляем в драйвер USART после приема символа.

term_srv_attach вызывается при подключении пользователя к серверу. В это время выводится приглашение.

term_srv_detach вызывается для сброса сервера при изменении уровня на DTR пине или еще при каком-нибудь событии.

Просто не правда ли? Теперь реализуем все это дело:

void term_srv_init(void(*_send_data)(const char*, uint16_t), term_srv_cmd_t* _ext_cmd_list, uint8_t _ext_cmd_count) {
    send_data = _send_data;
    ext_cmd_list = _ext_cmd_list;
    ext_cmd_count = _ext_cmd_count;
    term_srv_detach();
}

void term_srv_attach(void) {
    send_data("\r\n", 2);
    send_data(GREETING_STRING, strlen(GREETING_STRING));
}

void term_srv_detach(void) {
    cursor_pos = 0;
    memset(&current_cmd, 0, sizeof(current_cmd));
    
    esc_seq_length = 0;
    memset(esc_seq_buffer, 0, sizeof(esc_seq_buffer));
    
    memset(history_elements, 0, sizeof(history_elements));
    for (int16_t i = 0; i < MAX_COMMAND_HISTORY_LENGTH; ++i) {
        history[i] = &history_elements[i];
    }
    history_len = 0;
    history_pos = 0;
}

Тут всё предельно просто: в случае init сохраняем переданные параметры и сбрасываем состояние сервера. В случае detach обнуляем вообще всё. Как-будто нечего и не было. В attach выводим приглашение в консоль клиента.

Теперь самое интересное — обработка символов

void term_srv_process(char symbol) {
    if (current_cmd.length + 1 >= MAX_COMMAND_LENGTH) {
        return; // Incoming buffer is overflow
    }
    
    if (esc_seq_length == 0) {
        // Check escape signature
        if (symbol <= 0x1F || symbol == 0x7F) {
            esc_seq_buffer[esc_seq_length++] = symbol;
        }
        // Print symbol if escape sequence signature is not found
        if (esc_seq_length == 0) {
            if (cursor_pos < current_cmd.length) {
                memmove(&current_cmd.cmd[cursor_pos + 1], &current_cmd.cmd[cursor_pos], current_cmd.length - cursor_pos); // Offset symbols after cursor
                save_cursor_pos();
                send_data(&current_cmd.cmd[cursor_pos], current_cmd.length - cursor_pos + 1);
                load_cursor_pos();
            }
            current_cmd.cmd[cursor_pos++] = symbol;
            ++current_cmd.length;
            send_data(&symbol, 1); 
        }
    } else {
        esc_seq_buffer[esc_seq_length++] = symbol;
    }
    
    // Process escape sequence
    if (esc_seq_length != 0) {
        int8_t possible_esc_seq_count = 0;
        int8_t possible_esc_idx = 0;
        for (int8_t i = 0; i < ESCAPE_SEQUENCES_COUNT; ++i) {
            if (esc_seq_length <= esc_seq_list[i].len && memcmp(esc_seq_buffer, esc_seq_list[i].cmd, esc_seq_length) == 0) {
                ++possible_esc_seq_count;
                possible_esc_idx = i;
            }
        }
        
        switch (possible_esc_seq_count) {
        case 0: // No sequence - display all symbols
            for (int8_t i = 0; i < esc_seq_length && current_cmd.len + 1 < MAX_COMMAND_LENGTH; ++i) {
                if (esc_seq_buffer[i] <= 0x1F || esc_seq_buffer[i] == 0x7F) {
                    esc_seq_buffer[i] = '?';
                }
                current_cmd.cmd[cursor_pos + i] = esc_seq_buffer[i];
                ++current_cmd.len;
            }
            send_data(&current_cmd.cmd[cursor_pos], esc_seq_length);
            cursor_pos += esc_seq_length;
            esc_seq_length = 0;
            break;
        
        case 1: // We found one possible sequence - check size and call handler
            if (esc_seq_list[possible_esc_idx].len == esc_seq_length) {
                esc_seq_length = 0;
                esc_seq_list[possible_esc_idx].handler();
            }
            break;
        
        default: // We found few possible sequences
            break;
        }
    }
}

Погнали по строчкам.

  • 8-10: тут мы проверяем соответствие символа на сигнатуру УП и УС, если до этого мы её еще не обнаружили. Если мы все же нашли соответствие, то мы сохраняем символ в другой буфер и переходим в режим их обработки (esc_seq_length != 0);

  • 12-21: если мы не нашли причин считать, что мы принимаем УП или УС, то просто отправляем принятый символ для отображения в консоли клиента и записываем его в буфер;

  • 13-18: этот кейс работает, если курсор находится не в конце команды (переместили его ранее). Данные в буфере смещаются вправо и отображаются в консоли клиента. Тут используются save_cursor_pos и load_cursor_pos, чтобы руками не вычислять каждый раз позицию курсора - пусть за нас это сделает клиент;

  • 29-36: сюда мы попадаем только в режиме обработки УП и УС. Тут мы бегаем по нашему списку и ищем количество частичных совпадений.
    - possible_esc_seq_count показывает столько мы таких нашли,
    - possible_esc_idx хранит индекс в списке для последнего найденного совпадения;

  • 40-50: сюда мы попадаем, если входная УП оказалась не УП, либо мы её не поддерживаем. В этом случае вываливаем всё обратно в консоль клиента, заменяя невидимые символы на '?';

  • 47-52: сюда мы попадаем, если мы однозначно определили УП или УС, но могли её не полностью принять (проверяем её длину). Как только мы получили её полностью — дергаем обработчик и выходим из режима обработки последовательностей. При этом саму последовательность мы не отправляем обратно.

Обработчики УП и УС

Начнем с базовых вещей: \r и \n, стрелок влево\вправо, home, end.

static void esc_return_handler(const char* cmd) {
    send_data("\r\n", 2);
    
    // Add command into history
    if (current_cmd.len) {
        if (history_len >= MAX_COMMAND_HISTORY_LENGTH) {
            // If history is overflow -- offset all items to begin by 1 position, first item move to end
            void* temp_addr = history[0];
            for (int16_t i = 0; i < MAX_COMMAND_HISTORY_LENGTH - 1; ++i) {
                history[i] = history[i + 1];
            }
            history[MAX_COMMAND_HISTORY_LENGTH - 1] = temp_addr;
            --history_len;
        }
        memcpy(history[history_len++], &current_cmd, sizeof(current_cmd));
        history_pos = history_len;
    }
    
    // Calc command length without args
    void* addr = memchr(current_cmd.cmd, ' ', current_cmd.len);
    int16_t cmd_len = current_cmd.len;
    if (addr != NULL) {
        cmd_len = (uint32_t)addr - (uint32_t)current_cmd.cmd;
    }
    
    // Search command
    bool is_find = false;
    for (int16_t i = 0; i < ext_cmd_count; ++i) {
        if (cmd_len == ext_cmd_list[i].len && memcmp(ext_cmd_list[i].cmd, current_cmd.cmd, cmd_len) == 0) {
            ext_cmd_list[i].handler(current_cmd.cmd);
            send_data("\r\n", 2);
            is_find = true;
            break;
        }
    }
    if (!is_find && cmd_len) {
        send_data(current_cmd.cmd, cmd_len);
        send_data(UNKNOWN_COMMAND_STRING, strlen(UNKNOWN_COMMAND_STRING));
        send_data("\r\n", 2);
    }
    send_data(GREETING_STRING, strlen(GREETING_STRING));

    // Clear buffer to new command
    memset(&current_cmd, 0, sizeof(current_cmd));
    cursor_pos = 0;
}
static void esc_left_handler(const char* cmd) {
    if (cursor_pos > 0) {
        send_data("\x1B[D", 3);
        --cursor_pos;
    }
}
static void esc_right_handler(const char* cmd) {
    if (cursor_pos < current_cmd.length) {
        send_data("\x1B[C", 3);
        ++cursor_pos;
    }
}
static void esc_home_handler(const char* cmd) {
    while (cursor_pos > 0) {
        send_data("\x1B[D", 3);
        --cursor_pos;
    }
}
static void esc_end_handler(const char* cmd) {
    while (cursor_pos < current_cmd.length) {
        send_data("\x1B[C", 3);
        ++cursor_pos;
    }
}

Просто и лаконично.

Аргумент cmd для внутренних обработчиков всегда NULL, я использовал структуру term_srv_cmd_t, чтобы не дублировать её еще раз под другим именем из-за различий в прототипе.

  • esc_return_handler: мы сразу же посылаем клиенту "\r\n", что приведет к переводу каретки в начало новой строки. Сохраняем команду в историю и выполняем её автоматическую сортировку по времени. Дальше мы пробегаемся по массиву пользовательских команд и ищем совпадения, если нашли дергаем функцию-обработчик. После этого мы обнуляем буфер команды и посылаем приглашение (в данном случае это "root@hexapod-AIWM: ")

  • esc_right_handler и esc_left_handler: тут мы просто смещаем внутреннюю позицию курсора и пересылаем УП "\x1B[C" обратно, чтобы переместить курсор уже в консоли клиента. Аналогично и для esc_left_handler, только там в другую сторону.

  • esc_home_handler и esc_end_handler: по сути это те же перемещения курсора влево\вправо только не на одну позицию, а в начало и конец — цикл с посылкой нужной УП ("\x1B[D" влево, "\x1B[C" вправо).

Мы научились печатать символы, перемещать курсор и нажимать Enter. Теперь возьмем что-нибудь сложнее — УС backspace и УС delete подойдут, символы то нужно уметь стирать.

static void esc_backspace_handler(const char* cmd) {
    if (cursor_pos > 0) {
        memmove(&current_cmd.cmd[cursor_pos - 1], &current_cmd.cmd[cursor_pos], current_cmd.length - cursor_pos); // Remove symbol from buffer
        current_cmd.cmd[current_cmd.length - 1] = 0; // Clear last symbol
        --current_cmd.length;
        --cursor_pos;

        send_data("\x7F", 1); // Remove symbol
        save_cursor_pos();
        send_data(&current_cmd.cmd[cursor_pos], current_cmd.length - cursor_pos); // Replace old symbols
        send_data(" ", 1);    // Hide last symbol
        load_cursor_pos();
    }
}
static void esc_del_handler(const char* cmd) {
    if (cursor_pos < current_cmd.length) {
        memmove(&current_cmd.cmd[cursor_pos], &current_cmd.cmd[cursor_pos + 1], current_cmd.length - cursor_pos); // Remove symbol from buffer
        current_cmd.cmd[current_cmd.length - 1] = 0; // Clear last symbol
        --current_cmd.length;

        save_cursor_pos();
        send_data(&current_cmd.cmd[cursor_pos], current_cmd.length - cursor_pos);
        send_data(" ", 1); // Hide last symbol
        load_cursor_pos();
    }
}

В самом начале идут манипуляции с буфером команды, от туда мы удаляем символ под курсором и сдвигаем всё что после него влево. Этот код превращается в диалог типа "Удали текущий символ со сдвигом курсора влево, сохрани позицию курсора, отобрази все символы после него, замени последний пробелом и восстанови положение курсора".

С delete все тоже самое, только символ стирается после курсора.

История и авто завершение

Перемещение по истории производится стрелками вверх и вниз. История представляет себя отсортированный по времени массив указателей на структуру команды (сама команда + длина + обработчик). Тут не так сложно как может показаться. Мы просто перемещаемся по истории и отображаем команду одну за одной из нее.

Единственная сложность это учесть размер команд. Например, текущая команда длиннее следующей и если просто её отобразить, то мы увидим в консоли конец предыдущей команды. Для решения этой проблемы вычисляется разница между текущей и следующей командами, и затираем хвосты в зависимости от её значения.

static void esc_up_handler(const char* cmd) {
    if (history_pos - 1 < 0) {
        return;
    }
    --history_pos;

    // Calculate diff between current command and command from history
    int16_t remainder = current_cmd.len - history[history_pos]->len;

    // Move cursor to begin of command
    while (cursor_pos) {
      send_data("\x1B[D", 3);
      --cursor_pos;
    }

    // Print new command
    memcpy(&current_cmd, history[history_pos], sizeof(current_cmd));
    send_data(current_cmd.cmd, current_cmd.len);
    cursor_pos += current_cmd.len;

    // Clear others symbols
    save_cursor_pos();
    for (int16_t i = 0; i < remainder; ++i) {
        send_data(" ", 1);
    }
    load_cursor_pos();
}

static void esc_down_handler(const char* cmd) {
    if (history_pos + 1 > history_len) {
        return;
    }
    ++history_pos;

    int16_t remainder = 0;
    if (history_pos < history_len) {

       // Calculate diff between current command and command from history
       remainder = current_cmd.len - history[history_pos]->len;

       // Move cursor to begin of command
       while (cursor_pos) {
           send_data("\x1B[D", 3);
           --cursor_pos;
       }

       // Print new command
       memcpy(&current_cmd, history[history_pos], sizeof(current_cmd));
       send_data(current_cmd.cmd, current_cmd.len);
       cursor_pos += current_cmd.len;
    } 
    else {
       remainder = current_cmd.len;

       // Move cursor to begin of command
       while (cursor_pos) {
           send_data("\x1B[D", 3);
           --cursor_pos;
       }

       memset(&current_cmd, 0, sizeof(current_cmd));
    }

    // Clear others symbols
    save_cursor_pos();
    for (int16_t i = 0; i < remainder; ++i) {
        send_data(" ", 1);
    }
    load_cursor_pos();
}

Вот, кстати, пример одновременного перемещения курсора на стороне сервера и на стороне клиента. В данном случае курсор перемещается в начало команды.

while (cursor_pos) {
    send_data("\x1B[D", 3);
    --cursor_pos;
}

Авто завершение. Я не стал запариваться с умным авто завершением и сделал по однозначному совпадению. Т.е. если у нас есть 2 похожих команды (command1, command2), то при вводе "comm" и нажатии TAB авто завершение не сработает. А вот если команда command1 единственная в своем роде, то при вводе "co" команда автоматически допишется.

Результат

Надеюсь он кому-нибудь будет полезен. Всем спасибо за внимание!

Источник: https://habr.com/ru/post/572630/


Интересные статьи

Интересные статьи

Перед вами четвёртый материал о разработке REST-серверов на Go. Здесь мы поговорим о том, как можно воспользоваться OpenAPI и Swagger для реализации стандартизированного подхода к описани...
На конференции Linux Plumbers Conference 2020 разработчики Microsoft рассказали о своем экспериментальном проекте — оптимизации ядра Linux для серверных ARM-процессоров. С докладо...
Процедурные макросы в Rust — это очень мощный инструмент кодогенерации, позволяющий обходиться без написания тонны шаблонного кода, или выражать какие-то новые концепции, как сделали, к примеру, ...
Если в вашей компании хотя бы два сотрудника, отвечающих за работу со сделками в Битрикс24, рано или поздно возникает вопрос распределения лидов между ними.
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...