diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9a31923 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# cowterm - simple terminal emulator +include config.mk + +SRC = cowterm.c +OBJ = ${SRC:.c=.o} + +all: options cowterm + +options: + @echo cowterm build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.mk + +cowterm: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + @rm ${OBJ} + +clean: + rm -f cowterm + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f cowterm ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/cowterm + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/cowterm + +.PHONY: all options clean install uninstall diff --git a/README.md b/README.md index dca8898..db0e7d7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ # cowterm +What the hell is a cowmonk? -A simple terminal \ No newline at end of file +## What is cowterm? +cowterm is a really simple terminal that is written in pure C, using gtk. I don't recommend anyone seriously using this. +If you wish to, then good luck I guess? Here are some features that are in cowterm: +- You can kill processes in it using Ctrl+C + - Yes, this had to be manually implemented +- You can clear it using Ctrl+L + - It won't clear if you type in "clear" +- You can delete everything behind the cursor using Ctrl+U + - Essentials amirite? +- It can be resized + - So many Segfaults to get to this revolutionary moment +- It's hella lightweight + - like seriously, it is only like around 600 sloc so like no ram usage at all + +Here are some ~~downsides~~ upsides to using cowterm: +- no scrolling + - who needs it amirite? bloat booo +- boot it up like a chainsaw + - Might segfault once or twice before it actually decides to work, I have no idea how to fix this +- Fonts are hell to configure +- No wayland + - Actually this might be the best "feature" yet +- ~~ Missing critical features for a terminal~~ Removed lots of bloat + +## Building & Installation +### Dependencies: +- C compiler (gcc or clang) +- C libraries +- Xorg Libs (xfont, xutil, and xlibs) +- GTK 3.0 + +### Compilation & Installation +The steps are very simple, nothing like you've never seen before: +```bash +git clone https://git.based.pt/cowmonk/cowterm.git +cd cowterm +# Recomended to check config.h and see if it's to your liking +# vim/nano config.h +# Also recommended to check config.mk (advanced pro-gamers only!) +make +doas/sudo make install +``` +Enjoy your new ~~awful~~ great terminal + +# Credits +- Me (cowmonk) diff --git a/config.h b/config.h new file mode 100644 index 0000000..940c346 --- /dev/null +++ b/config.h @@ -0,0 +1,35 @@ +#ifndef CONFIG_H_ +#define CONFIG_H_ + +// Colors are just configured like RBG, if you don't recognize +// this color format, search it up and find it out yourself +static const unsigned long int RED = 0xFF0000; +static const unsigned long int GREEN = 0x00FF00; +static const unsigned long int YELLOW = 0xFFFF00; +static const unsigned long int BLUE = 0x0000FF; +static const unsigned long int MAGENTA = 0xFF00FF; +static const unsigned long int CYAN = 0x00FFFF; +static const unsigned long int DARK_GRAY = 0x808080; +static const unsigned long int LIGHT_RED = 0xFF8080; +static const unsigned long int LIGHT_GREEN = 0x80FF80; +static const unsigned long int LIGHT_YELLOW = 0xFFFF80; +static const unsigned long int LIGHT_BLUE = 0x8080FF; +static const unsigned long int LIGHT_MAGENTA = 0xFF80FF; +static const unsigned long int LIGHT_CYAN = 0x80FFFF; + +// Configuring Fonts (for dummies) +// They are formated like this: +// -foundry-family-weight-slant-width--pixels-points-horz-vert-spacing-width-charset +// Here are some common examples: +// "-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1" +// "-adobe-courier-medium-r-normal--14-140-75-75-m-90-iso8859-1" +// "-bitstream-terminal-medium-r-normal--18-140-100-100-c-110-iso8859-1" +// Use 'xlsfonts' command to list available fonts +static const char REGULAR_FONT[] = "-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1"; +static const char BOLD_FONT[] = "-misc-fixed-bold-r-normal--13-120-75-75-c-70-iso8859-1"; +static const char ITALICS_FONT[] = "-misc-fixed-medium-o-normal--13-120-75-75-c-70-iso8859-1"; + +// Change the window name to whatever you want lmao +static const char window_name[] = "CowTerm"; + +#endif // CONFIG_H_ diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..b327692 --- /dev/null +++ b/config.mk @@ -0,0 +1,16 @@ +# cowterm - a simple terminal emulator + +# paths +PREFIX = /usr/local/ + +# includes and libs +INCS = -I/usr/X11R6/include +LIBS = -L/usr/X11R6/lib -lX11 -lutil + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 +CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os ${INCS} ${CPPFLAGS} +LDFLAGS = ${LIBS} + +# compiler and linker +CC = cc diff --git a/cowterm.c b/cowterm.c new file mode 100644 index 0000000..47e8ca8 --- /dev/null +++ b/cowterm.c @@ -0,0 +1,724 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#define CURSOR_BLINK_INTERVAL 500000 // 500ms in microseconds + +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 Pixmap buffer_pixmap = None; +static int master_fd = -1; +static XFontStruct *regular_font = NULL; +static XFontStruct *bold_font = NULL; +static XFontStruct *italic_font = NULL; + +#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]; + + // Draw inverted cursor background + XSetForeground(display, gc, original_color); + XFillRectangle(display, buffer_pixmap, gc, cursor_x * CHAR_WIDTH, + cursor_y * CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT); + + // Draw the character in inverted color + 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); + + XDrawString(display, buffer_pixmap, gc, cursor_x * CHAR_WIDTH, + (cursor_y + 1) * CHAR_HEIGHT - 2, str, 1); + } + } + + // 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_blink(Display *display, Window window) { + struct timeval now; + gettimeofday(&now, NULL); + + long elapsed = (now.tv_sec - last_blink.tv_sec) * 1000000 + + (now.tv_usec - last_blink.tv_usec); + + if (elapsed >= CURSOR_BLINK_INTERVAL) { + cursor_visible = !cursor_visible; + last_blink = now; + draw_cursor(display, window); + XFlush(display); + } +} + +// 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))); +} + +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); + + 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); +} + +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; + draw_terminal(display, window); +} + +// 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; + } +} + +// Handles those pesky ANSI color escape codes that terminals use. +// When \033[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[16] = {0}; + int num_idx = 0; + (*idx)++; // Skip [ + + while (*idx < max_len && buf[*idx] != 'm') { + if (buf[*idx] >= '0' && buf[*idx] <= '9') { + num_buf[num_idx++] = buf[*idx]; + } + (*idx)++; + } + + if (num_buf[0] != '\0') { + int code = atoi(num_buf); + switch (code) { + 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; // Red + break; + case 32: + current_color = GREEN; // Green + break; + case 33: + current_color = YELLOW; // Yellow + break; + case 34: + current_color = BLUE; // Blue + break; + case 35: + current_color = MAGENTA; // Magenta + break; + case 36: + current_color = CYAN; // Cyan + break; + case 37: + current_color = XWhitePixel(display, DefaultScreen(display)); + break; + case 90: + current_color = DARK_GRAY; // Dark gray + break; + case 91: + current_color = LIGHT_RED; // Light red + break; + case 92: + current_color = LIGHT_GREEN; // Light green + break; + case 93: + current_color = LIGHT_YELLOW; // Light yellow + break; + case 94: + current_color = LIGHT_BLUE; // Light blue + break; + case 95: + current_color = LIGHT_MAGENTA; // Light magenta + break; + case 96: + current_color = LIGHT_CYAN; // Light cyan + break; + case 97: + current_color = 0xFFFFFF; // Bright white teehee + break; + } + } +} + +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; + draw_terminal(ctx->display, ctx->window); + } + } 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] = ' '; + draw_char(ctx->display, buffer_pixmap, cursor_x, cursor_y); + XCopyArea(ctx->display, buffer_pixmap, ctx->window, gc, + cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT, CHAR_WIDTH, + CHAR_HEIGHT, cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT); + } + } 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; + draw_char(ctx->display, buffer_pixmap, cursor_x, cursor_y); + XCopyArea(ctx->display, buffer_pixmap, ctx->window, gc, + cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT, CHAR_WIDTH, + CHAR_HEIGHT, cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT); + cursor_x++; + if (cursor_x >= term_cols) { + cursor_x = 0; + cursor_y++; + if (cursor_y >= term_rows) { + scroll_up(); + cursor_y = term_rows - 1; + draw_terminal(ctx->display, ctx->window); + } + } + } + } + } + cursor_visible = 1; + draw_cursor(ctx->display, ctx->window); + XFlush(ctx->display); + } + 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)) { + kill(child_pid, SIGINT); + 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); + } else if (keysym == XK_BackSpace) { + write(*master, "\b", 1); + } 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); + + 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); + 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 KeyPress: + key_press_cb((XKeyEvent *)&event, &master); + break; + case Expose: + draw_terminal(display, window); + break; + } + } + + fd_set fds; + struct timeval tv = {0, 100000}; // 100ms timeout for cursor blink + FD_ZERO(&fds); + FD_SET(master, &fds); + if (select(master + 1, &fds, NULL, NULL, &tv) > 0) { + master_cb(master, &ctx); + } + + update_cursor_blink(display, window); + } + + return 0; +}