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
@@ -0,0 +1,108 @@
|
|||||||
|
name: Build OrionOS ISO
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_name:
|
||||||
|
description: 'Release label (e.g. v0.1-mintaka)'
|
||||||
|
required: false
|
||||||
|
default: 'nightly'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install dependencies on host
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libx11-dev libxft-dev libfontconfig1-dev \
|
||||||
|
libimlib2-dev \
|
||||||
|
pkg-config \
|
||||||
|
xorg-dev
|
||||||
|
|
||||||
|
- name: Build OrionOS shell components
|
||||||
|
run: |
|
||||||
|
make skykit rigel quill lens || true
|
||||||
|
make wm deskbar launcher installer nebula orionpkg || true
|
||||||
|
|
||||||
|
- name: Build ISO (Docker)
|
||||||
|
run: |
|
||||||
|
chmod +x build-iso.sh
|
||||||
|
./build-iso.sh
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
|
||||||
|
- name: Verify ISO was created
|
||||||
|
run: |
|
||||||
|
ls -lh dist/*.iso
|
||||||
|
sha256sum dist/*.iso > dist/SHA256SUMS
|
||||||
|
|
||||||
|
- name: Upload ISO as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: orionos-iso-${{ github.run_number }}
|
||||||
|
path: |
|
||||||
|
dist/*.iso
|
||||||
|
dist/SHA256SUMS
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: OrionOS ${{ github.ref_name }}
|
||||||
|
body: |
|
||||||
|
## OrionOS ${{ github.ref_name }}
|
||||||
|
|
||||||
|
Bootable live ISO. Burn to USB with:
|
||||||
|
```
|
||||||
|
sudo dd if=orionos.iso of=/dev/sdX bs=4M status=progress && sync
|
||||||
|
```
|
||||||
|
Or use [Balena Etcher](https://etcher.balena.io/).
|
||||||
|
|
||||||
|
### What's included
|
||||||
|
- OrionWM — tiling/floating window manager
|
||||||
|
- Rigel — native file manager (right-click menu, search)
|
||||||
|
- Quill — native text editor
|
||||||
|
- Lens — native image viewer
|
||||||
|
- OrionDeskbar — application bar
|
||||||
|
- Nebula — system settings
|
||||||
|
- OrionPkg — package manager GUI
|
||||||
|
- OrionInstall — graphical installer
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
```
|
||||||
|
sha256sum -c SHA256SUMS
|
||||||
|
```
|
||||||
|
files: |
|
||||||
|
dist/*.iso
|
||||||
|
dist/SHA256SUMS
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
|
||||||
|
|
||||||
|
# Separate job: build and push Docker image used by the ISO builder
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Build ISO builder image
|
||||||
|
run: docker build -t orionos-builder:latest iso/docker/
|
||||||
|
- name: Smoke test builder
|
||||||
|
run: docker run --rm orionos-builder:latest echo "Builder image OK"
|
||||||
@@ -7,6 +7,8 @@ orionpkg/orionpkg
|
|||||||
skykit/libskykit.a
|
skykit/libskykit.a
|
||||||
skykit/skykit.o
|
skykit/skykit.o
|
||||||
rigel/rigel
|
rigel/rigel
|
||||||
|
quill/quill
|
||||||
|
lens/lens
|
||||||
installer/orioninstall
|
installer/orioninstall
|
||||||
installer/orioninstall-gui
|
installer/orioninstall-gui
|
||||||
launcher/orionlauncher
|
launcher/orionlauncher
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# make clean — remove build artifacts
|
# make clean — remove build artifacts
|
||||||
# make iso — build the bootable ISO (requires Arch Linux + archiso)
|
# make iso — build the bootable ISO (requires Arch Linux + archiso)
|
||||||
|
|
||||||
.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel clean install iso
|
.PHONY: all wm deskbar launcher installer nebula orionpkg skykit rigel quill lens clean install iso
|
||||||
|
|
||||||
all: wm deskbar launcher installer nebula orionpkg skykit rigel
|
all: wm deskbar launcher installer nebula orionpkg skykit rigel quill lens
|
||||||
|
|
||||||
wm:
|
wm:
|
||||||
$(MAKE) -C wm
|
$(MAKE) -C wm
|
||||||
@@ -33,6 +33,12 @@ skykit:
|
|||||||
rigel:
|
rigel:
|
||||||
$(MAKE) -C rigel
|
$(MAKE) -C rigel
|
||||||
|
|
||||||
|
quill:
|
||||||
|
$(MAKE) -C quill
|
||||||
|
|
||||||
|
lens:
|
||||||
|
$(MAKE) -C lens
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C wm clean
|
$(MAKE) -C wm clean
|
||||||
$(MAKE) -C deskbar clean
|
$(MAKE) -C deskbar clean
|
||||||
@@ -42,6 +48,8 @@ clean:
|
|||||||
$(MAKE) -C orionpkg clean
|
$(MAKE) -C orionpkg clean
|
||||||
$(MAKE) -C skykit clean
|
$(MAKE) -C skykit clean
|
||||||
$(MAKE) -C rigel clean
|
$(MAKE) -C rigel clean
|
||||||
|
$(MAKE) -C quill clean
|
||||||
|
$(MAKE) -C lens clean
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
# binaries
|
# binaries
|
||||||
@@ -52,6 +60,8 @@ install: all
|
|||||||
install -Dm755 nebula/nebula /usr/local/bin/nebula
|
install -Dm755 nebula/nebula /usr/local/bin/nebula
|
||||||
install -Dm755 orionpkg/orionpkg /usr/local/bin/orionpkg
|
install -Dm755 orionpkg/orionpkg /usr/local/bin/orionpkg
|
||||||
install -Dm755 rigel/rigel /usr/local/bin/rigel
|
install -Dm755 rigel/rigel /usr/local/bin/rigel
|
||||||
|
install -Dm755 quill/quill /usr/local/bin/quill
|
||||||
|
install -Dm755 lens/lens /usr/local/bin/lens
|
||||||
# GTK theme
|
# GTK theme
|
||||||
mkdir -p /usr/share/themes
|
mkdir -p /usr/share/themes
|
||||||
cp -r themes/OrionOS /usr/share/themes/
|
cp -r themes/OrionOS /usr/share/themes/
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ ttf-liberation
|
|||||||
noto-fonts
|
noto-fonts
|
||||||
libxft
|
libxft
|
||||||
fontconfig
|
fontconfig
|
||||||
|
imlib2
|
||||||
xf86-video-intel
|
xf86-video-intel
|
||||||
xf86-video-vesa
|
xf86-video-vesa
|
||||||
mesa
|
mesa
|
||||||
@@ -242,6 +243,14 @@ fi
|
|||||||
if [[ -f "$SRC/rigel/rigel" ]]; then
|
if [[ -f "$SRC/rigel/rigel" ]]; then
|
||||||
install -m755 "$SRC/rigel/rigel" "$AIROOTFS/usr/local/bin/rigel"
|
install -m755 "$SRC/rigel/rigel" "$AIROOTFS/usr/local/bin/rigel"
|
||||||
fi
|
fi
|
||||||
|
# Quill text editor (compiled binary)
|
||||||
|
if [[ -f "$SRC/quill/quill" ]]; then
|
||||||
|
install -m755 "$SRC/quill/quill" "$AIROOTFS/usr/local/bin/quill"
|
||||||
|
fi
|
||||||
|
# Lens image viewer (compiled binary)
|
||||||
|
if [[ -f "$SRC/lens/lens" ]]; then
|
||||||
|
install -m755 "$SRC/lens/lens" "$AIROOTFS/usr/local/bin/lens"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 5. GTK theme ──────────────────────────────────────────────────────
|
# ── 5. GTK theme ──────────────────────────────────────────────────────
|
||||||
echo ">>> Installing GTK theme..."
|
echo ">>> Installing GTK theme..."
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -O2 -Wall -Wextra \
|
||||||
|
$(shell pkg-config --cflags fontconfig xft x11 imlib2)
|
||||||
|
LIBS = $(shell pkg-config --libs fontconfig xft x11 imlib2) -lm
|
||||||
|
|
||||||
|
lens: lens.c
|
||||||
|
$(CC) $(CFLAGS) -o lens lens.c $(LIBS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f lens
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
+503
@@ -0,0 +1,503 @@
|
|||||||
|
/* lens.c — OrionOS Image Viewer
|
||||||
|
* Named for the optical lens. Native C/Xlib + Imlib2.
|
||||||
|
* No GTK. Uses imlib2 for image decoding (JPEG, PNG, GIF, BMP, TIFF, WEBP).
|
||||||
|
*
|
||||||
|
* Keybindings:
|
||||||
|
* n / Right Next image in directory
|
||||||
|
* p / Left Previous image
|
||||||
|
* +/- Zoom in / out
|
||||||
|
* 0 Actual size (100%)
|
||||||
|
* f Fit to window (default)
|
||||||
|
* r Rotate 90° clockwise
|
||||||
|
* Esc / q Quit
|
||||||
|
* F11 Fullscreen toggle
|
||||||
|
*
|
||||||
|
* Mouse:
|
||||||
|
* Scroll Zoom in/out
|
||||||
|
* Drag Pan when zoomed in
|
||||||
|
*/
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/Xutil.h>
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
#include <Imlib2.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
|
||||||
|
#define WIN_W 900
|
||||||
|
#define WIN_H 640
|
||||||
|
#define STAT_H 28
|
||||||
|
|
||||||
|
/* palette */
|
||||||
|
#define C_BG 0x090C12UL
|
||||||
|
#define C_STAT 0x0D1117UL
|
||||||
|
#define C_BORDER 0x1A2035UL
|
||||||
|
#define C_TEXT 0xCDD6F4UL
|
||||||
|
#define C_DIM 0x4A5880UL
|
||||||
|
|
||||||
|
/* ── App state ────────────────────────────────────────────── */
|
||||||
|
typedef struct {
|
||||||
|
Display *dpy;
|
||||||
|
int scr;
|
||||||
|
Window win;
|
||||||
|
GC gc;
|
||||||
|
Visual *vis;
|
||||||
|
Colormap cmap;
|
||||||
|
int depth;
|
||||||
|
int win_w, win_h;
|
||||||
|
Atom wm_del;
|
||||||
|
Atom net_wm_state;
|
||||||
|
Atom net_wm_fullscreen;
|
||||||
|
|
||||||
|
/* image */
|
||||||
|
Imlib_Image img;
|
||||||
|
int img_w, img_h;
|
||||||
|
int rotation; /* 0,90,180,270 */
|
||||||
|
|
||||||
|
/* file list */
|
||||||
|
char **files;
|
||||||
|
int nfiles;
|
||||||
|
int cur_file;
|
||||||
|
|
||||||
|
/* view */
|
||||||
|
double zoom;
|
||||||
|
int fit; /* 1 = fit to window */
|
||||||
|
int pan_x, pan_y;
|
||||||
|
int drag;
|
||||||
|
int drag_sx, drag_sy;
|
||||||
|
int drag_px, drag_py;
|
||||||
|
|
||||||
|
/* Xft for status text */
|
||||||
|
XftFont *font;
|
||||||
|
|
||||||
|
int fullscreen;
|
||||||
|
int running;
|
||||||
|
int dirty;
|
||||||
|
} Lens;
|
||||||
|
|
||||||
|
static Lens L;
|
||||||
|
|
||||||
|
/* ── Helpers ──────────────────────────────────────────────── */
|
||||||
|
static unsigned long px_alloc(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(L.dpy, L.cmap, &c);
|
||||||
|
return c.pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill(int x, int y, int w, int h, unsigned long rgb) {
|
||||||
|
XSetForeground(L.dpy, L.gc, px_alloc(rgb));
|
||||||
|
XFillRectangle(L.dpy, L.win, L.gc, x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hline(int x, int y, int w, unsigned long rgb) {
|
||||||
|
XSetForeground(L.dpy, L.gc, px_alloc(rgb));
|
||||||
|
XDrawLine(L.dpy, L.win, L.gc, x, y, x + w - 1, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int baseline(XftFont *f, int y, int h) {
|
||||||
|
return y + (h + f->ascent - f->descent) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void xft_str(XftFont *f, unsigned long rgb, int x, int y,
|
||||||
|
const char *s) {
|
||||||
|
XftDraw *xd = XftDrawCreate(L.dpy, L.win, L.vis, L.cmap);
|
||||||
|
XRenderColor rc = {
|
||||||
|
.red = ((rgb >> 16) & 0xFF) * 257,
|
||||||
|
.green = ((rgb >> 8) & 0xFF) * 257,
|
||||||
|
.blue = ( rgb & 0xFF) * 257,
|
||||||
|
.alpha = 0xFFFF
|
||||||
|
};
|
||||||
|
XftColor col;
|
||||||
|
XftColorAllocValue(L.dpy, L.vis, L.cmap, &rc, &col);
|
||||||
|
XftDrawStringUtf8(xd, &col, f, x, y,
|
||||||
|
(const FcChar8 *)s, (int)strlen(s));
|
||||||
|
XftColorFree(L.dpy, L.vis, L.cmap, &col);
|
||||||
|
XftDrawDestroy(xd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int img_extw(void) { return (L.rotation % 180) ? L.img_h : L.img_w; }
|
||||||
|
static int img_exth(void) { return (L.rotation % 180) ? L.img_w : L.img_h; }
|
||||||
|
|
||||||
|
/* ── Image loading ────────────────────────────────────────── */
|
||||||
|
static const char *image_exts[] = {
|
||||||
|
".jpg",".jpeg",".png",".gif",".bmp",".tiff",".tif",
|
||||||
|
".webp",".pnm",".pgm",".ppm",".xpm",".ico",".svg", NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static int is_image(const char *name) {
|
||||||
|
const char *dot = strrchr(name, '.');
|
||||||
|
if (!dot) return 0;
|
||||||
|
for (int i = 0; image_exts[i]; i++) {
|
||||||
|
if (strcasecmp(dot, image_exts[i]) == 0) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_str(const void *a, const void *b) {
|
||||||
|
return strcmp(*(const char **)a, *(const char **)b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void load_dir_list(const char *dir_path) {
|
||||||
|
DIR *d = opendir(dir_path);
|
||||||
|
if (!d) return;
|
||||||
|
|
||||||
|
/* free old list */
|
||||||
|
for (int i = 0; i < L.nfiles; i++) free(L.files[i]);
|
||||||
|
free(L.files);
|
||||||
|
L.files = NULL;
|
||||||
|
L.nfiles = 0;
|
||||||
|
|
||||||
|
int cap = 256;
|
||||||
|
L.files = malloc(cap * sizeof(char *));
|
||||||
|
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d))) {
|
||||||
|
if (!is_image(de->d_name)) continue;
|
||||||
|
if (L.nfiles >= cap) {
|
||||||
|
cap *= 2;
|
||||||
|
L.files = realloc(L.files, cap * sizeof(char *));
|
||||||
|
}
|
||||||
|
char full[4096];
|
||||||
|
snprintf(full, sizeof(full), "%s/%s", dir_path, de->d_name);
|
||||||
|
L.files[L.nfiles++] = strdup(full);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
qsort(L.files, L.nfiles, sizeof(char *), cmp_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void load_image(const char *path) {
|
||||||
|
if (L.img) { imlib_context_set_image(L.img); imlib_free_image(); L.img = NULL; }
|
||||||
|
L.img = imlib_load_image(path);
|
||||||
|
if (!L.img) return;
|
||||||
|
imlib_context_set_image(L.img);
|
||||||
|
L.img_w = imlib_image_get_width();
|
||||||
|
L.img_h = imlib_image_get_height();
|
||||||
|
L.rotation = 0;
|
||||||
|
L.fit = 1;
|
||||||
|
L.pan_x = 0;
|
||||||
|
L.pan_y = 0;
|
||||||
|
|
||||||
|
/* compute fit zoom */
|
||||||
|
int view_h = L.win_h - STAT_H;
|
||||||
|
double zw = (double)L.win_w / L.img_w;
|
||||||
|
double zh = (double)view_h / L.img_h;
|
||||||
|
L.zoom = (zw < zh) ? zw : zh;
|
||||||
|
if (L.zoom > 1.0) L.zoom = 1.0; /* never upscale for fit */
|
||||||
|
|
||||||
|
/* set window title */
|
||||||
|
const char *name = strrchr(path, '/');
|
||||||
|
name = name ? name + 1 : path;
|
||||||
|
char title[4096];
|
||||||
|
snprintf(title, sizeof(title), "Lens — %s", name);
|
||||||
|
XStoreName(L.dpy, L.win, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nav_delta(int delta) {
|
||||||
|
if (L.nfiles == 0) return;
|
||||||
|
L.cur_file = (L.cur_file + delta + L.nfiles) % L.nfiles;
|
||||||
|
load_image(L.files[L.cur_file]);
|
||||||
|
L.dirty = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Rendering ────────────────────────────────────────────── */
|
||||||
|
static void render(void) {
|
||||||
|
int view_h = L.win_h - STAT_H;
|
||||||
|
|
||||||
|
/* background */
|
||||||
|
fill(0, 0, L.win_w, view_h, C_BG);
|
||||||
|
|
||||||
|
if (!L.img) {
|
||||||
|
fill(0, view_h, L.win_w, STAT_H, C_STAT);
|
||||||
|
hline(0, view_h, L.win_w, C_BORDER);
|
||||||
|
xft_str(L.font, C_DIM,
|
||||||
|
L.win_w / 2 - 60, baseline(L.font, view_h, STAT_H),
|
||||||
|
"No image loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imlib_context_set_image(L.img);
|
||||||
|
imlib_context_set_display(L.dpy);
|
||||||
|
imlib_context_set_visual(L.vis);
|
||||||
|
imlib_context_set_colormap(L.cmap);
|
||||||
|
imlib_context_set_drawable(L.win);
|
||||||
|
|
||||||
|
/* compute displayed size */
|
||||||
|
int ew = img_extw(), eh = img_exth();
|
||||||
|
int dw, dh;
|
||||||
|
|
||||||
|
if (L.fit) {
|
||||||
|
double zw = (double)L.win_w / ew;
|
||||||
|
double zh = (double)view_h / eh;
|
||||||
|
L.zoom = (zw < zh) ? zw : zh;
|
||||||
|
if (L.zoom > 8.0) L.zoom = 8.0;
|
||||||
|
dw = (int)(ew * L.zoom);
|
||||||
|
dh = (int)(eh * L.zoom);
|
||||||
|
L.pan_x = (L.win_w - dw) / 2;
|
||||||
|
L.pan_y = (view_h - dh) / 2;
|
||||||
|
} else {
|
||||||
|
dw = (int)(ew * L.zoom);
|
||||||
|
dh = (int)(eh * L.zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* clamp pan */
|
||||||
|
if (!L.fit) {
|
||||||
|
int min_px = L.win_w - dw;
|
||||||
|
int min_py = view_h - dh;
|
||||||
|
if (L.pan_x > 0) L.pan_x = 0;
|
||||||
|
if (L.pan_y > 0) L.pan_y = 0;
|
||||||
|
if (L.pan_x < min_px) L.pan_x = min_px;
|
||||||
|
if (L.pan_y < min_py) L.pan_y = min_py;
|
||||||
|
/* center if smaller than viewport */
|
||||||
|
if (dw < L.win_w) L.pan_x = (L.win_w - dw) / 2;
|
||||||
|
if (dh < view_h) L.pan_y = (view_h - dh) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* create rotated + scaled image for display */
|
||||||
|
Imlib_Image rotated = L.img;
|
||||||
|
int free_rotated = 0;
|
||||||
|
|
||||||
|
if (L.rotation != 0) {
|
||||||
|
imlib_context_set_image(L.img);
|
||||||
|
rotated = imlib_create_rotated_image(L.rotation * M_PI / 180.0);
|
||||||
|
if (rotated) free_rotated = 1;
|
||||||
|
else rotated = L.img;
|
||||||
|
}
|
||||||
|
|
||||||
|
imlib_context_set_image(rotated);
|
||||||
|
int rw = imlib_image_get_width();
|
||||||
|
int rh = imlib_image_get_height();
|
||||||
|
Imlib_Image scaled = imlib_create_cropped_scaled_image(0, 0, rw, rh, dw, dh);
|
||||||
|
if (free_rotated) { imlib_context_set_image(rotated); imlib_free_image(); }
|
||||||
|
|
||||||
|
if (scaled) {
|
||||||
|
imlib_context_set_image(scaled);
|
||||||
|
imlib_context_set_drawable(L.win);
|
||||||
|
imlib_render_image_on_drawable(L.pan_x, L.pan_y);
|
||||||
|
imlib_free_image();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status bar */
|
||||||
|
fill(0, view_h, L.win_w, STAT_H, C_STAT);
|
||||||
|
hline(0, view_h, L.win_w, C_BORDER);
|
||||||
|
|
||||||
|
char stat[512];
|
||||||
|
const char *name = L.nfiles > 0 ? strrchr(L.files[L.cur_file], '/') : NULL;
|
||||||
|
name = name ? name + 1 : "?";
|
||||||
|
snprintf(stat, sizeof(stat),
|
||||||
|
"%s %d × %d %d%% [%d / %d]",
|
||||||
|
name, ew, eh, (int)(L.zoom * 100),
|
||||||
|
L.nfiles > 0 ? L.cur_file + 1 : 0, L.nfiles);
|
||||||
|
xft_str(L.font, C_TEXT, 12,
|
||||||
|
baseline(L.font, view_h, STAT_H), stat);
|
||||||
|
|
||||||
|
const char *hint = "n/p next/prev +/- zoom f fit r rotate q quit";
|
||||||
|
XGlyphInfo ext;
|
||||||
|
XftTextExtentsUtf8(L.dpy, L.font,
|
||||||
|
(const FcChar8 *)hint, (int)strlen(hint), &ext);
|
||||||
|
xft_str(L.font, C_DIM, L.win_w - ext.xOff - 10,
|
||||||
|
baseline(L.font, view_h, STAT_H), hint);
|
||||||
|
|
||||||
|
XFlush(L.dpy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Key handling ─────────────────────────────────────────── */
|
||||||
|
static void handle_key(XKeyEvent *ev) {
|
||||||
|
char text[16] = {0};
|
||||||
|
KeySym ksym = 0;
|
||||||
|
XLookupString(ev, text, sizeof(text)-1, &ksym, NULL);
|
||||||
|
|
||||||
|
int view_h = L.win_h - STAT_H;
|
||||||
|
|
||||||
|
switch (ksym) {
|
||||||
|
case XK_q: case XK_Escape: L.running = 0; return;
|
||||||
|
|
||||||
|
case XK_n: case XK_Right: nav_delta(+1); return;
|
||||||
|
case XK_p: case XK_Left: nav_delta(-1); return;
|
||||||
|
|
||||||
|
case XK_plus: case XK_equal:
|
||||||
|
L.fit = 0;
|
||||||
|
L.zoom *= 1.25;
|
||||||
|
if (L.zoom > 32.0) L.zoom = 32.0;
|
||||||
|
L.dirty = 1; return;
|
||||||
|
case XK_minus:
|
||||||
|
L.fit = 0;
|
||||||
|
L.zoom /= 1.25;
|
||||||
|
if (L.zoom < 0.02) L.zoom = 0.02;
|
||||||
|
L.dirty = 1; return;
|
||||||
|
case XK_0:
|
||||||
|
L.fit = 0;
|
||||||
|
L.zoom = 1.0;
|
||||||
|
L.pan_x = (L.win_w - img_extw()) / 2;
|
||||||
|
L.pan_y = (view_h - img_exth()) / 2;
|
||||||
|
L.dirty = 1; return;
|
||||||
|
case XK_f:
|
||||||
|
L.fit = !L.fit;
|
||||||
|
L.dirty = 1; return;
|
||||||
|
case XK_r:
|
||||||
|
L.rotation = (L.rotation + 90) % 360;
|
||||||
|
L.dirty = 1; return;
|
||||||
|
case XK_F11:
|
||||||
|
L.fullscreen = !L.fullscreen;
|
||||||
|
{
|
||||||
|
XEvent xev = {0};
|
||||||
|
xev.type = ClientMessage;
|
||||||
|
xev.xclient.window = L.win;
|
||||||
|
xev.xclient.message_type = L.net_wm_state;
|
||||||
|
xev.xclient.format = 32;
|
||||||
|
xev.xclient.data.l[0] = L.fullscreen ? 1 : 0;
|
||||||
|
xev.xclient.data.l[1] = (long)L.net_wm_fullscreen;
|
||||||
|
XSendEvent(L.dpy, DefaultRootWindow(L.dpy), False,
|
||||||
|
SubstructureNotifyMask | SubstructureRedirectMask, &xev);
|
||||||
|
}
|
||||||
|
L.dirty = 1; return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ─────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "usage: lens <image-file>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
L.dpy = XOpenDisplay(NULL);
|
||||||
|
if (!L.dpy) { fputs("lens: cannot open display\n", stderr); return 1; }
|
||||||
|
L.scr = DefaultScreen(L.dpy);
|
||||||
|
L.vis = DefaultVisual(L.dpy, L.scr);
|
||||||
|
L.cmap = DefaultColormap(L.dpy, L.scr);
|
||||||
|
L.depth = DefaultDepth(L.dpy, L.scr);
|
||||||
|
L.win_w = WIN_W;
|
||||||
|
L.win_h = WIN_H;
|
||||||
|
|
||||||
|
L.font = XftFontOpenName(L.dpy, L.scr, "Sans:size=9");
|
||||||
|
if (!L.font) L.font = XftFontOpenName(L.dpy, L.scr, "fixed");
|
||||||
|
|
||||||
|
XSetWindowAttributes swa = {
|
||||||
|
.background_pixel = px_alloc(C_BG),
|
||||||
|
.event_mask = ExposureMask | KeyPressMask
|
||||||
|
| ButtonPressMask | ButtonReleaseMask
|
||||||
|
| PointerMotionMask | StructureNotifyMask
|
||||||
|
};
|
||||||
|
L.win = XCreateWindow(L.dpy, RootWindow(L.dpy, L.scr),
|
||||||
|
80, 60, WIN_W, WIN_H, 0,
|
||||||
|
L.depth, InputOutput, L.vis,
|
||||||
|
CWBackPixel | CWEventMask, &swa);
|
||||||
|
|
||||||
|
L.gc = XCreateGC(L.dpy, L.win, 0, NULL);
|
||||||
|
|
||||||
|
L.wm_del = XInternAtom(L.dpy, "WM_DELETE_WINDOW", False);
|
||||||
|
L.net_wm_state = XInternAtom(L.dpy, "_NET_WM_STATE", False);
|
||||||
|
L.net_wm_fullscreen = XInternAtom(L.dpy, "_NET_WM_STATE_FULLSCREEN", False);
|
||||||
|
XSetWMProtocols(L.dpy, L.win, &L.wm_del, 1);
|
||||||
|
|
||||||
|
XClassHint *ch = XAllocClassHint();
|
||||||
|
ch->res_name = "lens";
|
||||||
|
ch->res_class = "Lens";
|
||||||
|
XSetClassHint(L.dpy, L.win, ch);
|
||||||
|
XFree(ch);
|
||||||
|
|
||||||
|
/* imlib2 setup */
|
||||||
|
imlib_context_set_display(L.dpy);
|
||||||
|
imlib_context_set_visual(L.vis);
|
||||||
|
imlib_context_set_colormap(L.cmap);
|
||||||
|
imlib_context_set_drawable(L.win);
|
||||||
|
imlib_set_cache_size(32 * 1024 * 1024);
|
||||||
|
|
||||||
|
/* build sibling file list from directory of argv[1] */
|
||||||
|
char abs_path[4096];
|
||||||
|
if (!realpath(argv[1], abs_path)) {
|
||||||
|
snprintf(abs_path, sizeof(abs_path), "%s", argv[1]);
|
||||||
|
}
|
||||||
|
char dir_buf[4096];
|
||||||
|
snprintf(dir_buf, sizeof(dir_buf), "%s", abs_path);
|
||||||
|
char *dir = dirname(dir_buf);
|
||||||
|
load_dir_list(dir);
|
||||||
|
|
||||||
|
/* find current file in list */
|
||||||
|
L.cur_file = 0;
|
||||||
|
for (int i = 0; i < L.nfiles; i++) {
|
||||||
|
if (strcmp(L.files[i], abs_path) == 0) { L.cur_file = i; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
load_image(abs_path);
|
||||||
|
L.running = 1;
|
||||||
|
L.dirty = 1;
|
||||||
|
|
||||||
|
XMapWindow(L.dpy, L.win);
|
||||||
|
XFlush(L.dpy);
|
||||||
|
|
||||||
|
while (L.running) {
|
||||||
|
if (L.dirty && !XPending(L.dpy)) {
|
||||||
|
render();
|
||||||
|
L.dirty = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
XEvent ev;
|
||||||
|
XNextEvent(L.dpy, &ev);
|
||||||
|
switch (ev.type) {
|
||||||
|
case Expose:
|
||||||
|
if (ev.xexpose.count == 0) L.dirty = 1;
|
||||||
|
break;
|
||||||
|
case ConfigureNotify:
|
||||||
|
L.win_w = ev.xconfigure.width;
|
||||||
|
L.win_h = ev.xconfigure.height;
|
||||||
|
L.dirty = 1;
|
||||||
|
break;
|
||||||
|
case KeyPress:
|
||||||
|
handle_key(&ev.xkey);
|
||||||
|
break;
|
||||||
|
case ButtonPress:
|
||||||
|
if (ev.xbutton.button == Button4) {
|
||||||
|
/* scroll up = zoom in */
|
||||||
|
L.fit = 0;
|
||||||
|
L.zoom *= 1.15;
|
||||||
|
if (L.zoom > 32.0) L.zoom = 32.0;
|
||||||
|
L.dirty = 1;
|
||||||
|
} else if (ev.xbutton.button == Button5) {
|
||||||
|
L.fit = 0;
|
||||||
|
L.zoom /= 1.15;
|
||||||
|
if (L.zoom < 0.02) L.zoom = 0.02;
|
||||||
|
L.dirty = 1;
|
||||||
|
} else if (ev.xbutton.button == Button1) {
|
||||||
|
L.drag = 1;
|
||||||
|
L.drag_sx = ev.xbutton.x;
|
||||||
|
L.drag_sy = ev.xbutton.y;
|
||||||
|
L.drag_px = L.pan_x;
|
||||||
|
L.drag_py = L.pan_y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ButtonRelease:
|
||||||
|
if (ev.xbutton.button == Button1) L.drag = 0;
|
||||||
|
break;
|
||||||
|
case MotionNotify:
|
||||||
|
if (L.drag && !L.fit) {
|
||||||
|
L.pan_x = L.drag_px + ev.xmotion.x - L.drag_sx;
|
||||||
|
L.pan_y = L.drag_py + ev.xmotion.y - L.drag_sy;
|
||||||
|
L.dirty = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ClientMessage:
|
||||||
|
if ((Atom)ev.xclient.data.l[0] == L.wm_del) L.running = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (L.img) { imlib_context_set_image(L.img); imlib_free_image(); }
|
||||||
|
for (int i = 0; i < L.nfiles; i++) free(L.files[i]);
|
||||||
|
free(L.files);
|
||||||
|
XftFontClose(L.dpy, L.font);
|
||||||
|
XFreeGC(L.dpy, L.gc);
|
||||||
|
XDestroyWindow(L.dpy, L.win);
|
||||||
|
XCloseDisplay(L.dpy);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -O2 -Wall -Wextra \
|
||||||
|
$(shell pkg-config --cflags fontconfig xft x11)
|
||||||
|
LIBS = $(shell pkg-config --libs fontconfig xft x11)
|
||||||
|
|
||||||
|
quill: quill.c
|
||||||
|
$(CC) $(CFLAGS) -o quill quill.c $(LIBS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f quill
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
+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;
|
||||||
|
}
|
||||||
+527
-89
@@ -10,7 +10,7 @@
|
|||||||
* [ | IMG photo.jpg 2 MB Jul 10 ]
|
* [ | IMG photo.jpg 2 MB Jul 10 ]
|
||||||
* [ Home | ]
|
* [ Home | ]
|
||||||
* [ Docs |────────────────────────────────────── ]
|
* [ Docs |────────────────────────────────────── ]
|
||||||
* [ Down | 47 items ]
|
* [ Down | 47 items [search bar when active] ]
|
||||||
*/
|
*/
|
||||||
#include <X11/Xlib.h>
|
#include <X11/Xlib.h>
|
||||||
#include <X11/Xutil.h>
|
#include <X11/Xutil.h>
|
||||||
@@ -64,6 +64,12 @@
|
|||||||
#define PATH_W (WIN_W - PATH_X - 8)
|
#define PATH_W (WIN_W - PATH_X - 8)
|
||||||
#define PATH_H 26
|
#define PATH_H 26
|
||||||
|
|
||||||
|
/* context menu */
|
||||||
|
#define CTX_W 200
|
||||||
|
#define CTX_IH 24
|
||||||
|
#define CTX_SH 8
|
||||||
|
#define CTX_PAD 4
|
||||||
|
|
||||||
/* ── File types ───────────────────────────────────────────────────── */
|
/* ── File types ───────────────────────────────────────────────────── */
|
||||||
typedef enum {
|
typedef enum {
|
||||||
FT_DIR, FT_IMAGE, FT_AUDIO, FT_VIDEO, FT_ARCHIVE,
|
FT_DIR, FT_IMAGE, FT_AUDIO, FT_VIDEO, FT_ARCHIVE,
|
||||||
@@ -127,6 +133,21 @@ static const struct { const char *name; const char *path; } BKMARKS[] = {
|
|||||||
};
|
};
|
||||||
#define NBKMARKS ((int)(sizeof(BKMARKS)/sizeof(BKMARKS[0])))
|
#define NBKMARKS ((int)(sizeof(BKMARKS)/sizeof(BKMARKS[0])))
|
||||||
|
|
||||||
|
/* ── Context menu items ───────────────────────────────────────────── */
|
||||||
|
typedef struct { const char *lbl; int sep; } CtxItem;
|
||||||
|
static const CtxItem CTX[] = {
|
||||||
|
{ "Open", 0 },
|
||||||
|
{ "Open in Quill", 0 },
|
||||||
|
{ "Open in Terminal", 0 },
|
||||||
|
{ NULL, 1 },
|
||||||
|
{ "Copy Path", 0 },
|
||||||
|
{ "Rename", 0 },
|
||||||
|
{ "Move to Trash", 0 },
|
||||||
|
{ NULL, 1 },
|
||||||
|
{ "Properties", 0 },
|
||||||
|
};
|
||||||
|
#define CTX_N ((int)(sizeof(CTX)/sizeof(CTX[0])))
|
||||||
|
|
||||||
/* ── App struct ───────────────────────────────────────────────────── */
|
/* ── App struct ───────────────────────────────────────────────────── */
|
||||||
#define HIST_MAX 64
|
#define HIST_MAX 64
|
||||||
|
|
||||||
@@ -147,21 +168,41 @@ typedef struct {
|
|||||||
FileEntry files[FMAX];
|
FileEntry files[FMAX];
|
||||||
int nfiles;
|
int nfiles;
|
||||||
int scroll;
|
int scroll;
|
||||||
int hover_row;
|
int hover_row; /* files[] index, -1 if none */
|
||||||
int sel_row;
|
int sel_row; /* files[] index, -1 if none */
|
||||||
int show_hidden;
|
int show_hidden;
|
||||||
int sort_col; /* 0=name 1=size 2=date */
|
int sort_col; /* 0=name 1=size 2=date */
|
||||||
int sort_dir; /* +1=asc -1=desc */
|
int sort_dir; /* +1=asc -1=desc */
|
||||||
|
|
||||||
int sidebar_hover;
|
int sidebar_hover;
|
||||||
int hover_btn; /* 1=back 2=fwd 3=up */
|
int hover_btn; /* 1=back 2=fwd 3=up */
|
||||||
|
|
||||||
char path_buf[4096];
|
char path_buf[4096];
|
||||||
int path_editing;
|
int path_editing;
|
||||||
int path_cursor;
|
int path_cursor;
|
||||||
|
|
||||||
Time last_click_time;
|
Time last_click_time;
|
||||||
int last_click_row;
|
int last_click_row; /* files[] index */
|
||||||
|
|
||||||
|
/* ── search / filter ──────────────────────────── */
|
||||||
|
char search_str[256];
|
||||||
|
int search_len;
|
||||||
|
int search_on;
|
||||||
|
int filtered[FMAX]; /* files[] indices */
|
||||||
|
int nfiltered;
|
||||||
|
|
||||||
|
/* ── right-click context menu ─────────────────── */
|
||||||
|
Window ctx_win;
|
||||||
|
int ctx_vis;
|
||||||
|
int ctx_x, ctx_y;
|
||||||
|
int ctx_file_idx; /* files[] index */
|
||||||
|
int ctx_hover; /* item index, -1 if none */
|
||||||
|
|
||||||
|
/* ── rename overlay ───────────────────────────── */
|
||||||
|
char rename_buf[512];
|
||||||
|
int rename_len;
|
||||||
|
int rename_on;
|
||||||
|
int rename_idx; /* files[] index */
|
||||||
} Rigel;
|
} Rigel;
|
||||||
|
|
||||||
/* ── Path helpers ─────────────────────────────────────────────────── */
|
/* ── Path helpers ─────────────────────────────────────────────────── */
|
||||||
@@ -198,7 +239,6 @@ static int g_sort_col, g_sort_dir;
|
|||||||
static int cmp_files(const void *a, const void *b) {
|
static int cmp_files(const void *a, const void *b) {
|
||||||
const FileEntry *fa = (const FileEntry *)a;
|
const FileEntry *fa = (const FileEntry *)a;
|
||||||
const FileEntry *fb = (const FileEntry *)b;
|
const FileEntry *fb = (const FileEntry *)b;
|
||||||
/* dirs always first */
|
|
||||||
int da = (fa->type == FT_DIR), db = (fb->type == FT_DIR);
|
int da = (fa->type == FT_DIR), db = (fb->type == FT_DIR);
|
||||||
if (da != db) return da ? -1 : 1;
|
if (da != db) return da ? -1 : 1;
|
||||||
int r = 0;
|
int r = 0;
|
||||||
@@ -215,6 +255,10 @@ static void list_dir(Rigel *r) {
|
|||||||
r->nfiles = 0;
|
r->nfiles = 0;
|
||||||
r->scroll = 0;
|
r->scroll = 0;
|
||||||
r->sel_row = -1;
|
r->sel_row = -1;
|
||||||
|
r->hover_row = -1;
|
||||||
|
r->search_on = 0;
|
||||||
|
r->search_len = 0;
|
||||||
|
r->search_str[0] = '\0';
|
||||||
|
|
||||||
DIR *dir = opendir(r->cwd);
|
DIR *dir = opendir(r->cwd);
|
||||||
if (!dir) return;
|
if (!dir) return;
|
||||||
@@ -245,6 +289,37 @@ static void list_dir(Rigel *r) {
|
|||||||
qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files);
|
qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Search filter ───────────────────────────────────────────────── */
|
||||||
|
static void search_filter(Rigel *r) {
|
||||||
|
r->nfiltered = 0;
|
||||||
|
r->scroll = 0;
|
||||||
|
r->sel_row = -1;
|
||||||
|
if (!r->search_str[0]) return;
|
||||||
|
for (int i = 0; i < r->nfiles; i++) {
|
||||||
|
if (strcasestr(r->files[i].name, r->search_str))
|
||||||
|
r->filtered[r->nfiltered++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_nvis(Rigel *r) { return r->search_on ? r->nfiltered : r->nfiles; }
|
||||||
|
static FileEntry *get_file(Rigel *r, int vis_idx) {
|
||||||
|
if (r->search_on) {
|
||||||
|
if (vis_idx < 0 || vis_idx >= r->nfiltered) return NULL;
|
||||||
|
return &r->files[r->filtered[vis_idx]];
|
||||||
|
}
|
||||||
|
if (vis_idx < 0 || vis_idx >= r->nfiles) return NULL;
|
||||||
|
return &r->files[vis_idx];
|
||||||
|
}
|
||||||
|
/* translate visual row (vis_idx) → files[] index */
|
||||||
|
static int get_fidx(Rigel *r, int vis_idx) {
|
||||||
|
if (r->search_on) {
|
||||||
|
if (vis_idx < 0 || vis_idx >= r->nfiltered) return -1;
|
||||||
|
return r->filtered[vis_idx];
|
||||||
|
}
|
||||||
|
if (vis_idx < 0 || vis_idx >= r->nfiles) return -1;
|
||||||
|
return vis_idx;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Navigation ──────────────────────────────────────────────────── */
|
/* ── Navigation ──────────────────────────────────────────────────── */
|
||||||
static void nav_cd(Rigel *r, const char *path) {
|
static void nav_cd(Rigel *r, const char *path) {
|
||||||
char real[4096];
|
char real[4096];
|
||||||
@@ -253,7 +328,6 @@ static void nav_cd(Rigel *r, const char *path) {
|
|||||||
} else {
|
} else {
|
||||||
snprintf(r->cwd, sizeof(r->cwd), "%s", path);
|
snprintf(r->cwd, sizeof(r->cwd), "%s", path);
|
||||||
}
|
}
|
||||||
/* push history */
|
|
||||||
if (r->hist_len > 0 && strcmp(r->history[r->hist_pos], r->cwd) == 0)
|
if (r->hist_len > 0 && strcmp(r->history[r->hist_pos], r->cwd) == 0)
|
||||||
goto done;
|
goto done;
|
||||||
r->hist_pos++;
|
r->hist_pos++;
|
||||||
@@ -292,9 +366,9 @@ static void nav_up(Rigel *r) {
|
|||||||
else { *sl = '\0'; nav_cd(r, parent); }
|
else { *sl = '\0'; nav_cd(r, parent); }
|
||||||
}
|
}
|
||||||
|
|
||||||
static void open_entry(Rigel *r, int idx) {
|
static void open_entry(Rigel *r, int fidx) {
|
||||||
if (idx < 0 || idx >= r->nfiles) return;
|
if (fidx < 0 || fidx >= r->nfiles) return;
|
||||||
FileEntry *fe = &r->files[idx];
|
FileEntry *fe = &r->files[fidx];
|
||||||
if (fe->type == FT_DIR) {
|
if (fe->type == FT_DIR) {
|
||||||
char path[4096*2];
|
char path[4096*2];
|
||||||
snprintf(path, sizeof(path), "%s/%s", r->cwd, fe->name);
|
snprintf(path, sizeof(path), "%s/%s", r->cwd, fe->name);
|
||||||
@@ -307,6 +381,179 @@ static void open_entry(Rigel *r, int idx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Context menu ────────────────────────────────────────────────── */
|
||||||
|
static int ctx_item_h(int i) { return CTX[i].sep ? CTX_SH : CTX_IH; }
|
||||||
|
|
||||||
|
static int ctx_total_h(void) {
|
||||||
|
int h = CTX_PAD * 2;
|
||||||
|
for (int i = 0; i < CTX_N; i++) h += ctx_item_h(i);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ctx_item_y(int idx) {
|
||||||
|
int y = CTX_PAD;
|
||||||
|
for (int i = 0; i < idx; i++) y += ctx_item_h(i);
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ctx_item_at(int py) {
|
||||||
|
int y = CTX_PAD;
|
||||||
|
for (int i = 0; i < CTX_N; i++) {
|
||||||
|
int h = ctx_item_h(i);
|
||||||
|
if (!CTX[i].sep && py >= y && py < y + h) return i;
|
||||||
|
y += h;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctx_hide(Rigel *r) {
|
||||||
|
if (!r->ctx_vis) return;
|
||||||
|
XUngrabPointer(r->sk.dpy, CurrentTime);
|
||||||
|
XDestroyWindow(r->sk.dpy, r->ctx_win);
|
||||||
|
r->ctx_vis = 0;
|
||||||
|
r->ctx_win = 0;
|
||||||
|
r->ctx_hover = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctx_draw(Rigel *r) {
|
||||||
|
if (!r->ctx_vis) return;
|
||||||
|
int ch = ctx_total_h();
|
||||||
|
XftDraw *xd = XftDrawCreate(r->sk.dpy, r->ctx_win, r->sk.vis, r->sk.cmap);
|
||||||
|
|
||||||
|
XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x1A2030UL));
|
||||||
|
XFillRectangle(r->sk.dpy, r->ctx_win, r->gc, 0, 0, CTX_W, ch);
|
||||||
|
|
||||||
|
for (int i = 0; i < CTX_N; i++) {
|
||||||
|
int iy = ctx_item_y(i);
|
||||||
|
if (CTX[i].sep) {
|
||||||
|
XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x2A3040UL));
|
||||||
|
XDrawLine(r->sk.dpy, r->ctx_win, r->gc, 8, iy + 4, CTX_W - 8, iy + 4);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (i == r->ctx_hover) {
|
||||||
|
XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x264878UL));
|
||||||
|
XFillRectangle(r->sk.dpy, r->ctx_win, r->gc, 2, iy, CTX_W - 4, CTX_IH);
|
||||||
|
}
|
||||||
|
unsigned long fg = (i == r->ctx_hover) ? 0xF0F4FFUL : 0xC8D4E8UL;
|
||||||
|
XftColor col;
|
||||||
|
XRenderColor rc = {
|
||||||
|
.red = ((fg >> 16) & 0xFF) * 257,
|
||||||
|
.green = ((fg >> 8) & 0xFF) * 257,
|
||||||
|
.blue = ( fg & 0xFF) * 257,
|
||||||
|
.alpha = 0xFFFF
|
||||||
|
};
|
||||||
|
XftColorAllocValue(r->sk.dpy, r->sk.vis, r->sk.cmap, &rc, &col);
|
||||||
|
int ty = iy + (CTX_IH + r->sk.font_ui->ascent - r->sk.font_ui->descent) / 2;
|
||||||
|
XftDrawStringUtf8(xd, &col, r->sk.font_ui,
|
||||||
|
CTX_PAD + 4, ty,
|
||||||
|
(const FcChar8 *)CTX[i].lbl, (int)strlen(CTX[i].lbl));
|
||||||
|
XftColorFree(r->sk.dpy, r->sk.vis, r->sk.cmap, &col);
|
||||||
|
}
|
||||||
|
XftDrawDestroy(xd);
|
||||||
|
XFlush(r->sk.dpy);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctx_show(Rigel *r, int x, int y, int fidx) {
|
||||||
|
if (r->ctx_vis) ctx_hide(r);
|
||||||
|
r->ctx_file_idx = fidx;
|
||||||
|
r->ctx_hover = -1;
|
||||||
|
|
||||||
|
int sw = DisplayWidth(r->sk.dpy, r->sk.screen);
|
||||||
|
int sh = DisplayHeight(r->sk.dpy, r->sk.screen);
|
||||||
|
int ch = ctx_total_h();
|
||||||
|
int wx, wy;
|
||||||
|
Window dummy;
|
||||||
|
XTranslateCoordinates(r->sk.dpy, r->win, r->sk.root, x, y, &wx, &wy, &dummy);
|
||||||
|
if (wx + CTX_W > sw) wx = sw - CTX_W - 2;
|
||||||
|
if (wy + ch > sh) wy = sh - ch - 2;
|
||||||
|
|
||||||
|
r->ctx_x = wx; r->ctx_y = wy;
|
||||||
|
|
||||||
|
XSetWindowAttributes swa = {
|
||||||
|
.override_redirect = True,
|
||||||
|
.background_pixel = sk_color(&r->sk, 0x1A2030UL),
|
||||||
|
.event_mask = ExposureMask | ButtonPressMask
|
||||||
|
| ButtonReleaseMask | PointerMotionMask
|
||||||
|
| LeaveWindowMask,
|
||||||
|
.save_under = True,
|
||||||
|
};
|
||||||
|
r->ctx_win = XCreateWindow(r->sk.dpy, r->sk.root,
|
||||||
|
wx, wy, CTX_W, ch, 1,
|
||||||
|
r->sk.depth, InputOutput, r->sk.vis,
|
||||||
|
CWOverrideRedirect | CWBackPixel | CWEventMask | CWSaveUnder,
|
||||||
|
&swa);
|
||||||
|
XSetWindowBorder(r->sk.dpy, r->ctx_win, sk_color(&r->sk, SK_BORDER));
|
||||||
|
XMapRaised(r->sk.dpy, r->ctx_win);
|
||||||
|
XGrabPointer(r->sk.dpy, r->ctx_win, False,
|
||||||
|
ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
|
||||||
|
GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
|
||||||
|
r->ctx_vis = 1;
|
||||||
|
ctx_draw(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctx_exec(Rigel *r, int item) {
|
||||||
|
int fidx = r->ctx_file_idx;
|
||||||
|
if (fidx < 0 || fidx >= r->nfiles) return;
|
||||||
|
FileEntry *fe = &r->files[fidx];
|
||||||
|
char full[4096 + 512];
|
||||||
|
snprintf(full, sizeof(full), "%s/%s", r->cwd, fe->name);
|
||||||
|
char cmd[sizeof(full) + 256];
|
||||||
|
|
||||||
|
switch (item) {
|
||||||
|
case 0: /* Open */
|
||||||
|
open_entry(r, fidx);
|
||||||
|
break;
|
||||||
|
case 1: /* Open in Quill */
|
||||||
|
snprintf(cmd, sizeof(cmd), "quill \"%s\" &", full);
|
||||||
|
system(cmd);
|
||||||
|
break;
|
||||||
|
case 2: /* Open in Terminal */
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"alacritty --working-directory \"%s\" &",
|
||||||
|
fe->type == FT_DIR ? full : r->cwd);
|
||||||
|
system(cmd);
|
||||||
|
break;
|
||||||
|
case 4: /* Copy Path */
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"printf '%%s' \"%s\" | xclip -selection clipboard 2>/dev/null"
|
||||||
|
" || printf '%%s' \"%s\" | xsel --clipboard --input 2>/dev/null",
|
||||||
|
full, full);
|
||||||
|
system(cmd);
|
||||||
|
break;
|
||||||
|
case 5: /* Rename */
|
||||||
|
snprintf(r->rename_buf, sizeof(r->rename_buf), "%s", fe->name);
|
||||||
|
r->rename_len = (int)strlen(r->rename_buf);
|
||||||
|
r->rename_idx = fidx;
|
||||||
|
r->rename_on = 1;
|
||||||
|
break;
|
||||||
|
case 6: /* Move to Trash */
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"gio trash \"%s\" 2>/dev/null"
|
||||||
|
" || mv \"%s\" ~/.local/share/Trash/files/ 2>/dev/null",
|
||||||
|
full, full);
|
||||||
|
system(cmd);
|
||||||
|
list_dir(r);
|
||||||
|
break;
|
||||||
|
case 8: /* Properties */
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
if (stat(full, &st) == 0) {
|
||||||
|
char sbuf[32]; fmt_size(st.st_size, sbuf, sizeof(sbuf));
|
||||||
|
char dbuf[32]; fmt_date(st.st_mtime, dbuf, sizeof(dbuf));
|
||||||
|
char msg[1024];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"Name: %s\nPath: %s\nSize: %s\nModified: %s\nMode: %o",
|
||||||
|
fe->name, full, sbuf, dbuf, (unsigned)st.st_mode);
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"alacritty -T 'Properties' -e bash -c "
|
||||||
|
"'echo \"%s\"; read -p \"Press Enter to close...\"' &", msg);
|
||||||
|
system(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Drawing helpers ─────────────────────────────────────────────── */
|
/* ── Drawing helpers ─────────────────────────────────────────────── */
|
||||||
static void draw_flush(Rigel *r) {
|
static void draw_flush(Rigel *r) {
|
||||||
XCopyArea(r->sk.dpy, r->buf, r->win, r->gc,
|
XCopyArea(r->sk.dpy, r->buf, r->win, r->gc,
|
||||||
@@ -318,7 +565,6 @@ static void draw_toolbar(Rigel *r) {
|
|||||||
sk_fill(&r->p, SK_SURFACE, 0, 0, WIN_W, TOOL_H);
|
sk_fill(&r->p, SK_SURFACE, 0, 0, WIN_W, TOOL_H);
|
||||||
sk_line(&r->p, SK_BORDER, 0, TOOL_H-1, WIN_W, TOOL_H-1);
|
sk_line(&r->p, SK_BORDER, 0, TOOL_H-1, WIN_W, TOOL_H-1);
|
||||||
|
|
||||||
/* back / fwd / up buttons */
|
|
||||||
int can_back = r->hist_pos > 0;
|
int can_back = r->hist_pos > 0;
|
||||||
int can_fwd = r->hist_pos < r->hist_len - 1;
|
int can_fwd = r->hist_pos < r->hist_len - 1;
|
||||||
|
|
||||||
@@ -351,7 +597,6 @@ static void draw_toolbar(Rigel *r) {
|
|||||||
px + 6, sk_baseline(r->sk.font_ui, py, ph),
|
px + 6, sk_baseline(r->sk.font_ui, py, ph),
|
||||||
ptext, px+1, py+1, pw-2, ph-2);
|
ptext, px+1, py+1, pw-2, ph-2);
|
||||||
|
|
||||||
/* cursor when editing */
|
|
||||||
if (r->path_editing) {
|
if (r->path_editing) {
|
||||||
char tmp[4096];
|
char tmp[4096];
|
||||||
int cpos = r->path_cursor;
|
int cpos = r->path_cursor;
|
||||||
@@ -366,7 +611,6 @@ static void draw_sidebar(Rigel *r) {
|
|||||||
sk_fill(&r->p, SK_SIDEBAR, 0, 0, SIDE_W, WIN_H);
|
sk_fill(&r->p, SK_SIDEBAR, 0, 0, SIDE_W, WIN_H);
|
||||||
sk_line(&r->p, SK_BORDER, SIDE_W, 0, SIDE_W, WIN_H);
|
sk_line(&r->p, SK_BORDER, SIDE_W, 0, SIDE_W, WIN_H);
|
||||||
|
|
||||||
/* header */
|
|
||||||
sk_fill(&r->p, 0x06090FUL, 0, TOOL_H, SIDE_W, SIDE_HDR_H);
|
sk_fill(&r->p, 0x06090FUL, 0, TOOL_H, SIDE_W, SIDE_HDR_H);
|
||||||
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 12,
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 12,
|
||||||
sk_baseline(r->sk.font_sm, TOOL_H, SIDE_HDR_H), "BOOKMARKS");
|
sk_baseline(r->sk.font_sm, TOOL_H, SIDE_HDR_H), "BOOKMARKS");
|
||||||
@@ -375,7 +619,6 @@ static void draw_sidebar(Rigel *r) {
|
|||||||
int iy = TOOL_H + SIDE_HDR_H;
|
int iy = TOOL_H + SIDE_HDR_H;
|
||||||
for (int i = 0; i < NBKMARKS; i++, iy += SIDE_ITEM_H) {
|
for (int i = 0; i < NBKMARKS; i++, iy += SIDE_ITEM_H) {
|
||||||
int hov = (r->sidebar_hover == i);
|
int hov = (r->sidebar_hover == i);
|
||||||
/* check if active */
|
|
||||||
char expanded[4096];
|
char expanded[4096];
|
||||||
expand_path(BKMARKS[i].path, expanded, sizeof(expanded));
|
expand_path(BKMARKS[i].path, expanded, sizeof(expanded));
|
||||||
int active = (strcmp(expanded, r->cwd) == 0);
|
int active = (strcmp(expanded, r->cwd) == 0);
|
||||||
@@ -383,7 +626,6 @@ static void draw_sidebar(Rigel *r) {
|
|||||||
unsigned long bg = active ? SK_SEL : (hov ? SK_ROW_HOV : SK_SIDEBAR);
|
unsigned long bg = active ? SK_SEL : (hov ? SK_ROW_HOV : SK_SIDEBAR);
|
||||||
sk_fill(&r->p, bg, 0, iy, SIDE_W, SIDE_ITEM_H);
|
sk_fill(&r->p, bg, 0, iy, SIDE_W, SIDE_ITEM_H);
|
||||||
|
|
||||||
/* accent bar for active */
|
|
||||||
if (active) sk_fill(&r->p, SK_ACCENT, 0, iy, 3, SIDE_ITEM_H);
|
if (active) sk_fill(&r->p, SK_ACCENT, 0, iy, 3, SIDE_ITEM_H);
|
||||||
|
|
||||||
unsigned long fg = active ? SK_TEXT_HDR : (hov ? SK_TEXT_HOV : SK_TEXT);
|
unsigned long fg = active ? SK_TEXT_HDR : (hov ? SK_TEXT_HOV : SK_TEXT);
|
||||||
@@ -406,7 +648,6 @@ static void draw_col_header(Rigel *r) {
|
|||||||
unsigned long fc = (r->sort_col == cols[i].col) ? SK_TEXT_HDR : SK_TEXT_DIM;
|
unsigned long fc = (r->sort_col == cols[i].col) ? SK_TEXT_HDR : SK_TEXT_DIM;
|
||||||
sk_text(&r->p, r->sk.font_sm, fc, cols[i].x + 4,
|
sk_text(&r->p, r->sk.font_sm, fc, cols[i].x + 4,
|
||||||
sk_baseline(r->sk.font_sm, hy, COL_H), cols[i].lbl);
|
sk_baseline(r->sk.font_sm, hy, COL_H), cols[i].lbl);
|
||||||
/* sort arrow */
|
|
||||||
if (r->sort_col == cols[i].col) {
|
if (r->sort_col == cols[i].col) {
|
||||||
const char *arr = (r->sort_dir > 0) ? "▲" : "▼";
|
const char *arr = (r->sort_dir > 0) ? "▲" : "▼";
|
||||||
sk_text(&r->p, r->sk.font_sm, SK_ACCENT,
|
sk_text(&r->p, r->sk.font_sm, SK_ACCENT,
|
||||||
@@ -419,19 +660,21 @@ static void draw_col_header(Rigel *r) {
|
|||||||
static void draw_filelist(Rigel *r) {
|
static void draw_filelist(Rigel *r) {
|
||||||
sk_fill(&r->p, SK_BG, FILE_X, LIST_Y, FILE_W, LIST_H);
|
sk_fill(&r->p, SK_BG, FILE_X, LIST_Y, FILE_W, LIST_H);
|
||||||
|
|
||||||
int visible = r->nfiles - r->scroll;
|
int nvis = get_nvis(r);
|
||||||
|
int visible = nvis - r->scroll;
|
||||||
if (visible > LIST_ROWS) visible = LIST_ROWS;
|
if (visible > LIST_ROWS) visible = LIST_ROWS;
|
||||||
|
|
||||||
for (int i = 0; i < visible; i++) {
|
for (int i = 0; i < visible; i++) {
|
||||||
int ri = i + r->scroll;
|
int vis_idx = i + r->scroll;
|
||||||
int ry = LIST_Y + i * ROW_H;
|
int ry = LIST_Y + i * ROW_H;
|
||||||
FileEntry *fe = &r->files[ri];
|
int fidx = get_fidx(r, vis_idx);
|
||||||
|
FileEntry *fe = (fidx >= 0) ? &r->files[fidx] : NULL;
|
||||||
|
if (!fe) break;
|
||||||
|
|
||||||
/* row background */
|
|
||||||
unsigned long bg = SK_BG;
|
unsigned long bg = SK_BG;
|
||||||
if (ri == r->sel_row) bg = SK_SEL;
|
if (fidx == r->sel_row) bg = SK_SEL;
|
||||||
else if (ri == r->hover_row) bg = SK_ROW_HOV;
|
else if (fidx == r->hover_row) bg = SK_ROW_HOV;
|
||||||
else if (i % 2 == 1) bg = SK_ROW_ALT;
|
else if (i % 2 == 1) bg = SK_ROW_ALT;
|
||||||
sk_fill(&r->p, bg, FILE_X, ry, FILE_W - SB_W, ROW_H);
|
sk_fill(&r->p, bg, FILE_X, ry, FILE_W - SB_W, ROW_H);
|
||||||
|
|
||||||
/* type badge */
|
/* type badge */
|
||||||
@@ -444,10 +687,24 @@ static void draw_filelist(Rigel *r) {
|
|||||||
|
|
||||||
/* name */
|
/* name */
|
||||||
int ty = sk_baseline(r->sk.font_ui, ry, ROW_H);
|
int ty = sk_baseline(r->sk.font_ui, ry, ROW_H);
|
||||||
unsigned long fg = (ri == r->sel_row || ri == r->hover_row) ? SK_TEXT_HDR : SK_TEXT;
|
unsigned long fg = (fidx == r->sel_row || fidx == r->hover_row) ? SK_TEXT_HDR : SK_TEXT;
|
||||||
|
if (r->search_on && r->search_str[0]) {
|
||||||
|
char *match = strcasestr(fe->name, r->search_str);
|
||||||
|
if (match) {
|
||||||
|
/* accent rect behind the matching substring */
|
||||||
|
int pre_len = (int)(match - fe->name);
|
||||||
|
int match_len = r->search_len;
|
||||||
|
int nx = FILE_X + BADGE_W + 2;
|
||||||
|
char tmp[512];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%.*s", pre_len, fe->name);
|
||||||
|
int mx = nx + sk_textw(&r->sk, r->sk.font_ui, tmp);
|
||||||
|
snprintf(tmp, sizeof(tmp), "%.*s", match_len, match);
|
||||||
|
int mw = sk_textw(&r->sk, r->sk.font_ui, tmp);
|
||||||
|
sk_fill(&r->p, SK_ACCENT, mx, ry + 2, mw, ROW_H - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
sk_text_clip(&r->p, r->sk.font_ui, fg,
|
sk_text_clip(&r->p, r->sk.font_ui, fg,
|
||||||
FILE_X + BADGE_W + 2, ty,
|
FILE_X + BADGE_W + 2, ty, fe->name,
|
||||||
fe->name,
|
|
||||||
FILE_X + BADGE_W, ry, NAME_W, ROW_H);
|
FILE_X + BADGE_W, ry, NAME_W, ROW_H);
|
||||||
|
|
||||||
/* size */
|
/* size */
|
||||||
@@ -462,17 +719,16 @@ static void draw_filelist(Rigel *r) {
|
|||||||
sk_text_clip(&r->p, r->sk.font_ui, SK_TEXT_DIM, dx, ty,
|
sk_text_clip(&r->p, r->sk.font_ui, SK_TEXT_DIM, dx, ty,
|
||||||
dbuf, dx, ry, DATE_W - 4, ROW_H);
|
dbuf, dx, ry, DATE_W - 4, ROW_H);
|
||||||
|
|
||||||
/* row separator */
|
|
||||||
sk_line(&r->p, SK_SEP, FILE_X, ry + ROW_H - 1, WIN_W - SB_W, ry + ROW_H - 1);
|
sk_line(&r->p, SK_SEP, FILE_X, ry + ROW_H - 1, WIN_W - SB_W, ry + ROW_H - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* scrollbar */
|
/* scrollbar */
|
||||||
if (r->nfiles > LIST_ROWS) {
|
if (nvis > LIST_ROWS) {
|
||||||
int sbx = WIN_W - SB_W;
|
int sbx = WIN_W - SB_W;
|
||||||
sk_fill(&r->p, SK_PANEL, sbx, LIST_Y, SB_W, LIST_H);
|
sk_fill(&r->p, SK_PANEL, sbx, LIST_Y, SB_W, LIST_H);
|
||||||
int th = LIST_H * LIST_ROWS / r->nfiles;
|
int th = LIST_H * LIST_ROWS / nvis;
|
||||||
if (th < 20) th = 20;
|
if (th < 20) th = 20;
|
||||||
int ty2 = LIST_Y + (LIST_H - th) * r->scroll / (r->nfiles - LIST_ROWS);
|
int ty2 = LIST_Y + (LIST_H - th) * r->scroll / (nvis - LIST_ROWS);
|
||||||
sk_fill(&r->p, SK_ACCENT, sbx, ty2, SB_W, th);
|
sk_fill(&r->p, SK_ACCENT, sbx, ty2, SB_W, th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,35 +738,77 @@ static void draw_statusbar(Rigel *r) {
|
|||||||
sk_fill(&r->p, SK_SURFACE, 0, sy, WIN_W, STAT_H);
|
sk_fill(&r->p, SK_SURFACE, 0, sy, WIN_W, STAT_H);
|
||||||
sk_line(&r->p, SK_BORDER, 0, sy, WIN_W, sy);
|
sk_line(&r->p, SK_BORDER, 0, sy, WIN_W, sy);
|
||||||
|
|
||||||
char msg[128];
|
if (r->search_on) {
|
||||||
if (r->sel_row >= 0 && r->sel_row < r->nfiles) {
|
/* show search bar */
|
||||||
char sb[32];
|
sk_fill(&r->p, 0x0A1020UL, FILE_X, sy, FILE_W, STAT_H);
|
||||||
fmt_size(r->files[r->sel_row].size, sb, sizeof(sb));
|
|
||||||
snprintf(msg, sizeof(msg), "%d items — \"%s\" %s",
|
|
||||||
r->nfiles, r->files[r->sel_row].name,
|
|
||||||
r->files[r->sel_row].type == FT_DIR ? "(folder)" : sb);
|
|
||||||
} else {
|
|
||||||
snprintf(msg, sizeof(msg), "%d item%s", r->nfiles, r->nfiles==1?"":"s");
|
|
||||||
}
|
|
||||||
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 10,
|
|
||||||
sk_baseline(r->sk.font_sm, sy, STAT_H), msg);
|
|
||||||
|
|
||||||
/* show-hidden indicator */
|
|
||||||
if (r->show_hidden) {
|
|
||||||
const char *hint = "· showing hidden";
|
|
||||||
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
||||||
WIN_W - sk_textw(&r->sk, r->sk.font_sm, hint) - 10,
|
FILE_X + 8, sk_baseline(r->sk.font_sm, sy, STAT_H), "Filter: ");
|
||||||
sk_baseline(r->sk.font_sm, sy, STAT_H), hint);
|
int pfx = FILE_X + 8 + sk_textw(&r->sk, r->sk.font_sm, "Filter: ");
|
||||||
|
if (r->search_len > 0)
|
||||||
|
sk_text(&r->p, r->sk.font_ui, SK_TEXT_HDR, pfx,
|
||||||
|
sk_baseline(r->sk.font_ui, sy, STAT_H), r->search_str);
|
||||||
|
/* cursor */
|
||||||
|
int cx = pfx + sk_textw(&r->sk, r->sk.font_ui, r->search_str);
|
||||||
|
sk_fill(&r->p, SK_ACCENT, cx, sy + 4, 2, STAT_H - 8);
|
||||||
|
/* result count */
|
||||||
|
char cnt[32];
|
||||||
|
snprintf(cnt, sizeof(cnt), "%d match%s Esc: clear",
|
||||||
|
r->nfiltered, r->nfiltered == 1 ? "" : "es");
|
||||||
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
||||||
|
WIN_W - sk_textw(&r->sk, r->sk.font_sm, cnt) - 10,
|
||||||
|
sk_baseline(r->sk.font_sm, sy, STAT_H), cnt);
|
||||||
|
} else {
|
||||||
|
char msg[128];
|
||||||
|
if (r->sel_row >= 0 && r->sel_row < r->nfiles) {
|
||||||
|
char sb[32];
|
||||||
|
fmt_size(r->files[r->sel_row].size, sb, sizeof(sb));
|
||||||
|
snprintf(msg, sizeof(msg), "%d items — \"%s\" %s",
|
||||||
|
r->nfiles, r->files[r->sel_row].name,
|
||||||
|
r->files[r->sel_row].type == FT_DIR ? "(folder)" : sb);
|
||||||
|
} else {
|
||||||
|
snprintf(msg, sizeof(msg), "%d item%s", r->nfiles, r->nfiles==1?"":"s");
|
||||||
|
}
|
||||||
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 10,
|
||||||
|
sk_baseline(r->sk.font_sm, sy, STAT_H), msg);
|
||||||
|
|
||||||
|
if (r->show_hidden) {
|
||||||
|
const char *hint = "· showing hidden Ctrl+F: filter";
|
||||||
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
||||||
|
WIN_W - sk_textw(&r->sk, r->sk.font_sm, hint) - 10,
|
||||||
|
sk_baseline(r->sk.font_sm, sy, STAT_H), hint);
|
||||||
|
} else {
|
||||||
|
const char *hint = "Ctrl+F: filter";
|
||||||
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
||||||
|
WIN_W - sk_textw(&r->sk, r->sk.font_sm, hint) - 10,
|
||||||
|
sk_baseline(r->sk.font_sm, sy, STAT_H), hint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void draw_rename_overlay(Rigel *r) {
|
||||||
|
if (!r->rename_on) return;
|
||||||
|
int ry = WIN_H - STAT_H - 34;
|
||||||
|
sk_fill(&r->p, 0x0A1020UL, FILE_X, ry, FILE_W, 32);
|
||||||
|
sk_rect(&r->p, SK_ACCENT, FILE_X, ry, FILE_W, 32);
|
||||||
|
sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM,
|
||||||
|
FILE_X + 8, sk_baseline(r->sk.font_sm, ry, 32), "Rename: ");
|
||||||
|
int pfx = FILE_X + 8 + sk_textw(&r->sk, r->sk.font_sm, "Rename: ");
|
||||||
|
if (r->rename_len > 0)
|
||||||
|
sk_text(&r->p, r->sk.font_ui, SK_TEXT_HDR,
|
||||||
|
pfx, sk_baseline(r->sk.font_ui, ry, 32), r->rename_buf);
|
||||||
|
int cx = pfx + sk_textw(&r->sk, r->sk.font_ui, r->rename_buf);
|
||||||
|
sk_fill(&r->p, SK_ACCENT, cx, ry + 6, 2, 20);
|
||||||
|
}
|
||||||
|
|
||||||
static void draw(Rigel *r) {
|
static void draw(Rigel *r) {
|
||||||
draw_toolbar(r);
|
draw_toolbar(r);
|
||||||
draw_sidebar(r);
|
draw_sidebar(r);
|
||||||
draw_col_header(r);
|
draw_col_header(r);
|
||||||
draw_filelist(r);
|
draw_filelist(r);
|
||||||
draw_statusbar(r);
|
draw_statusbar(r);
|
||||||
|
draw_rename_overlay(r);
|
||||||
draw_flush(r);
|
draw_flush(r);
|
||||||
|
if (r->ctx_vis) ctx_draw(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Hit testing ─────────────────────────────────────────────────── */
|
/* ── Hit testing ─────────────────────────────────────────────────── */
|
||||||
@@ -523,7 +821,7 @@ static int toolbar_btn_at(int x, int y) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int file_row_at(int x, int y) {
|
static int file_vis_row_at(int x, int y) {
|
||||||
if (!sk_in_rect(x, y, FILE_X, LIST_Y, FILE_W, LIST_H)) return -1;
|
if (!sk_in_rect(x, y, FILE_X, LIST_Y, FILE_W, LIST_H)) return -1;
|
||||||
return (y - LIST_Y) / ROW_H;
|
return (y - LIST_Y) / ROW_H;
|
||||||
}
|
}
|
||||||
@@ -543,22 +841,27 @@ static int sidebar_item_at(int x, int y) {
|
|||||||
|
|
||||||
/* ── Event handlers ──────────────────────────────────────────────── */
|
/* ── Event handlers ──────────────────────────────────────────────── */
|
||||||
static void on_motion(Rigel *r, int x, int y) {
|
static void on_motion(Rigel *r, int x, int y) {
|
||||||
int hrow = file_row_at(x, y);
|
int vrow = file_vis_row_at(x, y);
|
||||||
if (hrow >= 0) hrow += r->scroll;
|
int hrow = -1;
|
||||||
int hbtn = toolbar_btn_at(x, y);
|
if (vrow >= 0) {
|
||||||
|
int vis_idx = vrow + r->scroll;
|
||||||
|
hrow = get_fidx(r, vis_idx);
|
||||||
|
}
|
||||||
|
int hbtn = toolbar_btn_at(x, y);
|
||||||
int hside = sidebar_item_at(x, y);
|
int hside = sidebar_item_at(x, y);
|
||||||
|
|
||||||
int redraw = (hrow != r->hover_row ||
|
int redraw = (hrow != r->hover_row ||
|
||||||
hbtn != r->hover_btn ||
|
hbtn != r->hover_btn ||
|
||||||
hside != r->sidebar_hover);
|
hside != r->sidebar_hover);
|
||||||
r->hover_row = hrow;
|
r->hover_row = hrow;
|
||||||
r->hover_btn = hbtn;
|
r->hover_btn = hbtn;
|
||||||
r->sidebar_hover = hside;
|
r->sidebar_hover = hside;
|
||||||
if (redraw) draw(r);
|
if (redraw) draw(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_scroll(Rigel *r, int dir) {
|
static void on_scroll(Rigel *r, int dir) {
|
||||||
int max = r->nfiles - LIST_ROWS;
|
int nvis = get_nvis(r);
|
||||||
|
int max = nvis - LIST_ROWS;
|
||||||
if (max < 0) max = 0;
|
if (max < 0) max = 0;
|
||||||
r->scroll += dir * 3;
|
r->scroll += dir * 3;
|
||||||
if (r->scroll < 0) r->scroll = 0;
|
if (r->scroll < 0) r->scroll = 0;
|
||||||
@@ -573,7 +876,12 @@ static void path_commit(Rigel *r) {
|
|||||||
nav_cd(r, expanded);
|
nav_cd(r, expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_click(Rigel *r, int x, int y, Time t) {
|
static void on_click(Rigel *r, int x, int y, Time t, int btn) {
|
||||||
|
/* dismiss context if open */
|
||||||
|
if (r->ctx_vis) { ctx_hide(r); draw(r); return; }
|
||||||
|
/* dismiss rename if open and clicking outside */
|
||||||
|
if (r->rename_on) { r->rename_on = 0; }
|
||||||
|
|
||||||
/* path bar */
|
/* path bar */
|
||||||
int py = (TOOL_H - PATH_H) / 2;
|
int py = (TOOL_H - PATH_H) / 2;
|
||||||
if (sk_in_rect(x, y, PATH_X, py, PATH_W, PATH_H)) {
|
if (sk_in_rect(x, y, PATH_X, py, PATH_W, PATH_H)) {
|
||||||
@@ -586,22 +894,23 @@ static void on_click(Rigel *r, int x, int y, Time t) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* deactivate path edit if clicked elsewhere */
|
|
||||||
if (r->path_editing) {
|
if (r->path_editing) {
|
||||||
r->path_editing = 0;
|
r->path_editing = 0;
|
||||||
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
||||||
draw(r);
|
draw(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* toolbar buttons */
|
/* toolbar buttons (left click only) */
|
||||||
int btn = toolbar_btn_at(x, y);
|
if (btn == Button1) {
|
||||||
if (btn == 1) { nav_back(r); draw(r); return; }
|
int b = toolbar_btn_at(x, y);
|
||||||
if (btn == 2) { nav_fwd(r); draw(r); return; }
|
if (b == 1) { nav_back(r); draw(r); return; }
|
||||||
if (btn == 3) { nav_up(r); draw(r); return; }
|
if (b == 2) { nav_fwd(r); draw(r); return; }
|
||||||
|
if (b == 3) { nav_up(r); draw(r); return; }
|
||||||
|
}
|
||||||
|
|
||||||
/* sidebar */
|
/* sidebar */
|
||||||
int si = sidebar_item_at(x, y);
|
int si = sidebar_item_at(x, y);
|
||||||
if (si >= 0) {
|
if (si >= 0 && btn == Button1) {
|
||||||
char expanded[4096];
|
char expanded[4096];
|
||||||
expand_path(BKMARKS[si].path, expanded, sizeof(expanded));
|
expand_path(BKMARKS[si].path, expanded, sizeof(expanded));
|
||||||
nav_cd(r, expanded);
|
nav_cd(r, expanded);
|
||||||
@@ -611,37 +920,84 @@ static void on_click(Rigel *r, int x, int y, Time t) {
|
|||||||
|
|
||||||
/* column header */
|
/* column header */
|
||||||
int col = col_header_at(x, y);
|
int col = col_header_at(x, y);
|
||||||
if (col >= 0) {
|
if (col >= 0 && btn == Button1) {
|
||||||
if (r->sort_col == col) r->sort_dir = -r->sort_dir;
|
if (r->sort_col == col) r->sort_dir = -r->sort_dir;
|
||||||
else { r->sort_col = col; r->sort_dir = 1; }
|
else { r->sort_col = col; r->sort_dir = 1; }
|
||||||
g_sort_col = r->sort_col; g_sort_dir = r->sort_dir;
|
g_sort_col = r->sort_col; g_sort_dir = r->sort_dir;
|
||||||
qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files);
|
qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files);
|
||||||
|
if (r->search_on) search_filter(r);
|
||||||
draw(r);
|
draw(r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* file list */
|
/* file list */
|
||||||
int ri = file_row_at(x, y);
|
int vrow = file_vis_row_at(x, y);
|
||||||
if (ri < 0) return;
|
if (vrow < 0) return;
|
||||||
int row = ri + r->scroll;
|
int vis_idx = vrow + r->scroll;
|
||||||
if (row >= r->nfiles) return;
|
int fidx = get_fidx(r, vis_idx);
|
||||||
|
if (fidx < 0) return;
|
||||||
|
|
||||||
/* double-click detection (300ms) */
|
if (btn == Button3) {
|
||||||
int dbl = (row == r->last_click_row && (t - r->last_click_time) < 300);
|
r->sel_row = fidx;
|
||||||
|
draw(r);
|
||||||
|
ctx_show(r, x, y, fidx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* double-click detection */
|
||||||
|
int dbl = (fidx == r->last_click_row && (t - r->last_click_time) < 300);
|
||||||
r->last_click_time = t;
|
r->last_click_time = t;
|
||||||
r->last_click_row = row;
|
r->last_click_row = fidx;
|
||||||
|
|
||||||
if (dbl) {
|
if (dbl) {
|
||||||
open_entry(r, row);
|
open_entry(r, fidx);
|
||||||
} else {
|
} else {
|
||||||
r->sel_row = (r->sel_row == row) ? -1 : row;
|
r->sel_row = (r->sel_row == fidx) ? -1 : fidx;
|
||||||
}
|
}
|
||||||
draw(r);
|
draw(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_key(Rigel *r, XKeyEvent *ke) {
|
static void on_key(Rigel *r, XKeyEvent *ke) {
|
||||||
KeySym sym = XLookupKeysym(ke, 0);
|
KeySym sym = XLookupKeysym(ke, 0);
|
||||||
|
int mod = ke->state;
|
||||||
|
|
||||||
|
/* ── rename overlay ────────────────────────────────────── */
|
||||||
|
if (r->rename_on) {
|
||||||
|
if (sym == XK_Escape) {
|
||||||
|
r->rename_on = 0;
|
||||||
|
} else if (sym == XK_Return || sym == XK_KP_Enter) {
|
||||||
|
/* commit rename */
|
||||||
|
if (r->rename_len > 0 && r->rename_idx >= 0
|
||||||
|
&& r->rename_idx < r->nfiles) {
|
||||||
|
char old_path[4096 + 512];
|
||||||
|
char new_path[4096 + 512];
|
||||||
|
snprintf(old_path, sizeof(old_path), "%s/%s",
|
||||||
|
r->cwd, r->files[r->rename_idx].name);
|
||||||
|
snprintf(new_path, sizeof(new_path), "%s/%s",
|
||||||
|
r->cwd, r->rename_buf);
|
||||||
|
rename(old_path, new_path);
|
||||||
|
list_dir(r);
|
||||||
|
}
|
||||||
|
r->rename_on = 0;
|
||||||
|
} else if (sym == XK_BackSpace) {
|
||||||
|
if (r->rename_len > 0)
|
||||||
|
r->rename_buf[--r->rename_len] = '\0';
|
||||||
|
} else {
|
||||||
|
char buf[16];
|
||||||
|
int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL);
|
||||||
|
if (n > 0 && (unsigned char)buf[0] >= 0x20) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
int len = (int)strlen(r->rename_buf);
|
||||||
|
if (len + n < (int)sizeof(r->rename_buf) - 1) {
|
||||||
|
memcpy(r->rename_buf + len, buf, n + 1);
|
||||||
|
r->rename_len += n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw(r); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── path bar editing ──────────────────────────────────── */
|
||||||
if (r->path_editing) {
|
if (r->path_editing) {
|
||||||
if (sym == XK_Return || sym == XK_KP_Enter) {
|
if (sym == XK_Return || sym == XK_KP_Enter) {
|
||||||
path_commit(r);
|
path_commit(r);
|
||||||
@@ -666,7 +1022,6 @@ static void on_key(Rigel *r, XKeyEvent *ke) {
|
|||||||
} else if (sym == XK_End) {
|
} else if (sym == XK_End) {
|
||||||
r->path_cursor = (int)strlen(r->path_buf);
|
r->path_cursor = (int)strlen(r->path_buf);
|
||||||
} else {
|
} else {
|
||||||
/* printable character */
|
|
||||||
char buf[16];
|
char buf[16];
|
||||||
int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL);
|
int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL);
|
||||||
if (n > 0 && (unsigned char)buf[0] >= 32) {
|
if (n > 0 && (unsigned char)buf[0] >= 32) {
|
||||||
@@ -681,12 +1036,42 @@ static void on_key(Rigel *r, XKeyEvent *ke) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draw(r);
|
draw(r); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* navigation shortcuts */
|
/* ── search mode ───────────────────────────────────────── */
|
||||||
int mod = ke->state;
|
if (r->search_on) {
|
||||||
|
if (sym == XK_Escape) {
|
||||||
|
r->search_on = 0;
|
||||||
|
r->search_len = 0;
|
||||||
|
r->search_str[0] = '\0';
|
||||||
|
} else if (sym == XK_BackSpace) {
|
||||||
|
if (r->search_len > 0) {
|
||||||
|
r->search_str[--r->search_len] = '\0';
|
||||||
|
search_filter(r);
|
||||||
|
}
|
||||||
|
if (r->search_len == 0) {
|
||||||
|
r->search_on = 0;
|
||||||
|
}
|
||||||
|
} else if (sym == XK_Return) {
|
||||||
|
/* open first result */
|
||||||
|
if (r->nfiltered > 0) open_entry(r, r->filtered[0]);
|
||||||
|
} else {
|
||||||
|
char buf[8];
|
||||||
|
int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL);
|
||||||
|
if (n > 0 && (unsigned char)buf[0] >= 0x20) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
if (r->search_len + n < (int)sizeof(r->search_str) - 1) {
|
||||||
|
memcpy(r->search_str + r->search_len, buf, n + 1);
|
||||||
|
r->search_len += n;
|
||||||
|
search_filter(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw(r); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── normal navigation ─────────────────────────────────── */
|
||||||
if (sym == XK_BackSpace || (sym == XK_Left && (mod & Mod1Mask))) {
|
if (sym == XK_BackSpace || (sym == XK_Left && (mod & Mod1Mask))) {
|
||||||
nav_back(r);
|
nav_back(r);
|
||||||
} else if (sym == XK_Right && (mod & Mod1Mask)) {
|
} else if (sym == XK_Right && (mod & Mod1Mask)) {
|
||||||
@@ -696,7 +1081,6 @@ static void on_key(Rigel *r, XKeyEvent *ke) {
|
|||||||
} else if (sym == XK_Up) {
|
} else if (sym == XK_Up) {
|
||||||
if (r->sel_row > 0) r->sel_row--;
|
if (r->sel_row > 0) r->sel_row--;
|
||||||
else if (r->sel_row < 0 && r->nfiles > 0) r->sel_row = 0;
|
else if (r->sel_row < 0 && r->nfiles > 0) r->sel_row = 0;
|
||||||
/* scroll to keep selection visible */
|
|
||||||
if (r->sel_row >= 0 && r->sel_row < r->scroll) r->scroll = r->sel_row;
|
if (r->sel_row >= 0 && r->sel_row < r->scroll) r->scroll = r->sel_row;
|
||||||
} else if (sym == XK_Down) {
|
} else if (sym == XK_Down) {
|
||||||
if (r->sel_row < r->nfiles - 1) r->sel_row++;
|
if (r->sel_row < r->nfiles - 1) r->sel_row++;
|
||||||
@@ -708,11 +1092,36 @@ static void on_key(Rigel *r, XKeyEvent *ke) {
|
|||||||
on_scroll(r, -4); return;
|
on_scroll(r, -4); return;
|
||||||
} else if (sym == XK_Page_Down) {
|
} else if (sym == XK_Page_Down) {
|
||||||
on_scroll(r, 4); return;
|
on_scroll(r, 4); return;
|
||||||
|
} else if (sym == XK_F2) {
|
||||||
|
/* rename selected */
|
||||||
|
if (r->sel_row >= 0 && r->sel_row < r->nfiles) {
|
||||||
|
snprintf(r->rename_buf, sizeof(r->rename_buf), "%s",
|
||||||
|
r->files[r->sel_row].name);
|
||||||
|
r->rename_len = (int)strlen(r->rename_buf);
|
||||||
|
r->rename_idx = r->sel_row;
|
||||||
|
r->rename_on = 1;
|
||||||
|
}
|
||||||
|
} else if (sym == XK_Delete) {
|
||||||
|
/* trash selected */
|
||||||
|
if (r->sel_row >= 0 && r->sel_row < r->nfiles) {
|
||||||
|
char cmd[4096 + 512];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"gio trash \"%s/%s\" 2>/dev/null"
|
||||||
|
" || mv \"%s/%s\" ~/.local/share/Trash/files/ 2>/dev/null",
|
||||||
|
r->cwd, r->files[r->sel_row].name,
|
||||||
|
r->cwd, r->files[r->sel_row].name);
|
||||||
|
system(cmd);
|
||||||
|
list_dir(r);
|
||||||
|
}
|
||||||
} else if (sym == XK_h && (mod & ControlMask)) {
|
} else if (sym == XK_h && (mod & ControlMask)) {
|
||||||
r->show_hidden = !r->show_hidden;
|
r->show_hidden = !r->show_hidden;
|
||||||
list_dir(r);
|
list_dir(r);
|
||||||
|
} else if ((sym == XK_f && (mod & ControlMask)) || sym == XK_F3) {
|
||||||
|
r->search_on = 1;
|
||||||
|
r->search_len = 0;
|
||||||
|
r->search_str[0] = '\0';
|
||||||
|
search_filter(r);
|
||||||
} else if (sym == XK_l && (mod & ControlMask)) {
|
} else if (sym == XK_l && (mod & ControlMask)) {
|
||||||
/* focus path bar */
|
|
||||||
r->path_editing = 1;
|
r->path_editing = 1;
|
||||||
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
||||||
r->path_cursor = (int)strlen(r->path_buf);
|
r->path_cursor = (int)strlen(r->path_buf);
|
||||||
@@ -738,8 +1147,8 @@ static void init(Rigel *r) {
|
|||||||
r->hover_row = -1;
|
r->hover_row = -1;
|
||||||
r->sidebar_hover = -1;
|
r->sidebar_hover = -1;
|
||||||
r->last_click_row = -1;
|
r->last_click_row = -1;
|
||||||
|
r->ctx_hover = -1;
|
||||||
|
|
||||||
/* start in home directory */
|
|
||||||
const char *home = getenv("HOME");
|
const char *home = getenv("HOME");
|
||||||
if (!home || chdir(home) != 0) home = "/";
|
if (!home || chdir(home) != 0) home = "/";
|
||||||
snprintf(r->history[0], sizeof(r->history[0]), "%s", home);
|
snprintf(r->history[0], sizeof(r->history[0]), "%s", home);
|
||||||
@@ -749,7 +1158,6 @@ static void init(Rigel *r) {
|
|||||||
list_dir(r);
|
list_dir(r);
|
||||||
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd);
|
||||||
|
|
||||||
/* window */
|
|
||||||
XSetWindowAttributes swa = {
|
XSetWindowAttributes swa = {
|
||||||
.background_pixel = sk_color(&r->sk, SK_BG),
|
.background_pixel = sk_color(&r->sk, SK_BG),
|
||||||
.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask
|
.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask
|
||||||
@@ -762,11 +1170,9 @@ static void init(Rigel *r) {
|
|||||||
CWBackPixel | CWEventMask, &swa);
|
CWBackPixel | CWEventMask, &swa);
|
||||||
r->gc = XCreateGC(r->sk.dpy, r->win, 0, NULL);
|
r->gc = XCreateGC(r->sk.dpy, r->win, 0, NULL);
|
||||||
|
|
||||||
/* double-buffer */
|
|
||||||
r->buf = XCreatePixmap(r->sk.dpy, r->win, WIN_W, WIN_H, r->sk.depth);
|
r->buf = XCreatePixmap(r->sk.dpy, r->win, WIN_W, WIN_H, r->sk.depth);
|
||||||
r->p = (SKPainter){ .app = &r->sk, .drw = r->buf, .gc = r->gc };
|
r->p = (SKPainter){ .app = &r->sk, .drw = r->buf, .gc = r->gc };
|
||||||
|
|
||||||
/* window hints */
|
|
||||||
XStoreName(r->sk.dpy, r->win, "Rigel — File Manager");
|
XStoreName(r->sk.dpy, r->win, "Rigel — File Manager");
|
||||||
XClassHint *ch = XAllocClassHint();
|
XClassHint *ch = XAllocClassHint();
|
||||||
if (ch) { ch->res_name = (char*)"rigel"; ch->res_class = (char*)"Rigel";
|
if (ch) { ch->res_name = (char*)"rigel"; ch->res_class = (char*)"Rigel";
|
||||||
@@ -792,13 +1198,45 @@ static void run(Rigel *r) {
|
|||||||
while (r->running) {
|
while (r->running) {
|
||||||
XEvent ev;
|
XEvent ev;
|
||||||
XNextEvent(r->sk.dpy, &ev);
|
XNextEvent(r->sk.dpy, &ev);
|
||||||
|
|
||||||
|
/* ── context menu events ─────────────────────────────── */
|
||||||
|
if (r->ctx_vis && ev.xany.window == r->ctx_win) {
|
||||||
|
if (ev.type == Expose) {
|
||||||
|
ctx_draw(r);
|
||||||
|
} else if (ev.type == MotionNotify) {
|
||||||
|
int it = ctx_item_at(ev.xmotion.y);
|
||||||
|
if (it != r->ctx_hover) {
|
||||||
|
r->ctx_hover = it;
|
||||||
|
ctx_draw(r);
|
||||||
|
}
|
||||||
|
} else if (ev.type == ButtonPress) {
|
||||||
|
int it = ctx_item_at(ev.xbutton.y);
|
||||||
|
ctx_hide(r);
|
||||||
|
if (it >= 0 && !CTX[it].sep) ctx_exec(r, it);
|
||||||
|
draw(r);
|
||||||
|
} else if (ev.type == LeaveNotify) {
|
||||||
|
r->ctx_hover = -1;
|
||||||
|
ctx_draw(r);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* click outside context menu */
|
||||||
|
if (r->ctx_vis && (ev.type == ButtonPress || ev.type == KeyPress)) {
|
||||||
|
ctx_hide(r);
|
||||||
|
draw(r);
|
||||||
|
if (ev.type == KeyPress) on_key(r, &ev.xkey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── main window events ──────────────────────────────── */
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case Expose:
|
case Expose:
|
||||||
if (ev.xexpose.count == 0) draw(r);
|
if (ev.xexpose.count == 0) draw(r);
|
||||||
break;
|
break;
|
||||||
case ButtonPress:
|
case ButtonPress:
|
||||||
if (ev.xbutton.button == Button1)
|
if (ev.xbutton.button == Button1 || ev.xbutton.button == Button3)
|
||||||
on_click(r, ev.xbutton.x, ev.xbutton.y, ev.xbutton.time);
|
on_click(r, ev.xbutton.x, ev.xbutton.y, ev.xbutton.time,
|
||||||
|
ev.xbutton.button);
|
||||||
else if (ev.xbutton.button == Button4) on_scroll(r, -1);
|
else if (ev.xbutton.button == Button4) on_scroll(r, -1);
|
||||||
else if (ev.xbutton.button == Button5) on_scroll(r, 1);
|
else if (ev.xbutton.button == Button5) on_scroll(r, 1);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CC = gcc
|
||||||
|
CFLAGS = -O2 -Wall -I.. $(shell pkg-config --cflags x11 xft fontconfig)
|
||||||
|
LIBS = -L.. -lskykit $(shell pkg-config --libs x11 xft fontconfig)
|
||||||
|
|
||||||
|
hello: hello.c ../libskykit.a
|
||||||
|
$(CC) $(CFLAGS) -o hello hello.c $(LIBS)
|
||||||
|
|
||||||
|
../libskykit.a:
|
||||||
|
$(MAKE) -C ..
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f hello
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/* hello.c — minimal SkyKit application template
|
||||||
|
*
|
||||||
|
* Build:
|
||||||
|
* cd skykit && make (builds libskykit.a)
|
||||||
|
* cd template
|
||||||
|
* gcc -O2 hello.c -I.. -L.. -lskykit \
|
||||||
|
* $(pkg-config --libs --cflags x11 xft fontconfig) -o hello
|
||||||
|
*
|
||||||
|
* This template shows the minimum boilerplate for a native OrionOS app:
|
||||||
|
* - sk_app_init() opens the display and loads fonts
|
||||||
|
* - Create a window and a Pixmap back-buffer
|
||||||
|
* - Wrap them in an SKPainter
|
||||||
|
* - Draw to the Pixmap, then XCopyArea to the window
|
||||||
|
* - Respond to Expose, KeyPress, ButtonPress, ClientMessage
|
||||||
|
*/
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/Xutil.h>
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "skykit.h"
|
||||||
|
|
||||||
|
#define WIN_W 640
|
||||||
|
#define WIN_H 400
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
SKApp app;
|
||||||
|
sk_app_init(&app);
|
||||||
|
|
||||||
|
/* ── Window ──────────────────────────────────────────── */
|
||||||
|
XSetWindowAttributes swa = {
|
||||||
|
.background_pixel = sk_color(&app, SK_BG),
|
||||||
|
.event_mask = ExposureMask | KeyPressMask
|
||||||
|
| ButtonPressMask | StructureNotifyMask,
|
||||||
|
};
|
||||||
|
Window win = XCreateWindow(
|
||||||
|
app.dpy, app.root,
|
||||||
|
200, 100, WIN_W, WIN_H, 0,
|
||||||
|
app.depth, InputOutput, app.vis,
|
||||||
|
CWBackPixel | CWEventMask, &swa);
|
||||||
|
|
||||||
|
/* Back-buffer for flicker-free rendering */
|
||||||
|
GC gc = XCreateGC(app.dpy, win, 0, NULL);
|
||||||
|
Pixmap buf = XCreatePixmap(app.dpy, win, WIN_W, WIN_H, app.depth);
|
||||||
|
|
||||||
|
/* SKPainter wraps the back-buffer */
|
||||||
|
SKPainter p = { .app = &app, .drw = buf, .gc = gc };
|
||||||
|
|
||||||
|
/* WM_DELETE_WINDOW protocol */
|
||||||
|
Atom wm_del = XInternAtom(app.dpy, "WM_DELETE_WINDOW", False);
|
||||||
|
XSetWMProtocols(app.dpy, win, &wm_del, 1);
|
||||||
|
|
||||||
|
XStoreName(app.dpy, win, "Hello OrionOS");
|
||||||
|
XClassHint *ch = XAllocClassHint();
|
||||||
|
if (ch) { ch->res_name = (char*)"hello"; ch->res_class = (char*)"Hello"; XSetClassHint(app.dpy, win, ch); XFree(ch); }
|
||||||
|
|
||||||
|
XMapWindow(app.dpy, win);
|
||||||
|
XFlush(app.dpy);
|
||||||
|
|
||||||
|
/* Click counter for demo */
|
||||||
|
int clicks = 0;
|
||||||
|
|
||||||
|
/* ── Draw function ───────────────────────────────────── */
|
||||||
|
/* We define a lambda-style local redraw — in a real app this would be
|
||||||
|
* a proper function taking your app struct. */
|
||||||
|
#define REDRAW() do { \
|
||||||
|
sk_fill(&p, SK_BG, 0, 0, WIN_W, WIN_H); \
|
||||||
|
\
|
||||||
|
/* Accent bar at top */ \
|
||||||
|
sk_fill(&p, SK_ACCENT, 0, 0, WIN_W, 3); \
|
||||||
|
\
|
||||||
|
/* Centred heading */ \
|
||||||
|
const char *title = "Hello, OrionOS"; \
|
||||||
|
int tw = sk_textw(&app, app.font_hdr, title); \
|
||||||
|
sk_text(&p, app.font_hdr, SK_TEXT_HDR, \
|
||||||
|
(WIN_W - tw) / 2, sk_baseline(app.font_hdr, 60, 32), title); \
|
||||||
|
\
|
||||||
|
/* Subtitle */ \
|
||||||
|
const char *sub = "A SkyKit native application"; \
|
||||||
|
int sw = sk_textw(&app, app.font_ui, sub); \
|
||||||
|
sk_text(&p, app.font_ui, SK_TEXT_DIM, \
|
||||||
|
(WIN_W - sw) / 2, sk_baseline(app.font_ui, 100, 24), sub); \
|
||||||
|
\
|
||||||
|
/* Click counter button */ \
|
||||||
|
SKBtn btn = { (WIN_W - 160) / 2, 160, 160, 36, "Click me", 1 }; \
|
||||||
|
sk_draw_btn(&p, &btn, 0); \
|
||||||
|
\
|
||||||
|
/* Counter label */ \
|
||||||
|
char cnt[64]; snprintf(cnt, sizeof(cnt), "Clicked %d time%s", clicks, clicks==1?"":"s"); \
|
||||||
|
int cw = sk_textw(&app, app.font_ui, cnt); \
|
||||||
|
sk_text(&p, app.font_ui, SK_TEXT, (WIN_W - cw) / 2, \
|
||||||
|
sk_baseline(app.font_ui, 215, 24), cnt); \
|
||||||
|
\
|
||||||
|
/* Hint */ \
|
||||||
|
const char *hint = "Press Q or Esc to quit"; \
|
||||||
|
int hw = sk_textw(&app, app.font_sm, hint); \
|
||||||
|
sk_text(&p, app.font_sm, SK_TEXT_DIM, (WIN_W - hw) / 2, \
|
||||||
|
sk_baseline(app.font_sm, WIN_H - 36, 24), hint); \
|
||||||
|
\
|
||||||
|
/* Blit to window */ \
|
||||||
|
XCopyArea(app.dpy, buf, win, gc, 0, 0, WIN_W, WIN_H, 0, 0); \
|
||||||
|
XFlush(app.dpy); \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
REDRAW();
|
||||||
|
|
||||||
|
/* ── Event loop ──────────────────────────────────────── */
|
||||||
|
int running = 1;
|
||||||
|
while (running) {
|
||||||
|
XEvent ev;
|
||||||
|
XNextEvent(app.dpy, &ev);
|
||||||
|
switch (ev.type) {
|
||||||
|
case Expose:
|
||||||
|
if (ev.xexpose.count == 0) REDRAW();
|
||||||
|
break;
|
||||||
|
case KeyPress: {
|
||||||
|
KeySym ks = XLookupKeysym(&ev.xkey, 0);
|
||||||
|
if (ks == XK_q || ks == XK_Q || ks == XK_Escape) running = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ButtonPress:
|
||||||
|
if (ev.xbutton.button == Button1) {
|
||||||
|
/* check if inside the button */
|
||||||
|
int bx = (WIN_W - 160) / 2, by = 160;
|
||||||
|
if (sk_in_rect(ev.xbutton.x, ev.xbutton.y, bx, by, 160, 36)) {
|
||||||
|
clicks++;
|
||||||
|
REDRAW();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ClientMessage:
|
||||||
|
if ((Atom)ev.xclient.data.l[0] == wm_del) running = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef REDRAW
|
||||||
|
|
||||||
|
sk_app_free(&app);
|
||||||
|
XFreePixmap(app.dpy, buf);
|
||||||
|
XFreeGC(app.dpy, gc);
|
||||||
|
XDestroyWindow(app.dpy, win);
|
||||||
|
XCloseDisplay(app.dpy);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@ static const MenuItem MENU_ITEMS[] = {
|
|||||||
{ "App Launcher", "orionlauncher &" },
|
{ "App Launcher", "orionlauncher &" },
|
||||||
{ "Terminal", "alacritty &" },
|
{ "Terminal", "alacritty &" },
|
||||||
{ "File Manager", "rigel &" },
|
{ "File Manager", "rigel &" },
|
||||||
{ "Text Editor", "mousepad &" },
|
{ "Text Editor", "quill &" },
|
||||||
{ "──────────────────", NULL },
|
{ "──────────────────", NULL },
|
||||||
{ "Web Browser", "firefox &" },
|
{ "Web Browser", "firefox &" },
|
||||||
{ "System Monitor", "alacritty -T 'System Monitor' -e htop &" },
|
{ "System Monitor", "alacritty -T 'System Monitor' -e htop &" },
|
||||||
|
|||||||
Reference in new issue
Block a user