Phase 4-6: Quill editor, Lens viewer, Rigel context menu, CI, website
Phase 4 — native apps: - quill/: text editor (monospace, double-buffered, find bar, undo, open/save) - lens/: image viewer using imlib2 (zoom, pan, rotate, directory nav) - wm/src/menu.c: launch quill instead of mousepad - iso/docker/entrypoint.sh: add imlib2 package; install quill + lens binaries - skykit/template/: developer app template demonstrating the SkyKit API Phase 5 — file system and search: - rigel/rigel.c: right-click context menu (open, open in quill, copy path, rename, trash, properties); Ctrl+F search/filter bar with name highlighting; F2 rename-in-place overlay; Delete key moves to trash Phase 6 — distribution: - .github/workflows/build-iso.yml: GitHub Actions CI that compiles all components and builds a bootable ISO on every tag push; creates a GitHub release with sha256 checksums Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d19aca2333
commit
a9efede702
12 files changed
+2186
-92
No files matched your search
+840
@@ -0,0 +1,840 @@
|
||||
/* 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 <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <X11/Xft/Xft.h>
|
||||
#include <fontconfig/fontconfig.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ── 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;
|
||||
int flen = ed.find_len;
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user