diff --git a/README.md b/README.md index ad507ecbd..a443e1f10 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Twitter), and join us on [Discord](https://discord.gg/ohmyzsh). - [Using Plugins](#using-plugins) - [Themes](#themes) - [Selecting A Theme](#selecting-a-theme) + - [Shellsavers](#shellsavers) - [FAQ](#faq) - [Advanced Topics](#advanced-topics) - [Advanced Installation](#advanced-installation) @@ -241,6 +242,33 @@ If you only know which themes you don't like, you can add them similarly to an i ZSH_THEME_RANDOM_IGNORED=(pygmalion tjkirch_mod) ``` +### Shellsavers + +Give your terminal a little downtime with `omz shellsaver`—our playful name for +terminal screensavers. The aquarium is the default; choose another scene to change the mood: + +```zsh +omz shellsaver # A quiet aquarium full of sea shells +omz shellsaver logo # The updater's rainbow logo, bouncing around +omz shellsaver hermit # A slightly lost hermit crab +omz shellsaver party # A rainbow stage and a dancing crowd +``` + +Press any key or Ctrl-C to return to your shell. Screensavers run only when invoked +and restore the terminal when they exit. No plugin or configuration change is needed. +Use `omz help` to discover the command, and Tab to complete scene names and options. + +```zsh +omz shellsaver aquarium --seconds 30 # Stop automatically after 30 seconds +omz shellsaver logo --mono # Use the terminal's foreground color +omz shellsaver --list # List scenes (aquarium is the default) +omz shellsaver --help # Show all scenes and options +``` + +Requires Zsh 5.8 or newer, `stty`, and an xterm-compatible terminal with alternate-screen +support. No additional animation package is required. `NO_COLOR` is respected. +A window of at least 49 columns by 18 rows is recommended. + ### FAQ If you have some more questions or issues, you might find a solution in our diff --git a/lib/cli.zsh b/lib/cli.zsh index c713db5d7..ed4af16ef 100644 --- a/lib/cli.zsh +++ b/lib/cli.zsh @@ -28,12 +28,25 @@ function _omz { 'plugin:Manage plugins' 'pr:Manage Oh My Zsh Pull Requests' 'reload:Reload the current zsh session' + 'shellsaver:Launch a whimsical terminal screensaver' 'shop:Open the Oh My Zsh shop' 'theme:Manage themes' 'update:Update Oh My Zsh' 'version:Show the version' ) + if [[ "$words[2]" == (screensaver|shellsaver) ]] && (( CURRENT > 2 )); then + _arguments \ + '1:command:' \ + '2::scene:(aquarium logo hermit party)' \ + '--list[List available scenes]' \ + '--mono[Use the default foreground color]' \ + '--seconds[Exit after a number of seconds]:seconds:' \ + '--snapshot[Print a plain-text still]::frame:' \ + '--help[Show Shellsaver help]' + return + fi + if (( CURRENT == 2 )); then _describe 'command' cmds elif (( CURRENT == 3 )); then @@ -174,6 +187,7 @@ Available commands: plugin Manage plugins pr Manage Oh My Zsh Pull Requests reload Reload the current zsh session + shellsaver [scene] Launch a Shellsaver (aquarium by default) shop Open the Oh My Zsh shop theme Manage themes update Update Oh My Zsh @@ -182,6 +196,15 @@ Available commands: EOF } +function _omz::shellsaver { + command zsh -f "$ZSH/tools/shellsaver.zsh" "$@" +} + +# Hidden alias; only shellsaver is advertised in help and command completion. +function _omz::screensaver { + _omz::shellsaver "$@" +} + function _omz::changelog { local version=${1:-HEAD} format=${3:-"--text"} diff --git a/tools/shellsaver.zsh b/tools/shellsaver.zsh new file mode 100644 index 000000000..4eb2c84b1 --- /dev/null +++ b/tools/shellsaver.zsh @@ -0,0 +1,657 @@ +#!/usr/bin/env zsh +# Shared on-demand renderer for `omz shellsaver `. +# The CLI launches a separate Zsh process, keeping all state out of the caller. +{ + emulate -L zsh + setopt extendedglob + unsetopt multibyte + + integer tick=0 width=96 height=28 resized=1 active=0 seconds_limit=0 + integer mono=0 snapshot=0 scene_selected=0 + local saved_tty='' key='' blank='' result='' scene='' + local -a canvas inks palette + + _zshell_list() { + print -r -- 'aquarium Sea shells, a ~ hermit crab, and a quiet current (default). +logo The Oh My Zsh wordmark takes a bouncing coffee break. +hermit A bewildered hermit crab tries to find its working directory. +party A rainbow terminal stage and a crowd of dancing shell fans.' + } + + _zshell_usage() { + print -r -- 'Oh My Zsh / experimental Shellsavers + +Usage: omz shellsaver [aquarium|logo|hermit|party] [--mono] [--seconds N] + omz shellsaver [aquarium|logo|hermit|party] --snapshot [FRAME] + omz shellsaver --list +' + _zshell_list + print -r -- ' +Any key returns to the shell. Ctrl-C also exits safely. +--list List available scenes and the default. +--mono Use the terminal default foreground color. +--seconds N Exit automatically after 1-3600 seconds. +--snapshot Print a still at 96 columns x 28 rows (no terminal needed). + +Requires Zsh 5.8+, stty, and an xterm-compatible terminal with an alternate screen. +Nothing is installed or enabled automatically.' + } + case "${1-}" in + --help|-h) _zshell_usage; exit 0 ;; + ''|--*) scene=aquarium ;; + aquarium|logo|hermit|party) scene=$1; scene_selected=1; shift ;; + *) print -u2 -r -- "omz shellsaver: unknown scene: $1 (choose aquarium, logo, hermit, or party)"; exit 2 ;; + esac + + while (( $# )); do + case "$1" in + aquarium|logo|hermit|party) + if (( scene_selected )); then + print -u2 -r -- 'omz shellsaver: choose only one scene (try --help)' + exit 2 + fi + scene=$1; scene_selected=1 ;; + --help|-h) _zshell_usage; exit 0 ;; + --list) _zshell_list; exit 0 ;; + --mono) mono=1 ;; + --seconds) + shift + if [[ $1 != <1-3600> ]]; then + print -u2 -r -- 'omz shellsaver: --seconds requires an integer from 1 to 3600' + exit 2 + fi + seconds_limit=$1 ;; + --snapshot) + snapshot=1 + if [[ ${2-} == <0-100000> ]]; then tick=$2; shift; fi ;; + *) print -u2 -r -- "omz shellsaver: unknown argument: $1 (try --help)"; exit 2 ;; + esac + shift + done + + # Paint into separate ASCII text and color planes. Clip before slicing so a + # creature partly outside the viewport never wraps onto another terminal row. + _zshell_put() { + integer x=$1 y=$2 ink=$3 count skip + local text=$4 prefix='' suffix='' iprefix='' isuffix='' + (( y >= 1 && y <= height && x <= width )) || return 0 + if (( x < 1 )); then + skip=$(( 2 - x )) + (( skip <= ${#text} )) || return 0 + text=${text[$skip,-1]} + x=1 + fi + count=$(( width - x + 1 )) + text=${text[1,$count]} + count=${#text} + (( count )) || return 0 + if (( x > 1 )); then + prefix=${canvas[$y][1,$((x-1))]} + iprefix=${inks[$y][1,$((x-1))]} + fi + if (( x + count <= width )); then + suffix=${canvas[$y][$((x+count)),-1]} + isuffix=${inks[$y][$((x+count)),-1]} + fi + canvas[$y]="$prefix$text$suffix" + inks[$y]="$iprefix${(pl:$count::$ink:)}$isuffix" + } + + _zshell_sprite() { + integer sx=$1 sy=$2 color=$3 + shift 3 + local line + for line in "$@"; do + _zshell_put $sx $sy $color "$line" + (( sy++ )) + done + } + + _zshell_logo() { + # The full-size artwork and per-letter color bands match tools/upgrade.sh. + # A compact badge preserves the bounce in narrower terminal windows. + local -a logo logo_inks quips + local line glyph bands run + integer logo_width=0 logo_height span_x span_y px py x y color i j n offset + logo=( + ' __ __ ' + ' ____ / /_ ____ ___ __ __ ____ _____/ /_ ' + ' / __ \/ __ \ / __ `__ \/ / / / /_ / / ___/ __ \ ' + '/ /_/ / / / / / / / / / / /_/ / / /_(__ ) / / / ' + '\____/_/ /_/ /_/ /_/ /_/\__, / /___/____/_/ /_/ ' + ' /____/ ' + ) + logo_inks=( + 11111111122222222333333333334444444455555556666677777 + 11111111222222223333333333344444444555555566666777777 + 11111112222222233333333333444444445555555666666777777 + 11111112222222333333333333444444455555556666667777777 + 11111122222223333333333334444444555555556666677777777 + 11112222222233333333333444444445555555666667777777777 + ) + if (( width < 64 )); then + logo=( '.------------------.' '| Oh My Zsh |' '| ~ >_ |' "'------------------'" ) + logo_inks=() + for line in "${logo[@]}"; do + bands='' + for (( j=1; j<=${#line}; j++ )); do + bands+=$((1+(j-1)*7/${#line})) + done + logo_inks+=( "$bands" ) + done + fi + for line in "${logo[@]}"; do + (( ${#line} > logo_width )) && logo_width=${#line} + done + logo_height=${#logo} + span_x=$(( width-logo_width-4 )) + span_y=$(( height-logo_height-8 )) + px=$(( (tick/2+span_x/2) % (2*span_x) )) + py=$(( (tick/6+span_y/2) % (2*span_y) )) + x=$(( 3 + (px <= span_x ? px : 2*span_x-px) )) + y=$(( 5 + (py <= span_y ? py : 2*span_y-py) )) + + _zshell_put 3 2 1 'O H M Y Z S H' + _zshell_put $((width-17)) 2 0 'prompt on a break' + for (( i=0; i_' + if (( (px == 0 || px == span_x) && (py == 0 || py == span_y) )); then + _zshell_put $((x-2)) $((y-1)) 3 '*' + _zshell_put $((x+logo_width+1)) $((y-1)) 3 '*' + _zshell_put $(( (width-17)/2 )) $((height-2)) 3 'corner committed!' + else + quips=( 'home is where the ~ is' 'plugins on a coffee break' 'a delightful little detour' 'your prompt will be right back' ) + line=${quips[$((1+tick/80%${#quips}))]} + _zshell_put $(( (width-${#line})/2 )) $((height-2)) 0 "$line" + fi + _zshell_put 3 $height 0 'SCREENSAVER / LOGO' + _zshell_put $((width-21)) $height 0 'any key: back to work' + } + + _zshell_hermit() { + integer beat=$((tick/40%8)) within=$((tick%40)) shuffle=0 x y i + local left_eye='(o )' right_eye='(o )' mouth='._.' thought='...' legs + # Long, still looks make the tiny blinks and indecisive steps more visible. + case $beat in + 0) thought='...where was I going?' ;; + 1) left_eye='( o)'; right_eye='( o)'; thought='...over there?' ;; + 2) left_eye='(O )'; right_eye='( O)'; mouth=' o '; thought='pwd...?' ;; + 3) left_eye='( o)'; right_eye='(o )'; mouth='-_-'; thought='I brought home. Now what?' ;; + 4) left_eye='( o)'; right_eye='( o)'; shuffle=$((within/10)); thought='maybe this way.' ;; + 5) left_eye='( o)'; right_eye='(o )'; shuffle=3; thought='wrong directory.' ;; + 6) shuffle=$((3-within/10)); thought='cd .. ?' ;; + 7) left_eye='( ^)'; right_eye='(^ )'; mouth=' u '; thought='~ sweet ~ ...wait.' ;; + esac + # Short blinks run independently of the longer expression cycle. + if (( tick%73 >= 71 )); then left_eye='( -)'; right_eye='(- )'; fi + x=$(( (width-42)/2 + shuffle )) + y=$(( (height-12)/2 + 1 )) + _zshell_put 3 2 1 'O H M Y Z S H' + _zshell_put $((width-21)) 2 0 'a little shell-shocked' + # Photo-inspired three-quarter silhouette: tapered shell whorls behind + # the exposed head, unequal eyestalks, antennae and a hanging front claw. + _zshell_sprite $x $y 3 \ + ' __...---.' \ + " _.-' .--. '." \ + " .-' .' '. \\" \ + " .' _.-./ ~ \ |" \ + " / .' \ / |" \ + "/ / .-. |'-----' /" \ + '| ( ( @ ) / /' \ + "\ \ '-' .' .'" \ + " '-._'--'______.-'" + # Staggered eyes on stalks; the small head emerges from the shell opening. + _zshell_put $((x+25)) $((y+2)) 5 "$left_eye" + _zshell_put $((x+32)) $((y+1)) 5 "$right_eye" + _zshell_put $((x+27)) $((y+3)) 4 '|' + _zshell_put $((x+27)) $((y+4)) 4 '|' + _zshell_put $((x+33)) $((y+2)) 4 '/' + _zshell_put $((x+32)) $((y+3)) 4 '/' + _zshell_put $((x+31)) $((y+4)) 4 '/' + _zshell_put $((x+36)) $((y+3)) 5 "_.-'" + _zshell_put $((x+33)) $((y+4)) 5 ".-'" + _zshell_put $((x+32)) $((y+5)) 5 '/' + _zshell_put $((x+22)) $((y+5)) 4 '/___' + _zshell_put $((x+21)) $((y+6)) 4 '\___' + _zshell_put $((x+20)) $((y+7)) 4 '\__' + _zshell_put $((x+26)) $((y+5)) 4 '.----.' + _zshell_put $((x+25)) $((y+6)) 4 '/ \' + _zshell_put $((x+23)) $((y+7)) 4 '__\______/' + _zshell_put $((x+21)) $((y+8)) 4 '\___/ /\ \__' + _zshell_put $((x+27)) $((y+6)) 5 "$mouth" + # Legs bend down and forward instead of spreading symmetrically sideways. + _zshell_put $((x+18)) $((y+9)) 4 '\___/ / \___' + _zshell_put $((x+16)) $((y+10)) 4 '___/ / \' + legs=' _/____/ \_' + if (( (beat == 4 || beat == 6) && within/3%2 )); then + legs=' /___/ _/' + fi + _zshell_put $x $((y+11)) 4 "$legs" + # The big claw hangs in front. The much smaller claw tucks under the face. + if (( beat == 3 )); then + _zshell_sprite $((x+35)) $((y+7)) 4 ' __ ' '/ \' '| / |' ' V V ' + _zshell_put $((x+32)) $((y+6)) 4 '()' + else + _zshell_sprite $((x+35)) $((y+8)) 4 ' __ ' '/ \' '| / |' ' V V ' + _zshell_put $((x+30)) $((y+8)) 4 '()' + fi + if (( beat == 2 || beat == 5 )); then + _zshell_put $((x+39)) $y 1 '?' + fi + if (( height >= 24 )); then + for (( i=3; i= 120 )); then + scale=2 + fi + letters=( O H M Y Z S H ) + letter_colors=( 4 3 6 1 7 2 4 ) + logo_width=$(( (7*glyph_width+6+4)*scale )) + logo_x=$(( (width-logo_width)/2+1 )) + logo_y=$(( height/4 )); (( logo_y < 4 )) && logo_y=4 + + # A terminal window behind the crowd, including tiny window controls. + border="+${(pl:$((width-8))::-:)}+" + _zshell_put 4 2 5 "$border" + _zshell_put 4 $((height-5)) 5 "$border" + for (( y=3; y 1 )); then + _zshell_put $((sx+1)) $((sy+3)) 5 '|' + _zshell_put $((sx+6)) $((sy+3)) 5 '|' + fi + leg_y=$((sy+3+body_rows)) + if (( outfit == 2 )); then + if (( pose%2 )); then + _zshell_sprite $sx $leg_y 5 ' | |\ \ ' ' |_/_\_\' + else + _zshell_sprite $sx $leg_y 5 ' | || | ' ' |_||_| ' + fi + else + case $pose in + 0) _zshell_sprite $sx $leg_y 5 ' / \ ' ' _/ \_ ' ;; + 1) _zshell_sprite $sx $leg_y 5 ' > / ' ' / /_ ' ;; + 2) _zshell_sprite $sx $leg_y 5 ' / < ' ' /_ \_ ' ;; + 3) _zshell_sprite $sx $leg_y 5 ' \ \ ' ' _\ _\ ' ;; + esac + fi + # Arms animate around each silhouette, leaving hair and clothes stable. + if (( pose == 1 || pose == 3 )); then + _zshell_put $sx $sy 5 '\' + _zshell_put $((sx+1)) $((sy+1)) 5 '\' + _zshell_put $((sx+1)) $((sy+2)) 5 '|' + else + _zshell_put $((sx+1)) $((sy+3)) 5 '/' + _zshell_put $sx $((sy+4)) 5 '/' + fi + if (( pose == 1 || pose == 2 )); then + _zshell_put $((sx+7)) $sy 5 '/' + _zshell_put $((sx+6)) $((sy+1)) 5 '/' + _zshell_put $((sx+6)) $((sy+2)) 5 '|' + else + _zshell_put $((sx+6)) $((sy+3)) 5 '\' + _zshell_put $((sx+7)) $((sy+4)) 5 '\' + fi + done + _zshell_put 3 $height 0 'SCREENSAVER / PARTY' + _zshell_put $((width-21)) $height 0 'any key: after-party' + } + + _zshell_scene() { + integer y x i j base phase sway drift bob floor=$(( height - 5 )) + local leaf bubble + blank=${(pl:$width:: :)} + canvas=() inks=() + for (( y=1; y<=height; y++ )); do + canvas+=( "$blank" ) + inks+=( "${(pl:$width::0:)}" ) + done + if (( width < 48 || height < 18 )); then + if [[ $scene == aquarium ]]; then + _zshell_put 2 2 1 'A little more ocean, please.' + else + _zshell_put 2 2 1 'A little more room, please.' + fi + _zshell_put 2 4 0 'Resize to at least 49 x 18.' + _zshell_put 2 6 0 'Any key returns to your shell.' + return + fi + if [[ $scene == logo ]]; then + _zshell_logo + return + elif [[ $scene == hermit ]]; then + _zshell_hermit + return + elif [[ $scene == party ]]; then + _zshell_party + return + fi + + _zshell_put 3 2 1 'O H M Y Z S H' + _zshell_put $((width-17)) 2 0 'a shell aquarium' + _zshell_put 3 $height 0 'SCREENSAVER / AQUARIUM' + _zshell_put $((width-22)) $height 0 'any key: back to shore' + + # Tiny particles occupy the background; bubbles rise at different speeds. + for (( i=0; i 1 )) && leaf='(' + _zshell_put $((base + (sway>1))) $((floor+1-j)) 6 "$leaf" + done + done + + # Spirals ride a slow current. A triangle wave provides gentle vertical bob. + drift=$(( (tick / 9) % (width + 18) )) + x=$(( (width/4 + drift) % (width+18) - 9 )) + phase=$(( tick/12 % 6 )); bob=$(( phase < 3 ? phase : 5-phase )) + _zshell_sprite $x $((5+bob)) 2 ' __ ' ' /@/)' ' \_/' + + # Nautilus moves in the opposite direction, tentacles trailing behind it. + x=$(( (width*3/4 + width+20 - tick/6 % (width+20)) % (width+20) - 10 )) + _zshell_sprite $x $((5+(tick/18%2))) 2 ' .---. ' ' / (@) \ ' ' \ __/~~' ' `" ~~' + + # A fan shell closes briefly, then scoots during the same part of its cycle. + phase=$(( tick % 100 )) + drift=$(( (tick/100*4 + (phase>80 ? (phase-80)/5 : 0)) % (width+14) )) + x=$(( (width/2 + drift) % (width+14) - 7 )) + y=$(( floor-4 - (phase>80 && phase<95 ? 1 : 0) )) + if (( phase > 80 )); then + _zshell_sprite $x $y 4 ' _______ ' '(_______)' ' \/' + else + _zshell_sprite $x $y 4 ' .-----.' ' /|/|\|\' ' \|||||/' ' \___/' + fi + + # Conch and hermit crab live on different stretches of the seabed. + x=$(( 7 + (width/5 + tick/24) % (width-22) )) + _zshell_sprite $x $((floor-1)) 3 ' /\__' ' _/ / /\>' '(__/_/_)' + x=$(( 8 + (width*2/3 + tick/10) % (width-22) )) + if (( tick/5 % 2 )); then + _zshell_sprite $x $((floor-1)) 5 ' .-~-. ' ' ( ~ )oo' ' _/___/\\_' + else + _zshell_sprite $x $((floor-1)) 5 ' .-~-. ' ' ( ~ )oo' ' /_/_/ /\' + fi + } + + # Render color runs rather than individual cells. One write per frame and no + # per-frame subprocesses; the final column stays unused to prevent autowrap. + _zshell_render() { + integer row n + local text colors ink run + result=${terminfo[home]} + for (( row=1; row<=height; row++ )); do + text=${canvas[$row]} colors=${inks[$row]} + while [[ -n $colors ]]; do + ink=${colors[1]} + run=${colors%%[^$ink]*} + n=${#run} + result+="${palette[$((ink+1))]}${text[1,$n]}" + text=${text[$((n+1)),-1]} + colors=${colors[$((n+1)),-1]} + done + result+=${terminfo[el]} + (( row < height )) && result+=$'\r\n' + done + result+=${terminfo[sgr0]} + } + + if (( snapshot )); then + _zshell_scene + print -r -- "${(F)canvas}" + exit 0 + fi + if [[ ! -t 0 || ! -t 1 || -z $TERM || $TERM == dumb ]]; then + print -u2 -r -- 'omz shellsaver: run in an interactive terminal (or use --snapshot)' + exit 1 + fi + zmodload zsh/terminfo || exit 1 + zmodload zsh/datetime || exit 1 + local capability + for capability in smcup rmcup home clear civis cnorm sgr0 el; do + if [[ -z ${terminfo[$capability]} ]]; then + print -u2 -r -- "omz shellsaver: terminal lacks $capability; try --snapshot" + exit 1 + fi + done + + if (( mono || ${+NO_COLOR} || ${terminfo[colors]:-0} < 8 )); then + palette=( '' '' '' '' '' '' '' '' '' ) + elif (( ${terminfo[colors]} >= 256 )); then + palette=( $'\e[38;5;60m' $'\e[38;5;159m' $'\e[38;5;183m' + $'\e[38;5;222m' $'\e[38;5;210m' $'\e[38;5;230m' + $'\e[38;5;72m' $'\e[38;5;110m' $'\e[38;5;137m' ) + else + palette=( $'\e[34m' $'\e[36m' $'\e[35m' $'\e[33m' $'\e[31m' + $'\e[37m' $'\e[32m' $'\e[36m' $'\e[33m' ) + fi + + # Match the updater's exact truecolor and 256-color rainbow. The other + # scenes retain their own palettes, and monochrome always takes precedence. + if [[ $scene == logo ]] && (( !mono && !${+NO_COLOR} && ${terminfo[colors]:-0} >= 8 )); then + local -a rainbow + if [[ $COLORTERM == (truecolor|24bit) || $TERM == (iterm|tmux-truecolor|linux-truecolor|xterm-truecolor|screen-truecolor) ]]; then + rainbow=( $'\e[38;2;255;0;0m' $'\e[38;2;255;97;0m' $'\e[38;2;247;255;0m' + $'\e[38;2;0;255;30m' $'\e[38;2;77;0;255m' $'\e[38;2;168;0;255m' + $'\e[38;2;245;0;172m' ) + elif (( ${terminfo[colors]} >= 256 )); then + rainbow=( $'\e[38;5;196m' $'\e[38;5;202m' $'\e[38;5;226m' + $'\e[38;5;082m' $'\e[38;5;021m' $'\e[38;5;093m' $'\e[38;5;163m' ) + else + rainbow=( $'\e[31m' $'\e[33m' $'\e[33m' $'\e[32m' $'\e[34m' $'\e[35m' $'\e[35m' ) + fi + integer color_index + for (( color_index=1; color_index<=7; color_index++ )); do + palette[$((color_index+1))]=${rainbow[$color_index]} + done + fi + + _zshell_cleanup() { + # Ignore handled signals throughout restoration, including in child stty. + # Keep active set until both the screen and terminal modes are restored. + trap '' INT TERM HUP QUIT TSTP WINCH + (( active )) || return 0 + print -rn -- "${terminfo[sgr0]}${terminfo[cnorm]}${terminfo[rmcup]}" + command stty "$saved_tty" 2>/dev/null + active=0 + } + _zshell_size() { + local dimensions + integer old_width=$width old_height=$height first=$resized + dimensions=$(command stty size 2>/dev/null) || return 1 + local -a size=( ${=dimensions} ) + (( ${#size} == 2 && size[1] > 0 && size[2] > 1 )) || return 1 + height=$size[1] width=$((size[2]-1)) + # Very large terminals retain a calm, bounded scene in the top-left corner. + (( width > 180 )) && width=180 + (( height > 55 )) && height=55 + resized=0 + if (( first || old_width != width || old_height != height )); then + print -rn -- "${terminfo[clear]}" + fi + return 0 + } + + saved_tty=$(command stty -g) || exit 1 + trap '_zshell_cleanup' EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + trap 'exit 129' HUP + trap 'exit 131' QUIT + # Suspending a raw terminal would strand the parent prompt. Exit on Ctrl-Z. + trap 'exit 148' TSTP + trap 'resized=1' WINCH + { + active=1 + command stty -echo -icanon -ixon min 1 time 0 || exit 1 + print -rn -- "${terminfo[smcup]}${terminfo[civis]}" + float started=$EPOCHREALTIME deadline wait_for next_size_check=0 + while true; do + # Zsh can defer WINCH while a timed read is active. Poll at 2 Hz as well. + if (( resized || EPOCHREALTIME >= next_size_check )); then + _zshell_size || exit 1 + next_size_check=$(( EPOCHREALTIME + 0.5 )) + fi + tick=$(( (EPOCHREALTIME-started)*10 )) + if (( seconds_limit && EPOCHREALTIME-started >= seconds_limit )); then break; fi + deadline=$(( EPOCHREALTIME + 0.1 )) + _zshell_scene + _zshell_render + print -rn -- "$result" + wait_for=$(( deadline-EPOCHREALTIME )) + (( wait_for < 0.005 )) && wait_for=0.005 + if read -r -k 1 -u 0 -t $wait_for key; then + # Drain the entire key sequence or paste until input goes quiet, so no + # queued suffix (including pasted commands) reaches the parent prompt. + while read -r -k 1 -u 0 -t 0.01 key; do :; done + break + fi + done + } always { + _zshell_cleanup + } + exit 0 +}