/*
 * cy	Copyright © 1995 by Rick Younie
 *		Freely Distributable.
 *		Do not distribute your changes please.
 *
 * $VER: Rick Younie  10 March 1995
 *		rick@freenet.vancouver.bc.ca (quicker)
 *		rick@emma.panam.wimsey.com (may be more reliable)
 *
 * USAGE: '?' for usage
 *
 * SYNOPSIS: 
 *	- Cybercron management program
 *
 */
	signal on BREAK_C
	signal on HALT

/*	-------------------------------------------------------------------
 *	constants and assigns
 */
	/* set these for your system */

	screenwidth		= 640
	screenheight	= 200
	editor			= 'C:ed -sticky'	/* editor must hang on to CLI */
	CyberCron		= 'CyberCron'		/* path+name of CC program */
	crontab			= 'SYS:s/CronTab'
	sendmail		= '"sendmail -f %s -R *"%s*""'
	logfile			= 't:CyberCron.log'

	/* end settings */


	lf				= '0a'x
	cr				= '0d'x
	crlf			= cr || lf
	csi				= '9b'x
	rows			= screenheight % 8 - 2
	cols			= screenwidth % 8 - 1
	startcmd		= CyberCron 'CRONTAB' crontab,
								'SENDMAIL' sendmail

/*	-------------------------------------------------------------------
 *
 */
Main:
	address 'CYBERCRON'
	options results
	options failat 99
	arg option

	select
		when option = '-N' then call NextEvent
		when option = '-L' then call ListEvents

		when option = '-E' then call Edit

		when option = '-B' then call StartCyber
		when option = '-Q' | option = '-K' then call KillCyber

		when option = '-S'
			then if Suspend_Resume('SUSPEND') then say ShowStatus()
		when option = '-R'
			then if Suspend_Resume('RESUME') then say ShowStatus()

		when option = '' then call RawStuff

		otherwise Call Usage
	end
	exit 0

/*####################################################################
  ##                         raw window                             ##
  ####################################################################*/

/*  -----------------------------------------------------------------*/

RawStuff:

	cursorUp		= '41'x
	cursorDown		= '42'x
	cursorRight		= '43'x
	cursorLeft		= '44'x

	if ~Running()
		then call ErX "..CyberCron not running - '?' for Usage"

	call MakeRaw

Refresh:
	call WriteSkel			/* write header,footer */

ReScan:
	numevents = ReadEvents()		/* get all queued events from CC */

	if numevents = 0	then page = 0
						else page = 1
	row				= 1

	itemsPerPage	= rows - 8
	itemsLastPage	= numevents // itemsPerPage
	maxpage			= numevents % itemsPerPage + (itemsLastPage~=0)

	items.			= itemsPerPage
	items.0			= 1
	if itemsLastPage = 0	then items.maxpage = itemsPerPage
							else items.maxpage = itemsLastPage

	if numevents~=0 then call WritePage
	else call WriteSkel

	/* we spend most of our time here, handling keystrokes
	 *
	 */
	do forever

		oldpos = row
		char = readch('STDIN', 1)

		select

			/* handle cursor keys */
			when char = '9b'x then do
				char = readch('STDIN', 1)
				select
					when numevents=0 then nop
					when char = cursorDown then do
						row = row + 1
						if row > items.page then row = 1
						call MoveCursor
					end
					when char = cursorUp then do
						row = row - 1
						if row = 0 then row = items.page
						call MoveCursor
					end
					when char = cursorRight then do
						if page ~= maxpage then do
							page = page + 1
							call WritePage
						end
					end
					when char = cursorLeft then do
						if page > 1 then do
							page = page - 1
							call WritePage
						end	
					end
					otherwise nop
				end

			end		/* cursor keys */

			/* not a cursor key */
			otherwise do
				select
					when char = 's'
						then if Suspend_Resume('SUSPEND') then call WriteStatus
					when char = 'r'
						then if Suspend_Resume('RESUME') then call WriteStatus
					when char = 'a' then call AddEvent('NORM')
					when char = 'A' then call AddEvent('CC')
					when char = 'e' then call EditCrontab
					when char = 'n' then signal Refresh /* ReScan? */
					when char = 'q' then do
						call CLineMsg '..really quit? Y/n '
						char = readch('STDIN',1)
						if upper(char)~='N' then leave
						call MsgAndDelay '..quit aborted'
					end
					when char = 'g' then call ToggleLogFile

					/* dont allow the following options if no events */
					when numevents = 0 then nop

					when char = 't' then do
						tmp = (page-1)*itemsPerPage + row
						tag.tmp = (tag.tmp=0)	/* toggle flag */
						if tag.tmp	then star = '*'
									else star = ' '
						call writech 'STDOUT',csi || row+2';3H'star
						row = row + 1
						if row > items.page then row = 1
						call MoveCursor
					end

					/*	space bar follows Tin behavior
					 *		>1 page?
					 *			- if ~last page, go to next
					 *			- else if not at end of page, go to end
					 *					- else go to first page
					 *		=1 page; if not at bottom, go there
					 *					else go to top
					 */
					when char = ' ' then do
						if maxpage > 1 then do
							if page ~= maxpage then do
								page = page + 1
								call WritePage
							end
							else do
								if row ~= items.page then do
									row = items.page
									call MoveCursor
								end
								else do
									page = 1
									call WritePage
								end
							end
						end
						else do	/* only 1 page; go to bot/top */
							if row = items.page then row = 1
												else row = items.page
							call MoveCursor
						end
					end

					when char = 'd' then call DeleteEvent
					when char = 'p' then call PurgeRexx
					otherwise nop
				end
			end

		end /* select */

		call PrintTime

	end	/* forever */

	exit 0

MoveCursor:
	call writech 'STDOUT', csi || oldpos+2';1H  '
	call writech 'STDOUT', csi ||    row+2';1H->' || csi || rows';1H'
	return
	
CLineMsg:
	call writech 'STDOUT', csi || rows';1H'csi'K   'arg(1) || cr
	return

/*  -------------------------------------------------------------------
 *	
 */
WriteSkel:
	prompt.1		= '[s]uspend [r]esume  [t]ag [d]elete  [e]dit_crontab resca[n]  lo[g]_file'
	prompt.2		= '<=prev page  (spacebar)  next page=>  [aA]dd  [q]uit  [p]urge_rexx_events'

	'VERSION'
	title	= 'CyberCron' word(result,1)

	call writech 'STDOUT', csi'H'csi'J' || center(title,cols) || crlf
	call writech 'STDOUT', csi || rows - 4';1H'center(prompt.1,cols)
	call writech 'STDOUT', csi || rows - 3';1H'center(prompt.2,cols)

WriteStatus:
	call writech 'STDOUT', csi || rows - 2';1H'center(ShowStatus(),cols)
	call CLineMsg ''

PrintTime:
	call writech 'STDOUT', csi || '1;4H'time()'  'date('E') || csi || rows';1H'
	return

/*  ------------------------------------------------------------------- */
WritePage:

	tmp = right('p'page 'of' maxpage,10)
	call writech 'STDOUT', csi || '1;'cols-length(tmp)'H'tmp

	offset = (page-1)*itemsPerPage	/* offset into event.i */

	/* write this pages events to window */
	do i = 1 to items.page
		tmp = offset + i
		if tag.tmp	then star = '*'
					else star = ' '
		call writech 'STDOUT', csi || i+2';1H'csi'K  'star || event.tmp || crlf
	end i

	/* clear leftover lines */
	do i = items.page+1 to itemsPerPage + 1
		call writech 'STDOUT', csi || i+2';1H'csi'K'
	end i

	/* reset to first entry whenever page is redrawn */
	row = 1
	call writech 'STDOUT', csi || row+2';1H->'

WipeBottom2:
	/* wipe 2 command lines, positions cursor at bottom */
	call writech 'STDOUT', csi || rows-1';1H'csi'J'csi || rows';1H'
	return

/*  ----------------------------------------------------------------
 *	ssc_id.i = 12345678'ext' 12345678 0x12345678    for the sort
 *		- ext=ext+1 for every CC event read so :OBEYQUEUE-type
 *            events dont get mixed up by the sort
 *		- shifted to just 'ssc id' after the sort
 *	event.i = time date event_to_execute
 */
ReadEvents:

	tag.		= 0			/* clear tags on a new read */

	/* get all events in CC-id format */
	'LIST_EVENTS'
	list = result

	if left(list,2)~='0x' then ssc_id.0 = 0	/* no events queued */

	else do
		if option = '' then call CLineMsg '..getting events'
		/* fill 'sssc.ext sssc 0x' array for sort */
		ssc_id.0 = words(list)
		do i = 1 to ssc_id.0
			/* get time for its next execution */
			id = word(list,i)
			'EVENT_NEXT_EXEC' id
			ssc =  result
			if ssc = '' then tmp = 0 		/* could not be found */
			tmp = ssc || right('0'i,2)	/* 01,02..added to events
							so QUEUE events of same time dont scramble */
			ssc_id.i = tmp ssc id
		end i

		if option = '' then call CLineMsg '..sorting'

		/* one of Knuth's sort algorithms
			- rony@wu-wien.ac.at (Rony G. Flatscher) */
		m = 1
		do while (9 * m + 4) < ssc_id.0
			m = m * 3 + 1
		end
	
		do while m > 0
			k = ssc_id.0 - M
			do j = 1 to k
				q = j
				do while q > 0
					l = q + m
					if ssc_id.q <= ssc_id.l then leave
					/* switch elements */
					tmp		= ssc_id.q
					ssc_id.q = ssc_id.l
					ssc_id.l = tmp
					q = q - m
				end
			end
			m = m % 3
		end		/* sort */

		if option = '' then call CLineMsg '..converting'

		/* now fill event.i with events sorted as to time */
		today = date('E')
		scnwidth = cols - 4
		do i = 1 to ssc_id.0

			parse var ssc_id.i . ssc id
			/* lose the ssc+extension variable to save a little confusion */
			ssc_id.i = ssc id

			'SHOW_EVENT' id
			event.i = subword(result,7)		/* the event */

			'EXPAND_SSSC' ssc
			parse var result . min hr day mon yr .
			date = right('00'day,2)'/'right('00'mon,2)'/'right(yr,2)
			if date = today then date = '        '

			/* truncate for screen */
			event.i = right('00'hr,2)':'right('00'min,2) date event.i
			if length(event.i) > scnwidth then event.i = left(event.i,scnwidth)
		end i

	end

	return ssc_id.0

/*  -------------------------------------------------------------------
 *
 */
PurgeRexx:
	'PURGE_REXX_EVENTS'		/* no RC? */
	call MsgAndDelay '..rexx events purged'
	signal ReScan

/*  -------------------------------------------------------------------
 *
 */
ToggleLogFile:

	if right(ShowStatus(),2) = 'ON' then 'CLOSE_LOG_FILE'
	else 'NEW_LOG_FILE' logfile

	if RC ~= 0 then call MsgAndDelay '..error toggling' logfile

	signal WriteStatus

	return

/*  -------------------------------------------------------------------
 *
 */
EditCrontab:
	call CLineMsg '..editing' crontab
	if Edit()~=0 then do
		call delay 3*50
		signal Refresh	/* dos can mess up window with err msg */
	end
	signal ReScan

Edit:
	address 'COMMAND' editor crontab
	return RC

/*  -------------------------------------------------------------------
 *	NORM	= hh:mm event
 *	CC		= min hour day month dow(Sun=0) event
 */
AddEvent:
	parse arg type

	prompt.NORM	= 'hh:mm'
	prompt.CC	= 'min hr day mon dow(Sun=0)'
	prompt = csi || rows-1';1H'csi'K'prompt.type' event'csi || rows';1H'

	do forever
		call writech 'STDOUT',prompt
		string = GetEvent()
		if string = '' then do
			call MsgAndDelay '..aborting - no add'
			signal WipeBottom2
		end
		if type = 'NORM' then do
			parse var string hr ':' min event
			event = strip(event)
			string = min hr '* * *' event
		end

		'ADD_EVENT' string
		
		/* event was added ok - go get new lineup from CC */
		if RC=0 then do
			call MsgAndDelay 'ok'
			signal ReScan
		end

		else do			/* not added - try again */
			call writech 'STDOUT', csi || rows-1';33H **error** [ <ret> to continue ]'
			call readch 'STDIN', 1
			call writech 'STDOUT', csi || rows-1';1H'csi'J'csi || rows
		end

	end
	return

/*  ----------------handle user add_event input---------------------- */
GetEvent:
	string = ''

	do forever
		char = readch('STDIN',1)
		select
			when char>'1f'x & char < '7f'x then do
				if length(string) <= cols-2 then string = string || char
				else char = '07'x
				call writech 'STDOUT',char
			end

			/* delete a character */
			when char = '08'x then do
				if string ~== '' then do
					string = substr(string,1,length(string)-1)
					call writech 'STDOUT', csi'D'csi'P'
				end
			end

			/* done */
			when char = cr then break

			/* discard cursor key 2nd byte */
			when char = csi then call readch 'STDIN',1
			otherwise nop
		end
	end
	return string

/*  -------------------------------------------------------------------
 *	Delete all tagged events; else current event
 */
DeleteEvent:

	/* any tagged for delete? */
	tagged = 0
	do i = 1 to numevents
		tagged = tagged + tag.i
	end i

	/* got tagged event(s) */
	if tagged > 0 then do
		call CLineMsg 'delete' tagged 'tagged event'copies('s',tagged~=1)'? Y/n '
		char = readch('STDIN',1)
		if upper(char)='N' then break
		do i = 1 to numevents
			if tag.i then call DeleteOne word(ssc_id.i,2)
		end
		signal ReScan
	end

	/* delete current event? */
	call CLineMsg 'delete this event? Y/n '
	char = readch('STDIN',1)
	if upper(char)='N' then call MsgAndDelay '..aborting - no delete'
	else do
		tmp = (page-1)*itemsPerPage+row
		call DeleteOne word(ssc_id.tmp,2)
		signal ReScan
	end
	return

DeleteOne:
	parse arg id
	call CLineMsg '..deleting'
	'DELETE_EVENT' id
	if RC~=0 then call MsgAndDelay '..couldn''t find event'
	return	

/* ------ write a message to command line, wait, then wipe it ----- */
MsgAndDelay:
	parse arg msg
	call CLineMsg msg
	call delay 25
	call CLineMsg ''
	return	

/*####################################################################
  ##                  do command line requests                      ##
  ####################################################################*/

/*  -----------------------------------------------------------------*/
ListEvents:
	if ~Running() then say '..CyberCron isn''t running'
	else do
		'LIST_EVENTS'
		list = result
		if left(list,2)~='0x' then say '..no events queued'
		else do i = 1 to words(list)
			'SHOW_EVENT' word(list,i)
			say subword(result,2)
		end
		say '  'ShowStatus()
	end
	return

/*  -----------------------------------------------------------------*/
NextEvent:
	if ~Running() then say '..CyberCron isn''t running'
	else do
		numevents = ReadEvents()
		if numevents = 0 then say '..no events queued'
		else do
			lasttime = word(ssc_id.1,1)	/* show all events queued for next time */
			do i = 1 to numevents
				thistime = word(ssc_id.i,1)
				if thistime ~= lasttime then break
				id = word(ssc_id.i,2)
				say event.i
				lasttime = thistime
			end i
			say '  'ShowStatus()
		end
	end
	return

/*  -------------------------------------------------------------  */
Suspend_Resume:
	running = Running()
	if ~running then say '..CyberCron isn''t running'
	else ''arg(1)''
	return running

/*  -------------------------------------------------------------  */
ShowStatus:	PROCEDURE
	'SHOW_STATUS'
	parse var result activity ' "' ctab '" "' lfile '"'
	star = copies('*',activity='SUSPENDED')
	if lfile = '<None>' then tmp = 'OFF'
	else tmp = 'ON'
	return 'Status:'star||activity||star 'Crontab='ctab 'Log='tmp

/*  -------------------------------------------------------------  */
KillCyber:
	if ~Running() then say '..CyberCron isn''t running'
	else do
		'QUIT'
		do i = 1 to 10
			call delay 25
			if ~Running() then break
		end i

		if i <= 10 then say '..CyberCron is dead'
		else say '..CyberCron will quit after the current job is finished'
	end
	return

/*  -------------------------------------------------------------  */
StartCyber:
	if Running() then say '..CyberCron is already running'
	else do
		address 'COMMAND' 'run >NIL: <NIL:' startcmd

		do i = 1 to 10
			call delay 25
			if Running() then break
		end i

		if i <= 10 then say '..CyberCron started'
		else say '..couldn''t start' CyberCron
	end
	return

/*  ------ 0=not running; 1=running ------------------------------  */
Running:
	return show('PORT','CYBERCRON')

/* ---------------------------------------------------------------- */
MakeRaw:
	call close 'STDOUT'
	call close 'STDIN'
	call open 'STDOUT','RAW:0/0/'screenwidth'/'screenheight'//NOSIZE'
	call pragma '*','STDOUT'
	call open 'STDIN','*'
	return

/* ----------------------------------------------------------------  */
Usage:
	say 'Usage: cy -option'
	say '    -e  edit crontab ['editor crontab']'
	say '    -b  begin CyberCron ['CyberCron']'
	say '    -q  quit (kill) CyberCron'
	say '    -s  suspend'
	say '    -r  resume'
	say '    -n  next event(s) to execute'
	say '    -l  list all events'
	say '        no option is the most fun'
	say '   editor  =' editor
	say '   logfile =' logfile
	say '   crontab =' crontab
	exit 0

ErX:
	say arg(1)

HALT:
BREAK_C:
	exit 10
