PROGRAM XTCL ! Copyright 1994-2023 Marcus Rhodes ! This program is free software; You can redistribute it and/or ! modify it under the terms of the GNU general public license ! version 3 as published by the Free Software Foundation. ! Modified: 05/27/2022 09:42:17 by Marcus ! Platform: Any Pick; Any OS; any emulator; Any emulation ! Function: The eXTended Command Line gives you a better CLI experience. ! It looks like the the same, old command-line that you're used ! to, but is fully editable using the navigation keys, back- ! space and delete, and even has a scrolling command history. ! Syntax : XTCL ! (Options: None (yet) ! Examples: XTCL ! Upcoming: Prevent multiple instances. ! Done! 09/22/2021 17:27:09 by Marcus ! Prevent *, as in CT FILE *, from leaving list 0 active. ! Done! 09/23/2021 09:55:10 by Marcus ! Chain the LOGTO command to prevent named common conflicts. ! Done! 09/29/2021 09:47:11 by Marcus ! Show man page with Page Up (=.L) ! Done! 11/24/2021 09:15:12 by Marcus ! Invert/flash cursor when select-list active instead of ! doubling it (>>). ! Done! 05/27/2022 09:42:17 by Marcus ! Add backward search (^G). ! Done! 08/09/2022 15:25:18 by Marcus ! Limit history to 32K for performance reasons. ! Done! 08/31/2022 10:11:19 by Marcus ! Full-screen scrollable history. ! Change is the only constant. -- Heraclitus ! Old name: N/A ! Catalog : XTCL ! New name: N/A EQU IDENTITY TO 'XTCL' INCLUDE ANY_KEY_EQU IF IDENTITY THEN GOSUB INI.VARS IF POSSIBLE THEN GOSUB GET.FILEVAR IF POSSIBLE THEN GOSUB GET.HISTORY IF POSSIBLE THEN GOSUB GET.PRESENT IF POSSIBLE THEN GOSUB SAY.HI IF POSSIBLE THEN GOSUB GET.INPUT STOP INI.VARS: CRT IDENTITY : ' starting ... ' : @( -4 ) : GOSUB GET.SIZE CALL ANY_KEY_TRM EXECUTE 'TERM ' : ( LINE_LEN + 1 ) : ',' : ( VERTICAL + 1 ) CMND_IDX = 0 CMND_LST = '' CMND_MAX = 0 DEL_CMND = '' HORIZNTL = NOT( NOT( SYSTEM( 11 ) ) ) + 1 LAST_KEY = '' LSTFNAME = 'POINTER-FILE' LST_NAME = IDENTITY : '_' : @LOGNAME POSITION = 1 POSSIBLE = @TRUE SRCH_STR = '' TCL_CMND = '' MAN_PAGE = '----------------- Welcome to XTCL, the eXTended Command-' MAN_PAGE := 'Line! -----------------~- Easily Edit your command-line ' MAN_PAGE := '& history using standard cursor & editing keys!~- Left/r' MAN_PAGE := 'ight now moves your cursor one char at a time, while ' MAN_PAGE := 'Ctrl+left/right~ moves your cursor one word at a time.~' MAN_PAGE := '- Pressing Shift at the same time highlights characters ' MAN_PAGE := 'to be copied/deleted.~- Likewise, Backspace & Delete era' MAN_PAGE := 'se one char at a time, while Ctrl+Backspace &~ Delete e' MAN_PAGE := 'rase a word at a time.~- Page Up displays the last # lin' MAN_PAGE := 'es of your command history. (#=display height)~- Up/down' MAN_PAGE := ' scrolls through your command history.~- Ctrl+F searches' MAN_PAGE := ' your command history for the string entered at the prom' MAN_PAGE := 'pt.~- Ctrl+G reverses the direction of the search from t' MAN_PAGE := 'he current position.~- Pressing Ctrl+F/G again repeats t' MAN_PAGE := 'he search from the last location.~- Enter executes the c' MAN_PAGE := 'ommand currently visible at the prompt.~- Ctrl-D deletes' MAN_PAGE := ' the currently visible command from history.~- .L[#], .D' MAN_PAGE := '[#], & .X[#] are still respected, but ...~- .R[#] now ju' MAN_PAGE := 'st recalls the specified command for editing/executing.~' MAN_PAGE := '- ! lets you execute Unix commands without `shelling out' MAN_PAGE := '`. E.g.: !ls -al|more~- An active select-list causes th' MAN_PAGE := 'e cursor to invert and flash.~- Esc first clears the com' MAN_PAGE := 'mand-line, if anything`s there.~- Esc then clears select' MAN_PAGE := '-list 0, if active, restoring the normal prompt.~- Esc f' MAN_PAGE := 'inally exits to plain, old TCL again. (But why would yo' MAN_PAGE := 'u want to?)~- You won`t see this again when starting XTC' MAN_PAGE := 'L, but, if you ever need it, ...~- F1 or ? shows this he' MAN_PAGE := 'lp screen.' MAN_PAGE = LINE_BEG : CHANGE( MAN_PAGE, '~', CHAR( 13 ) : CHAR( 10 ) ) RETURN GET.PRESENT: CURR_LST = SYSTEM( 33 ) CURR_MAX = DCOUNT( CURR_LST, @AM ) FOR CURR_IDX = 1 TO CURR_MAX CURR_CMD = CURR_LST< CURR_IDX > LOCATE CURR_CMD IN CMND_LST SETTING ITS_SPOT ELSE CMND_LST< -1 > = CURR_CMD END NEXT CURR_IDX RETURN GET.FILEVAR: OPEN LSTFNAME TO SVD_LSTS ELSE ERR_MSSG = LINE_BEG : IDENTITY : ' couldn`t open the saved-lists ' ERR_MSSG := 'file, ' : LSTFNAME : '! Verify the name is correct.' CRT ERR_MSSG CRT_CODE = '' POSSIBLE = @FALSE END RETURN SAY.HI: IF CMND_MAX THEN CRT STR( CHAR( 8 ), LEN( IDENTITY ) + 14 ) : IDENTITY : ' started!' : @( -4 ) END ELSE GOSUB MAN.PAGE END RETURN GET.INPUT: LOOP WHILE POSSIBLE DO DOT_CMND = @FALSE TCL_CMND = CMND_LST< CMND_IDX > GOSUB SET.PROMPT CALL EMV_INP_FLD( TCL_CMND, META_KEY, HORIZNTL, VERTICAL, LINE_LEN, POSITION, 0, 0 ) UPPR_CMD = OCONV( TCL_CMND, 'MCU' ) BEGIN CASE CASE META_KEY EQ K_DOWN ; GOSUB GET.NEXT CASE META_KEY EQ K_ESCAPE ; GOSUB GET.OUT CASE META_KEY EQ K_F01 ; GOSUB MAN.PAGE CASE META_KEY EQ K_PAGE_UP ; GOSUB LIST.CMNDS CASE META_KEY EQ K_UP ; GOSUB GET.PREV CASE META_KEY EQ KC_D ; GOSUB CUT.CMND CASE META_KEY EQ KC_END ; GOSUB GET.LAST CASE META_KEY EQ KC_F ; GOSUB FIND.CMND.FWD CASE META_KEY EQ KC_G ; GOSUB FIND.CMND.BWD CASE META_KEY EQ KC_HOME ; GOSUB GET.FIRST CASE META_KEY EQ KC_R ; GOSUB RULER CASE TCL_CMND EQ '?' ; GOSUB MAN.PAGE CASE UPPR_CMD[ 1, 5 ] EQ 'HELP ' ; GOSUB CMD.HELP CASE UPPR_CMD EQ 'WHO' ; GOSUB SAY.WHO CASE TCL_CMND[ 1, 1 ] EQ '!' ; GOSUB RUN.UNIX CASE UPPR_CMD[ 1, 2 ] EQ '.R' ; GOSUB GET.IDX CASE UPPR_CMD[ 1, 2 ] EQ '.D' ; GOSUB CUT.CMND CASE UPPR_CMD[ 1, 2 ] EQ '.L' ; GOSUB LIST.CMNDS CASE UPPR_CMD[ 1, 2 ] EQ '.X' ; GOSUB RUN.NUM CASE UPPR_CMD[ 1, 4 ] EQ 'XTCL' ; GOSUB EXIT.AND.CHAIN CASE UPPR_CMD[ 1, 6 ] EQ 'LOGTO ' ; GOSUB EXIT.AND.CHAIN CASE META_KEY EQ K_ENTER ; GOSUB RUN.CMND END CASE LAST_KEY = META_KEY REPEAT RETURN SAY.WHO: ! A recent AIX update made this necessary. ! WHO.CUBS1 = 7 SAPROD From Marcus.Rhodes ! WHO = 12 SAPROD 7 Marcus.Rhodes pts/12 ACCTPATH = @TTY TTY_NMBR = FIELD( ACCTPATH, '/', DCOUNT( ACCTPATH, '/' ) ) ACCTNAME = @PATH ACCTNAME = FIELD( ACCTNAME, '/', DCOUNT( ACCTNAME, '/' ) ) PORT_NUM = SYSTEM( 18 ) ADVISORY = LINE_BEG : TTY_NMBR : ' ' : ACCTNAME : ' ' : PORT_NUM : ' ' ADVISORY := @LOGNAME : ' ' : ACCTPATH CRT ADVISORY RETURN FIND.CMND.FWD: IF ASSIGNED( TCL_CMND ) AND TCL_CMND NE '' THEN IF LAST_KEY EQ KC_F OR LAST_KEY EQ KC_G THEN TCL_CMND = SRCH_STR END ELSE START_AT = 1 END FOR FIND_IDX = START_AT TO CMND_MAX IF INDEX( OCONV( CMND_LST< FIND_IDX >, 'MCU' ), OCONV( TCL_CMND, 'MCU' ), 1 ) THEN SRCH_STR = TCL_CMND LAST_KEY = KC_F START_AT = FIND_IDX + 1 CMND_IDX = FIND_IDX FIND_IDX = CMND_MAX TCL_CMND = CMND_LST< CMND_IDX > END NEXT FIND_IDX END RETURN FIND.CMND.BWD: IF ASSIGNED( TCL_CMND ) AND TCL_CMND NE '' THEN IF LAST_KEY EQ KC_F OR LAST_KEY EQ KC_G THEN TCL_CMND = SRCH_STR END ELSE START_AT = CMND_MAX END FOR FIND_IDX = START_AT TO 1 STEP -1 IF INDEX( OCONV( CMND_LST< FIND_IDX >, 'MCU' ), OCONV( TCL_CMND, 'MCU' ), 1 ) THEN SRCH_STR = TCL_CMND TCL_CMND = CMND_LST< FIND_IDX > LAST_KEY = KC_G START_AT = FIND_IDX - 1 CMND_IDX = FIND_IDX FIND_IDX = 1 TCL_CMND = CMND_LST< CMND_IDX > END NEXT FIND_IDX END RETURN MAN.PAGE: CRT LINE_BEG : MAN_PAGE RETURN RUN.NUM: GOSUB GET.IDX TCL_CMND = CMND_LST< CMND_IDX > GOSUB SET.PROMPT CRT TCL_CMND : @( -4 ) : GOSUB RUN.CMND RETURN SET.PROMPT: GOSUB GET.SIZE IF SYSTEM( 11 ) THEN PRMT_STR = @( -5 ) : @( -13 ) : '>' : @( -14 ) : @( -6 ) ! PRMT_STR = @( -13 ) : '>' : @( -14 ) END ELSE PRMT_STR = '>' END CRT @( 0, VERTICAL ) : PRMT_STR : RETURN RUN.CMND: IF TCL_CMND THEN GOSUB DEL.CMND PERFORM TCL_CMND IF TCL_CMND[ 2 ] EQ ' *' THEN PERFORM 'CLEARSELECT' END IF OCONV( TCL_CMND[ 1, 5 ], 'MCU' ) EQ 'TERM ' THEN GOSUB GET.SIZE END TCL_CMND = '' END RETURN GET.SIZE: VERTICAL = SYSTEM( 3 ) - 1 LINE_LEN = SYSTEM( 2 ) - 1 LIST_WID = LINE_LEN - 3 LINE_FMT = 'L#' : LINE_LEN LIST_FMT = 'L#' : LIST_WID LINE_BEG = @( 0, VERTICAL ) RETURN RUN.UNIX: IF TCL_CMND[ 2, 1 ] THEN GOSUB DEL.CMND EXECUTE \SH -c '\ : TCL_CMND[ 2, 9999 ] : \'\ TCL_CMND = '' END RETURN DEL.CMND: ! Delete prior instances of this command from history. LOOP LOCATE TCL_CMND IN CMND_LST SETTING CMND_IDX THEN DEL CMND_LST< CMND_IDX > CMND_MAX -= 1 END ELSE EXIT END REPEAT CMND_MAX += 1 CMND_IDX = 0 CMND_LST = TCL_CMND : @AM : CMND_LST GOSUB SAVE.CMNDS CRT RETURN GET.FIRST: CMND_IDX = 1 RETURN GET.LAST: CMND_IDX = CMND_MAX RETURN GET.PREV: IF CMND_IDX LT CMND_MAX THEN CMND_IDX += 1 TCL_CMND = CMND_LST< CMND_IDX > END RETURN GET.NEXT: IF CMND_IDX GT 0 THEN CMND_IDX -= 1 TCL_CMND = CMND_LST< CMND_IDX > END RETURN CUT.CMND: GOSUB GET.IDX IF 0 LT CMND_IDX AND CMND_IDX LE CMND_MAX THEN DEL_CMND = CMND_LST< CMND_IDX > DEL CMND_LST< CMND_IDX > CMND_MAX -= 1 IF CMND_IDX GT CMND_MAX THEN CMND_IDX = CMND_MAX END END IF DOT_CMND THEN CMND_IDX = 0 END RETURN GET.IDX: ! Safe to call from anywhere because it limits itself to just the ! numeric suffixes of .commands that have them (e.g.: .d9, .x7). IF TCL_CMND[ 1, 1 ] EQ '.' THEN DOT_CMND = @TRUE CMND_NUM = TCL_CMND[ 3, 9 ] IF LEN( CMND_NUM ) AND NUM( CMND_NUM ) THEN IF 0 LT CMND_NUM AND CMND_NUM LE CMND_MAX THEN CMND_IDX = CMND_NUM END END ELSE CMND_IDX = 1 END END RETURN LIST.CMNDS: HIST_LST = LINE_BEG FOR LIST_IDX = VERTICAL TO 1 STEP -1 IF CMND_LST< LIST_IDX > THEN ! HIST_LST := LIST_IDX 'R#3 ' : CMND_LST< LIST_IDX > LIST_FMT HIST_LST := LIST_IDX 'R#3 ' : CMND_LST< LIST_IDX >[ 1, LIST_WID ] : @( -4 ) HIST_LST := CHAR( 13 ) : CHAR( 10 ) END NEXT LIST_IDX CRT LINE_BEG : HIST_LST : CMND_IDX = 0 RETURN GET.OUT: IF TCL_CMND OR CMND_IDX THEN TCL_CMND = '' CMND_IDX = 0 END ELSE IF SYSTEM( 11 ) THEN CRT @( 0, VERTICAL ) : '>' : PERFORM 'CLEARSELECT' END ELSE GOSUB SAVE.CMNDS CRT LINE_BEG : IDENTITY : ' exited!' : @( -4 ) CRT_CODE = '' POSSIBLE = @FALSE END END RETURN EXIT.AND.CHAIN: IF SYSTEM( 11 ) THEN PERFORM 'CLEARSELECT' END GOSUB SAVE.CMNDS CRT_CODE = '' POSSIBLE = @FALSE CHAIN TCL_CMND RETURN SAVE.CMNDS: ! Breaking out with Ctrl+C will cause XTCL to abort without saving the ! command history, which could be an unacceptable loss, so we save the ! command history after every command. But, if multiple instances are ! running on the same account, they'll keep overwriting each others' ! lists, and the last one to exit cleanly will be the winner, again, ! possibly losing more history than we'd like. So, the solution is ! for every instance to merge the save-file's copy of the history into ! its own copy of the history before writing. That way, all the ! sessions on the same account will share a (more or less) identical ! command history (just in a different order). Now, this may begin to ! impose a performance penalty, but we have a back-up plan for that: ! Eliminate shorter commands like WHO, LISTU, etc., and/or limit the ! size of the history to, say, 1000. We already eliminate duplicates, ! so that helps. GOSUB GET.HISTORY WRITE CMND_LST[ 1, 32768 ] ON SVD_LSTS, LST_NAME RETURN GET.HISTORY: FIND_IDX = 0 EXECUTE 'GET-LIST ' : LST_NAME RTNLIST LIST_VAR CAPTURING WHATEVER LOOP READNEXT OLD_CMND FROM LIST_VAR THEN IF LEN( OLD_CMND ) AND OLD_CMND NE DEL_CMND THEN LOCATE OLD_CMND IN CMND_LST SETTING FOUND_AT THEN FIND_IDX = FOUND_AT END ELSE CMND_MAX += 1 FIND_IDX += 1 CMND_LST = INSERT( CMND_LST, FIND_IDX; OLD_CMND ) END END END ELSE EXIT END REPEAT DEL_CMND = '' CMND_IDX = 0 RETURN RULER: TERM_RLR = LINE_BEG : SPACE( INT( LINE_LEN / 2 ) ) : '!' TERM_RLR := @( -4 ) : @( 0, VERTICAL - 2 ) CRT TERM_RLR : INPUT WHATEVER: RETURN CMD.HELP: CURR_WID = SYSTEM( 2 ) CURR_HGT = SYSTEM( 3 ) PERFORM 'TERM 80,' : CURR_HGT PERFORM UPPR_CMD PERFORM 'TERM ' : CURR_WID : ',' : CURR_HGT RETURN