git home / emma home
logo

twn

Minimal terminal multiplexer without curses.
git clone https://git.y1.nz/archives/twn.tar.gz
README | Files | Log | Refs | LICENSE

commit 66a4bea481b042f54617d7c43d731e5e0b0cde28
Author: emma <emma@potato.my.domain>
Date:   Sun,  7 Jun 2026 21:52:00 -0500

Initial work on window positioning and controls

Diffstat:
AMakefile23+++++++++++++++++++++++
AREADME39+++++++++++++++++++++++++++++++++++++++
ATODO5+++++
Acodes.h213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.h19+++++++++++++++++++
Aconfig.mk4++++
Adirection.h6++++++
Amain.c84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asubterm.c1+
Asubterm.h11+++++++++++
Asubterm.o0
Aterm.c49+++++++++++++++++++++++++++++++++++++++++++++++++
Aterm.h1+
Aterm.o0
Atwn0
Awindow.c214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awindow.h37+++++++++++++++++++++++++++++++++++++
Awindow.o0
18 files changed, 706 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,23 @@ +#include "config.mk" + +all: twn + +twn: config.mk config.h main.c term.o subterm.o window.o + $(CC) main.c term.o window.o -o twn + +window.o: config.h window.c window.h + $(CC) window.c -c -o window.o + +subterm.o: config.h subterm.c subterm.h + $(CC) subterm.c -c -o subterm.o + +term.o: config.h term.c term.h + $(CC) term.c -c -o term.o + +clean: + rm -f window.o term.o twn + +install: twn + cp -f twn $(PREFIX)/bin + +PHONEY: install diff --git a/README b/README @@ -0,0 +1,39 @@ + - ########### - + * - # # - * + O - # TWN # - O + . * - # # - * . + o - ########### - o + + TWN is a Bare-Bones Terminal Multiplexer, like tmux or mtm. It has + distinct goals from either of those projects though. Its goals are: + + (1) Be as "transparent" as possible. That is, it should delegate as + much of its behavior as possible to the terminal it's running in. + (2) Be configurable. It should be easy to change "normal" behaviors, to + support different base terminals or to customize appearance/controls. + (3) Have maximal code quality, with minimal total LoC. This includes + dependencies! + + So, it doesn't use the curses library as this would harm goals (1) and (3). + Goal (1) allows it to ignore things like terminfo, csets, and anything else + that the base terminal implements. + + BUILD + + To build this project, first update config.mk to reflect your system's + configuration, then run "make". If you wish to install TWN, you can then + run "make install" with root priveleges. + + CONFIGURATION + + TWN can be configured by editing config.h. Feel free to edit the code, + and send in patches if you think you've made changes that might benefit + others. + + CONTRIBUTING + + Send patches to git at y1.nz, if you have an improvement. + Be cool please&thx u <3 + + With Love, + Emma <3 diff --git a/TODO b/TODO @@ -0,0 +1,5 @@ +TODO on this / ROADMAP of what to do in what order. + +[x] draw subdividing windows +[ ] correctly render a shell in a subwindow (just one is fine for now) +[ ] run shells in multiple subwindows w/ proper switching diff --git a/codes.h b/codes.h @@ -0,0 +1,213 @@ +/* list of all codes I need to worry about, with descriptions */ +/* (that is, if they do window-specific things? or...) */ +/* this is psuedocode, but will later be converted to c. */ + +/* BECAUSE I'M LAZY: these are based on the alpine linux console_codes man page. */ +/* Those are just the codes my system supports. Feel free to edit these */ +/* to support your system as well. If you send me a patch, and a description */ +/* of your system (so I can test/integrate your changes correctly), I will */ +/* see if it is possible to add support for it. */ + +#define ESC (char) 0x1B +#define CSI (char) 0x9B + +/**********************/ +/* CONTROL CHARACTERS */ +/**********************/ + +/* backspace one column (but not past beginning of line) */ +#define BS (char) 0x08 + +/* goes to next tab stop, or to EOL */ +#define HT (char) 0x09 + +/* linefeeds (plus carriage return if LF/NL is set) */ +#define LF (char) 0x0A +#define VT (char) 0x0B +#define FF (char) 0x0C + +/* carriage return */ +#define CR (char) 0x0D + +/**********************************/ +/* ESCAPE (but Not CSI) SEQUENCES */ +/**********************************/ + +/* "reset" (what attributes, exactly?) */ +#define RIS 'c' + +/* linefeed */ +#define IND 'D' + +/* newline */ +#define NEL 'E' + +/* set tab stop at current column */ +#define HTS 'H' + +/* reverse linefeed */ +#define RI 'M' + +/* DEC private identification (return "ESC[?6c") */ +#define DECID 'Z' +// TODO or should this be passed to parent? hm... + +/************************/ +/* ESC[ or CSI SEQUENCES */ +/************************/ +/* the numerical arguments are referred to by $1, $2, $3, etc. */ +/* ALSO (I think?) all row/col numbers are 1-INDEXED!!! */ + +/* insert $1 blank characters */ +#define ICH '@' + +/* Move cursor up $1 steps */ +#define CUU 'A' + +/* Move cursor down $1 steps */ +#define CUD 'B' + +/* Move cursor right $1 steps */ +#define CUF 'C' + +/* Move cursor left $1 steps */ +#define CUB 'D' + +/* Move cursor down $1 of rows, and to column 1 */ +#define CNL 'E' + +/* Move cursor up $1 rows and to column 1 */ +#define CPL 'F' + +/* Move cursor to column $1 in current row */ +#define CHA 'G' + +/* Move cursor to row $1 and column $2 */ +#define CUP 'H' + +/* Erase display, based on $1: */ +/* 1 > (1, 1) to cursor */ +/* 2 > whole display */ +/* 3 > whole display and scrollback buffer */ +/* else > cursor to (max, max) */ +#define ED 'J' + +/* Erase line, based on $1: */ +/* 1 > from column 1 to cursor */ +/* 2 > whole line */ +/* else > from cursor to end of line */ +#define EL 'K' + +/* Insert $1 blank lines */ +#define IL 'L' + +/* Delete $1 lines */ +#define DL 'M' + +/* Delete $1 chars on current line */ +#define DCH 'P' + +/* Erase $1 chars on current line */ +#define ECH 'X' + +/* Move cursor right $1 columns */ +#define HPR 'a' + +/* Move cursor to row $1, current column */ +#define VPA 'd' + +/* Move cursor down $1 rows */ +#define VPR 'e' + +/* Move cursor to row $1, column $2 */ +#define HVP 'f' + +/* Clear tab stop(s) based on $1: */ +/* empty > only at current position */ +/* 3 > clear all tab stops */ +#define TBC 'g' + +/* Save cursor location */ +#define SCOSC 's' + +/* Restore cursor location */ +#define SCORC 'u' + +/* Move cursor to column $1 in current row */ +#define HPA '`' + +/***************************************************/ +/* ESC [ $1 ; $2 ; ... m: select graphic rendition */ +/***************************************************/ +/* we need to manage these too, so subterms have independent coloring. */ +/* the values defined here correspond to $1. */ + +#define ATTR_RESET 0 +#define ATTR_SET_BOLD 1 +#define ATTR_SET_HALF_BRIGHT 2 +#define ATTR_SET_ITALIC 3 +#define ATTR_SET_UNDERSCORE 4 +#define ATTR_SET_BLINK 5 +#define ATTR_SET_REVERSE 6 + +#define ATTR_SET_UNDERLINE 21 +#define ATTR_SET_NORMAL_INTENSITY 22 +#define ATTR_SET_NO_ITALIC 23 +#define ATTR_SET_NO_UNDERLINE 24 +#define ATTR_SET_NO_BLINK 25 +#define ATTR_SET_NO_REVERSE 27 + +#define ATTR_SET_FG_BLACK 30 +#define ATTR_SET_FG_RED 31 +#define ATTR_SET_FG_GREEN 32 +#define ATTR_SET_FG_BROWN 33 +#define ATTR_SET_FG_BLUE 34 +#define ATTR_SET_FG_MAGENTA 35 +#define ATTR_SET_FG_CYAN 36 +#define ATTR_SET_FG_WHITE 37 +#define ATTR_SET_FG_CUSTOM 38 /* takes additional parameter */ +#define ATTR_SET_DEFAULT_FG 39 /* takes additional parameter */ + +#define ATTR_SET_BG_BLACK 40 +#define ATTR_SET_BG_RED 41 +#define ATTR_SET_BG_GREEN 42 +#define ATTR_SET_BG_BROWN 43 +#define ATTR_SET_BG_BLUE 44 +#define ATTR_SET_BG_MAGENTA 45 +#define ATTR_SET_BG_CYAN 46 +#define ATTR_SET_BG_WHITE 47 +#define ATTR_SET_BG_CUSTOM 48 /* takes additional parameter */ +#define ATTR_SET_DEFAULT_BG 49 /* takes additional parameter */ + +#define ATTR_SET_FG_BLACK_BRIGHT 100 +#define ATTR_SET_FG_RED_BRIGHT 101 +#define ATTR_SET_FG_GREEN_BRIGHT 102 +#define ATTR_SET_FG_BROWN_BRIGHT 103 +#define ATTR_SET_FG_BLUE_BRIGHT 104 +#define ATTR_SET_FG_MAGENTA_BRIGHT 105 +#define ATTR_SET_FG_CYAN_BRIGHT 106 +#define ATTR_SET_FG_WHITE_BRIGHT 107 + +#define ATTR_SET_BG_BLACK_BRIGHT 100 +#define ATTR_SET_BG_RED_BRIGHT 101 +#define ATTR_SET_BG_GREEN_BRIGHT 102 +#define ATTR_SET_BG_BROWN_BRIGHT 103 +#define ATTR_SET_BG_BLUE_BRIGHT 104 +#define ATTR_SET_BG_MAGENTA_BRIGHT 105 +#define ATTR_SET_BG_CYAN_BRIGHT 106 +#define ATTR_SET_BG_WHITE_BRIGHT 107 + +/****************************************/ +/* ESC [ $1 ; $2 ; ... h: MODE SWITCHES */ (some of which are positional) +/****************************************/ +/* the values here correspond to $1. */ + +#define DECCRM 3 /* display control chars. do we need to handle this? */ +#define DECIM 4 /* set insert mode. again, not sure what this does... */ +#define LFNL 20 /* set LF/NL - echo CR after all LF, VT, and FF */ + +/*****************************************/ +/* ESC [ $1 ; $2 ; ... n: STATUS REPORTS */ +/*****************************************/ + +#define CPR 6 /* report cursor position. Anwers with 'ESC [ y ; x R' */ diff --git a/config.h b/config.h @@ -0,0 +1,19 @@ + +#define DIVIDER L'#' + +/* padding of the root window */ +#define ROOT_LEFT_PAD 0 +#define ROOT_RIGHT_PAD 1 +#define ROOT_UP_PAD 0 +#define ROOT_DOWN_PAD 1 + +/* padding between splits */ +#define SPLIT_LEFT_PAD 0 +#define SPLIT_RIGHT_PAD 1 +#define SPLIT_UP_PAD 0 +#define SPLIT_DOWN_PAD 1 + +/* visual style of pad */ +#define H_PAD_CHAR '#' +#define V_PAD_CHAR '#' +#define CORNER_PAD_CHAR '#' diff --git a/config.mk b/config.mk @@ -0,0 +1,4 @@ + +PREFIX=/usr/local + +CC=cc -lc -std=c99 #... diff --git a/direction.h b/direction.h @@ -0,0 +1,6 @@ + +#define left 0 +#define right 1 +#define up 2 +#define down 3 + diff --git a/main.c b/main.c @@ -0,0 +1,84 @@ +#include <stdio.h> /* for like everything. */ +#include <signal.h> /* for signal, SIGCHLD, and SIG_IGN */ + +#include "window.h" +#include "config.h" +#include "direction.h" +#include "term.h" + +int +init(Window *root) +{ + int w, h; + + /* automatically reap children */ + signal(SIGCHLD, SIG_IGN); + + /* CSI ED: clear whole screen */ + fprintf(stdout, "\033[2J"); + + /* determine base terminal's bounds */ + if (!get_root_size(&w, &h)) { + fprintf(stderr, "Fatal: failed to measure base terminal :(" "\n"); + return 1; + } + + /* init root window */ + root->l = 1; root->u = 1; root->r = w; root->d = h; + root->pl = ROOT_LEFT_PAD; root->pu = ROOT_UP_PAD; + root->pr = ROOT_RIGHT_PAD; root->pd = ROOT_DOWN_PAD; + root->child1 = NULL; + root->child2 = NULL; + root->parent = NULL; + root->t = NULL; + + render(root, root); +} + +void +clean_up() +{ + /* move to the end of the screen */ + printf("\033[9999;9999H"); + + /* reset bg */ + printf("\033[49m"); + + /* print a cute message! */ + printf("\n" "ALLL done :3" "\n"); +} + +int +main(int argc, char **argv) +{ + Window _root; Window *root = &_root; + int result; + + result = init(root); + if (result > 0) return result; + + Window *focus = root; + Window *next; + char ch; + for (;;) { + ch = fgetc(stdin); + switch (ch) { + case 'h': next = find_next_window(root, focus, left); focus = next == NULL ? focus : next; break; + case 'k': next = find_next_window(root, focus, up); focus = next == NULL ? focus : next; break; + case 'l': next = find_next_window(root, focus, right); focus = next == NULL ? focus : next; break; + case 'j': next = find_next_window(root, focus, down); focus = next == NULL ? focus : next; break; + case 'H': focus = split_window(focus, left); break; + case 'K': focus = split_window(focus, up); break; + case 'L': focus = split_window(focus, right); break; + case 'J': focus = split_window(focus, down); break; + case 'q': focus = close_window(root, focus); break; + default: goto end; + } + if (focus == NULL) goto end; + render(root, focus); + } + +end: + clean_up(); + printf("Terminating char was %d (%c)", (int) ch, ch); +} diff --git a/subterm.c b/subterm.c @@ -0,0 +1 @@ + diff --git a/subterm.h b/subterm.h @@ -0,0 +1,11 @@ + +typedef struct _subterm { + /* relative cursor position (is this 0 or 1 indexed?) */ + int cx, cy; + + // TODO shell, in/out streams + + // TODO color, modes, blah blah blah +} Subterm; + + diff --git a/subterm.o b/subterm.o Binary files differ. diff --git a/term.c b/term.c @@ -0,0 +1,49 @@ + +#include <stdio.h> +#include <termios.h> +#include <unistd.h> +#include <ctype.h> + +#define RESPONSE_SIZE 10 + +int +get_root_size(int *x, int *y) +{ + struct termios original, changed; + char response[RESPONSE_SIZE] = ""; + int index = 0; + int ch = 0; + int result; + + tcgetattr(STDIN_FILENO, &original); + changed = original; + changed.c_lflag &= ~(ICANON | ECHO); + changed.c_cc[VMIN] = 1; + changed.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &changed); + + /* move down/right until end of screen */ + printf("\033[9999;9999H"); + fflush(stdout); + + /* query cursor position */ + printf("\033[6n"); + fflush(stdout); + + for (;;) { + ch = getchar(); + if (ch == 'R' || ch == EOF || index >= RESPONSE_SIZE) break; + if (!isprint(ch)) continue; + response[index++] = ch; + } + response[index] = '\0'; + + result = sscanf(response, "[%d;%d", y, x); + + /* move cursor to top-left of screen */ + printf("\033[1;1H"); + + // tcsetattr(STDIN_FILENO, TCSANOW, &original); + + return result == 2; +} diff --git a/term.h b/term.h @@ -0,0 +1 @@ +int get_root_size(int *x, int *y); diff --git a/term.o b/term.o Binary files differ. diff --git a/twn b/twn Binary files differ. diff --git a/window.c b/window.c @@ -0,0 +1,214 @@ +#include <stdio.h> /* for stdin/out, fprintf, etc */ +#include <stdlib.h> /* for malloc, free */ + +#include "config.h" +#include "window.h" +#include "direction.h" + +/* interior bounds, inclusive. */ +/* min and max cursor positions in the text area of the given subterm */ +#define IN_L(win_ptr) ((win_ptr)->l + (win_ptr)->pl) +#define IN_U(win_ptr) ((win_ptr)->u + (win_ptr)->pu) +#define IN_R(win_ptr) ((win_ptr)->r - (win_ptr)->pr) +#define IN_D(win_ptr) ((win_ptr)->d - (win_ptr)->pd) + +/* collision check */ +#define IN_WINDOW(w, x, y) (w->l <= x && x <= w->r && w->u <= y && y <= w->d) + +/* get cursor position within window, in base-terminal coordinates */ +// TODO use actual cursor instead of top-left corner +#define ABS_CURS_X(w) (w)->l +#define ABS_CURS_Y(w) (w)->u + + +/* recursive helper for close_window */ +void +resize_others(Window *w, Window *other) +{ + if (other->l == w->r+1) other->l = w->l; + if (other->r == w->l-1) other->r = w->r; + if (other->u == w->d+1) other->u = w->u; + if (other->d == w->u-1) other->d = w->d; + if (other->child1 != NULL) { + resize_others(w, other->child1); + resize_others(w, other->child2); + } +} + +/* FOCUS MUST BE A LEAF */ +/* returns the new focus */ +Window * +close_window(Window *root, Window *focus) { + Window *parent = focus->parent; + Window *sibling; + Window *new_focus; + + if (parent == NULL) return NULL; + + sibling = focus == parent->child1 ? + parent->child2 : parent->child1; + + /* merge sibling and parent */ + parent->child1 = sibling->child1; + parent->child2 = sibling->child2; + if (parent->child1 != NULL) { + parent->child1->parent = parent; + parent->child2->parent = parent; + } + // TODO set parent's subterm, cursor, etc to sibling's + resize_others(focus, parent); + new_focus = find_window(root, ABS_CURS_X(focus), ABS_CURS_Y(focus)); + free(focus); + free(sibling); + + return new_focus; +} + +Window * +clone_window(Window *a) +{ + Window *b = (Window *) malloc(sizeof(Window)); + b->l = a->l; b->u = a->u; + b->r = a->r; b->d = a->d; + b->pl = a->pl; b->pu = a->pu; + b->pr = a->pr; b->pd = a->pd; + b->parent = NULL; + b->child1 = NULL; + b->child2 = NULL; + b->t = NULL; + return b; +} + +/* Shrink the given window and create a new one. */ +/* Returns the pointer to the new focused window. */ +Window * +split_window(Window *parent, int direction) +{ + Window *c1 = clone_window(parent), *c2 = clone_window(parent); + + /* calculate the new windows' geometries */ + int xmid = (parent->l + parent->r) / 2; + int ymid = (parent->u + parent->d) / 2; + switch (direction) { + case down: + c1->d = ymid; c1->pd = SPLIT_DOWN_PAD; + c2->u = ymid+1; c2->pu = SPLIT_UP_PAD; + break; + case up: + c1->u = ymid; c1->pu = SPLIT_UP_PAD; + c2->d = ymid-1; c2->pd = SPLIT_DOWN_PAD; + break; + case right: + c1->r = xmid; c1->pr = SPLIT_RIGHT_PAD; + c2->l = xmid+1; c2->pl = SPLIT_LEFT_PAD; + break; + case left: + c1->l = xmid; c1->pl = SPLIT_LEFT_PAD; + c2->r = xmid-1; c2->pr = SPLIT_RIGHT_PAD; + break; + } + + /* update the window tree */ + parent->t = NULL; + c1->parent = parent; c2->parent = parent; + parent->child1 = c1; parent->child2 = c2; + + // TODO open a new subterm for c2 + // c2->t = ... + + return c2; +} + +/* Find the leaf window containing the given point, or else NULL */ +/* Recursing helper method for find_window_in_direction */ +Window * +find_window(Window *w, int x, int y) +{ + // TODO may be unnecessary: only check root window? + if (!IN_WINDOW(w, x, y)) return NULL; + + /* no children */ + if (w->child1 == NULL) return w; + + return find_window(IN_WINDOW(w->child1, x, y) ? w->child1 : w->child2, x, y); +} + +/* Find the next window in a given direction */ +Window * +find_next_window(Window *root, Window *focus, int direction) +{ + int x, y; /* target position */ + switch(direction) { + case left: x = focus->l - 1; y = ABS_CURS_Y(focus); break; + case up: y = focus->u - 1; x = ABS_CURS_X(focus); break; + case right: x = focus->r + 1; y = ABS_CURS_Y(focus); break; + case down: y = focus->d + 1; x = ABS_CURS_X(focus); break; + } + return find_window(root, x, y); +} + +void +fill_rect(int l, int u, int r, int d, char c) +{ + int x = l, y = u; + for (;;) { + /* CSI sequence HVP: move cursor to row,col */ + printf("\033[%d;%df", y, x); + + putchar(c); + + x++; + if (x > r) { + x = l; + y++; + } + if (y > d) break; + } +} + +void +fill_window_pad(Window *w) +{ + if (w->pu > 0) fill_rect(IN_L(w), w->u, IN_R(w), IN_U(w) - 1, V_PAD_CHAR); + if (w->pd > 0) fill_rect(IN_L(w), IN_D(w) + 1, IN_R(w), w->d, V_PAD_CHAR); + + if (w->pl > 0) fill_rect(w->l, IN_U(w), IN_L(w) - 1, IN_D(w), H_PAD_CHAR); + if (w->pr > 0) fill_rect(IN_R(w) + 1, IN_U(w), w->r, IN_D(w), H_PAD_CHAR); + + if (w->pl > 0 && w->pu > 0) fill_rect(w->l, w->u, IN_L(w) - 1, IN_U(w) - 1, CORNER_PAD_CHAR); + if (w->pl > 0 && w->pd > 0) fill_rect(w->l, IN_D(w)+1, IN_L(w) - 1, w->d, CORNER_PAD_CHAR); + if (w->pr > 0 && w->pu > 0) fill_rect(IN_R(w) + 1, w->u, w->r, IN_U(w) - 1, CORNER_PAD_CHAR); + if (w->pr > 0 && w->pd > 0) fill_rect(IN_R(w) + 1, IN_D(w)+1, w->r, w->d, CORNER_PAD_CHAR); + // TODO draw corners... +} + +void +fill_window(Window *w, char c) +{ + fill_rect(IN_L(w), IN_U(w), IN_R(w), IN_D(w), c); +} + +void +render(Window *w, Window *focus) +{ + int r; + if (w == NULL) return; + + if (w->child1 != NULL) { + render(w->child1, focus); + render(w->child2, focus); + return; + } + + /* reset bg */ + fprintf(stdout, "\033[49m"); + + fill_window(w, ' '); + + /* show border in a color... */ + if (w == focus) fprintf(stdout, "\033[41m"); + + fill_window_pad(w); + fflush(stdout); +} + diff --git a/window.h b/window.h @@ -0,0 +1,37 @@ + +#include "subterm.h" + +typedef struct _window { + /* inclusive bounds: left/up/right/down. (always order as LURD when possible!) */ + int l, u, r, d; + + /* padding by direction, cutting into the l/u/r/d bounds */ + int pl, pu, pr, pd; + + /* terminal in this window. has IO + colors + modes + etc */ + /* NULL if there are children */ + struct Subterm *t; + + /* NULL iff root window */ + struct _window *parent; + + /* always either both defined, or both NULL. */ + struct _window *child1, *child2; +} Window; + +/* Close the current window and fix the tree around it. */ +Window *close_window(Window *root, Window *focus); + +/* Shrink the given window and create a new one. */ +Window *split_window(Window *parent, int direction); + +/* find the leaf that contains (x, y) */ +Window *find_window(Window *root, int x, int y); + +/* find the next window, from the focus in the given direction */ +Window *find_next_window(Window *root, Window *focus, int direction); + +/* drawing helpers */ +void fill_window(Window *a, char c); +void fill_window_pad(Window *a); +void render(Window *w, Window *focus); diff --git a/window.o b/window.o Binary files differ.

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.