#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <langinfo.h>
#include <linux/input.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fira_mono_tables.h>
#include <keymap.h>
#include <shell.h>
static void
germ_init(void);
static void
germ_interact(void);
int
main(void)
{
germ_init();
for (;;)
germ_interact();
}
static void
germ_io_add(int fd, uint32_t events);
struct germ {
unsigned char *buffer;
unsigned int back_buffer:1;
unsigned int back_buffer_is_locked:1;
unsigned int is_draw_needed:1;
uint32_t keyboard_modifiers;
uint32_t width, height;
int epoll_fd;
int line_size;
char *command_buffer;
int cursor;
};
#define array_size(a) (sizeof(a) / sizeof((a)[0]))
static _Noreturn void
die(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stderr, "Error: ");
vfprintf(stderr, fmt, args);
va_end(args);
fputc('\n', stderr);
exit(1);
}
void *
malloc_or_die(size_t size)
{
void *x = malloc(size);
if (size > 0 && x == NULL)
die("Failed to allocate memory.");
return x;
}
static void
germ_draw(struct germ *germ);
static void
germ_launch(struct germ *germ, uint32_t width, uint32_t height,
unsigned char *buffer);
static void
germ_on_input(struct germ *germ, uint16_t code, int32_t value);
#define BYTES_PER_PIXEL 4
// TODO Font metrics should be stored with the font bitmaps, no?
static struct metrics {
int line_spacing;
int advance;
int baseline;
int border_size_top;
int border_size_bottom;
int border_size_left;
int border_size_right;
} metrics;
static void
on_buffer_unlocked(struct germ *germ)
{
germ->back_buffer_is_locked = 0;
if (germ->is_draw_needed)
germ_draw(germ);
}
static uint32_t
encode_color_xrgb(uint8_t r, uint8_t g, uint8_t b)
{
union {
uint32_t u;
uint8_t bytes[4];
} x = {
.bytes = { b, g, r, 0 },
};
return x.u;
}
static void
draw_pixmap(void *mem, const uint8_t *bytes, int rows, int width,
int x_start, int y_start, uint32_t stride)
{
// The transformation from input bytes to output color values is the
// linear mapping that maps the extremes as follows:
//
// 0x00 => 0xff
// 0xff => 0x47
//
// In other words, a clear byte (0x00) is mapped to white (0xff) and a
// fully-set byte (0xff) is mapped to a dark gray (0x47).
uint32_t *pixels = mem;
uint32_t stride_in_pixels = stride / BYTES_PER_PIXEL;
int x_end = x_start + width;
int y_end = y_start + rows;
for (int y = y_start, v = 0; y < y_end; y++, v++) {
for (int x = x_start, u = 0; x < x_end; x++, u++) {
uint32_t byte = bytes[v * width + u];
uint32_t value = (byte * 0x48 + (0xff - byte) * 0x100) / 0x100;
uint32_t color = encode_color_xrgb(value, value, value);
pixels[y * stride_in_pixels + x] = color;
}
}
}
static int
clamp(int x, int a, int b)
{
if (x < a)
return a;
if (x > b)
return b;
return x;
}
static uint32_t
find_bitmap_offset(int codepoint)
{
// Assumption: interval_starts is not empty.
// Assumption: interval_starts is strictly increasing.
// Assumption: interval_starts[0] == 0.
// Assumption: intervals do not overlap.
// Assumption: The null glyph has bitmap offset 0.
int i = 0;
while (i + 1 < array_size(interval_starts) && interval_starts[i + 1] <= codepoint)
i++;
// Invariant: i is the highest index such that interval_starts[i] <= codepoint.
if (codepoint >= interval_starts[i] + interval_sizes[i]) {
// Invariant: codepoint is not part of any interval.
return 0;
}
// Invariant: codepoint belongs to interval i.
int j = codepoint - interval_starts[i];
while (--i >= 0)
j += interval_sizes[i];
return bitmap_offsets[j];
}
static void
draw_text(void *mem, uint32_t stride, const char *text)
{
int pen_y = metrics.border_size_top + metrics.baseline;
int pen_x = metrics.border_size_left;
for (int j = 0; text[j] != '\0'; j++) {
uint32_t bitmap_offset = find_bitmap_offset(text[j]);
int top = bitmap_bytes[bitmap_offset + 0];
if (top > 127) top -= 256;
int left = bitmap_bytes[bitmap_offset + 1];
if (left > 127) left -= 256;
int rows = bitmap_bytes[bitmap_offset + 2];
int width = bitmap_bytes[bitmap_offset + 3];
draw_pixmap(mem, &bitmap_bytes[bitmap_offset + 4],
rows, width,
pen_x + left, pen_y - top, stride);
pen_x += metrics.advance;
}
}
static void
draw_cursor(void *mem, uint32_t stride, int cursor)
{
uint32_t *pixels = mem;
uint32_t stride_in_pixels = stride / BYTES_PER_PIXEL;
int x_start = metrics.border_size_left + cursor * metrics.advance;
int y_start =
metrics.border_size_top + metrics.line_spacing - 5;
int x_end = clamp(x_start + metrics.advance, 0, stride_in_pixels);
int y_end = y_start + 3;
int guide_start = (y_end - 2) * stride_in_pixels;
int guide_end = guide_start + stride_in_pixels;
{
uint32_t color = encode_color_xrgb(0xcc, 0xcc, 0xcc);
for (int i = guide_start; i < guide_end; i++)
pixels[i] = color;
}
{
uint32_t color = encode_color_xrgb(0x48, 0x48, 0x48);
for (int y = y_start; y < y_end; y++)
for (int x = x_start; x < x_end; x++)
pixels[y * stride_in_pixels + x] = color;
}
}
static void
draw_background(void *mem, uint32_t width, uint32_t height)
{
uint32_t color = encode_color_xrgb(0xff, 0xff, 0xff);
uint32_t *pixel = mem;
for (uint32_t y = 0; y < height; y++)
for (uint32_t x = 0; x < width; x++)
*pixel++ = color;
}
static void
send_reset(int fd);
static void
send_show(int fd);
static void
germ_draw(struct germ *germ)
{
uint32_t stride;
unsigned char *buffer;
if (germ->back_buffer_is_locked) {
germ->is_draw_needed = 1;
return;
}
buffer = germ->buffer +
(germ->back_buffer * BYTES_PER_PIXEL * germ->width * germ->height);
draw_background(buffer, germ->width, germ->height);
stride = BYTES_PER_PIXEL * germ->width;
draw_cursor(buffer, stride, germ->cursor);
draw_text(buffer, stride, germ->command_buffer);
send_show(3);
germ->back_buffer = !germ->back_buffer;
germ->back_buffer_is_locked = 1;
germ->is_draw_needed = 0;
}
static void
init_metrics(void)
{
metrics.advance = 10;
metrics.line_spacing = 24;
metrics.baseline = 14;
metrics.border_size_top = 10;
metrics.border_size_bottom = 10;
metrics.border_size_left = 5;
metrics.border_size_right = 5;
}
static void
germ_launch(struct germ *germ, uint32_t width, uint32_t height,
unsigned char *buffer)
{
germ->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (germ->epoll_fd == -1)
die("Failed to initialize.");
germ->buffer = buffer;
germ->back_buffer = 1;
germ->back_buffer_is_locked = 0;
germ->is_draw_needed = 0;
germ->width = width;
germ->height = height;
germ->keyboard_modifiers = 0;
}
#define MOD_CONTROL_MASK 1
#define MOD_ALT_MASK 2
#define MOD_SHIFT_MASK 4
static void
germ_on_input(struct germ *germ, uint16_t code, int32_t value)
{
{
// TODO Handle the following weird case: left-shift is pressed,
// right-shift is pressed, left-shift is released: the modifier_mask
// should still indicate that shift is pressed.
uint32_t modifier_mask = 0;
switch (code) {
case KEY_CAPSLOCK:
modifier_mask = MOD_CONTROL_MASK;
break;
case KEY_LEFTALT:
case KEY_RIGHTALT:
modifier_mask = MOD_ALT_MASK;
break;
case KEY_LEFTSHIFT:
case KEY_RIGHTSHIFT:
modifier_mask = MOD_SHIFT_MASK;
break;
}
if (modifier_mask != 0) {
if (value == 0)
germ->keyboard_modifiers &= ~modifier_mask;
else
germ->keyboard_modifiers |= modifier_mask;
return;
}
}
if (value == 0)
return;
if (code == KEY_BACKSPACE && germ->cursor > 0) {
memmove(germ->command_buffer + (germ->cursor - 1),
germ->command_buffer + germ->cursor,
strlen(germ->command_buffer + germ->cursor) + 1);
germ->cursor--;
germ_draw(germ);
return;
}
switch (code) {
case KEY_UP:
return;
case KEY_DOWN:
return;
case KEY_RIGHT:
if (germ->command_buffer[germ->cursor] != '\0') {
germ->cursor++;
germ_draw(germ);
}
return;
case KEY_LEFT:
if (germ->cursor > 0) {
germ->cursor--;
germ_draw(germ);
}
return;
}
if (code == KEY_ENTER) {
send_reset(3);
return;
}
char ascii = (germ->keyboard_modifiers & MOD_SHIFT_MASK) ?
keymap_lookup_shifted(code) : keymap_lookup(code);
if (germ->keyboard_modifiers & MOD_CONTROL_MASK) {
switch (ascii) {
case 'u':
if (germ->cursor != 0) {
memmove(germ->command_buffer,
germ->command_buffer + germ->cursor,
strlen(germ->command_buffer + germ->cursor) + 1);
germ->cursor = 0;
germ_draw(germ);
}
return;
case 'w':
{
int cursor_begin = germ->cursor;
while (germ->cursor > 0 && germ->command_buffer[germ->cursor - 1] == ' ')
germ->cursor--;
while (germ->cursor > 0 && germ->command_buffer[germ->cursor - 1] != ' ')
germ->cursor--;
memmove(germ->command_buffer + germ->cursor,
germ->command_buffer + cursor_begin,
strlen(germ->command_buffer + cursor_begin) + 1);
}
germ_draw(germ);
return;
case 'd':
if (germ->command_buffer[germ->cursor] != '\0') {
memmove(germ->command_buffer + germ->cursor,
germ->command_buffer + (germ->cursor + 1),
strlen(germ->command_buffer + germ->cursor + 1) + 1);
germ_draw(germ);
}
return;
case 'k':
germ->command_buffer[germ->cursor] = '\0';
germ_draw(germ);
return;
case 'f':
if (germ->command_buffer[germ->cursor] != '\0') {
germ->cursor++;
germ_draw(germ);
}
return;
case 'b':
if (germ->cursor > 0) {
germ->cursor--;
germ_draw(germ);
}
return;
case 'a':
germ->cursor = 0;
germ_draw(germ);
return;
case 'e':
while (germ->command_buffer[germ->cursor] != '\0')
germ->cursor++;
germ_draw(germ);
return;
}
}
if (ascii != '\0') {
switch (ascii) {
case ' ':
case '-':
case '_':
case '/':
case '.':
break;
default:
if (!isalnum(ascii))
return;
break;
}
if (strlen(germ->command_buffer) + 1 >= germ->line_size)
return;
memmove(germ->command_buffer + (germ->cursor + 1),
germ->command_buffer + germ->cursor,
strlen(germ->command_buffer + germ->cursor) + 1);
germ->command_buffer[germ->cursor] = ascii;
germ->cursor++;
germ_draw(germ);
}
}
static void
receive_welcome(int fd, struct shell_message_welcome *message)
{
ssize_t r = read(fd, message, sizeof(*message));
if (r != sizeof(*message))
die("Failed to read welcome message.");
if (message->class != SHELL_MESSAGE_WELCOME)
die("Failed to receive welcome.");
}
static void
receive_keyboard_event(void *data, struct germ *germ)
{
struct shell_message_keyboard_event *message = data;
germ_on_input(germ, message->code, message->value);
}
static void
receive_buffer_unlocked(void *data, struct germ *germ)
{
(void)data;
on_buffer_unlocked(germ);
}
static void
receive_keyboard_detached(void *data)
{
(void)data;
fprintf(stderr, "Keyboard detached.\n");
}
static int
fetch_bit(const uint8_t *bits, unsigned i)
{
return (bits[i / 8] & (1 << (i % 8))) ? 1 : 0;
}
static void
receive_keyboard_attached(void *data, struct germ *germ)
{
struct shell_message_keyboard_attached *message = data;
const uint8_t *key_bits = message->key_bits;
if (fetch_bit(key_bits, KEY_CAPSLOCK))
germ->keyboard_modifiers |= MOD_CONTROL_MASK;
else
germ->keyboard_modifiers &= ~MOD_CONTROL_MASK;
if (fetch_bit(key_bits, KEY_LEFTALT) || fetch_bit(key_bits, KEY_RIGHTALT))
germ->keyboard_modifiers |= MOD_ALT_MASK;
else
germ->keyboard_modifiers &= ~MOD_ALT_MASK;
if (fetch_bit(key_bits, KEY_LEFTSHIFT) || fetch_bit(key_bits, KEY_RIGHTSHIFT))
germ->keyboard_modifiers |= MOD_SHIFT_MASK;
else
germ->keyboard_modifiers &= ~MOD_SHIFT_MASK;
fprintf(stderr, "Keyboard attached.\n");
}
static void
send_reset(int fd)
{
struct shell_message_reset message = {
.class = SHELL_MESSAGE_RESET,
};
ssize_t r = write(fd, &message, sizeof(message));
if (r != sizeof(message))
die("Failed to write reset message.");
}
static void
send_show(int fd)
{
struct shell_message_show message = {
.class = SHELL_MESSAGE_SHOW,
};
ssize_t r = write(fd, &message, sizeof(message));
if (r != sizeof(message))
die("Failed to write show message.");
}
static void
handle_shell_message(int fd, struct germ *germ, uint32_t events)
{
if (events & EPOLLHUP)
exit(0);
if (events & EPOLLIN) {
static _Alignas(max_align_t) char packet[SHELL_PACKET_SIZE_MAX];
int r;
do {
r = read(fd, packet, sizeof(packet));
} while (r == -1 && errno == EINTR);
if (r <= 0)
die("Failed to read from shell socket.");
uint16_t class = *(uint16_t *)packet;
switch (class) {
case SHELL_MESSAGE_RESET_COMPLETED:
{
// Count whitespace-separated "words" to determine argc.
int argc = 0;
for (int i = 0, state = ' '; germ->command_buffer[i] != '\0'; i++) {
if (state == ' ' && germ->command_buffer[i] != ' ') {
argc++;
state = '-';
}
if (germ->command_buffer[i] == ' ')
state = ' ';
}
if (argc == 0)
die("Failed to launch client.");
// Construct argv.
char *argv[argc + 1];
argc = 0;
for (int i = 0, state = ' '; germ->command_buffer[i] != '\0'; i++) {
if (state == ' ' && germ->command_buffer[i] != ' ') {
argv[argc++] = germ->command_buffer + i;
state = '-';
}
if (germ->command_buffer[i] == ' ') {
germ->command_buffer[i] = '\0';
state = ' ';
}
}
argv[argc] = NULL;
// Exec!
execvp(argv[0], argv);
die("Failed to launch client.");
}
break;
case SHELL_MESSAGE_KEYBOARD_EVENT:
receive_keyboard_event(packet, germ);
break;
case SHELL_MESSAGE_BUFFER_UNLOCKED:
receive_buffer_unlocked(packet, germ);
break;
case SHELL_MESSAGE_KEYBOARD_DETACHED:
receive_keyboard_detached(packet);
break;
case SHELL_MESSAGE_KEYBOARD_ATTACHED:
receive_keyboard_attached(packet, germ);
break;
}
}
}
static struct germ germ;
static void
germ_init(void)
{
init_metrics();
uint32_t width, height;
{
struct shell_message_welcome message;
receive_welcome(3, &message);
width = message.width;
height = message.height;
}
size_t mem_size = 8 * width * height;
void *mem = mmap(NULL, mem_size, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0);
if (mem == MAP_FAILED)
die("Failed to create draw buffers for child process.");
germ_launch(&germ, width, height, mem);
germ_io_add(3, EPOLLIN);
germ.line_size =
(width - metrics.border_size_left - metrics.border_size_right) /
metrics.advance;
germ.command_buffer = malloc_or_die(germ.line_size);
germ.command_buffer[0] = '\0';
germ.cursor = 0;
germ_draw(&germ);
}
static void
germ_interact(void)
{
struct epoll_event events[2];
int num_events;
{
int r;
do {
r = epoll_wait(germ.epoll_fd, events, array_size(events), -1);
} while (r == -1 && errno == EINTR);
if (r == -1)
die("epoll_wait: %m");
num_events = r;
}
for (int i = 0; i < num_events; i++) {
int fd = events[i].data.fd;
if (fd == 3)
handle_shell_message(3, &germ, events[i].events);
}
}
static void
germ_io_add(int fd, uint32_t events)
{
struct epoll_event event = {
.data = { .fd = fd },
.events = events,
};
int r = epoll_ctl(germ.epoll_fd, EPOLL_CTL_ADD, fd, &event);
if (r == -1)
die("epoll_ctl: %m");
}