Pular para o conteúdo

Console e Emulação de Terminal (Sequências de Escape ANSI)

VGA & Framebuffers já cobre desenhar um caractere ou um pixel na tela; um console é a camada que a maioria dos kernels constrói diretamente em cima disso, interpretando um fluxo de bytes contendo sequências de escape embutidas para cor, movimento de cursor, e rolagem, a mesma interface que qualquer programa de terminal comum (vim, less, um prompt de shell) já espera poder escrever sem modificação alguma.

Toda sequência de escape ANSI começa com o byte único 0x1B (ESC), e a família mais comum, sequências CSI (Control Sequence Introducer), o segue com [, depois zero ou mais parâmetros numéricos separados por ;, depois uma única letra identificando o comando.

ESC [ 3 1 ; 1 m
^^^^^^^ ^
params letra de comando (m = SGR, Select Graphic Rendition)

ESC[31;1m define negrito, primeiro plano vermelho; ESC[2J (sem parâmetros, comando J) limpa a tela inteira; ESC[10;5H move o cursor para linha 10, coluna 5. SGR (m) é a família com a qual um console gasta mais tempo na prática, já que é o que um programa usa para toda mudança de cor e atributo de texto, com parâmetros tanto para as 16 cores clássicas (30-37 primeiro plano, 40-47 fundo) quanto, num console que suporta, variantes estendidas de 256 cores e RGB de 24 bits através de mais sub-sequências de parâmetro.

Um console não pode presumir que uma sequência de escape inteira chega numa única leitura do que quer que esteja alimentando sua saída, um byte de cada vez vindo de um produtor lento, ou dividida arbitrariamente entre duas escritas separadas são ambos casos normais, então a análise precisa ser uma máquina de estados que retoma corretamente entre chamadas em vez de uma única função que espera uma sequência completa num único buffer.

enum console_state { STATE_NORMAL, STATE_ESCAPE, STATE_CSI };
void console_putchar(uint8_t c) {
static enum console_state state = STATE_NORMAL;
static int params[8], param_count = 0;
switch (state) {
case STATE_NORMAL:
if (c == 0x1B) { state = STATE_ESCAPE; return; }
draw_char(c);
return;
case STATE_ESCAPE:
if (c == '[') { state = STATE_CSI; param_count = 0; params[0] = 0; return; }
state = STATE_NORMAL;
return;
case STATE_CSI:
if (c >= '0' && c <= '9') { params[param_count] = params[param_count] * 10 + (c - '0'); return; }
if (c == ';') { param_count++; params[param_count] = 0; return; }
apply_csi_command(c, params, param_count + 1); // c é a letra de comando
state = STATE_NORMAL;
return;
}
}

Todo byte avança a máquina de estados exatamente um passo independentemente de quando chega em relação ao resto de sua sequência, acumulando parâmetros numéricos conforme dígitos chegam e só de fato executando um comando uma vez que a letra terminadora é vista; um byte que não se encaixa no estado atual (um caractere inesperado no meio de uma sequência CSI, por exemplo) volta para STATE_NORMAL em vez de deixar o parser preso no meio de uma sequência que nunca vai completar corretamente.

Rolar o console para cima em uma linha, a operação que acontece em essencialmente toda quebra de linha uma vez que o cursor alcança a linha de baixo, é implementada como uma cópia de memória: toda linha, da segunda até a última, é copiada para cima o equivalente a uma linha de bytes, e a nova linha exposta embaixo é limpa para a cor de fundo atual, tudo isso sem tocar linha alguma que não precisava de fato se mover.

void console_scroll(void) {
memmove(video_mem, video_mem + ROW_BYTES, (ROWS - 1) * ROW_BYTES);
memset(video_mem + (ROWS - 1) * ROW_BYTES, 0, ROW_BYTES);
}

Isso é consideravelmente mais barato do que redesenhar a tela inteira caractere por caractere, já que é uma cópia em massa de dados de pixel ou célula de caractere já formatados em vez de uma re-renderização de qualquer coisa: as linhas mantidas não precisam ter sua cor ou conteúdo recalculados de forma alguma, só realocados, e num framebuffer linear isso se reduz exatamente ao tipo de movimentação grande e sequencial de memória que hardware trata com eficiência.

Um console serial precisa do mesmo parser, na direção oposta

Seção intitulada “Um console serial precisa do mesmo parser, na direção oposta”

Um console operado através de uma porta serial em vez de um framebuffer precisa do mesmo tratamento de sequência de escape idêntico, mas rodando na direção oposta ao que este artigo cobre até aqui: em vez do kernel emitir sequências para um terminal do host interpretar, é o kernel recebendo sequências (teclas de cursor, por exemplo, que um terminal codifica como suas próprias sequências ESC[ de múltiplos bytes em vez de um único caractere) de qualquer emulador de terminal conectado do outro lado da linha serial, exigindo a mesma análise de máquina de estados descrita acima, aplicada à entrada em vez de à saída.

Uma implementação de console precisa decidir, deliberadamente, quais das muitas sequências de escape acumuladas por décadas de história de terminal ela de fato suporta: implementar o subconjunto comum VT100/ANSI (movimento de cursor, cores SGR, limpeza de tela) cobre a grande maioria do uso do mundo real, enquanto sequências obscuras de tipos históricos específicos de terminal são seguras de simplesmente ignorar, analisadas o suficiente para serem consumidas sem travar a máquina de estados mas sem agir sobre elas. Um programa que consulta as capacidades do console (via TERM num ambiente hospedado, embora um kernel hobbyist tipicamente não tenha negociação equivalente alguma e em vez disso fixe no código um conjunto de recursos presumido) e recebe de volta uma sequência de escape que não esperava ter que tratar é uma fonte comum de saída embaralhada, motivo pelo qual se ater a um subconjunto bem conhecido e estreito em vez de inventar sequências customizadas mantém a saída de um console previsível através de qualquer software de terminal que de fato se conecte a ele.

  1. ^ ECMA International, ECMA-48: Control Functions for Coded Character Sets: a especificação formal por trás das sequências de escape estilo ANSI/VT100.
  • VGA & Framebuffers: a camada de desenho sobre a qual o interpretador de sequência de escape deste console fica.
  • Serial Port: onde um console precisa da mesma análise aplicada a sequências chegando na direção oposta.