System 59

File

system59-0.1/germ.c

#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");
}