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()