From 8767e4ca6ee8e4032aad3743a24774c881b28439 Mon Sep 17 00:00:00 2001 From: Emile Clark-Boman Date: Sat, 27 Sep 2025 20:47:20 +1000 Subject: [PATCH] merge dorne specific cursetree files into main repo --- cursetree/_ncurses.h | 6 + cursetree/cursetree.c | 128 +++++++++++++++ cursetree/cursetree.h | 11 ++ cursetree/dims.c | 88 +++++++++++ cursetree/dims.h | 41 +++++ cursetree/ncrswrap.c | 114 ++++++++++++++ cursetree/ncrswrap.h | 35 ++++ cursetree/node.c | 359 ++++++++++++++++++++++++++++++++++++++++++ cursetree/node.h | 66 ++++++++ cursetree/pty/_pty.c | 177 +++++++++++++++++++++ cursetree/pty/_pty.h | 30 ++++ cursetree/pty/child.c | 63 ++++++++ cursetree/pty/child.h | 14 ++ cursetree/pty/epty.c | 45 ++++++ cursetree/pty/epty.h | 8 + cursetree/surface.c | 81 ++++++++++ cursetree/surface.h | 38 +++++ cursetree/tree.c | 38 +++++ cursetree/tree.h | 15 ++ cursetree/util.h | 12 ++ 20 files changed, 1369 insertions(+) create mode 100644 cursetree/_ncurses.h create mode 100644 cursetree/cursetree.c create mode 100644 cursetree/cursetree.h create mode 100644 cursetree/dims.c create mode 100644 cursetree/dims.h create mode 100644 cursetree/ncrswrap.c create mode 100644 cursetree/ncrswrap.h create mode 100644 cursetree/node.c create mode 100644 cursetree/node.h create mode 100644 cursetree/pty/_pty.c create mode 100644 cursetree/pty/_pty.h create mode 100644 cursetree/pty/child.c create mode 100644 cursetree/pty/child.h create mode 100644 cursetree/pty/epty.c create mode 100644 cursetree/pty/epty.h create mode 100644 cursetree/surface.c create mode 100644 cursetree/surface.h create mode 100644 cursetree/tree.c create mode 100644 cursetree/tree.h create mode 100644 cursetree/util.h diff --git a/cursetree/_ncurses.h b/cursetree/_ncurses.h new file mode 100644 index 0000000..dc92562 --- /dev/null +++ b/cursetree/_ncurses.h @@ -0,0 +1,6 @@ +/* This file exists to isolate the ncurses instance being used + * ie ncurses, ncursesw, ncursest, ncursestw + */ + +/* libncurses with wide-character support. */ +#include diff --git a/cursetree/cursetree.c b/cursetree/cursetree.c new file mode 100644 index 0000000..31504a8 --- /dev/null +++ b/cursetree/cursetree.c @@ -0,0 +1,128 @@ +#include +#include +#include + +#include "_ncurses.h" +#include "ncrswrap.h" +#include "ncurses.h" +#include "node.h" +#include "tree.h" + +volatile sig_atomic_t got_winch = 0; + +void handle_SIGWINCH(int sig) { + // endwin(); + // Needs to be called after an endwin() so ncurses will initialize + // itself with the new terminal dimensions. + got_winch = 1; +} + +/* + * If ncurses is configured to supply its own SIGWINCH handler, + * - on receipt of a SIGWINCH, the handler sets a flag + * - which is tested in wgetch(3X), doupdate(3X) and restartterm(3X), + * - in turn, calling the resizeterm function, + * - which ungetch's a KEY_RESIZE which will be read on the next call to + * wgetch. REF: `man resizeterm(3x)` + */ +static void bind_SIGWINCH(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_SIGWINCH; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; /* Restart functions if + interrupted by handler */ + if (sigaction(SIGWINCH, &sa, NULL) == -1) { + perror("bind_SIGWINCH"); + exit(2); + } +} + +int ct_init(struct ct_tree **const tree) { + // bind_SIGWINCH(); + /* Initialise NCurses Library & Root Node */ + init_ncurses(); + init_tree(tree); + + return EXIT_SUCCESS; +} + +void ct_redraw(void) { + /* TODO: hide doupdate() behind some other method */ + /* flush ncurses virtual screen -> physical screen */ + doupdate(); +} + +/* Recursively search the tree for update requests. + * Returns: + * 0 - success (no action required) + * 1 - success (request: redraw the screen) + */ +static int __update_rec(struct ct_node *const node) { + int result = 0; + struct ct_dims *dims; + + if (node->flags & NFLAG_RESIZE) { + /* TODO: the child has requested a resizing, but resize_node() + * TODO: wastes CPU time resizing itself! + */ + dims = IS_ROOT_NODE(node) ? termdims() : dup_dims(node->surface->dims); + resize_node(node, dims); + node->flags &= ~NFLAG_RESIZE; + result = 1; + } + if (node->surface->updatereq) { + // TODO: is this necessary or does mvresize_win do this for us? + wnoutrefresh(node->surface->win); + node->surface->updatereq = 0; + } + + for (int i = 0; i < node->cindex; i++) { + result |= __update_rec(node->child[i]); + } + return result; +} + +void ct_update(struct ct_tree *const tree) { + // WARNING: update_rec does not flush the screen, wgetch will do that for us + if (__update_rec(tree->root)) { + doupdate(); + // redrawwin(curscr); + // wrefresh(curscr); + } +} + +void ct_process(struct ct_tree *const tree) { + const int key = wgetch(curscr); + // int key = -1; + + /* ncurses binds a SIGWINCH handler if SIGWINCH has SIG_DFL disposition + * when initscr(3x) is called. This handler emits KEY_RESIZE (decimal 410) to + * stdin. REF: manpages -> resizeterm(3x) initscr(3x) wgetch(3x) + */ + switch (key) { + case -1: + // wclear(tree->root->surface->win); + // mvwprintw(tree->root->surface->win, 0, 0, " \r-1\n"); + // wrefresh(tree->root->surface->win); + break; + case KEY_RESIZE: + // got_winch = 1; + tree->root->flags |= NFLAG_RESIZE; + break; + default: + // wclear(tree->root->surface->win); + // mvwprintw(tree->root->surface->win, 0, 0, " \r%d\n", key); + // wrefresh(tree->root->surface->win); + break; + } + + // if (got_winch == 1) { + // tree->root->flags |= NFLAG_RESIZE; + // got_winch = 0; + // } + + // if (__update_rec(tree->root)) { + // ct_redraw(); + // } +} diff --git a/cursetree/cursetree.h b/cursetree/cursetree.h new file mode 100644 index 0000000..d066429 --- /dev/null +++ b/cursetree/cursetree.h @@ -0,0 +1,11 @@ +#ifndef CURSETREE_CURSETREE_H +#define CURSETREE_CURSETREE_H + +#include "tree.h" + +/* === External Interface === */ +int ct_init(struct ct_tree **const tree); +void ct_update(struct ct_tree *const tree); +void ct_process(struct ct_tree *const tree); + +#endif /* CURSETREE_CURSETREE_H */ diff --git a/cursetree/dims.c b/cursetree/dims.c new file mode 100644 index 0000000..771c68a --- /dev/null +++ b/cursetree/dims.c @@ -0,0 +1,88 @@ +#include +#include +#include +#include + +#include "dims.h" +#include "node.h" +#include "util.h" + +struct ct_dims *new_dims(const int x, const int y, const int width, const int height) { + struct ct_dims *dims; + + dims = (struct ct_dims *)malloc(sizeof(struct ct_dims)); + *dims = (struct ct_dims){ + .x = x, + .y = y, + .width = width, + .height = height, + }; + return dims; +} + +struct ct_dims *dup_dims(const struct ct_dims *const dims) { + struct ct_dims *dup; + dup = (struct ct_dims *)malloc(sizeof(struct ct_dims)); + memcpy(dup, dims, sizeof(struct ct_dims)); + + return dup; +} + +static struct ct_bounds *__bounds(const enum ct_boundtype type, const int wmin, + const int wmax, const int hmin, + const int hmax) { + struct ct_bounds *bounds; + + bounds = (struct ct_bounds *)malloc(sizeof(struct ct_bounds)); + *bounds = (struct ct_bounds){ + .type = type, + .wmin = wmin, + .wmax = wmax, + .hmin = hmin, + .hmax = hmax, + }; + return bounds; +} + +static inline void __clamp(int *const val, const int min, const int max, + const int do_ceiling) { + if (*val == __BOUND_UNLIMITED) + *val = do_ceiling ? max : min; + else + *val = clampi(*val, min, max); +} +#define CLAMP_ABS(val, is_max) \ + (__clamp(&val, __BOUND_ABS_MIN, __BOUND_ABS_MAX, is_max)) +#define CLAMP_REL(val, is_max) \ + (__clamp(&val, __BOUND_REL_MIN, __BOUND_REL_MAX, is_max)) + +struct ct_bounds *bounds_none(void) { + return __bounds(BOUND_NONE, __BOUND_ABS_MIN, __BOUND_ABS_MAX, __BOUND_ABS_MIN, + __BOUND_ABS_MAX); +} + +struct ct_bounds *bounds_absolute(int wmin, int wmax, + int hmin, int hmax) { + CLAMP_ABS(wmin, false); + CLAMP_ABS(wmax, true); + CLAMP_ABS(hmin, false); + CLAMP_ABS(hmax, true); + return __bounds(BOUND_ABSOLUTE, wmin, wmax, hmin, hmax); +} + +struct ct_bounds *bounds_relative(int wmin, int wmax, + int hmin, int hmax) { + CLAMP_REL(wmin, false); + CLAMP_REL(wmax, true); + CLAMP_REL(hmin, false); + CLAMP_REL(hmax, true); + return __bounds(BOUND_RELATIVE, wmin, wmax, hmin, hmax); +} + +struct ct_bounds *dup_bounds(const struct ct_bounds *const bounds) { + struct ct_bounds *dup; + dup = (struct ct_bounds *)malloc(sizeof(struct ct_bounds)); + memcpy(dup, bounds, sizeof(struct ct_bounds)); + + return dup; +} diff --git a/cursetree/dims.h b/cursetree/dims.h new file mode 100644 index 0000000..86417ba --- /dev/null +++ b/cursetree/dims.h @@ -0,0 +1,41 @@ +#ifndef CURSETREE_DIMS_H +#define CURSETREE_DIMS_H + +#define __BOUND_UNLIMITED (-1) +#define __BOUND_ABS_MIN (1) +#define __BOUND_ABS_MAX (INT_MAX / CINDEX_MAX - 1) +#define __BOUND_REL_MIN ((float)0) +#define __BOUND_REL_MAX ((float)1) + +enum ct_axis { + AXIS_X, + AXIS_Y, +}; + +enum ct_boundtype { + BOUND_NONE, + BOUND_ABSOLUTE, + BOUND_RELATIVE, +}; + +/* Stores a node's starting x,y coordinates, width, & height. + * NOTE: Intended for interfunction communication. + */ +struct ct_dims { + int x, y, width, height; +}; + +struct ct_bounds { + enum ct_boundtype type; + int wmin, wmax, hmin, hmax; +}; + +struct ct_dims *new_dims(const int x, const int y, const int width, const int height); +struct ct_dims *dup_dims(const struct ct_dims *const dims); + +struct ct_bounds *bounds_none(void); +struct ct_bounds *bounds_absolute(int wmin, int wmax, int hmin, int hmax); +struct ct_bounds *bounds_relative(int wmin, int wmax, int hmin, int hmax); +struct ct_bounds *dup_bounds(const struct ct_bounds *const bounds); + +#endif /* CURSETREE_DIMS_H */ diff --git a/cursetree/ncrswrap.c b/cursetree/ncrswrap.c new file mode 100644 index 0000000..d81cc4b --- /dev/null +++ b/cursetree/ncrswrap.c @@ -0,0 +1,114 @@ +#include +#include + +#include "_ncurses.h" + +#include "ncrswrap.h" + +#define CRS_LOCALE "en_US.UTF-8" + +/* Set ncurses terminal mode (buffering, character processing, + * Key->SIG handling, and other termios(3) functionality). + */ +int termmode(const enum crs_termmode mode) { + switch (mode) { + case TMODE_CBREAK: + return cbreak(); + case TMODE_RAW: + return raw(); + case TMODE_NOCBREAK: + return nocbreak(); + case TMODE_NORAW: + return noraw(); + default: + /* defaulting is not possible. */ + return 1; + } +} + +void termsize(int *const width, int *const height) { + *width = COLS; + *height = LINES; +} + +struct ct_dims *termdims(void) { + struct ct_dims *dims = new_dims(0, 0, 0, 0); + termsize(&dims->width, &dims->height); + + return dims; +} + +/* Apply a default IO configuration for an ncurses WINDOW. + */ +static inline void __conf_window(WINDOW *const win) { + nodelay(win, TRUE); // getch(3x) nonblocking IO + keypad(win, TRUE); // allow function keys + // intrflush(3x) - flush terminal input buffer on interrupt key + intrflush(win, FALSE); +} + +/* Initialise ncurses for our usecase. + * WARNING: This function should only be called once. + */ +void init_ncurses(void) { + // ncurses expects a locale for consistent behaviour + setlocale(LC_ALL, CRS_LOCALE); + + /* NCurses Initialisation */ + initscr(); + /* WARNING: no you shouldn't delwin(stdscr) it breaks everything... */ + __conf_window(stdscr); + start_color(); + + /* Screen Configuration */ + termmode(TMODE_CBREAK); + noecho(); // manually echo from getch(3x) + + curs_set(0); // hide cursor +} + +void end_ncurses(void) { + endwin(); +} + +/* Initialise (with default IO configuration) a new ncurses WINDOW. + */ +WINDOW *new_window(const int x, const int y, const int width, + const int height) { + WINDOW *win = newwin(height, width, y, x); + __conf_window(win); + + return win; +} + +/* Initialise (with default IO configuration) a fullscreen ncurses WINDOW. + */ +WINDOW *new_window_fs(void) { + WINDOW *rootwin = new_window(0, 0, COLS, LINES); + __conf_window(rootwin); + return rootwin; +} + +// void destroy_window(WINDOW *) __attribute__((alias("delwin"))); + +// int winposx(WINDOW *) __attribute__((alias("getbegy"))); +// int winposy(WINDOW *) __attribute__((alias("getbegy"))); +// int winwidth(WINDOW *) __attribute__((alias("getmaxx"))); +// int winheight(WINDOW *) __attribute__((alias("getmaxy"))); + +// struct ct_dims *windims(WINDOW *win) { +// int x, y, width, height; +// winpos(win, x, y); +// winsize(win, width, height); +// return new_dims(x, y, width, height); +// } + +/* Resize and move (if resized successfully) an ncurses WINDOW. + * Returns ERR (1) on fail, and OK (0) on success. + * NOTE: Failure occurs if width or height <= 0, + * NOTE: or if the x,y coordinate is outside the screen bounds. + */ +int resizemv_window(WINDOW *const win, const int x, const int y, + const int width, const int height) { + return wresize(win, height, width) || mvwin(win, y, x); +} diff --git a/cursetree/ncrswrap.h b/cursetree/ncrswrap.h new file mode 100644 index 0000000..f21ffbf --- /dev/null +++ b/cursetree/ncrswrap.h @@ -0,0 +1,35 @@ +#ifndef CURSETREE_NCRSWRAP_H +#define CURSETREE_NCRSWRAP_H + +#include "dims.h" + +#ifndef __NCURSES_H +typedef struct _win_st WINDOW; +#endif /* __NCURSES_H */ + +enum crs_termmode { + /* tty cbreak mode */ + TMODE_CBREAK, + /* tty raw mode */ + TMODE_RAW, + /* tty cooked mode (ISIG & IXON not modified)*/ + TMODE_NOCBREAK, + /* tty cooked mode (ISIG & IXON set) */ + TMODE_NORAW, +}; + +int termmode(const enum crs_termmode mode); +void termsize(int *const width, int *const height); +struct ct_dims *termdims(void); + +void init_ncurses(void); +void end_ncurses(void); + +WINDOW *new_window(const int x, const int y, const int width, const int height); +WINDOW *new_window_fs(void); +void destroy_window(WINDOW *); + +int resizemv_window(WINDOW *const win, const int x, const int y, + const int width, const int height); + +#endif /* CURSETREE_NCRSWRAP_H */ diff --git a/cursetree/node.c b/cursetree/node.c new file mode 100644 index 0000000..d3c4422 --- /dev/null +++ b/cursetree/node.c @@ -0,0 +1,359 @@ +#include +#include +#include +#include +#include + +#include "dims.h" +#include "node.h" +#include "surface.h" +#include "util.h" + +/* Internal allocator method for ct_node structures. + */ +static inline struct ct_node *__alloc_node(void) { + struct ct_node *node = (struct ct_node *)malloc(sizeof(struct ct_node)); + return node; +} + +/* Returns NULL if memory allocation failed. + * TODO: should dims be given as a parameter, or lazily computed in ct_update? + */ +struct ct_node *__node(struct ct_dims *const dims, + struct ct_bounds *const bounds, + struct ct_node *const parent) { + struct ct_node *node = __alloc_node(); + if (node != NULL) { + *node = (struct ct_node){ + .surface = new_surface(dims, bounds), + .flags = NFLAG_EMPTY, + + .parent = parent, + .child = (struct ct_node **)malloc(NODE_INIT_CHILDREN * + sizeof(struct ct_node *)), + .csize = NODE_INIT_CHILDREN, + .cindex = 0, + .axis = AXIS_X, + }; + } + + return node; +} + +struct ct_node *new_node(struct ct_bounds *const bounds, + struct ct_node *const parent) { + /* copy the parent's dimensions for now and request + * cursetree resize this node appropriately afterwards + * WARNING: new_node doesn't set the NFLAG_RESIZE request flag + * WARNING: that should be done by a function calling new_node + */ + return __node(dup_dims(parent->surface->dims), bounds, parent); +} + +/* WARNING: Do NOT use __destroy_node() to destroy a node's children! + * WARNING: Use the destroy_child_node() function instead. + */ +void __destroy_node(struct ct_node *const node) { + if (node == NULL) + return; + + // destroy children first + for (int j = 0; j < node->cindex; j++) { + __destroy_node(node->child[j]); + } + + destroy_surface(node->surface); + free(node); +} + +/* Surface Partition Dimensions */ +/* TODO: can I use unsigned short instead of int? */ +struct ct_spdims { + // Main Axis & Orthogonal Axis Sizes + int axm_size; + bool fixed; + int min, max; +}; + +/* + */ +int resize_node(struct ct_node *const node, struct ct_dims *dims) { + resize_surface(node->surface, dims); + dims = node->surface->dims; + + if (node->surface->bounds->wmin > dims->width || + node->surface->bounds->hmin > dims->height) { + node->flags |= NFLAG_SMALL; + return 1; + } else if (node->surface->bounds->wmax < dims->width || + node->surface->bounds->hmax < dims->height) { + node->flags |= NFLAG_LARGE; + return 1; + } + + if (!IS_PARENT_NODE(node)) + return 0; + + if (node->cbounds.wmin_abs + node->cbounds.wmin_rel * dims->width > + dims->width || + node->cbounds.hmin_abs + node->cbounds.hmin_rel * dims->height > + dims->height) { + node->flags |= NFLAG_SMALLCHILD; + return 1; + } + + int axm_size, axm_free; + size_t bounds_axm_min_offset, bounds_axm_max_offset; + size_t dims_axm_pos_offset, dims_axm_size_offset; + + if (node->axis == AXIS_X) { + axm_free = dims->width; + axm_size = dims->width; + + dims_axm_pos_offset = offsetof(struct ct_dims, x); + dims_axm_size_offset = offsetof(struct ct_dims, width); + bounds_axm_min_offset = offsetof(struct ct_bounds, wmin); + bounds_axm_max_offset = offsetof(struct ct_bounds, wmax); + } else { + assert(node->axis == AXIS_Y); + axm_free = dims->height; + axm_size = dims->height; + + dims_axm_pos_offset = offsetof(struct ct_dims, y); + dims_axm_size_offset = offsetof(struct ct_dims, height); + bounds_axm_min_offset = offsetof(struct ct_bounds, hmin); + bounds_axm_max_offset = offsetof(struct ct_bounds, hmax); + } + + struct ct_spdims cspdims[node->cindex]; + cindex parts_n = node->cindex; + memset(cspdims, 0, sizeof(struct ct_spdims) * node->cindex); + for (int i = 0; i < node->cindex; i++) { + cspdims[i].min = *(int *)((char *)node->child[i]->surface->bounds + + bounds_axm_min_offset); + cspdims[i].max = *(int *)((char *)node->child[i]->surface->bounds + + bounds_axm_max_offset); + + if (node->child[i]->surface->bounds->type == BOUND_RELATIVE) { + cspdims[i].min *= axm_size; + cspdims[i].max *= axm_size; + } + + cspdims[i].axm_size = 0; + } + + int split, new_size; + while (axm_free && parts_n) { + split = (axm_free > parts_n) ? (axm_free / parts_n) : 1; + for (int i = 0; i < node->cindex; i++) { + if (cspdims[i].fixed) + continue; + + new_size = + clampi(cspdims[i].axm_size + split, cspdims[i].min, cspdims[i].max); + + if (new_size == cspdims[i].axm_size) { + cspdims[i].fixed = true; + parts_n--; + continue; + } + + axm_free -= (new_size - cspdims[i].axm_size); + cspdims[i].axm_size = new_size; + + if (axm_free == 0) + break; + } + } + + struct ct_dims *cdims = dup_dims(dims); + /* NOTE: the statements below are done implicitly by dup_dims */ + // *(int*)(cdims + dims_axm_pos_offset) = *(int*)(dims + + // dims_axm_pos_offset); + // *(int*)(cdims + dims_axo_pos_offset) = *(int*)(dims + + // dims_axo_pos_offset); + // *(int*)(cdims + dims_axo_size_offset) = axo_size; + for (int i = 0; i < node->cindex; i++) { + *(int *)((char *)cdims + dims_axm_size_offset) = cspdims[i].axm_size; + resize_node(node->child[i], dup_dims(cdims)); + *(int *)((char *)cdims + dims_axm_pos_offset) += cspdims[i].axm_size; + } + free(cdims); + + return 0; +} + +static int __set_cbounds(struct ct_node *const parent, + const struct ct_node *const child, + const bool additive) { + /* child and parent w/h min aliases */ + int c_wmin, c_hmin; + int p_wmin_rel, p_hmin_rel; + + c_wmin = child->surface->bounds->wmin; + c_hmin = child->surface->bounds->hmin; + + if (!additive) { + c_wmin *= -1; + c_hmin *= -1; + } + + if (child->surface->bounds->type == BOUND_RELATIVE) { + parent->cbounds.wmin_abs++; + parent->cbounds.hmin_abs++; + + p_wmin_rel = parent->cbounds.wmin_rel + c_wmin; + p_hmin_rel = parent->cbounds.hmin_rel + c_wmin; + if (p_wmin_rel > __BOUND_REL_MAX || p_hmin_rel >= __BOUND_REL_MAX) + return 1; + + parent->cbounds.wmin_rel = p_wmin_rel; + parent->cbounds.hmin_rel = p_hmin_rel; + assert(parent->cbounds.wmin_rel >= __BOUND_REL_MIN); + assert(parent->cbounds.hmin_rel >= __BOUND_REL_MIN); + } else { + parent->cbounds.wmin_abs += c_wmin; + parent->cbounds.hmin_abs += c_hmin; + assert(parent->cbounds.wmin_abs >= __BOUND_ABS_MIN); + assert(parent->cbounds.hmin_abs >= __BOUND_ABS_MIN); + } + return 0; +} + +/* Returns: + * 0 -> success + * 1 -> failed (max child limit reached) + * 2 -> failed (cumulative relative bounds surpasses 100%) + */ +int insert_child_node(struct ct_node *const parent, struct ct_node *const child, + const cindex i) { + if (parent->cindex == CINDEX_MAX) + return 1; + else if (__set_cbounds(parent, child, true)) + return 2; + else if (parent->cindex == parent->csize) { + // grow child array size and clamp maximum + cindex new_csize = parent->csize * NODE_CHILDREN_GROWTH; + // check overflow + parent->csize = (parent->csize <= new_csize) ? new_csize : CINDEX_MAX; + + parent->child = + reallocarray(parent->child, parent->csize, sizeof(struct ct_node *)); + } + + // shift all children up for insertion + for (int j = parent->cindex; j > i; j--) { + parent->child[j] = parent->child[j - 1]; + } + // do insertion + parent->child[i] = child; + parent->cindex++; + // request cursetree recompute dimensions recursively from parent + parent->flags |= NFLAG_RESIZE; + return EXIT_SUCCESS; +} + +/* Returns: + * 0 -> success + * 1 -> failed (max child limit reached) + * 2 -> failed (cumulative relative bounds surpasses 100%) + */ +int append_child_node(struct ct_node *const parent, + struct ct_node *const child) { + return insert_child_node(parent, child, parent->cindex); +} + +/* Remove a child node from a parent node by index. + * Returns NULL if an invalid index was provided, otherwise returns + * a pointer to the child node removed. + * NOTE: This does NOT destroy the child node and should be + * NOTE: used when moving the child somewhere else. + * NOTE: Otherwise use destroy_child_node() instead. + */ +struct ct_node *remove_child_node(struct ct_node *const parent, + const cindex i) { + struct ct_node *child; + + if (i >= parent->cindex) + return NULL; + else if (parent->cindex <= parent->csize / NODE_CHILDREN_GROWTH) { + // shrink child array to avoid memory bloat + parent->csize /= NODE_CHILDREN_GROWTH; + parent->child = + reallocarray(parent->child, parent->csize, sizeof(struct ct_node *)); + } + + child = &parent[i]; + // shift all children down to fill removal + for (int j = i; j < parent->cindex; j++) { + parent->child[j] = parent->child[j + 1]; + } + parent->cindex--; + __set_cbounds(parent, child, false); + // request cursetree recompute dimensions recursively from parent + parent->flags |= NFLAG_RESIZE; + return child; +} + +/* Remove and destroy a child node by index. + * Returns 1 on failure (invalid index provided), and 0 on success. + * NOTE: Use remove_child_node() instead if the child should live. + */ +int destroy_child_node(struct ct_node *const parent, const cindex i) { + struct ct_node *child = remove_child_node(parent, i); + __destroy_node(child); + return (child == NULL); +} + +/* + * Returns: + * 0 -> success + * 1 -> failed (node has no parent) + * 2 -> failed (parent has no reference to node, CRITICAL) + */ +static int __index_as_child(const struct ct_node *const node, + int *const index) { + if (node->parent == NULL) + return 1; + + for (int i = 0; i < node->parent->cindex; i++) { + if (node->parent->child[i] == node) { + *index = i; + return 0; + } + } + + return 2; +} + +/* If preserve_bounds is set then upon collapse the new node maintains the + * original node's bounds struct. Otherwise the bounds of the child collapsed + * onto are used. + */ +void collapse_node(struct ct_node **const node, const int i, + const bool preserve_bounds) { + assert(0 <= i && i < (*node)->cindex); + int parent_index; + struct ct_node *const parent = (*node)->parent; + struct ct_node *collapse_target = remove_child_node(*node, i); + struct ct_dims *dims = dup_dims((*node)->surface->dims); + struct ct_bounds *bounds; + + if (preserve_bounds) { + bounds = dup_bounds((*node)->surface->bounds); + rebind_surface(collapse_target->surface, bounds); + } + + /* Destroy original node and point to the collapse_target */ + if (__index_as_child(*node, &parent_index)) { + /* __index_as_child fails (typically) because + * the *node is cursetree's root node. */ + __destroy_node(*node); + *node = collapse_target; + } else { + destroy_child_node(parent, parent_index); + insert_child_node(parent, collapse_target, parent_index); + } + + resize_node(collapse_target, dims); +} diff --git a/cursetree/node.h b/cursetree/node.h new file mode 100644 index 0000000..316b8e3 --- /dev/null +++ b/cursetree/node.h @@ -0,0 +1,66 @@ +#ifndef CURSETREE_NODE_H +#define CURSETREE_NODE_H + +#include "dims.h" +#include "limits.h" +#include "surface.h" + +#ifndef __NCURSES_H +typedef struct _win_st WINDOW; +#endif /* __NCURSES_H */ + +#define NODE_INIT_CHILDREN 4 +#define NODE_CHILDREN_GROWTH 1.5 +#define CINDEX_MAX (UCHAR_MAX) + +#define NFLAG_EMPTY (0) +#define NFLAG_RESIZE (1 << 0) +#define NFLAG_SMALL (1 << 1) +#define NFLAG_SMALLCHILD (1 << 2) +#define NFLAG_LARGE (1 << 3) + +/* Child Index */ +typedef unsigned char cindex; + +struct ct_node { + struct ct_surface *surface; + unsigned char flags; + + struct ct_node *parent; + enum ct_axis axis; + struct ct_node **child; + cindex csize, cindex; + /* child imposed minimum bounds */ + struct { + int wmin_abs; + int hmin_abs; + int wmin_rel; + int hmin_rel; + } cbounds; +}; + +/* === External Interface === */ +#define IS_ROOT_NODE(node) (node->parent == NULL) +#define IS_PARENT_NODE(node) (node->cindex != 0) + +struct ct_node *__node(struct ct_dims *const dims, + struct ct_bounds *const bounds, + struct ct_node *const parent); +struct ct_node *new_node(struct ct_bounds *const bounds, + struct ct_node *const parent); +void __destroy_node(struct ct_node *const node); + +int resize_node(struct ct_node *const node, struct ct_dims *dims); + +int insert_child_node(struct ct_node *const parent, struct ct_node *const child, + const cindex i); +int append_child_node(struct ct_node *const parent, + struct ct_node *const child); +struct ct_node *remove_child_node(struct ct_node *const parent, + const cindex i); +int destroy_child_node(struct ct_node *const parent, const cindex i); + +void collapse_node(struct ct_node **const node, const int i, + const bool preserve_bounds); + +#endif /* CURSETREE_NODE_H */ diff --git a/cursetree/pty/_pty.c b/cursetree/pty/_pty.c new file mode 100644 index 0000000..6625e3d --- /dev/null +++ b/cursetree/pty/_pty.c @@ -0,0 +1,177 @@ +/* WARNING: The mkpty(), forkmkpty(), & bindpty() functions are based on + * WARNING: the glibc openpty(), forkpty(), & login_tty() functions respectively. + * WARNING: + * WARNING: The GNU C Library's COPYING & COPYING.LIB licenses are available at + * WARNING: LICENSES/GLIBC-COPYING & LICENSES/GLIBC-COPYING.LIB in this repo. + * WARNING: + * WARNING: login_tty() maintains the original University of California license + * WARNING: available at LICENSES/UC-LICENSE in this repo. + */ + +/* _XOPEN_SOURCE unlocks pty/ptmx/pts declarations. */ +#define _XOPEN_SOURCE 600 +/* _GNU_SOURCE unlocks the ptsname_r declaration*/ +#define _GNU_SOURCE +#include +#include +#include /* TIOC* constants */ +#include + +#include "_pty.h" + +/* Allocate PTY master and slave file descriptors. + * errno will have been set if newpty() fails. + * + * NOTE: This function is my alternative to GLibC's + * NOTE: openpty() function. It exists as a learning resource. + * REF: https://sourceware.org/git/glibc.git -> ./login/openpty.c + */ +int mkpty(int *fdmx, int *fds) { + int _fdmx = -1, _fds = -1; + char sname[TTYNAME_MAX]; + + // Configure PTY master (file descriptor) + _fdmx = posix_openpt(O_RDWR | O_NOCTTY); + if (_fdmx == -1) + return EXIT_FAILURE; + + if (grantpt(_fdmx)) + goto fail; + if (unlockpt(_fdmx)) + goto fail; + +#ifdef TIOCGPTPEER + /* Try to allocate slave fd solely based on PTMX fd first. */ + _fds = ioctl(_fdmx, TIOCGPTPEER, O_RDWR | O_NOCTTY); +#endif + if (_fds == -1) { + /* Fallback to path-based slave fd allocation + * (if the kernel doesn't support TIOCGPTPEER, ie Linux <4.13) */ + if(ptsname_r(_fdmx, sname, sizeof(sname))) + goto fail; + + _fds = open(sname, O_RDWR | O_NOCTTY); + if (_fds == -1) + goto fail; + } + + /* Propagate file descriptors via parameters */ + *fdmx = _fdmx; + *fds = _fds; + return EXIT_SUCCESS; + +fail: + if (_fdmx == -1) { + close(_fdmx); + if (_fds == -1) + close(_fds); + } + return EXIT_FAILURE; +} + +/* Set fdty as the controlling terminal for the calling process. + * Returns 0 on success, and 1 on failure. + * NOTE: This function is my alternative to GLibC's + * NOTE: login_tty() function. It exists as a learning resource. + * REF: https://sourceware.org/git/glibc.git -> ./login/login_tty.c + */ +static inline int setctty(const int fdty) { + /* We assume any kernel compiling this defines TIOCSCTTY, + * otherwise this implementation won't exactly work... */ + return (ioctl(fdty, TIOCSCTTY, 0) != -1); +} + +/* Bind fdty (terminal fd) to stdin/stdout/stderr for the calling process. + * This functions blocks until the EBUSY (see `man dup2`) race condition lifts. + * NOTE: This function is my alternative to GLibC's + * NOTE: login_tty() function. It exists as a learning resource. + * WARNING: This function maintains the original University of California + * WARNING: LICENSE (1990-1993) as per glibc.git:/login/login_tty.c + * WARNING: available at LICENSES/UC-LICENSE in this repo. + * REF: https://sourceware.org/git/glibc.git -> ./login/login_tty.c + */ +void bindpty(const int fdty) { + /* Adjust stdin/stdout/stderr to refer to fd*/ + BIND(fdty, STDIN_FILENO); + BIND(fdty, STDOUT_FILENO); + BIND(fdty, STDERR_FILENO); + if (fdty > 2) + close(fdty); +} + +/* Allocate a PTY and fork, giving ptmx (master) to the parent + * and binding the child's stdin/stdout/stderr to pts (slave). + * Return value is indentical to fork(2). + * NOTE: This function is my alternative to GLibC's + * NOTE: forkpty() function. It exists as a learning resource. + * REF: https://sourceware.org/git/glibc.git -> ./login/forkpty.c + */ +pid_t forkmkpty(int *fdmx) { + int _fdmx, fds; + pid_t pid; + if (mkpty(&_fdmx, &fds)) + return EXIT_FAILURE; + + switch (pid = fork()) { + case -1: + close(_fdmx); + close(fds); + return -1; + case 0: + /* Child Process */ + close(_fdmx); + setctty(fds); + bindpty(fds); + break; + + default: + /* Parent Process */ + close(fds); + // propagate ptmx (master) fd + *fdmx = _fdmx; + break; + } + + /* Both Processes */ + return pid; +} + +/* Set pseudoterminal slave's window size. + * Returns 0 on success, and fails with -1 if the kernel doesn't + * implement this, or 1 for general errors (errno will be set). + * NOTE: Typically this is part of a glibc openpty() call. + */ +int setptsxy(const unsigned short rows, const unsigned short cols, const int fds) { +#ifndef TIOCSWINSZ + /* Fail if kernel doesn't support TIOCSWINSZ. */ + return -1; +#else + struct winsize win = { + .ws_row = rows, + .ws_col = cols, + }; + + if (ioctl(fds, TIOCSWINSZ, &win) == -1) + return EXIT_FAILURE; + return EXIT_SUCCESS; +#endif /* TIOCSWINSZ */ +} + +/* Get pseudoterminal slave's window size. + * Returns 0 on success, and fails with -1 if the kernel doesn't + * implement this, or 1 for general errors (errno will be set). + */ +int getptsxy(unsigned short *rows, unsigned short *cols, const int fds) { +#ifndef TIOCGWINSZ + /* Fail if kernel doesn't support TIOCGWINSZ. */ + return -1; +#else + struct winsize win = (struct winsize){ 0 }; + if (ioctl(fds, TIOCGWINSZ, &win) == -1) + return EXIT_FAILURE; + *rows = win.ws_row; + *cols = win.ws_col; + return EXIT_SUCCESS; +#endif /* TIOCGWINSZ */ +} + diff --git a/cursetree/pty/_pty.h b/cursetree/pty/_pty.h new file mode 100644 index 0000000..652178f --- /dev/null +++ b/cursetree/pty/_pty.h @@ -0,0 +1,30 @@ +#ifndef CURSETREE_PTY_H +#define CURSETREE_PTY_H + +#include +#include +#include +#include + +#ifdef PATH_MAX +#define TTYNAME_MAX PATH_MAX +#else +#define TTYNAME_MAX 512 +#endif /* PATH_MAX */ + +#define BIND(fdsrc, fddst) \ + while (dup2(fdsrc, fddst) == -1 && errno == EBUSY) \ + ; + +/* Custom implementation of glibc::openpty() */ +int mkpty(int *fdmx, int *fds); +/* Custom implementation of glibc::login_tty() */ +void bindpty(const int fdty); +/* Custom implementation of glibc::forkpty() */ +pid_t forkmkpty(int *fdmx); + +int setptsxy(const unsigned short rows, const unsigned short cols, + const int fds); +int getptsxy(unsigned short *rows, unsigned short *cols, const int fds); + +#endif /* CURSETREE_PTY_H */ diff --git a/cursetree/pty/child.c b/cursetree/pty/child.c new file mode 100644 index 0000000..6fe3313 --- /dev/null +++ b/cursetree/pty/child.c @@ -0,0 +1,63 @@ +#include +#include +#include +#include + +#include "child.h" +#include "epty.h" + +/* SIGTERM grace period before a child is sent SIGKILL. + * Value is in microseconds (us), 1000us = 1ms */ +#define CHILD_GRACE_US 100000 + +/* Check if a child is still alive by pid + * by kill(3p) sending a null signal (0). + */ +static int is_alive(struct d_child *child) { + return (kill(child->pid, 0) == 0); +} + +/* Force a child to die, first by sending SIGTERM, otherwise SIGKILL. + * NOTE: killchild() ASSUMES the child is LIVING at the start. + * NOTE: Keep this in mind when threading! + */ +void killchild(struct d_child *child) { + int stat; + + /* Request Death */ + kill(child->pid, SIGTERM); + usleep(CHILD_GRACE_US); + + /* Force Death */ + if (is_alive(child)) + kill(child->pid, SIGKILL); + + /* Reap */ + waitpid(child->pid, &stat, 0); +} + +/* Fork to spawn a child process running bin/shfx in an "epty". + */ +int spawnchild(struct d_child *child) { + *child = (struct d_child){ 0 }; + + /* fork(2) and allocate an "error-piped pseudoterminal" (epty) */ + switch (child->pid = forkepty(&child->fdmx, &child->fderr)) { + case -1: + perror("forkepty"); + return EXIT_FAILURE; + case 0: + char *args[] = {"shfx", NULL}; + execvp("shfx", args); + perror("execvp"); + exit(1); + default: + usleep(1000); + char bufout[10]; + bufout[9] = '\0'; + read(child->fdmx, bufout, 9); + printf("child stdout: \"%s\"\n", bufout); + break; + } + return EXIT_SUCCESS; +} diff --git a/cursetree/pty/child.h b/cursetree/pty/child.h new file mode 100644 index 0000000..294b1c3 --- /dev/null +++ b/cursetree/pty/child.h @@ -0,0 +1,14 @@ +#ifndef CURSETREE_CHILD_H +#define CURSETREE_CHILD_H + +#include + +struct d_child { + pid_t pid; + int fdmx; /* PTY master fd (read/write) */ + int fderr; /* child's stderr (readonly) */ +} __attribute__((packed, aligned(4))); + +int spawnchild(struct d_child *child); + +#endif /* CURSETREE_CHILD_H */ diff --git a/cursetree/pty/epty.c b/cursetree/pty/epty.c new file mode 100644 index 0000000..050a2d7 --- /dev/null +++ b/cursetree/pty/epty.c @@ -0,0 +1,45 @@ +#include + +#include "_pty.h" + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +/* Allocate a PTY and fork, giving fdmx (master) to the parent + * and binding the child's stdin/stdout to fds (slave). + * Return value is indentical to fork(2). + * NOTE: This function is my alternative to GLibC's + * NOTE: forkpty() function. It exists as a learning resource. + * REF: https://sourceware.org/git/glibc.git -> ./login/forkpty.c + */ +pid_t forkepty(int *fdmx, int *fderr) { + // master/slave, and stderr pipe fds + int epipe[2]; + pid_t pid; + + if (pipe(epipe) == -1) + return EXIT_FAILURE; + switch (pid = forkmkpty(fdmx)) { + case -1: + /* forkmkpty() will close fdmx/fds for us */ + close(epipe[PIPE_READ]); + close(epipe[PIPE_WRITE]); + return -1; + case 0: + /* Child Process */ + /* forkmkpty() will close fdmx for us */ + close(epipe[PIPE_READ]); + BIND(epipe[PIPE_WRITE], STDERR_FILENO); + break; + + default: + /* Parent Process */ + /* forkmkpty() will close fds for us */ + close(epipe[PIPE_WRITE]); + *fderr = epipe[PIPE_READ]; + break; + } + + /* Both Processes */ + return pid; +} diff --git a/cursetree/pty/epty.h b/cursetree/pty/epty.h new file mode 100644 index 0000000..fa013bb --- /dev/null +++ b/cursetree/pty/epty.h @@ -0,0 +1,8 @@ +#ifndef CURSETREE_EPTY_H +#define CURSETREE_EPTY_H + +#include + +pid_t forkepty(int *fdmx, int *fderr); + +#endif /* CURSETREE_EPTY_H */ diff --git a/cursetree/surface.c b/cursetree/surface.c new file mode 100644 index 0000000..a028bc8 --- /dev/null +++ b/cursetree/surface.c @@ -0,0 +1,81 @@ +#include + +#include "ncrswrap.h" +#include "surface.h" +#include "_ncurses.h" +#include "ncurses.h" + +static inline struct ct_surface *__surface(struct ct_dims *const dims, + struct ct_bounds *const bounds, + WINDOW *const win) { + struct ct_surface *surface; + + surface = (struct ct_surface *)malloc(sizeof(struct ct_surface)); + *surface = (struct ct_surface){ + .dims = dims, + .bounds = bounds, + .win = win, + .updatereq = true, + }; + + return surface; +} + +struct ct_surface *new_surface(struct ct_dims *const dims, + struct ct_bounds *const bounds) { + WINDOW *const win = new_window(dims->x, dims->y, dims->width, dims->height); + return __surface(dims, bounds, win); +} + +void destroy_surface(const struct ct_surface *const surface) { + delwin(surface->win); + free(surface->dims); +} + +void resize_surface(struct ct_surface *const surface, + struct ct_dims *const dims) { + free(surface->dims); + surface->dims = dims; + surface->updatereq = true; + + resizemv_window(surface->win, dims->x, dims->y, dims->width, dims->height); +} + +void rebind_surface(struct ct_surface *const surface, + struct ct_bounds *const bounds) { + free(surface->bounds); + surface->bounds = bounds; +} + +int sfwidth(const struct ct_surface *const surface) { + return getmaxx(surface->win); +} + +int sfheight(const struct ct_surface *const surface) { + return getmaxy(surface->win); +} + +int sfposx(const struct ct_surface *const surface) { + return getbegx(surface->win); +} + +int sfposy(const struct ct_surface *const surface) { + return getbegy(surface->win); +} + +struct ct_dims *sfdims(const struct ct_surface *const surface) { + int x, y, width, height; + sfpos(surface, x, y); + sfsize(surface, width, height); + return new_dims(x, y, width, height); +} + +void sfclear(struct ct_surface *const surface) { + wclear(surface->win); + surface->updatereq = true; +} + +void sfflush(struct ct_surface *const surface) { + wnoutrefresh(surface->win); + surface->updatereq = false; +} diff --git a/cursetree/surface.h b/cursetree/surface.h new file mode 100644 index 0000000..c84f7ee --- /dev/null +++ b/cursetree/surface.h @@ -0,0 +1,38 @@ +#ifndef CURSETREE_SURFACE_H +#define CURSETREE_SURFACE_H + +#include "dims.h" + +#ifndef __NCURSES_H +typedef struct _win_st WINDOW; +#endif /* __NCURSES_H */ + +struct ct_surface { + struct ct_dims *dims; + struct ct_bounds *bounds; + WINDOW *win; + unsigned char updatereq; +}; + +struct ct_surface *new_surface(struct ct_dims *const dims, + struct ct_bounds *const bounds); +void destroy_surface(const struct ct_surface *const surface); + +void resize_surface(struct ct_surface *const surface, + struct ct_dims *const dims); +void rebind_surface(struct ct_surface *const surface, + struct ct_bounds *const bounds); + +#define sfpos(surface, x, y) (x = sfposx(surface), y = sfposy(surface)) +#define sfsize(surface, width, height) (width = sfwidth(surface), height = sfheight(surface)) + +int sfwidth(const struct ct_surface *const surface); +int sfheight(const struct ct_surface *const surface); +int sfposx(const struct ct_surface *const surface); +int sfposy(const struct ct_surface *const surface); +struct ct_dims *sfdims(const struct ct_surface *const surface); + +void sfclear(struct ct_surface *const surface); +void sfflush(struct ct_surface *const surface); + +#endif /* CURSETREE_SURFACE_H */ diff --git a/cursetree/tree.c b/cursetree/tree.c new file mode 100644 index 0000000..4f49aff --- /dev/null +++ b/cursetree/tree.c @@ -0,0 +1,38 @@ +#include + +#include "ncrswrap.h" +#include "tree.h" + +/* + */ +static inline struct ct_node *__root_node(void) { + return __node(termdims(), bounds_none(), NULL); +} + +int init_tree(struct ct_tree **const tree) { + *tree = (struct ct_tree *)malloc(sizeof(struct ct_tree)); + (*tree)->root = __root_node(); + return EXIT_SUCCESS; +} + +void destroy_tree(struct ct_tree *const tree) { + __destroy_node(tree->root); + end_ncurses(); + free(tree); +} + +void resize_tree(struct ct_tree *const tree, struct ct_dims *const dims) { + resize_node(tree->root, dims); +} + +void switch_nodes(struct ct_node **const node0, struct ct_node **const node1) { + struct ct_node *const node0ptr = *node0; + struct ct_dims *const node0dims = dup_dims((*node0)->surface->dims); + struct ct_dims *const node1dims = dup_dims((*node1)->surface->dims); + + *node0 = *node1; + resize_node(*node0, node1dims); + + *node1 = node0ptr; + resize_node(*node1, node0dims); +} diff --git a/cursetree/tree.h b/cursetree/tree.h new file mode 100644 index 0000000..a170143 --- /dev/null +++ b/cursetree/tree.h @@ -0,0 +1,15 @@ +#ifndef CURSETREE_TREE_H +#define CURSETREE_TREE_H + +#include "node.h" + +struct ct_tree { + struct ct_node *root; +}; + +/* === External Interface === */ +int init_tree(struct ct_tree **const tree); +void destroy_tree(struct ct_tree *const tree); +void resize_tree(struct ct_tree *const tree, struct ct_dims *const dims); + +#endif /* CURSETREE_TREE_H */ diff --git a/cursetree/util.h b/cursetree/util.h new file mode 100644 index 0000000..6b97a12 --- /dev/null +++ b/cursetree/util.h @@ -0,0 +1,12 @@ +#ifndef CURSETREE_UTIL_H +#define CURSETREE_UTIL_H + +static inline int clampi(int val, int min, int max) { + if (val > max) + return max; + else if (val < min) + return min; + return val; +} + +#endif /* CURSETREE_UTIL_H */