tl
init
d6dbd7ac86a356e9bc4b97e7380672615d937275
IIIlllIIIllI <seb.michalk@gmail.com>
2025-12-19 11:32:28 +0000
build.sh | 44 +++++++++++++++++ readme.txt | 26 +++++++++++ tl.nim | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..37b5eff --- /dev/null +++ b/build.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# tl - build script +# see LICENSE for copyright and license details. + +die() { + printf "error: %s\n" "$1" >&2 + exit 1 +} + +check_cmd() { + command -v "$1" >/dev/null 2>&1 || die "$1 not found" +} + +check_pkg() { + pkg-config --exists "$1" 2>/dev/null || die "$1 not found (install lib$1-dev or equivalent)" +} + +check_cmd nim +check_cmd pkg-config + +if ! nimble path x11 >/dev/null 2>&1; then + printf "x11 nim package not found, installing...\n" + check_cmd nimble + nimble install x11 -y || die "failed to install x11 nim package" +fi + +check_pkg x11 +check_pkg xft +check_pkg freetype2 +check_pkg fontconfig + +CFLAGS="-O2 -march=native" +LDFLAGS="$(pkg-config --libs x11 xft freetype2 fontconfig)" + +printf "building tl...\n" + +nim c \ + -d:release \ + --opt:size \ + --passC:"$CFLAGS" \ + --passL:"$LDFLAGS" \ + tl.nim || die "build failed" + +printf "done. run ./tl\n" diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..2f5ac74 --- /dev/null +++ b/readme.txt @@ -0,0 +1,26 @@ +tl - tiny X11 launcher + +Dependencies + - Nim compiler + - X11 development headers + - Xft, Fontconfig, FreeType (for fonts) + +Build + ./build.sh + +Run + ./tl + +Keys + Enter spawn command (execvp on PATH, no shell) + Esc quit + Backspace delete last char + +Config + Edit consts at top of tl.nim: + barH, fontName (Xft pattern), prompt, colors, buffer length. + +Notes + - Sets EWMH hints: dialog type, above, sticky (floats and stays on top). + - Centered window; fixed-size bar; no mouse support. + - No shell parsing: args/pipes need /bin/sh -c wrapping if desired. diff --git a/tl.nim b/tl.nim new file mode 100644 index 0000000..381d567 --- /dev/null +++ b/tl.nim @@ -0,0 +1,156 @@ +# tl - tinylauncher +# +# see LICENSE for copyright and license details. +# +# (c) 2025 sebastian michalk <sebastian.michalk@pm.me> + +import posix +import x11/x +import x11/xlib +import x11/xutil +import x11/keysym +import x11/xft +import x11/xrender + +# compile-time conf +const + barH: cint = 56 + fontName = "monospace:size=18" + prompt = "launch: " + maxLen = 256 + + colBg = 0xFFFFFF'u32 # white + colFg = 0x000000'u32 # black + colSel = 0xDD6666'u32 # highlight + +type + App = object + d: PDisplay + win: Window + gc: GC + xftd: PXftDraw + xftFont: PXftFont + colFgXft: XftColor + colSelXft: XftColor + buf: array[maxLen, char] + len: int + +proc die(msg: string) {.noreturn.} = + stderr.writeLine msg + quit(1) + +proc draw(app: var App) = + discard XClearWindow(app.d, app.win) + discard XSetForeground(app.d, app.gc, colBg) + let screen = DefaultScreen(app.d) + discard XFillRectangle(app.d, app.win, app.gc, 0, 0, cuint(DisplayWidth(app.d, screen)), cuint(barH)) + let baseline = barH div 2 + app.xftFont.ascent div 2 + + var pExt, iExt: XGlyphInfo + XftTextExtentsUtf8(app.d, app.xftFont, cast[PFcChar8](prompt.cstring), prompt.len.cint, addr pExt) + XftTextExtentsUtf8(app.d, app.xftFont, cast[PFcChar8](addr app.buf[0]), app.len.cint, addr iExt) + + XftDrawStringUtf8(app.xftd, addr app.colSelXft, app.xftFont, cint(8), cint(baseline), cast[PFcChar8](prompt.cstring), prompt.len.cint) + XftDrawStringUtf8(app.xftd, addr app.colFgXft, app.xftFont, cint(8 + pExt.xOff), cint(baseline), cast[PFcChar8](addr app.buf[0]), app.len.cint) + discard XFlush(app.d) + +proc spawn(cmd: cstring) = + let pid = fork() + if pid < 0: die("fork failed") + if pid == 0: + discard setsid() + let args = allocCStringArray(@[$cmd]) + discard execvp(cmd, args) + quit(1) + +proc handleKey(app: var App, e: var XKeyEvent) = + var keybuf: array[32, char] + var keysym: KeySym = 0 + let n = XLookupString(addr e, cast[cstring](addr keybuf[0]), keybuf.len.cint, addr keysym, nil) + + case keysym + of XK_Return: + if app.len > 0: + app.buf[app.len] = '\0' + spawn(cast[cstring](addr app.buf[0])) + quit(0) + of XK_Escape: + quit(0) + of XK_BackSpace: + if app.len > 0: app.len.dec + else: + if n > 0 and app.len + n < maxLen: + for i in 0 ..< n: + app.buf[app.len + i] = keybuf[i] + app.len += n + +proc setFloatingHints(app: var App) = + let atomWindowType = XInternAtom(app.d, "_NET_WM_WINDOW_TYPE".cstring, XBool(0)) + let atomDialog = XInternAtom(app.d, "_NET_WM_WINDOW_TYPE_DIALOG".cstring, XBool(0)) + let atomAtom = XInternAtom(app.d, "ATOM".cstring, XBool(0)) + if atomWindowType != 0 and atomDialog != 0 and atomAtom != 0: + var types = [atomDialog] + discard XChangeProperty(app.d, app.win, atomWindowType, atomAtom, 32, PropModeReplace, cast[cstring](addr types[0]), types.len.cint) + + let atomState = XInternAtom(app.d, "_NET_WM_STATE".cstring, XBool(0)) + let atomAbove = XInternAtom(app.d, "_NET_WM_STATE_ABOVE".cstring, XBool(0)) + let atomSticky = XInternAtom(app.d, "_NET_WM_STATE_STICKY".cstring, XBool(0)) + var states: array[2, Atom] + var count = 0 + if atomAbove != 0: + states[count] = atomAbove + inc count + if atomSticky != 0: + states[count] = atomSticky + inc count + if atomState != 0 and atomAtom != 0 and count > 0: + discard XChangeProperty(app.d, app.win, atomState, atomAtom, 32, PropModeReplace, cast[cstring](addr states[0]), count.cint) + +proc main() = + var app: App + app.d = XOpenDisplay(nil) + if app.d.isNil: die("cannot open display") + + let screen = DefaultScreen(app.d) + let sw = DisplayWidth(app.d, screen) + let sh = DisplayHeight(app.d, screen) + + let w: cuint = cuint(sw div 4) + let h: cuint = cuint(barH) + let x: cint = cint((sw - cint(w)) div 2) + let y: cint = cint((sh - cint(h)) div 2) + + app.win = XCreateSimpleWindow(app.d, RootWindow(app.d, screen), x, y, w, h, 0, colFg, colBg) + discard XSelectInput(app.d, app.win, ExposureMask or KeyPressMask) + discard XStoreName(app.d, app.win, "zmen-nim") + setFloatingHints(app) + discard XMapWindow(app.d, app.win) + + app.gc = XCreateGC(app.d, app.win, 0, nil) + let cmap = DefaultColormap(app.d, screen) + app.xftFont = XftFontOpenName(app.d, screen, fontName.cstring) + if app.xftFont.isNil: die("failed to load xft font") + if XftColorAllocName(app.d, DefaultVisual(app.d, screen), cmap, "#000000".cstring, addr app.colFgXft) == 0: + die("failed to alloc fg color") + if XftColorAllocName(app.d, DefaultVisual(app.d, screen), cmap, "#dd6666".cstring, addr app.colSelXft) == 0: + die("failed to alloc sel color") + app.xftd = XftDrawCreate(app.d, app.win, DefaultVisual(app.d, screen), cmap) + if app.xftd.isNil: die("failed to create xft draw") + + while true: + var ev: XEvent + discard XNextEvent(app.d, addr ev) + case ev.theType + of Expose: + draw(app) + of KeyPress: + var ke = cast[PXKeyEvent](addr ev) + handleKey(app, ke[]) + draw(app) + else: + discard + + discard XCloseDisplay(app.d) + +when isMainModule: + main()