summary refs log tree commit diff
path: root/Functions
diff options
context:
space:
mode:
Diffstat (limited to 'Functions')
-rw-r--r--Functions/Newuser/zsh-newuser-install1006
1 files changed, 994 insertions, 12 deletions
diff --git a/Functions/Newuser/zsh-newuser-install b/Functions/Newuser/zsh-newuser-install
index 6c54dc509..e042af08a 100644
--- a/Functions/Newuser/zsh-newuser-install
+++ b/Functions/Newuser/zsh-newuser-install
@@ -1,23 +1,1005 @@
 # Function to install startup files for a new user.
-# This dummy version simply creates a new .zshrc with a comment.
-# FIXME: we don't want to distribute a file that does that, it
-# would be preferable to do nothing at all.
+# Currently it only creates or edits .zshrc.
+#
+# It can be run again by giving it the option "-f".
 
 # Sanitize environment.
 emulate -L zsh
+setopt extendedglob nonomatch warncreateglobal
 
+# How the function will be referred to.
+local myname=zsh-newuser-install
+# The directory in which to look for and save .zshrc.
 local zd=${ZDOTDIR:-$HOME}
+# The same directory in a user friendly form, i.e. with ~ replacement.
+# (We don't want to use glob_subst since that has other side effects.)
+local zdmsg
+# The message used if an other blank .zshrc is created.
+local msg="# Created by newuser for $ZSH_VERSION"
+# The lines marking the start and end of the section edited.
+local startline="# Lines configured by $myname"
+local endline="# End of lines configured by $myname"
+# Prompts used for reading a key.  The initial "?" is required.
+local shortprompt="?--- Type a key --- "
+local longprompt="?--- Type one of the keys in parentheses --- "
+# Prefix for all temporary files.  Any files starting with this
+# will be removed at the end of the script.
+local tmpfile=${TMPPREFIX:-/tmp/zsh}-zni-$$
+# Report of the state of settings for the top-level menu.
+local -A install_state
+# Values of all parameters etc. to be saved (including
+# those read in from the existing file.)
+local -A parsed_parameters parsed_options parsed_bindings parsed_keymaps
+# Corresponding state in a user-readable form.
+local -A state_parameters state_options state_bindings state_keymaps
+# Lines read in from between $startline and $endline which were
+# not understood.  These are retained but moved out of that section
+# with a message.
+local -a unparsed
+# Lines used in submenus: the setting to output in a form
+# that can be exeucuted (an assignment, setopt or unsetopt), a brief message
+# about the setting, and the state copied from and to state_parameters or
+# state_options.  Elements of all three arrays must correspond.
+local -a output_lines display_lines state_lines
+# Variable indicating some of the lines in the above variables
+# have been read in, i.e. the user has already configured the
+# particular set of settings.
+integer lines_read
+# Lines to set up completion.  This is special as it is only
+# edited by compinstall, not this function.
+local -a completion_lines
+# Utility variables
+local -a reply match mbegin mend
+# Key read from user, used all over the place.
+local key
+integer save lines_found
 
-# The zsh/newuser module already tests for the following, so this test only
-# triggers if zsh-newuser-install is run by hand.
-#
-# In future we may want to use this mechanism to update startup files.
-if [[ -e $zd/.zshenv || -e $zd/.zprofile || -e $zd/.zshrc || -e $zs/.zlogin ]]
-then
-  print "zsh-newuser-install:  startup files exist, aborting" >&2
+install_state[history]=Recommended
+install_state[completion]=Recommended
+
+# Don't save anything if interrupted.
+trap 'save=0' HUP INT QUIT
+
+# Substitute an initial ~ for human consumption.
+if [[ $zd = $HOME ]]; then
+  zdmsg="~"
+else
+  zdmsg=$zd
+fi
+
+# Don't run unless we can talk to the user.
+if [[ ! -t 0 || ! -t 1 ]]; then
+  if [[ $1 = -f ]]; then
+    print -r "$myname: can only be used interactively." >&2
+  fi
+  return 1
+fi
+
+# Don't run unless terminal is sane.
+if (( ${LINES:-0} < 15 || ${COLUMNS:-0} < 72 )); then
   return 1
 fi
 
-echo "# Created by newuser for $ZSH_VERSION" >$zd/.zshrc
+if [[ $1 != -f ]]; then
+  # The zsh/newuser module already tests for the following, so this test only
+  # triggers if zsh-newuser-install is run by hand.
+  if [[ -e $zd/.zshenv || -e $zd/.zprofile || \
+        -e $zd/.zshrc || -e $zd/.zlogin ]]; then
+    print -r "$myname:  startup files exist, aborting.
+
+Use the argument -f if you want to force the function to be run again." >&2
+    return 1
+  fi
+fi
+
+
+# start of try block for tidy-up in always block
+{
+
+########################################################################
+# Utility functions
+########################################################################
+
+# All internal functions start with __zni_.  These will be removed
+# when the main function exits.
+
+# Read existing lines from .zshrc, if any.
+__zni_retrieve_lines() {
+  local line
+
+  reply=()
+
+  lines_found=0
+
+  [[ -f $zd/.zshrc ]] || return 1
+
+  grep "$startline" $zd/.zshrc 1>/dev/null 2>&1 || return 1
+
+  lines_found=1
+
+  sed -n "/^[	]*$startline/,/^[	]*$endline/p" $zd/.zshrc |
+  while read -r line; do
+    reply+=($line)
+  done
+
+  return 0
+}
+
+
+# First argument is a state; other arguments are lines
+# to parse.  They should either contain single assignments or
+# setopt or unsetopt statements.  The state for each parameter
+# or option so parsed is set to the value given by the first argument.
+__zni_parse_lines() {
+  local line opt warned first
+  local -a args
+  local state=$1
+
+  shift
+
+  for line in "$@"; do
+    case $line in
+      ((#b)[[:blank:]]#([[:IDENT:]]##)=(*))
+      parsed_parameters[$match[1]]=$match[2]
+      state_parameters[$match[1]]=$state
+      ;;
+
+      ((#b)[[:blank:]]#(un|)setopt[[:blank:]]##(*))
+      # TBD: handle setopt noX / unsetopt X
+      for opt in ${=match[2]}; do
+	opt=${${opt//(#m)[[:upper:]]/${(L)MATCH}}//_}
+	if [[ $match[1] = un ]]; then
+	  parsed_options[$opt]=off
+	else
+	  parsed_options[$opt]=on
+	fi
+	state_options[$opt]=$state
+      done
+      ;;
+
+      ((#b)[[:blank:]]#bindkey[[:blank:]]##(*))
+      args=(${(z)match[1]})
+      # store keys unquoted: will need quoting for output.
+      first=${(Q)args[1]}
+      shift args
+      if [[ $first = -[ev] && ${#args} -eq 0 ]]; then
+	case $first in
+	  (-e)
+	  parsed_keymaps[main]=emacs
+	  ;;
+
+	  (-v)
+	  parsed_keymaps[main]=vi
+	  ;;
+	esac
+	state_keymaps[main]=$state
+      else
+	# TODO: handling keymap options
+	parsed_bindings[first]=${args[2,-1]}
+	state_bindings[first]=$state
+      fi
+      ;;
+
+      ([[:blank:]]#($startline|$endline|))
+      ;;
+
+      (*)
+      unparsed+=($line)
+      print -r "WARNING: failed to understand line:
+  $line
+which will be retained but not edited."
+      warned=1
+      ;;
+    esac
+  done
+
+  if [[ -n $warned ]]; then
+    read -k key$shortprompt
+  fi
+}
+
+# Apply defaults.  Arguments in the form
+#   -p parameter_name default_value description
+#      ...
+#   -o option_name default=on|off description
+#      ...
+#   -b bindkey_string default_value description
+#      ...
+#   -B default_keymap=emacs|vi|none description
+#
+# They're not really defaults (they're not the same as the
+# builtin defaults), so the description output is "not yet saved".
+#
+# All variables to be edited in this section must be mentioned,
+# though defaults can be blank in which case nothing will be
+# saved unless the variable is set by the user.  The description
+# is then "no value set".
+#
+# -B is a bit strange: it's simply designed to allow the user to
+# select "bindkey -e" for Emacs or "bindkey -v" for vi.  It only
+# takes a single argument.  Real key bindings use -b.
+#
+# This operation transfers some subset of settings from the parsed_*
+# and state_* variables to the *_lines variables for editing.
+__zni_apply_defaults() {
+  local un
+
+  # Reset the lines to be edited.
+  state_lines=()
+  display_lines=()
+  output_lines=()
+  lines_read=0
+
+  case $1 in
+    (-p)
+    shift
+    while [[ $# -gt 0 && $1 != -* ]]; do
+      # skip default if it was read in
+      if [[ -z $state_parameters[$1] ]]; then
+	parsed_parameters[$1]=$2
+	if [[ -n $2 ]]; then
+	  state_parameters[$1]="not yet saved"
+	else
+	  state_parameters[$1]="no value set"
+	fi
+      elif [[ $state_parameters[$1] = saved ]]; then
+	(( lines_read++ ))
+      fi
+      state_lines+=($state_parameters[$1])
+      display_lines+=("$3")
+      output_lines+=("$1=$parsed_parameters[$1]")
+
+      shift 3
+    done
+    ;;
+
+    (-o)
+    shift
+    while [[ $# -gt 0 && $1 != -* ]]; do
+      # skip default if there was a setting
+      if [[ -z $state_options[$1] ]]; then
+	parsed_options[$1]=$2
+	if [[ -n $2 ]]; then
+	  state_options[$1]="not yet saved"
+	else
+	  state_options[$1]="no value set"
+	fi
+      elif [[ $state_parameters[$1] = saved ]]; then
+	(( lines_read++ ))
+      fi
+      if [[ $parsed_options[$1] = on ]]; then
+	un=
+      else
+	# display as unsetopt even if no value to save yet
+	un=un
+      fi
+      state_lines+=($state_options[$1])
+      display_lines+=("$3")
+      output_lines+=("${un}setopt $1")
+
+      shift 3
+    done
+    ;;
+
+    (-b)
+    shift
+    # this will barf on bindings beginning -; there's no good
+    # reason to rebind that, even in vi command mode, so perhaps
+    # we just add it to the sanity checks when we get around to them.
+    while [[ $# -gt 0 && $1 != -* ]]; do
+      if [[ -z $state_bindings[$1] ]]; then
+	parsed_bindings[$1]=$2
+	if [[ -n $2 ]]; then
+	  state_bindings[$1]="not yet saved"
+	else
+	  state_bindings[$1]="no value set"
+	fi
+      elif [[ $state_bindings[$1] = saved ]]; then
+	(( lines_read++ ))
+      fi
+      state_lines+=($state_bindings[$1])
+      display_lines+=("$3")
+      output_lines+=("bindkey ${(qq)1}${2:+ $2}")
+
+      shift 3
+    done
+    ;;
+
+    (-B)
+    shift
+    if [[ -z $state_keymaps[main] ]]; then
+      parsed_keymaps[main] = $1
+      if [[ $1 = none ]]; then
+	state_keymaps[main]="no value set"
+      else
+	state_keymaps[main]="not yet saved"
+      fi
+    elif [[ $state_keymaps[main] = saved ]]; then
+      (( lines_read++ ))
+    fi
+    state_lines+=($state_keymaps[main])
+    display_lines+=("$2")
+    # display as -e even if no value to save yet
+    if [[ $parsed_keymaps[main] = vi ]]; then
+      output_lines+=("bindkey -v")
+    else
+      output_lines+=("bindkey -e")
+    fi
+
+    shift 2
+    ;;
+  esac
+}
+
+
+# Display and edit the settings given by the set of *_lines arrays.
+# If requested by the user, apply the settings, updating the
+# parsed_* and state_* variables.
+__zni_display_and_edit() {
+  integer i changes
+  local default edval ldisp rdisp
+  local -a states displays outputs tstval
+
+  states=("${state_lines[@]}")
+  displays=("${display_lines[@]}")
+  outputs=("${output_lines[@]}")
+
+  while true; do
+    clear
+    print -r $1
+    # snicker...
+    print -r ${(l.${#1}..=.):-}
+    print
+    if (( $# > 1 )); then
+      print -rl $argv[2,-1]
+      print
+    fi
+
+    # Output each setting with a description and state.
+    for (( i = 1; i <= ${#output_lines}; i++ )); do
+      default=$states[$i]
+      if [[ $default = ("no value set"|"not to be saved") ]]; then
+	ldisp="# $outputs[$i]"
+      else
+	ldisp=$outputs[$i]
+      fi
+      rdisp=${default:+($default)}
+      print -r "# ($i) $displays[$i]
+$ldisp${(l.$COLUMNS-${#ldisp}-${#rdisp}-1.):-}$rdisp"
+    done
+
+    if (( changes )); then
+      print -r "
+# (0)  Remember edits and return to main menu (does not save file yet)
+# (q)  Abandon edits and return to main menu
+"
+    else
+      print -r "
+# (0) or (q)  Return to main menu (no changes made yet)
+"
+    fi
+    read -k key$longprompt
+    print
+
+    if [[ $key = <-> && $key -ge 1 && $key -le ${#outputs} ]]; then
+      (( i = key ))
+      case $outputs[$i] in
+	((#b)(|un)setopt' '(*))
+	while true; do
+	  clear
+	  print "Option $match[2]:  $displays[$i]
+The option is currently ${match[1]:+un}set.
+Type:
+  (s) to set it
+  (u) to unset it
+  (n) not to set or unset it (use shell default)
+  (k) to keep the current setting:"
+	  read -k key$shortprompt
+	  print
+
+	  case $key in
+	    (s)
+	    (( changes++ ))
+	    outputs[$i]="setopt $match[2]"
+	    states[$i]="set but not saved"
+	    ;;
+
+	    (s)
+	    (( changes++ ))
+	    outputs[$i]="unsetopt $match[2]"
+	    states[$i]="set but not saved"
+	    ;;
+
+	    (n)
+	    (( changes++ ))
+	    outputs[$i]="unsetopt $match[2]"
+	    states[$i]="no value set"
+	    ;;
+
+	    (k)
+	    ;;
+
+	    (*)
+	    continue
+	    ;;
+	  esac
+	  break;
+	done
+	;;
+
+	((#b)([^=]##)=(*))
+	print -r "Variable ${match[1]}:  $displays[$i]
+Edit a value.  If it is left blank, nothing will be saved:"
+	edval=$match[2]
+	if vared -p "$match[1]> " -h edval; then
+	  # check this assignment doesn't produce multiple words
+	  # e.g. "HISTFILE=never rm -f ~" does produce multiple words...
+	  # this isn't perfect, e.g. "(this would get split on assignment)",
+	  # but that's fairly benign.
+	  tstval=(${=edval})
+	  if (( ${#tstval} > 1 )); then
+	    print "Error: value isn't a single word.
+Use quotes or backslashes if your value contains spaces.
+Note that you shouldn't quote an initial ~ in file names." >&2
+	    read -k key$shortprompt
+	    # now check the assignment works...
+	    # don't suppress any errors, they may be useful.
+	    # this means we need to suppress warncreateglobal.
+	  elif ! ( typeset -g $match[1]; eval "$match[1]=$edval" ); then
+	    print "Error: bad shell syntax in value.
+The value will be assigned to the variable exactly as you enter it.
+Make sure all quotes are paired." >&2
+	    read -k key$shortprompt
+	  else
+	    outputs[$i]="$match[1]=$edval"
+	    if [[ -n $edval ]]; then
+	      states[$i]="set but not saved"
+	    else
+	      states[$i]="no value set"
+	    fi
+	    (( changes++ ))
+	  fi
+	else
+	  read -k key'?--- Edit abandoned, type a key --- '
+	fi
+	;;
+
+	(bindkey' '-[ev])
+	while true; do
+	  print -nr "Pick a keymap (set of keys) to use when editing.
+Type:
+  (e) for Emacs keymap (recommended unless you are vi user)
+  (v) for Vi keymap
+  (n) not to set a keymap (allow shell to choose)
+  (k) to keep the current setting, "
+	  if [[ $state_lines[$i] = ("no value set"|"not to be saved") ]]
+	  then
+	    print -r "(n):"
+	  elif [[ $output_lines[$i] = *-v ]]; then
+	    print -r "(v):"
+	  else
+	    print -r "(e):"
+	  fi
+	  read -k key$longprompt
+	  case $key in
+	    (e)
+	    (( changes++ ))
+	    outputs[$i]="bindkey -e"
+	    states[$i]="set but not saved"
+	    ;;
+
+	    (v)
+	    (( changes++ ))
+	    outputs[$i]="bindkey -v"
+	    states[$i]="set but not saved"
+	    ;;
+
+	    (n)
+	    (( changes++ ))
+	    outputs[$i]="bindkey -e"
+	    states[$i]="not to be saved"
+	    ;;
+
+	    (k)
+	    ;;
+
+	    (*)
+	    continue
+	    ;;
+	  esac
+	  break
+	done
+	;;
+
+	(bindkey' '*)
+	# TODO: this needs writing.  We need to be able to read
+	# keys and translate them, sanity check them, and ideally
+	# handle keymaps, at least vi command and insert.
+	;;
+
+	(*)
+	print "*** Internal error: bad setting '$outputs[$i]' ***" >&2
+	read -k key'?--- Type a key in forlorn hope --- '
+	;;
+      esac
+    elif [[ $key = 0 ]]; then
+      # Update the *_lines variables
+      state_lines=("${states[@]}")
+      display_lines=("${displays[@]}")
+      output_lines=("${outputs[@]}")
+
+      # Also save any lines suitably marked to parsed_* and state_*
+      # by rerunning __zni_parse_lines on each such line.
+      for (( i = 1; i <= ${#output_lines}; i++ )); do
+	if [[ $state_lines[$i] = ("set but not saved"|"not to be saved") ]]
+	then
+	  __zni_parse_lines $state_lines[$i] $output_lines[$i]
+	fi
+      done
+
+      return $(( changes == 0 ))
+    elif [[ $key = [qQ] ]]; then
+      return 1
+    fi
+  done
+}
+
+
+# Print and despatch a submenu.
+# The first argument is the title.  The remaining arguments
+# are pairs of descriptions and functions to execute.
+# There shouldn't be more than 9 entries.
+# The usual entries 0 and q are added automatically.
+__zni_submenu() {
+  local title=$1
+  local desc func
+  local -a descs funcs
+  integer i
+
+  shift
+
+  clear
+  print -r $title
+  print -r ${(l.${#title}..=.):-}
+
+  for desc func; do
+    if [[ -z $func ]]; then
+      print "*** Internal error: bad argument set for __zni_submenu ***" >&2
+      read -k key'?--- Type a key in forlorn hope --- '
+      return 1
+    fi
+
+    descs+=($desc)
+    funcs+=($func)
+  done
+
+  while true; do
+    for (( i = 1; i <= ${#descs}; i++ )); do
+      print -r "
+($i)  $descs[$i]"
+    done
+    print -r "
+(0) or (q)  Return to previous menu"
+
+    read -k key$longprompt
+
+    if [[ $key = [0qQ] ]]; then
+      return 1
+    elif (( key >= 1 && key <= ${#funcs} )); then
+      $funcs[$key]
+    fi
+  done
+}
+
+
+# Save all values that have been edited to .zshrc.
+__zni_save() {
+  local key optline newline
+  local -a on_opts off_opts lines lines2
+  integer i
+
+  # Record lines containing parameter settings, sorted.
+  for key in ${(ok)parsed_parameters}; do
+    if [[ $state_parameters[$key] != ("no value set"|"not to be saved") ]]
+    then
+      lines+=("$key=$parsed_parameters[$key]")
+    fi
+  done
+
+  # Search through sorted options, make list of those to
+  # be turned on and off.  Those marked "no value set" aren't
+  # to be output.
+  for key in ${(ok)parsed_options}; do
+    if [[ $state_options[$key] != ("no value set"|"not to be saved") ]]; then
+      if [[ $parsed_options[$key] = on ]]; then
+	on_opts+=($key)
+      else
+	off_opts+=($key)
+      fi
+    fi
+  done
+
+  # Construct lines of options to turn on, keeping them short.
+  optline="setopt"
+  for (( i = 1; i <= ${#on_opts}; i++ )); do
+    newline="$optline $on_opts[$i]"
+    if [[ ${#newline} -ge 72 ]]; then
+      lines+=($optline)
+      optline="setopt $on_opts[$i]"
+    else
+      optline=$newline
+    fi
+    if (( i == ${#on_opts} )); then
+      lines+=($optline)
+    fi
+  done
+
+  # Construct lines of options to turn off, keeping them short.
+  optline="unsetopt "
+  for (( i = 1; i <= ${#off_opts}; i++ )); do
+    newline="$optline $off_opts[$i]"
+    if [[ ${#newline} -ge 72 ]]; then
+      lines+=($optline)
+      optline="unsetopt $off_opts[$i]"
+    else
+      optline=$newline
+    fi
+    if (( i == ${#off_opts} )); then
+      lines+=($optline)
+    fi
+  done
+
+  # Construct lines of bindkey commands.  First the keymap.
+  if [[ $state_keymaps[main] != (|"no value set"|"not to be saved") ]]; then
+    case $parsed_keymaps[main] in
+      (emacs)
+      lines+=("bindkey -e")
+      ;;
+
+      (vi)
+      lines+=("bindkey -v")
+      ;;
+    esac
+  fi
+  # Now bindings.
+  for key in ${(ok)parsed_bindings}; do
+    if [[ $state_bindings[$key] != ("no value set"|"not to be saved") ]]; then
+      lines+=("bindkey ${(qq)key} ${parsed_bindings[$key]}")
+    fi
+  done
+
+  # Save the lines with a start and end marker to a temporary file.
+  print -rl $startline $lines $endline >$tmpfile
+
+  if (( ${#unparsed} )); then
+    print "# The following lines were read by $myname.
+# They were moved here as they could not be understood.
+# $(date)
+${(F)unparsed}
+# End of lines moved by $myname." >>$tmpfile
+  fi
+
+  if grep "$startline"  $zd/.zshrc 1>/dev/null 2>&1; then
+    # Found the start line; replace the section.
+    # We could this by reading the lines in zsh, but in case
+    # the .zshrc is huge it's perhaps better to use sed.
+    sed -e "/^[		]*$endline/r $tmpfile
+/^[	]*$startline/,/^[	]*$endline/d" $zd/.zshrc >${tmpfile}.repl &&
+    cp ${tmpfile}.repl $zd/.zshrc
+  else
+    # No current start marker; just append.
+    cat $tmpfile >>$zd/.zshrc
+  fi
+}
+
+
+########################################################################
+# Specific configurations
+########################################################################
+
+__zni_history_config() {
+  __zni_apply_defaults -p \
+    HISTORY 1000 "Number of lines of history kept within shell" \
+    HISTFILE $zdmsg/.histfile "File where history is saved" \
+    SAVEHIST 1000 "Number of lines of history to save to \$HISTFILE"
+
+  if __zni_display_and_edit "History configuration"; then
+    install_state[history]="Unsaved changes"
+    save=1
+  fi
+}
+
+
+__zni_completion_config() {
+  autoload -Uz compinstall
+  if compinstall -d; then
+    print "The completion system has already been activated.
+You can run the configuration tool (compinstall) at any time by typing
+   autoload -Uz compinstall
+   compinstall
+Do you wish to run it now [y/n]?"
+    read -k key$shortprompt
+    if [[ $key = [yY] ]]; then
+      compinstall
+    fi
+    print
+  else
+    while true; do
+      clear
+      print "The new completion system (compsys) allows you to complete
+commands, arguments and special shell syntax such as variables.  It provides
+completions for a wide range of commonly used commands in most cases simply
+by typing the TAB key.  Documentation is in the zshcompsys manual page.
+If it is not turned on, only a few simple completions such as filenames
+are available but the time to start the shell is slightly shorter.
+
+You can:
+  (1)  Turn on completion with the default options.
+
+  (2)  Run the configuration tool (compinstall).  You can also run
+       this from the command line with the following commands:
+        autoload -Uz compinstall
+        compinstall
+       if you don't want to configure completion now.
+
+  (0)  Don't turn on completion.
+"
+      read -k key$longprompt
+      case $key in
+	(1)
+	completion_lines=${(f)"$(compinstall -o)"}
+	install_state[completion]="Unsaved changes"
+	save=1
+	;;
+
+	(2)
+	compinstall
+	install_state[completion]="Configured"
+	;;
+
+	(0)
+	completion_lines=()
+	install_state[completion]="Recommended"
+	;;
+
+	(*)
+	continue
+	;;
+      esac
+      break
+    done
+  fi
+}
+
+__zni_bindkey_config() {
+  __zni_apply_defaults -B none "Change default editing configuration"
+
+  if __zni_display_and_edit "Default editing configuration" \
+    "The keys in the shell's line editor can be made to behave either" \
+    "like Emacs or like Vi, two common Unix editors.  If you have no" \
+    "experience of either, Emacs is recommended.  If you don't pick one," \
+    "the shell will try to guess based on the EDITOR environment variable." \
+    "Usually it's better to pick one explicitly."; then
+    install_state[bindkey]="Unsaved changes"
+    save=1
+  fi
+}
+
+__zni_completion_save() {
+  if (( ${#completion_lines} )); then
+    # We don't try to replace existing lines of completion configuration ---
+    # that's up to compinstall.  We should already have tested that
+    # there was no existing completion set up.
+    print -rl $completion_lines >>$zd/.zshrc
+  fi
+}
+
+
+__zni_options_config() {
+  # when we have enough, should use:
+  #   __zni_submenu "Common shell options"
+
+  # This is deliberately just a tiny selection.
+  # Feel free to extend it, but if you do, consider using __zni_submenu.
+  # The "no" prefix is used to indicate options on by default.
+  __zni_apply_defaults -o autocd '' "Change directory given just path" \
+    extendedglob '' "Use additional pattern matching features" \
+    appendhistory '' "Append new history lines instead of overwriting" \
+    nonomatch '' "Pass unmatched patterns to command instead of error" \
+    nobeep '' "Don't beep on errors" \
+    notify '' "Immediately report changes in background job status"
+
+  if __zni_display_and_edit "Common shell options" \
+  "The following are some of the shell options that are most often used." \
+  "The descriptions are very brief; if you would like more information," \
+  "read the zshoptions manual page (type \"man zshoptions\")."; then
+    install_state[options]="Unsaved changes"
+    save=1
+  fi
+}
+
+
+########################################################################
+# Main function
+########################################################################
+
+# Read and parse any existing lines, in case the function
+# was called again.
+__zni_retrieve_lines &&
+  __zni_parse_lines saved "$reply[@]"
+
+if [[ $state_parameters[HISTORY] = saved ]]; then
+  install_state[history]="Saved"
+fi
+autoload -Uz compinstall
+zstyle :compinstall filename $zd/.zshrc
+if compinstall -d; then
+  install_state[completion]="Saved"
+fi
+
+
+clear
+print -r "This is the Z Shell configuration function for new users, $myname."
+if [[ $1 != -f ]]; then
+  print -r "You are seeing this message because you have no zsh startup files
+(the files .zshenv, .zprofile, .zshrc, .zlogin in the directory
+$zdmsg).  This function can help you with a few settings that should
+make your use of the shell easier."
+fi
+
+print -r "
+You can:
+
+(q)  Quit and do nothing.  The function will be run again next time."
+if [[ ! -f $zd/.zshrc ]]; then
+  print -r "
+(0)  Exit, creating the file $zdmsg/.zshrc containing just a comment.
+     That will prevent this function being run again."
+fi
+print -r "
+(1)  Continue to main menu.
+"
+
+read -k key$longprompt
+print
+
+case $key in
+  ([qQ])
+  return 0
+  ;;
+
+  (0)
+  print -r $msg >$zd/.zshrc
+  return 0
+  ;;
+
+  (1)
+  ;;
+
+  (*)
+  print -r "Aborting."
+  if [[ $1 != -f ]]; then
+    print "The function will be run again next time.  To prevent this, execute:
+  touch $zdmsg/.zshrc"
+  fi
+  return 1
+  ;;
+esac
+
+while true; do
+  clear
+  print -nr "Please pick one of the following options:
+
+(1)  Configure settings for history, i.e. command lines remembered
+     and saved by the shell.\
+${install_state[history]:+  ($install_state[history].)}
+
+(2)  "
+  if [[ $install_state[completion] = Recommended ]]; then
+    print -nr "Configure"
+  else
+    print -nr "Use"
+  fi
+  print -r " the new completion system.\
+${install_state[completion]:+  ($install_state[completion].)}
+
+(3)  Configure how keys behave when editing command lines.
+
+(4)  Pick some of the more common shell options.  These are simple on
+     or off switches controlling the shell's features.  \
+${install_state[options]:+  ($install_state[options].)}
+"
+  print -nr "(0)  Exit, "
+  if (( save )); then
+    print -r "saving the new settings.  They will take effect immediately."
+  elif [[ -f $zd/.zshrc ]]; then
+    print -r "leaving the existing $zdmsg/.zshrc alone."
+  else
+    print -r "creating a blank $zdmsg/.zshrc file."
+  fi
+  print -r "
+(a)  Abort all settings and start from scratch.  Note this will overwrite
+     any settings from $myname already in the startup file.
+     It will not alter any of your other settings, however."
+  if [[ $1 = -f ]]; then
+    print -r "
+(q)  Quit and do nothing else."
+  else
+    print -r "
+(q)  Quit and do nothing else.  The function will be run again next time."
+  fi
+
+  read -k key$longprompt
+  print
+
+  case $key in
+    ([qQ])
+    break
+    ;;
+
+    ([aA])
+    parsed_parameters=()
+    state_parameters=()
+    parsed_options=()
+    state_options=()
+    parsed_keymaps=()
+    state_keymaps=()
+    parsed_bindings=()
+    state_bindings=()
+    unparsed=()
+    ;;
+
+    (0)
+    clear
+    if (( save )); then
+      if [[ -f $zd/.zshrc ]]; then
+	cp $zd/.zshrc $zd/.zshrc.zni &&
+	print -r "Copied old '$zdd/.zshrc' to '$zdd/.zshrc.zni'.
+"
+      fi
+
+      __zni_save
+      __zni_completion_save
+    elif [[ ! -f $zd/.zshrc ]]; then
+      print -r $msg >$zd/.zshrc
+    fi
+    if [[ $1 != -f ]]; then
+      print -r "The function will not be run in future, but you can run
+it yourself as follows:
+  autoload $myname
+  $myname -f
+
+The code added to $zdmsg/.zshrc is marked by the lines
+$startline
+$endline
+You should not edit anything between these lines if you intend to
+run $myname again.  You may, however, edit any other part
+of the file."
+    fi
+    break
+    ;;
+
+    (1)
+    __zni_history_config
+    ;;
+
+    (2)
+    __zni_completion_config
+    ;;
+
+    (3)
+    __zni_bindkey_config
+    ;;
+
+    (4)
+    __zni_options_config
+    ;;
+  esac
+done
 
-unfunction zsh-newuser-install
+} always {
+  # Tidy up: always executed unless the shell is stopped dead
+  # in its tracks.
+  unfunction -m $myname __zni_\*
+  rm -f $tmpfile*
+}