/* quill.c — OrionOS Text Editor * Named for the writing instrument. Native C/Xlib, no GTK. * * Keybindings: * Ctrl+N New file * Ctrl+O Open file (shows path bar) * Ctrl+S Save * Ctrl+F Find (Esc or Enter to dismiss) * Ctrl+Z Undo (single snapshot) * Ctrl+Left/Right Word skip * Ctrl+Home/End File start/end * Ctrl+G Go to line */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* ── Geometry ─────────────────────────────────────────────── */ #define WIN_W 900 #define WIN_H 620 #define HDR_H 38 #define STAT_H 22 #define GUTTER_W 54 #define ROW_H 18 #define TEXT_X GUTTER_W #define TEXT_Y HDR_H #define TEXT_W (WIN_W - GUTTER_W) #define TEXT_H (WIN_H - HDR_H - STAT_H) #define VIS_ROWS (TEXT_H / ROW_H) /* find bar overlays bottom of text area */ #define FIND_H 28 #define FIND_Y (WIN_H - STAT_H - FIND_H) /* ── Buffer ───────────────────────────────────────────────── */ #define MAX_LINES 8192 #define LINE_CAP 2048 /* ── Palette ──────────────────────────────────────────────── */ #define C_BG 0x0D1117UL #define C_HDR 0x090C12UL #define C_GUTTER 0x0A0E16UL #define C_CURLINE 0x13192AUL #define C_BORDER 0x1A2035UL #define C_TEXT 0xCDD6F4UL #define C_DIM 0x3A4860UL #define C_LINENUM 0x4A5880UL #define C_ACCENT 0x1E58A8UL #define C_CURSOR 0x89B4FAUL #define C_FIND_BG 0x141A28UL #define C_FIND_HL 0x264878UL #define C_MOD 0xF38BA8UL #define C_SAVED 0xA6E3A1UL #define C_SEL 0x1E3A5AUL /* ── Editor state ─────────────────────────────────────────── */ typedef struct { char lines[MAX_LINES][LINE_CAP]; int nlines; int cur; /* cursor line */ int col; /* cursor column */ int scroll; /* first visible line */ char path[4096]; int modified; /* find */ char find[128]; int find_len; int find_on; /* find bar visible */ /* open overlay */ char open_buf[4096]; int open_len; int open_on; /* goto overlay */ char goto_buf[16]; int goto_len; int goto_on; /* undo */ char undo_lines[MAX_LINES][LINE_CAP]; int undo_nlines, undo_cur, undo_col; int undo_valid; } Editor; /* ── App state ────────────────────────────────────────────── */ static Display *dpy; static int scr; static Window win; static GC gc; static Pixmap buf; static XftFont *fmono; /* editor body font */ static XftFont *fui; /* UI chrome font */ static Visual *vis; static Colormap cmap; static int depth; static Atom wm_del; static int running = 1; static int dirty = 1; static int char_w = 0; static Editor ed; /* ── Colour helpers ───────────────────────────────────────── */ static unsigned long px(unsigned long rgb) { XColor c; c.red = ((rgb >> 16) & 0xFF) * 257; c.green = ((rgb >> 8) & 0xFF) * 257; c.blue = ( rgb & 0xFF) * 257; c.flags = DoRed | DoGreen | DoBlue; XAllocColor(dpy, cmap, &c); return c.pixel; } static void fill(int x, int y, int w, int h, unsigned long rgb) { XSetForeground(dpy, gc, px(rgb)); XFillRectangle(dpy, buf, gc, x, y, w, h); } static void rect(int x, int y, int w, int h, unsigned long rgb) { XSetForeground(dpy, gc, px(rgb)); XDrawRectangle(dpy, buf, gc, x, y, w - 1, h - 1); } static void hline(int x, int y, int w, unsigned long rgb) { XSetForeground(dpy, gc, px(rgb)); XDrawLine(dpy, buf, gc, x, y, x + w - 1, y); } static XftColor xftc(unsigned long rgb) { XftColor c; XRenderColor rc = { .red = ((rgb >> 16) & 0xFF) * 257, .green = ((rgb >> 8) & 0xFF) * 257, .blue = ( rgb & 0xFF) * 257, .alpha = 0xFFFF }; XftColorAllocValue(dpy, vis, cmap, &rc, &c); return c; } static void xft_str(XftDraw *xd, XftFont *f, unsigned long rgb, int x, int y, const char *s, int len) { XftColor c = xftc(rgb); XftDrawStringUtf8(xd, &c, f, x, y, (const FcChar8 *)s, len); XftColorFree(dpy, vis, cmap, &c); } static int text_w(XftFont *f, const char *s, int len) { XGlyphInfo ext; XftTextExtentsUtf8(dpy, f, (const FcChar8 *)s, len, &ext); return ext.xOff; } /* baseline for vertically centred text in a band */ static int baseline(XftFont *f, int y, int h) { return y + (h + f->ascent - f->descent) / 2; } /* ── Editor operations ────────────────────────────────────── */ static void ed_save_undo(void) { memcpy(ed.undo_lines, ed.lines, sizeof(ed.lines)); ed.undo_nlines = ed.nlines; ed.undo_cur = ed.cur; ed.undo_col = ed.col; ed.undo_valid = 1; } static void ed_undo(void) { if (!ed.undo_valid) return; memcpy(ed.lines, ed.undo_lines, sizeof(ed.lines)); ed.nlines = ed.undo_nlines; ed.cur = ed.undo_cur; ed.col = ed.undo_col; ed.modified = 1; ed.undo_valid = 0; } static void ed_clamp_col(void) { int len = (int)strlen(ed.lines[ed.cur]); if (ed.col > len) ed.col = len; } static void ed_ensure_visible(void) { if (ed.cur < ed.scroll) ed.scroll = ed.cur; else if (ed.cur >= ed.scroll + VIS_ROWS) ed.scroll = ed.cur - VIS_ROWS + 1; if (ed.scroll < 0) ed.scroll = 0; } static void ed_init(void) { memset(&ed, 0, sizeof(ed)); ed.lines[0][0] = '\0'; ed.nlines = 1; } static void ed_load(const char *path) { FILE *f = fopen(path, "r"); if (!f) return; ed.nlines = 0; while (ed.nlines < MAX_LINES && fgets(ed.lines[ed.nlines], LINE_CAP, f)) { char *ln = ed.lines[ed.nlines]; int len = (int)strlen(ln); if (len > 0 && ln[len-1] == '\n') ln[len-1] = '\0'; ed.nlines++; } if (ed.nlines == 0) { ed.lines[0][0] = '\0'; ed.nlines = 1; } fclose(f); snprintf(ed.path, sizeof(ed.path), "%s", path); ed.cur = ed.col = ed.scroll = 0; ed.modified = 0; ed.undo_valid = 0; } static void ed_save(void) { if (!ed.path[0]) return; FILE *f = fopen(ed.path, "w"); if (!f) return; for (int i = 0; i < ed.nlines; i++) fprintf(f, "%s\n", ed.lines[i]); fclose(f); ed.modified = 0; } static void ed_insert(char c) { char *line = ed.lines[ed.cur]; int len = (int)strlen(line); if (len >= LINE_CAP - 1) return; memmove(line + ed.col + 1, line + ed.col, len - ed.col + 1); line[ed.col++] = c; ed.modified = 1; } static void ed_backspace(void) { if (ed.col > 0) { char *line = ed.lines[ed.cur]; int len = (int)strlen(line); memmove(line + ed.col - 1, line + ed.col, len - ed.col + 1); ed.col--; } else if (ed.cur > 0) { char *prev = ed.lines[ed.cur - 1]; char *curr = ed.lines[ed.cur]; int plen = (int)strlen(prev); int clen = (int)strlen(curr); if (plen + clen < LINE_CAP - 1) { memcpy(prev + plen, curr, clen + 1); memmove(ed.lines + ed.cur, ed.lines + ed.cur + 1, (ed.nlines - ed.cur - 1) * LINE_CAP); ed.nlines--; ed.col = plen; ed.cur--; } } ed.modified = 1; } static void ed_delete(void) { char *line = ed.lines[ed.cur]; int len = (int)strlen(line); if (ed.col < len) { memmove(line + ed.col, line + ed.col + 1, len - ed.col); ed.modified = 1; } else if (ed.cur < ed.nlines - 1) { char *next = ed.lines[ed.cur + 1]; int nlen = (int)strlen(next); if (len + nlen < LINE_CAP - 1) { memcpy(line + len, next, nlen + 1); memmove(ed.lines + ed.cur + 1, ed.lines + ed.cur + 2, (ed.nlines - ed.cur - 2) * LINE_CAP); ed.nlines--; ed.modified = 1; } } } static void ed_newline(void) { if (ed.nlines >= MAX_LINES) return; memmove(ed.lines + ed.cur + 2, ed.lines + ed.cur + 1, (ed.nlines - ed.cur - 1) * LINE_CAP); ed.nlines++; char *curr = ed.lines[ed.cur]; char *next = ed.lines[ed.cur + 1]; int tail = (int)strlen(curr + ed.col); memcpy(next, curr + ed.col, tail + 1); curr[ed.col] = '\0'; /* auto-indent: copy leading whitespace */ int ws = 0; while (curr[ws] == ' ' || curr[ws] == '\t') ws++; if (ws > 0 && ws + tail < LINE_CAP - 1) { memmove(next + ws, next, tail + 1); memcpy(next, curr, ws); ed.col = ws; } else { ed.col = 0; } ed.cur++; ed.modified = 1; } static void ed_word_left(void) { if (ed.col == 0) { if (ed.cur > 0) { ed.cur--; ed.col = (int)strlen(ed.lines[ed.cur]); } return; } ed.col--; char *l = ed.lines[ed.cur]; while (ed.col > 0 && !isalnum((unsigned char)l[ed.col - 1])) ed.col--; while (ed.col > 0 && isalnum((unsigned char)l[ed.col - 1])) ed.col--; } static void ed_word_right(void) { char *l = ed.lines[ed.cur]; int len = (int)strlen(l); if (ed.col == len) { if (ed.cur < ed.nlines - 1) { ed.cur++; ed.col = 0; } return; } while (ed.col < len && !isalnum((unsigned char)l[ed.col])) ed.col++; while (ed.col < len && isalnum((unsigned char)l[ed.col])) ed.col++; } /* find: returns line of next match, -1 if none */ static int ed_find_next(int from_line, int from_col) { if (!ed.find[0]) return -1; for (int pass = 0; pass < 2; pass++) { int start_line = (pass == 0) ? from_line : 0; for (int i = start_line; i < ed.nlines; i++) { char *s = ed.lines[i]; int start_c = (i == from_line && pass == 0) ? from_col : 0; char *found = strstr(s + start_c, ed.find); if (found) { ed.cur = i; ed.col = (int)(found - s); ed_ensure_visible(); return i; } } /* wrap: search from beginning */ } return -1; } /* ── Window title ─────────────────────────────────────────── */ static void set_title(void) { char t[4096 + 32]; const char *name = ed.path[0] ? strrchr(ed.path, '/') : NULL; name = name ? name + 1 : (ed.path[0] ? ed.path : "[untitled]"); snprintf(t, sizeof(t), "Quill — %s%s", name, ed.modified ? " *" : ""); XStoreName(dpy, win, t); } /* ── Drawing ──────────────────────────────────────────────── */ static void redraw(void) { XftDraw *xd = XftDrawCreate(dpy, buf, vis, cmap); /* header */ fill(0, 0, WIN_W, HDR_H, C_HDR); hline(0, HDR_H - 1, WIN_W, C_BORDER); /* gutter */ fill(0, HDR_H, GUTTER_W, TEXT_H, C_GUTTER); hline(GUTTER_W, HDR_H, 1, C_BORDER); /* text area background */ fill(TEXT_X, TEXT_Y, TEXT_W, TEXT_H, C_BG); /* status bar */ fill(0, WIN_H - STAT_H, WIN_W, STAT_H, C_HDR); hline(0, WIN_H - STAT_H, WIN_W, C_BORDER); /* find bar overlay */ if (ed.find_on) { fill(0, FIND_Y, WIN_W, FIND_H, C_FIND_BG); hline(0, FIND_Y, WIN_W, C_BORDER); } /* visible lines */ for (int row = 0; row < VIS_ROWS; row++) { int lnum = ed.scroll + row; if (lnum >= ed.nlines) break; int ry = TEXT_Y + row * ROW_H; int bl = baseline(fmono, ry, ROW_H); /* current line highlight */ if (lnum == ed.cur) fill(0, ry, WIN_W, ROW_H, C_CURLINE); /* line number */ char num[12]; snprintf(num, sizeof(num), "%d", lnum + 1); int nw = text_w(fui, num, (int)strlen(num)); xft_str(xd, fui, C_LINENUM, GUTTER_W - nw - 8, baseline(fui, ry, ROW_H), num, (int)strlen(num)); /* line text */ char *line = ed.lines[lnum]; int llen = (int)strlen(line); if (llen > 0) xft_str(xd, fmono, C_TEXT, TEXT_X + 4, bl, line, llen); /* find highlights */ if (ed.find_on && ed.find[0]) { char *s = line; char *p; while ((p = strstr(s, ed.find)) != NULL) { int fx = TEXT_X + 4 + text_w(fmono, line, (int)(p - line)); int fw = text_w(fmono, ed.find, ed.find_len); fill(fx, ry, fw, ROW_H, C_FIND_HL); xft_str(xd, fmono, C_TEXT, fx, bl, ed.find, ed.find_len); s = p + ed.find_len; } } /* cursor */ if (lnum == ed.cur) { int cx = TEXT_X + 4 + text_w(fmono, line, ed.col); fill(cx, ry, 2, ROW_H, C_CURSOR); } } /* ── header chrome ─────────────────────────────────────── */ if (ed.open_on) { /* open-file path bar in header */ fill(4, 6, WIN_W - 8, HDR_H - 12, C_FIND_BG); rect(4, 6, WIN_W - 8, HDR_H - 12, C_ACCENT); int bl2 = baseline(fmono, 6, HDR_H - 12); xft_str(xd, fui, C_DIM, 10, bl2, "Open: ", 6); int pfx = 10 + text_w(fui, "Open: ", 6); if (ed.open_len > 0) xft_str(xd, fmono, C_TEXT, pfx, bl2, ed.open_buf, ed.open_len); /* cursor */ int cx = pfx + text_w(fmono, ed.open_buf, ed.open_len); fill(cx, 8, 2, HDR_H - 16, C_CURSOR); } else if (ed.goto_on) { fill(4, 6, 200, HDR_H - 12, C_FIND_BG); rect(4, 6, 200, HDR_H - 12, C_ACCENT); int bl2 = baseline(fmono, 6, HDR_H - 12); xft_str(xd, fui, C_DIM, 10, bl2, "Go to line: ", 12); int pfx = 10 + text_w(fui, "Go to line: ", 12); if (ed.goto_len > 0) xft_str(xd, fmono, C_TEXT, pfx, bl2, ed.goto_buf, ed.goto_len); int cx = pfx + text_w(fmono, ed.goto_buf, ed.goto_len); fill(cx, 8, 2, HDR_H - 16, C_CURSOR); } else { /* normal header: filename */ const char *name = ed.path[0] ? strrchr(ed.path, '/') : NULL; name = name ? name + 1 : (ed.path[0] ? ed.path : "[untitled]"); int bl2 = baseline(fui, 0, HDR_H); xft_str(xd, fui, C_TEXT, 12, bl2, name, (int)strlen(name)); if (ed.modified) { int nx = 12 + text_w(fui, name, (int)strlen(name)); xft_str(xd, fui, C_MOD, nx + 6, bl2, "●", 3); } /* right side hints */ const char *hint = "Ctrl+S Save Ctrl+O Open Ctrl+F Find"; int hw = text_w(fui, hint, (int)strlen(hint)); xft_str(xd, fui, C_DIM, WIN_W - hw - 12, bl2, hint, (int)strlen(hint)); } /* ── find bar ───────────────────────────────────────────── */ if (ed.find_on) { int bl2 = baseline(fmono, FIND_Y, FIND_H); xft_str(xd, fui, C_DIM, 10, bl2, "Find: ", 6); int pfx = 10 + text_w(fui, "Find: ", 6); if (ed.find_len > 0) xft_str(xd, fmono, C_TEXT, pfx, bl2, ed.find, ed.find_len); int cx = pfx + text_w(fmono, ed.find, ed.find_len); fill(cx, FIND_Y + 4, 2, FIND_H - 8, C_CURSOR); xft_str(xd, fui, C_DIM, cx + 10, bl2, "Enter: next Esc: close", 23); } /* ── status bar ─────────────────────────────────────────── */ { char stat[128]; snprintf(stat, sizeof(stat), "Line %d, Col %d %d lines", ed.cur + 1, ed.col + 1, ed.nlines); int bl2 = baseline(fui, WIN_H - STAT_H, STAT_H); xft_str(xd, fui, C_DIM, 12, bl2, stat, (int)strlen(stat)); const char *enc = "UTF-8"; int ew = text_w(fui, enc, 5); xft_str(xd, fui, C_DIM, WIN_W - ew - 12, bl2, enc, 5); } XftDrawDestroy(xd); XCopyArea(dpy, buf, win, gc, 0, 0, WIN_W, WIN_H, 0, 0); XFlush(dpy); } /* ── Key handling ─────────────────────────────────────────── */ static void handle_key(XKeyEvent *ev) { char text[32] = {0}; KeySym ksym = 0; int n = XLookupString(ev, text, sizeof(text)-1, &ksym, NULL); int ctrl = (ev->state & ControlMask) != 0; int shift = (ev->state & ShiftMask) != 0; /* ── open-file overlay ─────────────────────────────────── */ if (ed.open_on) { if (ksym == XK_Escape) { ed.open_on = 0; } else if (ksym == XK_Return) { if (ed.open_buf[0]) ed_load(ed.open_buf); ed.open_on = 0; } else if (ksym == XK_BackSpace) { if (ed.open_len > 0) ed.open_buf[--ed.open_len] = '\0'; } else if (n > 0 && (unsigned char)text[0] >= 0x20) { if (ed.open_len < (int)sizeof(ed.open_buf) - 1) { ed.open_buf[ed.open_len++] = text[0]; ed.open_buf[ed.open_len] = '\0'; } } dirty = 1; return; } /* ── goto overlay ──────────────────────────────────────── */ if (ed.goto_on) { if (ksym == XK_Escape) { ed.goto_on = 0; } else if (ksym == XK_Return) { int target = atoi(ed.goto_buf) - 1; if (target >= 0 && target < ed.nlines) { ed.cur = target; ed.col = 0; ed_ensure_visible(); } ed.goto_on = 0; } else if (ksym == XK_BackSpace) { if (ed.goto_len > 0) ed.goto_buf[--ed.goto_len] = '\0'; } else if (n > 0 && isdigit((unsigned char)text[0])) { if (ed.goto_len < 9) { ed.goto_buf[ed.goto_len++] = text[0]; ed.goto_buf[ed.goto_len] = '\0'; } } dirty = 1; return; } /* ── find bar ──────────────────────────────────────────── */ if (ed.find_on) { if (ksym == XK_Escape) { ed.find_on = 0; } else if (ksym == XK_Return) { ed_find_next(ed.cur, ed.col + 1); } else if (ksym == XK_BackSpace) { if (ed.find_len > 0) { ed.find[--ed.find_len] = '\0'; ed_find_next(0, 0); } } else if (n > 0 && (unsigned char)text[0] >= 0x20) { if (ed.find_len < 127) { ed.find[ed.find_len++] = text[0]; ed.find[ed.find_len] = '\0'; ed_find_next(0, 0); } } dirty = 1; return; } /* ── global shortcuts ──────────────────────────────────── */ if (ctrl) { switch (ksym) { case XK_n: ed_save_undo(); ed_init(); dirty = 1; return; case XK_o: memset(ed.open_buf, 0, sizeof(ed.open_buf)); ed.open_len = 0; ed.open_on = 1; dirty = 1; return; case XK_s: if (!ed.path[0]) { memset(ed.open_buf, 0, sizeof(ed.open_buf)); ed.open_len = 0; ed.open_on = 1; } else { ed_save(); set_title(); } dirty = 1; return; case XK_f: ed.find_on = 1; ed.find[0] = '\0'; ed.find_len = 0; dirty = 1; return; case XK_z: ed_undo(); ed_ensure_visible(); dirty = 1; return; case XK_g: memset(ed.goto_buf, 0, sizeof(ed.goto_buf)); ed.goto_len = 0; ed.goto_on = 1; dirty = 1; return; case XK_Home: ed.cur = 0; ed.col = 0; ed_ensure_visible(); dirty = 1; return; case XK_End: ed.cur = ed.nlines - 1; ed.col = (int)strlen(ed.lines[ed.cur]); ed_ensure_visible(); dirty = 1; return; case XK_Left: ed_word_left(); ed_ensure_visible(); dirty = 1; return; case XK_Right: ed_word_right(); ed_ensure_visible(); dirty = 1; return; } } /* ── navigation ────────────────────────────────────────── */ switch (ksym) { case XK_Up: if (ed.cur > 0) { ed.cur--; ed_clamp_col(); } ed_ensure_visible(); dirty = 1; return; case XK_Down: if (ed.cur < ed.nlines - 1) { ed.cur++; ed_clamp_col(); } ed_ensure_visible(); dirty = 1; return; case XK_Left: if (ed.col > 0) ed.col--; else if (ed.cur > 0) { ed.cur--; ed.col = (int)strlen(ed.lines[ed.cur]); ed_ensure_visible(); } dirty = 1; return; case XK_Right: if (ed.col < (int)strlen(ed.lines[ed.cur])) ed.col++; else if (ed.cur < ed.nlines - 1) { ed.cur++; ed.col = 0; ed_ensure_visible(); } dirty = 1; return; case XK_Home: /* smart home: first non-ws char, then col 0 */ { char *l = ed.lines[ed.cur]; int ws = 0; while (l[ws] == ' ' || l[ws] == '\t') ws++; ed.col = (ed.col != ws) ? ws : 0; } dirty = 1; return; case XK_End: ed.col = (int)strlen(ed.lines[ed.cur]); dirty = 1; return; case XK_Page_Up: ed.cur -= VIS_ROWS; if (ed.cur < 0) ed.cur = 0; ed_clamp_col(); ed_ensure_visible(); dirty = 1; return; case XK_Page_Down: ed.cur += VIS_ROWS; if (ed.cur >= ed.nlines) ed.cur = ed.nlines - 1; ed_clamp_col(); ed_ensure_visible(); dirty = 1; return; case XK_Escape: ed.find_on = 0; dirty = 1; return; case XK_BackSpace: ed_save_undo(); ed_backspace(); ed_ensure_visible(); dirty = 1; return; case XK_Delete: ed_save_undo(); ed_delete(); dirty = 1; return; case XK_Return: ed_save_undo(); ed_newline(); ed_ensure_visible(); dirty = 1; return; case XK_Tab: if (shift) { /* unindent: remove leading spaces/tab */ char *l = ed.lines[ed.cur]; if (l[0] == '\t') { memmove(l, l+1, strlen(l)); if (ed.col > 0) ed.col--; } else { int rm = 0; while (rm < 4 && l[0] == ' ') { memmove(l, l+1, strlen(l)); rm++; if (ed.col > 0) ed.col--; } } } else { /* indent: insert 4 spaces */ for (int i = 0; i < 4; i++) ed_insert(' '); } ed.modified = 1; dirty = 1; return; } /* ── printable text ─────────────────────────────────────── */ if (n > 0) { for (int i = 0; i < n; i++) { unsigned char c = (unsigned char)text[i]; if (c >= 0x20 || c == 0x09) { if (ed.col == 0 && (int)strlen(ed.lines[ed.cur]) == 0) ed_save_undo(); ed_insert((char)c); } } ed_ensure_visible(); dirty = 1; } } /* ── Mouse handling ───────────────────────────────────────── */ static void handle_button(XButtonEvent *ev) { if (ev->y < HDR_H || ev->y >= WIN_H - STAT_H) return; int ry = ev->y - TEXT_Y; if (ry < 0) return; int row = ry / ROW_H; int lnum = ed.scroll + row; if (lnum >= ed.nlines) return; ed.cur = lnum; /* click to column */ int click_x = ev->x - TEXT_X - 4; char *line = ed.lines[lnum]; int len = (int)strlen(line); int best = 0, bestd = abs(click_x); for (int c = 1; c <= len; c++) { int cx = text_w(fmono, line, c); int d = abs(click_x - cx); if (d < bestd) { bestd = d; best = c; } } ed.col = best; dirty = 1; } static void handle_scroll(XButtonEvent *ev) { int delta = (ev->button == Button4) ? -3 : 3; ed.scroll += delta; if (ed.scroll < 0) ed.scroll = 0; int max_sc = ed.nlines - VIS_ROWS; if (max_sc < 0) max_sc = 0; if (ed.scroll > max_sc) ed.scroll = max_sc; dirty = 1; } /* ── Main ─────────────────────────────────────────────────── */ int main(int argc, char **argv) { dpy = XOpenDisplay(NULL); if (!dpy) { fputs("quill: cannot open display\n", stderr); return 1; } scr = DefaultScreen(dpy); vis = DefaultVisual(dpy, scr); cmap = DefaultColormap(dpy, scr); depth = DefaultDepth(dpy, scr); fmono = XftFontOpenName(dpy, scr, "Monospace:size=11"); if (!fmono) fmono = XftFontOpenName(dpy, scr, "Sans:size=11"); fui = XftFontOpenName(dpy, scr, "Sans:size=9"); if (!fui) fui = fmono; /* measure monospace char width */ { XGlyphInfo ext; XftTextExtentsUtf8(dpy, fmono, (const FcChar8 *)"M", 1, &ext); char_w = ext.xOff; } XSetWindowAttributes swa = { .background_pixel = px(C_BG), .event_mask = ExposureMask | KeyPressMask | ButtonPressMask | StructureNotifyMask }; win = XCreateWindow(dpy, RootWindow(dpy, scr), 100, 60, WIN_W, WIN_H, 0, depth, InputOutput, vis, CWBackPixel | CWEventMask, &swa); buf = XCreatePixmap(dpy, win, WIN_W, WIN_H, depth); gc = XCreateGC(dpy, buf, 0, NULL); wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(dpy, win, &wm_del, 1); /* set WM_CLASS */ XClassHint *ch = XAllocClassHint(); ch->res_name = "quill"; ch->res_class = "Quill"; XSetClassHint(dpy, win, ch); XFree(ch); ed_init(); if (argc > 1) ed_load(argv[1]); set_title(); XMapWindow(dpy, win); XFlush(dpy); while (running) { if (dirty && !XPending(dpy)) { redraw(); set_title(); dirty = 0; } XEvent ev; XNextEvent(dpy, &ev); switch (ev.type) { case Expose: if (ev.xexpose.count == 0) dirty = 1; break; case KeyPress: handle_key(&ev.xkey); break; case ButtonPress: if (ev.xbutton.button == Button4 || ev.xbutton.button == Button5) handle_scroll(&ev.xbutton); else handle_button(&ev.xbutton); break; case ClientMessage: if ((Atom)ev.xclient.data.l[0] == wm_del) running = 0; break; } } XftFontClose(dpy, fmono); if (fui != fmono) XftFontClose(dpy, fui); XFreePixmap(dpy, buf); XFreeGC(dpy, gc); XDestroyWindow(dpy, win); XCloseDisplay(dpy); return 0; }