/* e - minimal EMACS-like text editor
**
** Copyright (C) 2022,2025 by Jef Poskanzer <jef@mail.acme.com>.
** All rights reserved.
**
** You may use this software however you like as long as you keep my
** name on it and don't sue me.
**
** For commentary on this license please see http://acme.com/license.html
*/

#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/select.h>
#include <sys/queue.h>
#include <sysexits.h>

#define _XOPEN_SOURCE_EXTENDED 1
#include <curses.h>

#include "version.h"
#include "buffer.h"
#include "port.h"
#include "popen2.h"
#include "leakfinder.h"

#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) > (b) ? (a) : (b))
#define odd(a) (((a) & 1) != 0)

#define ASCII_START 0
#define ASCII_END 127
#define META_BIT 0x80
#define META_START 128
#define META_END 255

#define INITFILE ".erc"
#define MAX_FILL_COLUMN 1000


/* Variable definitions. */

typedef enum { vt_int, vt_str, vt_bool } vartype;
typedef struct {
    char* name;
    vartype vt;
    char* desc;
    union {
	int i;
	char s[100];
	bool b;
	} val;
    } var;
static var vars[] = {
    { "fill-column", vt_int, "What column to use for the fill and justify commands." },
    { "kill-ring-max", vt_int, "Maximum number of kill strings to save." },
    { "matchchars", vt_str, "Pairs of enclosing characters for the matchchar command." },
    { "auto-indent", vt_bool, "Auto-indent mode." },
    };
/* Initialize the vals later - static initialization of unions in C is hard. */

static int var_find( char* name );
static bool var_set_int( char* name, int i );
static bool var_set_str( char* name, char* s );
static bool var_set_bool( char* name, bool b );


/* Transformer definitions. */

typedef bool (transformer)( size_t );	/* robot in disguise */

static bool transform( size_t from, size_t to, transformer tcb );
static bool transform_word( transformer tcb );
static bool transform_region( transformer tcb );
static transformer tcb_upcase, tcb_downcase;


/* Command definitions. */

typedef bool (command)( void );

static command cmd_undefined, cmd_execute_extended_command, cmd_prefix, cmd_self_insert, cmd_quoted_insert, cmd_newline_and_indent, cmd_open_line, cmd_indent_for_tab_command, cmd_forward_char, cmd_backward_char, cmd_forward_word, cmd_backward_word, cmd_mark_word, cmd_mark_whole_buffer, cmd_forward_sentence, cmd_backward_sentence, cmd_next_line, cmd_previous_line, cmd_move_beginning_of_line, cmd_move_end_of_line, cmd_back_to_indentation, cmd_goto_line, cmd_count_lines_region, cmd_forward_paragraph, cmd_backward_paragraph, cmd_mark_line, cmd_mark_paragraph, cmd_set_fill_column, cmd_fill_paragraph, cmd_justify_paragraph, cmd_fill_justify_paragraph_common, cmd_center_line, cmd_center_paragraph, cmd_center_region, cmd_scroll_down_command, cmd_scroll_up_command, cmd_isearch, cmd_reverse_isearch, cmd_isearch_run, cmd_query_replace, cmd_query_replace_run, cmd_beginning_of_buffer, cmd_end_of_buffer, cmd_delete_backward_char, cmd_delete_forward_char, cmd_kill_word, cmd_backward_kill_word, cmd_kill_line, cmd_kill_sentence, cmd_backward_kill_sentence, cmd_kill_region, cmd_kill_ring_save, cmd_yank, cmd_yank_pop, cmd_zap_to_char, cmd_transpose_chars, cmd_transpose_words, cmd_transpose_sentences, cmd_transpose_lines, cmd_set_mark_command, cmd_exchange_point_and_mark, cmd_upcase_word, cmd_downcase_word, cmd_capitalize_word, cmd_upcase_region, cmd_downcase_region, cmd_delete_horizontal_space, cmd_just_one_space, cmd_recenter_top_bottom, cmd_undo, cmd_matchchar, cmd_keyboard_quit, cmd_suspend, cmd_find_file, cmd_find_file_read_only, cmd_find_file_common, cmd_insert_file, cmd_write_file, cmd_save_buffer, cmd_save_buffer_exit, cmd_shell_command, cmd_shell_command_on_region, cmd_universal_argument, cmd_universal_argument_run, cmd_prompt_accumulate, cmd_promptkey, cmd_help_command, cmd_apropos_command, cmd_describe_function, cmd_describe_key, cmd_help_with_tutorial, cmd_help_with_news, cmd_describe_variables;

static pwriteread_cb scor_write_cb, scor_read_cb;

typedef bool (prompter)( char* );
typedef bool (promptkeyer)( char );

static prompter execute_pcb, yesno_pcb, query_replace_pcb1, query_replace_pcb2, find_file_pcb, find_file_yes_pcb, find_file_no_pcb, insert_file_pcb, write_file_yes_pcb, write_file_pcb, save_yes_pcb, save_no_pcb, shell_command_pcb, shell_command_on_region_pcb, apropos_pcb, function_pcb, tutorial_yes_pcb, tutorial_no_pcb, news_yes_pcb, news_no_pcb;
static promptkeyer describe_pkcb, zap_pkcb;

/* This table gives the correspondence between string command names and
** actual command functions. It also includes the short destription strings.
*/
typedef enum { at_repeat, at_self, at_no } argtype;
typedef struct {
    char* name;
    command* cmd;
    argtype at;
    char* desc;
    } command_def;
static command_def command_defs[] = {
    { "undefined", cmd_undefined, at_repeat, "Invalid command." },
    { "execute-extended-command", cmd_execute_extended_command, at_no, "Run a command by name." },
    { "prefix", cmd_prefix, at_no, "Prefix for more keys." },
    { "self-insert", cmd_self_insert, at_repeat, "Insert the typed character." },
    { "quoted-insert", cmd_quoted_insert, at_self, "Insert the next character even if it's special." },
    { "newline-and-indent", cmd_newline_and_indent, at_repeat, "Insert a newline character and possibly indent." },
    { "open-line", cmd_open_line, at_repeat, "Insert a newline after point." },
    { "indent-for-tab-command", cmd_indent_for_tab_command, at_repeat, "Insert a tab newline character and possibly indent." },
    { "forward-char", cmd_forward_char, at_repeat, "Move forward one character." },
    { "backward-char", cmd_backward_char, at_repeat, "Move backward one character." },
    { "forward-word", cmd_forward_word, at_repeat, "Move forward one word." },
    { "backward-word", cmd_backward_word, at_repeat, "Move backward one word." },
    { "mark-word", cmd_mark_word, at_repeat, "Mark the end of the next word." },
    { "mark-whole-buffer", cmd_mark_whole_buffer, at_no, "Make the region be the whole buffer." },
    { "forward-sentence", cmd_forward_sentence, at_repeat, "Move forward one sentence." },
    { "backward-sentence", cmd_backward_sentence, at_repeat, "Move backward one sentence." },
    { "next-line", cmd_next_line, at_repeat, "Move to the next line." },
    { "previous-line", cmd_previous_line, at_repeat, "Move to the previous line." },
    { "move-beginning-of-line", cmd_move_beginning_of_line, at_repeat, "Move to the beginning of the line, or if already there move to the previous line." },
    { "move-end-of-line", cmd_move_end_of_line, at_repeat, "Move to the end of the line, or if already there move to the next line." },
    { "back-to-indentation", cmd_back_to_indentation, at_no, "Move to first non-whitespace on the line." },
    { "goto-line", cmd_goto_line, at_self, "Move to the line number given as argument." },
    { "count-lines-region", cmd_count_lines_region, at_no, "Count the number of lines and characters in the region." },
    { "forward-paragraph", cmd_forward_paragraph, at_repeat, "Move forward one paragraph." },
    { "backward-paragraph", cmd_backward_paragraph, at_repeat, "Move backward one paragraph." },
    { "mark-line", cmd_mark_line, at_self, "Make the region be the current line or lines." },
    { "mark-paragraph", cmd_mark_paragraph, at_self, "Make the region be the current paragraph or paragraphs." },
    { "set-fill-column", cmd_set_fill_column, at_self, "Set the fill-column variable to the current column, or the argument." },
    { "fill-paragraph", cmd_fill_paragraph, at_self, "Fill the current paragraph or paragraphs." },
    { "justify-paragraph", cmd_justify_paragraph, at_self, "Fill with right-justification." },
    { "center-line", cmd_center_line, at_self, "Center the current line or lines." },
    { "center-paragraph", cmd_center_paragraph, at_self, "Center the current paragraph or paragraphs." },
    { "center-region", cmd_center_region, at_no, "Center the region." },
    { "scroll-down-command", cmd_scroll_down_command, at_self, "Scroll down a page." },
    { "scroll-up-command", cmd_scroll_up_command, at_self, "Scroll up a page." },
    { "isearch", cmd_isearch, at_no, "Incremental search." },
    { "reverse-isearch", cmd_reverse_isearch, at_no, "Reverse incremental search." },
    { "isearch-run", cmd_isearch_run, at_no, "Run an I-Search." },
    { "query-replace", cmd_query_replace, at_no, "Interactive search and replace." },
    { "query-replace-run", cmd_query_replace_run, at_no, "Run a query-replace." },
    { "beginning-of-buffer", cmd_beginning_of_buffer, at_no, "Move to the beginning of the buffer." },
    { "end-of-buffer", cmd_end_of_buffer, at_no, "Move to the end of the buffer." },
    { "delete-backward-char", cmd_delete_backward_char, at_repeat, "Delete the previous character." },
    { "delete-forward-char", cmd_delete_forward_char, at_repeat, "Delete the next character." },
    { "kill-word", cmd_kill_word, at_repeat, "Kill to end of word." },
    { "backward-kill-word", cmd_backward_kill_word, at_repeat, "Kill to start of word." },
    { "kill-line", cmd_kill_line, at_self, "Kill to end of line." },
    { "kill-sentence", cmd_kill_sentence, at_repeat, "Kill to end of sentence." },
    { "backward-kill-sentence", cmd_backward_kill_sentence, at_repeat, "Kill back to beginning of sentence." },
    { "kill-region", cmd_kill_region, at_no, "Kill the region." },
    { "kill-ring-save", cmd_kill_ring_save, at_no, "Copy the region to the kill ring." },
    { "yank", cmd_yank, at_repeat, "Yank back last killed text." },
    { "yank-pop", cmd_yank_pop, at_repeat, "Replace yanked text with previous kill." },
    { "zap-to-char", cmd_zap_to_char, at_no, "Kill through the next occurrence of char." },
    { "transpose-chars", cmd_transpose_chars, at_repeat, "Swap the two characters around point." },
    { "transpose-words", cmd_transpose_words, at_repeat, "Swap the two words around point." },
    { "transpose-sentences", cmd_transpose_sentences, at_repeat, "Swap the two sentences around point." },
    { "transpose-lines", cmd_transpose_lines, at_repeat, "Swap the two lines around point." },
    { "set-mark-command", cmd_set_mark_command, at_no, "Set the mark (C-SP also works)." },
    { "exchange-point-and-mark", cmd_exchange_point_and_mark, at_no, "Swap the point and mark." },
    { "upcase-word", cmd_upcase_word, at_repeat, "Raise all letters in the next word to upper case." },
    { "downcase-word", cmd_downcase_word, at_repeat, "Pound all letters in the next word to lower case." },
    { "capitalize-word", cmd_capitalize_word, at_repeat, "Make the next word start with a capital and then be all lower case." },
    { "upcase-region", cmd_upcase_region, at_no, "Raise all letters in the region to upper case." },
    { "downcase-region", cmd_downcase_region, at_no, "Pound all letters in the region to lower case." },
    { "delete-horizontal-space", cmd_delete_horizontal_space, at_no, "Delete spaces and tabs around point." },
    { "just-one-space", cmd_just_one_space, at_no, "Delete spaces and tabs around point, leaving one space." },
    { "recenter-top-bottom", cmd_recenter_top_bottom, at_self, "Refresh the screen, with point alternately at the center/top/bottom." },
    { "undo", cmd_undo, at_repeat, "Undo previous change." },
    { "matchchar", cmd_matchchar, at_no, "Move to the enclosing character matching the one at point or the next one." },
    { "keyboard-quit", cmd_keyboard_quit, at_no, "Cancel any current command." },
    { "suspend", cmd_suspend, at_no, "Suspend the editor and go back to the parent shell." },
    { "find-file", cmd_find_file, at_no, "Edit a file." },
    { "find-file-read-only", cmd_find_file_read_only, at_no, "Open a file in readonly mode." },
    { "insert-file", cmd_insert_file, at_no, "Insert a file at the current position." },
    { "write-file", cmd_write_file, at_no, "Write a new file." },
    { "save-buffer", cmd_save_buffer, at_no, "Save the current file." },
    { "save-buffer-exit", cmd_save_buffer_exit, at_no, "Save the file and exit." },
    { "shell-command", cmd_shell_command, at_no, "Run a command and insert the results." },
    { "shell-command-on-region", cmd_shell_command_on_region, at_no, "Run a command with the region as input and insert the results." },
    { "universal-argument", cmd_universal_argument, at_no, "Accumulate the argument for a command." },
    { "universal-argument-run", cmd_universal_argument_run, at_no, "Run a command argument times." },
    { "prompt-accumulate", cmd_prompt_accumulate, at_no, "Accumulate the answer to a prompt." },
    { "promptkey", cmd_promptkey, at_no, "Handle the answer to a promptkey." },
    { "help-command", cmd_help_command, at_no, "Help commands." },
    { "apropos-command", cmd_apropos_command, at_no, "Show commands matching a string." },
    { "describe-function", cmd_describe_function, at_no, "Describe a function." },
    { "describe-key", cmd_describe_key, at_no, "Describe what a key does." },
    { "help-with-tutorial", cmd_help_with_tutorial, at_no, "Display the e tutorial." },
    { "help-with-news", cmd_help_with_news, at_no, "Display e news." },
    { "describe-variables", cmd_describe_variables, at_no, "Show all variables." },
    };


/* Key bindings. */

/* This table (from an include file generated from a text file) gives the
** default bindings from key sequences to command names.
*/
typedef struct {
    char* keys;
    char* name;
    } key_binding;
static key_binding builtin_key_bindings[] = {
#include "key_bindings.h"
    };

/* And here's a similar table for user-specified key bindings. */
static key_binding* user_key_bindings;
static int user_key_bindings_size = 0;
static int user_key_bindings_len = 0;

/* And these are the data structures that the above tables get transmogrified
** into - a list of key/command pairs, plus a default.
*/
typedef struct {
    int key;
    command* cmd;
    } keytab_entry;
typedef struct {
    command* default_command;
    int size, len;
    keytab_entry* entries;
    } keytab;
static keytab base_keytab;
static keytab help_keytab;
static keytab ctrlq_keytab;
static keytab ctrlu_keytab;
static keytab ctrlx_keytab;
static keytab meta_keytab;
static keytab metabracket_keytab;
static keytab metabracket2_keytab;
static keytab metabracket3_keytab;
static keytab metabracket5_keytab;
static keytab metabracket6_keytab;
static keytab metaO_keytab;
static keytab prompt_keytab;
static keytab promptkey_keytab;
static keytab is_keytab;
static keytab qr_keytab;
static keytab* curr_keytab;
static keytab* next_keytab;

/* This small table is a helper for decoding the named key sequences. */
typedef struct {
    char* name;
    int key;
    keytab* kt;
    int skey;
    } namekey;
static namekey namekeys[] = {
    { "ESC", '', (keytab*) 0, 0 },
    { "SP", ' ', (keytab*) 0, 0 },
    { "TAB", '\t', (keytab*) 0, 0 },
    { "NL", '\n', (keytab*) 0, 0 },
    { "CR", '\r', (keytab*) 0, 0 },
    { "DEL", '', (keytab*) 0, 0 },
    { "UP", KEY_UP, &metabracket_keytab, 'A' },
    { "DOWN", KEY_DOWN, &metabracket_keytab, 'B' },
    { "RIGHT", KEY_RIGHT, &metabracket_keytab, 'C' },
    { "LEFT", KEY_LEFT, &metabracket_keytab, 'D' },
    { "ENTER", KEY_ENTER, (keytab*) 0, 0 },
    { "HOME", KEY_HOME, &metabracket_keytab, 'H' },
    { "BACKSPACE", KEY_BACKSPACE, (keytab*) 0, 0 },
    { "END", KEY_END, &metabracket_keytab, 'F' },
    { "IC", KEY_IC, &metabracket2_keytab, '~' },
    { "DC", KEY_DC, &metabracket3_keytab, '~' },
    { "PPAGE", KEY_PPAGE, &metabracket5_keytab, '~' },
    { "NPAGE", KEY_NPAGE, &metabracket6_keytab, '~' },
    { "SR", KEY_SR, (keytab*) 0, 0 },
    { "SF", KEY_SF, (keytab*) 0, 0 },
    { "FIND", KEY_FIND, (keytab*) 0, 0 },
    { "HELP", KEY_HELP, (keytab*) 0, 0 },
    { "MARK", KEY_MARK, (keytab*) 0, 0 },
    { "REFRESH", KEY_REFRESH, (keytab*) 0, 0 },
    { "SAVE", KEY_SAVE, (keytab*) 0, 0 },
    { "UNDO", KEY_UNDO, (keytab*) 0, 0 },
    { "F0", KEY_F0, (keytab*) 0, 0 },
    { "F1", KEY_F(1), &metaO_keytab, 'P' },
    { "F2", KEY_F(2), &metaO_keytab, 'Q' },
    { "F3", KEY_F(3), &metaO_keytab, 'R' },
    { "F4", KEY_F(4), &metaO_keytab, 'S' },
    { "F5", KEY_F(5), (keytab*) 0, 0 },
    { "F6", KEY_F(6), (keytab*) 0, 0 },
    { "F7", KEY_F(7), (keytab*) 0, 0 },
    { "F8", KEY_F(8), (keytab*) 0, 0 },
    { "F9", KEY_F(9), (keytab*) 0, 0 },
    { "F10", KEY_F(10), (keytab*) 0, 0 },
    { "F11", KEY_F(11), (keytab*) 0, 0 },
    { "F12", KEY_F(12), (keytab*) 0, 0 },
    };

/* A little table defining the keytab transitions for prefix keys. */
typedef struct {
    keytab* curr_kt;
    char key;
    keytab* next_kt;
    } prefix_def;
static prefix_def prefix_defs[] = {
    { &base_keytab, '', &help_keytab },
    { &base_keytab, '', &ctrlx_keytab },
    { &base_keytab, '', &meta_keytab },
    { &is_keytab, '', &help_keytab },
    { &is_keytab, '', &ctrlx_keytab },
    { &is_keytab, '', &meta_keytab },
    { &meta_keytab, '[', &metabracket_keytab },
    { &metabracket_keytab, '2', &metabracket2_keytab },
    { &metabracket_keytab, '3', &metabracket3_keytab },
    { &metabracket_keytab, '5', &metabracket5_keytab },
    { &metabracket_keytab, '6', &metabracket6_keytab },
    { &meta_keytab, 'O', &metaO_keytab },
    };

/* List of command / anti-command pairs for implementing negative arguments. */
typedef struct {
    command* cmd1;
    command* cmd2;
    } anti_command;
static anti_command anti_commands[] = {
    { cmd_forward_char, cmd_backward_char },
    { cmd_forward_word, cmd_backward_word },
    { cmd_forward_sentence, cmd_backward_sentence },
    { cmd_next_line, cmd_previous_line },
    { cmd_delete_forward_char, cmd_delete_backward_char },
    { cmd_kill_word, cmd_backward_kill_word },
    { cmd_kill_sentence, cmd_backward_kill_sentence },
    { cmd_scroll_down_command, cmd_scroll_up_command },
    };

/* List of commands that are unbound but should not be reported as such. */
static command* unbound_ok[] = {
    cmd_undefined, cmd_self_insert, cmd_isearch_run, cmd_query_replace_run,
    cmd_universal_argument_run, cmd_prompt_accumulate, cmd_promptkey,
    };


/* Forwards. */

static void make_keytabs( void );
static void destroy_keytabs( void );
static void keytab_init( keytab* kt, command* default_command );
static void keytab_destroy( keytab* kt );
static void keytab_fill( keytab* kt, int from, int to, command* cmd );
static command* keytab_get( keytab* kt, int key );
static void keytab_set( keytab* kt, int key, command* cmd );
static void commands( void );
static void check( void );
static void check_keytab( keytab* kt, const char* prefix );
static command* name_to_command( const char* name );
static char* command_to_name( command* cmd );
static char* command_to_desc( command* cmd );
static argtype command_to_argtype( command* cmd );
static char* name_to_keys( const char* name );
static char* keys_to_name( const char* keys );
static bool is_unbound_ok( command* cmd );
static char* render_key( int key );
static void decode_keys( const char* keys, keytab** ktP, int* keyP );
static void read_initfile( void );
static bool add_user_key_binding( char* keys, char* name );
static void init_curses( void );
static void fini_curses( void );
static void display( void );
static bool read_file( const char* fn );
static bool insert_file( const char* fn );
static bool write_file( const char* fn );
static bool load_tutorial( void );
static bool load_news( void );
static bool load_builtin_text( char* title, char** strs, int nstrs, bool readonly );
static void show_status( int rows, int cols, int row, size_t last );
static void set_next_message( const char* format, ... ) __attribute__((format(printf,1,2)));
static void set_next_message_valist( const char* format, va_list args );
static void prompt( prompter* cb, const char* format, ... ) __attribute__((format(printf,2,3)));
static void prompt_valist( prompter* cb, const char* format, va_list args );
static void prompt_yesno( prompter* ycb, prompter* ncb, const char* format, ... ) __attribute__((format(printf,3,4)));
static void prompt_key( promptkeyer* cb, bool pm, const char* format, ... ) __attribute__((format(printf,3,4)));
static void promptkey_valist( promptkeyer* cb, bool pm, const char* format, va_list args );
static void addline( int rows, int cols, int row, char* line );
static size_t span_charset( size_t pos, const char* charset );
static size_t rspan_charset( size_t pos, const char* ccharset );
static size_t span_ccharset( size_t pos, const char* charset );
static size_t rspan_ccharset( size_t pos, const char* ccharset );
static char lower_if( char c, bool ic );
static ssize_t search_char( size_t pos, char c, bool ic );
static ssize_t search_str( size_t pos, const char* pat, bool ic );
static ssize_t rsearch_str( size_t pos, const char* pat, bool ic );
static size_t search_eow( size_t pos );
static size_t rsearch_bow( size_t pos );
static size_t search_eos( size_t pos );
static size_t rsearch_bos( size_t pos );
static size_t search_eol( size_t pos );
static size_t rsearch_bol( size_t pos );
static size_t search_eop( size_t pos );
static size_t rsearch_bop( size_t pos );
static int what_column( size_t pos );
static size_t find_column( size_t pos, int col );
static char* keytab_to_prefix( keytab* kt );
static void describe_key( keytab* kt, int key );
static bool transpose( size_t fromind1, size_t fromind2, size_t fromind3, size_t toind3 );
static bool copy_buffer_to_buffer( buffer* frombuf, size_t fromind, buffer* tobuf, size_t toind, size_t len );
static bool overwrite_bytes( buffer* frombuf, size_t fromind, buffer* tobuf, size_t toind, size_t len );
static bool file_exists( const char* fn );
static bool file_writeable( const char* fn );
static time_t file_mtime( const char* fn );
static bool file_lock( const char* fn );
static void file_unlock( void );
static bool check_filename( char* s );
static bool check_shellcommand( char* s );
static bool is_readonly( void );
static bool at_start( void );
static bool at_end( void );
static void printfs_start( void );
static void printfs_end( void );
static char* render_character( char ch );
static bool insert_spaces( size_t n );
static size_t screen_positions( char ch );
static char controlify( char ch );
static char decontrolify( char ch );
static int tab_stop( int col );
static size_t strlcatc( char* dst, char ch, size_t size );
static bool isasciiall( char* s );
static bool islowerall( char* s );
static int hexit( char ch );
static void set_next_is_message( void );

/* Logger. */
#ifdef DEBUG
#include <time.h>
#endif /*DEBUG*/
void elog( const char* format, ... ) __attribute__((format(printf,1,2)));


/* Globals. */

static char* argv0;
static buffer* buf;
static buffer* transbuf1;
static buffer* transbuf2;
static buffer* filterbuf;
static buffer* whitespacebuf;
static str_ring* kill_ring;
static int kill_ring_entries;
static char input_char;
static int input_int;
static bool done;
int display_rows, status_row, message_row;
static char message[1000], next_message[1000];
static bool prompt_mode, next_prompt_mode;
static bool onekey_mode, next_onekey_mode;
static int desired_col, next_desired_col;
static command* prev_cmd;
static command* next_prev_cmd;
static int argument;
static const char* whitespace_charset = " \t\r\n";
static const char* hwhitespace_charset = " \t";
static const char* word_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'-$%";
static const char* sentence_end_charset = ".?!";
static prompter* pcb;
static promptkeyer* pkcb;
static char prompt_answer[100];

/* These globals relate to the file being edited. There are a few different
** possible states:
**   filename[0] == '\0'
**     no file
**   filename[0] != '\0'
**     mtime == -1, lockfd == -1
**       new file
**     mtime != -1, lockfd == -1
**       readonly file
**     mtime != -1, lockfd != -1
**       file
** And not_a_file is for special cases like the tutorial which are not
** loaded from a file.
*/
static char filename[1000];
static time_t mtime;
static int lockfd;
static bool not_a_file;


/* Main loop. */

int main( int argc, char** argv )
    {
    command* cmd;

    /* Figure out the program's name. */
    argv0 = strrchr( argv[0], '/' );
    if ( argv0 != (char*) 0 )
	++argv0;
    else
	argv0 = argv[0];
    elog( "%s starting", argv0 );

    var_set_int( "fill-column", 70 );
    var_set_int( "kill-ring-max", 60 );
    var_set_str( "matchchars", "()[]{}" );
    var_set_bool( "auto-indent", true );

    make_keytabs();

    /* Check args. */
    if ( argc == 1 )
	filename[0] = '\0';
    else if ( argc == 2 && strcmp( argv[1], "-commands" ) == 0 )
	{
	/* Write out a list of all available commands. */
	commands();
	exit( EX_OK );
	}
    else if ( argc == 2 && strcmp( argv[1], "-check" ) == 0 )
	{
	/* Sekrit flag to do an internal consistency check. */
	check();
	exit( EX_OK );
	}
    else if ( argc == 2 )
	my_strlcpy( filename, argv[1], sizeof(filename) );
    else
	{
	(void) fprintf( stderr, "usage: %s [filename]\n", argv0 );
	exit( EX_USAGE );
	}

    curr_keytab = &base_keytab;
    desired_col = -1;

    buf = buffer_create();
    transbuf1 = buffer_create();
    transbuf2 = buffer_create();
    filterbuf = buffer_create();
    whitespacebuf = buffer_create();
    kill_ring = str_ring_create();
    kill_ring_entries = 0;
    if ( buf == (buffer*) 0 || transbuf1 == (buffer*) 0 ||
	 transbuf2 == (buffer*) 0 || filterbuf == (buffer*) 0 ||
	 whitespacebuf == (buffer*) 0 || kill_ring == (str_ring*) 0 )
	{
	(void) fprintf( stderr, "%s: out of memory\n", argv0 );
	exit( EX_OSERR );
	}

    init_curses();
    set_next_message( "%s version %s. Type C-h for help.", E_PROGRAM, E_VERSION );
    next_prompt_mode = false;
    next_onekey_mode = false;
    prev_cmd = (command*) 0;

    mtime = -1;
    lockfd = -1;
    not_a_file = false;
    if ( filename[0] == '\0' )
	{
	elog( "no file" );
	not_a_file = true;
	}
    else
	(void) read_file( filename );

    /* Initial display. */
    (void) my_strlcpy( message, next_message, sizeof(message) );
    prompt_mode = next_prompt_mode;
    onekey_mode = next_onekey_mode;
    display();

    elog( "main loop starting" );
    done = false;
    do
	{
	/* Read a key. */
	input_int = getch();
	input_char = input_int;

	/* Find the command to run. */
	cmd = keytab_get( curr_keytab, input_int );

	/* Set up the default next state, if the command doesn't 
	** change anything.
	*/
	next_keytab = &base_keytab;
	set_next_message( "%s", "" );
	next_prompt_mode = false;
	next_onekey_mode = false;
	next_desired_col = -1;
	next_prev_cmd = cmd;
	argument = 1;

	/* Run the command. */
	elog( "Running %s", command_to_name( cmd ) );
	(void) buffer_undo_enable( buf );
	if ( ! cmd() )
	    {
	    elog( "command '%s' failed", command_to_name( cmd ) );
	    flash();
	    }
	(void) buffer_undo_disable( buf );

	/* Install the next state, possibly changed by the command. */
	curr_keytab = next_keytab;
	(void) my_strlcpy( message, next_message, sizeof(message) );
	prompt_mode = next_prompt_mode;
	onekey_mode = next_onekey_mode;
	desired_col = next_desired_col;
	prev_cmd = next_prev_cmd;

	/* Update display. */
	display();
	}
    while ( ! done );

    if ( lockfd != -1 )
	file_unlock();

    fini_curses();

    buffer_destroy( buf );
    buffer_destroy( transbuf1 );
    buffer_destroy( transbuf2 );
    buffer_destroy( filterbuf );
    buffer_destroy( whitespacebuf );
    str_ring_destroy( kill_ring );
    destroy_keytabs();

    elog( "%s exiting", argv0 );
    exit( EX_OK );
    }


static void make_keytabs( void )
    {
    int key;
    int i;
    keytab* kt;
    command* cmd;

    /* Initialize the base keytab and the various prefix keytabs. */
    keytab_init( &base_keytab, cmd_undefined );
    keytab_init( &help_keytab, cmd_undefined );
    keytab_init( &ctrlq_keytab, cmd_self_insert );
    keytab_init( &ctrlu_keytab, cmd_universal_argument_run );
    keytab_init( &ctrlx_keytab, cmd_undefined );
    keytab_init( &meta_keytab, cmd_undefined );
    keytab_init( &metabracket_keytab, cmd_undefined );
    keytab_init( &metabracket2_keytab, cmd_undefined );
    keytab_init( &metabracket3_keytab, cmd_undefined );
    keytab_init( &metabracket5_keytab, cmd_undefined );
    keytab_init( &metabracket6_keytab, cmd_undefined );
    keytab_init( &metaO_keytab, cmd_undefined );
    keytab_init( &prompt_keytab, cmd_undefined );
    keytab_init( &promptkey_keytab, cmd_promptkey );
    keytab_init( &is_keytab, cmd_undefined );
    keytab_init( &qr_keytab, cmd_undefined );

    /* Some bindings are hard-coded. */
    /* Set all regular ASCII characters (except C-g) to self-insert. */
    keytab_fill( &base_keytab, ASCII_START, ASCII_END, cmd_self_insert );
    /* C-q prefix. C-g gets inserted too. */
    keytab_set( &ctrlq_keytab, '', cmd_self_insert );
    /* C-u prefix. */
    keytab_set( &ctrlu_keytab, '', cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, '0', '9', cmd_universal_argument );
    keytab_fill( &meta_keytab, '0', '9', cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, '0' | META_BIT, '9' | META_BIT, cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, 'a', 'f', cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, 'a' | META_BIT, 'f' | META_BIT, cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, 'A', 'F', cmd_universal_argument );
    keytab_fill( &ctrlu_keytab, 'A' | META_BIT, 'F' | META_BIT, cmd_universal_argument );
    keytab_set( &ctrlu_keytab, '-', cmd_universal_argument );
    keytab_set( &meta_keytab, '-', cmd_universal_argument );
    keytab_set( &ctrlu_keytab, 'x', cmd_universal_argument );
    keytab_set( &ctrlu_keytab, 'X', cmd_universal_argument );
    keytab_set( &ctrlu_keytab, '', cmd_universal_argument );
    keytab_set( &ctrlu_keytab, '', cmd_universal_argument );
    /* Prompt mode. */
    keytab_fill( &prompt_keytab, ASCII_START, ASCII_END, cmd_prompt_accumulate );
    keytab_set( &prompt_keytab, KEY_BACKSPACE, cmd_prompt_accumulate );
    /* Prompt key mode - C-g can be returned too. */
    keytab_set( &promptkey_keytab, '', cmd_promptkey );
    /* Query-replace mode. */
    keytab_set( &qr_keytab, ' ', cmd_query_replace_run );
    keytab_set( &qr_keytab, 'y', cmd_query_replace_run );
    keytab_set( &qr_keytab, 'Y', cmd_query_replace_run );
    keytab_set( &qr_keytab, '', cmd_query_replace_run );	/* DEL */
    keytab_set( &qr_keytab, 'n', cmd_query_replace_run );
    keytab_set( &qr_keytab, 'N', cmd_query_replace_run );
    keytab_set( &qr_keytab, '!', cmd_query_replace_run );
    keytab_set( &qr_keytab, '^', cmd_query_replace_run );
    keytab_set( &qr_keytab, '\n', cmd_query_replace_run );
    keytab_set( &qr_keytab, '\r', cmd_query_replace_run );
    keytab_set( &qr_keytab, KEY_ENTER, cmd_query_replace_run );
    keytab_set( &qr_keytab, 'q', cmd_query_replace_run );
    keytab_set( &qr_keytab, 'Q', cmd_query_replace_run );

    /* Decode built-in key bindings. */
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	{
	decode_keys( builtin_key_bindings[i].keys, &kt, &key );
	cmd = name_to_command( builtin_key_bindings[i].name );
	if ( cmd == (command*) 0 )
	    {
	    (void) fprintf( stderr, "%s: internal consistency error\n", argv0 );
	    exit( EX_SOFTWARE );
	    }
	keytab_set( kt, key, cmd );
	}

    /* Read the init file, which may add some custom key bindings. */
    read_initfile();

    /* Some systems have Meta or Alt keys that set the high bit - neat!
    ** So we make the base keytab entry point to the corresponding
    ** meta keytab entry.
    */
    for ( key = META_START; key <= META_END; ++key )
	keytab_set( &base_keytab, key, keytab_get( &meta_keytab, key & ~META_BIT ) );

    /* The I-Search keytab gets initialized last, because it copies bindings
    ** from the base keytab - except self-insert becomes I-Search.
    */
    for ( i = 0; i < base_keytab.len; ++i )
	{
	key = base_keytab.entries[i].key;
	cmd = base_keytab.entries[i].cmd;
	if ( key != '' )
	    {
	    if ( cmd == cmd_self_insert )
		keytab_set( &is_keytab, key, cmd_isearch_run );
	    else
		keytab_set( &is_keytab, key, cmd );
	    }
	}
    /* And also a few other keys. */
    keytab_set( &is_keytab, '\n', cmd_isearch_run );
    keytab_set( &is_keytab, '\r', cmd_isearch_run );
    keytab_set( &is_keytab, KEY_ENTER, cmd_isearch_run );
    keytab_set( &is_keytab, '', cmd_isearch_run );		/* DEL */
    keytab_set( &is_keytab, '', cmd_isearch_run );
    keytab_set( &is_keytab, KEY_BACKSPACE, cmd_isearch_run );
    keytab_set( &is_keytab, '', cmd_isearch_run );
    keytab_set( &is_keytab, '', cmd_isearch_run );
    }


static void destroy_keytabs( void )
    {
    keytab_destroy( &base_keytab );
    keytab_destroy( &help_keytab );
    keytab_destroy( &ctrlq_keytab );
    keytab_destroy( &ctrlu_keytab );
    keytab_destroy( &ctrlx_keytab );
    keytab_destroy( &meta_keytab );
    keytab_destroy( &metabracket_keytab );
    keytab_destroy( &metabracket2_keytab );
    keytab_destroy( &metabracket3_keytab );
    keytab_destroy( &metabracket5_keytab );
    keytab_destroy( &metabracket6_keytab );
    keytab_destroy( &metaO_keytab );
    keytab_destroy( &prompt_keytab );
    keytab_destroy( &promptkey_keytab );
    keytab_destroy( &is_keytab );
    keytab_destroy( &qr_keytab );
    }


/* Keytab routines. */

static void keytab_init( keytab* kt, command* default_command )
    {
    kt->default_command = default_command;
    kt->size = 10;
    kt->len = 0;
    kt->entries = (keytab_entry*) MALLOC( kt->size * sizeof(keytab_entry) );
    if ( kt->entries == (keytab_entry*) 0 )
	{
	(void) fprintf( stderr, "%s: keytab_init - out of memory\n", argv0 );
	exit( EX_SOFTWARE );
	}
    /* All keytabs get a C-g entry. */
    keytab_set( kt, '', cmd_keyboard_quit );
    }

static void keytab_destroy( keytab* kt )
    {
    free( (void*) kt->entries );
    }

static void keytab_fill( keytab* kt, int from, int to, command* cmd )
    {
    int key;

    for ( key = from; key <= to; ++key )
	if ( key != '' )
	    keytab_set( kt, key, cmd );
    }

static command* keytab_get( keytab* kt, int key )
    {
    int i;

    for ( i = 0; i < kt->len; ++i )
	if ( kt->entries[i].key == key )
	    return kt->entries[i].cmd;
    return kt->default_command;
    }

static void keytab_set( keytab* kt, int key, command* cmd )
    {
    int i;

    /* Is this key already in the table? */
    for ( i = 0; i < kt->len; ++i )
	if ( kt->entries[i].key == key )
	    {
	    /* Yes; override it. */
	    kt->entries[i].cmd = cmd;
	    return;
	    }

    /* Add a new entry. */
    if ( kt->len == kt->size )
	{
	kt->size *= 2;
	kt->entries = (keytab_entry*) REALLOC( (void*) kt->entries, kt->size * sizeof(keytab_entry) );
	if ( kt->entries == (keytab_entry*) 0 )
	    {
	    (void) fprintf( stderr, "%s: keytab_set - out of memory\n", argv0 );
	    exit( EX_SOFTWARE );
	    }
	}
    kt->entries[kt->len].key = key;
    kt->entries[kt->len].cmd = cmd;
    ++kt->len;
    }


static void commands( void )
    {
    char* format = "%-10s %-25s %s\n";
    int i;

    if ( user_key_bindings_len > 0 )
	{
	(void) printf( "user key bindings:\n" );
	(void) printf( format, "keys", "command", "description" );
	for ( i = 0; i < user_key_bindings_len; ++i )
	    (void) printf( format, user_key_bindings[i].keys, user_key_bindings[i].name, command_to_desc( name_to_command( user_key_bindings[i].name ) ) );
	(void) printf( "\n" );
	(void) printf( "built-in key bindings:\n" );
	}

    (void) printf( format, "keys", "command", "description" );
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	(void) printf( format, builtin_key_bindings[i].keys, builtin_key_bindings[i].name, command_to_desc( name_to_command( builtin_key_bindings[i].name ) ) );

    /* Also show unbound commands. */
    for ( i = 0; i < sizeof(command_defs) / sizeof(*command_defs); ++i )
	if ( name_to_keys( command_defs[i].name ) == (char*) 0 && ! is_unbound_ok( command_defs[i].cmd ) )
	    (void) printf( format, "[unbound]", command_defs[i].name, command_defs[i].desc );
    }


static void check( void )
    {
    int i;
    keytab* kt;
    int key;

    /* Look for anything in command_defs that is not in key bindings. */
    for ( i = 0; i < sizeof(command_defs) / sizeof(*command_defs); ++i )
	if ( name_to_keys( command_defs[i].name ) == (char*) 0 && ! is_unbound_ok( command_defs[i].cmd ) )
	    printf( "%s is in command_defs but not in key bindings\n", command_defs[i].name );

    printf( "\n" );

    /* Look for anything in user or built-in key bindings that is not in
    ** command_defs.
    */
    for ( i = 0; i < user_key_bindings_len; ++i )
	if ( name_to_command( user_key_bindings[i].name ) == (command*) 0 )
	    printf( "%s is in user_key_bindings but not in command_defs\n", user_key_bindings[i].name );
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	if ( name_to_command( builtin_key_bindings[i].name ) == (command*) 0 )
	    printf( "%s is in builtin_key_bindings but not in command_defs\n", builtin_key_bindings[i].name );

    printf( "\n" );

    /* Look for anything in user or built-in key bindings that is not in the
    ** keytabs.
    */
    for ( i = 0; i < user_key_bindings_len; ++i )
	{
	decode_keys( user_key_bindings[i].keys, &kt, &key );
	if ( name_to_command( user_key_bindings[i].name ) != keytab_get( kt, key ) )
	    printf( "%s is in user_key_bindings but not in keytabs\n", user_key_bindings[i].keys );
	}
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	{
	decode_keys( builtin_key_bindings[i].keys, &kt, &key );
	if ( name_to_command( builtin_key_bindings[i].name ) != keytab_get( kt, key ) )
	    printf( "%s is in builtin_key_bindings but not in keytabs\n", builtin_key_bindings[i].keys );
	}

    printf( "\n" );

    /* Look for anything in the keytabs that is not in key bindings. */
    check_keytab( &base_keytab, "" );
    check_keytab( &help_keytab, "C-h " );
    check_keytab( &ctrlx_keytab, "C-x " );
    check_keytab( &meta_keytab, "M-" );
    check_keytab( &metabracket_keytab, "M-[ " );
    check_keytab( &metabracket2_keytab, "M-[ 2 " );
    check_keytab( &metabracket3_keytab, "M-[ 3 " );
    check_keytab( &metabracket5_keytab, "M-[ 5 " );
    check_keytab( &metabracket6_keytab, "M-[ 6 " );
    check_keytab( &metaO_keytab, "M-O " );
    }


static void check_keytab( keytab* kt, const char* prefix )
    {
    int i, key;
    command* cmd;
    char keys[20];

    for ( i = 0; i < kt->len; ++i )
	{
	key = kt->entries[i].key;
	/* Don't bother complaining about C-g. */
	if ( key == '' )
	    continue;
	/* Skip checking the copied meta commands in the base keytab. */
	if ( kt == &base_keytab && ( key & META_BIT ) )
	    continue;
	/* Also skip the M-digits. */
	if ( kt == &meta_keytab && ( isdigit( key ) || key == '-' ) )
	    continue;
	cmd = kt->entries[i].cmd;
	if ( cmd == cmd_undefined || cmd == cmd_self_insert )
	    continue;
	(void) snprintf( keys, sizeof(keys), "%s%s", prefix, render_key( key ) );
	if ( keys_to_name( keys ) == (char*) 0 )
	    printf( "%s is in keytabs as %s but not in key bindings\n", keys, command_to_name( cmd ) );
	}
    }


/* Static index for all loops over command_defs. A cheap one-item cache. */
static int cmdidx = -1;


static command* name_to_command( const char* name )
    {
    if ( cmdidx != -1 && strcmp( command_defs[cmdidx].name, name ) == 0 )
	return command_defs[cmdidx].cmd;

    for ( cmdidx = 0; cmdidx < sizeof(command_defs) / sizeof(*command_defs); ++cmdidx )
	if ( strcmp( command_defs[cmdidx].name, name ) == 0 )
	    return command_defs[cmdidx].cmd;

    /* Unknown command. */
    cmdidx = -1;
    return (command*) 0;
    }


static char* command_to_name( command* cmd )
    {
    if ( cmdidx != -1 && command_defs[cmdidx].cmd == cmd )
	return command_defs[cmdidx].name;

    for ( cmdidx = 0; cmdidx < sizeof(command_defs) / sizeof(*command_defs); ++cmdidx )
	if ( command_defs[cmdidx].cmd == cmd )
	    return command_defs[cmdidx].name;

    /* Shouldn't ever fail. */
    cmdidx = -1;
    return (char*) 0;
    }


static char* command_to_desc( command* cmd )
    {
    if ( cmdidx != -1 && command_defs[cmdidx].cmd == cmd )
	return command_defs[cmdidx].desc;

    for ( cmdidx = 0; cmdidx < sizeof(command_defs) / sizeof(*command_defs); ++cmdidx )
	if ( command_defs[cmdidx].cmd == cmd )
	    return command_defs[cmdidx].desc;

    /* Shouldn't ever fail. */
    cmdidx = -1;
    return (char*) 0;
    }


static argtype command_to_argtype( command* cmd )
    {
    if ( cmdidx != -1 && command_defs[cmdidx].cmd == cmd )
	return command_defs[cmdidx].at;

    for ( cmdidx = 0; cmdidx < sizeof(command_defs) / sizeof(*command_defs); ++cmdidx )
	if ( command_defs[cmdidx].cmd == cmd )
	    return command_defs[cmdidx].at;

    /* Shouldn't ever fail. */
    cmdidx = -1;
    return at_no;
    }


static char* name_to_keys( const char* name )
    {
    int i;

    for ( i = 0; i < user_key_bindings_len; ++i )
	if ( strcmp( name, user_key_bindings[i].name ) == 0 )
	    return user_key_bindings[i].keys;
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	if ( strcmp( name, builtin_key_bindings[i].name ) == 0 )
	    return builtin_key_bindings[i].keys;
    /* May fail if key bindings get out of sync. */
    return (char*) 0;
    }


static char* keys_to_name( const char* keys )
    {
    int i;

    for ( i = 0; i < user_key_bindings_len; ++i )
	if ( strcmp( keys, user_key_bindings[i].keys ) == 0 )
	    return user_key_bindings[i].name;
    for ( i = 0; i < sizeof(builtin_key_bindings) / sizeof(*builtin_key_bindings); ++i )
	if ( strcmp( keys, builtin_key_bindings[i].keys ) == 0 )
	    return builtin_key_bindings[i].name;
    /* May fail if key bindings get out of sync. */
    return (char*) 0;
    }


static bool is_unbound_ok( command* cmd )
    {
    int i;

    for ( i = 0; i < sizeof(unbound_ok) / sizeof(*unbound_ok); ++i )
	if ( cmd == unbound_ok[i] )
	    return true;
    return false;
    }


static char* render_key( int key )
    {
    char* metastr;
    int i;
    static char keystr[10];

    if ( key & META_BIT )
	{
	metastr = "M-";
	key &= ~META_BIT;
	}
    else
	metastr = "";

    for ( i = 0; i < sizeof(namekeys) / sizeof(*namekeys); ++i )
	if ( key == namekeys[i].key )
	    {
	    (void) snprintf( keystr, sizeof(keystr), "%s%s", metastr, namekeys[i].name );
	    return keystr;
	    }
    if ( iscntrl( key ) )
	{
	(void) snprintf( keystr, sizeof(keystr), "%sC-%c", metastr, decontrolify( key ) );
	return keystr;
	}
    if ( key > ASCII_END )
	(void) snprintf( keystr, sizeof(keystr), "%s\\%o", metastr, key );
    else
	(void) snprintf( keystr, sizeof(keystr), "%s%c", metastr, key );
    return keystr;
    }


static void decode_keys( const char* keys, keytab** ktP, int* keyP )
    {
    bool control = false;
    int i;

    /* Peel off any prefix, turning it into the corresponding keytab. */
    *ktP = &base_keytab;
    if ( strncmp( keys, "C-x ", 4 ) == 0 )
	{
	*ktP = &ctrlx_keytab;
	keys += 4;
	}
    if ( strncmp( keys, "C-h ", 4 ) == 0 )
	{
	*ktP = &help_keytab;
	keys += 4;
	}
    if ( strncmp( keys, "M-[ 2 ", 6 ) == 0 )
	{
	*ktP = &metabracket2_keytab;
	keys += 6;
	}
    if ( strncmp( keys, "M-[ 3 ", 6 ) == 0 )
	{
	*ktP = &metabracket3_keytab;
	keys += 6;
	}
    if ( strncmp( keys, "M-[ 5 ", 6 ) == 0 )
	{
	*ktP = &metabracket5_keytab;
	keys += 6;
	}
    if ( strncmp( keys, "M-[ 6 ", 6 ) == 0 )
	{
	*ktP = &metabracket6_keytab;
	keys += 6;
	}
    if ( strncmp( keys, "M-[ ", 4 ) == 0 )
	{
	*ktP = &metabracket_keytab;
	keys += 4;
	}
    if ( strncmp( keys, "M-O ", 4 ) == 0 )
	{
	*ktP = &metaO_keytab;
	keys += 4;
	}
    if ( strncmp( keys, "M-", 2 ) == 0 )
	{
	*ktP = &meta_keytab;
	keys += 2;
	}
    if ( strncmp( keys, "C-", 2 ) == 0 )
	{
	control = true;
	keys += 2;
	}

    /* Is the remainder in the namekeys table? */
    for ( i = 0; i < sizeof(namekeys) / sizeof(*namekeys); ++i )
	if ( strcmp( keys, namekeys[i].name ) == 0 )
	    {
	    *keyP = namekeys[i].key;
	    if ( control )
		*keyP = controlify( *keyP );
	    return;
	    }

    /* If there's other one character left, that's an error. */
    if ( strlen( keys ) != 1 )
	*ktP = (keytab*) 0;
    else
	{
	/* It's just the key, verbatim. */
	*keyP = *keys;
	if ( control )
	    *keyP = controlify( *keyP );
	}
    }


/* Initialization file stuff. */

typedef void (initfile_command)( char** );

static void initfile_bind( char** words )
    {
    keytab* kt;
    int key;
    command* cmd;
    char* keys;
    char* name;

    keys = words[1];
    name = words[2];
    elog( "bind '%s' to '%s'", keys, name );
    decode_keys( keys, &kt, &key );
    if ( kt == (keytab*) 0 )
	{
	(void) fprintf( stderr, "%s: init file bind command - '%s' is an invalid key sequence\n", argv0, keys );
	exit( EX_USAGE );
	}
    cmd = name_to_command( name );
    if ( cmd == (command*) 0 )
	{
	(void) fprintf( stderr, "%s: init file bind command - '%s' is an unknown command\n", argv0, name );
	exit( EX_USAGE );
	}
    /* Save the new binding in user_key_bindings. */
    if ( ! add_user_key_binding( keys, name ) )
	{
	(void) fprintf( stderr, "%s: init file bind command - out of memory\n", argv0 );
	exit( EX_SOFTWARE );
	}
    /* Ok! */
    keytab_set( kt, key, cmd );
    }

static void initfile_unbind( char** words )
    {
    elog( "unbind '%s'", words[1] );
    /* Just bind it to undefined. */
    words[2] = command_to_name( cmd_undefined );
    initfile_bind( words );
    }

static void initfile_set( char** words )
    {
    int ind, i;
    bool b;
    char* cp;

    elog( "set '%s' to '%s'", words[1], words[2] );
    ind = var_find( words[1] );
    if ( ind == -1 )
	{
	(void) fprintf( stderr, "%s: init file set command - '%s' is an unknown variable\n", argv0, words[1] );
	exit( EX_USAGE );
	}
    switch ( vars[ind].vt )
	{
	case vt_int:
	i = strtol( words[2], &cp, 10 );
	if ( *words[2] == '\0' || *cp != '\0' )
	    {
	    (void) fprintf( stderr, "%s: init file set command - '%s' is an invalid int value\n", argv0, words[2] );
	    exit( EX_USAGE );
	    }
	vars[ind].val.i = i;
	break;
	case vt_str:
	(void) my_strlcpy( vars[ind].val.s, words[2], sizeof(vars[ind].val.s) );
	break;
	case vt_bool:
	if ( strcasecmp( words[2], "true" ) == 0 )
	    b = true;
	else if ( strcasecmp( words[2], "false" ) == 0 )
	    b = false;
	else
	    {
	    (void) fprintf( stderr, "%s: init file set command - '%s' is an invalid boolen value\n", argv0, words[2] );
	    exit( EX_USAGE );
	    }
	vars[ind].val.b = b;
	break;
	}
    }


typedef struct {
    char* name;
    int nargs;
    initfile_command* cmd;
    } initfile_command_def;
static initfile_command_def initfile_command_defs[] = {
    { "bind", 2, initfile_bind },
    { "unbind", 1, initfile_unbind },
    { "set", 2, initfile_set },
    };

static void read_initfile( void )
    {
    char* home;
    FILE* fp;
    char s[1000];
    char* cp;
#define MAX_WORDS 100
    char* words[MAX_WORDS];
    int nwords;
    int i;

    home = getenv( "HOME" );
    if ( home == (char*) 0 )
	return;
    (void) snprintf( s, sizeof(s), "%s/%s", home, INITFILE );
    fp = fopen( s, "r" );
    if ( fp == (FILE*) 0 )
	return;

    /* Loop reading lines. */
    while ( fgets( s, sizeof(s), fp ) != (char*) 0 )
	{
	/* Trim comments. */
	if ( ( cp = strchr( s, '#' ) ) != (char*) 0 )
	    *cp = '\0';
	/* Trim trailing whitespace. */
	for ( cp = &s[strlen(s) - 1]; cp >= s && isspace( *cp ); --cp )
	    *cp = '\0';
	/* Skip leading whitespace. */
	for ( cp = s; isspace( *cp ); ++cp )
	    ;
	/* Ignore empty lines. */
	if ( *cp == '\0' )
	    continue;
	elog( "init file command: %s", cp );

	/* Parse into words, possibly quoted by either '' or "". */
	nwords = 0;
	while ( *cp != '\0' && nwords < MAX_WORDS )
	    {
	    if ( *cp == '\'' )
		{
		++cp;
		words[nwords++] = cp;
		cp = strchr( cp, '\'' );
		if ( cp == (char*) 0 )
		    break;
		*cp++ = '\0';
		}
	    else if ( *cp == '"' )
		{
		++cp;
		words[nwords++] = cp;
		cp = strchr( cp, '"' );
		if ( cp == (char*) 0 )
		    break;
		*cp++ = '\0';
		}
	    else
		{
		words[nwords++] = cp;
		cp += strcspn( cp, " \t" );
		*cp++ = '\0';
		}
	    /* And skip over any whitespace before the next word. */
	    cp += strspn( cp, " \t" );
	    }
	if ( nwords == 0 )
	    continue;

	/* Dispatch to commands. */
	for ( i = 0; i < sizeof(initfile_command_defs) / sizeof(*initfile_command_defs); ++i )
	    {
	    if ( strcmp( words[0], initfile_command_defs[i].name ) == 0 )
		{
		if ( nwords != initfile_command_defs[i].nargs + 1 )
		    {
		    (void) fprintf( stderr, "%s: the '%s' init file command requires %d arguments'\n", argv0, words[0], initfile_command_defs[i].nargs );
		    exit( EX_USAGE );
		    }
		initfile_command_defs[i].cmd( words );
		goto ok;
		}
	    }
	(void) fprintf( stderr, "%s: unknown init file command '%s'\n", argv0, words[0] );
	exit( EX_USAGE );
	ok: ;
	}

    (void) fclose( fp );
    }


static bool add_user_key_binding( char* keys, char* name )
    {
    int i;
    key_binding* newbindings;
    int newsize;

    /* Is it already in the table? */
    for ( i = 0; i < user_key_bindings_len; ++i )
	if ( strcmp( keys, user_key_bindings[i].keys ) == 0 )
	    {
	    name = strdup( name );
	    if ( name == (char*) 0 )
		return false;
	    FREE( (void*) user_key_bindings[i].name );
	    user_key_bindings[i].name = name;
	    return true;
	    }
    /* No; add a new entry. */
    if ( user_key_bindings_size == 0 )
	{
	newsize = 10;	/* arbitrary initial size */
	newbindings = (key_binding*) MALLOC( newsize * sizeof(key_binding) );
	if ( newbindings == (key_binding*) 0 )
	    return false;
	user_key_bindings = newbindings;
	user_key_bindings_size = newsize;
	}
    else if ( user_key_bindings_len == user_key_bindings_size )
	{
	newsize = ( user_key_bindings_len + 1 ) * 2;
	newbindings = REALLOC( (void*) user_key_bindings, newsize * sizeof(key_binding) );
	if ( newbindings == (key_binding*) 0 )
	    return false;
	user_key_bindings = newbindings;
	user_key_bindings_size = newsize;
	}
    keys = strdup( keys );
    name = strdup( name );
    if ( keys == (char*) 0 || name == (char*) 0 )
	return false;
    user_key_bindings[user_key_bindings_len].keys = keys;
    user_key_bindings[user_key_bindings_len].name = name;
    ++user_key_bindings_len;

    return true;
    }


static void init_curses( void )
    {
    initscr();
    nonl();
    intrflush( stdscr, FALSE );
    raw();
    noecho();
#ifdef notdef
    /* Enabling keypad mode results in inconsistent and incompatible
    ** handling of C-h on different systems. Since C-h is the primary
    ** help character, we really want to avoid this, so we can't use
    ** keypad mode. Instead we have key bindings for the ANSI escape
    ** sequences generated by the arrow keys and other special function
    ** keys. This won't work on non-ANSI terminals, but very few of those
    ** still exist.
    */
    keypad( stdscr, TRUE );
#endif /*notdef*/
    }


static void fini_curses( void )
    {
    noraw();
    endwin();

    /* Write a single newline so that the shell prompt doesn't go on
    ** top of the message line.
    */
    putchar( '\n' );
    }


static void display( void )
    {
    int try;
    int rows, cols, row, col, point_row, point_col;
    int desired_row, i;
    size_t ind, last;
    size_t* rowinds;
    int nrowinds;
    char ch;
    char* s;
    size_t len;

    getmaxyx( stdscr, rows, cols );
    display_rows = rows - 2;
    status_row = rows - 2;
    message_row = rows - 1;

    /* Outer loop. Try laying out the screen; if it fails, reposition
    ** and try again.
    */
    for ( try = 1; ; ++try )
	{
	point_row = point_col = -1;
	erase();
	move( 0, 0 );
	for ( ind = buf->first; ; ++ind )
	    {
	    getyx( stdscr, row, col );
	    /* If we're at point, save the row & col. */
	    if ( ind == buf->point )
		{
		point_row = row;
		point_col = col;
		}
	    /* If we're at the end of the buffer, exit the loop. */
	    if ( ind >= buf->len )
		break;
	    ch = buffer_get_byte( buf, ind );
	    s = render_character( ch );
	    len = strlen( s );
	    /* Show an over-long line indicator? */
	    if ( ( ch != '\n' && col + len >= cols ) ||
		 ( ch == '\t' && tab_stop( col ) >= cols ) )
		{
		move( row, cols - 1 );
		addch( '\\' );
		getyx( stdscr, row, col );
		/* And update point position. */
		if ( ind == buf->point )
		    {
		    point_row = row;
		    point_col = col;
		    }
		}
	    /* Curses doesn't properly handle NLs on the bottom line, so we
	    ** don't use the bottom line. (Except for the status/message area,
	    ** which we know won't have any NLs.)
	    */
	    if ( row >= display_rows )
		break;
	    if ( ch == '\t' || ch == '\n' )
		addch( ch );	/* let curses handle it */
	    else
		{
		if ( len > 1 )
		    attrset( A_BOLD );
		addstr( s );
		if ( len > 1 )
		    attrset( 0 );
		}
	    }
	last = ind;

	/* If point is visible, success! */
	if ( point_row != -1 && point_col != -1 )
	    break;

	/* Point was not visible; we have to re-compute first. There
	** are a number of different ways we can do this.
	*/
	elog( "recomputing first, try %d", try );
	desired_row = display_rows / 2;
	switch ( try )
	    {
	    case 1:
	    /* First attempt. This is a special case for when the user
	    ** moves off the beginning or end of the screen. We try a
	    ** one-line change in the displayed area and see if that
	    ** works.
	    */
	    if ( buf->point < buf->first )
		{
		/* Move first back one line. */
		buf->first = rsearch_bol( buf->first );
		if ( buf->first > 0 )
		    buf->first = rsearch_bol( buf->first - 1 );
		}
	    else
		{
		/* Move first forward one line. */
		buf->first = search_eol( buf->first );
		if ( buf->first < buf->len )
		    ++buf->first;
		}
	    break;

	    case 2:
	    /* Second attempt. Step backwards from point through the right
	    ** number of lines. This won't work right if there are lines
	    ** that wrap, but it's simple and in most cases it will be
	    ** correct or close to it.
	    */
	    ind = buf->point;
	    i = 0;
	    for (;;)
		{
		ind = rsearch_bol( ind );
		if ( ind == 0 || i >= desired_row - 1 )
		    break;
		--ind;
		++i;
		}
	    buf->first = ind;
	    break;

	    case 3:
	    /* Third attempt. The furthest back first could possibly be, and
	    ** still show point on the screen, is rows*cols characters. So we
	    ** start there (or at zero) and render text moving forward, exactly
	    ** as it would be rendered on the screen, keeping track of the
	    ** starts of lines. When we get back to point we pick the saved
	    ** line-start that is closest to desired_row lines before point.
	    */
	    rowinds = (size_t*) MALLOC( rows * cols * sizeof(size_t) );
	    if ( rowinds == (size_t*) 0 )
		break;
	    nrowinds = 0;
	    col = 1;
	    for ( ind = max( (int) buf->point - rows * cols, 0 ); ind < buf->point; ++ind )
		{
		ch = buffer_get_byte( buf, ind );
		if ( ch == '\t' )
		    col = tab_stop( col );
		else if ( ch == '\n' )
		    {
		    rowinds[nrowinds++] = ind + 1;
		    col = 0;
		    }
		else
		    col += screen_positions( ch );
		if ( col >= cols )
		    {
		    rowinds[nrowinds++] = ind;
		    col = 0;
		    }
		}
	    if ( nrowinds < desired_row )
		buf->first = 0;
	    else
		buf->first = rowinds[nrowinds - desired_row];
	    FREE( (void*) rowinds );
	    break;

	    case 4:
	    /* That should have worked, but maybe there was some weirdly
	    ** long line or something? Try setting first to half the possible
	    ** screen characters back from point (or zero).
	    */
	    buf->first = max( (int) buf->point - rows * cols / 2, 0 );
	    break;

	    case 5:
	    /* That didn't work either? Well, it was worth a try. Ok, just
	    ** set first to point, that way we know point will be on screen.
	    */
	    buf->first = buf->point;
	    break;

	    default:
	    /* Should never happen, and there's no way to recover. Exit! */
	    fini_curses();
	    (void) fprintf( stderr, "display() failed! Exiting.\n" );
	    exit( EX_SOFTWARE );
	    }
	}
    buf->last = last;

    /* Render the status line. */
    show_status( rows, cols, status_row, ind );

    /* Render the message line. */
    addline( rows, cols, message_row, message );

    /* Place the cursor. */
    if ( prompt_mode )
	move( message_row, strlen( message ) );
    else
	move( point_row, point_col );

    /* And actually update the screen. */
    refresh();
    }


static bool read_file( const char* fn )
    {
    if ( lockfd != -1 )
	file_unlock();
    if ( fn != filename )
	my_strlcpy( filename, fn, sizeof(filename) );
    buffer_clear( buf );
    if ( ! file_exists( filename ) )
	{
	/* New file. */
	mtime = -1;
	lockfd = -1;
	elog( "new file %s", filename );
	}
    else
	{
	/* Existing file. */
	mtime = file_mtime( filename );
	if ( mtime == -1 )
	    {
	    set_next_message( "Couldn't get mtime for %s", filename );
	    return false;
	    }
	if ( ! insert_file( filename ) )
	    {
	    set_next_message( "Couldn't read %s", filename );
	    return false;
	    }
	if ( ! file_writeable( filename ) )
	    buf->readonly = true;
	else if ( ! file_lock( filename ) )
	    {
	    set_next_message( "%s is locked, session is readonly", filename );
	    buf->readonly = true;
	    }
	buf->modified = false;
	elog( "read in %s, %lu chars", filename, buf->len );
	}
    return true;
    }


static bool insert_file( const char* fn )
    {
    FILE* fp;
    size_t saved_point;
    char ch;

    fp = fopen( fn, "r" );
    if ( fp == (FILE*) 0 )
	return false;

    /* Save the current point, to restore later. */
    saved_point = buf->point;

    while ( ( ch = getc( fp ) ) != EOF )
	{
	if ( ! buffer_insert_byte( buf, ch ) )
	    {
	    (void) fclose( fp );
	    return false;
	    }
	}

    /* Restore. We want point at the start of the inserted file, not the end. */
    buf->point = saved_point;

    (void) fclose( fp );
    return true;
    }


static bool write_file( const char* fn )
    {
    FILE* fp;
    size_t ind;
    char ch;

    if ( lockfd != -1 )
	file_unlock();
    if ( fn != filename )
	my_strlcpy( filename, fn, sizeof(filename) );

    fp = fopen( filename, "w" );
    if ( fp == (FILE*) 0 )
	return false;

    for ( ind = 0; ind < buf->len; ++ind )
	{
	ch = buffer_get_byte( buf, ind );
	if ( putc( ch, fp ) == EOF )
	    {
	    (void) fclose( fp );
	    return false;
	    }
	}

    (void) fclose( fp );
    buf->modified = false;
    mtime = file_mtime( filename );
    if ( ! file_lock( filename ) )
	buf->readonly = true;
    return true;
    }


static bool load_tutorial( void )
    {
    char* teache[] = {
#include "teache.h"
	};
    return load_builtin_text( "e Tutorial", teache, sizeof(teache) / sizeof(*teache), false );
    }


static bool load_news( void )
    {
    char* enews[] = {
#include "enews.h"
	};
    return load_builtin_text( "e News", enews, sizeof(enews) / sizeof(*enews), true );
    }


static bool load_builtin_text( char* title, char** strs, int nstrs, bool readonly )
    {
    int i;

    if ( lockfd != -1 )
	file_unlock();
    my_strlcpy( filename, title, sizeof(filename) );
    buffer_clear( buf );

    for ( i = 0; i < nstrs; ++i )
	buffer_insert_string( buf, strs[i] );

    buf->point = 0;
    buf->modified = false;
    buf->readonly = readonly;
    mtime = -1;
    lockfd = -1;
    not_a_file = true;
    return true;
    }


#define FILENAME_SHOWN 45
#define SPACING 3

static void show_status( int rows, int cols, int row, size_t last )
    {
    char line[1000];
    int i;
    char filename_part[FILENAME_SHOWN+1];
    char position_part[10];
    char name_part[10];
    size_t filename_len, filename_part_len, position_part_len, name_part_len;

    for ( i = 0; i < min( cols, sizeof(line) - 1 ); ++i )
	line[i] = '-';
    line[i] = '\0';

    /* Name part. */
    (void) snprintf( name_part, sizeof(name_part), " %s ", E_PROGRAM );
    name_part_len = strlen( name_part );
    (void) memmove( &line[SPACING], name_part, name_part_len * sizeof(*name_part) );

    /* Position part. */
    if ( buf->first == 0 && last == buf->len )
	(void) my_strlcpy( position_part, " All ", sizeof(position_part) );
    else if ( buf->first == 0 )
	(void) my_strlcpy( position_part, " Top ", sizeof(position_part) );
    else if ( last == buf->len )
	(void) my_strlcpy( position_part, " Bot ", sizeof(position_part) );
    else
	(void) snprintf( position_part, sizeof(position_part), " %ld%% ", buf->first * 100 / buf->len );
    position_part_len = strlen( position_part );
    (void) memmove( &line[cols - position_part_len - SPACING], position_part, position_part_len * sizeof(*position_part) );

    /* Filename part. */
    (void) my_strlcpy( filename_part, " ", sizeof(filename_part) );
    if ( filename[0] == '\0' )
	(void) my_strlcat( filename_part, "[no file]", sizeof(filename_part) );
    else
	{
	filename_len = strlen( filename );
	if ( filename_len <= FILENAME_SHOWN )
	    (void) my_strlcat( filename_part, filename, sizeof(filename_part) );
	else
	    {
	    (void) my_strlcat( filename_part, "[...]", sizeof(filename_part) );
	    (void) my_strlcat( filename_part, &filename[filename_len - FILENAME_SHOWN + strlen( filename_part ) - 1], sizeof(filename_part) );
	    }
	if ( mtime == -1 && ! not_a_file )
	    (void) my_strlcat( filename_part, " [new file]", sizeof(filename_part) );
	}
    (void) my_strlcat( filename_part, " ", sizeof(filename_part) );
    if ( buf->modified )
	(void) my_strlcat( filename_part, "* ", sizeof(filename_part) );
    if ( buf->readonly )
	(void) my_strlcat( filename_part, "(readonly) ", sizeof(filename_part) );
    filename_part_len = strlen( filename_part );
    (void) memmove( &line[( cols - filename_part_len ) / 2], filename_part, filename_part_len * sizeof(*filename_part) );

    standout();
    addline( rows, cols, row, line );
    standend();
    }


static void set_next_message( const char* format, ... )
    {
    va_list args;

    va_start( args, format );
    set_next_message_valist( format, args );
    va_end( args );
    }


static void set_next_message_valist( const char* format, va_list args )
    {
    (void) vsnprintf( next_message, sizeof(next_message), format, args );
    }


static void prompt( prompter* cb, const char* format, ... )
    {
    va_list args;

    va_start( args, format );
    prompt_valist( cb, format, args );
    va_end( args );
    }


static void prompt_valist( prompter* cb, const char* format, va_list args )
    {
    next_keytab = &prompt_keytab;
    set_next_message_valist( format, args );
    next_prompt_mode = true;
    pcb = cb;
    prompt_answer[0] = '\0';
    }


static prompter* y_pcb;
static prompter* n_pcb;

static void prompt_yesno( prompter* ycb, prompter* ncb, const char* format, ... )
    {
    va_list args;

    y_pcb = ycb;
    n_pcb = ncb;
    va_start( args, format );
    prompt_valist( yesno_pcb, format, args );
    va_end( args );
    }

static bool yesno_pcb( char* answer )
    {
    elog( "yesno_pcb '%s'", answer );
    switch ( answer[0] )
	{
	case 'y': case 'Y':
	if ( y_pcb == (prompter*) 0 )
	    return true;
	else
	    return y_pcb( answer );

	case 'n': case 'N':
	if ( n_pcb == (prompter*) 0 )
	    return true;
	else
	    return n_pcb( answer );

	default:
	prompt( yesno_pcb, "Yes or no? " );
	return false;
	}
    }


static void prompt_key( promptkeyer* cb, bool pm, const char* format, ... )
    {
    va_list args;

    va_start( args, format );
    promptkey_valist( cb, pm, format, args );
    va_end( args );
    }


static void promptkey_valist( promptkeyer* cb, bool pm, const char* format, va_list args )
    {
    next_keytab = &promptkey_keytab;
    set_next_message_valist( format, args );
    next_prompt_mode = pm;	/* move cursor to prompt or keep on text */
    pkcb = cb;
    }


static void addline( int rows, int cols, int row, char* line )
    {
    int maxcols;
    char* cp;
    int col;

    /* Special case the last column of the last line. */
    if ( row == rows - 1 )
	maxcols = cols - 1;
    else
	maxcols = cols;

    move( row, 0 );
    for ( cp = line; ; ++cp )
	{
	getyx( stdscr, row, col );
	if ( col >= maxcols )
	    break;
	if ( *cp == '\0' )
	    break;
	if ( isgraph( *cp ) || isspace( *cp ) )
	    if ( *cp != '\n' && *cp != '\r' )
		addch( *cp );
	}
    }


/* Starting at pos and moving forward, how many characters from charset
** are there.
*/
static size_t span_charset( size_t pos, const char* charset )
    {
    size_t ind;

    for ( ind = pos; ind < buf->len; ++ind )
	if ( strchr( charset, buffer_get_byte( buf, ind ) ) == (char*) 0 )
	    break;
    return ind - pos;
    }


/* Starting at pos and moving backwards, how many characters from charset
** are there.
*/
static size_t rspan_charset( size_t pos, const char* ccharset )
    {
    size_t ind;

    if ( pos == 0 )
	return 0;
    for ( ind = pos - 1; ind > 0; --ind )
	if ( strchr( ccharset, buffer_get_byte( buf, ind ) ) == (char*) 0 )
	    return pos - ind - 1;
    /* Reached the beginning of the buffer. */
    return pos - 1;
    }


/* Starting at pos and moving forward, how many characters not in charset
** are there.
*/
static size_t span_ccharset( size_t pos, const char* charset )
    {
    size_t ind;

    for ( ind = pos; ind < buf->len; ++ind )
	if ( strchr( charset, buffer_get_byte( buf, ind ) ) != (char*) 0 )
	    break;
    return ind - pos;
    }


/* Starting at pos and moving backward, how many characters not in charset
** are there.
*/
static size_t rspan_ccharset( size_t pos, const char* ccharset )
    {
    size_t ind;

    if ( pos == 0 )
	return 0;
    for ( ind = pos - 1; ind > 0; --ind )
	if ( strchr( ccharset, buffer_get_byte( buf, ind ) ) != (char*) 0 )
	    return pos - ind - 1;
    /* Reached the beginning of the buffer. */
    return pos - 1;
    }


static char lower_if( char c, bool ic )
    {
    if ( ic && isupper( c ) )
	return tolower( c );
    else
	return c;
    }


static ssize_t search_char( size_t pos, char c, bool ic )
    {
    char c2;

    c = lower_if( c, ic );
    for ( ; pos < buf->len; ++pos )
	{
	c2 = buffer_get_byte( buf, pos );
	if ( c == lower_if( c2, ic ) )
	    return pos;
	}
    return -1;
    }


/* Boyer-Moore-Horspool text search.
** https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm
*/
static ssize_t search_str( size_t pos, const char* pat, bool ic )
    {
    size_t patlen = strlen( pat );
    ssize_t skip[256];
    ssize_t i;

    /* Initialize the skip table. */
    for ( i = 0; i < 256; ++i )
	skip[i] = patlen;
    for ( i = 0; i < patlen - 1; ++i )
	skip[(unsigned char) lower_if( pat[i], ic )] = patlen - 1 - i;

    /* Search. */
    while ( pos + patlen <= buf->len )
	{
	for ( i = patlen - 1; i >= 0; --i )
	    {
	    if ( lower_if( buffer_get_byte( buf, pos + i ), ic ) != lower_if( pat[i], ic ) )
		goto nope;
	    }
	return pos;
	nope:
	pos += skip[(unsigned char) lower_if( buffer_get_byte( buf, pos + patlen - 1 ), ic )];
	}

    /* Not found. */
    return -1;
    }


/* Reverse Boyer-Moore-Horspool! */
static ssize_t rsearch_str( size_t pos, const char* pat, bool ic )
    {
    size_t patlen = strlen( pat );
    ssize_t skip[256];
    ssize_t i, spos;

    /* Initialize the skip table. */
    for ( i = 0; i < 256; ++i )
	skip[i] = patlen;
    for ( i = 1; i < patlen; ++i )
	skip[(unsigned char) lower_if( pat[i], ic )] = i;

    /* Search. */
    if ( pos < patlen )
	return -1;
    spos = pos - patlen;
    while ( spos >= 0 )
	{
	for ( i = 0; i < patlen; ++i )
	    if ( lower_if( buffer_get_byte( buf, spos + i ), ic ) != lower_if( pat[i], ic ) )
		goto nope;
	return spos;
	nope:
	spos -= skip[(unsigned char) lower_if( buffer_get_byte( buf, spos ), ic )];
	}

    /* Not found. */
    return -1;
    }


/* Returns the index of the end of the next word after pos. */
static size_t search_eow( size_t pos )
    {
    pos += span_ccharset( pos, word_charset );
    return pos + span_charset( pos, word_charset );
    }


/* Returns the index of the previous word before pos. */
static size_t rsearch_bow( size_t pos )
    {
    pos -= rspan_ccharset( pos, word_charset );
    return pos - rspan_charset( pos, word_charset );
    }


/* Returns the index of the end of the sentence after pos. */
static size_t search_eos( size_t pos )
    {
    pos += span_ccharset( pos, sentence_end_charset );
    return pos + span_charset( pos, sentence_end_charset );
    }


/* Returns the index of the start of the sentence before pos. */
static size_t rsearch_bos( size_t pos )
    {
    pos -= rspan_charset( pos, whitespace_charset );
    pos -= rspan_charset( pos, sentence_end_charset );
    pos -= rspan_ccharset( pos, sentence_end_charset );
    return pos + span_ccharset( pos, word_charset );
    }


/* Returns the index of the end of the line contining pos. */
static size_t search_eol( size_t pos )
    {
    return pos + span_ccharset( pos, "\n" );
    }


/* Returns the index of the beginning of the line contining pos. */
static size_t rsearch_bol( size_t pos )
    {
    return pos - rspan_ccharset( pos, "\n" );
    }


/* Returns the index of the end of the paragraph contining pos. */
static size_t search_eop( size_t pos )
    {
    ssize_t r;

    r = search_str( pos, "\n\n", false );
    if ( r == -1 )
	r = buf->len;
    else
	++r;
    return r;
    }


/* Returns the index of the beginning of the paragraph contining pos. */
static size_t rsearch_bop( size_t pos )
    {
    ssize_t r;

    r = rsearch_str( pos, "\n\n", false );
    if ( r == -1 )
	r = 0;
    else
	r += 2;
    return r;
    }


/* Returns the column number of a given index. Columns start at zero. */
static int what_column( size_t pos )
    {
    size_t ind;
    int col;
    char ch;

    /* First go back to the beginning of this line. */
    ind = rsearch_bol( pos );

    /* Now march forward, keeping track of column, until we get back to pos. */
    for ( col = 0; ind < pos; ++ind )
	{
	ch = buffer_get_byte( buf, ind );
	if ( ch == '\t' )
	    col = tab_stop( col );
	else if ( ch == '\n' )
	    /* Shouldn't happen, but we might as well check. */
	    break;
	else
	    col += screen_positions( ch );
	}
    return col;
    }


/* Call with a index which should be the beginning of a line,
** and the column number you are aiming for. Returns the index
** with the closest available column number. Columns start at zero
*/
static size_t find_column( size_t pos, int col )
    {
    int acol;
    char ch;

    /* March forward, keeping track of column, until we pass the goal. */
    for ( acol = 0; acol < col && pos < buf->len; ++pos )
	{
	ch = buffer_get_byte( buf, pos );
	if ( ch == '\t' )
	    acol = tab_stop( acol );
	else if ( ch == '\n' )
	    break;
	else
	    acol += screen_positions( ch );
	}
    return pos;
    }


static char* keytab_to_prefix( keytab* kt )
    {
    if ( kt == &base_keytab )
	return "";
    if ( kt == &help_keytab )
	return "C-h ";
    if ( kt == &ctrlq_keytab )
	return "C-q ";
    if ( kt == &ctrlu_keytab )
	return "C-u ";
    if ( kt == &ctrlx_keytab )
	return "C-x ";
    if ( kt == &meta_keytab )
	return "M-";
    if ( kt == &metabracket_keytab )
	return "M-[ ";
    if ( kt == &metabracket2_keytab )
	return "M-[ 2 ";
    if ( kt == &metabracket3_keytab )
	return "M-[ 3 ";
    if ( kt == &metabracket5_keytab )
	return "M-[ 5 ";
    if ( kt == &metabracket6_keytab )
	return "M-[ 6 ";
    if ( kt == &metaO_keytab )
	return "M-O ";
    return "???";
    }


static void describe_key( keytab* kt, int key )
    {
    command* cmd;
    char* name;
    char* desc;
    char* prefix;
    char* keystr;
    int i;

    cmd = keytab_get( kt, key );
    name = command_to_name( cmd );
    desc = command_to_desc( cmd );

    if ( kt == &base_keytab )
	prefix = "";
    else
	prefix = keytab_to_prefix( kt );
    keystr = render_key( key );

    /* Special check for named key sequences, which can override the
    ** regular prefix/keystr.
    */
    for ( i = 0; i < sizeof(namekeys) / sizeof(*namekeys); ++i )
	if ( namekeys[i].kt != (keytab*) 0 && kt == namekeys[i].kt && key == namekeys[i].skey )
	    {
	    prefix = "";
	    keystr = namekeys[i].name;
	    break;
	    }

    set_next_message( "%s%s: %s - %s", prefix, keystr, name, desc );
    }


/* Transposes two spans of the buffer, also moving the span in between them. */
static bool transpose( size_t fromind1, size_t fromind2, size_t fromind3, size_t toind3 )
    {
    size_t len1, len2, len3;

    elog( "transpose %lu %lu %lu %lu", fromind1, fromind2, fromind3, toind3 );
    if ( fromind2 < fromind1 || fromind3 < fromind2 || toind3 < fromind3 )
	return false;
    len1 = fromind2 - fromind1;
    len2 = fromind3 - fromind2;
    len3 = toind3 - fromind3;
    buffer_clear( transbuf1 );
    buffer_clear( transbuf2 );
    if ( ! copy_buffer_to_buffer( buf, fromind1, transbuf1, 0, len1 ) )
	return false;
    if ( ! copy_buffer_to_buffer( buf, fromind2, transbuf2, 0, len2 ) )
	return false;
    if ( ! overwrite_bytes( buf, fromind3, buf, fromind1, len3 ) )
	return false;
    if ( ! overwrite_bytes( transbuf2, 0, buf, fromind1 + len3, len2 ) )
	return false;
    if ( ! overwrite_bytes( transbuf1, 0, buf, fromind1 + len3 + len2, len1 ) )
	return false;

    /* Move point so it stays with the part it was in. Except not if
    ** it was just two bytes being transposed.
    */
    if ( ! ( len1 == 1 && len2 == 0 && len3 == 1 ) )
	{
	if ( buf->point >= fromind1 && buf->point < fromind2 )
	    buf->point += len3 + len2;
	else if ( buf->point >= fromind2 && buf->point < fromind3 )
	    buf->point += len3 - len1;
	else if ( buf->point >= fromind3 && buf->point < toind3 )
	    buf->point -= len1 + len2;
	}

    return true;
    }


static bool copy_buffer_to_buffer( buffer* frombuf, size_t fromind, buffer* tobuf, size_t toind, size_t len )
    {
    size_t ind;

    tobuf->point = toind;
    for ( ind = 0; ind < len; ++ind )
	if ( ! buffer_insert_byte( tobuf, buffer_get_byte( frombuf, fromind + ind ) ) )
	    return false;
    return true;
    }


static bool append_bufferbytes_to_str( buffer* frombuf, size_t fromind, str* tos, size_t len )
    {
    char* part1_bytes;
    char* part2_bytes;
    size_t part1_len, part2_len;

    /* This runs reasonably fast character by character, but since the
    ** prepend version has to use the back-door routine (see below), this
    ** one might as well do the same.
    */
    if ( ! buffer_get_parts( buf, fromind, len, &part1_bytes, &part1_len, &part2_bytes, &part2_len ) )
	return false;
    elog( "append_bufferbytes_to_str lens %ld %ld", part1_len, part2_len );
    if ( ! str_append( tos, part1_bytes, part1_len ) )
	return false;
    if ( ! str_append( tos, part2_bytes, part2_len ) )
	return false;
    return true;
    }


static bool prepend_bufferbytes_to_str( buffer* frombuf, size_t fromind, str* tos, size_t len )
    {
    char* part1_bytes;
    char* part2_bytes;
    size_t part1_len, part2_len;

    /* Doing this character by character would be O(N^2), so instead we
    ** use a special secret back-door routine to get direct access to
    ** the buffer's bytes, in either one or two parts depending on if
    ** the span we request crosses the gap.
    */
    if ( ! buffer_get_parts( buf, fromind, len, &part1_bytes, &part1_len, &part2_bytes, &part2_len ) )
	return false;
    elog( "prepend_bufferbytes_to_str lens %ld %ld", part1_len, part2_len );
    if ( ! str_prepend( tos, part2_bytes, part2_len ) )
	return false;
    if ( ! str_prepend( tos, part1_bytes, part1_len ) )
	return false;
    return true;
    }


static bool copy_str_to_buffer( str* froms, buffer* tobuf, size_t toind )
    {
    size_t ind;

    tobuf->point = toind;
    for ( ind = 0; ind < froms->len; ++ind )
	if ( ! buffer_insert_byte( tobuf, froms->bytes[ind] ) )
	    return false;
    return true;
    }


static bool overwrite_bytes( buffer* frombuf, size_t fromind, buffer* tobuf, size_t toind, size_t len )
    {
    size_t ind;

    elog( "overwrite_bytes fromind %lu (fromlen %lu), toind %lu (tolen %lu), len %lu", fromind, frombuf->len, toind, tobuf->len, len );
    if ( fromind + len > frombuf->len )
	return false;
    for ( ind = 0; ind < len; ++ind )
	if ( ! buffer_set_byte( tobuf, toind + ind, buffer_get_byte( frombuf, fromind + ind ) ) )
	    return false;
    return true;
    }


static bool file_exists( const char* fn )
    {
    struct stat sb;

    return stat( fn, &sb ) == 0;
    }


static bool file_writeable( const char* fn )
    {
    return access( fn, W_OK ) == 0;
    }


static time_t file_mtime( const char* fn )
    {
    struct stat sb;

    if ( stat( fn, &sb ) < 0 )
	return -1;
    return sb.st_mtime;
    }


static bool file_lock( const char* fn )
    {
    lockfd = open( fn, O_RDONLY );
    if ( lockfd == -1 )
	return false;
    if ( flock( lockfd, LOCK_EX|LOCK_NB ) != 0 )
	return false;
    return true;
    }


static void file_unlock( void )
    {
    (void) close( lockfd );
    lockfd = -1;
    }


static bool check_filename( char* s )
    {
    if ( isasciiall( s ) )
	return true;
    set_next_message( "Sorry, filenames must be plain ASCII." );
    return false;
    }


static bool check_shellcommand( char* s )
    {
    if ( isasciiall( s ) )
	return true;
    set_next_message( "Sorry, shell commands must be plain ASCII." );
    return false;
    }


static bool is_readonly( void )
    {
    set_next_message( "Buffer is readonly." );
    return false;
    }


static bool at_start( void )
    {
    set_next_message( "At start of buffer." );
    return false;
    }


static bool at_end( void )
    {
    set_next_message( "At end of buffer." );
    return false;
    }


static void printfs_start( void )
    {
    int rows, cols;

    getmaxyx( stdscr, rows, cols );
    move( rows, 0 );
    refresh();
    (void) printf( "\r\n" );
    }


static void printfs_end( void )
    {
    (void) printf( "[hit any key to continue]" );
    (void) fflush( stdout );
    (void) getch();
    redrawwin( stdscr );
    refresh();
    }


static char* render_character( char ch )
    {
    static char s[20];

    if ( iscntrl( ch ) )
	(void) snprintf( s, sizeof(s), "^%c", decontrolify( ch ) );
    else if ( isprint( ch ) )
	(void) snprintf( s, sizeof(s), "%c", ch );
    else
	(void) snprintf( s, sizeof(s), "<x%02x>", (unsigned char) ch );
    return s;
    }


static bool insert_spaces( size_t n )
    {
    int i;

    for ( i = 0; i < n; ++i )
	if ( ! buffer_insert_byte( buf, ' ' ) )
	    return false;
    return true;
    }


/* How many screen positions the character takes up. */
static size_t screen_positions( char ch )
    {
    /* Uses more CPU cycles than necessary but nice and simple. */
    return strlen( render_character( ch ) );
    }


/* Given a character from a ^x or C-x rendering, return the actual control
** character.
*/
static char controlify( char ch )
    {
    if ( ch >= 'a' && ch <= 'z' )
	return ch - '`';
    if ( ch == '?' )
	return '';
    return ch - '@';
    }


/* Given a control character, return the visible character to use in
** a ^x or C-x rendering.
*/
static char decontrolify( char ch )
    {
    if ( ch >= '' && ch <= '' )
	return ch + '`';
    if ( ch == '' )
	return '?';
    return ch + '@';
    }


/* Returns the next tab stop after col. Cols start at zero. */
static int tab_stop( int col )
    {
    return ( col / 8 + 1 ) * 8;
    }


static size_t strlcatc( char* dst, char ch, size_t size )
    {
    char src[2];

    src[0] = ch;
    src[1] = '\0';
    return my_strlcat( dst, src, size );
    }


static bool isasciiall( char* s )
    {
    while ( *s != '\0' )
	if ( ! isascii( *s++ ) )
	    return false;
    return true;
    }


static bool islowerall( char* s )
    {
    while ( *s != '\0' )
	if ( isupper( *s++ ) )
	    return false;
    return true;
    }


static int hexit( char ch )
    {
    if ( isdigit( ch ) )
	return ch - '0';
    if ( ch >= 'a' && ch <= 'f' )
	return ch + 10 - 'a';
    if ( ch >= 'A' && ch <= 'F' )
	return ch + 10 - 'A';
    return -1;	/* shouldn't happen if we're called properly */
    }


/* Loggger. */

void elog( const char* format, ... )
    {
#ifdef DEBUG
    FILE* fp;
    struct timeval tv;
    char datebuf[100];
    va_list args;

    fp = fopen( "ELOG", "a" );
    (void) gettimeofday( &tv, (struct timezone*) 0 );
    (void) strftime( datebuf, sizeof(datebuf), "%d%b%Y %H:%M:%S", localtime( &tv.tv_sec ) );
    (void) fprintf( fp, "%s.%06ld: ", datebuf, tv.tv_usec );
    va_start( args, format );
    (void) vfprintf( fp, format, args );
    va_end( args );
    (void) fprintf( fp, "\n" );
    (void) fclose( fp );
#endif /*DEBUG*/
    }


/* Variables. */

static int var_find( char* name )
    {
    int ind;

    for ( ind = 0; ind < sizeof(vars)/sizeof(*vars); ++ind )
	if ( strcasecmp( name, vars[ind].name ) == 0 )
	    return ind;
    return -1;
    }


static bool var_set_int( char* name, int i )
    {
    int ind;

    ind = var_find( name );
    if ( ind == -1 || vars[ind].vt != vt_int )
	return false;
    vars[ind].val.i = i;
    return true;
    }


static bool var_set_str( char* name, char* s )
    {
    int ind;

    ind = var_find( name );
    if ( ind == -1 || vars[ind].vt != vt_str )
	return false;
    (void) my_strlcpy( vars[ind].val.s, s, sizeof(vars[ind].val.s) );
    return true;
    }


static bool var_set_bool( char* name, bool b )
    {
    int ind;

    ind = var_find( name );
    if ( ind == -1 || vars[ind].vt != vt_bool )
	return false;
    vars[ind].val.b = b;
    return true;
    }


/* Transformers. */


/* Calls the callback with the index of each character between from and to. */
static bool transform( size_t from, size_t to, bool (tcb)( size_t ) )
    {
    size_t ind;

    if ( buf->readonly )
	return is_readonly();
    if ( to < from )
	/* Tail recurse with args in the right order. */
	return transform( to, from, tcb );

    for ( ind = from; ind < to; ++ind )
	if ( ! tcb( ind ) )
	    return false;
    return true;
    }


/* Calls the callback with the index of each character in the current
** or next word.
*/
static bool transform_word( bool (tcb)( size_t ) )
    {
    size_t from, to;

    to = search_eow( buf->point );
    from = rsearch_bow( to );
    buf->point = to;
    return transform( from, to, tcb );
    }


/* Calls the callback with the index of each character in the region. */
static bool transform_region( bool (tcb)( size_t ) )
    {
    return transform( buf->point, buf->mark, tcb );
    }


static bool tcb_upcase( size_t ind )
    {
    char ch;

    ch = buffer_get_byte( buf, ind );
    if ( islower( ch ) )
	if ( ! buffer_set_byte( buf, ind, toupper( ch ) ) )
	    return false;
    return true;
    }


static bool tcb_downcase( size_t ind )
    {
    char ch;

    ch = buffer_get_byte( buf, ind );
    if ( isupper( ch ) )
	if ( ! buffer_set_byte( buf, ind, tolower( ch ) ) )
	    return false;
    return true;
    }


/* Commands. */

static bool cmd_undefined( void )
    {
    set_next_message( "Undefined." );
    return false;
    }


static bool cmd_execute_extended_command( void )
    {
    prompt( execute_pcb, "Run command: " );
    return true;
    }

static bool execute_pcb( char* answer )
    {
    command* cmd;

    cmd = name_to_command( answer );
    if ( cmd == (command*) 0 )
	{
	set_next_message( "Command not found." );
	return false;
	}
    elog( "Running %s", command_to_name( cmd ) );
    return cmd();
    }


static bool cmd_prefix( void )
    {
    int i;

    for ( i = 0; i < sizeof(prefix_defs) / sizeof(*prefix_defs); ++i )
	if ( curr_keytab == prefix_defs[i].curr_kt &&
	     input_char == prefix_defs[i].key )
	    {
	    next_keytab = prefix_defs[i].next_kt;
	    goto ok;
	    }
    set_next_message( "Unknown prefix." );
    return false;

    ok:
    set_next_message( "%s", keytab_to_prefix( next_keytab ) );
    next_prompt_mode = true;
    next_desired_col = desired_col;
    next_prev_cmd = prev_cmd;
    return true;
    }


static bool cmd_self_insert( void )
    {
    if ( buf->readonly )
	return is_readonly();
    if ( prev_cmd == next_prev_cmd )
	(void) buffer_undo_aggregate( buf );
    return buffer_insert_byte( buf, input_char );
    }


static bool cmd_quoted_insert( void )
    {
    if ( buf->readonly )
	return is_readonly();
    if ( argument > 1 )
	return buffer_insert_byte( buf, (char) argument );
    next_keytab = &ctrlq_keytab;
    set_next_message( "C-q " );
    return true;
    }


static bool cmd_newline_and_indent( void )
    {
    size_t bol, wslen;

    if ( buf->readonly )
	return is_readonly();
    /* If we're not auto-indenting, just insert a newline. */
    if ( ! vars[var_find( "auto-indent" )].val.b )
	return buffer_insert_byte( buf, '\n' );
    /* Find the beginning of the line. */
    bol = rsearch_bol( buf->point );
    /* Measure and copy the current line's whitespace. */
    wslen = min( span_charset( bol, hwhitespace_charset ), buf->point - bol );
    buffer_clear( whitespacebuf );
    if ( ! copy_buffer_to_buffer( buf, bol, whitespacebuf, 0, wslen ) )
	return false;
    /* Insert the newline. */
    if ( ! buffer_insert_byte( buf, '\n' ) )
	return false;
    /* Insert the copied whitespace. */
    if ( ! copy_buffer_to_buffer( whitespacebuf, 0, buf, buf->point, wslen ) )
	return false;
    return true;
    }


static bool cmd_open_line( void )
    {
    if ( buf->readonly )
	return is_readonly();
    return buffer_insert_byte( buf, '\n' ) && cmd_backward_char();
    }


static bool cmd_indent_for_tab_command( void )
    {
    size_t pbol, wslen;

    if ( buf->readonly )
	return is_readonly();
    /* There are a few cases where we just insert a tab:
    ** - not in auto-indent mode
    ** - at the beginning of the buffer
    ** - not at the beginning of a line
    */
    if ( ( ! vars[var_find( "auto-indent" )].val.b ) ||
	 buf->point == 0 ||
	 buffer_get_byte( buf, buf->point - 1 ) != '\n' )
	return buffer_insert_byte( buf, '\t' );
    /* Otherwise, we measure the indentation of the previous line and
    ** insert a copy.
    */
    pbol = rsearch_bol( buf->point - 1 );
    wslen = span_charset( pbol, hwhitespace_charset );
    /* If the previous line has no whitespace, just insert a tab. */
    if ( wslen == 0 )
	return buffer_insert_byte( buf, '\t' );
    buffer_clear( whitespacebuf );
    if ( ! copy_buffer_to_buffer( buf, pbol, whitespacebuf, 0, wslen ) )
	return false;
    if ( ! copy_buffer_to_buffer( whitespacebuf, 0, buf, buf->point, wslen ) )
	return false;
    return true;
    }


static bool cmd_forward_char( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    ++buf->point;
    return true;
    }


static bool cmd_backward_char( void )
    {
    if ( buf->point == 0 )
	return at_start();
    --buf->point;
    return true;
    }


static bool cmd_forward_word( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->point = search_eow( buf->point );
    return true;
    }


static bool cmd_backward_word( void )
    {
    if ( buf->point == 0 )
	return at_start();
    buf->point = rsearch_bow( buf->point );
    return true;
    }


static bool cmd_mark_word( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->mark = search_eow( buf->point );
    set_next_message( "Mark set." );
    return true;
    }


static bool cmd_mark_whole_buffer( void )
    {
    buf->point = 0;
    buf->mark = buf->len;
    set_next_message( "Mark set." );
    return true;
    }


static bool cmd_forward_sentence( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->point = search_eos( buf->point );
    return true;
    }


static bool cmd_backward_sentence( void )
    {
    if ( buf->point == 0 )
	return at_start();
    buf->point = rsearch_bos( buf->point );
    return true;
    }


static bool cmd_next_line( void )
    {
    int dc;
    size_t ind;

    if ( desired_col != -1 )
	dc = desired_col;
    else
	dc = what_column( buf->point );
    next_desired_col = dc;
    ind = search_eol( buf->point );
    if ( ind >= buf->len )
	{
	set_next_message( "No next line." );
	return false;
	}
    buf->point = find_column( ind + 1, dc );
    return true;
    }


static bool cmd_previous_line( void )
    {
    int dc;
    size_t ind;

    if ( desired_col != -1 )
	dc = desired_col;
    else
	dc = what_column( buf->point );
    next_desired_col = dc;
    ind = rsearch_bol( buf->point );
    if ( ind == 0 )
	{
	set_next_message( "No previous line." );
	return false;
	}
    ind = rsearch_bol( ind - 1 );
    buf->point = find_column( ind, dc );
    return true;
    }


static bool cmd_move_beginning_of_line( void )
    {
    buf->point = rsearch_bol( buf->point );
    return true;
    }


static bool cmd_move_end_of_line( void )
    {
    buf->point = search_eol( buf->point );
    return true;
    }


static bool cmd_back_to_indentation( void )
    {
    buf->point = rsearch_bol( buf->point );
    buf->point += span_charset( buf->point, hwhitespace_charset );
    return true;
    }


static bool cmd_goto_line( void )
    {
    size_t pos;
    int i;

    if ( argument < 1 )
	{
	set_next_message( "No such line." );
	return false;
	}
    pos = 0;
    for ( i = 1; i < argument; ++i )
	{
	pos = search_eol( pos );
	if ( pos >= buf->len )
	    {
	    set_next_message( "No such line." );
	    return false;
	    }
	++pos;
	}
    buf->point = pos;
    return true;
    }


static bool cmd_count_lines_region( void )
    {
    int lines, chars;
    size_t from, to, ind;
    char c;

    lines = chars = 0;
    from = min( buf->point, buf->mark );
    to = max( buf->point, buf->mark );
    for ( ind = from; ind < to; ++ind )
	{
	c = buffer_get_byte( buf, ind );
	if ( c == '\n' )
	    ++lines;
	++chars;
	}
    set_next_message( "Region has %d lines, %d characters.", lines, chars );
    return true;
    }


static bool cmd_forward_paragraph( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->point = search_eop( buf->point );
    return true;
    }


static bool cmd_backward_paragraph( void )
    {
    if ( buf->point == 0 )
	return at_start();
    buf->point = rsearch_bop( buf->point );
    if ( buf->point > 0 )
	--buf->point;
    return true;
    }


static bool cmd_mark_line( void )
    {
    int i;
    size_t pos;

    if ( argument > 1 )
	{
	/* Multi-line kill forward. */
	pos = buf->point;
	for ( i = 0; i < argument; ++i )
	    {
	    pos = search_eol( pos );
	    if ( pos >= buf->len )
		return at_end();
	    ++pos;
	    }
	buf->mark = pos;
	}
    else if ( argument < 0 )
	{
	/* Multi-line kill backward. */
	pos = buf->point;
	pos = rsearch_bol( pos );
	for ( i = 0; i < -argument; ++i )
	    {
	    if ( pos == 0 )
		return at_start();
	    --pos;
	    pos = rsearch_bol( pos );
	    }
	buf->mark = pos;
	}
    else
	{
	/* Regular old single-line kill. */
	if ( buf->point >= buf->len )
	    return at_end();
	if ( buffer_get_byte( buf, buf->point ) == '\n' )
	    buf->mark = buf->point + 1;
	else
	    buf->mark = search_eol( buf->point );
	}
    return true;
    }


static bool cmd_mark_paragraph( void )
    {
    int i;

    if ( argument > 0 )
	{
	/* Start by going forward and then back so that if point is between
	** paragraphs, we mark the next one, not the previous one.
	*/
	buf->point = search_eop( buf->point );
	buf->point = rsearch_bop( buf->point );
	buf->mark = buf->point;
	for ( i = 0; i < argument; ++i )
	    {
	    if ( buf->mark >= buf->len )
		return false;
	    buf->mark = search_eop( buf->mark );
	    }
	}
    else if ( argument < 0 )
	{
	buf->mark = search_eop( buf->point );
	buf->point = buf->mark;
	for ( i = 0; i > argument; --i )
	    {
	    if ( buf->point == 0 )
		return false;
	    buf->point = rsearch_bop( buf->point - 1 );
	    }
	}
    return true;
    }


static bool cmd_set_fill_column( void )
    {
    int fc;

    if ( argument == 1 )
	fc = what_column( buf->point );
    else if ( argument > 1 )
	fc = argument;
    else
	return false;
    if ( ! var_set_int( "fill-column", fc ) )
	return false;
    set_next_message( "fill-column set to %d.", fc );
    return true;
    }


static bool justify_paragraph;

static bool cmd_fill_paragraph( void )
    {
    justify_paragraph = false;
    return cmd_fill_justify_paragraph_common();
    }

static bool cmd_justify_paragraph( void )
    {
    justify_paragraph = true;
    return cmd_fill_justify_paragraph_common();
    }

static bool cmd_fill_justify_paragraph_common( void )
    {
    int fc;
    size_t from, ind;
    char ch;
    size_t space_ind[MAX_FILL_COLUMN];
    int space_col[MAX_FILL_COLUMN];
    int nspaces, col, nnls, line;
    bool ws;
    size_t wsstart = 0;

    fc = min( vars[var_find( "fill-column" )].val.i, MAX_FILL_COLUMN );

    /* Save a initial null record, just to get the proper point/mark saved. */
    (void) buffer_undo_save_null( buf );

    /* First, select the paragraph or paragraphs. */
    if ( ! cmd_mark_paragraph() )
	return false;

    /* Little state machine to fill/justify.
    **
    ** Loop starting from point. Any whitespace gets replaced by a single
    ** space. Space positions are noted in an array. If the column
    ** passes fill-column, optionally justify, change the last space to
    ** a newline, and start on the next line.
    **
    ** Empty lines get preserved, in case we're filling multiple paragraphs.
    **
    ** The buffer package updates mark as things change. When we get to
    ** mark, we're done.
    */
    from = ind = buf->point;
    nspaces = 0;
    col = 0;
    ws = false;
    nnls = 0;
    line = 0;
    for ( ind = from; ind < buf->mark; )
	{
	ch = buffer_get_byte( buf, ind );
	if ( ch == ' ' || ch == '\t' || ch == '\n' )
	    {
	    /* Whitespace. Note and delete it. */
	    if ( ! ws )
		{
		wsstart = ind;
		nnls = 0;
		}
	    buf->point = ind;
	    if ( ! buffer_delete_forward( buf, 1 ) )
		return false;
	    ws = true;
	    if ( ch == '\n' )
		++nnls;
	    }
	else
	    {
	    /* Non-whitespace. */
	    if ( ws )
		{
		/* But we were previously in whitespace. */
		if ( nnls > 1 )
		    {
		    /* Multiple NLs means it was a paragraph break. Put back
		    ** two NLs and start a new line.
		    */
		    buf->point = wsstart;
		    if ( ! buffer_insert_byte( buf, '\n' ) )
			return false;
		    if ( ! buffer_insert_byte( buf, '\n' ) )
			return false;
		    nspaces = 0;
		    col = 0;
		    ind += 2;
		    }
		else
		    {
		    /* Add back one space, and note its location. */
		    buf->point = wsstart;
		    if ( ! buffer_insert_byte( buf, ' ' ) )
			return false;
		    space_ind[nspaces] = wsstart;
		    space_col[nspaces] = col;
		    ++nspaces;
		    ++col;
		    ++ind;
		    }
		ws = false;
		nnls = 0;
		}
	    col += screen_positions( ch );
	    if ( col > fc )
		{
		/* Would overflow. Change the previous space to a newline.
		** If there aren't any spaces, it's an over-long word and
		** we just let it ride.
		*/
		if ( nspaces > 0 )
		    {
		    if ( ! buffer_set_byte( buf, space_ind[nspaces - 1], '\n' ) )
			return false;
		    if ( justify_paragraph )
			{
			/* Go back and insert more spaces to make the right
			** margin line up.
			*/
			int njustify, ns, s1, s2, ns1, ns2, i;

			/* ns is the number of places available to add spaces
			** njustify is the number of spaces we want to add
			** s1 and s2 are the two sizes of spaces we'll add;
			**   s2 is one space larger
			** ns1 and ns2 the counts for s1 and s2
			** s1 * ns1 + s2 * ns2 should sum up to njustify; if
			**   they don't that's a problem, and the text will
			**   not be justified correctly
			*/
			ns = nspaces - 1;
			if ( ns > 0 )
			    {
			    njustify = fc - space_col[nspaces - 1];
			    s1 = njustify / ns;
			    s2 = s1 + 1;
			    ns1 = ns;
			    ns2 = njustify - ( s1 * ns1 );
			    ns1 -= ns2;
			    if ( s1 * ns1 + s2 * ns2 != njustify )
				elog( "s1/ns1/s2/ns2 don't sum up to njustify" );
			    if ( odd( line ) )
				{
				/* On alternate lines, swap s1/s2 and ns1/ns2
				** to avoid "rivers" - places where the added
				** spaces line up vertically.
				*/
				int t;
				t = s1; s1 = s2; s2 = t;
				t = ns1; ns1 = ns2; ns2 = t;
				}
			    /* Loop through the spaces in reverse order, because
			    ** adding more spaces invalidates following positions.
			    */
			    for ( i = nspaces - 2; i >= 0; --i )
				{
				buf->point = space_ind[i];
				if ( ns1 > 0 )
				    {
				    if ( ! insert_spaces( s1 ) )
					return false;
				    ind += s1;
				    --ns1;
				    }
				else if ( ns2 > 0 )
				    {
				    if ( ! insert_spaces( s2 ) )
					return false;
				    ind += s2;
				    --ns2;
				    }
				else
				    break;
				}
			    }
			}
		    col = col - space_col[nspaces - 1] - 1;
		    nspaces = 0;
		    }
		++line;
		}
	    ++ind;
	    }
	}
    /* If we ended with a newline (probably), add it back. */
    if ( nnls > 0 )
	{
	buf->point = ind;
	if ( ! buffer_insert_byte( buf, '\n' ) )
	    return false;
	++buf->mark;
	}

    /* And set point/mark. */
    buf->point = buf->mark;
    buf->mark = from;
    return true;
    }


static bool cmd_center_line( void )
    {
    /* Save a initial null record, just to get the proper point/mark saved. */
    (void) buffer_undo_save_null( buf );

    /* Select the line or lines. */
    if ( ! cmd_mark_line() )
	return false;
    return cmd_center_region();
    }


static bool cmd_center_paragraph( void )
    {
    /* Save a initial null record, just to get the proper point/mark saved. */
    (void) buffer_undo_save_null( buf );

    /* Select the paragraph or paragraphs. */
    if ( ! cmd_mark_paragraph() )
	return false;
    return cmd_center_region();
    }


static bool cmd_center_region( void )
    {
    int fc;
    size_t from, eol, ws, len;

    fc = min( vars[var_find( "fill-column" )].val.i, MAX_FILL_COLUMN );

    from = min( buf->point, buf->mark );
    buf->mark = max( buf->point, buf->mark );
    buf->point = from;

    /* Loop centering lines until we get to mark. We use mark as the
    ** loop exit criterion because changes to the buffer update it.
    */
    for (;;)
	{
	eol = search_eol( buf->point );
	buf->point = rsearch_bol( buf->point );
	len = eol - buf->point;
	ws = span_charset( buf->point, hwhitespace_charset );
	if ( ! buffer_delete_forward( buf, ws ) )
	    return false;
	if ( len < fc )
	    if ( ! insert_spaces( ( fc - len ) / 2 ) )
		return false;
	buf->point = search_eol( buf->point );
	if ( buf->point < buf->len )
	    ++buf->point;
	if ( buf->point >= buf->mark )
	    break;
	}

    /* And set point to the end / mark to the start of the new region. */
    buf->point = buf->mark;
    buf->mark = from;
    return true;
    }


static bool cmd_scroll_down_command( void )
    {
    int i;

    if ( buf->last >= buf->len )
	return at_end();

    if ( argument > 1 )
	{
	/* Argument - scroll that many lines, not screens. */
	for ( i = 0; i < argument; ++i )
	    {
	    buf->first = search_eol( buf->first );
	    if ( buf->first < buf->len )
		++buf->first;
	    }
	if ( buf->first > buf->point )
	    buf->point = buf->first;
	}
    else
	{
	/* No argument - scroll by a screenful. */
	buf->point = buf->last;
	/* Adjust by a couple lines for some overlap. If the lines are
	** over-long this will be not quite right.
	*/
	buf->first = buf->point;
	for ( i = 0; i < 2; ++i )
	    if ( buf->first > 0 )
		buf->first = rsearch_bol( buf->first - 1 );
	}
    return true;
    }


static bool cmd_scroll_up_command( void )
    {
    int i;

    if ( buf->first == 0 )
	return at_start();

    if ( argument > 1 )
	{
	/* Argument - scroll that many lines, not screens. */
	for ( i = 0; i < argument; ++i )
	    {
	    buf->first = rsearch_bol( buf->first - 1 );
	    if ( buf->first == 0 )
		break;
	    /* Also scroll last. It's going to get recomputed by display(),
	    ** but we need it here to decide whether to adjust point.
	    */
	    buf->last = rsearch_bol( buf->last - 1 );
	    }
	if ( buf->last < buf->point )
	    buf->point = buf->last;
	if ( buf->point > 0 )
	    buf->point = rsearch_bol( buf->point - 1 );
	}
    else
	{
	/* No argument - scroll by a screenful. */
	/* This is not quite right because of too-long lines, but it'll do
	** for now.
	*/
	buf->point = buf->first;
	for ( i = 0; i < display_rows - 2; ++i )
	    {
	    buf->first = rsearch_bol( buf->first - 1 );
	    if ( buf->first == 0 )
		break;
	    if ( i == 0 )
		buf->point = buf->first;
	    }
	}
    return true;
    }


static bool is_reverse, is_failing, is_wrapped;
static char is_search[sizeof(prompt_answer)];
static char is_prev_search[sizeof(prompt_answer)] = "";

static bool cmd_isearch( void )
    {
    is_reverse = false;
    is_failing = false;
    is_wrapped = false;
    is_search[0] = '\0';
    next_keytab = &is_keytab;
    set_next_is_message();
    return true;
    }

static bool cmd_reverse_isearch( void )
    {
    is_reverse = true;
    is_failing = false;
    is_wrapped = false;
    is_search[0] = '\0';
    next_keytab = &is_keytab;
    set_next_is_message();
    return true;
    }

static bool cmd_isearch_run( void )
    {
    ssize_t pos;

    pos = buf->point;
    switch ( input_int )
	{
	case '\n':
	case '\r':
	case KEY_ENTER:
	/* Exit isearch. */
	return true;

	case '':						/* DEL */
	case '':
	case KEY_BACKSPACE:
	/* Delete a character from the search string. */
	if ( strlen( is_search ) == 0 )
	    flash();
	else
	    is_search[strlen( is_search ) - 1] = '\0';
	break;

	case '':
	is_reverse = false;
	/* Recall previous search. */
	if ( strlen( is_search ) == 0 )
	    {
	    if ( strlen( is_prev_search ) == 0 )
		flash();
	    else
		(void) strcpy( is_search, is_prev_search );
	    }
	if ( is_failing )
	    {
	    /* Wrap. */
	    pos = 0;
	    is_wrapped = true;
	    }
	else
	    {
	    /* Increment, to search for the next one. */
	    if ( pos < buf->len )
		++pos;
	    }
	break;

	case '':
	is_reverse = true;
	/* Recall previous search. */
	if ( strlen( is_search ) == 0 )
	    {
	    if ( strlen( is_prev_search ) == 0 )
		flash();
	    else
		(void) strcpy( is_search, is_prev_search );
	    }
	if ( is_failing )
	    {
	    /* Wrap. */
	    pos = buf->len;
	    is_wrapped = true;
	    }
	else
	    {
	    /* Decrement, to search for the previous one. */
	    if ( pos > 0 )
		--pos;
	    }
	break;

	default:
	/* Add to search string. */
	(void) strlcatc( is_search, input_char, sizeof(is_search) );
	break;
	}

    /* Save the search string. */
    (void) strcpy( is_prev_search, is_search );

    /* Find next match. */
    if ( strlen( is_search ) == 0 )
	is_failing = false;
    else if ( buf->len == 0 )
	{
	flash();
	is_failing = true;
	}
    else
	{
	bool ic = islowerall( is_search );
	/* Do the search. */
	if ( is_reverse )
	    pos = rsearch_str( pos + strlen( is_search ), is_search, ic );
	else
	    pos = search_str( pos, is_search, ic );
	if ( pos == -1 )
	    {
	    flash();
	    is_failing = true;
	    }
	else
	    {
	    buf->point = pos;
	    is_failing = false;
	    }
	}

    /* Continue. */
    next_keytab = &is_keytab;
    set_next_is_message();
    return true;
    }

static void set_next_is_message( void )
    {
    set_next_message( "%s%s%sI-search \"%s\"  (C-s C-r DEL CR/C-g or search character)", is_failing ? "Failing " : "", is_wrapped ? "Wrapped " : "", is_reverse ? "Reverse " : "", is_search );
    }


static bool cmd_query_replace( void )
    {
    if ( buf->readonly )
	return is_readonly();
    prompt( query_replace_pcb1, "Query replace: " );
    return true;
    }

static char qr_search[sizeof(prompt_answer)];
static char qr_replace[sizeof(prompt_answer)];
static int qr_count;
static bool qr_ic;

static bool query_replace_pcb1( char* answer )
    {
    elog( "query_replace_pcb1 '%s'", answer );
    if ( strlen( answer ) == 0 )
	return false;
    strcpy( qr_search, answer );
    prompt( query_replace_pcb2, "Query replace \"%s\" with: ", qr_search );
    return true;
    }

static bool query_replace_pcb2( char* answer )
    {
    ssize_t pos;

    elog( "query_replace_pcb2 '%s'", answer );
    if ( strlen( answer ) == 0 )
	return false;
    strcpy( qr_replace, answer );

    /* Search for the first occurence. */
    qr_ic = islowerall( qr_search );
    pos = search_str( buf->point, qr_search, qr_ic );
    if ( pos == -1 )
	{
	set_next_message( "\"%s\" not found.", qr_search );
	return false;
	}
    else
	buf->point = pos;

    /* Switch to QR mode. */
    next_keytab = &qr_keytab;
    set_next_message( "Query replace \"%s\" with \"%s\"  (SP/y DEL/n ! ^ CR/q/C-g)", qr_search, qr_replace );
    qr_count = 0;
    return true;
    }


static bool cmd_query_replace_run( void )
    {
    bool replace, replaceall, search, rsearch;
    ssize_t pos;

    /* When we get here, we should have an instance of qr_search at point. */
    replace = replaceall = search = rsearch = false;
    switch ( input_int )
	{
	case ' ':
	case 'y':
	case 'Y':
	replace = search = true;
	break;

	case '':						/* DEL */
	case 'n':
	case 'N':
	search = true;
	break;

	case '!':
	replaceall = true;
	break;

	case '^':
	rsearch = true;
	break;

	case '\n':						/* C-j */
	case '\r':						/* C-m */
	case KEY_ENTER:
	case 'q':
	case 'Q':
	return true;
	break;

	default:
	/* Shouldn't happen. */
	flash();
	break;
	}

    if ( replace )
	{
	/* Replace this instance. */
	if ( ! buffer_delete_forward( buf, strlen( qr_search ) ) )
	    return false;
	if ( ! buffer_insert_string( buf, qr_replace ) )
	    return false;
	buf->point -= strlen( qr_replace );
	++qr_count;
	}
    else if ( replaceall )
	{
	/* Replace all remaining instances. */
	for (;;)
	    {
	    /* Replace. */
	    if ( ! buffer_delete_forward( buf, strlen( qr_search ) ) )
		return false;
	    if ( ! buffer_insert_string( buf, qr_replace ) )
		return false;
	    buf->point -= strlen( qr_replace );
	    ++qr_count;
	    /* Move. */
	    ++buf->point;
	    pos = search_str( buf->point, qr_search, qr_ic );
	    if ( pos == -1 )
		{
		set_next_message( "Replaced %d occurences.", qr_count );
		return true;
		}
	    else
		buf->point = pos;
	    }
	}
    if ( search )
	{
	/* Move to the next instance. */
	++buf->point;
	pos = search_str( buf->point, qr_search, qr_ic );
	if ( pos == -1 )
	    {
	    set_next_message( "Replaced %d occurences.", qr_count );
	    return true;
	    }
	else
	    buf->point = pos;
	}
    else if ( rsearch )
	{
	pos = rsearch_str( buf->point, qr_search, qr_ic );
	if ( pos == -1 )
	    {
	    set_next_message( "Replaced %d occurences.", qr_count );
	    return true;
	    }
	else
	    buf->point = pos;
	}

    /* Continue. */
    next_keytab = &qr_keytab;
    set_next_message( "%s", message );
    return true;
    }


static bool cmd_beginning_of_buffer( void )
    {
    buf->point = 0;
    return true;
    }


static bool cmd_end_of_buffer( void )
    {
    buf->point = buf->len;
    return true;
    }


static bool cmd_delete_backward_char( void )
    {
    if ( buf->readonly )
	return is_readonly();
    if ( prev_cmd == next_prev_cmd )
	(void) buffer_undo_aggregate( buf );
    return buffer_delete_backward( buf, 1 );
    }


static bool cmd_delete_forward_char( void )
    {
    if ( buf->readonly )
	return is_readonly();
    if ( prev_cmd == next_prev_cmd )
	(void) buffer_undo_aggregate( buf );
    return buffer_delete_forward( buf, 1 );
    }


static bool cmd_kill_word( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->mark = search_eow( buf->point );
    return cmd_kill_region();
    }


static bool cmd_backward_kill_word( void )
    {
    if ( buf->point == 0 )
	return at_start();
    buf->mark = rsearch_bow( buf->point );
    return cmd_kill_region();
    }


static bool cmd_kill_line( void )
    {
    /* Save a initial null record, just to get the proper point/mark saved. */
    (void) buffer_undo_save_null( buf );

    if ( ! cmd_mark_line() )
	return false;
    return cmd_kill_region();
    }


static bool cmd_kill_sentence( void )
    {
    if ( buf->point >= buf->len )
	return at_end();
    buf->mark = search_eos( buf->point );
    return cmd_kill_region();
    }


static bool cmd_backward_kill_sentence( void )
    {
    if ( buf->point == 0 )
	return at_start();
    buf->mark = rsearch_bos( buf->point );
    return cmd_kill_region();
    }


static bool cmd_kill_region( void )
    {
    int krm;
    size_t from, to;
    str* s;
    bool r;

    krm = vars[var_find( "kill-ring-max" )].val.i;

    if ( buf->readonly )
	return is_readonly();
    if ( buf->point == buf->mark )
	{
	set_next_message( "Region is empty." );
	return false;
	}
    from = min( buf->point, buf->mark );
    to = max( buf->point, buf->mark );

    /* Get a kill string. If the previous command was also a kill,
    ** use that one.
    */
    if ( prev_cmd == cmd_kill_word || prev_cmd == cmd_backward_kill_word ||
         prev_cmd == cmd_kill_line || prev_cmd == cmd_kill_sentence ||
	 prev_cmd == cmd_backward_kill_sentence || prev_cmd == cmd_kill_region )
	s = str_ring_first( kill_ring );
    else
	{
	s = str_create();
	if ( s == (str*) 0 )
	    return false;
	if ( ! str_ring_addfirst( kill_ring, s ) )
	    {
	    str_destroy( s );
	    return false;
	    }
	if ( kill_ring_entries >= krm )
	    {
	    str* olds = str_ring_removelast( kill_ring );
	    str_destroy( olds );
	    }
	else
	    ++kill_ring_entries;
	}

    /* Copy to kill string. */
    if ( next_prev_cmd == cmd_backward_kill_word || next_prev_cmd == cmd_backward_kill_sentence )
	r = prepend_bufferbytes_to_str( buf, from, s, to - from );
    else
	r = append_bufferbytes_to_str( buf, from, s, to - from );
    if ( ! r )
	return false;

    /* Delete from buf. */
    buf->point = from;
    if ( ! buffer_delete_forward( buf, to - from ) )
	return false;

    return true;
    }


static bool cmd_kill_ring_save( void )
    {
    int krm;
    size_t from, to;
    str* s;

    krm = vars[var_find( "kill-ring-max" )].val.i;

    if ( buf->point == buf->mark )
	{
	set_next_message( "Region is empty." );
	return false;
	}
    from = min( buf->point, buf->mark );
    to = max( buf->point, buf->mark );
    s = str_create();
    if ( s == (str*) 0 )
	return false;
    if ( ! append_bufferbytes_to_str( buf, from, s, to - from ) )
	{
	str_destroy( s );
	return false;
	}
    if ( ! str_ring_addfirst( kill_ring, s ) )
	{
	str_destroy( s );
	return false;
	}
    if ( kill_ring_entries >= krm )
	{
	str* olds = str_ring_removelast( kill_ring );
	str_destroy( olds );
	}
    else
	++kill_ring_entries;
    set_next_message( "Region saved." );
    return true;
    }


static bool cmd_yank( void )
    {
    str* s;

    if ( buf->readonly )
	return is_readonly();
    if ( str_ring_empty( kill_ring ) )
	{
	set_next_message( "Nothing to yank." );
	return false;
	}

    /* Get the yank string. */
    s = str_ring_first( kill_ring );

    /* Insert it. */
    buf->mark = buf->point;
    if ( ! copy_str_to_buffer( s, buf, buf->point ) )
	return false;

    return true;
    }


static bool cmd_yank_pop( void )
    {
    str* s;

    if ( buf->readonly )
	return is_readonly();
    if ( str_ring_empty( kill_ring ) )
	{
	set_next_message( "Nothing to yank." );
	return false;
	}
    if ( prev_cmd != cmd_yank && prev_cmd != cmd_yank_pop )
	{
	set_next_message( "Previous command was not a yank or yank-pop." );
	return false;
	}

    /* Rotate the kill ring. */
    if ( ! str_ring_addlast( kill_ring, str_ring_removefirst( kill_ring ) ) )
	return false;
    s = str_ring_first( kill_ring );

    /* Delete the region, which should be the previous yank. */
    if ( buf->point < buf->mark )
	{
	if ( ! buffer_delete_forward( buf, buf->mark - buf->point ) )
	    return false;
	}
    else if ( buf->mark < buf->point )
	{
	if ( ! buffer_delete_backward( buf, buf->point - buf->mark ) )
	    return false;
	}

    /* Insert the replacement yank. */
    if ( ! copy_str_to_buffer( s, buf, buf->point ) )
	return false;

    return true;
    }


static bool cmd_zap_to_char( void )
    {
    prompt_key( zap_pkcb, false, "Zap to char: " );
    return true;
    }

static bool zap_pkcb( char answer )
    {
    size_t from;
    ssize_t to;

    elog( "zap_pkcb '%c'", answer );
    from = buf->point;
    to = search_char( buf->point, answer, false );
    if ( to == -1 )
	return false;
    buf->mark = from;
    buf->point = to + 1;
    return cmd_kill_region();
    }


static bool cmd_transpose_chars( void )
    {
    size_t ind;

    if ( buf->readonly )
	return is_readonly();
    if ( buf->len < 2 )
	{
	set_next_message( "There aren't two characters in the buffer." );
	return false;
	}

    /* Special cases for the beginning and end of the buffer. */
    if ( buf->point == 0 )
	ind = buf->point;
    else if ( buf->point == buf->len )
	ind = buf->point - 2;
    else
	ind = buf->point - 1;

    return transpose( ind, ind + 1, ind + 1, ind + 2 );
    }


static bool cmd_transpose_words( void )
    {
    size_t bow1, eow1, bow2, eow2;

    if ( buf->readonly )
	return is_readonly();
    bow1 = rsearch_bow( buf->point );
    eow1 = search_eow( bow1 );
    if ( bow1 == 0 && eow1 == buf->len )
	{
	set_next_message( "There aren't two words in the buffer." );
	return false;
	}
    else if ( eow1 == buf->len )
	{
	bow2 = bow1;
	eow2 = eow1;
	if ( bow2 == 0 )
	    return false;
	bow1 = rsearch_bow( bow2 - 1 );
	eow1 = search_eow( bow1 );
	}
    else
	{
	if ( eow1 == buf->len )
	    return false;
	eow2 = search_eow( eow1 + 1 );
	bow2 = rsearch_bow( eow2 );
	if ( bow1 == bow2 )
	    {
	    eow2 = eow1;
	    if ( bow1 == 0 )
		return false;
	    bow1 = rsearch_bow( bow1 - 1 );
	    eow1 = search_eow( bow1 );
	    if ( bow1 == bow2 )
		return false;
	    }
	}

    return transpose( bow1, eow1, bow2, eow2 );
    }


static bool cmd_transpose_sentences( void )
    {
    size_t bos1, eos1, bos2, eos2;

    if ( buf->readonly )
	return is_readonly();
    bos1 = rsearch_bos( buf->point );
    eos1 = search_eos( bos1 );
    if ( bos1 == 0 && eos1 == buf->len )
	{
	set_next_message( "There aren't two sentences in the buffer." );
	return false;
	}
    else if ( eos1 == buf->len )
	{
	bos2 = bos1;
	eos2 = eos1;
	if ( bos2 == 0 )
	    return false;
	bos1 = rsearch_bos( bos2 - 1 );
	eos1 = search_eos( bos1 );
	}
    else
	{
	if ( eos1 == buf->len )
	    return false;
	eos2 = search_eos( eos1 + 1 );
	bos2 = rsearch_bos( eos2 );
	}

    return transpose( bos1, eos1, bos2, eos2 );
    }


static bool cmd_transpose_lines( void )
    {
    size_t bol1, eol1, bol2, eol2;

    if ( buf->readonly )
	return is_readonly();
    bol1 = rsearch_bol( buf->point );
    eol1 = search_eol( bol1 );
    if ( bol1 == 0 && eol1 == buf->len )
	{
	set_next_message( "There aren't two lines in the buffer." );
	return false;
	}
    else if ( eol1 == buf->len )
	{
	bol2 = bol1;
	eol2 = eol1;
	if ( bol2 == 0 )
	    return false;
	bol1 = rsearch_bol( bol2 - 1 );
	eol1 = search_eol( bol1 );
	}
    else
	{
	if ( eol1 == buf->len )
	    return false;
	eol2 = search_eol( eol1 + 1 );
	bol2 = rsearch_bol( eol2 );
	}

    return transpose( bol1, eol1, bol2, eol2 );
    }


static bool cmd_set_mark_command( void )
    {
    buf->mark = buf->point;
    set_next_message( "Mark set." );
    return true;
    }


static bool cmd_exchange_point_and_mark( void )
    {
    size_t t;

    t = buf->mark;
    buf->mark = buf->point;
    buf->point = t;
    return true;
    }


static bool cmd_upcase_word( void )
    {
    return transform_word( tcb_upcase );
    }


static bool cmd_downcase_word( void )
    {
    return transform_word( tcb_downcase );
    }


static bool cmd_capitalize_word( void )
    {
    size_t from, to;

    to = search_eow( buf->point );
    from = rsearch_bow( to );
    if ( from >= buf->len )
	return false;
    buf->point = to;
    return tcb_upcase( from ) && transform( from + 1, to, tcb_downcase );
    }


static bool cmd_upcase_region( void )
    {
    return transform_region( tcb_upcase );
    }


static bool cmd_downcase_region( void )
    {
    return transform_region( tcb_downcase );
    }


static bool cmd_delete_horizontal_space( void )
    {
    if ( buf->readonly )
	return is_readonly();
    buf->mark = buf->point;
    buf->mark += span_charset( buf->mark, hwhitespace_charset );
    buf->point -= rspan_charset( buf->point, hwhitespace_charset );
    return buf->mark != buf->point && buffer_delete_forward( buf, buf->mark - buf->point );
    }


static bool cmd_just_one_space( void )
    {
    return cmd_delete_horizontal_space() && buffer_insert_byte( buf, ' ' );
    }


static bool cmd_recenter_top_bottom( void )
    {
    static int which;
    int shift = 0, i;

    if ( argument != 1 )
	{
	/* Shift point to the argument'th line. */
	if ( argument < 0 || argument > display_rows )
	    return false;
	shift = argument - 1;
	which = -1;	/* next time will be 0 */
	}
    else
	{
	if ( prev_cmd != next_prev_cmd )
	    which = 0;
	else
	    which = ( which + 1 ) % 3;
	switch ( which )
	    {
	    case 0:
	    /* Shift point to the center. */
	    shift = display_rows / 2;
	    break;
	    case 1:
	    /* Shift point to the top. */
	    shift = 2;
	    break;
	    case 2:
	    /* Shift point to the bottom. */
	    shift = display_rows - 3;
	    break;
	    }
	}
    /* Move first to shift lines before point. */
    buf->first = rsearch_bol( buf->point );
    for ( i = 0; i < shift; ++i )
	{
	if ( buf->first == 0 )
	    break;
	buf->first = rsearch_bol( buf->first - 1 );
	}

    /* Calling redrawwin doesn't actually invalidate everything; doing
    ** it twice does work.
    */
    redrawwin( stdscr );
    refresh();
    redrawwin( stdscr );
    return true;
    }


static bool cmd_undo( void )
    {
    if ( undo_stack_empty( buf->us ) )
        {
	set_next_message( "Nothing to undo." );
	return false;
	}

    (void) buffer_undo_disable( buf );		/* undo is not undoable */
    if ( ! buffer_undo_revert( buf ) )
	{
	set_next_message( "Undo failed!" );
	/* Unfortunately this means all remaining undo records are now
	** invalid, so we have to discard them.
	*/
	undo_stack_clear( buf->us );
	return false;
	}
    return true;
    }


static bool cmd_matchchar( void )
    {
    char* mc;
    ssize_t ind;
    char enc1, enc2, c;
    char* cp;
    int i, inc, depth;

    mc = vars[var_find( "matchchars" )].val.s;

    if ( odd( strlen( mc ) ) )
	{
	set_next_message( "matchchars is malformed" );
	return false;
	}

    /* Find the next enclosing character. */
    ind = buf->point + span_ccharset( buf->point, mc );
    if ( ind >= buf->len )
	{
	set_next_message( "no more enclosing characters found" );
	return false;
	}

    /* Get the enclosing character, figure out the opposite character,
    ** and which direction to loop.
    */
    enc1 = buffer_get_byte( buf, ind );
    cp = strchr( mc, enc1 );
    if ( cp == (char*) 0 )
	return false;	/* shouldn't happen */
    i = cp - mc;
    if ( odd( i ) )
	{
	enc2 = mc[i - 1];
	inc = -1;
	}
    else
	{
	enc2 = mc[i + 1];
	inc = 1;
	}

    /* Loop in the appropriate direction until a match is found and
    ** the expression depth is zero.
    */
    depth = 0;
    for (;;)
	{
	ind += inc;
	if ( ind < 0 || ind >= buf->len )
	    {
	    set_next_message( "no matching enclosing character found" );
	    return false;
	    }
	c = buffer_get_byte( buf, ind );
	if ( c == enc1 )
	    ++depth;
	else if ( c == enc2 )
	    {
	    if ( depth == 0 )
		break;
	    else
		--depth;
	    }
	}

    /* Success. */
    buf->point = ind;
    return true;
    }


static bool cmd_keyboard_quit( void )
    {
    next_keytab = &base_keytab;
    set_next_message( "%s", "" );
    return false;
    }


static bool cmd_suspend( void )
    {
    fini_curses();
    kill( 0, SIGTSTP );
    init_curses();
    return true;
    }


static bool find_file_readonly;

static bool cmd_find_file( void )
    {
    find_file_readonly = false;
    return cmd_find_file_common();
    }

static bool cmd_find_file_read_only( void )
    {
    find_file_readonly = true;
    return cmd_find_file_common();
    }

static bool cmd_find_file_common( void )
    {
    if ( buf->modified )
	{
	prompt_yesno( find_file_yes_pcb, find_file_no_pcb, "Save current file first? " );
	return true;
	}
    prompt( find_file_pcb, "Find file%s: ", find_file_readonly ? " (readonly)" : "" );
    return true;
    }

static bool find_file_yes_pcb( char* answer )
    {
    elog( "find_file_yes_pcb '%s'", answer );
    if ( ! cmd_save_buffer() )
	return false;
    prompt( find_file_pcb, "Ok. Find file%s: ", find_file_readonly ? " (readonly)" : "" );
    return true;
    }

static bool find_file_no_pcb( char* answer )
    {
    elog( "find_file_no_pcb '%s'", answer );
    prompt( find_file_pcb, "Ok. Find file%s: ", find_file_readonly ? " (readonly)" : "" );
    return true;
    }

static bool find_file_pcb( char* answer )
    {
    elog( "find_file_pcb '%s'", answer );
    if ( ! check_filename( answer ) )
	return false;
    if ( ! read_file( answer ) )
	return false;
    buf->readonly = find_file_readonly;
    return true;
    }


static bool cmd_insert_file( void )
    {
    if ( buf->readonly )
	return is_readonly();
    prompt( insert_file_pcb, "Insert file: " );
    return true;
    }

static bool insert_file_pcb( char* answer )
    {
    bool r;

    elog( "insert_file_pcb '%s'", answer );
    if ( ! check_filename( answer ) )
	return false;
    r = insert_file( answer );
    if ( r )
	set_next_message( "Inserted %s", answer );
    else
	set_next_message( "Couldn't read %s", answer );
    return r;
    }


static bool cmd_write_file( void )
    {
    prompt( write_file_pcb, "Write file: " );
    return true;
    }

static char write_file_name[sizeof(prompt_answer)];

static bool write_file_pcb( char* answer )
    {
    elog( "write_file_pcb '%s'", answer );
    if ( ! check_filename( answer ) )
	return false;
    if ( file_exists( answer ) )
	{
	strcpy( write_file_name, answer );
	prompt_yesno( write_file_yes_pcb, (prompter*) 0, "File already exists - overwrite? " );
	return true;
	}
    if ( ! write_file( answer ) )
	{
	set_next_message( "Problem saving file!" );
	return false;
	}
    set_next_message( "Written." );
    return true;
    }

static bool write_file_yes_pcb( char* answer )
    {
    elog( "write_file_yes_pcb '%s'", answer );
    if ( ! check_filename( answer ) )
	return false;
    if ( ! write_file( write_file_name ) )
	{
	set_next_message( "Problem saving file!" );
	return false;
	}
    set_next_message( "Written." );
    return true;
    }


static bool cmd_save_buffer( void )
    {
    if ( filename[0] == '\0' )
	{
	set_next_message( "No filename!" );
	return false;
	}
    if ( not_a_file )
	{
	set_next_message( "Not a file!" );
	return false;
	}
    if ( mtime != -1 && file_mtime( filename ) != mtime )
	{
	set_next_message( "File has been modified while you were editing!" );
	return false;
	}
    if ( ! write_file( filename ) )
	{
	set_next_message( "Problem saving file!" );
	return false;
	}
    set_next_message( "Saved." );
    return true;
    }


static bool cmd_save_buffer_exit( void )
    {
    if ( buf->modified )
	{
	prompt_yesno( save_yes_pcb, save_no_pcb, "Save file? " );
	return true;
	}
    done = true;
    return true;
    }

static bool save_yes_pcb( char* answer )
    {
    elog( "save_yes_pcb '%s'", answer );
    if ( ! cmd_save_buffer() )
	return false;
    done = true;
    return true;
    }

static bool save_no_pcb( char* answer )
    {
    elog( "save_no_pcb '%s'", answer );
    set_next_message( "Not saved." );
    done = true;
    return true;
    }


static bool cmd_shell_command( void )
    {
    if ( buf->readonly )
	return is_readonly();
    prompt( shell_command_pcb, "Run command: " );
    return true;
    }

static bool shell_command_pcb( char* answer )
    {
    FILE* fp;
    int ich;

    elog( "shell_command_pcb '%s'", answer );
    if ( ! check_shellcommand( answer ) )
	return false;
    fp = popen( answer, "r" );
    if ( fp == (FILE*) 0 )
	return false;
    buf->mark = buf->point;
    while ( ( ich = getc( fp ) ) != EOF )
	{
	if ( ! buffer_insert_byte( buf, ich ) )
	    {
	    (void) pclose( fp );
	    return false;
	    }
	}
    (void) pclose( fp );
    return true;
    }


static bool cmd_shell_command_on_region( void )
    {
    if ( buf->readonly )
	return is_readonly();
    if ( buf->point == buf->mark )
	{
	set_next_message( "Region is empty." );
	return false;
	}
    prompt( shell_command_on_region_pcb, "Filter through command: " );
    return true;
    }

typedef struct
    {
    size_t from, to, ind;
    } scor_write_data;

typedef struct
    {
    buffer* b;
    bool done;
    } scor_read_data;

static bool shell_command_on_region_pcb( char* answer )
    {
    phandle2 handle;
    scor_write_data write_data;
    scor_read_data read_data;
    size_t ind;

    elog( "shell_command_on_region_pcb '%s'", answer );
    if ( ! check_shellcommand( answer ) )
	return false;

    /* Run the command. */
    if ( popen2( answer, &handle ) != 0 )
	return false;
    write_data.from = min( buf->point, buf->mark );
    write_data.to = max( buf->point, buf->mark );
    write_data.ind = write_data.from;
    read_data.done = false;
    read_data.b = filterbuf;
    buffer_clear( read_data.b );
    while ( ! read_data.done )
	{
	if ( pwriteread2( &handle, scor_write_cb, &write_data, scor_read_cb, &read_data ) == -1 )
	    {
	    (void) pclose2( &handle );
	    return false;
	    }
	}
    (void) pclose2( &handle );

    /* Kill the input. */
    if ( ! cmd_kill_region() )
	return false;

    /* Insert the output into the buffer. */
    for ( ind = 0; ind < filterbuf->len; ++ind )
	if ( ! buffer_insert_byte( buf, buffer_get_byte( filterbuf, ind ) ) )
	    return false;

    /* Yay! */
    return true;
    }

static int scor_write_cb( phandle2* handle, void* client_data )
    {
    scor_write_data* write_data = (scor_write_data*) client_data;
    char ch;

    /* Write input to the child process. */
    /*!!! should write more than one char at a time */
    ch = buffer_get_byte( buf, write_data->ind );
    if ( write( handle->to_child, &ch, 1 ) == -1 )
	{
	(void) pclose2( handle );
	return -1;
	}
    ++write_data->ind;
    if ( write_data->ind >= write_data->to )
	{
	(void) close( handle->to_child );
	handle->to_child = -1;
	}
    return 0;
    }

static int scor_read_cb( phandle2* handle, void* client_data )
    {
    scor_read_data* read_data = (scor_read_data*) client_data;
    char s[1000];
    int r;

    /* Read output from the child process. The fd is in O_NDELAY mode
    ** so we can ask for a bunch of bytes and not worry about deadlock.
    */
    r = read( handle->from_child, s, sizeof(s) );
    if ( r == 0 )
	read_data->done = true;
    else if ( r < 0 || ! buffer_insert_bytes( read_data->b, s, r ) )
	{
	(void) pclose2( handle );
	return -1;
	}
    return 0;
    }


static int argument_count, argument_number;
static bool argument_minus;
static int argument_base;
static keytab* argument_keytab;

static bool cmd_universal_argument( void )
    {
    char m[100];
    int i;

    if ( curr_keytab != &ctrlu_keytab )
	{
	/* Initialize argument accumulation mode. */
	argument_count = 0;
	argument_number = 0;
	argument_minus = false;
	argument_base = 10;
	argument_keytab = &base_keytab;
	}

    /* If we already have a prefix, this character must run a command. */
    if ( argument_keytab != &base_keytab )
	return cmd_universal_argument_run();

    /* Is it a prefix character? */
    switch ( input_char )
	{
	case '':
	argument_keytab = &ctrlx_keytab;
	break;
	case '':
	argument_keytab = &meta_keytab;
	break;

	default:
	/* Not a prefix. */
	input_char &= ~META_BIT;	/* strip off the meta bit */
	if ( input_char == '' )
	    {
	    if ( argument_number != 0 )
		return false;
	    if ( argument_count >= 3 )
		flash();
	    else
		++argument_count;
	    }
	else if ( isdigit( input_char ) || isxdigit( input_char ) )
	    {
	    if ( argument_count > 1 )
		flash();
	    else if ( input_char == '0' && argument_number == 0 )
		flash();
	    else if ( argument_number >= 1000000 )
		flash();
	    else if ( ( ! isdigit( input_char ) ) && argument_base != 16 )
		flash();
	    else
		argument_number = argument_number * argument_base + hexit( input_char );
	    }
	else if ( input_char == '-' )
	    {
	    if ( argument_count <= 1 && argument_number == 0 )
		argument_minus = ! argument_minus;
	    else
		flash();
	    }
	else if ( input_char == 'x' || input_char == 'X' )
	    {
	    if ( argument_count <= 1 && argument_number == 0 )
		argument_base = 16;
	    else
		flash();
	    }
	else
	    return false;
	break;
	}

    /* Build the message. */
    if ( argument_number != 0 )
	{
	if ( argument_base == 16 )
	    (void) snprintf( m, sizeof(m), "C-u %sx%x ", argument_minus ? "-": "", argument_number );
	else
	    (void) snprintf( m, sizeof(m), "C-u %s%d ", argument_minus ? "-": "", argument_number );
	}
    else if ( argument_count <= 1 )
	(void) snprintf( m, sizeof(m), "C-u %s%s", argument_minus ? "- ": "", argument_base == 16 ? "x " : "" );
    else
	{
	m[0] = '\0';
	if ( argument_minus )
	    (void) my_strlcat( m, "- ", sizeof(m) );
	for ( i = 0; i < argument_count; ++i )
	    (void) my_strlcat( m, "C-u ", sizeof(m) );
	}
    if ( argument_keytab != &base_keytab )
	(void) my_strlcat( m, keytab_to_prefix( argument_keytab ), sizeof(m) );

    next_keytab = &ctrlu_keytab;
    set_next_message( "%s", m );
    next_prompt_mode = true;
    return true;
    }


static bool cmd_universal_argument_run( void )
    {
    int i;
    command* cmd;

    /* Figure out how many. */ 
    if ( argument_number != 0 )
       argument = argument_number;
    else
	argument = 1 << ( argument_count * 2 );
    if ( argument_minus )
	argument = -argument;

    /* Get the command. */
    cmd = keytab_get( argument_keytab, input_int );

    /* Does this command not take arguments? */
    if ( command_to_argtype( cmd ) == at_no )
	{
	set_next_message( "That command doesn't take an argument." );
	return false;
	}

    /* If the argument is negative, look for a command/anti-command
    ** pair.
    */
    if ( argument < 0 )
	{
	for ( i = 0; i < sizeof(anti_commands) / sizeof(*anti_commands); ++i )
	    {
	    if ( cmd == anti_commands[i].cmd1 )
		{
		cmd = anti_commands[i].cmd2;
		argument = -argument;
		break;
		}
	    else if ( cmd == anti_commands[i].cmd2 )
		{
		cmd = anti_commands[i].cmd1;
		argument = -argument;
		break;
		}
	    }
	}

    /* Does this command want to handle the argument itself? */
    if ( command_to_argtype( cmd ) == at_self )
	{
	elog( "Running %s with argument %d", command_to_name( cmd ), argument );
	next_prev_cmd = cmd;
	return cmd();
	}

    /* If the argument is still negative (no anti-command and not
    ** self-handled), that's an error.
    */
    if ( argument < 0 )
	{
	set_next_message( "That command doesn't take negative arguments." );
	return false;
	}

    /* Otherwise, we just repeat the command argument times. */
    elog( "Running %s %d times", command_to_name( cmd ), argument );
    next_prev_cmd = cmd;
    for ( i = 0; i < argument; ++i )
	{
	if ( ! cmd() )
	    return false;
	prev_cmd = next_prev_cmd;
	}

    return true;
    }


static bool cmd_prompt_accumulate( void )
    {
    if ( onekey_mode )
	{
	/* Just return the key. */
	(void) strlcatc( prompt_answer, input_char, sizeof(prompt_answer) );
	(void) strlcatc( message, input_char, sizeof(message) );
	return pcb( prompt_answer );
	}

    switch ( input_int )
	{
	case '\n':
	case '\r':
	if ( prompt_answer[0] == '\0' )
	    flash();
	else
	    return pcb( prompt_answer );
	break;

	case '':						/* DEL */
	case '':
	case KEY_BACKSPACE:
	if ( strlen( prompt_answer ) == 0 || strlen( message ) == 0 )
	    flash();
	else
	    {
	    prompt_answer[strlen( prompt_answer ) - 1] = '\0';
	    message[strlen( message ) - 1] = '\0';
	    }
	break;

	case '':
	if ( strlen( prompt_answer ) == 0 || strlen( message ) == 0 )
	    flash();
	else
	    {
	    while ( strlen( prompt_answer ) > 0 && strlen( message ) > 0 )
		{
		prompt_answer[strlen( prompt_answer ) - 1] = '\0';
		message[strlen( message ) - 1] = '\0';
		}
	    }
	break;

	case '':
	redrawwin( stdscr );
	break;

	default:
	(void) strlcatc( prompt_answer, input_char, sizeof(prompt_answer) );
	(void) strlcatc( message, input_char, sizeof(message) );
	break;
	}

    next_keytab = &prompt_keytab;
    set_next_message( "%s", message );
    next_prompt_mode = true;
    return true;
    }


static bool cmd_promptkey( void )
    {
    (void) strlcatc( prompt_answer, input_char, sizeof(prompt_answer) );
    (void) strlcatc( message, input_char, sizeof(message) );
    return pkcb( input_char );
    }


static bool cmd_help_command( void )
    {
    next_keytab = &help_keytab;
    set_next_message( "Help: (a[propos]/f[unction]/k[eys]/n[ews]/t[utorial]/v[ariables]) " );
    next_prompt_mode = true;
    return true;
    }


static bool cmd_apropos_command( void )
    {
    prompt( apropos_pcb, "Apropos command: " );
    return true;
    }

static bool apropos_pcb( char* answer )
    {
    int i;
    bool found = false;
    char* keys;

    printfs_start();
    for ( i = 0; i < sizeof(command_defs) / sizeof(*command_defs); ++i )
	if ( strstr( command_defs[i].name, answer ) != (char*) 0 )
	    {
	    found = true;
	    keys = name_to_keys( command_defs[i].name );
	    if ( keys == (char*) 0 )
		keys = "[unbound]";
	    (void) printf( "%s: %s - %s\r\n", keys, command_defs[i].name, command_defs[i].desc );
	    }
    if ( ! found )
	(void) printf( "No matches found.\r\n" );
    printfs_end();
    return true;
    }


static bool cmd_describe_function( void )
    {
    prompt( function_pcb, "Describe function: " );
    return true;
    }

static bool function_pcb( char* answer )
    {
    command* cmd;
    char* keys;

    cmd = name_to_command( answer );
    if ( cmd == (command*) 0 )
	{
	set_next_message( "Not found." );
	return false;
	}
    keys = name_to_keys( answer );
    if ( keys == (char*) 0 )
	keys = "[unbound]";
    set_next_message( "%s: %s - %s", keys, answer, command_to_desc( cmd ) );
    return true;
    }


static keytab* describekey_prefix_keytab;

static bool cmd_describe_key( void )
    {
    describekey_prefix_keytab = &base_keytab;
    prompt_key( describe_pkcb, true, "Describe key sequence: " );
    return true;
    }

static bool describe_pkcb( char answer )
    {
    int i;

    elog( "describe_pkcb '%c'", answer );

    /* Is it a prefix? */
    for ( i = 0; i < sizeof(prefix_defs) / sizeof(*prefix_defs); ++i )
	if ( describekey_prefix_keytab == prefix_defs[i].curr_kt &&
	     answer == prefix_defs[i].key )
	    {
	    /* Yes, prefix. Get another key. */
	    describekey_prefix_keytab = prefix_defs[i].next_kt;
	    prompt_key( describe_pkcb, true, "Describe key sequence: %s", keytab_to_prefix( describekey_prefix_keytab ) );
	    return true;
	    }
    /* Not a prefix - describe it. */
    describe_key( describekey_prefix_keytab, answer );
    return true;
    }


static bool cmd_help_with_tutorial( void )
    {
    if ( buf->modified )
	{
	prompt_yesno( tutorial_yes_pcb, tutorial_no_pcb, "Save current file first? " );
	return true;
	}
    return load_tutorial();
    }

static bool tutorial_yes_pcb( char* answer )
    {
    elog( "tutorial_no_pcb '%s'", answer );
    if ( ! cmd_save_buffer() )
	return false;
    return load_tutorial();
    }

static bool tutorial_no_pcb( char* answer )
    {
    elog( "tutorial_no_pcb '%s'", answer );
    return load_tutorial();
    }


static bool cmd_help_with_news( void )
    {
    if ( buf->modified )
	{
	prompt_yesno( news_yes_pcb, news_no_pcb, "Save current file first? " );
	return true;
	}
    return load_news();
    }

static bool news_yes_pcb( char* answer )
    {
    elog( "news_no_pcb '%s'", answer );
    if ( ! cmd_save_buffer() )
	return false;
    return load_news();
    }

static bool news_no_pcb( char* answer )
    {
    elog( "news_no_pcb '%s'", answer );
    return load_news();
    }


static bool cmd_describe_variables( void )
    {
    int i;

    printfs_start();
    for ( i = 0; i < sizeof(vars) / sizeof(*vars); ++i )
	{
	switch ( vars[i].vt )
	    {
	    case vt_int:
	    (void) printf( "%s (int %d): %s\r\n", vars[i].name, vars[i].val.i, vars[i].desc );
	    break;
	    case vt_str:
	    (void) printf( "%s (str \"%s\"): %s\r\n", vars[i].name, vars[i].val.s, vars[i].desc );
	    break;
	    case vt_bool:
	    (void) printf( "%s (bool %s): %s\r\n", vars[i].name, vars[i].val.b ? "true" : "false", vars[i].desc );
	    break;
	    default:
	    (void) printf( "%s (?): %s\r\n", vars[i].name, vars[i].desc );
	    break;
	    }
	}
    printfs_end();
    return true;
    }
