/* 	C/80 source code for SUPERSET version 1.0, 10/1/83 by
	David B. Ring, 1248 Truman Street, Redwood City, California
	94061.

	Copyright (c) 1983 by David B. Ring.  SUPERSET may be used, 
	copied and distributed freely for noncommercial purposes, 
	providing that a copy of this notice is included.  No commercial
	use of SUPERSET may be made without the author's express 
	written	permission.  SUPERSET was written and compiled using
	C/80 3.0 from the Software Toolworks.  Appreciation is
	due to Walt Bilofsky for allowing free distribution of
	programs compiled with C/80.  Appreciation is also expressed
	to Terje Bolstad for his public contribution of the CP/M
	bdos and bios calls used in the function syswrit().

	SUPERSET extends the Osborne SETUP program by allowing sets of
	special function key definitions to be saved and loaded as CP/M
	files.  Unlike SETUP, SUPERSET also provides display and editing
	of the arrow key definitions and the command issued on boot.  
	Comments, suggestions and fixes are appreciated and may be sent 
	to the address above.

	WARNING:  SUPERSET offers the option of recording edited soft
	features to the system tracks of a diskette.  If the #define
	options specifying track and sector are not properly configured
	for your system, a syswrit() operation could render your disk
	unbootable, in which case you will have to repair it by using
	SYSGEN.	*/

/****** HEADER SECTION **********************************************/

#include 	"a:tprintf.c"	/* short form of printf()  */

/* 	Macros which apply to either single or double density.  */

#define 	ESC		'\033'
#define 	CLEAR		putchar('\032')
#define 	DIM		putchar(ESC); putchar(')')
#define 	BRIGHT		putchar(ESC); putchar('(')
#define 	WIPEL		putchar(ESC); putchar('R')
#define 	PROMPT		cursor(21,0); WIPEL; WIPEL; WIPEL
#define 	FILENAME	14
#define 	SELDISK		14
#define 	SETTRK		10	
#define 	SETSEC		11	
#define 	SETDMA		12	
#define 	WRITE		14	
#define 	CURDRIVE	0X0004

/* 	Explanation of #defines:

	ESC		escape character
	CLEAR		clear video screen
	DIM		turn on dim video attribute
	BRIGHT		turn on bright video attribute
	WIPEL		clear current line on video screen
	PROMPT		move cursor to line 21 and clear 3 lines
	FILENAME	maximum size of CP/M filename
	SELDISK		select disk bdos service
	SETTRK		set track bios call
	SETSEC		set sector bios call
	SETDMA		set buffer address for disk I/O
	WRITE		write sector bios call
	CURDRIVE	address containing current drive #  */

/* 	Macros specific for single or double density.  The set
	actually used is toggled by #defining SD or DD (but not
	both!).  */

#define 	DD

#ifdef		DD
#define 	KEYTABL		0XE16B
#define 	ENDTABL		0XE187
#define 	LIMIT		0XE1FA
#define 	BOOTSWITCH	0XE19C
#define 	BOOTLENGTH	0XE19D
#define 	BOOTCOMMAND	0XE19E
#define 	BIOS		0XE100
#define 	FSTTRK		1
#define 	LSTTRK		1
#define 	FSTSEC		4
#define 	LSTSEC		5
#endif

#ifdef		SD
#define 	KEYTABL		0XE56B
#define 	ENDTABL		0XE587
#define 	LIMIT		0XE600
#define 	BOOTSWITCH	0XE589
#define 	BOOTLENGTH	0XE58A
#define 	BOOTCOMMAND	0XE58B
#define 	BIOS		0XE500
#define 	FSTTRK		2
#define 	LSTTRK		2
#define 	FSTSEC		4
#define 	LSTSEC		6
#endif

/* 	Explanation of #defines:

	KEYTABL		address of jump table to key defs
	ENDTABL		end of jump table
	LIMIT		first location AFTER key defs
	BOOTSWITCH	contains warm/cold/both/none choice
	BOOTLENGTH	# of chars in boot command + 1
	BOOTCOMMAND	starting address of boot command
	BIOS		starting address of bios
	FSTTRK		first track to write
	LSTTRK		last track to write
	FSTTRK		first sector to write
	LSTTRK		last sector to write  */

/****** MAIN PROGRAM ************************************************/

/* 	The command "SUPERSET" without arguments shows the key
	definitions currently in memory.  "SUPERSET FILENAME"
	starts by loading new definitions from the name file.
	"SUPERSET KEY K" (where K is 0-9, U, R, D, L) allows
	alteration of key K only, without display.  "SUPERSET
	BOOT" allows changing the cold or warm boot sequence.  */

main(argc, argv)
	int argc;
	char *argv[];
{
	int c;
	char *s;

/* 	First, check for and handle command line arguments.  */

	if (argc > 1) {
		if (!strcmp(argv[1], "KEY")) {
			c = argv[2][0];
			newkey(c);
			exit();
		}
		else if (!strcmp(argv[1], "BOOT")) {
			setboot();
			syswrit();
			exit();
		}
		else overlay(argv[1], KEYTABL, LIMIT - KEYTABL,"rb");
	}

/* 	Then, display current function key settings and menu.  */

	showkeys();

/* 	Now, respond to menu choice.  */

	while (1) {
		c = toupper(inkey());
		switch (c) {
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
			case 'U':
			case 'R':
			case 'D':
			case 'L':
				newkey(c);  /* insert a new def  */
				break;
			case 'F':
				filekeys();  /* save defs as file  */
				break;
			case 'B':
				setboot();  /* change boot command  */
				break;
			case 'S':
				syswrit();  /* save changes to disk  */
				break;
			case 'X':
				exit();  /* return to CP/M  */
			default:
				PROMPT;
				printf("Key not definable; try again.");
		}
		showkeys();
	}
} 

/****** FUNCTIONS CORRESPONDING TO PROGRAM MODULES ******************/

/* 	Showkeys displays the Osborne special function keys as well
	as the four arrow keys.		*/

showkeys()
{
	char *key, **offset, *s;
	int i;

	CLEAR;
	printf("SUPERSET v 1.0    (c) 10/1/83 by David B. Ring\n\n");	
	for (offset = KEYTABL, i = 0; offset < ENDTABL; offset++, i++) {
		if (i < 10) 		printf("%2d:  ", i);
		else if (i == 10) 	printf(" U:  ");
		else if (i == 11)	printf(" R:  ");
		else if (i == 12)	printf(" D:  ");
		else if (i == 13)	printf(" L:  ");
		for (key = *offset; key < *(offset + 1); key++) {
			showchar(*key);
		}
		printf("\n");
	}
	PROMPT;
	printf("Type 0-9, U, R, D, L to change key definition,\n");
	printf("B to change boot, F to save key defs as file,\n");
	printf("S to save changes to system tracks, X to exit.");
}

/* 	Newkey() allows entry of a new function or arrow key definition,
	and also inserts the new definition into the function key
	table in BIOS, moving old definitions and adjusting pointers
	as needed.	*/

newkey(c)
	int c;
{
	char **pkey, **pnext, **endtabl, s;
	int oldlen, newlen, free, diff;
	unsigned last;

	pkey = KEYTABL;
	switch (c) {
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			pkey += (c - '0');
			break;
		case 'U':
			pkey += 10;
			break;
		case 'R':
			pkey += 11;
			break;
		case 'D':
			pkey += 12;
			break;
		case 'L':
			pkey += 13;
			break;
	}

	pnext = pkey + 1;
	oldlen = *pnext - *pkey;
	endtabl = ENDTABL;
	last = *endtabl;
	free = LIMIT - last + oldlen;

	PROMPT;
 	printf("%d character spaces available for this key\n", free);
	printf("%c:  ", c);
	newlen = getbytes(s, free);

	diff = newlen - oldlen;
	cmove(*pnext, *pnext + diff, (int) (last - (unsigned) *pnext));
	cmove(s, *pkey, newlen);
	while ((unsigned) pnext <= ENDTABL) *pnext++ += diff;
}

/* 	Filekeys() prompts for a filename and then writes
	the chunk of BIOS containing function key pointers
	and definitions to that file.  Except for a length
	maximum of 14 characters, filekeys() does not check
	validity of filenames.  */

filekeys()
{
	char filename[FILENAME];

	PROMPT;
	printf("Disk:filename.typ for saving keys:  ");
	getstr(filename, FILENAME);
	overlay(filename, KEYTABL, LIMIT - KEYTABL, "wb");
}

/* 	Setboot() provides display and editing of the command issued
	on cold or warm boot.  By setting a toggle in memory, the
	command can be issued only on cold boot, only on warm boot,
	on both, or on neither.		*/
	
setboot()
{
	char s[7], *pswitch, *plen, *pcom, c;
	int i;

	pswitch = BOOTSWITCH;
	plen = BOOTLENGTH;
	pcom = BOOTCOMMAND;

	PROMPT;
	printf("Current boot sequence is:  ");
	for (i = 0; i < *plen - 1; i++) putchar(pcom[i]);
	printf("\nCommand to be issued on boot (<= 7 char):  ");
	*plen = getstr(s, 7) + 1;
	cmove(s, pcom, *plen);
	printf("Issue on 1=cold boot, 2=warm, 3=both, 0=neither:  ");
	c = inkey();
	*pswitch = c - '0';
}

/* 	Syswrit() writes the section of BIOS containing key definitions
	and boot command to the system tracks of the specified disk.  */

syswrit()
{
	char s[2], *curdrive;
	int olddrive, drive, dma, track, sector;

	curdrive = CURDRIVE;
	olddrive = *curdrive;

	PROMPT;
	printf("Drive on which to save current settings:  ");
	getstr(s, 2);
	switch (toupper(s[0])) {
		case 'A':
			drive = 0;
			break;
		case 'B':
			drive = 1;
			break;
		default:
			PROMPT;
			printf("Invalid drive; press any key to go on.");
			inkey();
			return;
	}

	bdos(SELDISK, drive);
	dma = BIOS;
	for (track = FSTTRK; track <= LSTTRK; track++) {
		for (sector = FSTSEC; sector <= LSTSEC; sector++) {
			bios(SETTRK, track, 0);
			bios(SETSEC, sector, 0);
			bios(SETDMA, dma, 0);
			bios(WRITE, 1, 0);
			dma += 128;
		}
	}
	bdos(SELDISK, olddrive);
}

/****** LOW LEVEL FUNCTIONS *****************************************/

/* 	Inkey() uses BDOS service 6 to get a character from 
	the console without echoing it to the video display.
	If no character is ready, the BDOS call returns 0, but
	the while loop waits until a nonzero character is
	returned.	*/

inkey()
{
	int c;

	c = 0;
	while (c == 0)  c = key();
	return(c);
}

key()
{
#asm
	LXI	B,6	;PUT FUNCTION #6 (DIRECT CONSOLE I/O) IN B
	LXI	D,255	;PUT PARAMETER IN D (CONSOLE INPUT)
	CALL	5	;BDOS CALL
	RET
#endasm
}

/* 	Toupper() converts alphabetical characters to upper case.  */

toupper(c)
	int c;
{
	if (c >= 'a' && c <= 'z') c += 'A' - 'a';
	return(c);
}

/* 	Showchar feeds through normal printing characters, but converts
	low ASCII nonprinting characters to their offset alphabetic
	equivalents.  The special characters are preceded by "^" and
	highlighted with the dim video feature.		*/

showchar(c)
	int c;
{
	if (c < 32) {
		DIM;
		putchar('^');
		putchar(c + 64);
		BRIGHT;
	}
	else if (c < 128)
		putchar(c);
}

/* 	Cursor() moves the cursor to the specified position.  */

cursor(y, x)
	int y, x;
{
	putchar(ESC);
	putchar('=');
	putchar(y + 32);
	putchar(x + 32);
}

/* 	Getstr() gets a string terminated by <CR> or excess length
	from the console.  In this program it is used to read in
	filenames.	*/

getstr(s, lim)
	char s[];
	int lim;
{
	int c, i;

	i = 0;
	while (--lim > 0 && (c = getchar()) != '\n') s[i++] = c;
	s[i] = '\0';
	return(i);
}

/* 	Getbytes() accepts a string of normal and control characters
	until terminated by <ESC><ESC> or the length limit.  */

getbytes(s, lim)
	char s[];
	int lim;
{
	int oldchar, c, i;

	oldchar = c = i = 0;
	while (--lim > 0 && ((c = inkey()) != ESC || oldchar != ESC)) {
		showchar(s[i++] = oldchar = c);		
	}
	if (c == ESC) --i;
	return(i);
}

/* 	Strcmp() is straight out of K&R.  It returns 0 if string
	s equals string t, or a negative or positive number if s
	is lexicographically less than or greater than t.	*/

strcmp(s, t)
	char s[], t[];
{
	int i;

	i = 0;
	while (s[i] == t[i]) if (s[i++] == '\0') return(0);
	return(s[i] - t[i]);
}

/* 	Overlay() either overlays the specified RAM address with the
	contents of the name file, or writes a block of RAM to a new
	file, prompting for the filename.  No testing for overflow of
	available RAM is performed.  Overlay() performs a similar
	function to the C/80 library functions read() and write(), but
	works with specified lengths rather than blocks of 128 
	characters.  */

overlay(filename, address, length, mode)
	char *filename, *address, *mode;
	int length;
{
	int i, chan;

	chan = fopen(filename, mode);
	if (chan == 0) {
		printf("Bad filename; press any key to go on.");
		inkey();
		return;
	}
	else if (mode == "rb" || mode == "RB")
		for (i = 0; i < length; i++) {
			*(address + i) = getc(chan);
		}
	else if (mode == "wb" || mode == "WB")
		for (i = 0; i < length; i++) {
			putc(*(address + i), chan);
		}
	fclose(chan);
}

/* 	Cmove() behaves like the same word in FORTH, i.e. it moves a
	block of n bytes residing at address 1 to address 2.  This
	version is smart enough to note the direction of short moves
	and move bytes from the proper end to avoid propagation.  
	Cmove() does no range checking and will over-write anything.  */

cmove(addr1, addr2, n)
	char *addr1, *addr2;
	int n;
{
	int i;

	if (addr2 - addr1 >= 0) {
		while (--n >= 0) {
			*(addr2 + n) = *(addr1 + n);
		}
	}
	else {
		for (i = 0; i < n; i++) {
			*(addr2 + i) = *(addr1 + i);
		}
	}
}

/* 	The following CP/M bdos and bios calls for C/80 were
	contributed to the June, 1983 issue of Dr. Dobb's
	Journal by Terje Bolstad and generously made available
	for public use.  The following contribution notice is
	included as requested.  */

/*
	CP/M BDOS AND BIOS CALLS FOR C/80
	
	Contributed by T. Bolstad, ELEKTROKONSULT AS
	Konnerudgaten. 3, N-3000 Drammen, NORWAY.

	Date:  January 17, 1983.    			*/

bdos(funct, arg)		/* corresponds to bdos((BC),(DE)) */
	int funct, arg;
{
	#asm

CPBASE	EQU	0		;NORMAL 0-ORG'ED CP/M
CPNTRY	EQU	CPBASE+5	;BDOS ENTRY

	POP	H		;GET RETURN ADDRESS
	POP	D		;GET ARG (INFORMATION ADDRESS)
	POP	B		;GET FUNCTION #
	PUSH	B		;RESTORE STACK
	PUSH	D
	PUSH	H

	PUSH	B		;SAVE FUNCTION # ON STACK
	CALL	CPNTRY		;BDOS CALL
	XCHG			;SAVE HL IN DE
	MOV	L,A		;SAVE A IN L
				;SIGN EXTENSION TO H:
	RLC			; GET SIGN BIT INTO CY
	SBB	A		; IF CY=0, RESULT AFTER SBB IS 0
				; IF CY=1, RESULT IS -1 (IE, ALL ONES)
	MOV	H,A		; A IS MOVED TO HL WITH SIGN EXTENSION

	POP	B		;GET FUNCTION # IN BC
	MOV 	A,C		;GET FUNCTION # IN A
	CPI	12		;WAS IT 'RETURN VERSION #' ?
	JZ	RETHL1
	CPI	24		;RETURN LOGIN VECTOR ?
	JZ	RETHL1
	CPI	27		;GET ALLOCATION ADDRESS ?
	JZ	RETHL1
	CPI	29		;GET READ/ONLY VECTOR ?
	JZ	RETHL1
	CPI	31		;GET DISK PARAMETER ADDRESS ?
	JZ	RETHL1
	JMP	BDOSRET

RETHL1:	XCHG
BDOSRET: RET			;WITH RETURNED VALUE IN HL

#endasm
}

bios(funct, arg1, arg2) /* corresponds to bios(function, (BC), (DE)) */
	int funct, arg1, arg2;
{
#asm

	POP	D	;RETURN ADDRESS
	POP	H	;ARGUMENT 2
	SHLD	ARG2S	;SAVE IT
	POP	B	;ARGUMENT 1
	XCHG		;GET RETURN ADDRESS INTO HL

	POP	D	;FUNCTION #

	PUSH	D	;RESTORE SP
	PUSH 	B
	PUSH	B
	PUSH	H	;RESTORE RETURN ADDRESS

	PUSH	D	;SAVE FUNCTION # ON STACK
	
	LXI	H,0	;CALCULATE OFFSET ADDRESS FROM FUNCTION:
	DAD	D	; GET FUNCTION # (OFFSET) IN HL
	DAD	H	; 2*OFFSET
	DAD	D	; 3*OFFSET
	XCHG		; SAVE OFFSET ADDRESS IN DE

	LHLD	CPBASE+1 ;GET POINTER TO BIOS WBOOT ENTRY
	DCX	H	;DECREMENT TO
	DCX	H	;  POINT TO
	DCX	H	;    START OF BIOS ENTRY JUMP TABLE

	DAD	D	;ADD OFFSET (RESULT IN HL)
	XCHG		;GET RESULT IN DE
	LXI	H,RET1	
	PUSH	H 	;SAVE RETURN ADDRESS ON STACK
	
	LHLD	ARG2S	;GET ARGUMENT 2
	XCHG		;GET ARGUMENT 2 INTO DE
			; AND BIOS FUNCTION ENTRY ADDRESS INTO HL

	PCHL		;GO TO BIOS

RET1:	XCHG		;SAVE HL IN DE
	MOV	L,A

	RLC		;GET SIGN BIT INTO CY
	SBB	A	; IF CY=0, RESULT AFTER SUBB IS 0
			; IF CY=1, RESULT AFTER SUBB IS -1 (ALL ONES)
	MOV	H,A
	POP	B	;GET BIOS FUNCTION # IN BC
	MOV	A,C	
	CPI	9	;SELECT DISK FUNCTION ?
	JZ	RETHL2
	CPI	16	;SECTOR TRANSLATION FUNCTION ?
	JZ	RETHL2
	
	JMP	RETBIOS
RETHL2:	XCHG		;RETURN VALUE IN HL
RETBIOS: RET
ARG2S:	DS	2

#endasm
}