Got bored... wrote this...

103 views
Skip to first unread message

flicker...@anduin.net

unread,
Apr 26, 2020, 3:39:09 PM4/26/20
to
It's the beginnings of a text mode windowed UI, written in rexx.
Should be multiform.


/*

Test rexx app for UI
version 0.1
Tom Dyer 2020

*/

call rxFuncAdd "SysLoadFuncs", "REXXUTIL", "SysLoadFuncs"
call SysLoadFuncs


wm = .windowmanager~new(.logger~new)
wm~logger~setloglevel(2)
win = .logwindow~new
win~logger=wm~logger
win= .window~new
win~screen= wm~screen
win~name = "The Logging Window"

label1 = .label~new("this is a label",1,1)
label2 = .label~new("a second label", 2,1)
input1 = .inputbox~new("", 1, 20, 30)
input2 = .inputbox~new("test", 2, 20, 30)
input3 = .inputbox~new("test2", 3, 20, 30)

fieldhelplabel = .label~new("",10,20)

input1~validator= .validator~new()
input2~validator= .numericvalidator~new()
input1~validator~fieldhelp=fieldhelplabel
input2~validator~fieldhelp=fieldhelplabel


win~objects~insert(label1)
win~objects~insert(label2)
win~objects~insert(input1)
win~objects~insert(input2)
win~objects~insert(input3)
win~objects~insert(fieldhelplabel)



wm~window~insert(win)
wm~run()

wm~logger~log( "wm is run",1)

do i = 1 to 3
say "shutdown "
call syssleep(i)
end


exit


::class windowmanager

::attribute running
::attribute logger
::attribute keyboard
::attribute screen
::attribute window
::attribute focusableitems
::attribute activeitem

::method init
expose logger screen
use arg logger
self~window = .list~new
self~focusableitems = .list~new
screen= .screen~new()


::method run unguarded
expose keyboard screen
keyboard= .keyboard~new()
keyboard~logger= self~logger
keyboard~listener=self
self~findfocusable

self~drawwindows
self~logger~log("start wm",1)
self~keyboard~poll
self~logger~log("back from pollkey",1 )

return

::method findfocusable
do i = 1 to self~window~at(0)~objects~items
if (self~window~at(0)~objects~at(i))~isInstanceOf(.focusable) then do
self~focusableitems~insert(self~window~at(0)~objects~at(i))
end
end
self~focusableitems~at(0)~hasFocus = .true
self~activeitem= self~focusableitems~at(0)



::method nextFocus
self~logger~log(self~focusableitems~items,0)

do i = 0 to self~focusableitems~items -1
self~logger~log(i ||"might"|| self~focusableitems~at(i)~hasFocus,0)

if self~focusableitems~at(i)~hasFocus = .true then do
self~focusableitems~at(i)~lostFocus

if i = self~focusableitems~items -1 then do
self~focusableitems~at(0)~gotFocus
self~activeitem=self~focusableitems~at(0)
return
end
else do
self~focusableitems~at(i+1)~gotFocus
self~activeitem=self~focusableitems~at(i+1)
return
end
end
end




::method drawwindows
do i = 1 to self~window~items
self~window~at(i-1)~draw
end


::method sendkey
use arg key
self~logger~log(key,0)

select
when key~alt= .true & upper(key~value) = "X" then do
self~keyboard~running= .false
self~screen~running = .false
self~logger~log("shutdown called",0)
end
when key~alt= .true & upper(key~value) = "R" then do
self~drawwindows
self~screen~draw
end
when key~tab=.true then do
self~nextFocus
end
otherwise
self~activeitem~sendkey(key)

end

self~drawwindows


::class screen inherit AlarmNotification

::attribute logger
::attribute rows
::attribute columns
::attribute platform
::attribute running
::attribute data
::constant ansi '1b5b'x

::method ansiclear
return self~ansi||'2J'

::constant isLinux linux
::constant isWindows windows

::method init
use arg logger
self~logger
self~data = .mutablebuffer~new()

if pos("Linux", SysVersion()) <> 0 then do
self~platform=self~isLinux
end
if pos("Windows", SysVersion()) <> 0 then do
self~platform =self~isWindows
end

self~getscreensize
self~setalarm


::method setalarm
if self~running = .false then exit
alarm = .alarm~new(1, self)


::method getscreensize
expose rows columns

if self~platform = self~isLinux then do
/* linux */
do line over .SystemQueue~new("stty size ") ; nop ; end
parse var line rows columns
end

if self~platform = self~isWindows then do
/* not tested but should work eh */
parse value SysTextScreenSize() with rows columns
end


::method triggered
self~draw

::method draw
self~clear
say self~data

self~setalarm

::method clear
say self~ansiclear
/* call SysCls */

::class keyboard

::attribute logger
::attribute running
::attribute listener

::method poll unguarded
expose k
if self~running = .false then exit
k = sysgetkey("noecho")
key = .key~new
key~value=k

if c2x(k) = .key~tabcode then do
key~tab=.true
key~value=""
end

if c2x(k) = .key~bscode then do
key~bs=.true
key~value=" "
end

if c2x(k) = .key~spacecode then do
key~space=.true
key~value=" "
end


if c2x(k) = .key~altcode then do
k = sysgetkey("noecho")
key~alt=.true
key~value= k
end

self~logger~log("key" key~get(), 1)
self~listener~sendkey(key)
self~poll


::class key
::constant altcode "1B"
::constant tabcode "09"
::constant spacecode "20"
::constant bscode "7F"
::attribute alt
::attribute tab
::attribute value
::attribute space
::attribute bs

::method get
expose value
return value


::class logger
::attribute logdata
::attribute running

::method init
self~logdata = .list~new

::method log
expose loglevel
use arg message, level
rc=lineout("logfile",message)

if level > loglevel then do
self~logdata~insert(message)
rc=lineout("logfile",message)
end

::method setloglevel
expose loglevel
parse arg loglevel



::class window
::attribute name
::attribute screen
::attribute objects

::method init
self~objects = .list~new

::method draw
self~makewindow
self~screen~data = self~makewindow

::method maketitlebar
titlebar = center( self~name, self~screen~columns, "*")
return titlebar

::method makewindow
d = self~maketitlebar
do i = 1 to self~screen~rows-3
d = D||"*" || right("*",self~screen~columns-1," ")
end

time = time()
d = d||center(time, self~screen~columns, "*")

do i = 1 to self~objects~items
d=d||self~objects~at(i-1)~draw
end

return d

::class logwindow subclass window
::attribute logger

::method draw
super~draw
do i = 1 to logger~logdata~allItems
self~screen~data(logger~logdata[i])
end



::class widget
::attribute fieldhelp
::attribute row
::attribute column
::attribute data
::attribute disdata
::attribute validator

::method predraw
self~disdata = self~data

::method draw
self~predraw
return x2c(1b)||x2c(5b)||self~row||";"self~column||"H "||self~disdata

::method init
expose data row column
use arg data, row , column
row = row + 2
column = column + 2

::class label subclass widget


::class inputbox subclass widget inherit focusable
::attribute length

::method init
expose data row column length
use arg data, row, column, length
self~validator = .nil
self~init:super(data, row, column)

::method predraw
self~disdata = left(self~data,self~length,"_")
if self~hasFocus = .true then do
self~disdata = self~disdata ||" * "
end

::method sendkey
use arg key
if key~bs = .true & length(self~data ) > 0 then do
self~data = left(self~data, length(self~data)-1 )
end
else do
if self~validator <> .nil then do
if self~validator~isValid(key) = .false then key~value = ""
end

self~data = self~data||key~value
end

::class focusable mixinclass object
::attribute hasFocus

::method gotFocus
self~hasfocus = .true


::method lostFocus
self~hasfocus = .false


::class validator
/*
normal validator letters and numbers
*/
::attribute validkeys
::attribute fieldhelpstring
::attribute fieldhelp

::method init
self~validkeys = .string~graph || " £"
self~fieldhelpstring = "This field is alphanumeric"


::method isValid
use arg key
if self~fieldhelp <> .nil then self~fieldhelp~data = self~fieldhelpstring
if pos(key~value, self~validkeys) > 0 then return .true
else return .false

::class numericvalidator subclass validator
/*
numeric validator 4
*/
::method init
self~validkeys = .string~digit
self~fieldhelpstring = "This field is numeric"


::class SystemQueue subclass RexxQueue
::method init
use strict arg command
command "| rxqueue"
self~init:super



Chris Date

unread,
Apr 27, 2020, 7:10:04 AM4/27/20
to
I would certainly like to use this TUI. Another good reason for me to switch from Regina to OORexx! Just need to decide whether to try version 4.2.0 or dive right into 5.0.0beta. Any thoughts? I use Debian Linux.

Chris

Bob Bobskin

unread,
Apr 27, 2020, 9:59:12 AM4/27/20
to

>
> I would certainly like to use this TUI. Another good reason for me to switch from Regina to OORexx! Just need to decide whether to try version 4.2.0 or dive right into 5.0.0beta. Any thoughts? I use Debian Linux.
>
> Chris

It's a bit of fun. I wrote it on oorexx 5 beta (I can't remember if I used any of the new functionality, I might have).

From my perspective, on various linux platforms, I am finding v5b working well.

I (hope) it displays some good OOP and OOD.

Bob Bobskin

unread,
Apr 27, 2020, 10:22:31 AM4/27/20
to
When one can add a password field with this amount of code, it's nice :)
(my partner disagrees, she looks bemused that anyone can be excited by a box you can type in, in a black screen, which does nothing).

::class passwordbox subclass inputbox
::method predraw
self~disdata = copies("*",self~length)
if self~hasFocus = .true then do
self~disdata = self~disdata ||" P "
end

Rony

unread,
Apr 29, 2020, 8:09:45 AM4/29/20
to
Hi Chris,

*very* impressive!

Had to test it on Windows where in principle it works (having ansicon installed, cf.
<https://github.com/adoxa/ansicon/releases> for the time being).

One thing that is interesting on Windows: in my case the screen buffer size is 135x3000 (width x
height) the window size is 135 x 45. It seems that the screen buffer size gets used on Windows. Also
the cursor positioning does not quite work (it is placed on the first column almost at the bottom),
though entering text will appear in the first field.

Hope you remain bored :), please keep up your interesting work!

---rony

Rony

unread,
Apr 29, 2020, 9:02:20 AM4/29/20
to
Hi Chris,

one hint ad Windows: SysTextScreenSize() got updated in ooRexx 5.0 and can be used to find out more
about the current command line window (and also to set the respective data).

Your current usage will return the "screen buffer size", which in my case is 135 x 3000.

The following Rexx program will demonstrate the new features and what they give in my case:

--- testscreen.rex - begin ---
-- screen buffer
parse value SysTextScreenSize() with rows columns

-- visible window size, positions in buffer (0-based)
parse value SysTextScreenSize("windowrect") with top left bottom right
say "current console buffer is" rows "rows by" columns "columns"
say "current window rectangle is ("top"," left") ("bottom"," right")" -
"(0-based, position in buffer)"

say "---"
say "sysTextScreenSize():" sysTextScreenSize()
say
do arg over ("buffersize", "windowrect", "maxwindowsize")
say "SysTextScreenSize("arg"):" sysTextScreenSize(arg)
end
--- testscreen.rex - end ---

Here the output:

--- output - begin ---
current console buffer is 3000 rows by 135 columns
current window rectangle is (0, 0) (44, 134) (0-based, position in buffer)
---
sysTextScreenSize(): 3000 135

SysTextScreenSize(buffersize): 3000 135
SysTextScreenSize(windowrect): 0 0 44 134
SysTextScreenSize(maxwindowsize): 56 135
--- output - end ---

After having used the command line screen for output from different commands, the buffer got filled
already and the rectangle that gets shown off the buffer is given e.g. as:

--- output - begin ---
current console buffer is 3000 rows by 135 columns
current window rectangle is (46, 0) (90, 134) (0-based, position in buffer)
---
sysTextScreenSize(): 3000 135

SysTextScreenSize(buffersize): 3000 135
SysTextScreenSize(windowrect): 52 0 96 134
SysTextScreenSize(maxwindowsize): 56 135
--- output - end ---

If you look up rexxref.pdf, 8.66. *CHG* SysTextScreenSize (Windows only), you will find more
information, also about setting/changing these values.

---

Another hint: as you are using ooRexx 5.0 (it is of release quality, faster, comes with very
interesting new features) you can also use ANSI/Regina style commands with the ability of
redirecting stdin, stdout and stderr from/to stems, but also from/to e.g. Rexx arrays.
rexxpg.ref,6.5 ADDRESS Instruction, gives an example of this new ooRexx feature, rexxref.pdf
documents it in full. You can therefore from now on forgo the " | rxqueue" pipe, if you want.


---rony






On 29.04.2020 14:09, Rony wrote:
> Hi Chris,
>
> *very* impressive!
>
> Had to test it on Windows where in principle it works (having ansicon installed, cf.
> <https://github.com/adoxa/ansicon/releases> for the time being).
>
> One thing that is interesting on Windows: in my case the screen buffer size is 135x3000 (width x
> height) the window size is 135 x 45. It seems that the screen buffer size gets used on Windows. Also
> the cursor positioning does not quite work (it is placed on the first column almost at the bottom),
> though entering text will appear in the first field.
>
> Hope you remain bored :), please keep up your interesting work!
>
> ---rony
>
>
>
> On 27.04.2020 16:22, Bob Bobskin wrote:

Gil Barmwater

unread,
Apr 29, 2020, 9:13:50 AM4/29/20
to
Rony,

I think the OP is Bob, not Chris. As for the issues you noted on
Windows, you should be aware of an open bug against the ANSI escape
sequence support. There is an easy workaround however; just issue a
"dummy" command to cmd.exe - like 'rem' - before you (or Bob's code)
issues any ANSI sequences. This will get the support enabled which it
automatically is for cmd.exe but not (yet) for ooRexx. HTH,

Gil B.
On 4/29/2020 8:09 AM, Rony wrote:
> Hi Chris,
>
> *very* impressive!
>
> Had to test it on Windows where in principle it works (having ansicon installed, cf.
> <https://github.com/adoxa/ansicon/releases> for the time being).
>
> One thing that is interesting on Windows: in my case the screen buffer size is 135x3000 (width x
> height) the window size is 135 x 45. It seems that the screen buffer size gets used on Windows. Also
> the cursor positioning does not quite work (it is placed on the first column almost at the bottom),
> though entering text will appear in the first field.
>
> Hope you remain bored :), please keep up your interesting work!
>
> ---rony
>
--
Gil Barmwater

Rony

unread,
Apr 29, 2020, 9:16:33 AM4/29/20
to
Bob, sorry for addressing you as "Chris", somehow the names got mixed-up in my head, apologies...

---rony

Gil Barmwater

unread,
Apr 29, 2020, 9:27:50 AM4/29/20
to
For anyone else who might be interested in using this on Windows, you
should be aware that Windows 10 since 2016 supports ANSI sequences to
the console. If you try it from ooRexx, however, it would appear to not
work. This is the bug I referenced below. But after ooRexx invokes CMD
the first time, the support becomes enabled so then ooRexx can use the
sequences.

On 4/29/2020 9:13 AM, Gil Barmwater wrote:
> Rony,
>
> I think the OP is Bob, not Chris. As for the issues you noted on
> Windows, you should be aware of an open bug against the ANSI escape
> sequence support. There is an easy workaround however; just issue a
> "dummy" command to cmd.exe - like 'rem' - before you (or Bob's code)
> issues any ANSI sequences. This will get the support enabled which it
> automatically is for cmd.exe but not (yet) for ooRexx. HTH,
>
> Gil B.

--
Gil Barmwater

Bob Bobskin

unread,
Apr 29, 2020, 2:15:31 PM4/29/20
to
Well, I have to admit, much to my partner's extreme annoyance, I had another day of boredom. I get bored easily... I've only got half a dozen businesses to run (including my IT / Legal compliance consultancy, an advertising agency which I run with my partner, an Immigration consultancy which I am trying to close which I ran with my ex-partner and a new online tabloid-esq newspaper which I've just launched... because I was bored and got even more bored of various editors and sub-editors taking my perfectly well argued freelance articles, and making a dogs dinner of them because they don't like content which is too aggressive).

For anyone looking for a read, or to write something knowing that our editors will correct your grammar and spelling, but will not try to censor your political views, check out my week old baby ... www.news-cyprus.com

Now back to the topic at hand.. (I know... I need a beer).

I am really quite surprised that this little thing has gone down so well. Last night, I sat down for a couple of hours, and made a series of improvements, which really helped with usability and the practicalness of the concept.

So here we go, v 0.12 of rexx tui, which now works in a cls file.


-- class file --
/*
tui.cls
Text Mode UI
v0.12
Tom Dyer fli...@anduin.net
2020

Free to use
*/

call rxFuncAdd "SysLoadFuncs", "REXXUTIL", "SysLoadFuncs"
call SysLoadFuncs


::class WindowManager public inherit SanityCheckSupported


::attribute running
::attribute logger
::attribute keyboard
::attribute screen
::attribute window
::attribute focusableitems
::attribute activeitem
::attribute activewindow
::attribute keybindabletasks
::attribute wmdms /* data management service for window manager, provides some validation etc */

::method init
expose logger screen
use arg logger
self~window = .list~new
self~focusableitems = .list~new
self~keybindabletasks= .list~new
screen= .screen~new()


::method run unguarded
expose keyboard screen
keyboard= .keyboard~new()
keyboard~logger= self~logger
keyboard~wm=self
self~findfocusable

self~drawwindows
self~logger~log("Within the WM code, about to poll keyboard",1)
self~keyboard~poll
self~logger~log("We have returned from polling the keyboard",1 )

return

/*
shortcode
*/
::method add
use arg obj
if .SanityChecker~new~check(obj ,self) = .false then return

if obj~isInstanceOf(.window) then do
self~addWindow(obj)
end

if obj~isInstanceOf(.keybindable) then do
self~addKeyBindableTask(obj)
end

if obj~isInstanceOf(.WMDataManagementService) then do
self~logger~log("added wdms",1)
self~wmdms = obj
end

if obj~isInstanceOf(.screen) then do
self~logger~log("added screen",1)
self~screen = obj
end

::method addKeyBindableTask
use arg keyBindableTask

illegaltobindkey1 = .key~new("[")
illegaltobindkey1~alt=.true
illegaltobindkey2 = .key~new("O")
illegaltobindkey2~alt=.true

if keyBindableTask~key~detail = illegaltobindkey1~detail | keyBindableTask~key~detail = illegaltobindkey2~detail
then do
self~logger~log("tried to add an illegal key binding, refused for" keyBindableTask~key~detail,1)
end
else do

self~logger~log("added keybindanble task to" keyBindableTask~key~detail,1)
self~keybindabletasks~insert(keyBindableTask)

end

::method addWindow
use arg window
if .SanityChecker~new~check(window,self) = .false then return
window~screen = self~screen
window~wm=self
self~window~insert(window)
if self~window~items=1 then do
self~activewindow = 0
end

::method gotoWindow
use arg windowNumber
self~activewindow= windowNumber
self~findfocusable


::method findfocusable
self~focusableitems = .list~new
do i = 0 to self~window~at(self~activewindow)~objects~items -1
if (self~window~at(self~activewindow)~objects~at(i))~isInstanceOf(.focusable) then do
self~focusableitems~insert(self~window~at(self~activewindow)~objects~at(i))
end
end
self~focusableitems~at(0)~hasFocus = .true
self~activeitem= self~focusableitems~at(0)



::method nextFocus
self~logger~log("Changing to next focus" self~focusableitems~items,0)

do i = 0 to self~focusableitems~items -1

if self~focusableitems~at(i)~hasFocus = .true then do
self~focusableitems~at(i)~lostFocus

if i = self~focusableitems~items -1 then do
self~focusableitems~at(0)~gotFocus
self~activeitem=self~focusableitems~at(0)
return
end
else do
self~focusableitems~at(i+1)~gotFocus
self~activeitem=self~focusableitems~at(i+1)
return
end
end
end



::method drawwindows
do i = 0 to self~window~items
self~window~at(self~activewindow)~draw
end


::method sendkey
use arg key
self~logger~log("WM - key pressed was" key~detail,0)

select
when key~alt= .true & upper(key~value) = "X" then do
self~keyboard~running= .false
self~screen~running = .false
self~logger~log("shutdown called",0)
end
when key~alt= .true & upper(key~value) = "R" then do
self~drawwindows
self~screen~draw
end
when key~tab=.true then do
self~nextFocus
end
when key~alt= .true then do
/*
send it to anything with global keybindability on alt
*/
do i = 0 to self~keybindabletasks~items -1
self~logger~log("WM sending keystroke to keybindabletask",2)
self~keybindabletasks~at(i)~sendkey(key)
end
end

otherwise
/*
then send the key to the active item
*/
self~activeitem~sendkey(key)

end

self~drawwindows


::class screen public inherit AlarmNotification SanityCheckSupported

::attribute logger
::attribute rows
::attribute columns
::attribute platform
::attribute running
::attribute data
::constant ansi '1b5b'x

::constant black 0
::constant red 1
::constant green 2
::constant yellow 3
::constant blue 4
::constant magenta 5
::constant cyan 6
::constant white 7
::constant grey 8
::constant brightred 9
::constant brightgreen 10
::constant brightyellow 11
::constant brightblue 12
::constant brightmagenta 13
::constant brightcyan 14
::constant brightwhite 15

::attribute recheckScreensize
::attribute forceClear


::method ansiclear
say self~ansi||'2J'

::constant isLinux linux
::constant isWindows windows

::method init
use arg logger
self~logger
self~data = .mutablebuffer~new()
self~recheckScreensize = 30 /* check every 30 seconds on repaint for screen size change */
self~forceClear = .false

if pos("Linux", SysVersion()) <> 0 then do
self~platform=self~isLinux
end
if pos("Windows", SysVersion()) <> 0 then do
self~platform =self~isWindows
end

self~getscreensize
self~setalarm


::method setalarm
if self~running = .false then exit
alarm = .alarm~new(1, self)


::method setcursor
use arg widget
/* ansi movement code in screen */
output=self~ansi||self~getPositionRow(widget)||";"self~getPositionColumn(widget)||"H "
return output

::method setcolour
use arg obj
output = ""
if obj~isInstanceOf(.coloured) then do
output = self~getColourForObject(obj)
end
return output


/*
relies on ansi colours working
*/
::method getColourForObject
use arg obj

if obj~fgcolour= "FGCOLOUR" then do
/* default */
output = self~ansi||"39m"
end
else do
output = self~ansi||"38;5;"||obj~fgcolour||"m"
end


/*
bgcolour doesn't work reliably
*/

if obj~bgcolour= "BGCOLOUR" then do
output = output /* don't deal with no bg colour */
end
else do
output = output||self~ansi||"48;5;"||obj~bgcolour||"m"
end




return output

::method getscreensize
expose rows columns

if self~platform = self~isLinux then do
/* linux */
do line over .SystemQueue~new("stty size ") ; nop ; end
parse var line rows columns
end

if self~platform = self~isWindows then do
/* not tested but should work eh */
parse value SysTextScreenSize() with rows columns
end


::method triggered

t = time(Seconds)
if t // self~recheckScreensize = 1 then do
self~getscreensize
end

self~draw

::method draw
if self~forceClear = .true then do
self~clear
end
say self~data
self~setalarm

::method clear
self~ansiclear
/* call SysCls */

::method getPositionRow
use arg widget
return widget~row

::method getPositionColumn
use arg widget
return widget~column

/*
Virtual Screen
This class treats the screen as if the itens were positioned on a 80/25 display and then moves the items to fit the actual display
*/

::class VirtualScreen public subclass Screen

::constant vColumns 80
::constant vRows 25

::method init
use arg logger
self~init:super(logger) /* normal init */
self~recheckScreensize = 5 /* set to check screen more frequently for size changes */
self~PlatformCheck /* run platform specific checks - this isolates some platform specific code */


::method PlatformCheck
if self~isLinux = .true then do
/*
Set the screen to run line by line, rather than char by char, used for high latency links
and ideal for making a programme which paints the entire screen at a time
function correctly.
*/

"stty extproc"

do line over .SystemQueue~new("echo $TERM") ; nop ; end /* $ */
parse var line terminal
if upper(terminal) = upper("LINUX") then do
/* I think we have been opened using a full screen window on a hard tty */
self~recheckScreensize = 60 /* so lets minimise the number of times we check the screen size to the minimum */

end

end



::method getPositionColumn
use arg widget
wcol = self~getPositionColumn:super(widget)
factor = self~columns / self~vColumns
rc=lineout("position", "factor c"||factor)

posC = (factor * wcol ) %1
rc=lineout("position", "C"||posC)
return posC


::method getPositionRow
use arg widget
wrow = self~getPositionRow:super(widget)
factor = self~rows / self~vRows
rc=lineout("position", "factor r"||factor)
posR = (factor * wrow)%1
rc=lineout("position", "R"||posR)
return posR

::class keyboard inherit SanityCheckSupported

::attribute logger
::attribute running
::attribute wm

::method poll unguarded

if self~running = .false then exit

/*
get key from keyboard
*/

k = sysgetkey("noecho")

/* now create an object for the key */

key = .key~new

/* now work out the correct treatment */

select
/* tab pressed, probably used for moving between fields */
when c2x(k) = .key~tabcode then do
key~tab=.true
key~value=""
end
/* backspace which will have special treatment */
when c2x(k) = .key~bscode then do
key~bs=.true
key~value=""
end
/* space needs some care as well */
when c2x(k) = .key~spacecode then do
key~space=.true
key~value=" "
end
/* if an alt button pressed, we need the next key to determine what was actually pressed */
/* special keyboard control characters such as F1 -> F12, and arrow keys need multiple buttons */
when c2x(k) = .key~altcode then do
k2 = sysgetkey("noecho")

select
when k2 = "O" then do
/* We've got a special control character such as fn buttons which use 3 parts */
k3 = sysgetkey("noecho")
/* note, you cannot bind to ALT-O alone */

select
when k3 = .key~fn1code then do
key~fn1 = .true
end
when k3 = .key~fn2code then do
key~fn2 = .true
end
when k3 = .key~fn3code then do
key~fn3 = .true
end
when k3 = .key~fn4code then do
key~fn4 = .true
end
when k3 = .key~fn5code then do
key~fn5 = .true
end
when k3 = .key~fn6code then do
key~fn6 = .true
end
when k3 = .key~fn7code then do
key~fn7 = .true
end
when k3 = .key~fn8code then do
key~fn8 = .true
end
when k3 = .key~fn9code then do
key~fn9 = .true
end
when k3 = .key~fn10code then do
key~fn10 = .true
end
when k3 = .key~fn11code then do
key~fn11 = .true
end
when k3 = .key~fn12code then do
key~fn12 = .true
end
otherwise do

end
end
end
when k2 = "[" then do
/* We've got a special control character such as an arrow key which uses 3 parts */
k3 = sysgetkey("noecho")
select
when k3 = .key~backtabcode then do
key~backtab = .true
end
when k3 = .key~uparrowcode then do
key~uparrow = .true
end
when k3 = .key~downarrowcode then do
key~downarrow = .true
end
when k3 = .key~rightarrowcode then do
key~rightarrow = .true
end
when k3 = .key~leftarrowcode then do
key~leftarrow = .true
end
/* we are not handling these keys */
otherwise do
nop
end
end
key~value=""
end
/* the key that was pressed was a standard ALT key such as ALT-P which uses 2 parts */
otherwise do
key~alt=.true
key~value= k2
end
end
end
/* it seems that it was a normal keystroke, so let's just set the value */
otherwise do
key~value = k
end
end


self~logger~log("keyboard class sending " key~detail(), 2)
/*
Now hand the job of working out what to do to the Window Manager
*/
self~wm~sendkey(key)

/*
and kick off the poll again...
*/

self~poll


/*

Class for keys used to handle input

*/

::class key public
::constant altcode "1B"
::constant tabcode "09"
::constant spacecode "20"
::constant bscode "7F"
::constant backtabcode "5A" /* 1B 5B 5A */
::constant uparrowcode "41" /* 1B 5B 41 */
::constant downarrowcode "42" /* 1B 5B 42 */
::constant rightarrowcode "43" /* 1B 5B 43 */
::constant leftarrowcode "44" /* 1B 5B 44 */
::constant fn1code
::constant fn2code
::constant fn3code
::constant fn4code
::constant fn5code
::constant fn6code
::constant fn7code
::constant fn8code
::constant fn9code
::constant fn10code
::constant fn11code
::constant fn12code

::attribute alt
::attribute tab
::attribute value
::attribute space
::attribute bs
::attribute uparrow
::attribute downarrow
::attribute rightarrow
::attribute leftarrow
::attribute backtab
::attribute fn1
::attribute fn2
::attribute fn3
::attribute fn4
::attribute fn5
::attribute fn6
::attribute fn7
::attribute fn8
::attribute fn9
::attribute fn10
::attribute fn11
::attribute fn12


/*
Comparison method for keys
*/
::method "="
use arg obj

/*
By default they don't match
*/

rc = .false

select
when obj~isInstanceOf(.Key) then do
/* if both keys then compare detail */
if obj~detail = self~detail then rc = .true
end
when obj~isInstanceOf(.String) then do
/* if one is a string and the other is special, they can't match */
if self~isSpecial = .true then rc = .false
else do
/* if it's not a special, then try comparing the letters themselves caseless*/
if upper(obj)= upper(self~value) then rc = .true
end
end
otherwise
rc = .false
end

return rc



::method get
expose value
return value

::method isSpecial
if self~alt = .true | self~tab = .true | self~space = .true | self~bs = .true | self~uparrow = .true | self~downarrow = .true | self~rightarrow = .true | self~leftarrow = .true | self~backtab = .true | self~fn1 = .true | self~fn2 = .true | self~fn3 = .true | self~fn4 = .true | self~fn5 = .true | self~fn6 = .true | self~fn7 = .true | self~fn8 = .true | self~fn9 = .true | self~fn10 = .true | self~fn11 = .true | self~fn12 = .true then return .true
else
return .false

::method init
use arg kv=""
self~value= kv

/*
detail of the key - in long form
*/
::method detail
return self~value c2x(self~value) self~alt self~tab self~space self~bs self~uparrow self~downarrow self~rightarrow self~leftarrow self~backtab self~fn1 self~fn2 self~fn3 self~fn4 self~fn5 self~fn6 self~fn7 self~fn8 self~fn9 self~fn10 self~fn11 self~fn12



/*
This isn't anywhere near working but if you uncomment the lineouts.. you can get some output
*/

::class logger public
::attribute logdata
::attribute running

::method init
expose stem
use arg stem=.nil

self~logdata = .list~new

::method log
expose loglevel
use arg message, level
if level > loglevel then do
self~logdata~insert(message)
rc=lineout("logfile",message)
end

::method setloglevel
expose loglevel
parse arg loglevel

::method unknown
expose stem
use arg msg, text
stem[0] += 1
index = stem[0]
stem[index] = text~makeString
self~log(msg,9)
self~log(text,9)
self~log(text~makeString,9)


/*
A window to the soul
*/

::class Window public inherit SanityCheckSupported coloured
::attribute name
::attribute screen
::attribute objects
::attribute wm
::attribute logger

::method init
self~objects = .list~new

::method draw
self~makewindow
self~screen~data = self~makewindow

::method maketitlebar
titlebar = center( self~name, self~screen~columns, "*")
return titlebar

::method makewindow
d = self~screen~setcolour(self)
d = d||self~maketitlebar
do i = 1 to self~screen~rows-2
d = D||"*" || right("*",self~screen~columns-1," ")
end

time = time()
d = d||center(time, self~screen~columns, "*")

do i = 1 to self~objects~items
d=d||self~screen~setcolour(self~objects~at(i-1))
d=d||self~screen~setcursor(self~objects~at(i-1))
d=d||self~objects~at(i-1)~draw
end

return d

::method add
use arg obj
if .SanityChecker~new~check(obj,self) = .false then return
if obj~isInstanceOf(.widget) then do
self~objects~insert(obj)
end
if obj~isInstanceOf(.keybindable) then do /* bind against the wm instead - fix for common error */
self~wm~add(obj)
end


::class logwindow subclass window
::attribute logger

::method draw
super~draw
do i = 1 to logger~logdata~allItems
self~screen~data(logger~logdata[i])
end








/*
The standard widget class

*/
::class widget inherit coloured
::attribute row
::attribute column
::attribute data
::attribute disdata
::attribute validator

::method predraw
self~disdata = self~data

::method draw
self~predraw
return self~disdata

::method init
expose data row column
use arg data, row , column
row = row + 2
column = column + 2

::method add
use arg obj

if .SanityChecker~new~check(obj) = .false then return

if obj~isInstanceOf(.validator) = .true then self~validator = obj
/* assume that any label being added to a widget is actually the field help field */
if obj~isInstanceOf(.label) = .true then self~validator~fieldhelp = obj
/* assume that we are trying to set the field help */
if obj~isInstanceOf(.string) = .true then self~validator~fieldhelpstring = obj




/* labels are boring but necessary */

::class label public subclass widget

/*everyone needs a button */

::class button public subclass widget inherit focusable focuscoloured
::attribute length
::attribute task

::method init
expose length
use arg data, row, column, length
self~init:super(data,row, column)

::method predraw
self~disdata = "["||center(self~data,self~length-2," ")||"]"

if self~hasFocus = .true then do
self~disdata = self~disdata ||" * "
end

::method sendkey
self~task~run


/*

Input box

*/


::class inputbox public subclass widget inherit focusable clearable focuscoloured
::attribute length
::attribute fixedlength
::attribute showlength

::method init
expose data row column length
use arg data, row, column, length
self~validator = .validator~new() /* default validator used */
self~fixedlength = .nil
self~init:super(data, row, column)

::method predraw
self~disdata = left(self~data,self~length,"_")
if self~hasFocus = .true then do
self~disdata = self~disdata ||" * "
end

if self~showlength= .true then do
if self~fixedlength = .true then do
self~disdata = self~disdata length(self~data)||"/"||self~length
end
else self~disdata = self~disdata length(self~data)
end

::method sendkey
use arg key
if key~bs = .true & length(self~data ) > 0 then do
self~data = left(self~data, length(self~data)-1 )
end
else do
if self~validator~isNil = .false then do
if self~validator~isValidKey(key) = .false then key~value = ""
end
/*
can't type too much in a fixed length field
*/
if self~fixedlength = .true then do
if length(self~data)+1 > self~length then key~value = ""
end
self~data = self~data||key~value
end

/*

*/
::class searchbox public subclass inputbox
::attribute list
::attribute keyed
::attribute matching

::method init
use arg data, row, column, length
self~keyed=""
self~matching=0
self~init:super(data,row,column,length)

::method sendkey
use arg key
self~sendkey:super(key)
self~matching = 0

if key~bs = .true then do
if length(self~keyed) >= 1 then do
self~keyed = strip(left(self~keyed,length(self~keyed)-1))

end
end
else do
self~keyed = self~keyed||key~value
end

do i = 0 to self~list~items -1
if upper(self~keyed) = upper(left(self~list~at(i),length(self~keyed))) then do
self~matching = self~matching + 1
self~data = self~list~at(i)
end
end

if self~matching = 0 then self~data = ""


::method predraw
self~predraw:super()
self~disdata = self~disdata || "Matching " self~matching


/*
Password box
*/


::class passwordbox public subclass inputbox
::method predraw
self~disdata = left(copies("*",self~data~length),self~length,"_")
if self~hasFocus = .true then do
self~disdata = self~disdata ||" P "
end


/*
Radio box
*/


::class radiobox public subclass inputbox
::attribute options
::attribute selected

::method init
use arg opts, row, column, length
if .SanityChecker~new~check(opts) = .false then do
self~lackingrequiredobject = .true
end

self~options= .list~new
self~options= opts
self~selected = 0
self~init:super(self~options~at(0),row, column, length)

::method predraw
self~disdata = ""
do i = 0 to self~options~items -1
if i = self~selected then do
self~disdata = self~disdata || " X " || self~options~at(i)
end
else do
self~disdata = self~disdata || " . " || self~options~at(i)
end
end
if self~hasFocus = .true then do
self~disdata = self~disdata ||" * "
end

::method sendkey
self~selected = self~selected + 1
if self~selected = self~options~items then self~selected = 0
self~data = self~options~at(self~selected)

::method clear
self~selected = 0
self~data=self~options~at(0)


/*
data entry validators
*/

::class validator public
/*
normal validator letters and numbers
*/
::attribute validkeys
::attribute fieldhelpstring
::attribute fieldhelp

::method init
use arg fieldhelp=.nil
self~validkeys = .string~graph || " £"
self~fieldhelpstring = "This field is alphanumeric"
self~fieldhelp = fieldhelp

::method isValidKey
use arg key

if self~fieldhelp~isNil = .false then do
self~fieldhelp~data = self~fieldhelpstring
end

if pos(key~value, self~validkeys) > 0 then return .true
else return .false

::method fieldContentValidate
use arg widget
if widget~isInstanceOf(.validatable) then do
self~validatedata(widget)
end



::class numericvalidator public subclass validator
/*
numeric validator
*/
::method init
use arg fieldhelp=.nil
self~init:super(fieldhelp)
self~validkeys = .string~digit
self~fieldhelpstring = "This field is numeric only"




/*
mixinclasses here

*/

::class focusable mixinclass object
::attribute hasFocus

::method gotFocus
self~hasfocus = .true
if self~isInstanceOf(.focuscoloured) then do
self~changetoFocusColour()
end


::method lostFocus
self~hasfocus = .false
if self~isInstanceOf(.focuscoloured) then do
self~changetoDefaultColour()
end


/*

*/
::class validatable mixinclass object
::attribute valid



/*
clears fields to have blank by default. radio boxes need different treatment and to override this
*/

::class clearable mixinclass object
::method clear
self~data = ""


/*
allows items to be coloured
*/
::class coloured mixinclass object
::attribute fgcolour
::attribute bgcolour

/*
focus coloured
*/

::class focuscoloured mixinclass object inherit coloured
::attribute focusfgcolour
::attribute defaultfgcolour

::method changetoFocusColour
if self~focusfgcolour != "FOCUSFGCOLOUR" then do
self~fgcolour = self~focusfgcolour
end

::method changetoDefaultColour
if self~defaultfgcolour != "DEFAULTFGCOLOUR" then do
self~fgcolour = self~defaultfgcolour
end



/*
used to make keybindable tasks
*/
::class keybindable mixinclass object
::attribute key

::method sendkey
use arg keypressed
if upper(self~key~value) = upper(keypressed~value) then do
self~run()
end

::class SanityCheckSupported mixinclass object

::method SanityCheckLog
use arg msg
self~logger~log(self " " message)


/*
because it's nice to have nice code
*/

::class SystemQueue subclass RexxQueue
::method init
use strict arg command
command "| rxqueue"
self~init:super

/*
crashfile
*/

::class trapout public
::method init
expose stem
use arg stem


::method unknown
expose stem
use arg msg, text
stem[0] += 1
index = stem[0]
stem[index] = text~makeString
.error~destination
call lineout("crashfile", msg " " text~makeString)



/*
Because it's not particularly easy if you insert a misformed object or widget, this SanityChecker is called to ensure that objects are correctly initialised
prior to adding them to a window, windowmanager, task etc.
*/

::class SanityChecker
::attribute errorcheckcode

::method check
use arg obj, callingobj=.nil
if callingobj <> .nil then do
if callingobj~isInstanceOf(.SanityCheckSupported) then do
response = self~performcheck(obj)
if response = .false then do
callingobj~SanityCheckLog("Problem "self~errorcheckcode "with object" obj "called from" callingobj)
end
end
end
else do /* not able to notify back */
response = self~performcheck(obj)
end

return response

::method performcheck
expose checkerrorcode
use arg obj

if obj = .nil then do
say "SanityChecker failed on" obj
return .false
end
return .true




/*
override this to perform a job when action occurs
*/
::class Task public
::attribute window

::method init
use arg window
self~window= window

::method run
/* override this method */



/*
example task to clear the fields
*/
::class clearEntryTask public subclass Task

::method run
do i = 0 to (self~window~objects~items -1)
if self~window~objects~at(i)~isInstanceOf(.clearable) = .true then do
self~window~objects~at(i)~clear
end
end

/*
Task to move to a specific window
*/

::class goToWindowTask public subclass Task
::attribute windownumber

::method init
use arg win, windownumber
self~windownumber = windownumber
self~init:super(win)

::method run
self~window~wm~gotoWindow(self~windownumber)



/*
this isn't a button, this is registered against a keystroke. Works the same way though
*/
::class goToWindowTaskkey public subclass goToWindowTask inherit keybindable




::class printAllFieldsToFile public subclass Task

::method init
use arg win
self~init:super(win)

::method run
do i over self~window~wm~wmdms~allIndexes
call lineout "fields", i " " self~window~wm~wmdms~at(i) " " self~window~wm~wmdms~at(i)~data
end



/*
WindowManager Data Management Service
Provides information on whether fields and windows have been displayed, whether they validated, and allows the registration of certain data requirements
to help for timely development
at the moment it's just a directory of fields
*/


::class WMDataManagementService public subclass Directory

::method getValue
use arg obj
field = self~get(obj)
if field~isNil = .false then return .nil
else do
return field~data
end




===================== END CLASS FILE =====================

===================== Test Application ===================


/*
rexxTUI.rex
Test rexx app for UI
version 0.12

*/

/*
some code to get a crash file in case everything goes pearshaped
*/
out.0 = 0 -- create the stem
pdest = .error~destination(.trapout~new(out.))

/*
main example code
*/

/*
create the window manager and set logging level
*/

wm = .windowmanager~new(.logger~new)
wm~logger~setloglevel(-1)


wmdms =.WMDataManagementService~new

/*
create a window
*/


win = .window~new
win~logger=wm~logger
win= .window~new
win~name = "Window number 1"


win~bgcolour= .screen~blue
win~fgcolour= .screen~white

secondwin= .window~new
secondwin~name = "Window number 2"
secondwin~logger = wm~logger
secondwin~bgcolour= .screen~blue
secondwin~fgcolour= .screen~brightyellow

/*
create some elements for the windows
*/


label1 = .label~new("this is a label",2,1)
label2 = .label~new("a second label", 3,1)
input1 = .inputbox~new("", 2, 20, 30)
input1~fixedlength=.false
input1~showlength=.true

input1~focusfgcolour = .screen~brightgreen

input2 = .inputbox~new("", 3, 20, 30)
input2~showlength=.true
input2~fixedlength=.true

input2~fgcolour = .screen~green
input2~focusfgcolour = .screen~brightyellow

input3 = .passwordbox~new("", 4, 20, 30)
input3~fgcolour = .screen~green
input3~focusfgcolour = .screen~brightgreen

radiooptions = .list~new
radiooptions~insert("Free like beer")
radiooptions~insert( "Free like speech")
radiooptions~insert("Freedom to bare arms")
input4 = .radiobox~new(radiooptions, 5,20,40)
input4~fgcolour = .screen~yellow

fieldhelplabel = .label~new("** I am sure I am not here **",15,20)


input1s2 = .inputbox~new("",2, 20, 30)
input2s2 = .searchbox~new("", 3, 20, 30)
input2s2~list=.list~new()
input2s2~list~insert("Dasha")
input2s2~list~insert("Dusy")
input2s2~list~insert("Thomas")
input2s2~list~insert("Bob")
input2s2~list~insert("Marina")
input2s2~list~insert("Martin")
input2s2~list~insert("Arthur")
input2s2~list~insert("Tamsyn")

input2s2~fgcolour=.screen~brightyellow




/*
set up a couple of field validators
The
*/

input1~add(.validator~new(fieldhelplabel))
input2~add(.numericvalidator~new(fieldhelplabel))



/*
keyboard listening tasks which executes code ... the same tasks can also be used on buttons
*/

task1= .goToWindowTaskKey~new(win,1)
task1~key=.key~new("N")
task1~key~alt=.true

task2= .goToWindowTaskKey~new(win,0)
task2~key=.key~new("P")
task2~key~alt=.true

/*
make some buttons which have tasks assigned to them.
*/

okButton= .button~new("OK",10,20,25)
okButton~task=.goToWindowTask~new(win,1)


okButton2= .button~new("OK",10,20,25)
okButton2~task= .goToWindowTask~new(secondwin,0)

print2= .button~new("Print to File",10,50,25)
print2~task=.printAllFieldsToFile~new(secondwin)

clearTask= .clearEntryTask~new(win)
clearButton= .button~new("CLEAR",10,50,25)
clearButton~task= cleartask


/* add all the widgets to the windows */

/* the first window */

win~add(label1)
win~add(label2)
win~add(input1)
win~add(input2)
win~add(input3)
win~add(input4)
win~add(okButton)
win~add(clearButton)
win~add(fieldhelplabel)


/* and the second window */

secondwin~add(label1)
secondwin~add(label2)
secondwin~add(input1s2)
secondwin~add(input2s2)
secondwin~add(okButton2)
secondwin~add(print2)


/* including something illegal here */
wm~add("qaer") /* The system is effectively going to ignore this, treating it as a string against the WM */

/*
This is the population of the data management table with
the field objects, along with the lookup name

For a framework, the creation of a WMDMS service
seems somewhat counterintuitive, until you think of the common use cases.
This allows a single location, where one can define criteria as to which fields must be completed,
which are are optional etc, and have the WM system understand the status of these.
Then by virtue of setting up rules, validation is easier. It also to act as a simple method to fetch
the current data values of the fields, by name, to avoid having to programatically trawl through each window to identify
where a given field is. In my experience, due to design changes, fields often move around screens, so having them all centrally
stored saves substantial time when refactoring, as it means that you don't have to change lots of code when you make minor layout changes
*/

wmdms~put(input1,"input1")
wmdms~put(input2,"input2")
wmdms~put(input3,"input3")
wmdms~put(input4,"input4")
wmdms~put(input1S2,"input1S2")
wmdms~put(input2S2,"input2S2")

/*
create a virtual screen and add it to the wm
The purpose of a virtual screen is to allow you to lay out items based upon a 80x25 screen layout,
and have it adapt if the screen layout is running in a window, and therefore can change.
It should be noted that it does not change the size of widgets (as in, the length of them). They are still "FIXED" size,
but it adapts where they are placed on the screen in an attempt to ensure that they are reasonably sensibly placed.

The code currently makes some attempts to try to deal with resizing of the display (window) during runtime, but the code
does not check for this event all the time, and as such, it can take a little while to adapt.
*/
vscreen = .VirtualScreen~new()
wm~add(vscreen)

/*add windows to window manager along with two key activated tasks*/
wm~add(win)
wm~add(secondwin)

/*
add the keyboard bound tasks to the window as well, in this case to go to screen 1 and screen 2
*/

wm~add(task1)
wm~add(task2)

/*
add the data management serviec to the window manager
*/
wm~add(wmdms)

/* run window manager */

wm~run()

/* once wm has stopped the following will run */

wm~logger~log( "Window Manager is shutdown",1)

do i = 1 to 3
say "shutdown "
call syssleep(1)
end

/*
ensure that this thread dies by killing all rexx running, rather brutal but this is a test application isn't it.
*/
"killall -9 rexx"

/* we are done */
exit




::requires "tui.cls"

Bob Bobskin

unread,
Apr 29, 2020, 2:18:09 PM4/29/20
to
Gil,

Thanks. I think I've got "just the location" to insert this "fix" in the new code base, in a bit of per platform code.

Bob Bobskin

unread,
Apr 29, 2020, 2:21:17 PM4/29/20
to
I don't have a windows machine to try it with, and whilst I did put in some (best guess) code to try to make it work on Windows, I wasn't really able to check.

Given that there is some interest, I think I'll aim to get bored again, and upload it to github or something as a project, so that people can "improve" it.

Best

Bob Bobskin

unread,
Apr 30, 2020, 11:56:22 AM4/30/20
to
Thanks for all the appreciative comments...
Uploaded it to github

https://github.com/BobBobskin/oorexxTUI

Now with a working slider, the F1 - F10 keybinding working in linux full screen and linux windowed (different keyboard codes eh)

And a reasonable little demo.

Bob Bobskin

unread,
May 7, 2020, 6:37:00 AM5/7/20
to
Substantial update made to improve behaviour.
Would love to have some test feedback on it.

Bob Bobskin

unread,
May 10, 2020, 4:57:44 PM5/10/20
to
Hopefully, my recent updates will make it work a bit better, and be a bit more tolerant on errors.

Updated a bit.

I have a question, has anyone made a multi platform version of winsystm.cls?
Reply all
Reply to author
Forward
0 new messages