diff options
| -rw-r--r-- | lib/Keymap.tcl | 306 | ||||
| -rw-r--r-- | lib/terminal.tcl | 7 | ||||
| -rw-r--r-- | virtual-programs/editor.folk | 160 | ||||
| -rw-r--r-- | virtual-programs/esc-restart.folk | 2 | ||||
| -rw-r--r-- | virtual-programs/keyboard.folk | 46 | ||||
| -rw-r--r-- | virtual-programs/terminal.folk | 5 | ||||
| -rw-r--r-- | virtual-programs/web/web-keyboards.folk | 4 |
7 files changed, 325 insertions, 205 deletions
diff --git a/lib/Keymap.tcl b/lib/Keymap.tcl index 92d737e2..29ce30c3 100644 --- a/lib/Keymap.tcl +++ b/lib/Keymap.tcl @@ -1,110 +1,246 @@ -source "lib/c.tcl" - namespace eval keymap { - rename [c create] kc - - kc include <stdio.h> - kc include <stdlib.h> - kc include <string.h> - kc include <keymap.h> - kc include <kbdfile.h> - kc include <linux/keyboard.h> - - kc code { - static const char *const dirpath[] = { - ".", - "/usr/lib/kbd/keymaps/**", - "/usr/share/kbd/keymaps/**", - NULL - }; - - static const char *const suffixes[] = { - "", - ".kmap", - ".map", - NULL - }; - - typedef struct lk_ctx lkctx_t; - } + try { + # C implementation based on libkeymap + # libkeymap is part of the kbd package, but most distros don't --enable-libkeymap + c loadlibLd libkeymap.so + c loadlibLd libkbdfile.so + + rename [c create] kc + + kc include <stdio.h> + kc include <stdlib.h> + kc include <string.h> + kc include <keymap.h> + kc include <kbdfile.h> + kc include <linux/keyboard.h> + + kc code { + static const char *const dirpath[] = { + ".", + "/usr/lib/kbd/keymaps/**", + "/usr/share/kbd/keymaps/**", + NULL + }; + + static const char *const suffixes[] = { + "", + ".kmap", + ".map", + NULL + }; + + typedef struct lk_ctx lkctx_t; + } - kc argtype lkctx_t* { - lkctx_t* $argname; sscanf(Tcl_GetString($obj), "(lkctx_t*) 0x%p", &$argname); - } + kc argtype lkctx_t* { + lkctx_t* $argname; sscanf(Tcl_GetString($obj), "(lkctx_t*) 0x%p", &$argname); + } - kc rtype lkctx_t* { - $robj = Tcl_ObjPrintf("(lkctx_t*) 0x%" PRIxPTR, (uintptr_t) $rvalue); - } + kc rtype lkctx_t* { + $robj = Tcl_ObjPrintf("(lkctx_t*) 0x%" PRIxPTR, (uintptr_t) $rvalue); + } - kc proc load {char* name} lkctx_t* { - lkctx_t* ctx = lk_init(); - // @TODO assert failures - struct kbdfile_ctx* fctx = kbdfile_context_new(); - struct kbdfile* file = kbdfile_new(fctx); - kbdfile_find(name, dirpath, suffixes, file); - lk_parse_keymap(ctx, file); - lk_add_constants(ctx); - kbdfile_close(file); - kbdfile_context_free(fctx); - return ctx; - } + kc proc load {char* name} lkctx_t* { + lkctx_t* ctx = lk_init(); + // @TODO assert failures + struct kbdfile_ctx* fctx = kbdfile_context_new(); + struct kbdfile* file = kbdfile_new(fctx); + kbdfile_find(name, dirpath, suffixes, file); + lk_parse_keymap(ctx, file); + lk_add_constants(ctx); + kbdfile_close(file); + kbdfile_context_free(fctx); + return ctx; + } - kc proc resolve {lkctx_t* ctx - int key - int mods} Tcl_Obj* { - if (!lk_map_exists(ctx, mods)) return Tcl_NewStringObj("", 0); - - // see https://github.com/legionus/kbd/blob/master/src/libkeymap/dump.c#L361-L395 - int code = lk_get_key(ctx, mods, key); - char* result; - - if (KTYP(code) == KT_LETTER) { - // this key should be affected by capslock, but do we care? - code = K(KT_LATIN, KVAL(code)); - result = lk_code_to_ksym(ctx, code); - } else if (KTYP(code) == KT_META && KVAL(code) < 128) { - code = K(KT_LATIN, KVAL(code)); - - char* base = lk_code_to_ksym(ctx, code); - asprintf(&result, "Meta_%s", base); - free(base); - } else { - result = lk_code_to_ksym(ctx, code); + kc proc resolve {lkctx_t* ctx + int key + int mods} Tcl_Obj* { + if (!lk_map_exists(ctx, mods)) return Tcl_NewStringObj("", 0); + + // see https://github.com/legionus/kbd/blob/master/src/libkeymap/dump.c#L361-L395 + int code = lk_get_key(ctx, mods, key); + char* result; + + if (KTYP(code) == KT_LETTER) { + // this key should be affected by capslock, but do we care? + code = K(KT_LATIN, KVAL(code)); + result = lk_code_to_ksym(ctx, code); + } else if (KTYP(code) == KT_META && KVAL(code) < 128) { + code = K(KT_LATIN, KVAL(code)); + + char* base = lk_code_to_ksym(ctx, code); + asprintf(&result, "Meta_%s", base); + free(base); + } else { + result = lk_code_to_ksym(ctx, code); + } + + if (result == NULL) { + return Tcl_NewStringObj("", 0); + } else { + Tcl_Obj* tclres = Tcl_NewStringObj(result, -1); + free(result); + return tclres; + } } - if (result == NULL) { - return Tcl_NewStringObj("", 0); - } else { - Tcl_Obj* tclres = Tcl_NewStringObj(result, -1); - free(result); - return tclres; + kc proc dump {lkctx_t* ctx} void { + lk_dump_keymaps(ctx, stdout); + lk_dump_keys(ctx, stdout, LK_SHAPE_FULL_TABLE, 0); + } + + kc proc destroy {lkctx_t* ctx} void { + lk_free(ctx); + } + + kc compile + + namespace export * + } on error e { + # Tcl implementation using loadkeys/dumpkeys + proc _fillRange {range} { + lassign [split $range -] from to + if {$to eq ""} {return $from} + + set out "" + for {set i $from} {$i <= $to} {incr i} { + lappend out $i + } + return $out } - } - kc proc dump {lkctx_t* ctx} void { - lk_dump_keymaps(ctx, stdout); - lk_dump_keys(ctx, stdout, LK_SHAPE_FULL_TABLE, 0); + proc _parseKey {key} { + if {$key eq "nul"} {return ""} + + if {[string index $key 0] eq "+"} { + return [string range $key 1 end] + } + + return $key + } + + proc _unparseKey {key} { + if {$key eq ""} {return "nul"} + + if {[string length $key] == 1} { + return "+$key" + } + + return $key + } + + proc load {name} { + if {$::isLaptop} { + exec sudo loadkeys -C /dev/tty0 $name + set table [exec sudo dumpkeys -C /dev/tty0 -kf] + set extraArgs {-C /dev/tty0} + } else { + exec loadkeys $name + set table [exec dumpkeys -kf] + } + + set mods [_fillRange 0-15] + set codes "" + + foreach line [split $table "\n"] { + switch [lindex $line 0] { + keymaps { + set map [split [lindex $line 1] ,] + set mods [concat {*}[lmap r $map {_fillRange $r}]] + } + + keycode { + set code [lindex $line 1] + set keys [lrange $line 3 end] + set keys [lmap k $keys {_parseKey $k}] + dict append codes $code $keys + } + } + } + + return [list $mods $codes] + } + + proc dump {km} { + lassign $km mods codes + puts "keymap [join $mods ,]" + dict fir {code keys} $codes { + set keys [lmap k $keys {_unparseKey $k}] + puts "keycode $code = [join $keys \t]" + } + } + + proc resolve {km code mod} { + # @TODO: error/range handling + lassign $km mods codes + set mod [lsearch -exact $mods $mod] + return [lindex [dict get $codes $code] $mod] + } + + proc destroy {km} {} ;# for compatibility with C impl + namespace export load dump resolve destroy } - kc proc free_km {lkctx_t* ctx} void { - lk_free(ctx); + set modWeights { + Shift 1 + AltGr 2 + Control 4 + Alt 8 } - c loadlibLd libkeymap.so - c loadlibLd libkbdfile.so - kc compile + # @TODO: complete this table or find a better data source + # https://wiki.linuxquestions.org/wiki/List_of_keysyms + set remapTable { + one 1 + two 2 + three 3 + four 4 + five 5 + six 6 + seven 7 + eight 8 + nine 9 + zero 0 + apostrophe ' + igrave ` + space " " + exclam ! + quotedbl \" + sterlin ??? + dollar \$ + percent % + ampersand & + slash / + parenleft ( + parenright ) + equal = + question ? + asciicircum ^ + numbersign # + plus + + minus - + underscore _ + } + proc printable {keysym} { + variable remapTable + if {![string is lower $keysym]} {return ""} + return [dict_getdef $remapTable $keysym $keysym] + } - namespace export * + namespace export modWeights printable namespace ensemble create } if {$::argv0 eq [info script]} { set tkm [keymap load "it"] - keymap dump $tkm + # keymap dump $tkm for {set i 0} {$i < 15} {incr i} { puts "30/$i [keymap resolve $tkm 30 $i]" } - keymap free_km $tkm + puts "252/0 [keymap resolve $tkm 252 0]" + puts [join [lmap code {5 12 27} {keymap printable [keymap resolve $tkm $code 0]}] ""] + keymap destroy $tkm } diff --git a/lib/terminal.tcl b/lib/terminal.tcl index 2ab8dd2d..164cdfd6 100644 --- a/lib/terminal.tcl +++ b/lib/terminal.tcl @@ -17,7 +17,8 @@ namespace eval Terminal { LEFT "\x1b\[D" \ ] - proc _remap {key ctrlPressed} { + proc _remap {key} { + # @TODO: this doesn't take ctrlPressed anymore variable keymap if {[string length $key] == 1} { # Convert ctrl-A through ctrl-Z and others to terminal control characters @@ -46,8 +47,8 @@ namespace eval Terminal { } # Writes a keyboard key to the terminal, handling control codes - proc write {term key ctrlPressed} { - set key [_remap $key $ctrlPressed] + proc write {term key} { + set key [_remap $key] if {[string length $key] > 0} { termWrite $term $key } diff --git a/virtual-programs/editor.folk b/virtual-programs/editor.folk index 624b9387..6ac074dd 100644 --- a/virtual-programs/editor.folk +++ b/virtual-programs/editor.folk @@ -15,8 +15,8 @@ When /page/ is a keyboard with path /kbPath/ &\ Claim $id has region [region move $r up 210%] When /nobody/ claims $id has program code /c/ { Commit "code$kbPath" { - Claim $id has program code $baseCode - Claim $id has editor code $baseCode + Claim $id has program code $baseCode + Claim $id has editor code $baseCode } } @@ -191,7 +191,7 @@ proc getCurrentLineLength {lines cursor} { When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { set id "$page$kbPath" - Every time keyboard $kbPath claims key /currentCharacter/ is /keyState/ with modifiers /modifier/ timestamp /timestamp/ &\ + Every time keyboard $kbPath claims key /currentCharacter/ is /keyState/ with timestamp /timestamp/ &\ the $kbPath cursor is /cursor/ &\ $id has program code /code/ &\ $id has editor code /editorCode/ &\ @@ -199,7 +199,7 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { if {($keyState == "down" || $keyState == "repeat") & $timestamp > ($startTime * 1000) } { Commit "cursor$kbPath" { switch $currentCharacter { - UP { + Up { set updatedCursor [updateCursor $cursor {y -1}] set currentLineLength [getCurrentLineLength $editorCode $updatedCursor] if {[x $updatedCursor] > $currentLineLength} { @@ -208,7 +208,7 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim the $kbPath cursor is $updatedCursor } } - DOWN { + Down { set linecount [llength [split $editorCode "\n"]] set updatedCursor [updateCursor $cursor {y 1}] set currentLineLength [getCurrentLineLength $editorCode $updatedCursor] @@ -222,7 +222,7 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim the $kbPath cursor is $updatedCursor } } - RIGHT { + Right { set currentLineLength [getCurrentLineLength $editorCode $cursor] if {[x $cursor] == $currentLineLength} { if {[y $cursor] == [expr {[llength [split $editorCode "\n"]] - 1}]} { @@ -235,7 +235,7 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim the $kbPath cursor is [updateCursor $cursor {x 1}] } } - LEFT { + Left { if {[x $cursor] == 0 && [y $cursor] == 0} { Claim the $kbPath cursor is $cursor } elseif {[x $cursor] == 0} { @@ -247,7 +247,7 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim the $kbPath cursor is [updateCursor $cursor {x -1}] } } - BACKSPACE { + Remove { # if cursor is at the beginning of the line, delete the newline if {[x $cursor] == 0 && [y $cursor] > 0} { set newCursor [updateCursor $cursor {y -1}] @@ -262,14 +262,14 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim $id has editor code [deleteCharacter $editorCode $cursor] } } - SPACE { + space { Claim the $kbPath cursor is [updateCursor $cursor {x 1}] Commit "code$kbPath" { Claim $id has program code $code Claim $id has editor code [insertCharacter $editorCode " " $cursor] } } - ENTER { + Return { set updatedCursor [updateCursor $cursor {y 1}] Claim the $kbPath cursor is [list 0 [y $updatedCursor]] Commit "code$kbPath" { @@ -277,99 +277,77 @@ When /page/ is a keyboard with path /kbPath/ & /page/ is an editor { Claim $id has editor code [insertNewline $editorCode $cursor] } } - DELETE - - INSERT - - MUTE - - VOLUMEUP - - VOLUMEDOWN - - ESC - - TAB - - CAPSLOCK - - LEFTSHIFT - - RIGHTSHIFT - - LEFTALT - - RIGHTALT - - LEFTCTRL - - RIGHTCTRL { - # TODO: Implement DELETE, operates like BACKSPACE, but in the opposite direction - # TODO: MUTE VOLUMEUP VOLUMEDOWN - # implement sound.folk that allows a system-wide - # volume setting to be adjusted. - # Perhaps `Wish $system volume is 0.5` or something - - Claim the $kbPath cursor is $cursor - } - default { - if {$modifier == "ctrl" & $currentCharacter == "p"} { - When $id has printed /lastPrintedCode/ at /previousTime/ { - if {($timestamp - $previousTime) < 1000} { - Commit "code$kbPath" { - Claim $id has program code $code - Claim $id has editor code $editorCode - } - Claim the $kbPath cursor is $cursor - return + # TODO: Implement DELETE, operates like BACKSPACE, but in the opposite direction + # TODO: MUTE VOLUMEUP VOLUMEDOWN + # implement sound.folk that allows a system-wide + # volume setting to be adjusted. + # Perhaps `Wish $system volume is 0.5` or something + Control_p { + When $id has printed /lastPrintedCode/ at /previousTime/ { + if {($timestamp - $previousTime) < 1000} { + Commit "code$kbPath" { + Claim $id has program code $code + Claim $id has editor code $editorCode } + Claim the $kbPath cursor is $cursor } + } - Commit print { Claim $id has printed $code at $timestamp} - Wish to print $code with job-id [expr {rand()}] + Commit print { Claim $id has printed $code at $timestamp} + Wish to print $code with job-id [expr {rand()}] - Commit "code$kbPath" { - Claim $id has program code $code - Claim $id has editor code $editorCode - } - Claim the $kbPath cursor is $cursor - return + Commit "code$kbPath" { + Claim $id has program code $code + Claim $id has editor code $editorCode } - if {$modifier == "ctrl" & $currentCharacter == "r"} { - Commit "code$kbPath" { - Claim $id has program code $baseCode - Claim $id has editor code $baseCode - } - Claim the $kbPath cursor is [list 0 0] - return + Claim the $kbPath cursor is $cursor + } + Control_r { + Commit "code$kbPath" { + Claim $id has program code $baseCode + Claim $id has editor code $baseCode } - if {$modifier == "ctrl" & $currentCharacter == "s"} { - Commit "code$kbPath" { - Claim $id has program code $editorCode - Claim $id has editor code $editorCode - } - Claim the $kbPath cursor is $cursor - return + Claim the $kbPath cursor is [list 0 0] + } + Control_s { + Commit "code$kbPath" { + Claim $id has program code $editorCode + Claim $id has editor code $editorCode } - if {$modifier == "ctrl" & $currentCharacter == "a"} { - Commit "code$kbPath" { - Claim $id has program code $code - Claim $id has editor code $editorCode - } - lassign $cursor x y - Claim the $kbPath cursor is [list 0 $y] - return + Claim the $kbPath cursor is $cursor + } + Control_a { + Commit "code$kbPath" { + Claim $id has program code $code + Claim $id has editor code $editorCode } - if {$modifier == "ctrl" & $currentCharacter == "e"} { - Commit "code$kbPath" { - Claim $id has program code $code - Claim $id has editor code $editorCode - } - lassign $cursor x y - Claim the $kbPath cursor is [list [getLineLength $editorCode $cursor] $y] - return + lassign $cursor x y + Claim the $kbPath cursor is [list 0 $y] + } + Control_e { + Commit "code$kbPath" { + Claim $id has program code $code + Claim $id has editor code $editorCode } - if {$modifier == "ctrl" & $currentCharacter == "u"} { - # delete from cursor back to 0 and move cursor to 0 + lassign $cursor x y + Claim the $kbPath cursor is [list [getLineLength $editorCode $cursor] $y] + } + Control_u { + # delete from cursor back to 0 and move cursor to 0 + Commit "code$kbPath" { + Claim $id has program code $code + Claim $id has editor code [deleteToBeginning $editorCode $cursor] + } + lassign $cursor x y + Claim the $kbPath cursor is [list 0 $y] + } + default { + if {[set printable [keymap::printable $currentCharacter]] neq ""} { + Claim the $kbPath cursor is [updateCursor $cursor {x 1}] Commit "code$kbPath" { Claim $id has program code $code - Claim $id has editor code [deleteToBeginning $editorCode $cursor] + Claim $id has editor code [insertCharacter $editorCode $printable $cursor] } - lassign $cursor x y - Claim the $kbPath cursor is [list 0 $y] - return - } - Claim the $kbPath cursor is [updateCursor $cursor {x 1}] - Commit "code$kbPath" { - Claim $id has program code $code - Claim $id has editor code [insertCharacter $editorCode $currentCharacter $cursor] } } } diff --git a/virtual-programs/esc-restart.folk b/virtual-programs/esc-restart.folk index 17b56163..1be6e0c7 100644 --- a/virtual-programs/esc-restart.folk +++ b/virtual-programs/esc-restart.folk @@ -1,3 +1,3 @@ -When keyboard /k/ claims key ESC is down with modifiers alt timestamp /any/ { +When keyboard /k/ claims key Meta_Escape is down with timestamp /any/ { exec sudo systemctl restart folk } diff --git a/virtual-programs/keyboard.folk b/virtual-programs/keyboard.folk index 46b2ebf0..fa242379 100644 --- a/virtual-programs/keyboard.folk +++ b/virtual-programs/keyboard.folk @@ -42,17 +42,30 @@ proc walkInputEventPaths {} { } set keyboardDevices [walkInputEventPaths] +set globalKeymap [keymap load "us"] # go through each keyboard device and start a process that foreach keyboard $keyboardDevices { Claim the keyboards are $keyboardDevices Start process "keyboard-$keyboard" { source "lib/Keymap.tcl" - set km [keymap load "us"] set KEY_STATES [list up down repeat] Wish $::thisProcess shares statements like \ - [list keyboard /kb/ claims key /k/ is /t/ with modifiers /m/ timestamp /timestamp/] + [list keyboard /kb/ claims key /k/ is /t/ with timestamp /timestamp/] + Wish $::thisProcess receives statements like \ + [list /someone/ claims /page/ is a keyboard with path $keyboard locale /locale/] + + set ::localKeymap "" + When $keyboardSpecifier is a keyboard with path $keyboard locale /locale/ { + set ::localKeymap [keymap load $locale] + + On unmatch { + keymap destroy $::localKeymap + set ::localKeymap "" + } + } + variable evtBytes 16 variable evtFormat iissi if {[exec getconf LONG_BIT] == 64} { @@ -64,42 +77,35 @@ foreach keyboard $keyboardDevices { variable keyboardChannel [open $keyboard r] chan configure $keyboardChannel -translation binary - set modifiers [dict create \ - shift 0 \ - ctrl 0 \ - alt 0 \ - ] + set modifiers [dict map {k v} $keymap::modWeights 0] while 1 { binary scan [read $keyboardChannel $evtBytes] $evtFormat \ tvSec tvUsec type code value if {$type == 0x01} { ;# EV_KEY - set shift [dict get $modifiers shift] - set key [keymap resolve $km $code $shift] + set activeKeymap [expr {$localKeymap eq "" ? $globalKeymap : $localKeymap}] + set mods [+ {*}[dict values $modifiers]] + set key [keymap resolve $activeKeymap $code $mods] if {$key eq ""} { continue } set keyState [lindex $KEY_STATES $value] set isDown [expr {$keyState != "up"}] - if {$key eq "Shift"} { - dict set modifiers shift $isDown - } elseif {$key eq "Control"} { - dict set modifiers ctrl $isDown - } elseif {$key eq "Alt"} { - dict set modifiers alt $isDown + if {[dict exists $keymap::modWeights $key]} { + set weight [dict get $keymap::modWeights $key] + dict set modifiers $key [expr {$isDown * $weight}] } - set heldModifiers [dict keys [dict filter $modifiers value 1]] set now [clock milliseconds] - Assert keyboard $keyboardSpecifier claims key $key is $keyState with \ - modifiers $heldModifiers timestamp $now + Assert keyboard $keyboardSpecifier claims key $key is $keyState with timestamp $now + puts "Assert keyboard $keyboardSpecifier claims key $key is $keyState with timestamp $now" # Retract all key events that are more than 5 seconds old. - set events [Statements::findMatches [list keyboard $keyboardSpecifier claims key /key/ is /keyState/ with modifiers /ms/ timestamp /timestamp/]] + set events [Statements::findMatches [list keyboard $keyboardSpecifier claims key /key/ is /keyState/ with timestamp /timestamp/]] foreach event $events { dict with event { if {$now - $timestamp > 5000} { - Retract keyboard $keyboardSpecifier claims key $key is $keyState with modifiers $ms timestamp $timestamp + Retract keyboard $keyboardSpecifier claims key $key is $keyState with timestamp $timestamp } } } diff --git a/virtual-programs/terminal.folk b/virtual-programs/terminal.folk index e5de23dd..a604fbd7 100644 --- a/virtual-programs/terminal.folk +++ b/virtual-programs/terminal.folk @@ -77,10 +77,9 @@ When /anyone/ wishes /thing/ is a terminal spawning /cmd/ { } When /anyone/ claims $thing has keyboard input \ - & keyboard /anyone/ claims key /key/ is /direction/ with modifiers /modifiers/ timestamp /timestamp/ { + & keyboard /anyone/ claims key /key/ is /direction/ timestamp /timestamp/ { if {$direction != "up"} { - set ctrlPressed [expr {"ctrl" in $modifiers}] - Terminal::write $term $key $ctrlPressed + Terminal::write $term $key } } } diff --git a/virtual-programs/web/web-keyboards.folk b/virtual-programs/web/web-keyboards.folk index 2e3c5afa..b1668c69 100644 --- a/virtual-programs/web/web-keyboards.folk +++ b/virtual-programs/web/web-keyboards.folk @@ -23,7 +23,7 @@ When the keyboards are /keyboards/ { ws.onopen = () => { document.getElementById('status').innerHTML = "<span style=background-color:seagreen;color:white;>Connnected</span>"; send(` - Assert when keyboard /kb/ claims key /k/ is down with modifiers /m/ timestamp /ts/ {{chan kb k m ts} { + Assert when keyboard /kb/ claims key /k/ is down timestamp /ts/ {{chan kb k ts} { if {\$ts > [clock milliseconds]} { ::websocket::send \$chan text "\$kb||\$k" } @@ -32,7 +32,7 @@ When the keyboards are /keyboards/ { }; ws.onclose = window.onbeforeunload = () => { document.getElementById('status').innerHTML = "<span style=background-color:red;color:white;>Disconnnected</span>"; - send(`Retract when keyboard /kb/ claims key /k/ is down with modifiers /m/ timestamp /ts/ /anything/ with environment \[list \$chan]`) + send(`Retract when keyboard /kb/ claims key /k/ is down timestamp /ts/ /anything/ with environment \[list \$chan]`) setTimeout(() => { wsConnect(); }, 1000); }; ws.onerror = (err) => { |
