tl
Owner: IIIlllIIIllI URL: git@github.com:nyangkosense/tl.git
init
Commit d6dbd7ac86a356e9bc4b97e7380672615d937275 by IIIlllIIIllI <seb.michalk@gmail.com> on 2025-12-19 12:32:28 +0100
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()