aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Haberkorn <rhaberkorn@fmsbw.de>2025-10-06 00:48:48 +0300
committerRobin Haberkorn <rhaberkorn@fmsbw.de>2025-10-06 00:48:48 +0300
commitad9e7cd5117c965222aae708f660e56d537914fc (patch)
tree580006656ec76513500170e5646e92e7819c6c0b
downloadsnippets-ad9e7cd5117c965222aae708f660e56d537914fc.tar.gz
imported all of my Github gists from https://gist.github.com/rhaberkorn
-rw-r--r--52-osc.pl42
-rw-r--r--99bottles.for17
-rw-r--r--README.md4
-rw-r--r--SciTEStartup.lua98
-rw-r--r--aes.apl69
-rw-r--r--exp2ook.sc94
-rwxr-xr-xexp2ook.sno72
-rwxr-xr-xgit/git-checkout-clean.sh19
-rwxr-xr-xgit/git-fixup.sh12
-rwxr-xr-xgit/git-move-tag.sh11
-rwxr-xr-xgit/git-rename-tag.sh12
-rwxr-xr-xgmail-refresh-token.tes13
-rwxr-xr-xgtimelog-stats.lua52
-rwxr-xr-xgtimelog-stats.sno58
-rwxr-xr-xmetraLine.sh16
-rw-r--r--rearrange.for85
-rwxr-xr-xtecat.lua14
-rwxr-xr-xtecat.sh6
-rw-r--r--tests/bait.c63
-rw-r--r--tests/mouse-test.c50
-rw-r--r--tests/relocatability-test.c28
-rw-r--r--theco.rex2178
-rwxr-xr-xtimebutler.sh27
-rw-r--r--xml.sc52
24 files changed, 3092 insertions, 0 deletions
diff --git a/52-osc.pl b/52-osc.pl
new file mode 100644
index 0000000..c73c261
--- /dev/null
+++ b/52-osc.pl
@@ -0,0 +1,42 @@
+#! perl
+
+=head1 NAME
+
+52-osc - Implement OSC 52 ; Interact with X11 clipboard
+
+=head1 SYNOPSIS
+
+ urxvt -pe 52-osc
+
+=head1 DESCRIPTION
+
+This extension implements OSC 52 for interacting with system clipboard
+
+This is adapted from https://gist.github.com/ojroques/30e9ada6edd9226f9cc1d6776ece31cc
+to use the actual system clipboard instead of the urxvt selection.
+=cut
+
+use Clipboard;
+use MIME::Base64;
+use Encode;
+
+my %clip_map = ('c' => "clipboard", 'p' => "primary", 's' => "secondary");
+
+sub on_osc_seq {
+ my ($term, $op, $args) = @_;
+ return () unless $op eq 52;
+
+ my ($clip, $data) = split ';', $args, 2;
+
+ if ($data eq '?') {
+ my $data_free = $Clipboard::driver->paste_from_selection($clip_map{$clip});
+ Encode::_utf8_off($data_free); # XXX
+ $term->tt_write("\e]52;$clip;".encode_base64($data_free, '')."\a");
+ } else {
+ my $data_decoded = decode_base64($data);
+ Encode::_utf8_on($data_decoded); # XXX
+ $Clipboard::driver->copy_to_selection($clip_map{$clip}, $data_decoded);
+ }
+
+ ()
+}
diff --git a/99bottles.for b/99bottles.for
new file mode 100644
index 0000000..b9b5aec
--- /dev/null
+++ b/99bottles.for
@@ -0,0 +1,17 @@
+C 99 bottles of beer in FORTRAN 77.
+C This one is almost an obfu, but valid FORTRAN 77.
+C Demonstrated features: character format identifiers, arrays as
+C internal files, implied-DO-loops, importance of the blank character,
+C rules for continuation lines, substrings, implicit type declaration...
+565760C H ARA C T ERM S G ( 4 ) * 1 3 7
+324650WRITE(MSG,'(93H(A,/,A,A,/,A,A,A,TL37,2HA ,/,A,TL62,2HA ,34X,2HA ,2
+ 34X,33HGo to the store and buy some more,A),/,136H(I2,'' bottles of
+ F beer on the wall, '',I2,'' bottles of beer.'',/,''Take one down a
+ Rnd pass it around, '',I2,'' bottles of beer on the wall.'',/) ) ')
+347650WRITE(MSG,MSG(1))MSG(2),MSG(2)(:110),MSG(2)(112:),MSG(1)(:12),MSG(
+ 11)(1 4: 4 8),M SG(1)(5 0:1 3 6),M SG( 1)(:6 3),M S G(1)(9 6:1 3 6)
+650860P R INT MS G ( 1 ) , ( K, K , K -1, K =9 9, 3, - 1)
+323450P RI N T M S G ( 2 ) , 2 , 2 , 1
+ PR I N TM S G ( 3) , 1 , 1 , 'no more'
+023000P R I NT M S G (4 ) , 'No more' , 'no more', 9 9
+543990E N D
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c27f7b5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# Snippets
+
+This repository contains assorted scripts and effectively
+replaces my [Github gists](https://gist.github.com/rhaberkorn).
diff --git a/SciTEStartup.lua b/SciTEStartup.lua
new file mode 100644
index 0000000..82a9467
--- /dev/null
+++ b/SciTEStartup.lua
@@ -0,0 +1,98 @@
+-- Primitive syntax-aware spell checker for the SciTE editor based on aspell.
+-- It only requires the aspell CLI tool to be installed, but perhaps it won't work on
+-- Windows.
+-- You can just copy this into SciTEStartup.lua.
+--
+-- TODO: Dictionaries should be configurable per file
+-- TODO: Support named styles in aspell.styles.lexer?
+--
+-- The following should be copied into SciTEUser.properties and adapted:
+--[[
+aspell.enabled=1
+# Comma-separated list of dictionaries
+aspell.dicts=en_US
+# Restrict spell checking to certain lexer styles
+aspell.styles.lua=1,2,3
+
+command.name.9.*=Manuall Spell Checking
+command.mode.9.*=subsystem:lua,savebefore:no
+command.9.*=aspell
+command.name.10.*=Toggle Automatic Spell Checking
+command.mode.10.*=subsystem:lua,savebefore:no
+command.10.*=aspell_toggle
+]]
+
+function aspell_toggle()
+ props['aspell.enabled'] = props['aspell.enabled'] == '1' and '0' or '1'
+ if props['aspell.enabled'] == "1" then
+ aspell()
+ else
+ editor.IndicatorCurrent = 2
+ editor:IndicatorClearRange(0, editor.Length)
+ end
+end
+
+function aspell()
+ local styles = props['aspell.styles.'..editor.LexerLanguage]
+ local styles_table = {}
+ for style in styles:gmatch("%d+") do styles_table[tonumber(style)] = true end
+
+ local in_name = os.tmpname()
+ local in_file = io.open(in_name, 'w')
+ in_file:write('^')
+
+ -- NOTE: Unfortunately, SCI_GETSTYLEDTEXT (editor:GetStyledText()) is not exposed.
+ for pos = 0, editor.Length-1 do
+ local c = editor.CharAt[pos] & 0xFF
+
+ if c == 10 then
+ -- Each line is prefixed with ^ since ispell/aspell accepts special
+ -- commands as well
+ in_file:write('\n^')
+ else
+ local must_spellcheck = styles == "" or styles_table[editor.StyleAt[pos]]
+ in_file:write(must_spellcheck and string.char(c) or ' ')
+ end
+ end
+
+ in_file:close()
+
+ editor.IndicatorCurrent = 2
+ editor.IndicStyle[2] = INDIC_SQUIGGLEPIXMAP
+ editor:IndicatorClearRange(0, editor.Length)
+
+ local out = io.popen('aspell -d '..props['aspell.dicts']..
+ ' --dont-guess --dont-suggest --byte-offsets -a <'..in_name)
+
+ local line = 0
+ for cmd in out:lines() do
+ if cmd == "" then -- no more commands on the current line
+ line = line + 1
+ elseif cmd:sub(1, 1) == "#" then -- word not found
+ local word, offset = cmd:match("^. ([^ ]+) (%d+)$")
+ editor:IndicatorFillRange(editor:PositionFromLine(line)+offset-1, #word)
+ end
+ end
+
+ out:close()
+ os.remove(in_name)
+end
+
+function OnOpen()
+ if props['aspell.enabled'] == '0' then return end
+
+ -- Make sure that everything is already styled as we spell check per style:
+ editor:Colourise(0, -1)
+ aspell()
+end
+
+function OnChar(new_chr)
+ -- Called on every inserted character
+ if props['aspell.enabled'] == '0' then return end
+
+ local wordchars = editor.WordChars
+
+ -- NOTE: new_chr is a string but not Unicode-aware.
+ -- FIXME: Even better would be spell checking only chunks of recently styled text.
+ if not wordchars:find(new_chr, 1, true) then aspell() end
+end
diff --git a/aes.apl b/aes.apl
new file mode 100644
index 0000000..1edf113
--- /dev/null
+++ b/aes.apl
@@ -0,0 +1,69 @@
+⍝ AES in GNU APL
+
+⍝ Left rotate ⍺ bit
+Rot8 ← {2⊥⍺⌽(8⍴2)⊤⍵}
+⍝ Addition and subtraction in finite field GF(2)
+Add2 ← {⍺ ⊤≠ ⍵}
+⍝ Multiplication in GF(2) [x]/(x8 + x4 + x3 + x + 1)
+Mul2 ← {⊤≠/({⍵,$FF ⊤∧ ($11B×$80≤¯1↑⍵) ⊤≠ 2ׯ1↑⍵}⍣7 ⍺) × ⌽(8⍴2)⊤⍵}
+
+⍝ Multiplicative inverse, calculated by brute force
+Mul2Inv ← {$FF ⊤∧ 1⍳⍨⍵ Mul2¨⍳255}
+SBox ← {⊤≠/$63,(1-⍨⍳5) Rot8¨Mul2Inv ⍵}¨1-⍨⍳256
+InvSBox ← {Mul2Inv ⊤≠/$5,(1 3 6) Rot8¨⍵}¨1-⍨⍳256
+⍝ ⎕ ← 16 16⍴6 ⎕CR¨SBox
+⍝ ⎕ ← 16 16⍴6 ⎕CR¨InvSBox
+
+⍝ Round constants (in rows)
+Rcon ← (10 1⍴$01 $02 $04 $08 $10 $20 $40 $80 $1B $36),10 3⍴0
+
+RotWord ← {1⌽⍵}
+SubWord ← {SBox[⍵+1]} ⍝ See SubBytes
+
+⍝ Round keys based on Key (array of 8-bit integers)
+∇RoundKeys ← KeyExpansion Key; NK; NR; i
+ NK ← 4÷⍨↑⍴Key
+ ⍝ Rounds: 11 for AES-128, 13 for AES-192, 15 for AES-256 (see NIST p.18)
+ NR ← NK+6+1 ⍝ We need one key more than rounds
+ RoundKeys ← (NR×4) 4⍴Key
+ i ← 1+NK
+Loop:
+ RoundKeys[i;] ← {Rcon[⌊i÷NK;] ⊤≠ SubWord RotWord ⍵}⍣(0=NK|i-1) RoundKeys[i-1;]
+ RoundKeys[i;] ← RoundKeys[i-NK;] ⊤≠ SubWord⍣((NK>6) ∧ 4=NK|i-1) RoundKeys[i;]
+ i ← i+1
+→(i≤NR×4)/Loop
+ RoundKeys ← NR 4 4⍴RoundKeys
+∇
+
+AddRoundKey ← {⍵ ⊤≠ ⍉⍺} ⍝ This is also its inverse
+SubBytes ← {SBox[⍵+1]}
+InvSubBytes ← {InvSBox[⍵+1]}
+ShiftRows ← {⍵⌽⍨1-⍨⍳4}
+InvShiftRows ← {⍵⌽⍨1+-⍳4}
+MixColumns ← {⍵ ⊤≠.Mul2⍨ (-⍳4)⌽⍤(0 1) 3 1 1 2}
+InvMixColumns ← {⍵ ⊤≠.Mul2⍨ (-⍳4)⌽⍤(0 1) $b $d $9 $e}
+
+∇CipherText←RoundKeys Cipher PlainText; State; Round
+ State ← RoundKeys[Round←1;;] AddRoundKey ⍉4 4⍴PlainText
+ State ← {RoundKeys[Round←Round+1;;] AddRoundKey MixColumns ShiftRows SubBytes ⍵}⍣(2-⍨↑⍴RoundKeys) State
+ CipherText ← ∊⍉RoundKeys[Round+1;;] AddRoundKey ShiftRows SubBytes State
+∇
+Encrypt ← {(KeyExpansion ⍺) Cipher ⍵}
+
+∇PlainText←RoundKeys InvCipher CipherText; State; Round
+ State ← RoundKeys[Round←↑⍴RoundKeys;;] AddRoundKey ⍉4 4⍴CipherText
+ State ← {InvMixColumns RoundKeys[Round←Round-1;;] AddRoundKey InvSubBytes InvShiftRows ⍵}⍣(2-⍨↑⍴RoundKeys) State
+ PlainText ← ∊⍉RoundKeys[Round-1;;] AddRoundKey InvSubBytes InvShiftRows State
+∇
+Decrypt ← {(KeyExpansion ⍺) InvCipher ⍵}
+
+RoundKeys ← KeyExpansion ⎕UCS 13 ⎕CR '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+⍝ 5 ⎕CR RoundKeys
+
+CipherText ← RoundKeys Cipher ⎕UCS 'Hello world!!!!!'
+⎕ ← 6 ⎕CR¨CipherText
+⍝ To check the cipher text:
+⍝ echo -en 'Hello world!!!!!' | openssl enc -aes-256-ecb -nosalt -nopad -K '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4' | hexdump -C
+
+PlainText ← ⎕UCS RoundKeys InvCipher CipherText
+⎕ ← PlainText
diff --git a/exp2ook.sc b/exp2ook.sc
new file mode 100644
index 0000000..41b040f
--- /dev/null
+++ b/exp2ook.sc
@@ -0,0 +1,94 @@
+# Snocone version (obscure C-like SNOBOL preprocessor)
+
+stack = array(10); sp = 0
+
+procedure push()
+{
+ nreturn .stack[sp = sp + 1]
+}
+
+procedure pop()
+{
+ pop = stack[sp]
+ sp = sp - 1
+}
+
+procedure printstack() i
+{
+ for (i = sp, i > 0, i = i - 1)
+ output = stack[i]
+}
+
+struct op {r, type, l}
+
+procedure binop() new
+{
+ new = op(pop(), pop(), pop())
+ push() = new
+}
+
+space = span(" ") | ""
+pre = space && "(" && *exp && space && ")" | space && span("0123456789") $ *push()
+post = space && any("+-") $ *push() && *exp && *binop() |
+ space && any("*/") $ *push() && pre && *binop() && *post | ""
+exp = pre && post
+
+&anchor = 1
+
+for (i = host(3), host(2, i) ? "-", i = i + 1) {}
+if (~host(2, i)) {
+ terminal = "No expression specified!"
+ go to end
+}
+
+if (~(trim(host(2, i)) ? exp && rpos(0))) {
+ terminal = "Invalid expression!"
+ go to end
+}
+
+procedure eval(node) left, right
+{
+ if (datatype(node) :!=: .op)
+ return convert(node, "integer")
+
+ left = eval(l(node))
+ right = eval(r(node))
+
+ type(node) ? "+" && *?(eval = left + right) |
+ "-" && *?(eval = left - right) |
+ "*" && *?(eval = left * right) |
+ "/" && *?(eval = left / right)
+}
+
+#output = eval(stack[1])
+
+procedure compile(node) o
+{
+ if (datatype(node) :!=: .op)
+ return dupl("+", node)
+
+ compile = compile(l(node)) && ">" && compile(r(node))
+
+ type(node) ? any("+-") $ o && *?(compile = compile && "[<" && o && ">-]<") | # a0 += a1 | a0 -= a1
+ "*" && *?(compile = ">>" && compile && "[<[<+<+>>-]<[>+<-]>>-]<[-]<<") | # a0 *= a1
+ "/" && *?(compile = ">" && compile && "<[->->+>>+<<<[>>+>[-]<<<-]>>[<<+>>-]>[-<<[<+>-]<<<+>>>>>]<<<<]>[-]>[-]<<<") # a0 /= a1
+}
+
+src = compile(stack[1]) &&
+ "[>++++++++++<[->->+>>+<<<[>>+>[-]<<<-]>>[<<+>>-]>[-<<[<+>-]>>>+<]<<<<]>>>>>[<<<<<+>>>>>-]>[>]" &&
+ dupl("+", 48) && "[<]<<<[>>>>[>]<+[<]<<<-]<[-]<]>>>>>>[>]<[.<]"
+
+if (host(2, host(3)) :==: "-bf") {
+ output = src
+} else {
+ output(.term, 6, "t")
+
+ src ? arbno("+" && *?(term = "Ook. Ook. ") |
+ "-" && *?(term = "Ook! Ook! ") |
+ ">" && *?(term = "Ook. Ook? ") |
+ "<" && *?(term = "Ook? Ook. ") |
+ "[" && *?(term = "Ook! Ook? ") |
+ "]" && *?(term = "Ook? Ook! ") |
+ "." && *?(term = "Ook! Ook. ") |
+ len(1)) && rpos(0)
+}
diff --git a/exp2ook.sno b/exp2ook.sno
new file mode 100755
index 0000000..7590ae4
--- /dev/null
+++ b/exp2ook.sno
@@ -0,0 +1,72 @@
+#!/usr/local/bin/snobol4 -b
+
+* Arithmetic expression to Brainfuck/Ook! compiler
+* ./exp2ook.sno [-bf] "expression"
+* Compiles "expression" and prints Ook! source to stdout
+* "-bf" prints the Brainfuck source only
+* The compiled programs calculate the result and display it (ASCII letters)
+
+EXP2OOK CODE('EXP2OOK')
+ DEFINE('PUSH()')
+ DEFINE('POP()')
+ DEFINE('BINOP()NEW')
+ DEFINE('COMPILE(NODE)O')
+
+ DATA('OP(R,TYPE,L)')
+
+ &ANCHOR = 1
+ &FULLSCAN = 1
+
+ SPACE = SPAN(" ") | ""
+ PRE = SPACE ("(" *EXP SPACE ")" | SPAN("0123456789") $ *PUSH())
+ POST = SPACE (ANY("+-") $ *PUSH() *EXP *BINOP() |
++ ANY("*/") $ *PUSH() PRE *BINOP() *POST) | ""
+ EXP = PRE POST
+
+ I = HOST(3)
+L.3 HOST(2, I) "-" :F(L.4)
+ I = I + 1 :(L.3)
+L.4 HOST(2, I) :S(L.5)
+ TERMINAL = "No expression specified!" :(END)
+
+L.5 STACK = ARRAY(10)
+ SP = 0
+
+ TRIM(HOST(2, I)) EXP RPOS(0) :S(L.6)
+ TERMINAL = "Invalid expression!" :(END)
+
+L.6 SRC = COMPILE(STACK<1>)
++ "[>++++++++++<[->->+>>+<<<[>>+>[-]<<<-]>>[<<+>>-]"
++ ">[-<<[<+>-]>>>+<]<<<<]>>>>>[<<<<<+>>>>>-]>[>]" DUPL("+",ORD("0"))
++ "[<]<<<[>>>>[>]<+[<]<<<-]<[-]<]>>>>>>[>]<[.<]"
+
+ LEQ(HOST(2, HOST(3)), "-bf") :F(L.9)
+ OUTPUT = SRC :(END)
+
+L.9 OUTPUT(.TERM, 6, "T")
+ SRC ARBNO("+" *?(TERM = "Ook. Ook. ") |
++ "-" *?(TERM = "Ook! Ook! ") |
++ ">" *?(TERM = "Ook. Ook? ") |
++ "<" *?(TERM = "Ook? Ook. ") |
++ "[" *?(TERM = "Ook! Ook? ") |
++ "]" *?(TERM = "Ook? Ook! ") |
++ "." *?(TERM = "Ook! Ook. ") | LEN(1)) RPOS(0) :(END)
+
+* PROCEDURES
+
+PUSH PUSH = .STACK<SP = SP + 1> :(NRETURN)
+POP POP = STACK<SP>
+ SP = SP - 1 :(RETURN)
+
+BINOP NEW = OP(POP(), POP(), POP())
+ PUSH() = NEW :(RETURN)
+
+COMPILE LEQ(DATATYPE(NODE), .OP) :S(L.8)
+ COMPILE = DUPL("+", NODE) :(RETURN)
+L.8 COMPILE = COMPILE(L(NODE)) ">" COMPILE(R(NODE))
+ TYPE(NODE) ANY("+-") $ O *?(COMPILE = COMPILE "[<" O ">-]<") |
++ "*" *?(COMPILE = ">>" COMPILE "[<[<+<+>>-]<[>+<-]>>-]<[-]<<") |
++ "/" *?(COMPILE = ">" COMPILE "<[->->+>>+<<<[>>+>[-]<<<-]>>"
++ "[<<+>>-]>[-<<[<+>-]<<<+>>>>>]<<<<]>[-]>[-]<<<") :(RETURN)
+
+END EXP2OOK
diff --git a/git/git-checkout-clean.sh b/git/git-checkout-clean.sh
new file mode 100755
index 0000000..927b6f8
--- /dev/null
+++ b/git/git-checkout-clean.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+# This script performs a checkout with same arguments as `git checkout`
+# but makes sure that submodules are initialized in exactly the way
+# described in the branch/tag.
+# This is useful to avoid having to cleanup the tree after checkout
+# to prevent files interfering with Eclispe and updating submodules
+# manually.
+#
+# NOTE: This does not prevent other untracked files from intefering
+# with Eclipse.
+#
+# WARNING: THIS WILL DISCARD ALL LOCAL MODIFICATIONS TO YOUR SUBMODULES.
+# MAKE SURE TO COMMIT/PUSH OR SAVE THEM BEFORE YOU EXECUTE THIS SCRIPT.
+set -e
+
+git submodule foreach 'rm -rf $toplevel/$path'
+git checkout "$@"
+git submodule sync
+git submodule update --init
diff --git a/git/git-fixup.sh b/git/git-fixup.sh
new file mode 100755
index 0000000..9dfc2ee
--- /dev/null
+++ b/git/git-fixup.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# Creates a commit from the given files and automatically
+# squashes it into the last commit on HEAD.
+set -e
+
+if [ "x`git branch -r --contains HEAD`" != x ]; then
+ echo "HEAD commit already pushed?"
+ exit 1
+fi
+
+git commit --fixup=HEAD "$@"
+git rebase --autostash --autosquash HEAD~~
diff --git a/git/git-move-tag.sh b/git/git-move-tag.sh
new file mode 100755
index 0000000..05e82be
--- /dev/null
+++ b/git/git-move-tag.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+# git-move-tag <tag-name> <target>
+
+tagName=$1
+# Support passing branch/tag names (not just full commit hashes)
+newTarget=$(git rev-parse $2^{commit})
+
+git cat-file -p refs/tags/$tagName |
+sed "1 s/^object .*$/object $newTarget/g" |
+git hash-object -w --stdin -t tag |
+xargs -I {} git update-ref refs/tags/$tagName {}
diff --git a/git/git-rename-tag.sh b/git/git-rename-tag.sh
new file mode 100755
index 0000000..282a7ea
--- /dev/null
+++ b/git/git-rename-tag.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+# git-rename-tag <old-name> <new-name>
+# NOTE: This works on the "origin" remote and preserves
+# annotations.
+set -e
+
+# Creates a new tag on the remote AND removes the old tag
+git push origin refs/tags/$1:refs/tags/$2 :refs/tags/$1
+# Remove the old local tag
+git tag -d $1
+# Fetch the new tag
+git fetch origin refs/tags/$2:refs/tags/$2
diff --git a/gmail-refresh-token.tes b/gmail-refresh-token.tes
new file mode 100755
index 0000000..fc3137d
--- /dev/null
+++ b/gmail-refresh-token.tes
@@ -0,0 +1,13 @@
+#!/usr/local/bin/sciteco -8qm
+!*
+ * To update the Gmail refresh token in Aerc's accounts.conf, first run
+ * oauth2.py --generate_oauth2_token --user="user@gmail.com" --client_id="<CLIENT_ID>" --client_secret="<CLIENT_SECRET>"
+ * ./gmail-refresh-token.tes <refresh-token>
+ *!
+:Q[^A1]"< 1^C^C '
+!* URL-encode the refresh token *!
+@EQ[^A1]// <0A:; @I/%/ 0A-16"<@I/0/' 16^R0A\^D D>
+@EB'accounts.conf' HXa @E%a'accounts.conf.bak'
+@S/imaps+oauthbearer:/ @S/:/ @FK/@/ G[^A1] L
+@S/smtps+oauthbearer:/ @S/:/ @FK/@/ G[^A1]
+:EX
diff --git a/gtimelog-stats.lua b/gtimelog-stats.lua
new file mode 100755
index 0000000..abf3bff
--- /dev/null
+++ b/gtimelog-stats.lua
@@ -0,0 +1,52 @@
+#!/usr/bin/lua5.2
+-- Small helper script parsing gtimelog's timelog.txt to summarize the time spent on specific tasks.
+-- It takes a Lua pattern and optional start date and prints the sum of all timelog entries after the start date matching the pattern.
+
+local function parse_date(str)
+ local t = {hour = 0, min = 0}
+ t.day, t.month, t.year = str:match("^(%d+)%.(%d+)%.(%d+)$")
+ return os.time(t)
+end
+
+local pattern = arg[1] or ""
+local time_begin = arg[2] and parse_date(arg[2]) or 0
+local time_end = arg[3] and parse_date(arg[3]) or os.time()
+
+local log = io.open(os.getenv("HOME").."/.local/share/gtimelog/timelog.txt")
+
+local entries = {}
+for line in log:lines() do
+ local t, date_str, msg = {}
+ date_str, t.year, t.month, t.day, t.hour, t.min, msg = line:match("^((%d+)%-(%d+)%-(%d+)) (%d+):(%d+): (.*)$")
+ if msg then table.insert(entries, {date_str = date_str, time = os.time(t), msg = msg}) end
+end
+
+log:close()
+
+local sum, sum_day, arrived = 0, 0
+for i, entry in ipairs(entries) do
+ if entry.msg:sub(-2) ~= "**" and os.difftime(entry.time, time_begin) >= 0 then
+ if i > 1 and entry.date_str ~= entries[i-1].date_str and sum_day > 0 then
+ -- print stats of last day
+ -- FIXME: Does not always print last day
+ print(string.format("%s: %d hours, %d minutes, %d seconds",
+ entries[i-1].date_str,
+ sum_day / (60*60), (sum_day % (60*60))/60, sum_day % 60))
+ sum_day = 0
+ end
+
+ if entry.msg:sub(1, 7) == "arrived" then
+ arrived = true
+ elseif arrived then
+ if os.difftime(entry.time, time_end + 24*60*60) >= 0 then break end
+
+ if entry.msg:lower():find("^"..pattern) then
+ sum_day = sum_day + os.difftime(entry.time, entries[i-1].time)
+ sum = sum + os.difftime(entry.time, entries[i-1].time)
+ end
+ end
+ end
+end
+
+print(string.format("Sum: %d hours, %d minutes, %d seconds",
+ sum / (60*60), (sum % (60*60))/60, sum % 60))
diff --git a/gtimelog-stats.sno b/gtimelog-stats.sno
new file mode 100755
index 0000000..63df843
--- /dev/null
+++ b/gtimelog-stats.sno
@@ -0,0 +1,58 @@
+#!/usr/local/bin/snobol4 -b
+# ./gtimelog-stats.sno <pattern> <begin> <end>
+
+-INCLUDE 'time.sno'
+
+ input(.log, 100,, host(4, "HOME") "/.local/share/gtimelog/timelog.txt")
+ data('entry(entry_date_str,entry_date,entry_msg)')
+
+ param.pattern = eval(host(2, host(3))) :f(end)
+ param.begin = (mktime(strptime(host(2, host(3) + 1), "%d.%m.%Y")), 0)
+ param.end = (mktime(strptime(host(2, host(3) + 2), "%d.%m.%Y")),
++ tv_sec(gettimeofday()))
+* Length of day in seconds
+ day_length = 24 * 60 * 60
+
+ entries = table()
+ entries[0] = 0
+next line = log :f(next.end)
+ line pos(0) (break(" ") . date " " arb) . date_time ": " rem . msg :f(next)
+ entries[entries[0] = entries[0] + 1] = entry(date, strptime(date_time, "%Y-%m-%d %R"), msg) :(next)
+next.end
+
+ i = 2
+
+* Skip slack lines and arrived lines
+l entry_msg(entries[i]) "**" rpos(0) :s(l.c)
+* Skip entries not containing <keywords> and before <begin> or after <end>
+ lt(mktime(entry_date(entries[i])), param.begin) :s(l.c)
+
+ ident(entry_date_str(entries[i]), entry_date_str(entries[i - 1])) :s(l.f)
+ eq(stats.sum_day) :s(l.f)
+* Print stats of last day
+* FIXME: does not always print last day
+ output = entry_date_str(entries[i - 1]) ": "
++ (stats.sum_day / (60 * 60)) " hours, "
++ (remdr(stats.sum_day, 60 * 60) / 60) " minutes, "
++ remdr(stats.sum_day, 60) " seconds"
+ stats.sum_day = 0
+
+* Skip "arrived" lines
+l.f entry_msg(entries[i]) pos(0) "arrived" :s(l.c)
+
+ ge(mktime(entry_date(entries[i])), param.end + day_length) :s(print_total)
+
+ replace(entry_msg(entries[i]), &ucase, &lcase) pos(0) param.pattern :f(l.c)
+* output = tm_yday(entry_date(entries[i])) ": " mktime(entry_date(entries[i])) ": " entry_msg(entries[i])
+
+ stats.sum_day = stats.sum_day + mktime(entry_date(entries[i])) -
++ mktime(entry_date(entries[i - 1]))
+ stats.sum = stats.sum + mktime(entry_date(entries[i])) -
++ mktime(entry_date(entries[i - 1]))
+l.c le(i = i + 1, entries[0]) :s(l)
+
+print_total
+ output = "Sum: " (stats.sum / (60 * 60)) " hours, "
++ (remdr(stats.sum, 60 * 60) / 60) " minutes, "
++ remdr(stats.sum, 60) " seconds"
+end
diff --git a/metraLine.sh b/metraLine.sh
new file mode 100755
index 0000000..09af9e9
--- /dev/null
+++ b/metraLine.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# A minimal UNIX terminal interface to Metratec devices
+# Requirements: socat, rlwrap
+# To connect using a FTDI serial device node
+# (requires the `ftdi_sio` driver or a native COM/UART port), e.g.: metraLine /dev/ttyUSB0
+# To connect to a product with builtin ethernet via TCP, e.g.: metraLine 192.168.2.239
+
+if [ -c $1 ]; then
+ # Assume $1 to be a serial device node
+ ADDRESS=FILE:$1,nonblock,raw,echo=0,b115200,cs8
+else
+ # Assume $1 to be an IP and $2 to be an optional port
+ ADDRESS=TCP:$1:${2:-10001}
+fi
+
+rlwrap -rS "> " socat $ADDRESS,cr STDOUT
diff --git a/rearrange.for b/rearrange.for
new file mode 100644
index 0000000..0b72da6
--- /dev/null
+++ b/rearrange.for
@@ -0,0 +1,85 @@
+* Rearranges patches of a wave-file (shuffles them like a card deck)
+
+ program REARRANGE
+ include 'fsublib.fi'
+
+ structure /WAVEHEADER/
+ integer main_id
+ integer length
+ integer wave_id
+ integer fmt_id
+ integer fmt_length
+ integer*2 format
+ integer*2 channels
+ integer samplerate
+ integer byte_per_second
+ integer*2 samplesize
+ integer*2 bit
+ integer data_id
+ integer data_length
+ end structure
+
+ character*256 arg
+ record /WAVEHEADER/ header
+ logical exists
+ integer*2 t_h, t_m, t_s, t_ms
+ integer*1 buffer(:)
+ integer src, dst, status, size, patchsize
+
+ real length
+
+ if(iargc() .NE. 4)
+ & stop 'rearrange {infile.wav} {outfile.wav} {patchsize|0}'
+
+ call igetarg(3, arg)
+ read(arg,*) length
+
+ call igetarg(1, arg)
+ inquire(file=arg, exist=exists)
+ if(.not. exists) go to 666
+
+ open(10, file=arg, status='OLD', access='SEQUENTIAL', err=666,
+ & form='UNFORMATTED', recordtype='FIXED', action='READ')
+ read(10,err=666) header
+ header.samplesize = 2
+ size = filesize(10) - isizeof(header)
+ allocate(buffer(size), stat=status)
+ if(status .NE. 0) go to 666
+ read(10,err=666) buffer
+ close(10)
+
+ call gettim(t_h, t_m, t_s, t_ms)
+ if(length .NE. 0)
+ & patchsize = length*header.samplesize*header.samplerate
+ i = 1
+ while(i .LE. size) do
+ if(length .EQ. 0)
+ & patchsize = urand(t_ms)*header.samplesize*header.samplerate
+ if(mod(patchsize, 2)) patchsize = patchsize + 1
+ if(i + patchsize .GT. size) quit
+ k = int(urand(t_ms)*(size-patchsize)/
+ & (header.channels*header.samplesize))*
+ & header.channels*header.samplesize + 1
+ do j = 0, patchsize
+ src = buffer(i+j)
+ dst = buffer(k+j)
+ buffer(i+j) = dst
+ buffer(k+j) = src
+ end do
+ i = i + patchsize
+ end while
+
+ call igetarg(2, arg)
+ inquire(file=arg, exist=exists)
+ if(exists) call fsystem('del '//arg(:lentrim(arg)))
+ open(10, file=arg, status='NEW', access='SEQUENTIAL',err=666,
+ & form='UNFORMATTED', recordtype='FIXED', action='WRITE')
+ write(10,err=666) header
+ write(10,err=666) buffer
+ close(10)
+
+ deallocate(buffer)
+
+ stop 'Finished properly'
+666 print *, 'Some kind of error occured - but I don''t care...'
+ end
diff --git a/tecat.lua b/tecat.lua
new file mode 100755
index 0000000..e287658
--- /dev/null
+++ b/tecat.lua
@@ -0,0 +1,14 @@
+#!/usr/local/bin/lua52
+-- Replace all control characters with printable representations as in SciTECO.
+-- These characters are printed in reverse using ANSI escape sequences.
+-- This is especially useful as the diff textconv filter for Git as in
+-- git config --global diff.teco.textconv tecat
+local file = #arg > 0 and io.open(arg[1], 'rb') or io.stdin
+while true do
+ local buf = file:read(1024)
+ if not buf then break end
+ io.write((buf:gsub("[\00-\08\11\12\14-\31]", function(c)
+ c = c:byte()
+ return "\27[7m"..(c == 27 and '$' or '^'..string.char(c+0x40)).."\27[27m"
+ end)))
+end
diff --git a/tecat.sh b/tecat.sh
new file mode 100755
index 0000000..26893e8
--- /dev/null
+++ b/tecat.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+# FIXME: POSIX sed cannot replace ASCII 0 (^@).
+for c in `seq 1 8` 11 12 `seq 14 26` `seq 28 31`; do
+ patterns="$patterns`printf 's/\\\\x%x/\\\\x1B[7m^\\\\x%x\\\\x1B[27m/g;' $c $(($c|0x40))`"
+done
+exec sed 's/\x1B/\x1B[7m$\x1B[27m/g;'$patterns "$@"
diff --git a/tests/bait.c b/tests/bait.c
new file mode 100644
index 0000000..fd6081b
--- /dev/null
+++ b/tests/bait.c
@@ -0,0 +1,63 @@
+// Scintilla "Bait" example to demonstrate various problems with folding
+#include <gtk/gtk.h>
+
+#include <Scintilla.h>
+//#include <SciLexer.h>
+#define PLAT_GTK 1
+#include <ScintillaWidget.h>
+
+static int exit_app(GtkWidget*w, GdkEventAny*e, gpointer p) {
+ gtk_main_quit();
+ return w||e||p||1; // Avoid warnings
+}
+
+int main(int argc, char **argv) {
+ GtkWidget *app;
+ GtkWidget *editor;
+ ScintillaObject *sci;
+
+ gtk_init(&argc, &argv);
+ app = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ editor = scintilla_new();
+ sci = SCINTILLA(editor);
+
+ gtk_container_add(GTK_CONTAINER(app), editor);
+ g_signal_connect(app, "delete_event", G_CALLBACK(exit_app), 0);
+
+ scintilla_set_id(sci, 0);
+ //gtk_widget_set_usize(editor, 500, 300);
+
+#define SSM(m, w, l) scintilla_send_message(sci, m, w, l)
+
+ SSM(SCI_STYLESETBACK, STYLE_DEFAULT, 0x000000);
+ SSM(SCI_STYLESETFORE, STYLE_DEFAULT, 0xFFFFFF);
+ SSM(SCI_STYLECLEARALL, 0, 0);
+
+ SSM(SCI_INSERTTEXT, 0, (sptr_t)
+ "int main(int argc, char **argv) {\n"
+ " // Start up the gnome\n"
+ " gnome_init(\"stest\", \"1.0\", argc, argv);\n}"
+ );
+
+ /*
+ * FIXME: Should enable automatic folding by clicking on the margin.
+ */
+ SSM(SCI_SETAUTOMATICFOLD, SC_AUTOMATICFOLD_CLICK | SC_AUTOMATICFOLD_SHOW | SC_AUTOMATICFOLD_CHANGE, 0);
+
+ SSM(SCI_SETMARGINWIDTHN, 2, SSM(SCI_TEXTWIDTH, 33, (sptr_t)"XX"));
+ SSM(SCI_SETMARGINMASKN, 2, (1 << SC_MARKNUM_FOLDER) | (1 << SC_MARKNUM_FOLDEROPEN));
+ /*
+ * FIXME: Should inherit background color from STYLE_DEFAULT?
+ */
+ SSM(SCI_SETMARGINTYPEN, 2, SC_MARGIN_BACK);
+
+ SSM(SCI_SETFOLDLEVEL, 0, (SC_FOLDLEVELBASE) | SC_FOLDLEVELHEADERFLAG);
+ SSM(SCI_SETFOLDLEVEL, 1, (SC_FOLDLEVELBASE + 1));
+ //SSM(SCI_TOGGLEFOLD, 0, 0);
+
+ gtk_widget_show_all(app);
+ gtk_widget_grab_focus(GTK_WIDGET(editor));
+ gtk_main();
+
+ return 0;
+}
diff --git a/tests/mouse-test.c b/tests/mouse-test.c
new file mode 100644
index 0000000..c7d3e57
--- /dev/null
+++ b/tests/mouse-test.c
@@ -0,0 +1,50 @@
+// Test the ncurses mouse API
+//gcc -o mouse-test mouse-test.c -I~/working-copies/ncurses-snapshots/include ~/working-copies/ncurses-snapshots/lib/libncursesw.a
+#include <strings.h>
+
+#include <curses.h>
+#include <term.h>
+
+#define BUTTON_NUM(X) \
+ (BUTTON##X##_PRESSED | BUTTON##X##_RELEASED | \
+ BUTTON##X##_CLICKED | BUTTON##X##_DOUBLE_CLICKED | BUTTON##X##_TRIPLE_CLICKED)
+#define BUTTON_EVENT(X) \
+ (BUTTON1_##X | BUTTON2_##X | BUTTON3_##X | BUTTON4_##X | BUTTON5_##X)
+
+int main(void)
+{
+ initscr();
+ raw();
+ noecho();
+ scrollok(stdscr, TRUE);
+
+ keypad(stdscr, TRUE);
+ mouseinterval(0);
+ mousemask(BUTTON_EVENT(PRESSED) | BUTTON_EVENT(RELEASED) /*| REPORT_MOUSE_POSITION*/, NULL);
+// mousemask(ALL_MOUSE_EVENTS, NULL);
+
+ for (;;) {
+ MEVENT event;
+ int c = wgetch(stdscr);
+
+ if (c == '\e')
+ break;
+ if (c != KEY_MOUSE)
+ continue;
+
+ while (getmouse(&event) == OK) {
+ printw("EVENT: 0x%016X == %02d [%c%c%c%c%c%c]\n",
+ event.bstate, ffs(event.bstate)-1,
+ event.bstate & BUTTON_NUM(4) ? 'U' : ' ',
+ event.bstate & BUTTON_NUM(5) ? 'D' : ' ',
+ event.bstate & BUTTON_EVENT(PRESSED) ? 'P' : ' ',
+ event.bstate & BUTTON_EVENT(RELEASED) ? 'R' : ' ',
+ event.bstate & BUTTON_EVENT(CLICKED) ? 'C' : ' ',
+ event.bstate & REPORT_MOUSE_POSITION ? 'M' : ' ');
+ }
+ }
+
+ endwin();
+
+ return 0;
+}
diff --git a/tests/relocatability-test.c b/tests/relocatability-test.c
new file mode 100644
index 0000000..ef970bc
--- /dev/null
+++ b/tests/relocatability-test.c
@@ -0,0 +1,28 @@
+/*
+ * This prints the location of the current binary as
+ * reported by the dynamic linker.
+ * It does not work on all UNIX-like platforms.
+ * For instance, it doesn't on Linux.
+ *
+ * Link with: cc -o relocatability-test relocatability-test.c -ldl
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <dlfcn.h>
+
+int
+main(int argc, char **argv)
+{
+ Dl_info info;
+
+ printf("argv[0] = %s\n", argv[0]);
+
+ if (!dladdr(main, &info)) {
+ fprintf(stderr, "Cannot query executable's path: %s\n",
+ dlerror());
+ return 1;
+ }
+
+ printf("dli_fname = %s\n", info.dli_fname);
+ return 0;
+}
diff --git a/theco.rex b/theco.rex
new file mode 100644
index 0000000..984a799
--- /dev/null
+++ b/theco.rex
@@ -0,0 +1,2178 @@
+#!/usr/local/bin/nthe -p
+/*
+ * Abandoned (Video)TECO implementation on top of The Hessling Editor, written in Open Object Rexx
+ */
+
+if .environment~theco.initialized \= .nil then return
+
+/*
+ * Initialize classic Rexx function packages
+ */
+call ReLoadFuncs
+
+/*
+ * THE profile code: THECO initialization
+ */
+keys = .Array~of(-
+ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",-
+ "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",-
+ "U", "V", "W", "X", "Y", "Z", "S-A", "S-B", "S-C", "S-D",-
+ "S-E", "S-F", "S-G", "S-H", "S-I", "S-J", "S-K", "S-L", "S-M", "S-N",-
+ "S-O", "S-P", "S-Q", "S-R", "S-S", "S-T", "S-U", "S-V", "S-W", "S-X",-
+ "S-Y", "S-Z", "0", "1", "2", "3", "4", "5", "6", "7",-
+ "8", "9", "`", "-", "=", "[", "]", "\", ";", "'",-
+ ",", ".", "/", ")", "!", "@", "#", "$", "%",-
+ "^", "&", "*", "(", "~", "_", "+", "{", "}", "|",-
+ ":", '"', "<", ">", "?", "SPACE", "ESC", "F0", "F1", "F2", "F3",-
+ "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "S-F1",-
+ "S-F2", "S-F3", "S-F4", "S-F5", "S-F6", "S-F7", "S-F8", "S-F9", "S-F10", "S-F11",-
+ "S-F12", "C-F1", "C-F2", "C-F3", "C-F4", "C-F5", "C-F6", "C-F7", "C-F8", "C-F9",-
+ "C-F10", "C-F11", "C-F12", "A-F1", "A-F2", "A-F3", "A-F4", "A-F5", "A-F6", "A-F7",-
+ "A-F8", "A-F9", "A-F10", "A-F11", "A-F12", "F13", "F14", "F15", "F16", "F17",-
+ "F18", "F19", "F20", "S-F13", "S-F14", "S-F15", "S-F16", "S-F17", "S-F18", "S-F19",-
+ "NUM0", "NUM1", "NUM2", "NUM3", "NUM4", "CENTER", "NUM6", "NUM7", "NUM8", "NUM9",-
+ "CURU", "CURD", "CURL", "CURR", "ENTER", "ENTER", "TAB", "HOME", "PGUP",-
+ "PGDN", "END", "INS", "DEL", "PLUS", "MINUS", "SLASH", "STAR", "NUMENTER", "NUMSTOP",-
+ "S-HOME", "S-END", "S-CURL", "S-CURR",-
+ "C-TAB", "C-HOME", "C-PGUP", "C-PGDN", "C-CURU", "C-CURD",-
+ "C-CURL", "C-CURR", "C-END",-
+ "S-TAB", "S-INS", "SELECT", "PRINT", "S-PRINT", "FIND", "S-FIND", "SUSPEND", "S-SUSPEND",-
+ "CLEAR", "OPTIONS", "S-OPTIONS", "BREAK", "CANCEL", "S-CANCEL", "HELP", "S-HELP", "S-TAB",-
+ "S-INS", "EXIT", "CURD",-
+ "CURU", "CURL", "CURR", "BACKSPACE", "HOME", "PF1", "PF2", "PF3", "PF4", "NUMENTER",-
+ "MINUS", "NUMSTOP", "COMMA", "STAR", "PLUS", "SLASH", "S-TAB", "FIND", "INS", "REMOVE",-
+ "DEL", "SELECT", "PGUP", "PGDN", "TAB", "ENTER", "TAB", "RETURN", "CSI", "BREAK",-
+ "DL", "IL", "DC", "INS", "EIC", "CLEAR", "EOS", "EOL", "SF", "SR",-
+ "PGDN", "PGUP", "S-TAB", "C-TAB", "CATAB", "ENTER", "S-RESET", "RESET", "PRINT", "LL",-
+ "A1", "A3", "B2", "C1", "C3", "S-TAB", "BEG", "CANCEL", "CLOSE", "COMMAND",-
+ "COPY", "CREATE", "END", "EXIT", "FIND", "HELP", "MARK", "MESSAGE", "MOVE", "NEXT",-
+ "OPEN", "OPTIONS", "PREVIOUS", "REDO", "REFERENCE", "REFRESH", "REPLACE", "RESTART", "RESUME", "SAVE",-
+ "S-BEG", "S-CANCEL", "S-COMMAND", "S-COPY", "S-CREATE", "S-DC", "S-DL", "SELECT", "S-END", "S-EOL",-
+ "S-EXIT", "S-FIND", "S-HELP", "S-HOME", "S-INS", "S-CURL", "S-MESSAGE", "S-MOVE", "S-NEXT", "S-OPTIONS",-
+ "S-PREVIOUS", "S-PRINT", "S-REDO", "S-REPLACE", "S-CURR", "S-RSUME", "S-SAVE", "S-SUSPEND", "S-UNDO", "SUSPEND",-
+ "UNDO", "C-CURL", "C-CURR", "C-CURU", "C-CURD", "C-HOME", "C-END", "C-PGUP", "C-PGDN", "C-A",-
+ "C-B", "C-C", "C-D", "C-E", "C-F", "C-G", "C-H", "C-I", "C-J", "C-K",-
+ "C-L", "C-M", "C-N", "C-O", "C-P", "C-Q", "C-R", "C-S", "C-T", "C-U",-
+ "C-V", "C-W", "C-X", "C-Y", "C-Z",-
+)
+'set msgmode off'
+do key over keys
+ 'define' key 'rexx call theco_keypress' stringify(key)';',
+ '::requires "theco"'
+end
+'set msgmode on'
+
+'set cmdline off'
+'set insertmode on'
+
+/*
+ * configurable by THECO macro
+ */
+'color filearea white black'
+'color pr green black'
+'color cpr black green'
+'color arrow green black'
+'color st black white'
+'color to bold green black'
+'color cto bold black green'
+'color divider black white'
+'color idline black white'
+'color scale green black'
+'color cur reverse'
+
+'ecolor b yellow black'
+'ecolor s white black'
+'ecolor f bright cyan on black'
+'ecolor i magenta on black'
+'ecolor c bright blue on black'
+'ecolor d bright green on black'
+'ecolor a blue on black'
+'ecolor x magenta on black'
+'ecolor 5 red on black'
+'ecolor 2 bright blue on black'
+'ecolor 6 bright green on black'
+'ecolor y bright green on black'
+'ecolor w bright red on black'
+
+'set beep on'
+'set insertmode on'
+/*'reprofile on'*/
+/* NOTE: currently broken on THE v3.3 RC1? */
+'set tabkey tab character'
+
+/*
+ * NOTE: control chars broken on THE v3.3 RC1
+ * NOTE: setting excape char yields error
+ * WORKAROUND: reset attribs after escaping the escape char
+ * WORKAROUND: disable messages for setting escape char
+ */
+'nomsg set ctlchar \ escape'
+'set ctlchar N protect normal'
+'set ctlchar R protect reverse'
+
+.environment~theco.cmdline = ""
+.environment~theco.undo = .UndoStackDummy~new
+.environment~theco.quit_requested = .false
+
+.environment~theco.escape = '1B'x
+.environment~theco.modifiers.at = .false
+.environment~theco.modifiers.colon = .false
+
+'set reserved -1' echo_cmdline(.environment~theco.cmdline)
+
+/*
+ * Parser state machine
+ */
+s = .Table~new
+s["start"] = .StateStart~new
+s["start"][""] = "start"
+s["start"][" "] = "start"
+s["start"]['0D'x] = "start"
+s["start"]['0A'x] = "start"
+s["start"]["!"] = "label"
+s["start"]["^"] = "ctlcmd"
+s["start"]["F"] = "fcmd"
+s["start"]['"'] = "condcmd"
+s["start"]["O"] = "cmd_goto"
+s["start"]["Q"] = "qcmd"
+s["start"]["U"] = "ucmd"
+s["start"]["%"] = "inccmd"
+s["start"]["M"] = "mcmd"
+s["start"]["E"] = "ecmd"
+s["start"]["I"] = "cmd_insert"
+s["start"]["S"] = "cmd_search"
+
+s["label"] = .StateLabel~new
+s["label"][""] = "label"
+
+s["ctlcmd"] = .StateCtlCmd~new
+s["ctlcmd"][""] = "ctlcmd"
+s["ctlcmd"]["U"] = "ctlucmd"
+
+s["ctlucmd"] = .StateCtlUCmd~new
+s["ctlucmd"][""] = "ctlucmd"
+
+s["cmd_ctlu"] = .StateCmdCtlU~new
+
+s["fcmd"] = .StateFCmd~new
+s["fcmd"][""] = "fcmd"
+
+s["condcmd"] = .StateCondCmd~new
+s["condcmd"][""] = "condcmd"
+
+s["cmd_goto"] = .StateCmdGoto~new
+
+s["qcmd"] = .StateQCmd~new
+s["qcmd"][""] = "qcmd"
+
+s["ucmd"] = .StateUCmd~new
+s["ucmd"][""] = "ucmd"
+
+s["inccmd"] = .StateIncCmd~new
+s["inccmd"][""] = "inccmd"
+
+s["mcmd"] = .StateMCmd~new
+s["mcmd"][""] = "mcmd"
+
+s["ecmd"] = .StateECmd~new
+s["ecmd"][""] = "ecmd"
+s["ecmd"]["B"] = "cmd_file"
+s["ecmd"]["Q"] = "eqcmd"
+
+s["cmd_file"] = .StateCmdFile~new
+
+s["eqcmd"] = .StateEQCmd~new
+s["eqcmd"][""] = "eqcmd"
+
+s["cmd_insert"] = .StateCmdInsert~new
+
+s["cmd_search"] = .StateCmdSearch~new
+
+.environment~theco.states = s
+.environment~theco.state = s["start"]
+
+/*
+ * Operator precedence table
+ * "=" is not a real operator and excluded from comparisons
+ */
+operators = .Array~of("^*","*","/","^/","+","-","&","#","(","<")
+.ArithmeticStack~precedence = .Table~new
+do i = 1 to operators~items
+ .ArithmeticStack~precedence[operators[i]] = i
+end
+.ArithmeticStack~precedence[.nil] = i
+
+.ArithmeticStack~operators = .Operators~new
+
+.environment~theco.stack = .ArithmeticStack~new
+
+.environment~theco.reg_arg = .nil
+/*
+ * Strings (for storing string arguments)
+ */
+.environment~theco.strings.1 = ""
+.environment~theco.strings.2 = ""
+
+/*
+ * Q-Registers
+ */
+.environment~theco.registers = .Table~new
+do c = "A"~c2d to "Z"~c2d
+ .environment~theco.registers[c~d2c] = .QRegister~new(c~d2c)
+end
+do c = 0 to 9
+ .environment~theco.registers[c] = .QRegister~new(c)
+end
+/* search string & status (examined by ";" command) */
+.environment~theco.registers["_"] = .QRegister~new("_")
+.environment~theco.registers["_"]~integer = 0 /* failure */
+
+/*
+ * THECO labels mapped to program counters
+ */
+.environment~theco.goto_table = .Table~new
+
+.environment~theco.pc = 0
+.environment~theco.exec = .true
+.environment~theco.skip_else = .false
+.environment~theco.skip_label = .nil
+.environment~theco.nest_level = 0
+
+'locate 1'
+
+/*
+ * Execute TECO.INI
+ */
+input = .Stream~new("teco.ini")
+input~open("read")
+
+if \execute(input~charIn(1, input~chars)) then do
+ say "Error executing teco.ini"
+ return
+end
+
+input~close
+
+.environment~theco.pc = 0
+.environment~theco.undo = .UndoStack~new
+
+.environment~theco.initialized = .true
+
+/*
+ * Main entry point, called on key press
+ */
+::routine theco_keypress public
+ use arg key_the
+
+ /*
+ * Translate THE key to TECO ASCII char
+ */
+ select
+ when lastkey.2() \== "" then
+ key_char = lastkey.2()~d2c
+ when key_the == "BACKSPACE" then
+ key_char = '08'x
+ when key_the == "DC" then
+ /* FIXME: preliminary escape surrogate */
+ key_char = '1B'x
+ otherwise
+ 'emsg WARNING: Unresolved key' key_the 'ignored'
+ return
+ end
+
+ cmdline = sor(.environment~theco.cmdline, "")
+
+ /*
+ * Process immediate editing commands
+ */
+ insert = ""
+ select
+ when key_the == "BACKSPACE" then do
+ .environment~theco.undo~pop(cmdline~length)
+ cmdline = cmdline~left(max(cmdline~length - 1, 0))
+ .environment~theco.pc = cmdline~length
+ end
+ when key_the == "C-T" | key_char == '09'x,-
+ .environment~theco.state~name == "cmd_file" then do
+ filename = .environment~theco.strings.1
+ insert = filename_complete(filename, .environment~theco.escape)
+ end
+ when key_the == "C-T" then do
+ start = last_match(cmdline, '0D 0A 09'x "<>,;@") + 1
+ insert = filename_complete(cmdline~substr(start))
+ end
+ otherwise
+ insert = key_char
+ end
+
+ old_cmdline = .environment~theco.cmdline
+ .environment~theco.cmdline = cmdline
+
+ /*
+ * Parse/execute characters
+ */
+ do insert_index = 1 to insert~length
+ cmdline ||= insert~subchar(insert_index)
+ .environment~theco.cmdline = cmdline
+
+ if \execute(cmdline) then do
+ .environment~theco.cmdline = old_cmdline
+ leave insert_index
+ end
+ end
+
+ /*
+ * Echo command line
+ */
+ 'set reserved -1' echo_cmdline(sor(.environment~theco.cmdline, ""))
+
+/*
+ * Parse/execute
+ */
+::routine execute
+ use arg code
+
+ do while .environment~theco.pc < code~length
+ .environment~theco.pc += 1
+
+ c = code~subchar(.environment~theco.pc)
+ if \.State~input(c) then do
+ .environment~theco.pc -= 1 /* FIXME */
+
+ 'emsg Syntax error "'c'"'
+ return .false
+ end
+ end
+
+ return .true
+
+/*
+ * Return cmdline in as a reserved line string (for echoing)
+ */
+::routine echo_cmdline
+ use arg cmdline
+
+ /* FIXME : could use CHANGESTR() */
+ line = ""
+ do i = 1 to cmdline~length
+ c = cmdline~subchar(i)
+
+ select
+ when c == "\" then
+ line ||= "\\N"
+ when c == '1B'x then
+ line ||= "$"
+ when c == '0D'x then
+ line ||= "<CR>"
+ when c == '0A'x then
+ line ||= "<LF>"
+ when c == '09'x then
+ line ||= "<TAB>"
+ when c~c2d < 32 then
+ line ||= "^"ctlecho(c)
+ otherwise
+ line ||= c
+ end
+ end
+
+ half_line = (lscreen.2() - 2) % 2
+ line = line~right(min(line~length,-
+ half_line + line~length // half_line))
+
+ return "*"line"\R "
+
+/*
+ * Complete filename/path (used for autocompletions)
+ */
+::routine filename_complete
+ use arg filename, completed=" "
+
+ /*
+ * Do not complete match specs
+ */
+ if is_matchspec(filename) then return ""
+
+ /*
+ * Get all files/directorie beginning with `filename`
+ */
+ if SysFileTree(filename"*", "matching.") \= 0 then return ""
+ if matching.0 = 0 then return ""
+
+ complete_chars = filespec("name", filename)~length
+
+ /*
+ * Complete the entire filename if possible
+ */
+ matching.1 = get_real_filename(matching.1)
+
+ if matching.0 = 1 then do
+ if matching.1~right(1) \== get_path_sep() then
+ matching.1 ||= completed
+ return matching.1~substr(complete_chars + 1)
+ end
+
+ /*
+ * Find the longest common prefix of all matching files/directories
+ * and complete it
+ */
+ longest_prefix = matching.1~length
+ longest_file = matching.1~length
+
+ do i = 2 to matching.0
+ matching.i = get_real_filename(matching.i)
+
+ longest_prefix = min(longest_prefix,-
+ matching.i~compare(matching.1) - 1)
+ longest_file = max(longest_file, matching.i~length)
+ end
+
+ if longest_prefix > complete_chars then
+ return matching.1~left(longest_prefix),
+ ~substr(complete_chars + 1)
+
+ /*
+ * If no completion is possible, display all matching files
+ */
+ if SysStemSort("matching.") \= 0 then return ""
+
+ screen_width = lscreen.2()
+ col_length = min(longest_file + 3, screen_width)
+
+ old_msglines = msgline.3()
+ 'set msgline on = * ='
+
+ line = ""
+ do i = 1 to matching.0
+ if line~length + col_length > screen_width then do
+ 'msg' line
+ line = ""
+ end
+ line ||= matching.i~left(col_length)
+ end
+ 'msg' line
+
+ 'set msgline on =' old_msglines '='
+ return ""
+
+::class UndoToken
+ ::attribute pos
+ ::attribute code
+
+ ::method INIT
+ use arg self~pos, self~code
+
+ ::method run
+ interpret self~code
+
+::class UndoStack subclass Queue
+ ::method push
+ use arg code
+
+ token = .UndoToken~new(.environment~theco.cmdline~length, code)
+ self~push:super(token)
+
+ ::method push_cmd
+ use arg cmd
+
+ self~push(stringify(cmd))
+
+ ::method pop
+ use arg pos
+
+ do while self~peek \= .nil, self~peek~pos = pos
+ self~pull~run
+ end
+
+/*
+ * Undo stack dummy implementation - use when rubout is not required
+ */
+::class UndoStackDummy
+ ::method push
+ ::method push_cmd
+ ::method pop
+
+/*
+ * Class implementing THECO operators, dy default forwarded to the String class
+ * (by default THECO operator equals Rexx operator)
+ */
+::class Operators
+ ::method "/"
+ return arg(1) % arg(2)
+ ::method "&"
+ return arg(1)~d2c~bitAnd(arg(2)~d2c)~c2d
+ ::method "#"
+ return arg(1)~d2c~bitOr(arg(2)~d2c)~c2d
+ ::method "^*"
+ return arg(1) ** arg(2)
+ ::method "^/"
+ return arg(1) // arg(2)
+
+ ::method UNKNOWN
+ use arg name, arguments
+
+ return arguments[1]~send(name, arguments[2])
+
+::class ArithmeticStack
+ ::attribute precedence class
+ ::attribute operators class
+
+ /* special value "" is pushed by "," and means: no argument (yet) */
+ ::attribute nums
+ ::attribute ops
+
+ ::attribute num_sign
+ ::attribute radix
+
+ ::method INIT
+ self~nums = .Queue~new
+ self~ops = .Queue~new
+
+ self~num_sign = 1
+ self~radix = 10
+
+ ::method set_radix
+ use arg radix
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~radix =" self~radix-
+ )
+ self~radix = radix
+
+ ::method push_num
+ do while self~nums~peek = ""
+ self~pop_num
+ end
+
+ self~push_op("=")
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~nums~pull"-
+ )
+ forward message "push" to (self~nums)
+
+ ::method pop_num
+ use arg index=1
+
+ n = self~~pop_op~nums~remove(index)
+ if n \= .nil then
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~nums~insert('"n"',"-
+ isor(index - 1, ".nil")")"-
+ )
+
+ return n
+
+ ::method pop_num_calc
+ use arg index=1, imply=(self~num_sign)
+
+ n = ""
+ if self~~eval~args > 0 then
+ n = self~pop_num(index)
+ if n == "" then
+ n = imply
+
+ if self~num_sign < 0 then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~num_sign = -1"-
+ )
+ self~num_sign = 1
+ end
+
+ return n
+
+ ::method add_digit
+ use arg digit
+
+ n = ""
+ if self~args > 0 then
+ n = self~pop_num
+
+ self~push_num(sor(n, 0)*self~radix + self~num_sign*digit)
+
+ ::method push_op
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~ops~pull"-
+ )
+ forward message "push" to (self~ops)
+
+ ::method push_op_calc
+ use arg op
+
+ /* calculate if op has lower precedence than op on stack */
+ if .ArithmeticStack~precedence[self~ops[self~first_op]] <=,
+ .ArithmeticStack~precedence[op] then self~calc
+ self~push_op(op)
+
+ ::method pop_op
+ use arg index=1
+
+ o = self~ops~remove(index)
+ if o \= .nil then
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~ops~insert('"o"',"-
+ isor(index - 1, ".nil")")"-
+ )
+
+ return o
+
+ ::method calc
+ vright = self~pop_num
+ op = self~pop_op
+ vleft = self~pop_num
+
+ self~push_num(-
+ .ArithmeticStack~operators~send(op, vleft, vright)-
+ )
+
+ ::method eval
+ use arg pop_brace=.false
+
+ if self~nums~items < 2 then return
+
+ do label calc forever
+ n = self~first_op
+ op = self~ops[n]
+
+ select
+ when op = .nil | op == "<" then leave calc
+ when op == "(" then do
+ if pop_brace then self~pop_op(n)
+ leave calc
+ end
+ otherwise self~calc
+ end
+ end calc
+
+ ::method args
+ do n = 0 while self~ops[n+1] = "="; end
+ return n
+
+ ::method first_op
+ do n = 1 to self~ops~items while self~ops[n] = "="; end
+ return n
+
+ ::method discard_args
+ do self~~eval~args
+ self~pop_num_calc
+ end
+
+::class QRegister
+ ::attribute name
+
+ ::attribute integer
+ ::attribute fileid
+
+ ::method INIT
+ use arg self~name
+ self~integer = 0
+
+ /*
+ * FIXME: create as pseudo-files if possible
+ */
+ prev_file = efileid.1()
+ 'nomsg edit' SysTempFileName("THECO.???")
+ 'add' /* no we are automatically in line 1 */
+ self~fileid = efileid.1()
+ 'edit' prev_file
+
+ ::method edit
+ .environment~theco.undo~~push_cmd('edit' efileid.1())
+ 'edit' self~fileid
+
+ ::method "string"
+ old_fileid = efileid.1()
+ 'edit' self~fileid
+ buffer = get_buffer()
+ 'edit' old_fileid
+
+ return buffer~toString("l", get_eol())
+
+ ::method "string="
+ use arg val
+
+ old_fileid = efileid.1()
+ 'edit' self~fileid
+
+ .environment~theco.undo~~push_cmd('edit' old_fileid),
+ ~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('locate :'line.1())
+ buffer = get_buffer()
+ do i = 1 to buffer~items
+ if i > 1 then
+ .environment~theco.undo~~push_cmd('split cursor')
+ .environment~theco.undo~~push_cmd('cinsert' buffer[i])
+ end
+ .environment~theco.undo~~push_cmd('add'),
+ ~~push_cmd('delete *'),
+ ~~push_cmd('locate :1'),
+ ~~push_cmd('edit' self~fileid)
+
+ 'locate :1'
+ 'delete *'
+ do line over tokenize(val, '0D 0A'x)
+ 'add'
+ 'cinsert' line
+ end
+ 'locate :1'
+
+ 'edit' old_fileid
+
+::class State
+ ::attribute name
+ ::attribute transitions
+
+ ::method INIT
+ use arg self~name
+ self~transitions = .Table~new
+
+ ::method "[]="
+ forward to (self~transitions)
+
+ ::method eval_colon class
+ if \.environment~theco.modifiers.colon then return .false
+
+ .environment~theco.modifiers.colon = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.modifiers.colon = true"-
+ )
+ return .true
+
+ ::method input class
+ use arg key
+
+ state = .environment~theco.state
+
+ do forever
+ next_state = state~get_next_state(key)
+
+ /* syntax error */
+ if next_state == "" then return .false
+
+ if next_state == state~name then leave
+ state = .environment~theco.states[next_state]
+
+ key = ""
+ end
+
+ if next_state \== .environment~theco.state~name then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.state ="-
+ ".environment~theco.states['"||-
+ .environment~theco.state~name"']"-
+ )
+ .environment~theco.state = state
+ end
+
+ return .true
+
+ ::method get_next_state
+ use arg key
+
+ next_state = self~transitions[key~upper]
+ if next_state = .nil then
+ next_state = self~custom(key)
+
+ return next_state
+
+ ::method custom abstract
+
+/*
+ * Super-class for states accepting string arguments
+ * Opaquely cares about alternative-escape characters,
+ * string building commands and accumulation into a string
+ */
+::class StateExpectString subclass State
+ ::attribute state class
+ ::attribute mode class
+ ::attribute toctl class
+
+ ::method INIT
+ .StateExpectString~state = "start"
+ .StateExpectString~mode = ""
+ .StateExpectString~toctl = .false
+
+ forward class (super)
+
+ ::method save_machine class
+ .environment~theco.undo~~push(-
+ ".StateExpectString~state =" stringify(self~state)-
+ )~~push(-
+ ".StateExpectString~mode =" stringify(self~mode)-
+ )~~push(".StateExpectString~toctl =" self~toctl)
+
+ ::method machine class
+ use arg input
+
+ select
+ when self~mode == "upper" then
+ input = input~upper
+ when self~mode == "lower" then
+ input = input~lower
+ otherwise
+ end
+
+ if self~toctl then do
+ input = input~upper~bitAnd('3F'x)
+ self~toctl = .false
+ end
+
+ select
+ when self~state == "escaped" then do
+ self~state = "start"
+ return input
+ end
+ when input == "^" then
+ self~toctl = .true
+ when self~state == "start" then do
+ if input~c2d >= 32 then return input
+
+ echo = ctlecho(input)
+ select
+ when echo == "Q" |,
+ echo == "R" then self~state = "escaped"
+ when echo == "V" then self~state = "lower"
+ when echo == "W" then self~state = "upper"
+ when echo == "E" then self~state = "ctle"
+ otherwise
+ return input
+ end
+ end
+ when self~state == "lower" then do
+ self~state = "start"
+
+ select
+ when input~c2d < 32, ctlecho(input) == "V" then
+ self~mode = "lower"
+ otherwise
+ return input~lower
+ end
+ end
+ when self~state == "upper" then do
+ self~state = "start"
+
+ select
+ when input~c2d < 32, ctlecho(input) == "W" then
+ self~mode = "upper"
+ otherwise
+ return input~upper
+ end
+ end
+ when self~state == "ctle" then do
+ input = input~upper
+
+ select
+ when input == "Q" then self~state = "ctleq"
+ when input == "U" then self~state = "ctleu"
+ otherwise
+ return .nil
+ end
+ end
+ when self~state == "ctleq" then do
+ reg = .environment~theco.registers[input~upper]
+ if reg = .nil then return .nil
+
+ self~state = "start"
+ return reg~string
+ end
+ when self~state == "ctleu" then do
+ reg = .environment~theco.registers[input~upper]
+ if reg = .nil then return .nil
+
+ self~state = "start"
+ return reg~integer~d2c
+ end
+ otherwise
+ return .nil
+ end
+
+ return ""
+
+ ::method custom
+ use arg key
+
+ if key == "" then do
+ if .environment~theco.exec then self~initial
+ return self~name
+ end
+
+ /*
+ * String termination handling
+ */
+ if .environment~theco.modifiers.at then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.modifiers.at = .true"-
+ )~~push(".environment~theco.escape = '1B'x")
+
+ .environment~theco.modifiers.at = .false
+ .environment~theco.escape = key~upper
+
+ return self~name
+ end
+
+ if key~upper == .environment~theco.escape then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.escape ="-
+ "'".environment~theco.escape~c2x"'x"-
+ )~~push(-
+ ".environment~theco.strings.1 ="-
+ stringify(.environment~theco.strings.1)-
+ )
+
+ .environment~theco.escape = '1B'x
+ str = .environment~theco.strings.1
+ .environment~theco.strings.1 = ""
+
+ .StateExpectString~~save_machine~state = "start"
+ .StateExpectString~mode = ""
+ .StateExpectString~toctl = .false
+
+ return self~done(str)
+ end
+
+ /*
+ * String building characters
+ */
+ insert = .StateExpectString~~save_machine~machine(key)
+ if insert = .nil then return ""
+ if insert == "" then return self~name
+
+ /*
+ * String accumulation
+ */
+ .environment~theco.undo~~push(-
+ ".environment~theco.strings.1 ="-
+ stringify(.environment~theco.strings.1)-
+ )
+ .environment~theco.strings.1 ||= insert
+
+ if .environment~theco.exec then
+ self~process(.environment~theco.strings.1,-
+ insert~length)
+ return self~name
+
+ ::method initial abstract
+ ::method process abstract
+ ::method done abstract
+
+/*
+ * Super class for states accepting Q-Register specifications
+ */
+::class StateExpectQReg subclass State
+ ::method INIT
+ forward class (super)
+
+ ::method save
+ use arg reg
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.registers['"reg~name"']~integer ="-
+ reg~integer-
+ )
+
+ ::method custom
+ use arg key
+
+ reg = .environment~theco.registers[key~upper]
+ if reg = .nil then return ""
+
+ return self~got_register(reg)
+
+ ::method got_register abstract
+
+::class StateStart subclass State
+ ::method INIT
+ forward array ("start") class (super)
+
+ ::method move
+ use arg n
+
+ .environment~theco.undo~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('locate :'line.1())
+
+ /* FIXME: do this in less commands */
+ if n > 0 then
+ do n
+ 'cursor cua right'
+ end
+ else
+ do -n
+ 'cursor cua left'
+ end
+
+ ::method move_lines
+ use arg n
+
+ .environment~theco.undo~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('locate' (-n))
+ 'locate' n
+ 'clocate :1'
+
+ ::method custom
+ use arg key
+
+ key = key~upper
+ select
+ /*
+ * <CTRL/x> commands implemented in `ctlcmd` state
+ */
+ when key~c2d < 32 then do
+ return .environment~theco.states["ctlcmd"],
+ ~get_next_state(ctlecho(key))
+ end
+ /*
+ * arithmetics
+ */
+ when key~matchchar(1, "0123456789") then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~add_digit(key)
+ end
+ when key~matchchar(1, "/*+&#") then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~push_op_calc(key)
+ end
+ when key == "-" then do
+ if \.environment~theco.exec then return self~name
+
+ if .environment~theco.stack~args = 0 |,
+ .environment~theco.stack~nums~peek == "" then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.stack~num_sign ="-
+ .environment~theco.stack~num_sign-
+ )
+ .environment~theco.stack~num_sign *= -1
+ end
+ else
+ .environment~theco.stack~push_op_calc("-")
+ end
+ when key == "(" then do
+ if \.environment~theco.exec then return self~name
+
+ if .environment~theco.stack~num_sign < 0 then
+ .environment~theco.stack,
+ ~~push_num(-1)~push_op_calc("*")
+ .environment~theco.stack~push_op("(")
+ end
+ when key == ")" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~eval(.true)
+ end
+ when key == "," then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~~eval~push_num("")
+ end
+ when key == "." then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~~eval~push_num(get_dot())
+ end
+ when key == "Z" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~~eval~push_num(get_size())
+ end
+ when key == "H" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.stack~~eval,
+ ~~push_num(0)~push_num(get_size())
+ end
+ /*
+ * control structures (loops)
+ */
+ when key == "<" then do
+ if \.environment~theco.exec then do
+ .environment~theco.nest_level += 1
+ .environment~theco.undo~~push(-
+ ".environment~theco.nest_level -= 1"-
+ )
+ return self~name
+ end
+
+ if .environment~theco.stack~~eval~args = 0 then
+ /* infinite loop */
+ .environment~theco.stack~push_num(-1)
+
+ if .environment~theco.stack~nums~peek = 0 then do
+ .environment~theco.stack~pop_num
+
+ /* skip up to end of loop (parse without exec) */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+ else
+ .environment~theco.stack,
+ ~~push_num(.environment~theco.pc),
+ ~~push_op("<")
+ end
+ when key == ">" then do
+ if \.environment~theco.exec then do
+ if .environment~theco.nest_level = 0 then do
+ .environment~theco.exec = .true
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec ="-
+ ".false"-
+ )
+ end
+ else do
+ .environment~theco.nest_level -= 1
+ .environment~theco.undo~~push(-
+ ".environment~theco.nest_level"-
+ "+= 1"-
+ )
+ end
+
+ return self~name
+ end
+
+ .environment~theco.stack~~discard_args~pop_op /* "<" */
+ loop_pc = .environment~theco.stack~pop_num
+ loop_cnt = .environment~theco.stack~pop_num
+
+ if loop_cnt \= 1 then do
+ /* repeat loop */
+ if loop_cnt > 0 then loop_cnt -= 1
+ .environment~theco.pc = loop_pc
+ .environment~theco.stack,
+ ~~push_num(loop_cnt),
+ ~~push_num(loop_pc),
+ ~~push_op("<")
+ end
+ end
+ when key == ";" then do
+ if \.environment~theco.exec then return self~name
+
+ search = .environment~theco.registers["_"]~integer
+ v = .environment~theco.stack~pop_num_calc(1, search)
+ if .State~eval_colon then v = complement(v)
+
+ if v >= 0 then do
+ .environment~theco.stack,
+ ~~discard_args,
+ ~~pop_op~~pop_num~~pop_num
+
+ /* skip up to end of loop (parse without exec) */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+ end
+ /*
+ * control structures (conditionals)
+ */
+ when key == "|" then do
+ if \.environment~theco.exec then do
+ if \.environment~theco.skip_else &,
+ .environment~theco.nest_level = 0 then do
+ .environment~theco.exec = .true
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec ="-
+ ".false"-
+ )
+ end
+
+ return self~name
+ end
+
+ /*
+ * skip up to end of conditional; skip ELSE-part
+ * (parse without exec)
+ */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+ when key == "'" then do
+ if \.environment~theco.exec then do
+ if .environment~theco.nest_level = 0 then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec ="-
+ ".false"-
+ )~~push(-
+ ".environment~theco.skip_else ="-
+ .environment~theco.skip_else-
+ )
+ .environment~theco.exec = .true
+ .environment~theco.skip_else = .false
+ end
+ else do
+ .environment~theco.nest_level -= 1
+ .environment~theco.undo~~push(-
+ ".environment~theco.nest_level"-
+ "+= 1"-
+ )
+ end
+
+ return self~name
+ end
+ end
+ /*
+ * modifiers
+ */
+ when key == "@" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.modidifiers.at ="-
+ .environment~theco.modidifiers.at-
+ )
+ .environment~theco.modifiers.at = .true
+ end
+ when key == ":" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.modidifiers.colon ="-
+ .environment~theco.modidifiers.colon-
+ )
+ .environment~theco.modifiers.colon = .true
+ end
+ /*
+ * commands
+ */
+ when key == "J" then do
+ if \.environment~theco.exec then return self~name
+
+ .environment~theco.undo~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('locate :'line.1())
+ call set_dot .environment~theco.stack~pop_num_calc(1, 0)
+ end
+ when key == "C" then do
+ if \.environment~theco.exec then return self~name
+
+ self~move(.environment~theco.stack~pop_num_calc)
+ end
+ when key == "R" then do
+ if \.environment~theco.exec then return self~name
+
+ self~move(-.environment~theco.stack~pop_num_calc)
+ end
+ when key == "L" then do
+ if \.environment~theco.exec then return self~name
+
+ self~move_lines(.environment~theco.stack~pop_num_calc)
+ end
+ when key == "B" then do
+ if \.environment~theco.exec then return self~name
+
+ self~move_lines(-.environment~theco.stack~pop_num_calc)
+ end
+ when key == "=" then do
+ if \.environment~theco.exec then return self~name
+
+ 'msg' .environment~theco.stack~pop_num_calc
+ end
+ when key == "D" then do
+ if \.environment~theco.exec then return self~name
+
+ v1 = .environment~theco.stack~pop_num_calc
+ if .environment~theco.stack~args = 0 then do
+ /* relative character range */
+ if v1 > 0 then do
+ from = get_dot()
+ to = from + v1
+ end
+ else do
+ to = get_dot()
+ from = to + v1
+ end
+ end
+ else do
+ /* absolute character range */
+ from = .environment~theco.stack~pop_num_calc
+ to = v1
+ end
+
+ eol_len = get_eol()~length
+
+ call set_dot from
+ dot = from
+
+ col = column.1()
+ do forever
+ line = curline.3()
+ size = min(to - dot, line~length - col + 1)
+ line = line~substr(col, size)
+
+ if line~length > 0 then do
+ /* THE bug: Undo insert does not work properly! */
+ .environment~theco.undo,
+ ~~push_cmd('cinsert' line)
+ 'cdelete' line~length
+ end
+
+ dot += line~length
+ if dot >= to then leave
+ dot += eol_len
+
+ 'join cursor'
+ .environment~theco.undo,
+ ~~push_cmd('split cursor')
+ end
+ end
+ otherwise
+ return ""
+ end
+
+ return self~name
+
+::class StateLabel subclass State
+ ::method INIT
+ forward array ("label") class (super)
+
+ ::method custom
+ use arg key
+
+ select
+ when key == "!" then do
+ label = .environment~theco.strings.1
+ escaped = stringify(label)
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.goto_table["escaped"] ="-
+ sor(.environment~theco.goto_table[label], ".nil")-
+ )~~push(".environment~theco.strings.1 =" escaped)
+ .environment~theco.goto_table[label] =,
+ .environment~theco.pc
+ .environment~theco.strings.1 = ""
+
+ if .environment~theco.skip_label == label then do
+ .environment~theco.skip_label = .nil
+ .environment~theco.exec = .true
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.skip_label ="-
+ stringify(label)-
+ )~~push(".environment~theco.exec = .false")
+ end
+
+ return "start"
+ end
+ otherwise
+ .environment~theco.undo~~push(-
+ ".environment~theco.strings.1 ="-
+ stringify(.environment~theco.strings.1)-
+ )
+ .environment~theco.strings.1 ||= key
+ return self~name
+ end
+
+::class StateCtlCmd subclass State
+ ::method INIT
+ forward array ("ctlcmd") class (super)
+
+ ::method custom
+ use arg key
+
+ key = key~upper
+ select
+ when key == "O" then do
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.stack~set_radix(8)
+ end
+ when key == "D" then do
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.stack~set_radix(10)
+ end
+ when key == "R" then do
+ if \.environment~theco.exec then return "start"
+
+ if .environment~theco.stack~~eval~args = 0 then
+ .environment~theco.stack~push_num(-
+ .environment~theco.stack~radix-
+ )
+ else
+ .environment~theco.stack~set_radix(-
+ .environment~theco.stack~pop_num_calc-
+ )
+ end
+ /*
+ * Alternatives: ^i, ^I, <CTRL/I>, <TAB>
+ */
+ when key == "I" then do
+ if \.environment~theco.exec then return "cmd_insert"
+
+ .environment~theco.stack~~eval~push_num(9)
+ return "cmd_insert"
+ end
+ /*
+ * Alternatives: ^[, <CTRL/[> (cannot be typed), <ESC>
+ */
+ when key == "[" then do
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.stack~discard_args
+
+ /*
+ * Does not allow the caret-escape form;
+ * must be typed with two consequtive <ESC>
+ */
+ if .environment~theco.cmdline~right(2) == '1B 1B'x then do
+ if .environment~theco.quit_requested then
+ do nbfile.1()
+ 'qquit'
+ end
+ .environment~theco.cmdline = ""
+ .environment~theco.undo~empty
+ end
+ end
+ /*
+ * Additional numeric operations
+ */
+ when key == "_" then do
+ if \.environment~theco.exec then return "start"
+
+ v = .environment~theco.stack~pop_num_calc
+ .environment~theco.stack~push_num(complement(v))
+ end
+ when key~matchchar(1, "*/") then do
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.stack~push_op_calc("^"key)
+ end
+ otherwise
+ return ""
+ end
+
+ return "start"
+
+::class StateCtlUCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("ctlucmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "cmd_ctlu"
+
+ .environment~theco.reg_arg = reg
+ return "cmd_ctlu"
+
+::class StateCmdCtlU subclass StateExpectString
+ ::method INIT
+ forward array ("cmd_ctlu") class (super)
+
+ ::method initial
+ /* nothing to be done */
+
+ ::method process
+ /* nothing to be done */
+
+ ::method done
+ use arg str
+
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.reg_arg~string = str
+ return "start"
+
+::class StateFCmd subclass State
+ ::method INIT
+ forward array ("fcmd") class (super)
+
+ ::method custom
+ use arg key
+
+ select
+ /*
+ * loop flow control
+ */
+ when key == "<" then do
+ if \.environment~theco.exec then return "start"
+
+ /* FIXME: what if in brackets? */
+ /* FIXME: what if not in loop -> set PC to 1 */
+ .environment~theco.stack~~discard_args~pop_op /* "<" */
+
+ /* repeat loop */
+ /* FIXME: peeking the program counter would be sufficient */
+ .environment~theco.pc = .environment~theco.stack~pop_num
+ .environment~theco.stack,
+ ~~push_num(.environment~theco.pc),
+ ~~push_op("<")
+ end
+ when key == ">" then do
+ if \.environment~theco.exec then return "start"
+
+ /* FIXME: what if in brackets? */
+ .environment~theco.stack~~discard_args~pop_op /* "<" */
+ loop_pc = .environment~theco.stack~pop_num
+ loop_cnt = .environment~theco.stack~pop_num
+
+ if loop_cnt > 1 then do
+ /* repeat loop */
+ .environment~theco.pc = loop_pc
+ .environment~theco.stack,
+ ~~push_num(loop_cnt-1),
+ ~~push_num(loop_pc),
+ ~~push_op("<")
+ end
+ else do
+ /* skip up to end of loop (parse without exec) */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+ end
+ /*
+ * conditional flow control
+ */
+ when key == "'" then do
+ if \.environment~theco.exec then return "start"
+
+ /*
+ * skip to end of conditional (parse without exec)
+ */
+ .environment~theco.exec = .false
+ .environment~theco.skip_else = .true
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )~~push(".environment~theco.skip_else = .false")
+ end
+ when key == "|" then do
+ if \.environment~theco.exec then return "start"
+
+ /*
+ * skip to ELSE-part or end of conditional
+ * (parse without exec)
+ */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+ otherwise
+ return ""
+ end
+
+ return "start"
+
+::class ConditionalTests
+ ::method "A"
+ return self~"V"(arg(1)) | self~"W"(arg(1))
+ ::method "C"
+ /* FIXME */
+ return self~"R"(arg(1))
+ ::method "D"
+ return arg(1) >= "0"~c2d & arg(1) <= "9"~c2d
+ ::method "E"
+ return arg(1) = 0
+ ::method "F"
+ forward message "E"
+ ::method "G"
+ return arg(1) > 0
+ ::method "L"
+ return arg(1) < 0
+ ::method "N"
+ return arg(1) \= 0
+ ::method "R"
+ return self~"A"(arg(1)) | self~"D"(arg(1))
+ ::method "S"
+ forward message "L"
+ ::method "T"
+ forward message "L"
+ ::method "U"
+ forward message "E"
+ ::method "V"
+ return arg(1) >= "a"~c2d & arg(1) <= "z"~c2d
+ ::method "W"
+ return arg(1) >= "A"~c2d & arg(1) <= "Z"~c2d
+ ::method "<"
+ forward message "L"
+ ::method ">"
+ forward message "G"
+ ::method "="
+ forward message "E"
+
+::class StateCondCmd subclass State
+ ::attribute tests
+
+ ::method INIT
+ self~tests = .ConditionalTests~new
+
+ forward array ("condcmd") class (super)
+
+ ::method custom
+ use arg key
+
+ if \self~tests~hasMethod(key) then return ""
+
+ if \.environment~theco.exec then do
+ .environment~theco.nest_level += 1
+ .environment~theco.undo~~push(-
+ ".environment~theco.nest_level -= 1"-
+ )
+ return "start"
+ end
+
+ v = .environment~theco.stack~pop_num_calc
+ if \self~tests~send(key, v) then do
+ /*
+ * skip to ELSE-part or end of conditional
+ * (parse without exec)
+ */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.exec = .true"-
+ )
+ end
+
+ return "start"
+
+::class StateCmdGoto subclass StateExpectString
+ ::method INIT
+ forward array ("cmd_goto") class (super)
+
+ ::method initial
+ /* nothing to be done */
+
+ ::method process
+ /* nothing to be done */
+
+ ::method done
+ use arg str
+
+ if \.environment~theco.exec then return "start"
+
+ labels = tokenize(str, ",")
+ label = labels[.environment~theco.stack~pop_num_calc]
+
+ if label \= .nil, label \== "" then do
+ pc = .environment~theco.goto_table[label]
+
+ if pc \= .nil then
+ .environment~theco.pc = pc
+ else do
+ .environment~theco.skip_label = label
+ /* skip till label is defined */
+ .environment~theco.exec = .false
+ .environment~theco.undo~~push(-
+ ".environment~theco.skip_label = .nil"-
+ )~~push(".environment~theco.exec = .true")
+ end
+ end
+
+ return "start"
+
+::class StateQCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("qcmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.stack~~eval~push_num(reg~integer)
+ return "start"
+
+::class StateUCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("ucmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "start"
+
+ self~save(reg)
+ reg~integer = .environment~theco.stack~pop_num_calc
+ return "start"
+
+::class StateIncCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("inccmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "start"
+
+ self~save(reg)
+ reg~integer += .environment~theco.stack~pop_num_calc
+ .environment~theco.stack~push_num(reg~integer)
+ return "start"
+
+::class StateMCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("mcmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "start"
+
+ pc = .environment~theco.pc
+ .environment~theco.pc = 0
+ .environment~theco.state = .environment~theco.states["start"]
+
+ if \execute(reg~string) then return ""
+
+ .environment~theco.pc = pc
+
+ return "start"
+
+::class StateECmd subclass State
+ ::method INIT
+ forward array ("ecmd") class (super)
+
+ ::method custom
+ use arg key
+
+ key = key~upper
+ select
+ when key == "X" then do
+ if \.environment~theco.exec then return "start"
+
+ .environment~theco.quit_requested = .true
+ return "start"
+ end
+ otherwise
+ return ""
+ end
+
+::class StateCmdFile subclass StateExpectString
+ ::method INIT
+ forward array ("cmd_file") class (super)
+
+ ::method do_edit
+ use arg filename
+
+ file_cnt = nbfile.1()
+ old_file = efileid.1()
+ 'edit' filename
+ if nbfile.1() > file_cnt then
+ /* file is new in ring */
+ .environment~theco.undo~~push_cmd('qquit')
+ else
+ .environment~theco.undo~~push_cmd('edit' old_file)
+
+ select
+ when size.1() = 0 then 'add'
+ when tof() then 'locate 1'
+ otherwise
+ end
+
+ ::method initial
+ /* nothing to be done */
+
+ ::method process
+ /* nothing to be done */
+
+ ::method done
+ use arg filename
+
+ if \.environment~theco.exec then return "start"
+
+ /* FIXME: match-spec error */
+ if SysFileTree(filename, "matching.", "FO") \= 0 then
+ return ""
+
+ if matching.0 = 0 then
+ /* no match-spec or non-existing file */
+ self~do_edit(filename)
+ else
+ do i = 1 to matching.0
+ self~do_edit(matching.i)
+ end
+
+ return "start"
+
+/*
+ * TODO: expect filename to read into Q-register
+ */
+::class StateEQCmd subclass StateExpectQReg
+ ::method INIT
+ forward array ("eqcmd") class (super)
+
+ ::method got_register
+ use arg reg
+
+ if \.environment~theco.exec then return "start"
+
+ reg~edit()
+ return "start"
+
+::class StateCmdInsert subclass StateExpectString
+ ::method INIT
+ forward array ("cmd_insert") class (super)
+
+ ::method do_insert
+ use arg key
+
+ if key == "" then return
+
+ select
+ when key == '0D'x | key == '0A'x then do
+ 'split cursor'
+ 'next'
+ 'clocate :1'
+ .environment~theco.undo,
+ ~~push_cmd('join cursor'),
+ ~~push_cmd('sos endchar'),
+ ~~push_cmd('up')
+ end
+ when key == '09'x, tabkey.2() == "TAB" then do
+ /* NOTE: sos tabf in insertmode currently broken on THE v3.3 RC1!? */
+ .environment~theco.undo,
+ ~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('sos tabb')
+ 'sos tabf'
+ end
+ otherwise
+ 'cinsert' key
+ 'clocate 1'
+ .environment~theco.undo~~push_cmd('sos cuadelback')
+ end
+
+ ::method initial
+ /*
+ * NOTE: cannot support VideoTECO's <n>I because
+ * beginning and end of strings must be determined
+ * syntactically
+ */
+ do i = .environment~theco.stack~~eval~args to 1 by -1
+ char = .environment~theco.stack~pop_num_calc(i)
+ self~do_insert(char~d2c)
+ end
+
+ ::method process
+ use arg str, new_chars
+
+ do i = new_chars-1 to 0 by -1
+ self~do_insert(str~subchar(str~length - i))
+ end
+
+ ::method done
+ /* nothing to be done when done */
+ return "start"
+
+::class StateCmdSearch subclass StateExpectString
+ ::attribute initial_dot
+
+ ::attribute from
+ ::attribute to
+ ::attribute count
+
+ ::method INIT
+ forward array ("cmd_search") class (super)
+
+ ::method initial
+ self~initial_dot = get_dot()
+
+ v = .environment~theco.stack~pop_num_calc
+ if .environment~theco.stack~args > 0 then do
+ self~from = .environment~theco.stack~pop_num_calc
+ self~to = v
+ self~count = 1
+ end
+ else do
+ self~from = self~initial_dot
+ self~to = get_size()
+ self~count = v
+ end
+
+ ::method process
+ use arg str
+
+ cre = ReComp(pattern2regexp(str), "x")
+ if cre~left(1) then do
+ .environment~theco.undo~~push(-
+ ".environment~theco.registers['_']~integer ="-
+ .environment~theco.registers["_"]~integer-
+ )
+ .environment~theco.registers["_"]~integer = 0 /* failure */
+
+ call ReFree cre
+ return
+ end
+
+ buffer = get_buffer(self~from, self~to)~toString("l", get_eol())
+
+ offset = 1
+ do self~count,
+ while ReExec(cre, buffer~substr(offset), "matches.", "p")
+ offset += matches.!match~word(1) - 1 /* offset */
+ offset += matches.!match~word(2) /* length */
+ end
+
+ call ReFree cre
+
+ .environment~theco.undo~~push_cmd('clocate :'column.1()),
+ ~~push_cmd('locate :'line.1())
+
+ .environment~theco.undo~~push(-
+ ".environment~theco.registers['_']~integer ="-
+ .environment~theco.registers["_"]~integer-
+ )
+ if matches.!match~word(2) = 0 then do
+ call set_dot self~initial_dot
+ .environment~theco.registers["_"]~integer = 0 /* failure */
+ end
+ else do
+ call set_dot self~from+offset-1
+ .environment~theco.registers["_"]~integer = -1 /* success */
+ end
+
+ ::method done
+ use arg str
+
+ if \.environment~theco.exec then return "start"
+
+ search_reg = .environment~theco.registers["_"]
+ if str == "" then
+ self~process(search_reg~string)
+ else
+ search_reg~string = str
+
+ return "start"
+
+/*
+ * auxilliary stuff
+ */
+::routine sor
+ use arg obj, val
+
+ select
+ when obj = .nil then return val
+ when obj == "" then return val
+ otherwise
+ return obj
+ end
+
+::routine isor
+ use arg obj, val
+
+ if obj = 0 then return val
+ return obj
+
+/*
+ * Ones complement (binary NOT), may be used to negate TECO boolean values
+ * (x < 0 and x >= 0)
+ */
+::routine complement
+ return -arg(1) - 1
+
+::routine last_match
+ use arg str, chars
+
+ do i = str~length to 1 by -1
+ if str~matchchar(i, chars) then return i
+ end
+ return 0
+
+::routine tokenize
+ use arg str, delims
+
+ tokens = .Array~new
+
+ start = 1
+ do i = 1 to str~length
+ if str~matchchar(i, delims) then do
+ tokens~append(str~substr(start, i - start))
+ start = i + 1
+ end
+ end
+ tokens~append(str~substr(start, i - start))
+
+ return tokens
+
+::routine stringify
+ return "'"arg(1)~changeStr("'", "''")"'"
+
+::routine ctlecho
+ return arg(1)~bitOr('40'x)
+
+::routine is_matchspec
+ use arg spec
+
+ /* FIXME: glob rules - different on Windows!? */
+ do i = 1 to spec~length
+ if spec~matchchar(i, "?*[") then return .true
+ end
+ return .false
+
+::routine get_path_sep
+ os = version.3()
+
+ if os == "OS2" | os == "WIN32" then return "\"
+ return "/"
+
+::routine get_real_filename
+ use arg file
+
+ name = filespec("name", file~word(5))
+ if file~word(4)~caselessPos("D") \= 0 then
+ return name || get_path_sep()
+ return name
+
+::routine get_eol
+ eol = eolout.1()
+
+ select
+ when eol == "LF" then return '0A'x
+ when eol == "CR" then return '0D'x
+ when eol == "CRLF" then return '0D 0A'x
+ end
+
+::routine get_dot
+ old_line = line.1()
+ eol_len = get_eol()~length
+
+ dot = 0
+ do l = 1 to old_line-1
+ 'up'
+ /* FIXME: for some reason length.1() always returns 19 */
+ dot += curline.3()~length + eol_len
+ end
+ 'locate :'old_line
+
+ return dot + column.1() - 1
+
+::routine get_size
+ line = line.1()
+ column = column.1()
+ 'locate :'size.1()
+ 'sos endchar'
+
+ size = get_dot()
+
+ 'locate :'line
+ 'clocate :'column
+
+ return size
+
+::routine set_dot
+ use arg dot
+
+ eol_len = get_eol()~length
+ 'locate :1'
+ 'clocate :1'
+
+ do forever
+ /* FIXME: for some reason length.1() always returns 19 */
+ line_length = curline.3()~length + eol_len
+ if dot < line_length then leave
+
+ dot -= line_length
+ 'next'
+ end
+ 'clocate' dot
+
+::routine get_buffer
+ use arg from=0, to=(get_size())
+
+ eol_len = get_eol()~length
+ old_line = line.1()
+ old_column = column.1()
+
+ call set_dot from
+ dot = from
+
+ /*
+ * THE bug workaround: sometimes necessary to fixup curline.3()
+ */
+ 'next'
+ 'up'
+
+ buffer = .Array~new
+ do forever
+ col = column.1()
+ line = curline.3()
+ line = line~substr(col, min(to - dot, line~length - col + 1))
+ buffer~append(line)
+
+ dot += line~length
+ if dot >= to then leave
+ dot += eol_len
+
+ 'next'
+ 'clocate :1'
+ end
+
+ 'locate :'old_line
+ 'clocate :'old_column
+ return buffer
+
+::routine regexp_escape
+ use arg char
+
+ if char~matchchar(1, ".[](){}^$*+?|\") then return "\"char
+ return char
+
+::routine pattern2regexp
+ use arg pattern
+
+ re = ""
+ state = "start"
+
+ do i = 1 to pattern~length
+ c = pattern~subchar(i)
+
+ select
+ when state == "start" then do
+ if c~c2d >= 32 then do
+ re ||= regexp_escape(c)
+ iterate i
+ end
+ echo = ctlecho(c)
+
+ select
+ when echo == "X" then re ||= "."
+ when echo == "S" then re ||= "[^[:alnum:]]"
+ when echo == "N" then state = "not"
+ when echo == "E" then state = "ctle"
+ otherwise
+ /* control characters never have to be escaped */
+ re ||= c
+ end
+ end
+ when state == "not" then do
+ if c~matchchar(1, "[]-\") then c = "\"c
+ re ||= "[^"c"]"
+
+ state = "start"
+ end
+ when state == "ctle" then do
+ c = c~upper
+
+ select
+ when c == "A" then re ||= "[[:alpha:]]"
+ when c == "B" then re ||= "[^[:alnum:]]"
+ when c == "C" then re ||= "[[:alnum:].$]"
+ when c == "D" then re ||= "[[:digit:]]"
+ /* when c == "G" then */
+ when c == "L" then re ||= "[\r\n\v\f]"
+ /* when c == "M" then */
+ when c == "R" then re ||= "[[:alnum:]]"
+ when c == "S" then re ||= "[[:blank:]]+"
+ when c == "V" then re ||= "[[:lower:]]"
+ when c == "W" then re ||= "[[:upper:]]"
+ when c == "X" then re ||= "."
+ /* when ^E<nnn> */
+ when c == "[" then re ||= "("
+ otherwise
+ return ""
+ end
+
+ state = "start"
+ end
+ end
+ end
+
+ return re
+
+/*
+ * External routines (classic Rexx function packages)
+ */
+::routine ReLoadFuncs external "REGISTERED rexxre reloadfuncs"
diff --git a/timebutler.sh b/timebutler.sh
new file mode 100755
index 0000000..e45c461
--- /dev/null
+++ b/timebutler.sh
@@ -0,0 +1,27 @@
+#!env bash
+# Minimalist Timebutler CLI interface
+# ./timebutler start|pause|resume|stop|cancel|status [project]
+# You have to set USERID and AUTH variables in ~/.timebutler!
+# Run `curl -X POST 'https://timebutler.de/api/v1/users' -d "auth=$AUTH"` to fetch user ids.
+set -e
+
+. ~/.timebutler
+test -n "$AUTH" -a -n "$USERID"
+
+COMMAND=$1
+if [ -z "$COMMAND" ]; then
+ echo "Missing command"
+ exit 1
+fi
+
+PROJECT=$2
+if [ -n "$PROJECT" ]; then
+ PROJECTID=(`curl -s -X POST 'https://timebutler.de/api/v1/projects' -d "auth=$AUTH" | awk -F ';' "\\$2~/$PROJECT/ {print \\$1}"`)
+ if [ ${#PROJECTID[*]} != 1 ]; then
+ echo "Unknown or ambiguous project \"$PROJECT\""
+ exit 1
+ fi
+ ARGS="$ARGS -d projectid=$PROJECTID"
+fi
+
+exec curl -X POST 'https://timebutler.de/api/v1/timeclock' -d "auth=$AUTH" -d "userid=$USERID" -d "command=$COMMAND" $ARGS
diff --git a/xml.sc b/xml.sc
new file mode 100644
index 0000000..7f45ce2
--- /dev/null
+++ b/xml.sc
@@ -0,0 +1,52 @@
+# Quick and dirty SNOBOL4/Snocone DOM-like XML parser
+
+struct tag {type, params, lastp, children, last, parent}
+struct param {name, value}
+word = SPAN("_0123456789" && &LCASE)
+xml = (("<!--" && ARB && "-->") $ *ITEM(children(current),last(current) = last(current) + 1) |
+ "<" && word $ *type(current = ITEM(children(current),last(current) = last(current) + 1) =
+ tag(NULL, ARRAY(10), 0, ARRAY(10), 0, current)) &&
+ ARBNO(SPAN(" ") && word $ *name(ITEM(params(current), lastp(current) = lastp(current) + 1) = param()) &&
+ ARBNO(" ") && "=" && ARBNO(" ") && ("'"|'"') $ st && BREAK(*st) $ *value(ITEM(params(current),lastp(current))) && *st) &&
+ (">" | "/>" && *godown()) | "</" && word $ tag_name && ">" && *(tag_name :: type(current)) && *godown() |
+ (BREAK("<") | REM) $ *ITEM(children(current),last(current) = last(current) + 1)) && (RPOS(0) | *xml)
+procedure godown() {
+ current = parent(current); return
+}
+
+# recursive function to produce a XML tree from a TAG data structure
+procedure node_to_xml(node) i {
+ if(type(node) :!: NULL) {
+ node_to_xml = node_to_xml && "<" && type(node)
+ for(i=1, i <= lastp(node), i=i+1)
+ node_to_xml = node_to_xml && " " && name(ITEM(params(node), i)) && '="' && value(ITEM(params(node), i)) && '"'
+ if(last(node) == 0)
+ node_to_xml = node_to_xml && "/>"
+ else
+ node_to_xml = node_to_xml && ">"
+ }
+
+ for(i=1, i <= last(node), i=i+1)
+ if(DATATYPE(ITEM(children(node),i)) :: "TAG")
+ node_to_xml = node_to_xml && node_to_xml(ITEM(children(node),i))
+ else
+ node_to_xml = node_to_xml && ITEM(children(node),i)
+
+ if(type(node) :!: NULL && last(node) > 0)
+ node_to_xml = node_to_xml && "</" && type(node) && ">"
+}
+
+# TEST CODE
+current = tag(NULL, NULL, NULL, ARRAY(10), 0, NULL)
+'<test arg1="a1" arg2="a2">hgg<br/>jklhg<b><!-- Kommentar -->Bold text</b></test>' ? xml
+
+test_tag = ITEM(children(current),1)
+OUTPUT = type(test_tag)
+OUTPUT = name(ITEM(params(test_tag),2))
+OUTPUT = ITEM(children(test_tag),1)
+b_tag = ITEM(children(test_tag),4)
+OUTPUT = ITEM(children(b_tag),1)
+OUTPUT = ITEM(children(b_tag),2)
+
+type(test_tag) = "neu"
+OUTPUT = node_to_xml(current)