;; This file is part of mowedline. ;; Copyright (C) 2011-2013 John J. Foerch ;; ;; mowedline is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; mowedline is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with mowedline. If not, see . (import chicken scheme foreign) (use srfi-1 srfi-4 ;; homogeneous numeric vectors srfi-18 ;; threads srfi-69 ;; hash tables coops data-structures (prefix dbus dbus:) extras filepath list-utils lolevel mailbox miscmacros posix regex ;; only needed for my .mowedline! xft (except xlib make-xrectangle xrectangle-x xrectangle-y xrectangle-width xrectangle-height) xtypes) (include "imperative-command-line-a") (import (prefix imperative-command-line-a icla:)) (include "llog") (import llog) (include "version") ;;; ;;; Language ;;; (define L list) (define rest cdr) ;;; ;;; Globals ;;; (define *display* #f) (define *windows* (list)) (define *widgets* (make-hash-table test: equal?)) (define *default-widgets* (list)) (define *internal-events* (make-mailbox)) ;;; ;;; Window Property Utils ;;; (define-record window-property type format data count) (define (make-atom-property atom-name) (let ((data (xinternatom *display* atom-name 0))) (let-location ((data unsigned-long data)) (make-window-property "ATOM" 32 (location data) 1)))) (define (make-number-property number) (let-location ((data unsigned-long number)) (make-window-property "CARDINAL" 32 (location data) 1))) (define (make-numbers-property numbers) (let* ((vec (list->u32vector numbers)) (len (u32vector-length vec)) (lvec ((foreign-lambda* c-pointer ((u32vector s) (int length)) "unsigned long * lvec = malloc(sizeof(unsigned long) * length);" "int i;" "for (i = 0; i < length; i++) {" " lvec[i] = s[i];" "}" "C_return(lvec);") vec len))) (set-finalizer! lvec free) (make-window-property "CARDINAL" 32 lvec len))) (define (make-text-property textp) (let ((tp (make-xtextproperty))) (set-xtextproperty-value! tp (make-locative textp)) (set-xtextproperty-encoding! tp XA_STRING) (set-xtextproperty-format! tp 32) (set-xtextproperty-nitems! tp 1) tp)) (define (window-property-set win key value) (xchangeproperty *display* win (xinternatom *display* key 0) (xinternatom *display* (window-property-type value) 0) (window-property-format value) PROPMODEREPLACE (window-property-data value) (window-property-count value))) (define (window-property-append win key value) (xchangeproperty *display* win (xinternatom *display* key 0) (xinternatom *display* (window-property-type value) 0) (window-property-format value) PROPMODEAPPEND (window-property-data value) (window-property-count value))) (define (switch-to-desktop/number desktop) (let ((root (xrootwindow *display* (xdefaultscreen *display*))) (event-mask (bitwise-ior SUBSTRUCTURENOTIFYMASK SUBSTRUCTUREREDIRECTMASK)) (event (make-xclientmessageevent))) (set-xclientmessageevent-type! event CLIENTMESSAGE) (set-xclientmessageevent-serial! event 0) (set-xclientmessageevent-send_event! event 1) (set-xclientmessageevent-display! event *display*) (set-xclientmessageevent-window! event root) (set-xclientmessageevent-message_type! event (xinternatom *display* "_NET_CURRENT_DESKTOP" 0)) (set-xclientmessageevent-format! event 32) (define make-event-data-l (foreign-lambda* void (((c-pointer long) data_l) (long l0) (long l1) (long l2) (long l3) (long l4)) "data_l[0] = l0; data_l[1] = l1;" "data_l[2] = l2; data_l[3] = l3;" "data_l[4] = l4;")) (make-event-data-l (xclientmessageevent-data-l event) desktop (current-seconds) 0 0 0) (xsendevent *display* root 0 event-mask event))) (define (switch-to-desktop desktop) (cond ((number? desktop) (switch-to-desktop/number desktop)) ((string? desktop) (let ((root (xrootwindow *display* (xdefaultscreen *display*))) (MAX_PROPERTY_VALUE_LEN 4096) (property (xinternatom *display* "_NET_DESKTOP_NAMES" 0)) (req_type (xinternatom *display* "UTF8_STRING" 0))) (let-location ((xa_ret_type unsigned-long) (ret_format int) (ret_nitems unsigned-long) (ret_bytes_after unsigned-long) (ret_prop unsigned-c-string*)) (xgetwindowproperty *display* root property 0 (/ MAX_PROPERTY_VALUE_LEN 4) 0 req_type (location xa_ret_type) (location ret_format) (location ret_nitems) (location ret_bytes_after) (location ret_prop)) (assert (= req_type xa_ret_type)) (define find-desktop (foreign-lambda* int ((unsigned-c-string target) ((c-pointer unsigned-c-string) names) (unsigned-long nitems)) "int i, d = 0, atstart = 1;" "for (i = 0; i < nitems; i++) {" " if (atstart) {" " if (0 == strcmp(target, &names[0][i]))" " C_return(d);" " atstart = 0;" " }" " if (names[0][i] == 0) {" " atstart = 1;" " d++;" " }" "}" "C_return(-1);")) (let ((desktop-number (find-desktop desktop (location ret_prop) ret_nitems))) (when (> desktop-number -1) (switch-to-desktop/number desktop-number)))))))) ;;; ;;; Button ;;; (define-record button xrectangle thunk) ;;; ;;; Window ;;; (define window-position (make-parameter 'top)) (define window-lower (make-parameter #t)) (define window-get-next-id (let ((last -1)) (lambda () (inc! last) last))) (define-class () ((id initform: (window-get-next-id)) (screen initform: (xdefaultscreen *display*)) (position initform: (window-position)) (height initform: #f) (width initform: #f) (baseline initform: #f) (widgets initform: (list)) (xwindow))) (define-method (initialize-instance (window )) (call-next-method) (for-each (lambda (widget) (widget-set-window! widget window)) (slot-value window 'widgets)) (let* ((screen (slot-value window 'screen)) (shei (xdisplayheight *display* screen)) (position (slot-value window 'position)) (width (or (slot-value window 'width) (xdisplaywidth *display* screen))) (height (or (slot-value window 'height) (fold max 1 (map widget-preferred-height (slot-value window 'widgets))))) (window-top (case position ((bottom) (- shei height)) (else 0))) (xwindow (xcreatesimplewindow *display* (xrootwindow *display* screen) 0 window-top width height 0 (xblackpixel *display* screen) (xwhitepixel *display* screen)))) (assert xwindow) (set! (slot-value window 'width) width) (set! (slot-value window 'height) height) (set! (slot-value window 'baseline) (fold max 1 (map widget-preferred-baseline (slot-value window 'widgets)))) (set! (slot-value window 'xwindow) xwindow) (for-each widget-init (slot-value window 'widgets)) (window-update-widget-dimensions! window) (let ((attr (make-xsetwindowattributes))) (set-xsetwindowattributes-background_pixel! attr (xblackpixel *display* screen)) (set-xsetwindowattributes-border_pixel! attr (xblackpixel *display* screen)) (set-xsetwindowattributes-override_redirect! attr 1) (xchangewindowattributes *display* xwindow (bitwise-ior CWBACKPIXEL CWBORDERPIXEL CWOVERRIDEREDIRECT) attr)) ;; Window Properties ;; (xstorename *display* xwindow "mowedline") (let ((p (make-xtextproperty)) (str (make-text-property (get-host-name)))) (xstringlisttotextproperty str 1 p) (xsetwmclientmachine *display* xwindow p)) (window-property-set xwindow "_NET_WM_PID" (make-number-property (current-process-id))) (window-property-set xwindow "_NET_WM_WINDOW_TYPE" (make-atom-property "_NET_WM_TYPE_DOCK")) (window-property-set xwindow "_NET_WM_DESKTOP" (make-number-property #xffffffff)) (window-property-set xwindow "_NET_WM_STATE" (make-atom-property "_NET_WM_STATE_BELOW")) (window-property-append xwindow "_NET_WM_STATE" (make-atom-property "_NET_WM_STATE_STICKY")) (window-property-append xwindow "_NET_WM_STATE" (make-atom-property "_NET_WM_STATE_SKIP_TASKBAR")) (window-property-append xwindow "_NET_WM_STATE" (make-atom-property "_NET_WM_STATE_SKIP_PAGER")) ;; Struts: left, right, top, bottom, ;; left_start_y, left_end_y, right_start_y, right_end_y, ;; top_start_x, top_end_x, bottom_start_x, bottom_end_x ;; ;; so for a top panel, we set top, top_start_x, and top_end_x. (window-property-set xwindow "_NET_WM_STRUT_PARTIAL" (make-numbers-property (if (eq? position 'bottom) (list 0 0 0 height 0 0 0 0 0 0 0 0) (list 0 0 height 0 0 0 0 0 0 0 0 0)))) (let ((d-atom (xinternatom *display* "WM_DELETE_WINDOW" 1))) (let-location ((atm unsigned-long d-atom)) (xsetwmprotocols *display* xwindow (location atm) 1))) (when (window-lower) (xlowerwindow *display* xwindow)) (push! window *windows*))) (define window-expose (case-lambda ((window xrectangle) ;; exposing a given rectangle means drawing all widgets which ;; intersect that rectangle, passing the rectangle in to them so they ;; can use it as a mask (via a region). (let ((widgets (slot-value window 'widgets)) (r (xcreateregion))) (xunionrectwithregion xrectangle (xcreateregion) r) (llog expose "window ~A, ~A" (slot-value window 'id) (xrectangle->string xrectangle)) (for-each (lambda (widget) ;; does this widget intersect xrectangle? (let* ((wrect (slot-value widget 'xrectangle)) (x (xrectangle-x wrect)) (y 0) (width (xrectangle-width wrect)) (height (slot-value window 'height))) (when (member (xrectinregion r x y width height) (L RECTANGLEPART RECTANGLEIN)) ;;intersect r with wrect and pass result to widget-draw (let ((wreg (xcreateregion)) (out (xcreateregion))) (xunionrectwithregion wrect out wreg) (xintersectregion r wreg out) (widget-draw widget out))))) widgets) ;; if the entire window is not filled with widgets, we may need to ;; clear the area to the right of the last widget. (let* ((lastwidget (last widgets)) (wrect (slot-value lastwidget 'xrectangle)) (p (+ (xrectangle-x wrect) (xrectangle-width wrect))) (m (+ (xrectangle-x xrectangle) (xrectangle-width xrectangle)))) (when (> m p) (xcleararea *display* (slot-value window 'xwindow) p 0 (- m p) (slot-value window 'height) 0))) (xflush *display*))) ((window) (window-expose window (make-xrectangle 0 0 (slot-value window 'width) (slot-value window 'height)))))) ;; window-update-widget-dimensions! sets x coordinates and widths of all ;; widgets in window. returns #f if nothing changed, otherwise an ;; xrectangle of the changed area. ;; (define (window-update-widget-dimensions! window) (let* ((widgets (slot-value window 'widgets)) (widsum 0) (flexsum 0) (wids (map ;; sum widths & flexes, and accumulate widths (lambda (widget) (and-let* ((flex (slot-value widget 'flex))) (inc! flexsum flex)) (and-let* ((wid (widget-preferred-width widget))) (inc! widsum wid) wid)) widgets)) (remainder (- (slot-value window 'width) widsum)) (flexunit (if (> flexsum 0) (/ remainder flexsum) 0)) (x 0) (rmin #f) ;; redraw range (rmax #f)) (for-each (lambda (widget wid) (let* ((rect (slot-value widget 'xrectangle)) (wid (or wid (* flexunit (slot-value widget 'flex)))) (oldx (xrectangle-x rect)) (oldwid (xrectangle-width rect))) (unless (and (= oldx x) (= oldwid wid)) (set! rmin (min (or rmin x) oldx x)) (let ((rt (+ x wid)) (oldrt (+ oldx oldwid))) (set! rmax (max (or rmax rt) oldrt rt)))) (set-xrectangle-x! rect x) (set-xrectangle-width! rect wid) (inc! x wid))) widgets wids) (if rmin (make-xrectangle rmin 0 (- rmax rmin) (slot-value window 'height)) #f))) (define (window-widget-at-position window x) (find (lambda (widget) (let ((wrect (slot-value widget 'xrectangle))) (and (>= x (xrectangle-x wrect)) (< x (+ (xrectangle-x wrect) (xrectangle-width wrect)))))) (slot-value window 'widgets))) ;;; ;;; Widgets ;;; (define-generic (widget-draw widget region)) (define-generic (widget-preferred-height widget)) (define-generic (widget-preferred-width widget)) (define-generic (widget-preferred-baseline widget)) (define-generic (widget-set-window! widget window)) (define-generic (widget-init widget)) (define-generic (widget-update widget params)) (define widget-background-color (make-parameter (list 0 0 0 1))) (define widget-flex (make-parameter #f)) (define-class () ((name initform: #f) (flex initform: (widget-flex)) (window) (xrectangle initform: (make-xrectangle 0 0 0 0)) (background-color initform: (widget-background-color)) (buttons initform: (list)))) (define-method (initialize-instance (widget )) (call-next-method) (and-let* ((name (slot-value widget 'name))) (when (hash-table-exists? *widgets* name) (error "duplicate widget name")) (hash-table-set! *widgets* name widget))) (define-method (widget-set-window! (widget ) (window )) (set! (slot-value widget 'window) window)) (define-method (widget-init (widget )) (let ((window (slot-value widget 'window))) (set-xrectangle-height! (slot-value widget 'xrectangle) (slot-value window 'height)))) (define-method (widget-preferred-baseline (widget )) 0) (define-method (widget-preferred-height (widget )) 1) (define-method (widget-preferred-width (widget )) (if (slot-value widget 'flex) #f 1)) (define (widget-button-at-position widget x) (find (lambda (button) (let ((rect (button-xrectangle button))) (and (>= x (xrectangle-x rect)) (< x (+ (xrectangle-x rect) (xrectangle-width rect)))))) (slot-value widget 'buttons))) ;; Text Widget ;; (define text-widget-font (make-parameter "mono-10:bold")) (define text-widget-color (make-parameter (list 1 1 1 1))) (define text-widget-format (make-parameter identity)) (define-class () ((text initform: "") (font initform: (text-widget-font)) (color initform: (text-widget-color)) (format initform: (text-widget-format)))) (define-method (widget-set-window! (widget ) (window )) (call-next-method) ;; initialize the font here so that the preferred height of the widget ;; can be queried as early as possible. (let ((font (slot-value widget 'font))) (when (string? font) (set! (slot-value widget 'font) (xft-font-open/name *display* (slot-value window 'screen) font))))) (define-method (widget-draw (widget ) region) (let* ((window (slot-value widget 'window)) (xwindow (slot-value window 'xwindow)) (wrect (slot-value widget 'xrectangle)) (font (slot-value widget 'font)) (x (xrectangle-x wrect)) (baseline (slot-value window 'baseline)) (visual (xdefaultvisual *display* (xdefaultscreen *display*))) (colormap (xdefaultcolormap *display* (xdefaultscreen *display*))) (draw (xftdraw-create *display* xwindow visual colormap))) (define (make-color c) (apply make-xftcolor *display* visual colormap (ensure-list c))) (set! (slot-value widget 'buttons) (list)) (let ((color (make-color (slot-value widget 'color))) (background-color (make-color (slot-value widget 'background-color)))) (xftdraw-set-clip! draw region) (xft-draw-rect draw background-color x 0 (xrectangle-width wrect) (xrectangle-height wrect)) (let walk ((term (slot-value widget 'text)) (colors (list color))) (cond ((string? term) (xft-draw-string draw font (first colors) x baseline term) (inc! x (xglyphinfo-xoff (xft-text-extents *display* font term)))) ((pair? term) (cond ((eq? 'button (first term)) (let ((thunk (second term)) (buttonx1 x)) (walk (cddr term) colors) (push! (make-button (make-xrectangle buttonx1 0 (- x buttonx1) (xrectangle-height wrect)) thunk) (slot-value widget 'buttons)))) ((eq? 'color (first term)) (walk (cddr term) (cons (make-color (second term)) colors))) (else (walk (first term) colors) (walk (rest term) colors))))))))) (define-method (widget-preferred-baseline (widget )) (xftfont-ascent (slot-value widget 'font))) (define-method (widget-preferred-height (widget )) ;; i find even common fonts extend a pixel lower than their ;; declared descent. tsk tsk. (let ((font (slot-value widget 'font))) (+ (xftfont-ascent font) (xftfont-descent font) 2))) (define-method (widget-preferred-width (widget )) (if (slot-value widget 'flex) #f (xglyphinfo-xoff (xft-text-extents *display* (slot-value widget 'font) (text-widget-raw-text widget))))) (define-method (widget-update (widget ) params) (set! (slot-value widget 'text) ((slot-value widget 'format) (first params)))) (define (text-widget-raw-text widget) (let walk ((term (slot-value widget 'text))) (cond ((null? term) "") ((string? term) term) ((and (pair? term) (memq (first term) '(color button))) (walk (cddr term))) ((pair? term) (string-append (walk (first term)) (walk (rest term)))) (else "")))) ;; Clock ;; (define (clock-thread widget) (lambda () (let loop () (let* ((time (seconds->local-time (current-seconds))) (s (vector-ref time 0))) (mailbox-send! *internal-events* (lambda () (update widget (time->string time (slot-value widget 'time-format))))) (thread-sleep! (- 60 s))) (loop)))) (define-class () ((time-format initform: "%a %b %e %H:%M %Z %Y"))) (define-method (widget-init (widget )) (call-next-method) (thread-start! (make-thread (clock-thread widget)))) ;; Flags ;; (define-class () ((flags initform: '()) (curflags initform: '()) (text initform: '()))) (define-method (widget-update (widget ) params) (let* ((changes (first params)) (op (cond ((string-prefix? "+" changes) 'add) ((string-prefix? "-" changes) 'remove) (else 'replace))) (tokenize-start (if (eq? 'replace op) 0 1)) (tokens (string-tokenize changes char-set:graphic tokenize-start))) (case op ((add) (set! (slot-value widget 'curflags) (lset-union equal? (slot-value widget 'curflags) tokens))) ((remove) (set! (slot-value widget 'curflags) (lset-difference equal? (slot-value widget 'curflags) tokens))) ((replace) (set! (slot-value widget 'curflags) tokens))) (let ((curflags (slot-value widget 'curflags))) (set! (slot-value widget 'text) ((slot-value widget 'format) (fold (lambda (flagdef result) (let ((flag (first flagdef)) (r (rest flagdef))) (if (member flag curflags) (cons r result) result))) '() (reverse (slot-value widget 'flags)))))))) ;;; ;;; Server ;;; (define (update widget-or-name . params) (llog (update "~S ~S" widget-or-name params) (and-let* ((widget (if (string? widget-or-name) (hash-table-ref/default *widgets* widget-or-name #f) widget-or-name))) (widget-update widget params) (let ((window (slot-value widget 'window))) (window-expose window (or (window-update-widget-dimensions! window) (slot-value widget 'xrectangle)))) #t))) (define (log-watch symlist . params) (for-each (lambda (x) (case (string-ref x 0) ((#\-) (llog-unwatch (string->symbol (string-drop x 1)))) ((#\+) (llog-watch (string->symbol (string-drop x 1)))) (else (llog-watch (string->symbol x))))) (string-split symlist ", ")) #t) (define bypass-startup-script (make-parameter #f)) (define (start-server commands) (set! *display* (xopendisplay #f)) (assert *display*) (let ((x-fd (xconnectionnumber *display*)) (event (make-xevent)) (done #f)) (define (quit . params) (set! done #t)) ;; process server commands (for-each (lambda (cmd) ((icla:callinfo-thunk cmd))) commands) (unless (null? *default-widgets*) (make 'widgets (reverse! *default-widgets*)) (set! *default-widgets* (list))) (if* (and (not (bypass-startup-script)) (find file-read-access? (L (filepath:join-path (L "~" ".mowedline")) (filepath:join-path (L "~" ".config" "mowedline" "init.scm"))))) (load it)) (when (null? *windows*) (make 'widgets (L (make 'name "default" 'flex 1)))) (let ((dbus-context (dbus:make-context service: 'mowedline.server interface: 'mowedline.interface))) (dbus:enable-polling-thread! enable: #f) (dbus:register-method dbus-context "update" update) (dbus:register-method dbus-context "quit" quit) (dbus:register-method dbus-context "log" log-watch)) (for-each (lambda (w) (xselectinput *display* (slot-value w 'xwindow) (bitwise-ior EXPOSUREMASK BUTTONPRESSMASK STRUCTURENOTIFYMASK)) (xmapwindow *display* (slot-value w 'xwindow)) (xnextevent *display* event) (window-expose w)) *windows*) (define (x-eventloop) (unless (> (xpending *display*) 0) (thread-wait-for-i/o! x-fd input:)) (xnextevent *display* event) (select (xevent-type event) ((CLIENTMESSAGE) (set! done #t)) ((EXPOSE) (let* ((xwindow (xexposeevent-window event)) (window (find (lambda (win) (equal? (slot-value win 'xwindow) xwindow)) *windows*)) (x (xexposeevent-x event)) (y (xexposeevent-y event)) (width (xexposeevent-width event)) (height (xexposeevent-height event))) (window-expose window (make-xrectangle x y width height)))) ((BUTTONPRESS) (and-let* ((xwindow (xexposeevent-window event)) (window (find (lambda (win) (equal? (slot-value win 'xwindow) xwindow)) *windows*)) (widget (window-widget-at-position window (xbuttonpressedevent-x event))) (button (widget-button-at-position widget (xbuttonpressedevent-x event)))) ((eval (button-thunk button)))))) (x-eventloop)) (define (dbus-eventloop) (dbus:poll-for-message) (unless done (thread-sleep! 0.01) (dbus-eventloop))) (define (internal-events-eventloop) (let loop () ((mailbox-receive! *internal-events*)) (loop))) (let ((x-thread (thread-start! x-eventloop)) (internal-events-thread (thread-start! internal-events-eventloop))) (dbus-eventloop) (thread-terminate! x-thread) (thread-terminate! internal-events-thread))) (xclosedisplay *display*)) ;;; ;;; Command Line ;;; (icla:help-heading (sprintf "mowedline version ~A, by John J. Foerch" version)) (icla:add-command-group "SERVER OPTIONS" ((q) doc: "bypass .mowedline" (bypass-startup-script #t)) ((text-widget name) (push! (make 'name name) *default-widgets*)) ((clock) (push! (make ) *default-widgets*)) ((bg color) doc: "set default background-color" (widget-background-color color)) ((fg color) doc: "set the default text color" (text-widget-color color)) ((flex value) doc: "set the default flex value" (widget-flex value)) ((position value) doc: "set the default window position (top or bottom)" (window-position (string->symbol value))) ((window) doc: "make a window containing the foregoing widgets" (make 'widgets (reverse! *default-widgets*)) (set! *default-widgets* (list)))) (let-values (((server-commands special-commands) (icla:parse (command-line-arguments) (cdr (second (icla:groups))) (cdr (first (icla:groups)))))) (cond ((not (null? special-commands)) (let ((cmd (first special-commands))) ((icla:callinfo-thunk cmd))) (unless (and (null? (rest special-commands)) (null? server-commands)) (printf "~%Warning: the following commands were ignored:~%") (for-each (lambda (x) (printf " ~S~%" (cons (icla:callinfo-name x) (icla:callinfo-args x)))) (append! (rest special-commands) server-commands)))) (else (start-server server-commands))))