cowterm/cowterm.c

972 lines
28 KiB
C
Raw Normal View History

2025-01-15 23:36:48 +00:00
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <errno.h>
#include <fcntl.h>
#include <pty.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#include "config.h"
// There isn't a need for MAX_ROWS or MAX_COLS, but ¯\_(ツ)_/¯
#define MAX_ROWS 1000
#define MAX_COLS 1000
#define CHAR_WIDTH 8
#define CHAR_HEIGHT 16
static pid_t child_pid;
static char **terminal_buffer = NULL;
static unsigned long **color_buffer = NULL;
static int **attr_buffer = NULL;
static int term_rows;
static int term_cols;
static int cursor_x = 0;
static int cursor_y = 0;
static unsigned long current_color;
static int current_attr = 0;
static GC gc = NULL;
static GC gc_bold = NULL;
static Display *display = NULL;
static int cursor_visible = 1;
static struct timeval last_blink;
static struct timeval last_refresh;
2025-01-15 23:36:48 +00:00
static Pixmap buffer_pixmap = None;
static int master_fd = -1;
static int needs_refresh = 0;
2025-01-15 23:36:48 +00:00
static XFontStruct *regular_font = NULL;
static XFontStruct *bold_font = NULL;
static XFontStruct *italic_font = NULL;
static int window_focused = 0;
2025-01-15 23:36:48 +00:00
// Alternatives to handle the ANSI escape codes
static char **saved_terminal = NULL;
static unsigned long **saved_colors = NULL;
static int **saved_attrs = NULL;
static int saved_cursor_x = 0;
static int saved_cursor_y = 0;
static int using_alternate = 0;
2025-01-15 23:36:48 +00:00
#define ATTR_BOLD 1
#define ATTR_ITALIC 2
// Pesky static smth smth error, so we "declare" it early
static void draw_terminal(Display *display, Window window);
static void draw_char(Display *display, Drawable d, int x, int y);
// Cursor gameplay <== ==>
static void draw_cursor(Display *display, Window window) {
if (!gc || !terminal_buffer || !color_buffer || cursor_y >= term_rows ||
cursor_x >= term_cols)
return;
// First restore the character that was under the cursor
if (cursor_x < term_cols && cursor_y < term_rows) {
draw_char(display, buffer_pixmap, cursor_x, cursor_y);
}
if (cursor_visible) {
// Save original color
unsigned long original_color = color_buffer[cursor_y][cursor_x];
if (!window_focused) {
XSetForeground(display, gc, original_color);
XDrawRectangle(display, buffer_pixmap, gc, cursor_x * CHAR_WIDTH,
cursor_y * CHAR_HEIGHT, CHAR_WIDTH - 1, CHAR_HEIGHT - 1);
} else {
XSetForeground(display, gc, original_color);
XFillRectangle(display, buffer_pixmap, gc, cursor_x * CHAR_WIDTH,
cursor_y * CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT);
if (cursor_x < term_cols && cursor_y < term_rows) {
char str[2] = {terminal_buffer[cursor_y][cursor_x], '\0'};
XSetForeground(display, gc,
XBlackPixel(display, DefaultScreen(display)));
XFontStruct *font = regular_font;
if (attr_buffer[cursor_y][cursor_x] & ATTR_BOLD) {
font = bold_font;
} else if (attr_buffer[cursor_y][cursor_x] & ATTR_ITALIC) {
font = italic_font;
}
XSetFont(display, gc, font->fid);
2025-01-15 23:36:48 +00:00
XDrawString(display, buffer_pixmap, gc, cursor_x * CHAR_WIDTH,
(cursor_y + 1) * CHAR_HEIGHT - 2, str, 1);
}
2025-01-15 23:36:48 +00:00
}
}
// Copy the relevant area to the window
XCopyArea(display, buffer_pixmap, window, gc, cursor_x * CHAR_WIDTH,
cursor_y * CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT,
cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT);
}
static void update_cursor_state(Display *display, Window window) {
if (window_focused) {
cursor_visible = 1;
return;
2025-01-15 23:36:48 +00:00
}
}
// resize_buffers visual:
//
// Old buffer (3x3): New buffer (4x4):
// [a][b][c] [a][b][c][ ]
// [d][e][f] -> [d][e][f][ ]
// [g][h][i] [g][h][i][ ]
// [ ][ ][ ][ ]
//
// 1. Allocates new larger buffer
// 2. Copies existing content
// 3. Fills new space with blanks
// 4. Updates cursor position
// 5. Frees old buffer
static void resize_buffers(int new_rows, int new_cols) {
if (new_rows > MAX_ROWS)
new_rows = MAX_ROWS;
if (new_cols > MAX_COLS)
new_cols = MAX_COLS;
if (new_rows < 1)
new_rows = 1;
if (new_cols < 1)
new_cols = 1;
size_t row_size = ((new_cols * sizeof(char) + 15) & ~15);
size_t color_row_size = ((new_cols * sizeof(unsigned long) + 15) & ~15);
size_t attr_row_size = ((new_cols * sizeof(int) + 15) & ~15);
char **new_term_buffer = malloc(new_rows * sizeof(char *));
unsigned long **new_color_buffer = malloc(new_rows * sizeof(unsigned long *));
int **new_attr_buffer = malloc(new_rows * sizeof(int *));
if (!new_term_buffer || !new_color_buffer || !new_attr_buffer) {
fprintf(stderr, "Failed to allocate memory for buffers\n");
return;
}
for (int i = 0; i < new_rows; i++) {
new_term_buffer[i] = malloc(row_size);
new_color_buffer[i] = malloc(color_row_size);
new_attr_buffer[i] = malloc(attr_row_size);
if (!new_term_buffer[i] || !new_color_buffer[i] || !new_attr_buffer[i]) {
fprintf(stderr, "Failed to allocate memory for buffer row\n");
// Clean up allocated memory
for (int j = 0; j < i; j++) {
free(new_term_buffer[j]);
free(new_color_buffer[j]);
free(new_attr_buffer[j]);
}
free(new_term_buffer);
free(new_color_buffer);
free(new_attr_buffer);
return;
}
if (i < term_rows && i < new_rows && terminal_buffer && color_buffer &&
attr_buffer) {
int copy_cols = (new_cols < term_cols) ? new_cols : term_cols;
memcpy(new_term_buffer[i], terminal_buffer[i], copy_cols * sizeof(char));
memcpy(new_color_buffer[i], color_buffer[i],
copy_cols * sizeof(unsigned long));
memcpy(new_attr_buffer[i], attr_buffer[i], copy_cols * sizeof(int));
if (new_cols > term_cols) {
memset(new_term_buffer[i] + term_cols, ' ', new_cols - term_cols);
for (int x = term_cols; x < new_cols; x++) {
new_color_buffer[i][x] = current_color;
new_attr_buffer[i][x] = current_attr;
}
}
} else {
memset(new_term_buffer[i], ' ', new_cols);
for (int x = 0; x < new_cols; x++) {
new_color_buffer[i][x] = current_color;
new_attr_buffer[i][x] = current_attr;
}
}
}
// Free old buffers after successful allocation
if (terminal_buffer && color_buffer && attr_buffer) {
for (int i = 0; i < term_rows; i++) {
free(terminal_buffer[i]);
free(color_buffer[i]);
free(attr_buffer[i]);
}
free(terminal_buffer);
free(color_buffer);
free(attr_buffer);
}
terminal_buffer = new_term_buffer;
color_buffer = new_color_buffer;
attr_buffer = new_attr_buffer;
term_rows = new_rows;
term_cols = new_cols;
cursor_x = (cursor_x >= term_cols) ? term_cols - 1 : cursor_x;
cursor_y = (cursor_y >= term_rows) ? term_rows - 1 : cursor_y;
// Resize buffer pixmap
if (buffer_pixmap != None) {
XFreePixmap(display, buffer_pixmap);
}
buffer_pixmap = XCreatePixmap(display, DefaultRootWindow(display),
term_cols * CHAR_WIDTH, term_rows * CHAR_HEIGHT,
DefaultDepth(display, DefaultScreen(display)));
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
static void destroy_cb(Display *display, Window window) {
if (child_pid > 0) {
kill(child_pid, SIGTERM);
waitpid(child_pid, NULL, 0);
}
if (gc)
XFreeGC(display, gc);
if (gc_bold)
XFreeGC(display, gc_bold);
if (buffer_pixmap != None)
XFreePixmap(display, buffer_pixmap);
if (display)
XCloseDisplay(display);
if (terminal_buffer) {
for (int i = 0; i < term_rows; i++) {
free(terminal_buffer[i]);
}
free(terminal_buffer);
}
if (color_buffer) {
for (int i = 0; i < term_rows; i++) {
free(color_buffer[i]);
}
free(color_buffer);
}
if (attr_buffer) {
for (int i = 0; i < term_rows; i++) {
free(attr_buffer[i]);
}
free(attr_buffer);
}
if (master_fd >= 0)
close(master_fd);
exit(0);
}
static void draw_char(Display *display, Drawable d, int x, int y) {
if (!gc || !terminal_buffer || !color_buffer || !attr_buffer)
return;
if (x < 0 || x >= term_cols || y < 0 || y >= term_rows)
return;
// Clear the character position first
XSetForeground(display, gc, XBlackPixel(display, DefaultScreen(display)));
XFillRectangle(display, d, gc, x * CHAR_WIDTH, y * CHAR_HEIGHT, CHAR_WIDTH,
CHAR_HEIGHT);
char str[2] = {terminal_buffer[y][x], '\0'};
XSetForeground(display, gc, color_buffer[y][x]);
// Select appropriate font based on attributes
XFontStruct *font = regular_font;
if (attr_buffer[y][x] & ATTR_BOLD) {
font = bold_font;
} else if (attr_buffer[y][x] & ATTR_ITALIC) {
font = italic_font;
}
XSetFont(display, gc, font->fid);
XDrawString(display, d, gc, x * CHAR_WIDTH, (y + 1) * CHAR_HEIGHT - 2, str,
1);
}
static void draw_terminal(Display *display, Window window) {
if (!gc) {
gc = XCreateGC(display, window, 0, NULL);
regular_font = XLoadQueryFont(display, REGULAR_FONT);
bold_font = XLoadQueryFont(display, BOLD_FONT);
italic_font = XLoadQueryFont(display, ITALICS_FONT);
2025-01-15 23:36:48 +00:00
if (!regular_font)
regular_font = XLoadQueryFont(display, "fixed");
if (!bold_font)
bold_font = regular_font;
if (!italic_font)
italic_font = regular_font;
XSetFont(display, gc, regular_font->fid);
}
if (!terminal_buffer || !color_buffer || !attr_buffer)
return;
if (buffer_pixmap == None) {
buffer_pixmap = XCreatePixmap(
display, DefaultRootWindow(display), term_cols * CHAR_WIDTH,
term_rows * CHAR_HEIGHT, DefaultDepth(display, DefaultScreen(display)));
}
// Draw to buffer first
for (int y = 0; y < term_rows; y++) {
for (int x = 0; x < term_cols; x++) {
draw_char(display, buffer_pixmap, x, y);
}
}
// Copy buffer to window
XCopyArea(display, buffer_pixmap, window, gc, 0, 0, term_cols * CHAR_WIDTH,
term_rows * CHAR_HEIGHT, 0, 0);
draw_cursor(display, window);
XFlush(display);
needs_refresh = 0;
}
static void check_refresh(Display *display, Window window) {
if (!needs_refresh)
return;
struct timeval now;
gettimeofday(&now, NULL);
long elapsed = (now.tv_sec - last_refresh.tv_sec) * 1000000 +
(now.tv_usec - last_refresh.tv_usec);
if (elapsed >= REFRESH_INTERVAL) {
draw_terminal(display, window);
last_refresh = now;
}
2025-01-15 23:36:48 +00:00
}
static void clear_terminal(Display *display, Window window) {
if (!terminal_buffer || !color_buffer || !attr_buffer)
return;
for (int y = 0; y < term_rows; y++) {
memset(terminal_buffer[y], ' ', term_cols);
for (int x = 0; x < term_cols; x++) {
color_buffer[y][x] = current_color;
attr_buffer[y][x] = current_attr;
}
}
cursor_x = 0;
cursor_y = 0;
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
// Scroll the terminal up one line
static void scroll_up(void) {
// Move all lines up one position
for (int y = 0; y < term_rows - 1; y++) {
memcpy(terminal_buffer[y], terminal_buffer[y + 1], term_cols);
memcpy(color_buffer[y], color_buffer[y + 1],
term_cols * sizeof(unsigned long));
memcpy(attr_buffer[y], attr_buffer[y + 1], term_cols * sizeof(int));
}
// Clear the bottom line
memset(terminal_buffer[term_rows - 1], ' ', term_cols);
for (int x = 0; x < term_cols; x++) {
color_buffer[term_rows - 1][x] = current_color;
attr_buffer[term_rows - 1][x] = current_attr;
}
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
// Alternative buffer visual:
//
// Normal buffer: Alternate buffer:
// [C][o][w] [ ][ ][ ]
// [T][e][r] [ ][ ][ ]
// [m][!][ ] <-> [ ][ ][ ]
//
// When applications like vim switch to alternate buffer it SHOULD:
// 1. Save the current buffer state (saved_terminal etc.)
// 2. creates a new empty alternate buffer
// 3. The application draws to the alternate buffer
// 4. When done, alternate buffer is freed
// 5. And the original buffer state is restored
//
// References:
// saved_terminal = backup of normal buffer
// saved_colors/attrs = backup of attributes
// using_alternate = tracks which buffer is active
static void handle_alternate_buffer(const char *buf, int *idx, int max_len) {
char num_buf[32] = {0};
size_t num_idx = 0;
(*idx)++;
while (*idx < max_len && buf[*idx] != 'h' && buf[*idx] != 'l') {
if (num_idx >= sizeof(num_buf) - 1)
break;
if (buf[*idx] >= '0' && buf[*idx] <= '9') {
num_buf[num_idx++] = buf[*idx];
}
(*idx)++;
}
if (num_buf[0] != '\0') {
int code = atoi(num_buf);
if (buf[*idx] == 'h' || buf[*idx] == 'l') {
switch (code) {
case 1047: // Alternate screen buffer
if (buf[*idx] == 'h') {
if (!saved_terminal) {
saved_terminal = terminal_buffer;
saved_colors = color_buffer;
saved_attrs = attr_buffer;
terminal_buffer = malloc(term_rows * sizeof(char *));
color_buffer = malloc(term_rows * sizeof(unsigned long *));
attr_buffer = malloc(term_rows * sizeof(int *));
for (int i = 0; i < term_rows; i++) {
terminal_buffer[i] = calloc(term_cols, sizeof(char));
color_buffer[i] = calloc(term_cols, sizeof(unsigned long));
attr_buffer[i] = calloc(term_cols, sizeof(int));
}
using_alternate = 1;
}
} else if (using_alternate) {
for (int i = 0; i < term_rows; i++) {
free(terminal_buffer[i]);
free(color_buffer[i]);
free(attr_buffer[i]);
}
free(terminal_buffer);
free(color_buffer);
free(attr_buffer);
terminal_buffer = saved_terminal;
color_buffer = saved_colors;
attr_buffer = saved_attrs;
saved_terminal = NULL;
using_alternate = 0;
}
break;
case 1048: // Save/restore cursor
if (buf[*idx] == 'h') {
saved_cursor_x = cursor_x;
saved_cursor_y = cursor_y;
} else {
cursor_x = saved_cursor_x;
cursor_y = saved_cursor_y;
}
break;
case 1049: // Combined alternate buffer + cursor
if (buf[*idx] == 'h') {
saved_cursor_x = cursor_x;
saved_cursor_y = cursor_y;
if (!saved_terminal) {
saved_terminal = terminal_buffer;
saved_colors = color_buffer;
saved_attrs = attr_buffer;
terminal_buffer = malloc(term_rows * sizeof(char *));
color_buffer = malloc(term_rows * sizeof(unsigned long *));
attr_buffer = malloc(term_rows * sizeof(int *));
for (int i = 0; i < term_rows; i++) {
terminal_buffer[i] = calloc(term_cols, sizeof(char));
color_buffer[i] = calloc(term_cols, sizeof(unsigned long));
attr_buffer[i] = calloc(term_cols, sizeof(int));
}
using_alternate = 1;
}
} else if (using_alternate) {
for (int i = 0; i < term_rows; i++) {
free(terminal_buffer[i]);
free(color_buffer[i]);
free(attr_buffer[i]);
}
free(terminal_buffer);
free(color_buffer);
free(attr_buffer);
terminal_buffer = saved_terminal;
color_buffer = saved_colors;
attr_buffer = saved_attrs;
saved_terminal = NULL;
cursor_x = saved_cursor_x;
cursor_y = saved_cursor_y;
using_alternate = 0;
}
break;
}
}
}
}
2025-01-15 23:36:48 +00:00
// Handles those pesky ANSI color escape codes that terminals use.
// When \033[<number>m is sent to the terminal, it changes text color:
// 30 = Black 31 = Red 32 = Green 33 = Yellow
// 34 = Blue 35 = Magenta 36 = Cyan 37 = White
// 0 sets everything back to plain white
// P.S. yes I had to google this up
static void parse_ansi_code(const char *buf, int *idx, int max_len) {
char num_buf[32] = {0};
int params[32] = {0};
size_t num_idx = 0;
size_t param_idx = 0;
2025-01-15 23:36:48 +00:00
(*idx)++; // Skip [
// Handle '?' prefix for private sequences
if (buf[*idx] == '?') {
handle_alternate_buffer(buf, idx, max_len);
return;
}
while (*idx < max_len && buf[*idx] != 'm' && buf[*idx] != 'H' &&
buf[*idx] != 'A' && buf[*idx] != 'B' && buf[*idx] != 'C' &&
buf[*idx] != 'D' && buf[*idx] != 'K' && buf[*idx] != 'J' &&
buf[*idx] != 'f' && buf[*idx] != 'r' && buf[*idx] != 's' &&
buf[*idx] != 'u' && buf[*idx] != 'h' && buf[*idx] != 'l') {
if (num_idx >= sizeof(num_buf) - 1 ||
param_idx >= sizeof(params) / sizeof(params[0]) - 1) {
break;
}
2025-01-15 23:36:48 +00:00
if (buf[*idx] >= '0' && buf[*idx] <= '9') {
num_buf[num_idx++] = buf[*idx];
} else if (buf[*idx] == ';') {
if (num_buf[0] != '\0') {
params[param_idx++] = atoi(num_buf);
memset(num_buf, 0, sizeof(num_buf));
num_idx = 0;
}
2025-01-15 23:36:48 +00:00
}
(*idx)++;
}
if (num_buf[0] != '\0' && param_idx < sizeof(params) / sizeof(params[0])) {
params[param_idx++] = atoi(num_buf);
}
switch (buf[*idx]) {
case 'H': // Set cursor position
case 'f': // Alternative set cursor position
if (param_idx >= 2) {
cursor_y =
(params[0] - 1 < 0)
? 0
: ((params[0] - 1) >= term_rows ? term_rows - 1 : params[0] - 1);
cursor_x =
(params[1] - 1 < 0)
? 0
: ((params[1] - 1) >= term_cols ? term_cols - 1 : params[1] - 1);
} else {
cursor_x = cursor_y = 0;
}
break;
case 'A': // Cursor up
cursor_y -= params[0] ? params[0] : 1;
if (cursor_y < 0)
cursor_y = 0;
break;
case 'B': // Cursor down
cursor_y += params[0] ? params[0] : 1;
if (cursor_y >= term_rows)
cursor_y = term_rows - 1;
break;
case 'C': // Cursor forward
cursor_x += params[0] ? params[0] : 1;
if (cursor_x >= term_cols)
cursor_x = term_cols - 1;
break;
case 'D': // Cursor back
cursor_x -= params[0] ? params[0] : 1;
if (cursor_x < 0)
cursor_x = 0;
break;
case 'K': // Clear line
for (int i = cursor_x; i < term_cols; i++) {
terminal_buffer[cursor_y][i] = ' ';
color_buffer[cursor_y][i] = current_color;
attr_buffer[cursor_y][i] = current_attr;
}
break;
case 'J': // Clear screen
if (params[0] == 2) {
for (int y = 0; y < term_rows; y++) {
memset(terminal_buffer[y], ' ', term_cols);
for (int x = 0; x < term_cols; x++) {
color_buffer[y][x] = current_color;
attr_buffer[y][x] = current_attr;
}
}
needs_refresh = 1;
}
break;
case 'm': // Handling of text
for (int i = 0; i < param_idx; i++) {
switch (params[i]) {
case 0:
current_color = XWhitePixel(display, DefaultScreen(display));
current_attr = 0;
break;
case 1:
current_attr |= ATTR_BOLD;
break;
case 3:
current_attr |= ATTR_ITALIC;
break;
case 22:
current_attr &= ~ATTR_BOLD;
break;
case 23:
current_attr &= ~ATTR_ITALIC;
break;
case 30:
current_color = XBlackPixel(display, DefaultScreen(display));
break;
case 31:
current_color = RED;
break;
case 32:
current_color = GREEN;
break;
case 33:
current_color = YELLOW;
break;
case 34:
current_color = BLUE;
break;
case 35:
current_color = MAGENTA;
break;
case 36:
current_color = CYAN;
break;
case 37:
current_color = XWhitePixel(display, DefaultScreen(display));
break;
case 90:
current_color = DARK_GRAY;
break;
case 91:
current_color = LIGHT_RED;
break;
case 92:
current_color = LIGHT_GREEN;
break;
case 93:
current_color = LIGHT_YELLOW;
break;
case 94:
current_color = LIGHT_BLUE;
break;
case 95:
current_color = LIGHT_MAGENTA;
break;
case 96:
current_color = LIGHT_CYAN;
break;
case 97:
current_color = 0xFFFFFF;
break;
}
2025-01-15 23:36:48 +00:00
}
break;
2025-01-15 23:36:48 +00:00
}
}
static int master_cb(int fd, void *data) {
if (!terminal_buffer || !color_buffer || !attr_buffer)
return 0;
char buf[4096];
ssize_t bytes_read;
struct {
Display *display;
Window window;
} *ctx = data;
bytes_read = read(fd, buf, sizeof(buf));
if (bytes_read <= 0 && errno != EAGAIN && errno != EINTR) {
destroy_cb(ctx->display, ctx->window);
return 0;
}
if (bytes_read > 0) {
for (int i = 0; i < bytes_read; i++) {
if (buf[i] == '\033' && i + 1 < bytes_read && buf[i + 1] == '[') {
parse_ansi_code(buf, &i, bytes_read);
continue;
}
if (buf[i] == '\n') {
cursor_x = 0;
cursor_y++;
if (cursor_y >= term_rows) {
scroll_up();
cursor_y = term_rows - 1;
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
} else if (buf[i] == '\r') {
cursor_x = 0;
} else if (buf[i] == '\b') {
if (cursor_x > 0) {
cursor_x--;
terminal_buffer[cursor_y][cursor_x] = ' ';
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
} else {
if (cursor_y < term_rows && cursor_x < term_cols) {
terminal_buffer[cursor_y][cursor_x] = buf[i];
color_buffer[cursor_y][cursor_x] = current_color;
attr_buffer[cursor_y][cursor_x] = current_attr;
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
cursor_x++;
if (cursor_x >= term_cols) {
cursor_x = 0;
cursor_y++;
if (cursor_y >= term_rows) {
scroll_up();
cursor_y = term_rows - 1;
needs_refresh = 1;
2025-01-15 23:36:48 +00:00
}
}
}
}
}
cursor_visible = 1;
if (needs_refresh) {
check_refresh(ctx->display, ctx->window);
} else {
draw_cursor(ctx->display, ctx->window);
XFlush(ctx->display);
}
2025-01-15 23:36:48 +00:00
}
return 1;
}
static void key_press_cb(XKeyEvent *event, void *data) {
int *master = (int *)data;
char buf[32];
KeySym keysym;
int len;
len = XLookupString(event, buf, sizeof(buf), &keysym, NULL);
if (event->state & ControlMask) {
if ((keysym & 0x1f) == ('L' & 0x1f)) {
write(*master, "\f", 1);
clear_terminal(event->display, event->window);
return;
}
if ((keysym & 0x1f) == ('C' & 0x1f)) {
write(*master, "\x03", 1);
2025-01-15 23:36:48 +00:00
return;
}
if ((keysym & 0x1f) == ('U' & 0x1f)) {
write(*master, "\025", 1);
return;
}
char ctrl_char = '@' + (keysym & 0x1f);
write(*master, &ctrl_char, 1);
return;
}
if (keysym == XK_Return || keysym == XK_KP_Enter) {
write(*master, "\r", 1); // Shell handles return
2025-01-15 23:36:48 +00:00
} else if (keysym == XK_BackSpace) {
write(*master, "\b", 1); // Shell handles backspace
} else if (keysym == XK_Left) {
write(*master, "\033[D", 3);
needs_refresh = 1;
} else if (keysym == XK_Right) {
write(*master, "\033[C", 3);
needs_refresh = 1;
} else if (keysym == XK_Up) { // Go up through history
write(*master, "\033[A", 3);
needs_refresh = 1;
} else if (keysym == XK_Down) { // Go down through history
write(*master, "\033[B", 3);
needs_refresh = 1;
} else if (keysym == XK_Tab) {
write(*master, "\t", 1); // Shell handles autocomplete
2025-01-15 23:36:48 +00:00
} else if (len > 0) {
write(*master, buf, len);
}
}
int main(void) {
int master, slave;
pid_t pid;
term_rows = 24;
term_cols = 80;
gettimeofday(&last_blink, NULL);
gettimeofday(&last_refresh, NULL);
2025-01-15 23:36:48 +00:00
struct winsize ws = {.ws_row = term_rows,
.ws_col = term_cols,
.ws_xpixel = term_cols * CHAR_WIDTH,
.ws_ypixel = term_rows * CHAR_HEIGHT};
terminal_buffer = malloc(term_rows * sizeof(char *));
color_buffer = malloc(term_rows * sizeof(unsigned long *));
attr_buffer = malloc(term_rows * sizeof(int *));
if (!terminal_buffer || !color_buffer || !attr_buffer) {
fprintf(stderr, "Failed to allocate memory for buffers\n");
return 1;
}
for (int i = 0; i < term_rows; i++) {
terminal_buffer[i] = malloc(term_cols * sizeof(char));
color_buffer[i] = malloc(term_cols * sizeof(unsigned long));
attr_buffer[i] = malloc(term_cols * sizeof(int));
if (!terminal_buffer[i] || !color_buffer[i] || !attr_buffer[i]) {
fprintf(stderr, "Failed to allocate memory for buffer row\n");
return 1;
}
memset(terminal_buffer[i], ' ', term_cols);
memset(attr_buffer[i], 0, term_cols * sizeof(int));
}
display = XOpenDisplay(NULL);
if (!display) {
fprintf(stderr, "Cannot open display\n");
return 1;
}
XInitThreads();
current_color = XWhitePixel(display, DefaultScreen(display));
current_attr = 0;
for (int y = 0; y < term_rows; y++) {
for (int x = 0; x < term_cols; x++) {
color_buffer[y][x] = current_color;
attr_buffer[y][x] = current_attr;
}
}
XSizeHints hints;
hints.flags = PResizeInc | PMinSize;
hints.width_inc = CHAR_WIDTH;
hints.height_inc = CHAR_HEIGHT;
hints.min_width = CHAR_WIDTH * 4;
hints.min_height = CHAR_HEIGHT * 1;
Window window = XCreateSimpleWindow(
display, DefaultRootWindow(display), 0, 0, term_cols * CHAR_WIDTH,
term_rows * CHAR_HEIGHT, 0, XBlackPixel(display, DefaultScreen(display)),
XBlackPixel(display, DefaultScreen(display)));
buffer_pixmap = XCreatePixmap(display, DefaultRootWindow(display),
term_cols * CHAR_WIDTH, term_rows * CHAR_HEIGHT,
DefaultDepth(display, DefaultScreen(display)));
XSetWMNormalHints(display, window, &hints);
XStoreName(display, window, window_name);
XSelectInput(display, window,
KeyPressMask | ExposureMask | StructureNotifyMask |
FocusChangeMask);
2025-01-15 23:36:48 +00:00
XMapWindow(display, window);
if (openpty(&master, &slave, NULL, NULL, &ws) == -1) {
perror("openpty");
return 1;
}
master_fd = master;
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
close(master);
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
setsid();
dup2(slave, 0);
dup2(slave, 1);
dup2(slave, 2);
if (slave > 2)
close(slave);
char *shell = getenv("SHELL");
if (!shell)
shell = "/bin/sh";
setenv("TERM", "xterm-256color", 1);
execlp(shell, shell, NULL);
perror("execlp");
exit(1);
}
child_pid = pid;
close(slave);
struct {
Display *display;
Window window;
} ctx = {display, window};
XEvent event;
while (1) {
int status;
pid_t wpid = waitpid(child_pid, &status, WNOHANG);
if (wpid == child_pid) {
destroy_cb(display, window);
return 0;
}
while (XPending(display)) {
XNextEvent(display, &event);
switch (event.type) {
case ConfigureNotify: {
XConfigureEvent *ce = (XConfigureEvent *)&event;
int new_rows = ce->height / CHAR_HEIGHT;
int new_cols = ce->width / CHAR_WIDTH;
if (new_rows != term_rows || new_cols != term_cols) {
resize_buffers(new_rows, new_cols);
ws.ws_row = term_rows;
ws.ws_col = term_cols;
ws.ws_xpixel = term_cols * CHAR_WIDTH;
ws.ws_ypixel = term_rows * CHAR_HEIGHT;
ioctl(master, TIOCSWINSZ, &ws);
}
break;
}
case FocusIn:
window_focused = 1;
needs_refresh = 1;
break;
case FocusOut:
window_focused = 0;
needs_refresh = 1;
break;
2025-01-15 23:36:48 +00:00
case KeyPress:
key_press_cb((XKeyEvent *)&event, &master);
break;
case Expose:
needs_refresh = 1;
check_refresh(display, window);
2025-01-15 23:36:48 +00:00
break;
}
}
fd_set fds;
struct timeval tv = {0, 1000}; // 1ms timeout for faster refresh
2025-01-15 23:36:48 +00:00
FD_ZERO(&fds);
FD_SET(master, &fds);
if (select(master + 1, &fds, NULL, NULL, &tv) > 0) {
master_cb(master, &ctx);
}
check_refresh(display, window);
update_cursor_state(display, window);
2025-01-15 23:36:48 +00:00
}
return 0;
}