An idea has long festered at the back of my mind that I might write a window manager in a scripting language. Since I am learning TCL I thought that I should finally get around to it and have a go in my new favourite language.
The code below is not (yet) that window manager. I decided that for v0.1, I should try (as a feasibility test) to implement the excellent tinywm as a TCL program. Tinywm is an attempt to write a usable, if basic, window manager in 50 lines of C.
There is more narrative needed but before boredom sets in I should probably show you some code. Keep reading further down...
# I got these from the wiki pages on combinatorial logic.
# I was almost completely lost, but the possibility of an elegant
# way of reading a file is appealing.
proc prog1 {value args} {set value;}
proc do2 {v f1 f2} {prog1 [$f1 $v] [$f2 $v];}
proc slurp {f} {do2 [open $f] read close;}
namespace eval X {
variable s;
variable screens;
variable properties;
variable ready 0;
variable queue [list];
variable request-count 0;
variable log-file;
variable errors;
variable sym-list;
# Hard-coding these values in the code would be easier
# (and possibly faster), but having them declared as variables is 'cleaner'...
array set errors {1 Request 2 Value 3 Window 4 Pixmap
5 Atom 6 Cursor 7 Font 8 Match 9 Drawable
10 Access 11 Alloc 12 Colourmap 13 GContext
14 IDChoice 15 Name 16 Length 17 Implementation};
array set keysyms [list F1 [expr 0xffbe]];
variable synchronous 0;
variable asynchronous 1;
array set mods {shift 1 lock 2 control 4 mod1 8 mod2 16 mod3 32 mod4 64
mod5 128 button1 256 button2 512 button3 1024 button4 2048 button5 4096}
array set event-masks {key-press 1 key-release 2 button-press 4 button-release 8 pointer-motion 64}
# ... and finally - some actual code...
proc dump s {
log " $s : [uplevel "set $s"]";
}
proc log s {
variable log-file;
puts $s;
puts ${log-file} $s;
}
# Pads s with enough null bytes to reach a length divisible by 4.
proc pad4 s {
upvar $s _s;
set a [expr {4-([string length $_s]%4)}];
if {$a!=4} {set _s $_s[binary format x$a];}
}
# Lazy but semi-valid way of authenticating.
# An alternative solution would be to parse the authority file by hand
# but this is fine.
proc connect {{display ""}} {
variable s;
variable screens;
variable properties;
variable log-file;
set log-file [open ~/.x11-log w+];
chan configure ${log-file} -buffering none;
if {$display==""} {set display $::env(DISPLAY);}
# Parse display.
lassign [split $display :] host temp;
lassign [split $temp .] display-no screen;
unset temp;
set s [socket localhost [expr {6000+${display-no}}]];
fconfigure $s -buffering none -blocking 0 -translation binary;
set auth [slurp "| xauth list $display"];
set connected 0;
foreach {discard auth-proto auth-data} $auth {
set auth-data [binary decode hex ${auth-data}]
log "Trying ${auth-proto}->[lindex $auth 2]";
set proto-len [string length ${auth-proto}];
set data-len [string length ${auth-data}];
pad4 auth-proto;
pad4 auth-data;
# 66 = B = MSB first. 108 = l = LSB first.
# Realistically, protocol will always be 'MIT-MAGIC-COOKIE-1 which is always of
# length 18 and the data length will always be 16, but we might as well do things properly...
set out [binary format cxSSSSx2a*a* 66 11 0 ${proto-len} ${data-len} ${auth-proto} ${auth-data}];
puts -nonewline $s $out;
# flush $s;
set start [clock milliseconds];
set waiting 1;
while {$waiting} {
set in [read $s 16384];
if {[string length $in]} {set waiting 0;}
after 5
# If used over a real network I have no idea how long to wait for a suitable timeout.
if {([clock milliseconds]-$start)>1000} {error "Connect timed out.";}
}
binary scan $in c code;
switch $code {
0 {log "X connect failed: [string range $in 8 end]";}
2 {log "X needs more authentication (unsupported): [string range $in 8 end]"; return 0;}
default {set connected 1; break;}
}
}
if {!$connected} {return 0;}
# Returned 1 - success...
# Almost none of this information is useful at this point. But knowing the vendor length and number of formats
# allows us to know the offset for the screen-list.
binary scan $in cxSSSIIIISSccccccccx4 \
code major minor additional release res-base res-mask motion-size \
vendor-len max-request screen-count format-count image-order bmp-order bmp-unit bmp-pad min-key max-key;
# This trick converts signed to unsigned.
set min-key [expr {${min-key}&0xff}];
set max-key [expr {${max-key}&0xff}];
# Min/Max key values are useful later. I have no use for the max-request size but I might as well save it.
set properties [list ${min-key} ${max-key} ${max-request}];
if {${vendor-len}%4} {incr vendor-len [expr {4-(${vendor-len}%4)}];}
set screen-start [expr {40+${vendor-len}+(8*${format-count})}];
set _screens [split-buffer [string range $in ${screen-start} [expr {${screen-start}-1+(40*${screen-count})}]] 40];
foreach scr $_screens {
binary scan $scr IIIIISS root colourmap whitep blackp current-masks width height;
lappend screens [list $root $width $height];
}
log "Connected to [string range $in 40 40+${vendor-len}] on root [lindex $screens 0 0]";
fileevent $s readable X::socket-readable;
return 1;
}
proc socket-readable "" {
receive;
}
proc send-request buffer {
variable request-count;
variable s;
incr request-count;
set reqno ${request-count};
if {${request-count}>65535} {set request-count 1; }
# log "SENDING [binary encode hex $buffer] ${request-count}";
puts -nonewline $s $buffer;
return $reqno;
}
# Just return the first item popped off of the queue.
proc get-next-item "" {
variable queue;
set ret [lindex $queue 0];
# TBD - might be a use for a K-trick here...?
if {$ret!=""} {
set queue [lreplace $queue 0 0];
}
return $ret;
}
# return (and remove from queue) the next item for the given sequence no.
proc get-next-seq-item {seq {include-events 0}} {
variable queue;
set n 0;
set found 0;
foreach i $queue {
if {([lindex $i 1]==$seq)} {
switch [lindex $i 0] {
ERROR -
REPLY {set found 1;
break;}
EVENT {if {${include-events} {set found 1; break;}}}
}
}
incr n;
}
if {$found} {
set queue [lreplace $queue $n $n];
return $i;
}
return "";
}
# The next few procedures are simply so that I can pass the correct keycode to grab-key. Since in the
# prototype there is only one key used (F1) it would be so much simpler to just use
# e.g: 'xmodmap -pk|grep F1' and hard-code the keycode (67) - but this is a useful exercise...
proc get-keymap "" {
variable properties;
variable sym-list;
# properties 0 and 1 are the min/max keys reported by the server.
lassign $properties min-key max-key;
set seq [send-request [binary format cxSccx2 101 2 ${min-key} [expr {1+${max-key}-${min-key}}]]];
receive;
lassign [get-next-seq-item $seq] type seq buffer;
if {$type!=""} {
# We have a reply...
switch $type {
REPLY {
binary scan $buffer xcSI syms-per-code seq2 sym-count;
set _syms [lmap x [split-buffer [string range $buffer 32 end] 4] {binary scan $x I item; set item;}];
# We now have a sequential list of (hundreds) of keysyms. Need to coalesce them into a list of lists.
set _syms2 [chop-list $_syms ${syms-per-code}];
# We now have the correct list, but it is offset by min-keycode positions.
set sym-list [concat [lrepeat ${min-key} {} ] $_syms2];
}
ERROR {
log "ERROR: {[parse-error $buffer]}";
exit;
}
}
}
}
# Did try to do this with lsearch but could not get that working...
proc find-keysym s {
variable sym-list;
set i 0;
foreach f ${sym-list} {
if {$s in $f} {
return $i;
}
incr i;
}
}
# Break a list into a list of smaller lists each of size n.
# e.g: chop-list {1 2 3 4 5 6} 3 -> {{1 2 3} {4 5 6}}
proc chop-list {list n} {
set ret [list];
set start 0;
set end [llength $list];
set inc [expr {$n-1}];
while {$start<$end} {
lappend ret [lrange $list $start $start+$inc];
incr start $n;
}
return $ret;
}
# Split an X11 'list' (e.g: contiguous buffer of repeated fixed-sized items) into a tcl list.
proc split-buffer {buffer size} {
set start 0;
set end [string length $buffer];
set list [list];
set inc [expr {$size-1}];
while {$start<$end} {
lappend list [string range $buffer $start $start+$inc];
incr start $size;
}
return $list;
}
proc grab-key {key mode {win ""}} {
if {$win==""} {
variable screens;
set win [lindex $screens 0 0];
}
# TBD - more params here, mask, sync modes etc...
send-request [binary format ccSIScccx3 33 1 4 $win $mode $key 1 1];
}
# configure-window is so general that I've chosen to have several separate progs
# to handle the various semantics: move,resize,move+resize,restack,set-border etc.
proc move-window {window x y} {
# 3 == set x and y
send-request [binary format cxSISx2II 12 5 $window 3 $x $y];
}
proc resize-window {window width height} {
# 12 == set height and width
send-request [binary format cxSISx2II 12 5 $window 12 $width $height];
}
proc grab-button {button owner-events modifiers event-mask grab-win {cursor 0} {pointer-mode 1} {keyboard-mode 1} {confine-win 0}} {
send-request [binary format ccSISccIIcxS 28 ${owner-events} 6 ${grab-win} ${event-mask} ${pointer-mode} ${keyboard-mode} ${confine-win} $cursor $button $modifiers];
}
proc grab-pointer {owner-events event-mask grab-win {cursor 0} {pointer-mode 1} {keyboard-mode 1} {confine-win 0} {time 0}} {
set seq [send-request [binary format ccSISccIII 26 ${owner-events} 6 ${grab-win} ${event-mask} ${pointer-mode} ${keyboard-mode} ${confine-win} $cursor $time]];
receive;
lassign [get-next-seq-item $seq] type seq buffer;
if {$type=="REPLY"} {
# 'Processs' the reply...
# binary scan $buffer xcS _status _seq ;
# ... actually there is nothing to do with the reply - we are just glad not to have an error...
} elseif {$type=="ERROR"} {
log "ERROR: {[parse-error $buffer]}";
exit;
}
}
proc ungrab-pointer "" {send-request [binary format cxSI 27 2 0];}
proc parse-event buffer {
# log "PARSE-EVENT [binary encode hex $buffer]";
binary scan $buffer ccS code detail seq;
switch $code {
default {
binary scan $buffer ccSIIIISSSSScx code detail seq f0 f1 f2 f3 f4 f5 f6 f7 f8 f9;
list $code $detail $seq $f0 $f1 $f2 $f3 $f4 $f5 $f6 $f7 $f8 $f9;
}
}
}
proc parse-error buffer {
variable errors;
binary scan $buffer xcSISc code seq data minor major;
list Code: $code ($errors($code)) Sequence: $seq Data: $data Minor: $minor Major: $major;
}
# Mode 0 = raiselowest, 1 = lowerhighest.
proc circulate-win {mode window} {
send-request [binary format ccSI 13 $mode 2 $window];}
# Read the data pending.
# Parse any errors and place on errors list.
# Parse any events and place on events list.
# Handle any reponse.
# This bad, synchronous model should ensure only one response at a time.
proc receive {{timeout 100}} {
variable s;
variable errors;
variable queue;
variable ready 0;
set ret "";
set start [clock milliseconds];
set waiting 1;
while {$waiting} {
set in [read $s 16384];
if {[string length $in]} {
set waiting 0;
} else {
if {([clock milliseconds]-$start)>$timeout} {set $waiting 0;}
}
after 5;
}
while {[string length $in]} {
binary scan $in c code;
switch $code {
0 { binary scan $in xcS code sequence;
lappend queue [list ERROR $sequence [string range $in 0 31]];
log "ERROR:[pretty-print-queue-item [lindex $queue end]]\n[parse-error [lindex $queue end 2]]";
set in [string range $in 32 end]; }
1 { binary scan $in x2SI sequence reply-len;
log "REPLY: seq $sequence\nlength ${reply-len}(x4!)=[expr ${reply-len}*4](+32 :) )"
lappend queue [list REPLY $sequence [string range $in 0 [expr {(${reply-len}*4)+31}]]];
set in [string range $in [expr {${reply-len}*4}] end]; }
default { binary scan $in x2S sequence;
lappend queue [list EVENT $sequence [string range $in 0 31]];
log "EVENT:[pretty-print-queue-item [lindex $queue end]]";
set in [string range $in 32 end]; }
}
set in [read $s 16384];
}
set ready 1;
}
# Only for debug logging...
proc pretty-print-queue-item s {
return "<[lrange $s 0 1] [binary encode hex [lindex $s 2]]>";
}
proc get-geometry w {
set seq [send-request [binary format cxSI 14 2 $w]];
receive;
lassign [get-next-seq-item $seq] type seq buffer;
if {$type=="REPLY"} {
binary scan $buffer xcSx4ISSSSS depth seq root x y width height border-width;
return [list $depth $seq $root $x $y $width $height ${border-width}];
} elseif {$type=="ERROR"} {
log "ERROR: {[parse-error [lindex $reply 2]]}";
}
}
}
# End X
# All that was X11 handling. Now we can write the WM itself.
proc window-manager "" {
if {![X::connect]} {exit 1;}
initialise;
set F1-sym [X::find-keysym $X::keysyms(F1)];
while 1 {
lassign [X::get-next-item] type seq buffer;
while {$type==""} {
set X::ready 0;
vwait X::ready;
lassign [X::get-next-item] type seq buffer;
}
set b [X::parse-event $buffer];
# I tried using variables for the switch values, but to no avail...
switch [lindex $b 0] {
4 { # Button Press
lassign $b discard detail seq time root event child root-x root-y event-x event-y state same-screen;
X::grab-pointer 1 [expr {${X::event-masks(pointer-motion)}|${X::event-masks(button-release)}}] [lindex $X::screens 0 0];
# set drag-info [list $detail $child {*}[lrange [X::get-geometry $child] 3 6] ${root-x} ${root-y}];
set drag-info [concat $detail $child [lrange [X::get-geometry $child] 3 6] ${root-x} ${root-y}];
# We will also need the offset of the click within the window. Rather
# than calculate it for each MotionNotify, just calculate it once here.
lappend drag-info [expr {${root-x}-[lindex ${drag-info} 2]}];
lappend drag-info [expr {${root-y}-[lindex ${drag-info} 3]}];
X::dump drag-info;
}
5 { # Cancel the pointer grab.
# X::log "RELEASE!";
X::ungrab-pointer;
}
6 { # Motion Notify
lassign $b discard detail seq time root event child root-x root-y event-x event-y state same-screen;
lassign ${drag-info} button window grab-x grab-y grab-width grab-height initial-x initial-y offset-x offset-y;
# X::log "MOTION $detail $root $event $child ${root-x} ${root-y} ${event-x} ${event-y} ";
if {$button==1} {
# Button 1 - drag.
X::move-window $window [expr {${root-x}-${offset-x}}] [expr {${root-y}-${offset-y}}];
} elseif {$button==3} {
# Button 3 - resize.
X::resize-window $window [expr {(${root-x}-${initial-x})+${grab-width}}] [expr {${root-y}-${initial-y}+${grab-height}}];
}
}
2 { # The only key that we grabbed was F1 with modifier 'alt' - but check anyway...
if {([lindex $b 1]==${F1-sym})&&([lindex $b 11]==$X::mods(mod1))} {
# Alt-F1
# Strictly speaking; tinywm.c used XRaiseWindow, this is more in line with tinywm.py
# which circulates windows.
X::circulate-win 0 [lindex $X::screens 0 0];
}
}
default {
X::log "WM:OTHER [X::pretty-print-queue-item [list $type $seq $buffer]]";
if {$type=="ERROR"} {X::log "ERROR: [X::parse-error $buffer]";exit;}
}
}
}
}
proc initialise "" {
variable s;
X::log " Inintialising.";
set root [lindex $X::screens 0 0];
X::get-keymap;
X::log "Grab key.";
X::grab-key [X::find-keysym $X::keysyms(F1)] $X::mods(mod1);
X::log "Grab button presses";
X::grab-button 1 0 $X::mods(mod1) ${X::event-masks(button-press)} $root;
X::grab-button 3 0 $X::mods(mod1) ${X::event-masks(button-press)} $root;
}
# Go!
window-manager;Before any flames and derision pour in I should offer a few disclaimers:
The feasibility test was desirable for me to check two things:
Now that the feasibility is complete, I will set about refactoring and adding the functionality that I actually want. I will post progress here if anyone in interested.
I hope this is of interest to someone.