diff options
author | Robin Haberkorn <rhaberkorn@fmsbw.de> | 2025-10-06 00:48:48 +0300 |
---|---|---|
committer | Robin Haberkorn <rhaberkorn@fmsbw.de> | 2025-10-06 00:48:48 +0300 |
commit | ad9e7cd5117c965222aae708f660e56d537914fc (patch) | |
tree | 580006656ec76513500170e5646e92e7819c6c0b | |
download | snippets-ad9e7cd5117c965222aae708f660e56d537914fc.tar.gz |
imported all of my Github gists from https://gist.github.com/rhaberkorn
-rw-r--r-- | 52-osc.pl | 42 | ||||
-rw-r--r-- | 99bottles.for | 17 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | SciTEStartup.lua | 98 | ||||
-rw-r--r-- | aes.apl | 69 | ||||
-rw-r--r-- | exp2ook.sc | 94 | ||||
-rwxr-xr-x | exp2ook.sno | 72 | ||||
-rwxr-xr-x | git/git-checkout-clean.sh | 19 | ||||
-rwxr-xr-x | git/git-fixup.sh | 12 | ||||
-rwxr-xr-x | git/git-move-tag.sh | 11 | ||||
-rwxr-xr-x | git/git-rename-tag.sh | 12 | ||||
-rwxr-xr-x | gmail-refresh-token.tes | 13 | ||||
-rwxr-xr-x | gtimelog-stats.lua | 52 | ||||
-rwxr-xr-x | gtimelog-stats.sno | 58 | ||||
-rwxr-xr-x | metraLine.sh | 16 | ||||
-rw-r--r-- | rearrange.for | 85 | ||||
-rwxr-xr-x | tecat.lua | 14 | ||||
-rwxr-xr-x | tecat.sh | 6 | ||||
-rw-r--r-- | tests/bait.c | 63 | ||||
-rw-r--r-- | tests/mouse-test.c | 50 | ||||
-rw-r--r-- | tests/relocatability-test.c | 28 | ||||
-rw-r--r-- | theco.rex | 2178 | ||||
-rwxr-xr-x | timebutler.sh | 27 | ||||
-rw-r--r-- | xml.sc | 52 |
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 @@ -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 @@ -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) |