# zsh only # source this file # See example usage at end of file. # http://yost.com/computers/shellscript-runner/ # formerly named demo-shell-commands.zsh # 2015-04-04 dave@yost.com # To do: # 1. When free-running, every command's exit status is 0. # 2. ssr-freerun should start execution without requiring control-right-arrow # 3. BUG: Consider a group of lines starting with a : line and followed # by a line not starting with :, the second line should be animated. #=============================================================================== # These keyboard shortcuts are defined: # control-right-arrow # control-left-arrow # These commands are defined: function _ssr-set_Help { cat << 'EOF' When running the script, * control-right-arrow copies your next script command to the command line * control-left-arrow copies the previous All shell features work as usual, unaltered, including these: * You can edit the command line before hitting Enter. * You can use shell history. No matter what else you do in the shell, you can continue your script from where you left off. You can make an animation finish early by hitting any key (space is a good choice). Use the ssr-speed command to change the speed of the animation of simulated typing. Use the ssr-freerun command to make 2 or more commands execute automatically. EOF } function _ssr-freerun_Help { cat << 'EOF' To run the script on its own, without having to hit Enter, ssr-freerun or use a positive integer to free-run a limited number of following commands, ssr-freerun 4 Type-ahead stops free-running mode. The space key is a good choice. EOF } function _ssr-speed_Help { cat << 'EOF' To disable animation, so that commands are always displayed quickly: ssr-speed 0 To enable animation at the default speed: ssr-speed 100 To enable animation at 2x speed: ssr-speed 200 Or use any other positive integer. EOF } #=============================================================================== function ssr-set { if [[ $1 == --help ]] then _ssr-set_Help return fi _ssrCommands=() local new=true local command= local line # IFS= avoids discarding leading whitespace # -r is for raw while IFS= read -r line ; do if [[ -z $line || $line[1] == '#' ]] then if [[ -n $command ]] then _ssrCommands=( ${_ssrCommands[@]} $command ) command= fi new=true elif [[ $new == true ]] then command="$line" new= else command="$command"$'\n'"$line" fi done _ssrIndex=0 } function ssr-freerun { local -r arg=$1 if [[ $arg == --help ]] then echo current value: $_ssrFreerunCount _ssr-freerun_Help return fi if [[ $# == 0 || $arg =~ "^-[.0-9]+$" ]] then _ssrFreerunCount=-1 elif [[ $arg == 0 ]] then _ssrFreerunCount=0 elif (($_ssrFreerunCount >= 0)) then # Change it only if not in freerunning mode. # You can stop freerunning mode with any key. _ssrFreerunCount=$arg fi } # 0 special case for no delay function ssr-speed { if [[ $1 == --help ]] then _ssrDescribeSpeed $_ssrSpeedFactor _ssr-speed_Help return fi if [[ $# == 0 ]] then _ssrDescribeSpeed $_ssrSpeedFactor else local -r arg=$1 # http://www.refining-linux.org/archives/58/ZSH-Gem-23-Working-with-extended-regular-expressions/ if [[ $arg =~ "^-[.0-9]+$" ]] then echo -n "Speed '$arg' cannot be negative" _ssrDescribeSpeed elif [[ ! $arg =~ "^[.0-9]+$" ]] then echo -n "Speed '$arg' is not a number" _ssrDescribeSpeed else _ssrSpeedFactor=$arg if (( $arg == 0 )) then _ssrDescribeSpeed $arg _ssrSpeedAdjustment=0 _ssrEndOfLineDelay=0 _ssrAfterPromptDelay=0 else _ssrSpeedAdjustment=$((.008 * $_ssrSpeedFactorNormal / $_ssrSpeedFactor)) _ssrEndOfLineDelay=$((.8 * 100 / $_ssrSpeedFactor)) _ssrAfterPromptDelay=$((.8 * 100 / $_ssrSpeedFactor)) fi fi fi } #=============================================================================== # internals _ssrCommands=() _ssrIndex=0 _ssrPreviousHISTCMD= _ssrAnimatingMode=true # ssr-speed _ssrSpeedFactorNormal=100 _ssrSpeedAdjustment=0.008 _ssrSpeedFactor=UNINITIALIZED _ssrEndOfLineDelay=UNINITIALIZED _ssrAfterPromptDelay=UNINITIALIZED # ssr-freerun _ssrFreerunCount=UNINITIALIZED zmodload zsh/zselect # Set the defaults ssr-speed 100 ssr-freerun 0 function _ssrDescribeSpeed { if [[ $# > 0 ]] then echo -n "Speed is " fi echo "$1 (default is $_ssrSpeedFactorNormal, 0 turns off animation)" } function _ssrNext { local continuing= while true ; do if (( $_ssrIndex >= ${#_ssrCommands} )) then _ssrFreerunCount=0 _ssrIndex=$((${#_ssrCommands} + 1)) _ssrSetBUFFER ": End of script" _ssrPreviousHISTCMD=$HISTCMD break fi if [[ $continuing == true ]] then _ssrDelayAfterPrompt fi if [[ $_ssrAnimatingMode == true ]] \ || [[ $HISTCMD != $_ssrPreviousHISTCMD ]] then # Reenable animation because the user actually executed something. _ssrAnimatingMode=true _ssrSetBUFFER ${_ssrCommands[++_ssrIndex]} true else _ssrSetBUFFER ${_ssrCommands[++_ssrIndex]} fi _ssrPreviousHISTCMD=$HISTCMD if [[ $_ssrFreerunCount == 0 ]] then break fi if (( _ssrFreerunCount > 0 )) then (( --_ssrFreerunCount )) if [[ $_ssrFreerunCount == 0 ]] then _ssrDelayAtEndOfLine # Act as if the user hit Enter. zle .accept-line break fi fi continuing=true _ssrDelayAtEndOfLine # Act as if the user hit Enter. zle _ssrExecuteNow done } function _ssrPrevious { _ssrAnimatingMode=true if (( $_ssrIndex <= 1 )) then _ssrIndex=0 _ssrSetBUFFER ": Start of script" else _ssrSetBUFFER ${_ssrCommands[--_ssrIndex]} fi } function _ssrSetBUFFER { local -r arg=$1 local useAnimationThisTime=$2 local previousType= BUFFER= if [[ $arg[1] == : ]] then useAnimationThisTime= fi for (( i = 1 ; i <= ${#arg} ; ++i )) do local char=${arg[i]} if [[ $useAnimationThisTime == true ]] then # Type-ahead quickly finishes the animation. read timeout previousType < <(_ssrDelayTimeForChar $char $previousType) if zselect -t $timeout -r 0 < /dev/tty ; then # Got something as type-ahead. # Complete the command line with no further delay. zle read-command _ssrFreerunCount=0 if [[ $REPLY == _ssrNext ]] then # Disable animation until animation is reenabled. _ssrAnimatingMode= fi useAnimationThisTime= fi fi # TAB completion if [[ $char == ' ' ]] then zle complete-word else LBUFFER[i]=$char fi # Flush output to the command line. zle -R done } # two results: # milliseconds # character type function _ssrDelayTimeForChar { if [[ $_ssrSpeedFactor == 0 ]] then echo 0 space return fi local char=$1 local previousType=$2 local type local result case $char in [A-Za-z0-9]) type=text ;; ' ') type=space ;; ' ') type=tab ;; *) type=punc ;; esac case $type in text) result=$(_ssrDelay 8 15) ;; space) result=$(_ssrDelay 15 5) ;; tab) result=$(_ssrDelay 40 0) ;; *) result=$(_ssrDelay 25 10) ;; esac if [[ $type != $previousType ]] then result=$(( $result + $(_ssrDelay 13 0) )) fi result=$(echo $(($result * 100)) | sed 's,\..*,,') echo $result $type } # Calculate the delay: # arg1 + arg2 * a random number between 0 and 1 function _ssrDelay { local -r randomFrom0to1=$(($RANDOM/32767.)) echo $(($_ssrSpeedAdjustment * ($1 + $2 * $randomFrom0to1))) } function _ssrDelayAtEndOfLine { if [[ $_ssrAnimatingMode == true ]] \ && [[ $_ssrEndOfLineDelay != 0 ]] then sleep $_ssrEndOfLineDelay fi } function _ssrDelayAfterPrompt { if [[ $_ssrAnimatingMode == true ]] \ && [[ $_ssrAfterPromptDelay != 0 ]] then sleep $_ssrAfterPromptDelay fi } # Remaining glitches will be that $? / $pipestatus won't be correct when # redrawing the prompt or executing a subsequent command. I can't # immediately think of a workaround. -- Bart Schaefer # http://www.zsh.org/mla/users/2015/msg00428.html function _ssrExecuteNow { zle -I print -S "$BUFFER" eval "$BUFFER" BUFFER= zle -R } zle -N _ssrExecuteNow # control-right-arrow zle -N _ssrNext bindkey '^[[1;5C' _ssrNext # control-left-arrow zle -N _ssrPrevious bindkey '^[[1;5D' _ssrPrevious if false ; then ssr-speed ssr-speed -1 ssr-speed 200a ssr-speed 0 ssr-speed 1 ssr-speed 100 ssr-speed 200 fi #======================================================================= # Example #----------------------------------------------------------------------- prompt='%B--------------------------------------------------------------------------------------------- %? %h Z%% %b' cat << 'EOF' | ssr-set # Comment lines begin with # are not shown to the user. # Commands are separated by one or more blank or comment lines. # Commands can be multi-line. # All command text here is passed to the command line without modification. # A command line starting with : is simply printed, with no animation. # Hitting return executes it, but the shell doesn't do anything. : Think of a line starting with : as a debug printout. echo same-line comment ;: "This part won't execute. Best to quote this." # Yes, the shell really executes the script commands. x=hello ; sleep 1 ; echo $x # Type-ahead stops free-running mode. # Run the next 3 cammands in freerun mode. : Hit Enter, then type control-right-arrow to start the 3 free-running commands. ssr-freerun 3 echo first of 3 # The exit status of this is 0, which is a free-run mode bug. sleep .5 ; false echo last one ; false # A comment line separates commands as well as a blank line does. touch /tmp/xyzzylongname # TAB completion works. (There is a TAB at the end this line.) echo /tmp/xyzzyl # Animation timing is different for text, punctuation, spaces, and tab. # Also the timing is different for any transition between these. echo first...... second # Multi-line commands are OK, and zsh lets you edit them. for x in 1 2 3 ; do echo $x done # Of course, backslashes are OK. echo continued \ here # Multiple commands without backslash work, weirdly enough. echo foo echo bar EOF ssr-speed ;: report current speed ssr-speed 0 ;: special case meaning fast, no animation ssr-speed 200 ;: 2x faster than normal ssr-speed 100 ;: normal speed #-----------------------------------------------------------------------