[Pkg-mysql-commits] r975 - in branches/sid-5.0/debian: . additions/innotop

Norbert Tretkowski nobse at alioth.debian.org
Thu Nov 8 19:04:39 UTC 2007


Author: nobse
Date: 2007-11-08 19:04:38 +0000 (Thu, 08 Nov 2007)
New Revision: 975

Modified:
   branches/sid-5.0/debian/additions/innotop/InnoDBParser.pm
   branches/sid-5.0/debian/additions/innotop/changelog.innotop
   branches/sid-5.0/debian/additions/innotop/innotop
   branches/sid-5.0/debian/changelog
Log:
Updated innotop to 1.4.3 release.

Modified: branches/sid-5.0/debian/additions/innotop/InnoDBParser.pm
===================================================================
--- branches/sid-5.0/debian/additions/innotop/InnoDBParser.pm	2007-11-08 18:39:40 UTC (rev 974)
+++ branches/sid-5.0/debian/additions/innotop/InnoDBParser.pm	2007-11-08 19:04:38 UTC (rev 975)
@@ -19,10 +19,8 @@
 # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 # Place, Suite 330, Boston, MA  02111-1307  USA
 
-our $VERSION = '1.4.0';
+our $VERSION = '1.4.3';
 
-# TODO: make some options you can pass, such as noparselocks...
-
 use Data::Dumper;
 $Data::Dumper::Sortkeys = 1;
 use English qw(-no_match_vars);
@@ -530,8 +528,15 @@
    $dl->{'timestring'} = ts_to_string($dl->{'ts'});
    $dl->{'txns'} = {};
 
-   my @sections = $fulltext =~ m/^\*{3} (.*)\n(?s:(.*?))(?=^\*)/gm;
+   my @sections
+      = $fulltext
+      =~ m{
+         ^\*{3}\s([^\n]*)  # *** (1) WAITING FOR THIS...
+         (.*?)             # Followed by anything, non-greedy
+         (?=(?:^\*{3})|\z) # Followed by another three stars or EOF
+      }gmsx;
 
+
    # Loop through each section.  There are no assumptions about how many
    # there are, who holds and wants what locks, and who gets rolled back.
    while ( my ($header, $body) = splice(@sections, 0, 2) ) {

Modified: branches/sid-5.0/debian/additions/innotop/changelog.innotop
===================================================================
--- branches/sid-5.0/debian/additions/innotop/changelog.innotop	2007-11-08 18:39:40 UTC (rev 974)
+++ branches/sid-5.0/debian/additions/innotop/changelog.innotop	2007-11-08 19:04:38 UTC (rev 975)
@@ -1,8 +1,122 @@
-This package contains innotop and InnoDBParser.  Please view the perldoc
-for more details on them.
+Changelog for innotop and InnoDBParser:
 
-Changelog:
+2007-07-16: version 1.4.3
 
+   Changes:
+   * Added standard --version command-line option
+   * Changed colors to cyan instead of blue; more visible on dark terminals.
+   * Added information to the filter-choosing dialog.
+   * Added column auto-completion when entering a filter expression.
+   * Changed Term::ReadKey from optional to mandatory.
+   * Clarified username in password prompting.
+   * Ten thousand words of documentation!
+
+   Bugs fixed:
+   * innotop crashed in W mode when InnoDB status data was truncated.
+   * innotop didn't display errors in tables if debug was enabled.
+   * The colored() subroutine wasn't being created in non-interactive mode.
+   * Don't prompt to save password except the first time.
+
+2007-05-03: version 1.4.2
+
+   This version contains all changes to the trunk until revision 239; some
+   changes in revisions 240:250 are included.
+
+   MAJOR CHANGES:
+
+   * Quick-filters to easily filter any column in any display
+   * Compatibility with MySQL 3.23 through 6.0
+   * Improved error handling when a server is down, permissions denied, etc
+   * Use additional SHOW INNODB STATUS information in 5.1.x
+   * Make all modes use tables consistently, so they can all be edited,
+     filtered, colored and sorted consistently
+   * Combine V, G and S modes into S mode, with v, g, and s hot-keys
+   * Let DBD driver read MySQL option files; permit connections without
+     user/pass/etc
+   * Compile SQL-like expressions into Perl subroutines; eliminate need to
+     know Perl
+   * Do not save all config data to config file, only save user's customizations
+   * Rewritten and improved command-line option handling
+   * Added --count, --delay, and other command-line options to support
+     run-and-exit operation
+   * Improve built-in variable sets
+   * Improve help screen with three-part balanced-column layout
+   * Simplify table-editor and improve hotkey support
+   * Require Perl to have high-resolution time support (Time::HiRes)
+   * Help the user choose a query to analyze or kill
+   * Enable EXPLAIN, show-full-query in T mode just like Q mode
+   * Let data-extraction access current, previous and incremental data sets
+     all at once
+
+   MINOR CHANGES:
+
+   * Column stabilizing for Q mode
+   * New color rules for T, Q, W modes
+   * Apply slave I/O filter to Q mode
+   * Improve detection of server version and other meta-data
+   * Make connection timeout a config variable
+   * Improve cross-version-compatible SQL syntax
+   * Get some information from the DBD driver instead of asking MySQL for it
+   * Improved error messages
+   * Improve server group creation/editing
+   * Improve connection/thread killing
+   * Fix broken key bindings and restore previously mapped hot-keys for
+     choosing columns
+   * Some documentation updates (but not nearly enough)
+   * Allow the user to specify graphing char in S mode (formerly G mode)
+   * Allow easy switching between variable sets in S mode
+   * Bind 'n' key globally to choose the 'next' server connection
+   * Bind '%' key globally to filter displayed tables
+   * Allow aligning columns on the decimal place for easy readability
+   * Add hide_hdr config variable to hide column headers in tables
+   * Add a feature to smartly run PURGE MASTER LOGS in Replication mode
+   * Enable debug mode as a globally configurable variable
+   * Improve error messages when an expression or filter doesn't compile or has
+     a run-time error; die on error when debug is enabled
+   * Allow user-configurable delays after executing SQL (to let the server
+     settle down before taking another measurement)
+   * Add an expression to show how long until a transaction is finished
+   * Add skip_innodb as a global config variable
+   * Add '%' after percentages to help disambiguate (user-configurable)
+   * Add column to M mode to help see how fast slave is catching up to master
+
+   BUG FIXES:
+
+   * T and W modes had wrong value for wait_status column
+   * Error tracking on connections didn't reset when the connection recovered
+   * wait_timeout on connections couldn't be set before MySQL 4.0.3
+   * There was a crash on 3.23 when wiping deadlocks
+   * Lettercase changes in some result sets (SHOW MASTER/SLAVE STATUS) between
+     MySQL versions crashed innotop
+   * Inactive connections crashed innotop upon access to DBD driver
+   * set_precision did not respect user defaults for number of digits
+   * --inc command-line option could not be negated
+   * InnoDB status parsing was not always parsing all needed information
+   * S mode (formerly G mode) could crash trying to divide non-numeric data
+   * M table didn't show Slave_open_temp_tables variable; incorrect lettercase
+   * DBD drivers with broken AutoCommit would crash innotop
+   * Some key bindings had incorrect labels
+   * Some config-file loading routines could load data for things that didn't
+     exist
+   * Headers printed too often in S mode
+   * High-resolution time was not used even when the user had it
+   * Non-interactive mode printed blank lines sometimes
+   * Q-mode header and statusbar showed different QPS numbers
+   * Formulas for key-cache and query-cache hit ratios were wrong
+   * Mac OS "Darwin" machines were mis-identified as Microsoft Windows
+   * Some multiplications crashed when given undefined input
+   * The commify transformation did not check its input and could crash
+   * Specifying an invalid mode on the command line or config file could crash
+     innotop
+
+2007-03-29: version 1.4.1
+
+   * More tweaks to display of connection errors.
+   * Fixed a problem with skip-innodb in MySQL 5.1.
+   * Fix a bug with dead connections in single-connection mode.
+   * Fix a regex to allow parsing more data from truncated deadlocks.
+   * Don't load active cxns from the config file if the cxn isn't defined.
+
 2007-03-03: version 1.4.0
 
    * Further tweak error handling and display of connection errors

Modified: branches/sid-5.0/debian/additions/innotop/innotop
===================================================================
--- branches/sid-5.0/debian/additions/innotop/innotop	2007-11-08 18:39:40 UTC (rev 974)
+++ branches/sid-5.0/debian/additions/innotop/innotop	2007-11-08 19:04:38 UTC (rev 975)
@@ -1,8 +1,5 @@
 #!/usr/bin/perl
 
-eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
-    if 0; # not running under some shell
-
 # vim: foldmethod=marker:tw=160:nowrap:expandtab:tabstop=3:shiftwidth=3:softtabstop=3
 # vim users, set modeline to enable auto-folding and compatibility with my preferred
 # formatting.  I use a very wide textwidth because there's tons of configuration
@@ -19,16 +16,21 @@
 use Getopt::Long;
 use List::Util qw(max min maxstr);
 use InnoDBParser;
+use POSIX qw(ceil);
+use Time::HiRes qw(time sleep);
+use Term::ReadKey qw(ReadMode ReadKey);
 
 # Version, license and warranty information. {{{1
 # ###########################################################################
-our $VERSION = '1.4.0';
+our $VERSION = '1.4.3';
+our $SVN_REV = sprintf("%d", q$Revision: 291 $ =~ m/(\d+)/g);
+our $SVN_URL = sprintf("%s", q$URL: https://innotop.svn.sourceforge.net/svnroot/innotop/branches/1.4/innotop $ =~ m/URL: (\S+)/g);
 
 my $innotop_license = <<"LICENSE";
 
 This is innotop version $VERSION, a MySQL and InnoDB monitor.
 
-This program is copyright (c) 2006 Baron Schwartz, baron at xaprb dot com.
+This program is copyright (c) 2006 Baron Schwartz.
 Feedback and improvements are welcome.
 
 THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
@@ -52,6 +54,7 @@
 # Really, really, super-global variables.
 my @config_versions = (
    "000-000-000", "001-003-000", # config file was one big name-value hash.
+   "001-003-000", "001-004-002", # config file contained non-user-defined stuff.
 );
 
 my $clear_screen_sub;
@@ -61,12 +64,12 @@
 my %col_props = (
    hdr   => '',
    just  => '-',
+   dec   => 0, # Whether to align the column on the decimal point
    num   => 0,
    label => '',
    user  => 0,
    src   => '',
    tbl   => '', # Helps when writing/reading custom columns in config files
-   expr  => '', # In case column's src is an expression, the name of the expr
    minw  => 0,
    maxw  => 0,
    trans => [],
@@ -79,39 +82,57 @@
 # Command-line parameters {{{2
 # ###########################################################################
 
-# Define cmdline args; each is spec, config, desc.  Add more hash entries as needed.
-my %opt_spec = (
-   h => { s => 'help|h',                       d => 'Show this help message' },
-   c => { s => 'config|c=s',                   d => 'Config file to read' },
-   n => { s => 'nonint|n',                     d => 'Non-interactive, output tab-separated fields' },
-   m => { s => 'mode|m=s',   config => 'mode', d => 'Operating mode to start in' },
+my @opt_spec = (
+   { s => 'help',       d => 'Show this help message' },
+   { s => 'config|c=s', d => 'Config file to read' },
+   { s => 'nonint|n',   d => 'Non-interactive, output tab-separated fields' },
+   { s => 'count=i',    d => 'Number of updates before exiting' },
+   { s => 'delay|d=f',  d => 'Delay between updates in seconds',  c => 'interval' },
+   { s => 'mode|m=s',   d => 'Operating mode to start in',        c => 'mode' },
+   { s => 'inc|i!',     d => 'Measure incremental differences',   c => 'status_inc' },
+   { s => 'version',    d => 'Output version information and exit' },
 );
 
-# Define the order cmdline opts will appear in help output.  Add any extra ones
-# defined above.
-my @opt_keys = qw( c n m h );
-
 # This is the container for the command-line options' values to be stored in
 # after processing.  Initial values are defaults.
 my %opts = (
    n => !( -t STDIN && -t STDOUT ), # If in/out aren't to terminals, we're interactive
 );
+# Post-process...
+my %opt_seen;
+foreach my $spec ( @opt_spec ) {
+   my ( $long, $short ) = $spec->{s} =~ m/^(\w+)(?:\|([^!+=]*))?/;
+   $spec->{k} = $short || $long;
+   $spec->{l} = $long;
+   $spec->{t} = $short;
+   $spec->{n} = $spec->{s} =~ m/!/;
+   $opts{$spec->{k}} = undef unless defined $opts{$spec->{k}};
+   die "Duplicate option $spec->{k}" if $opt_seen{$spec->{k}}++;
+}
 
 Getopt::Long::Configure('no_ignore_case', 'bundling');
-GetOptions( map { $opt_spec{$_}->{'s'} => \$opts{$_} }  @opt_keys );
+GetOptions( map { $_->{s} => \$opts{$_->{k}} } @opt_spec) or $opts{help} = 1;
 
-if ( $opts{'h'} ) {
-   print "Usage: innotop <options>\n\nOptions:\n\n";
-   foreach my $key ( @opt_keys ) {
-      my ( $long, $short ) = $opt_spec{$key}->{'s'} =~ m/^(\w+)(?:\|([^=]*))?/;
-      $long  = "--$long" . ( $short ? ',' : '' );
-      $short = $short ? " -$short" : '';
-      printf("  %-13s %-4s %s\n", $long, $short, $opt_spec{$key}->{'d'});
+if ( $opts{version} ) {
+   print "innotop  Ver $VERSION Changeset $SVN_REV from $SVN_URL\n";
+   exit(0);
+}
+
+if ( $opts{'help'} ) {
+   print "Usage: innotop <options>\n\n";
+   my $maxw = max(map { length($_->{l}) + ($_->{n} ? 4 : 0)} @opt_spec);
+   foreach my $spec ( sort { $a->{l} cmp $b->{l} } @opt_spec ) {
+      my $long  = $spec->{n} ? "[no]$spec->{l}" : $spec->{l};
+      my $short = $spec->{t} ? "-$spec->{t}" : '';
+      printf("  --%-${maxw}s %-4s %s\n", $long, $short, $spec->{d});
    }
    print <<USAGE;
 
-innotop connects to a MySQL database and displays information from it so you can
-monitor its status, such as what queries are running.
+innotop is a MySQL and InnoDB transaction/status monitor, like 'top' for
+MySQL.  It displays queries, InnoDB transactions, lock waits, deadlocks,
+foreign key errors, open tables, replication status, buffer information,
+row operations, logs, I/O operations, load graph, and more.  You can
+monitor many servers at once with innotop. 
 
 USAGE
    exit(1);
@@ -121,39 +142,21 @@
 # ###########################################################################
 
 # Expressions {{{3
-# Each expression looks like this when fully hydrated:
-# Name => { func => sub{ return 1 }, text => 'return 1', user => 1 }
-#   * The text is the plain text of the expression
-#   * The func is that text, compiled into a subroutine
-#   * The user is whether it's user-defined, and hence needs writing to config
-# So the loading is exactly the same whether user-defined or built-in, the
-# expressions aren't initially stored here; they are initially stored in a
-# hash that looks just like what comes out of the config file.  Those are
-# hydrated with the compile_expr() function.
+# Convenience so I can copy/paste these in several places...
 # ###########################################################################
-my %exprs         = ();
-my %builtin_exprs = (
-   # TODO remove more of these.
-   Host              => q{my $host = $set->{Host} || $set->{hostname} || ''; ($host) = $host =~ m/^((?:[\d.]+(?=:))|(?:[a-zA-Z]\w+))/; return $host || ''},
-   HostAndDomain     => q{my $host = $set->{Host} || $set->{hostname} || ''; ($host) = $host =~ m/^([^:]+)/; return $host || ''},
-   Port              => q{my ( $port ) = $set->{Host} =~ m/:(.*)$/; return $port || 0},
-   QPS               => q{$set->{Uptime_hires} ? $set->{Questions} / ($set->{Uptime_hires} || 1) : 0},
-   ReplByteLag       => q{defined $set->{Master_Log_File} && $set->{Master_Log_File} eq $set->{Relay_Master_Log_File} ? $set->{Read_Master_Log_Pos} - $set->{Exec_Master_Log_Pos} : 0},
-   OldVersions       => q{dulint_to_int($set->{IB_tx_trx_id_counter}) - dulint_to_int($set->{IB_tx_purge_done_for})},
-   MaxTxnTime        => q{max(map{ $_->{active_secs} } @{$set->{IB_tx_transactions}}) || 0},
-   NumTxns           => q{scalar @{$set->{IB_tx_transactions}} },
-   DirtyBufs         => q{ $set->{IB_bp_pages_modified} / ($set->{IB_bp_buf_pool_size} || 1) },
-   BufPoolFill       => q{ $set->{IB_bp_pages_total} / ($set->{IB_bp_buf_pool_size} || 1) },
+my %exprs = (
+   Host              => q{my $host = host || hostname || ''; ($host) = $host =~ m/^((?:[\d.]+(?=:))|(?:[a-zA-Z]\w+))/; return $host || ''},
+   Port              => q{my ($p) = host =~ m/:(.*)$/; return $p || 0},
+   OldVersions       => q{dulint_to_int(IB_tx_trx_id_counter) - dulint_to_int(IB_tx_purge_done_for)},
+   MaxTxnTime        => q/max(map{ $_->{active_secs} } @{ IB_tx_transactions }) || 0/,
+   NumTxns           => q{scalar @{ IB_tx_transactions } },
+   DirtyBufs         => q{ $cur->{IB_bp_pages_modified} / ($cur->{IB_bp_buf_pool_size} || 1) },
+   BufPoolFill       => q{ $cur->{IB_bp_pages_total} / ($cur->{IB_bp_buf_pool_size} || 1) },
+   ServerLoad        => q{ $cur->{Threads_connected}/(Questions||1)/Uptime_hires },
+   TxnTimeRemain     => q{ defined undo_log_entries && defined $pre->{undo_log_entries} && undo_log_entries < $pre->{undo_log_entries} ? undo_log_entries / (($pre->{undo_log_entries} - undo_log_entries)/((active_secs-$pre->{active_secs})||1))||1 : 0},
+   SlaveCatchupRate  => ' defined $cur->{seconds_behind_master} && defined $pre->{seconds_behind_master} && $cur->{seconds_behind_master} < $pre->{seconds_behind_master} ? ($pre->{seconds_behind_master}-$cur->{seconds_behind_master})/($cur->{Uptime_hires}-$pre->{Uptime_hires}) : 0',
+   QcacheHitRatio    => q{(Qcache_hits||0)/(((Com_select||0)+(Qcache_hits||0))||1)},
 );
-foreach my $key ( keys %builtin_exprs ) {
-   my ( $sub, $err ) = compile_expr($builtin_exprs{$key});
-   $exprs{$key} = {
-      func => $sub,
-      text => $builtin_exprs{$key},
-      user => 0,
-      name => $key, # useful for later
-   }
-}
 
 # ###########################################################################
 # Column definitions {{{3
@@ -173,6 +176,7 @@
    add_pool_alloc              => { hdr => 'Add\'l Pool',         num => 1, label => 'Additonal pool allocated' },
    attempted_op                => { hdr => 'Action',              num => 0, label => 'The action that caused the error' },
    awe_mem_alloc               => { hdr => 'AWE Memory',          num => 1, label => '[Windows] AWE memory allocated' },
+   binlog_cache_overflow       => { hdr => 'Binlog Cache',        num => 1, label => 'Transactions too big for binlog cache that went to disk' },
    binlog_do_db                => { hdr => 'Binlog Do DB',        num => 0, label => 'binlog-do-db setting' },
    binlog_ignore_db            => { hdr => 'Binlog Ignore DB',    num => 0, label => 'binlog-ignore-db setting' },
    bps_in                      => { hdr => 'BpsIn',               num => 1, label => 'Bytes per second received by the server', },
@@ -193,6 +197,8 @@
    connect_retry               => { hdr => 'Connect Retry',       num => 1, label => 'Slave connect-retry timeout' },
    cxn                         => { hdr => 'CXN',                 num => 0, label => 'Connection from which the data came', },
    db                          => { hdr => 'DB',                  num => 0, label => 'Current database', },
+   dict_mem_alloc              => { hdr => 'Dict Mem',            num => 1, label => 'Dictionary memory allocated' },
+   dirty_bufs                  => { hdr => 'Dirty Buf',           num => 1, label => 'Dirty buffer pool pages' },
    dl_txn_num                  => { hdr => 'Num',                 num => 0, label => 'Deadlocked transaction number', },
    event_set                   => { hdr => 'Evt Set?',            num => 1, label => '[Win32] if a wait event is set', },
    exec_master_log_pos         => { hdr => 'Exec Master Log Pos', num => 1, label => 'Exec Master Log Position' },
@@ -203,9 +209,9 @@
    hash_table_size             => { hdr => 'Size',                num => 1, label => 'Number of non-hash searches/sec' },
    heap_no                     => { hdr => 'Heap',                num => 1, label => 'Heap number' },
    heap_size                   => { hdr => 'Heap',                num => 1, label => 'Heap size' },
+   history_list_len            => { hdr => 'History',             num => 1, label => 'History list length' },
    host_and_domain             => { hdr => 'Host',                num => 0, label => 'Hostname/IP and domain' },
    host_and_port               => { hdr => 'Host/IP',             num => 0, label => 'Hostname or IP address, and port number', },
-   host_or_ip                  => { hdr => 'Host',                num => 0, label => 'Hostname or IP address', },
    hostname                    => { hdr => 'Host',                num => 0, label => 'Hostname' },
    index                       => { hdr => 'Index',               num => 0, label => 'The index involved' },
    index_ref                   => { hdr => 'Index Ref',           num => 0, label => 'Index referenced' },
@@ -228,6 +234,7 @@
    last_s_line                 => { hdr => 'S-Line',              num => 1, label => 'Line where last read locked' },
    last_x_file_name            => { hdr => 'X-File',              num => 0, label => 'Filename where last write locked' },
    last_x_line                 => { hdr => 'X-Line',              num => 1, label => 'Line where last write locked' },
+   load                        => { hdr => 'Load',                num => 1, label => 'Server load' },
    lock_cfile_name             => { hdr => 'Crtd File',           num => 0, label => 'Filename where lock created' },
    lock_cline                  => { hdr => 'Crtd Line',           num => 1, label => 'Line where lock created' },
    lock_mem_addr               => { hdr => 'Addr',                num => 0, label => 'The lock memory address' },
@@ -255,6 +262,7 @@
    master_ssl_cipher           => { hdr => 'Master SSL Cipher',   num => 0, label => 'Master SSL Cipher' },
    master_ssl_key              => { hdr => 'Master SSL Key',      num => 0, label => 'Master SSL Key' },
    master_user                 => { hdr => 'Master User',         num => 0, label => 'Master username' },
+   max_txn                     => { hdr => 'MaxTxnTime',          num => 1, label => 'MaxTxn' },
    merged_recs                 => { hdr => 'Merged Recs',         num => 1, label => 'Merged records' },
    merges                      => { hdr => 'Merges',              num => 1, label => 'Merges' },
    mutex_os_waits              => { hdr => 'Waits',               num => 1, label => 'Mutex OS Waits' },
@@ -267,13 +275,14 @@
    num_deletes_sec             => { hdr => 'Del/Sec',             num => 1, label => 'Number of deletes' },
    num_inserts                 => { hdr => 'Ins',                 num => 1, label => 'Number of inserts' },
    num_inserts_sec             => { hdr => 'Ins/Sec',             num => 1, label => 'Number of inserts' },
-   num_locks                   => { hdr => 'Num Lcks',            num => 1, label => 'Number of locks' },
+   num_locks                   => { hdr => 'Row Locks',           num => 1, label => 'Number of locks' },
    num_readers                 => { hdr => 'Readers',             num => 1, label => 'Number of readers' },
    num_reads                   => { hdr => 'Read',                num => 1, label => 'Number of reads' },
    num_reads_sec               => { hdr => 'Read/Sec',            num => 1, label => 'Number of reads' },
    num_res_ext                 => { hdr => 'BTree Extents',       num => 1, label => 'Number of extents reserved for B-Tree' },
    num_rows                    => { hdr => 'Row Count',           num => 1, label => 'Number of rows estimated to examine' },
    num_times_open              => { hdr => 'In Use',              num => 1, label => '# times table is opened', },
+   num_txns                    => { hdr => 'Txns',                num => 1, label => 'Number of transactions' },
    num_updates                 => { hdr => 'Upd',                 num => 1, label => 'Number of updates' },
    num_updates_sec             => { hdr => 'Upd/Sec',             num => 1, label => 'Number of updates' },
    os_file_reads               => { hdr => 'OS Reads',            num => 1, label => 'OS file reads' },
@@ -332,7 +341,7 @@
    replicate_wild_ignore_table => { hdr => 'Wild Ignore Table',   num => 0, label => 'Replicate-wild-ignore-table setting' },
    request_type                => { hdr => 'Type',                num => 0, label => 'Type of lock the thread waits for' },
    reservation_count           => { hdr => 'ResCnt',              num => 1, label => 'Reservation Count' },
-   row_header                  => { hdr => 'What',                num => 0, label => 'Row header' },
+   row_locks                   => { hdr => 'RLocks',              num => 1, label => 'Number of row locks' },
    rw_excl_os_waits            => { hdr => 'RW Waits',            num => 1, label => 'R/W Excl. OS Waits' },
    rw_excl_spins               => { hdr => 'RW Spins',            num => 1, label => 'R/W Excl. Spins' },
    rw_shared_os_waits          => { hdr => 'Sh Waits',            num => 1, label => 'R/W Shared OS Waits' },
@@ -343,6 +352,7 @@
    signal_count                => { hdr => 'Signals',             num => 1, label => 'Signal Count' },
    size                        => { hdr => 'Size',                num => 1, label => 'Size of the tablespace' },
    skip_counter                => { hdr => 'Skip Counter',        num => 1, label => 'Skip counter' },
+   slave_catchup_rate          => { hdr => 'Catchup',             num => 1, label => 'How fast the slave is catching up in the binlog' },
    slave_io_running            => { hdr => 'Slave-IO',            num => 0, label => 'Whether the slave I/O thread is running' },
    slave_io_state              => { hdr => 'Slave IO State',      num => 0, label => 'Slave I/O thread state' },
    slave_open_temp_tables      => { hdr => 'Temp',                num => 1, label => 'Slave open temp tables' },
@@ -350,7 +360,7 @@
    slow                        => { hdr => 'Slow',                num => 1, label => 'How many slow queries', },
    space_id                    => { hdr => 'Space',               num => 1, label => 'Tablespace ID' },
    special                     => { hdr => 'Special',             num => 0, label => 'Special/Other info' },
-   state                       => { hdr => 'State',               num => 0, label => 'Connection state', },
+   state                       => { hdr => 'State',               num => 0, label => 'Connection state', maxw => 18, },
    tables_in_use               => { hdr => 'Tbl Used',            num => 1, label => 'Number of tables in use' },
    tables_locked               => { hdr => 'Tbl Lck',             num => 1, label => 'Number of tables locked' },
    tbl                         => { hdr => 'Table',               num => 0, label => 'Table', },
@@ -367,15 +377,19 @@
    txn_id                      => { hdr => 'ID',                  num => 0, label => 'Transaction ID' },
    txn_sees_lt                 => { hdr => 'Txn Sees',            num => 1, label => 'Where txn read view is limited' },
    txn_status                  => { hdr => 'Txn Status',          num => 0, label => 'Transaction status' },
+   txn_time_remain             => { hdr => 'Remaining',           num => 1, label => 'Time until txn rollback/commit completes' },
    undo_log_entries            => { hdr => 'Undo',                num => 1, label => 'Number of undo log entries' },
+   undo_for                    => { hdr => 'Undo',                num => 0, label => 'Undo for' },
    until_condition             => { hdr => 'Until Condition',     num => 0, label => 'Slave until condition' },
    until_log_file              => { hdr => 'Until Log File',      num => 0, label => 'Slave until log file' },
    until_log_pos               => { hdr => 'Until Log Pos',       num => 1, label => 'Slave until log position' },
    used_cells                  => { hdr => 'Cells Used',          num => 1, label => 'Number of cells used' },
+   used_bufs                   => { hdr => 'Used Bufs',           num => 1, label => 'Number of buffer pool pages used' },
    user                        => { hdr => 'User',                num => 0, label => 'Database username', },
+   versions                    => { hdr => 'Versions',            num => 1, label => 'Number of InnoDB MVCC versions unpurged' },
    victim                      => { hdr => 'Victim',              num => 0, label => 'Whether this txn was the deadlock victim' },
    wait_array_size             => { hdr => 'Wait Array Size',     num => 1, label => 'Wait Array Size' },
-   wait_status                 => { hdr => 'Lock Wait?',          num => 0, label => 'Whether txn is waiting for a lock' },
+   wait_status                 => { hdr => 'Lock Status',         num => 0, label => 'Status of txn locks' },
    waited_at_filename          => { hdr => 'File',                num => 0, label => 'Filename at which thread waits' },
    waited_at_line              => { hdr => 'Line',                num => 1, label => 'Line at which thread waits' },
    waiters_flag                => { hdr => 'Waiters',             num => 1, label => 'Waiters Flag' },
@@ -399,7 +413,7 @@
 # Filters {{{3
 # This hash defines every filter that can be applied to a table.  These
 # become part of tbl_meta as well.  Each filter is just an expression that
-# returns true or false, just like values in %exprs.
+# returns true or false.
 # Properties of each entry:
 #  * func:   the subroutine
 #  * name:   the name, repeated
@@ -432,7 +446,7 @@
          return !$set->{state} || $set->{state} !~ m/^(?:Waiting for master|Has read all relay)/;
       END
       note => 'Removes slave I/O threads from the list',
-      tbls => [qw(slave_io_status)],
+      tbls => [qw(processlist slave_io_status)],
    },
    table_is_open => {
       text => <<'      END',
@@ -476,43 +490,139 @@
 }
 
 # Variable sets {{{3
-# Sets (arrayrefs) of variables that are used in V, S, G mode.  They are read/written to
+# Sets (arrayrefs) of variables that are used in S mode.  They are read/written to
 # the config file.
 my %var_sets = (
-   general => [ qw( Uptime Questions Com_delete Com_delete_multi Com_insert
-            Com_insert_select Com_replace Com_replace_select Com_select
-            Com_update Com_update_multi ) ],
-   query_status => [ qw( Uptime Select_full_join Select_full_range_join Select_range Select_range_check
-            Select_scan Slow_queries Sort_merge_passes Sort_range Sort_rows Sort_scan) ],
-   innodb => [ qw( Uptime Innodb_row_lock_current_waits Innodb_row_lock_time
-            Innodb_row_lock_time_avg Innodb_row_lock_time_max
-            Innodb_row_lock_waits Innodb_rows_deleted Innodb_rows_inserted
-            Innodb_rows_read Innodb_rows_updated) ],
-   txn => [ qw( Uptime Com_begin Com_commit Com_rollback Com_savepoint
-            Com_xa_commit Com_xa_end Com_xa_prepare Com_xa_recover
-            Com_xa_rollback Com_xa_start) ],
-   key_cache => [ qw( Uptime Key_blocks_not_flushed Key_blocks_unused
-            Key_blocks_used Key_read_requests Key_reads Key_write_requests
-            Key_writes ) ],
-   query_cache => [ qw( Uptime Qcache_free_blocks Qcache_free_memory Qcache_hits
-            Qcache_inserts Qcache_lowmem_prunes Qcache_not_cached
-            Qcache_queries_in_cache Qcache_total_blocks ) ],
-   handler => [ qw( Uptime Handler_read_key Handler_read_first Handler_read_next
-            Handler_read_prev Handler_read_rnd Handler_read_rnd_next
-            Handler_delete Handler_update Handler_write) ],
-   cxns_files_threads => [ qw( Uptime Aborted_clients Aborted_connects Bytes_received
-            Bytes_sent Compression Connections Created_tmp_disk_tables
-            Created_tmp_files Created_tmp_tables Max_used_connections
-            Open_files Open_streams Open_tables Opened_tables
-            Table_locks_immediate Table_locks_waited Threads_cached
-            Threads_connected Threads_created Threads_running) ],
-   prep_stmt => [ qw( Uptime Com_dealloc_sql Com_execute_sql Com_prepare_sql
-            Com_reset Com_stmt_close Com_stmt_execute Com_stmt_fetch
-            Com_stmt_prepare Com_stmt_reset Com_stmt_send_long_data ) ],
-   innodb_health => [ qw(OldVersions IB_sm_mutex_spin_waits IB_sm_mutex_spin_rounds
-            IB_sm_mutex_os_waits NumTxns MaxTxnTime IB_ro_queries_inside IB_ro_queries_in_queue
-            DirtyBufs BufPoolFill IB_bp_pages_total IB_bp_pages_read IB_bp_pages_written
-            IB_bp_pages_created) ],
+   general => {
+      text => join(
+         ', ',
+         'set_precision(Questions/Uptime_hires) as QPS',
+         'set_precision(Com_commit/Uptime_hires) as Commit_PS',
+         'set_precision((Com_rollback||0)/(Com_commit||1)) as Rollback_Commit',
+         'set_precision(('
+            . join('+', map { "($_||0)" }
+               qw(Com_delete Com_delete_multi Com_insert Com_insert_select Com_replace
+                  Com_replace_select Com_select Com_update Com_update_multi))
+            . ')/(Com_commit||1)) as Write_Commit',
+         'set_precision((Com_select+(Qcache_hits||0))/(('
+            . join('+', map { "($_||0)" }
+               qw(Com_delete Com_delete_multi Com_insert Com_insert_select Com_replace
+                  Com_replace_select Com_select Com_update Com_update_multi))
+            . ')||1)) as R_W_Ratio',
+         'set_precision(Opened_tables/Uptime_hires) as Opens_PS',
+         'percent($cur->{Open_tables}/($cur->{table_cache})) as Table_Cache_Used',
+         'set_precision(Threads_created/Uptime_hires) as Threads_PS',
+         'percent($cur->{Threads_cached}/($cur->{thread_cache_size}||1)) as Thread_Cache_Used',
+         'percent($cur->{Max_used_connections}/($cur->{max_connections}||1)) as CXN_Used_Ever',
+         'percent($cur->{Threads_connected}/($cur->{max_connections}||1)) as CXN_Used_Now',
+      ),
+   },
+   commands => {
+      text => join(
+         ', ',
+         qw(Uptime Questions Com_delete Com_delete_multi Com_insert
+         Com_insert_select Com_replace Com_replace_select Com_select Com_update
+         Com_update_multi)
+      ),
+   },
+   query_status => {
+      text => join(
+         ',',
+         qw( Uptime Select_full_join Select_full_range_join Select_range
+         Select_range_check Select_scan Slow_queries Sort_merge_passes
+         Sort_range Sort_rows Sort_scan)
+      ),
+   },
+   innodb => {
+      text => join(
+         ',',
+         qw( Uptime Innodb_row_lock_current_waits Innodb_row_lock_time
+         Innodb_row_lock_time_avg Innodb_row_lock_time_max Innodb_row_lock_waits
+         Innodb_rows_deleted Innodb_rows_inserted Innodb_rows_read
+         Innodb_rows_updated)
+      ),
+   },
+   txn => {
+      text => join(
+         ',',
+         qw( Uptime Com_begin Com_commit Com_rollback Com_savepoint
+         Com_xa_commit Com_xa_end Com_xa_prepare Com_xa_recover Com_xa_rollback
+         Com_xa_start)
+      ),
+   },
+   key_cache => {
+      text => join(
+         ',',
+         qw( Uptime Key_blocks_not_flushed Key_blocks_unused Key_blocks_used
+         Key_read_requests Key_reads Key_write_requests Key_writes )
+      ),
+   },
+   query_cache => {
+      text => join(
+         ',',
+         "percent($exprs{QcacheHitRatio}) as Hit_Pct",
+         'set_precision((Qcache_hits||0)/(Qcache_inserts||1)) as Hit_Ins',
+         'set_precision((Qcache_lowmem_prunes||0)/Uptime_hires) as Lowmem_Prunes_sec',
+         'percent(1-((Qcache_free_blocks||0)/(Qcache_total_blocks||1))) as Blocks_used',
+         qw( Qcache_free_blocks Qcache_free_memory Qcache_not_cached Qcache_queries_in_cache)
+      ),
+   },
+   handler => {
+      text => join(
+         ',',
+         qw( Uptime Handler_read_key Handler_read_first Handler_read_next
+         Handler_read_prev Handler_read_rnd Handler_read_rnd_next Handler_delete
+         Handler_update Handler_write)
+      ),
+   },
+   cxns_files_threads => {
+      text => join(
+         ',',
+         qw( Uptime Aborted_clients Aborted_connects Bytes_received Bytes_sent
+         Compression Connections Created_tmp_disk_tables Created_tmp_files
+         Created_tmp_tables Max_used_connections Open_files Open_streams
+         Open_tables Opened_tables Table_locks_immediate Table_locks_waited
+         Threads_cached Threads_connected Threads_created Threads_running)
+      ),
+   },
+   prep_stmt => {
+      text => join(
+         ',',
+         qw( Uptime Com_dealloc_sql Com_execute_sql Com_prepare_sql Com_reset
+         Com_stmt_close Com_stmt_execute Com_stmt_fetch Com_stmt_prepare
+         Com_stmt_reset Com_stmt_send_long_data )
+      ),
+   },
+   innodb_health => {
+      text => join(
+         ',',
+         "$exprs{OldVersions} as OldVersions",
+         qw(IB_sm_mutex_spin_waits IB_sm_mutex_spin_rounds IB_sm_mutex_os_waits),
+         "$exprs{NumTxns} as NumTxns",
+         "$exprs{MaxTxnTime} as MaxTxnTime",
+         qw(IB_ro_queries_inside IB_ro_queries_in_queue),
+         "set_precision($exprs{DirtyBufs} * 100) as dirty_bufs",
+         "set_precision($exprs{BufPoolFill} * 100) as buf_fill",
+         qw(IB_bp_pages_total IB_bp_pages_read IB_bp_pages_written IB_bp_pages_created)
+      ),
+   },
+   innodb_health2 => {
+      text => join(
+         ', ',
+         'percent(1-((Innodb_buffer_pool_pages_free||0)/($cur->{Innodb_buffer_pool_pages_total}||1))) as BP_page_cache_usage',
+         'percent(1-((Innodb_buffer_pool_reads||0)/(Innodb_buffer_pool_read_requests||1))) as BP_cache_hit_ratio',
+         'Innodb_buffer_pool_wait_free',
+         'Innodb_log_waits',
+      ),
+   },
+   slow_queries => {
+      text => join(
+         ', ',
+         'set_precision(Slow_queries/Uptime_hires) as Slow_PS',
+         'set_precision(Select_full_join/Uptime_hires) as Full_Join_PS',
+         'percent(Select_full_join/(Com_select||1)) as Full_Join_Ratio',
+      ),
+   },
 );
 
 # Server sets {{{3
@@ -522,9 +632,9 @@
 # Connections {{{3
 # This hash defines server connections.  Each connection is a string that can be passed to
 # the DBI connection.  These are saved in the connections section in the config file.
-# Each has dsn, user, pass, savepass properties.
 my %connections;
-my @conn_parts = qw(user pass dsn savepass dl_table);
+# Defines the parts of connections.
+my @conn_parts = qw(user have_user pass have_pass dsn savepass dl_table);
 
 # Graph widths {{{3
 # This hash defines the max values seen for various status/variable values, for graphing.
@@ -537,6 +647,34 @@
    Questions    => 100,
 );
 
+# ###########################################################################
+# Valid Term::ANSIColor color strings.
+# ###########################################################################
+my %ansicolors = map { $_ => 1 }
+   qw( black blink blue bold clear concealed cyan dark green magenta on_black
+       on_blue on_cyan on_green on_magenta on_red on_white on_yellow red reset
+       reverse underline underscore white yellow);
+
+# ###########################################################################
+# Valid comparison operators for color rules
+# ###########################################################################
+my %comp_ops = (
+   '==' => 'Numeric equality',
+   '>'  => 'Numeric greater-than',
+   '<'  => 'Numeric less-than',
+   '>=' => 'Numeric greater-than/equal',
+   '<=' => 'Numeric less-than/equal',
+   '!=' => 'Numeric not-equal',
+   'eq' => 'String equality',
+   'gt' => 'String greater-than',
+   'lt' => 'String less-than',
+   'ge' => 'String greater-than/equal',
+   'le' => 'String less-than/equal',
+   'ne' => 'String not-equal',
+   '=~' => 'Pattern match',
+   '!~' => 'Negated pattern match',
+);
+
 # Table definitions {{{3
 # This hash defines every table that can get displayed in every mode.  Each
 # table specifies columns and column data sources.  The column is
@@ -546,18 +684,16 @@
 # $columns{foo} for its definition) gets its data from the 'bar' element of
 # the current data set, whatever that is.
 #
-# Example 2: biz => { src => \%exprs{bat} } means the expression is
-# evaluated for the current data set.
-#
 # These columns are post-processed after being defined, because they get stuff
 # from %columns.  After all the config is loaded for columns, there's more
-# post-processing too; the subroutines compiled from src and expr get added to
+# post-processing too; the subroutines compiled from src get added to
 # the hash elements for extract_values to use.
 # ###########################################################################
 
 my %tbl_meta = (
    adaptive_hash_index => {
-      hdr  => 'Adaptive Hash Index',
+      capt => 'Adaptive Hash Index',
+      cust => {},
       cols => {
          cxn                 => { src => 'cxn' },
          hash_table_size     => { src => 'IB_ib_hash_table_size', trans => [qw(shorten)], },
@@ -573,7 +709,8 @@
       innodb   => 'ib',
    },
    buffer_pool => {
-      hdr  => 'Buffer Pool',
+      capt => 'Buffer Pool',
+      cust => {},
       cols => {
          cxn                        => { src => 'cxn' },
          total_mem_alloc            => { src => 'IB_bp_total_mem_alloc', trans => [qw(shorten)], },
@@ -584,6 +721,7 @@
          buf_pool_hit_rate          => { src => 'IB_bp_buf_pool_hit_rate' },
          buf_pool_reads             => { src => 'IB_bp_buf_pool_reads' },
          buf_pool_hits              => { src => 'IB_bp_buf_pool_hits' },
+         dict_mem_alloc             => { src => 'IB_bp_dict_mem_alloc' },
          pages_total                => { src => 'IB_bp_pages_total' },
          pages_modified             => { src => 'IB_bp_pages_modified' },
          reads_pending              => { src => 'IB_bp_reads_pending' },
@@ -605,7 +743,8 @@
       innodb   => 'bp',
    },
    deadlock_locks => {
-      hdr  => 'Deadlock Locks',
+      capt => 'Deadlock Locks',
+      cust => {},
       cols => {
          cxn              => { src => 'cxn' },
          mysql_thread_id  => { src => 'mysql_thread_id' },
@@ -632,7 +771,8 @@
       innodb   => 'dl',
    },
    deadlock_transactions => {
-      hdr  => 'Deadlock Transactions',
+      capt => 'Deadlock Transactions',
+      cust => {},
       cols => {
          cxn                => { src => 'cxn' },
          active_secs        => { src => 'active_secs' },
@@ -640,7 +780,7 @@
          has_read_view      => { src => 'has_read_view' },
          heap_size          => { src => 'heap_size' },
          host_and_domain    => { src => 'hostname' },
-         hostname           => { src => $exprs{Host}, expr => 'Host' },
+         hostname           => { src => $exprs{Host} },
          ip                 => { src => 'ip' },
          lock_structs       => { src => 'lock_structs' },
          lock_wait_time     => { src => 'lock_wait_time', trans => [ qw(secs_to_time) ] },
@@ -650,6 +790,7 @@
          query_id           => { src => 'query_id' },
          query_status       => { src => 'query_status' },
          query_text         => { src => 'query_text', trans => [ qw(no_ctrl_char) ] },
+         row_locks          => { src => 'row_locks' },
          tables_in_use      => { src => 'tables_in_use' },
          tables_locked      => { src => 'tables_locked' },
          thread_decl_inside => { src => 'thread_decl_inside' },
@@ -664,7 +805,7 @@
          undo_log_entries   => { src => 'undo_log_entries' },
          user               => { src => 'user' },
          victim             => { src => 'victim' },
-         wait_status        => { src => 'wait_status' },
+         wait_status        => { src => 'lock_wait_status' },
       },
       visible => [ qw(cxn mysql_thread_id timestring user hostname victim time undo_log_entries lock_structs query_text)],
       filters => [],
@@ -673,7 +814,8 @@
       innodb   => 'dl',
    },
    explain => {
-      hdr  => 'EXPLAIN Results',
+      capt => 'EXPLAIN Results',
+      cust => {},
       cols => {
          part_id       => { src => 'id' },
          select_type   => { src => 'select_type' },
@@ -685,7 +827,7 @@
          key_len       => { src => 'key_len' },
          index_ref     => { src => 'ref' },
          num_rows      => { src => 'rows' },
-         special       => { src => 'Extra' },
+         special       => { src => 'extra' },
       },
       visible => [ qw(select_type tbl partitions scan_type possible_keys index key_len index_ref num_rows special)],
       filters => [],
@@ -694,7 +836,8 @@
       innodb   => '',
    },
    file_io_misc => {
-      hdr  => 'File I/O Misc',
+      capt => 'File I/O Misc',
+      cust => {},
       cols => {
          cxn            => { src => 'cxn' },
          io_bytes_s     => { src => 'IB_io_avg_bytes_s' },
@@ -713,7 +856,8 @@
       innodb   => 'io',
    },
    fk_error => {
-      hdr  => 'Foreign Key Error Info',
+      capt => 'Foreign Key Error Info',
+      cust => {},
       cols => {
          timestring   => { src => 'IB_fk_timestring' },
          child_db     => { src => 'IB_fk_child_db' },
@@ -733,7 +877,8 @@
       innodb   => 'fk',
    },
    insert_buffers => {
-      hdr  => 'Insert Buffers',
+      capt => 'Insert Buffers',
+      cust => {},
       cols => {
          cxn           => { src => 'cxn' },
          inserts       => { src => 'IB_ib_inserts' },
@@ -750,15 +895,16 @@
       innodb   => 'ib',
    },
    innodb_transactions => {
-      hdr  => 'InnoDB Transactions',
+      capt => 'InnoDB Transactions',
+      cust => {},
       cols => {
          cxn                => { src => 'cxn' },
          active_secs        => { src => 'active_secs' },
          has_read_view      => { src => 'has_read_view' },
          heap_size          => { src => 'heap_size' },
-         hostname           => { src => $exprs{Host}, expr => 'Host' },
+         hostname           => { src => $exprs{Host} },
          ip                 => { src => 'ip' },
-         wait_status        => { src => 'wait_status' },
+         wait_status        => { src => 'lock_wait_status' },
          lock_wait_time     => { src => 'lock_wait_time', trans => [ qw(secs_to_time) ] },
          lock_structs       => { src => 'lock_structs' },
          mysql_thread_id    => { src => 'mysql_thread_id' },
@@ -767,6 +913,8 @@
          query_id           => { src => 'query_id' },
          query_status       => { src => 'query_status' },
          query_text         => { src => 'query_text', trans => [ qw(no_ctrl_char) ]  },
+         txn_time_remain    => { src => $exprs{TxnTimeRemain}, trans => [ qw(secs_to_time) ] },
+         row_locks          => { src => 'row_locks' },
          tables_in_use      => { src => 'tables_in_use' },
          tables_locked      => { src => 'tables_locked' },
          thread_decl_inside => { src => 'thread_decl_inside' },
@@ -784,17 +932,19 @@
       sort_cols => '-active_secs txn_status cxn mysql_thread_id',
       sort_dir => '1',
       innodb   => 'tx',
-      hide_hdr => 1,
+      hide_caption => 1,
       colors   => [
-         { col => 'wait_status', op => '>',  arg => 0,             color => 'black on_red' },
+         { col => 'wait_status', op => 'eq', arg => 'LOCK WAIT',   color => 'black on_red' },
          { col => 'time',        op => '>',  arg => 600,           color => 'red' },
          { col => 'time',        op => '>',  arg => 300,           color => 'yellow' },
-         { col => 'time',        op => '>',  arg => 30,            color => 'green' },
+         { col => 'time',        op => '>',  arg => 60,            color => 'green' },
+         { col => 'time',        op => '>',  arg => 30,            color => 'cyan' },
          { col => 'txn_status',  op => 'eq', arg => 'not started', color => 'white' },
       ],
    },
    io_threads => {
-      hdr  => 'I/O Threads',
+      capt => 'I/O Threads',
+      cust => {},
       cols => {
          cxn            => { src => 'cxn' },
          thread         => { src => 'thread' },
@@ -809,7 +959,8 @@
       innodb   => 'io',
    },
    lock_waits => {
-      hdr  => 'Lock Waits',
+      capt => 'Lock Waits',
+      cust => {},
       cols => {
          cxn              => { src => 'cxn' },
          db               => { src => 'db' },
@@ -833,9 +984,15 @@
       sort_cols => 'cxn -lock_wait_time',
       sort_dir => '1',
       innodb   => 'tx',
+      colors   => [
+         { col => 'lock_wait_time', op => '>',  arg => 60, color => 'red' },
+         { col => 'lock_wait_time', op => '>',  arg => 30, color => 'yellow' },
+         { col => 'lock_wait_time', op => '>',  arg => 10, color => 'green' },
+      ],
    },
    log_statistics => {
-      hdr  => 'Log Statistics',
+      capt => 'Log Statistics',
+      cust => {},
       cols => {
          cxn                 => { src => 'cxn' },
          last_chkp           => { src => 'IB_lg_last_chkp' },
@@ -853,22 +1010,25 @@
       innodb   => 'lg',
    },
    master_status => {
-      hdr  => 'Master Status',
+      capt => 'Master Status',
+      cust => {},
       cols => {
          cxn                         => { src => 'cxn' },
-         binlog_do_db                => { src => 'Binlog_Do_DB' },
-         binlog_ignore_db            => { src => 'Binlog_Ignore_DB' },
-         master_file                 => { src => 'File' },
-         master_pos                  => { src => 'Position' },
+         binlog_do_db                => { src => 'binlog_do_db' },
+         binlog_ignore_db            => { src => 'binlog_ignore_db' },
+         master_file                 => { src => 'file' },
+         master_pos                  => { src => 'position' },
+         binlog_cache_overflow       => { src => '(Binlog_cache_disk_use||0)/(Binlog_cache_use||1)', trans => [ qw(percent) ] },
       },
-      visible => [ qw(cxn master_file master_pos)],
+      visible => [ qw(cxn master_file master_pos binlog_cache_overflow)],
       filters => [ qw(cxn_is_master) ],
       sort_cols => 'cxn',
       sort_dir => '1',
       innodb   => '',
    },
    pending_io => {
-      hdr  => 'Pending I/O',
+      capt => 'Pending I/O',
+      cust => {},
       cols => {
          cxn                => { src => 'cxn' },
          p_normal_aio_reads => { src => 'IB_io_pending_normal_aio_reads' },
@@ -888,13 +1048,14 @@
       innodb   => 'io',
    },
    open_tables => {
-      hdr  => 'Open Tables',
+      capt => 'Open Tables',
+      cust => {},
       cols => {
          cxn            => { src => 'cxn' },
-         db             => { src => 'Database' },
-         tbl            => { src => 'Table' },
-         num_times_open => { src => 'In_use' },
-         is_name_locked => { src => 'Name_locked' },
+         db             => { src => 'database' },
+         tbl            => { src => 'table' },
+         num_times_open => { src => 'in_use' },
+         is_name_locked => { src => 'name_locked' },
       },
       visible => [ qw(cxn db tbl num_times_open is_name_locked)],
       filters => [ qw(table_is_open) ],
@@ -903,7 +1064,8 @@
       innodb   => '',
    },
    page_statistics => {
-      hdr  => 'Page Statistics',
+      capt => 'Page Statistics',
+      cust => {},
       cols => {
          cxn              => { src => 'cxn' },
          pages_read       => { src => 'IB_bp_pages_read' },
@@ -920,57 +1082,64 @@
       innodb   => 'bp',
    },
    processlist => {
-      hdr  => 'MySQL Process List',
+      capt => 'MySQL Process List',
+      cust => {},
       cols => {
          cxn             => { src => 'cxn',        minw => 6,  maxw => 10 },
-         mysql_thread_id => { src => 'Id',         minw => 6,  maxw => 0 },
-         user            => { src => 'User',       minw => 5,  maxw => 8 },
-         hostname        => { src => $exprs{Host}, minw => 13, maxw => 8, expr => 'Host' },
-         port            => { src => $exprs{Port}, minw => 0,  maxw => 0, expr => 'Port' },
-         host_and_port   => { src => 'Host',       minw => 0,  maxw => 0 },
+         mysql_thread_id => { src => 'id',         minw => 6,  maxw => 0 },
+         user            => { src => 'user',       minw => 5,  maxw => 8 },
+         hostname        => { src => $exprs{Host}, minw => 13, maxw => 8, },
+         port            => { src => $exprs{Port}, minw => 0,  maxw => 0, },
+         host_and_port   => { src => 'host',       minw => 0,  maxw => 0 },
          db              => { src => 'db',         minw => 6,  maxw => 12 },
-         cmd             => { src => 'Command',    minw => 5,  maxw => 0 },
-         time            => { src => 'Time',       minw => 5,  maxw => 0, trans => [ qw(secs_to_time) ] },
-         state           => { src => 'State',      minw => 0,  maxw => 0 },
-         info            => { src => 'Info',       minw => 0,  maxw => 0, trans => [ qw(no_ctrl_char) ] },
+         cmd             => { src => 'command',    minw => 5,  maxw => 0 },
+         time            => { src => 'time',       minw => 5,  maxw => 0, trans => [ qw(secs_to_time) ] },
+         state           => { src => 'state',      minw => 0,  maxw => 0 },
+         info            => { src => 'info',       minw => 0,  maxw => 0, trans => [ qw(no_ctrl_char) ] },
       },
       visible => [ qw(cxn mysql_thread_id user hostname db time info)],
       filters => [ qw(hide_self hide_inactive hide_slave_io) ],
       sort_cols => '-time cxn hostname mysql_thread_id',
       sort_dir => '1',
       innodb   => '',
-      hide_hdr => 1,
+      hide_caption => 1,
       colors   => [
-         { col => 'cmd',         op => 'eq', arg => 'Locked',      color => 'red' },
-         { col => 'cmd',         op => 'eq', arg => 'Query',       color => 'yellow' },
+         { col => 'state',       op => 'eq', arg => 'Locked',      color => 'black on_red' },
          { col => 'cmd',         op => 'eq', arg => 'Sleep',       color => 'white' },
          { col => 'user',        op => 'eq', arg => 'system user', color => 'white' },
-         { col => 'cmd',         op => 'eq', arg => 'Connect',     color => 'green' },
+         { col => 'cmd',         op => 'eq', arg => 'Connect',     color => 'white' },
          { col => 'cmd',         op => 'eq', arg => 'Binlog Dump', color => 'white' },
+         { col => 'time',        op => '>',  arg => 600,           color => 'red' },
+         { col => 'time',        op => '>',  arg => 120,           color => 'yellow' },
+         { col => 'time',        op => '>',  arg => 60,            color => 'green' },
+         { col => 'time',        op => '>',  arg => 30,            color => 'cyan' },
       ],
    },
    q_header => {
-      hdr  => 'Q-mode Header',
+      capt => 'Q-mode Header',
+      cust => {},
       cols => {
          cxn            => { src => 'cxn' },
          questions      => { src => 'Questions' },
-         qps            => { src => 'Questions/Uptime_hires',                       trans => [qw(shorten)] },
-         slow           => { src => 'Slow_queries',                                 trans => [qw(shorten)] },
-         q_cache_hit    => { src => 'Qcache_hits/Com_select',                       trans => [qw(percent)] },
-         key_buffer_hit => { src => '1-(Key_reads/Key_read_requests)',              trans => [qw(percent)] },
-         bps_in         => { src => 'Bytes_received/Uptime_hires',                  trans => [qw(shorten)] },
-         bps_out        => { src => 'Bytes_sent/Uptime_hires',                      trans => [qw(shorten)] },
+         qps            => { src => 'Questions/Uptime_hires',               dec => 1, trans => [qw(shorten)] },
+         load           => { src => $exprs{ServerLoad},                     dec => 1, trans => [qw(shorten)] },
+         slow           => { src => 'Slow_queries',                         dec => 1, trans => [qw(shorten)] },
+         q_cache_hit    => { src => $exprs{QcacheHitRatio},                 dec => 1, trans => [qw(percent)] },
+         key_buffer_hit => { src => '1-(Key_reads/(Key_read_requests||1))', dec => 1, trans => [qw(percent)] },
+         bps_in         => { src => 'Bytes_received/Uptime_hires',          dec => 1, trans => [qw(shorten)] },
+         bps_out        => { src => 'Bytes_sent/Uptime_hires',              dec => 1, trans => [qw(shorten)] },
          when           => { src => 'when' },
       },
-      visible => [ qw(cxn when qps slow q_cache_hit key_buffer_hit bps_in bps_out)],
+      visible => [ qw(cxn when load qps slow q_cache_hit key_buffer_hit bps_in bps_out)],
       filters => [],
       sort_cols => 'when cxn',
       sort_dir => '1',
       innodb   => '',
-      hide_hdr => 1,
+      hide_caption => 1,
    },
    row_operations => {
-      hdr  => 'InnoDB Row Operations',
+      capt => 'InnoDB Row Operations',
+      cust => {},
       cols => {
          cxn         => { src => 'cxn' },
          num_inserts => { src => 'IB_ro_num_rows_ins' },
@@ -990,7 +1159,8 @@
       innodb   => 'ro',
    },
    row_operation_misc => {
-      hdr  => 'Row Operation Misc',
+      capt => 'Row Operation Misc',
+      cust => {},
       cols => {
          cxn                 => { src => 'cxn' },
          queries_in_queue    => { src => 'IB_ro_queries_in_queue' },
@@ -1008,7 +1178,8 @@
       innodb   => 'ro',
    },
    semaphores => {
-      hdr  => 'InnoDB Semaphores',
+      capt => 'InnoDB Semaphores',
+      cust => {},
       cols => {
          cxn                => { src => 'cxn' },
          mutex_os_waits     => { src => 'IB_sm_mutex_os_waits' },
@@ -1031,24 +1202,25 @@
       innodb   => 'sm',
    },
    slave_io_status => {
-      hdr  => 'Slave I/O Status',
+      capt => 'Slave I/O Status',
+      cust => {},
       cols => {
          cxn                         => { src => 'cxn' },
-         connect_retry               => { src => 'Connect_Retry' },
-         master_host                 => { src => 'Master_Host', hdr => 'Master'},
-         master_log_file             => { src => 'Master_Log_File', hdr => 'File' },
-         master_port                 => { src => 'Master_Port' },
-         master_ssl_allowed          => { src => 'Master_SSL_Allowed' },
-         master_ssl_ca_file          => { src => 'Master_SSL_CA_File' },
-         master_ssl_ca_path          => { src => 'Master_SSL_CA_Path' },
-         master_ssl_cert             => { src => 'Master_SSL_Cert' },
-         master_ssl_cipher           => { src => 'Master_SSL_Cipher' },
-         master_ssl_key              => { src => 'Master_SSL_Key' },
-         master_user                 => { src => 'Master_User' },
-         read_master_log_pos         => { src => 'Read_Master_Log_Pos', hdr => 'Pos' },
-         relay_log_size              => { src => 'Relay_Log_Space', trans => [qw(shorten)] },
-         slave_io_running            => { src => 'Slave_IO_Running', hdr => 'On?' },
-         slave_io_state              => { src => 'Slave_IO_State', hdr => 'State' },
+         connect_retry               => { src => 'connect_retry' },
+         master_host                 => { src => 'master_host', hdr => 'Master'},
+         master_log_file             => { src => 'master_log_file', hdr => 'File' },
+         master_port                 => { src => 'master_port' },
+         master_ssl_allowed          => { src => 'master_ssl_allowed' },
+         master_ssl_ca_file          => { src => 'master_ssl_ca_file' },
+         master_ssl_ca_path          => { src => 'master_ssl_ca_path' },
+         master_ssl_cert             => { src => 'master_ssl_cert' },
+         master_ssl_cipher           => { src => 'master_ssl_cipher' },
+         master_ssl_key              => { src => 'master_ssl_key' },
+         master_user                 => { src => 'master_user' },
+         read_master_log_pos         => { src => 'read_master_log_pos', hdr => 'Pos' },
+         relay_log_size              => { src => 'relay_log_space', trans => [qw(shorten)] },
+         slave_io_running            => { src => 'slave_io_running', hdr => 'On?' },
+         slave_io_state              => { src => 'slave_io_state', hdr => 'State' },
       },
       visible => [ qw(cxn master_host slave_io_running master_log_file relay_log_size read_master_log_pos slave_io_state)],
       filters => [ qw( cxn_is_slave ) ],
@@ -1060,33 +1232,35 @@
       innodb   => '',
    },
    slave_sql_status => {
-      hdr  => 'Slave SQL Status',
+      capt => 'Slave SQL Status',
+      cust => {},
       cols => {
          cxn                         => { src => 'cxn' },
-         exec_master_log_pos         => { src => 'Exec_Master_Log_Pos', hdr => 'Master Pos' },
-         last_errno                  => { src => 'Last_Errno' },
-         last_error                  => { src => 'Last_Error' },
-         master_host                 => { src => 'Master_Host', hdr => 'Master' },
-         relay_log_file              => { src => 'Relay_Log_File' },
-         relay_log_pos               => { src => 'Relay_Log_Pos' },
-         relay_log_size              => { src => 'Relay_Log_Space', trans => [qw(shorten)] },
-         relay_master_log_file       => { src => 'Relay_Master_Log_File', hdr => 'Master File' },
-         replicate_do_db             => { src => 'Replicate_Do_DB' },
-         replicate_do_table          => { src => 'Replicate_Do_Table' },
-         replicate_ignore_db         => { src => 'Replicate_Ignore_DB' },
-         replicate_ignore_table      => { src => 'Replicate_Ignore_Table' },
-         replicate_wild_do_table     => { src => 'Replicate_Wild_Do_Table' },
-         replicate_wild_ignore_table => { src => 'Replicate_Wild_Ignore_Table' },
-         skip_counter                => { src => 'Skip_Counter' },
-         slave_sql_running           => { src => 'Slave_SQL_Running', hdr => 'On?' },
-         until_condition             => { src => 'Until_Condition' },
-         until_log_file              => { src => 'Until_Log_File' },
-         until_log_pos               => { src => 'Until_Log_Pos' },
-         time_behind_master          => { src => 'Seconds_Behind_Master', trans => [ qw(secs_to_time) ] },
-         bytes_behind_master         => { src => $exprs{ReplByteLag}, trans => [qw(shorten)], expr => 'ReplByteLag' },
+         exec_master_log_pos         => { src => 'exec_master_log_pos', hdr => 'Master Pos' },
+         last_errno                  => { src => 'last_errno' },
+         last_error                  => { src => 'last_error' },
+         master_host                 => { src => 'master_host', hdr => 'Master' },
+         relay_log_file              => { src => 'relay_log_file' },
+         relay_log_pos               => { src => 'relay_log_pos' },
+         relay_log_size              => { src => 'relay_log_space', trans => [qw(shorten)] },
+         relay_master_log_file       => { src => 'relay_master_log_file', hdr => 'Master File' },
+         replicate_do_db             => { src => 'replicate_do_db' },
+         replicate_do_table          => { src => 'replicate_do_table' },
+         replicate_ignore_db         => { src => 'replicate_ignore_db' },
+         replicate_ignore_table      => { src => 'replicate_ignore_table' },
+         replicate_wild_do_table     => { src => 'replicate_wild_do_table' },
+         replicate_wild_ignore_table => { src => 'replicate_wild_ignore_table' },
+         skip_counter                => { src => 'skip_counter' },
+         slave_sql_running           => { src => 'slave_sql_running', hdr => 'On?' },
+         until_condition             => { src => 'until_condition' },
+         until_log_file              => { src => 'until_log_file' },
+         until_log_pos               => { src => 'until_log_pos' },
+         time_behind_master          => { src => 'seconds_behind_master', trans => [ qw(secs_to_time) ] },
+         bytes_behind_master         => { src => 'master_log_file && master_log_file eq relay_master_log_file ? read_master_log_pos - exec_master_log_pos : 0', trans => [qw(shorten)] },
+         slave_catchup_rate          => { src => $exprs{SlaveCatchupRate}, trans => [ qw(set_precision) ] },
          slave_open_temp_tables      => { src => 'Slave_open_temp_tables' },
       },
-      visible => [ qw(cxn master_host slave_sql_running time_behind_master slave_open_temp_tables relay_log_pos last_error)],
+      visible => [ qw(cxn master_host slave_sql_running time_behind_master slave_catchup_rate slave_open_temp_tables relay_log_pos last_error)],
       filters => [ qw( cxn_is_slave ) ],
       sort_cols => 'slave_sql_running cxn',
       sort_dir => '1',
@@ -1098,8 +1272,44 @@
          { col => 'time_behind_master', op => '==', arg => 0,     color => 'white' },
       ],
    },
+   t_header => {
+      capt => 'T-Mode Header',
+      cust => {},
+      cols => {
+         cxn                         => { src => 'cxn' },
+         dirty_bufs                  => { src => $exprs{DirtyBufs},           trans => [qw(percent)] },
+         history_list_len            => { src => 'IB_tx_history_list_len' },
+         lock_structs                => { src => 'IB_tx_num_lock_structs' },
+         num_txns                    => { src => $exprs{NumTxns} },
+         max_txn                     => { src => $exprs{MaxTxnTime},          trans => [qw(secs_to_time)] },
+         undo_for                    => { src => 'IB_tx_purge_undo_for' },
+         used_bufs                   => { src => $exprs{BufPoolFill},         trans => [qw(percent)]},
+         versions                    => { src => $exprs{OldVersions} },
+      },
+      visible => [ qw(cxn history_list_len versions undo_for dirty_bufs used_bufs num_txns max_txn lock_structs)],
+      filters => [ ],
+      sort_cols => 'cxn',
+      sort_dir => '1',
+      innodb   => '',
+      colors   => [],
+      hide_caption => 1,
+   },
+   var_status => {
+      capt      => 'Variables & Status',
+      cust      => {},
+      cols      => {}, # Generated from current varset
+      visible   => [], # Generated from current varset
+      filters   => [],
+      sort_cols => '',
+      sort_dir  => 1,
+      innodb    => '',
+      temp      => 1, # Do not persist to config file.
+      hide_caption  => 1,
+      pivot     => 0,
+   },
    wait_array => {
-      hdr  => 'InnoDB Wait Array',
+      capt => 'InnoDB Wait Array',
+      cust => {},
       cols => {
          cxn                => { src => 'cxn' },
          thread             => { src => 'thread' },
@@ -1130,13 +1340,15 @@
    },
 );
 
-# TODO: V, G, S mode should have a table in tbl_meta
+# Initialize %tbl_meta from %columns and do some checks.
+foreach my $table_name ( keys %tbl_meta ) {
+   my $table = $tbl_meta{$table_name};
+   my $cols  = $table->{cols};
 
-# Initialize %tbl_meta from %columns
-foreach my $table ( values %tbl_meta ) {
-   foreach my $col_name ( keys %{$table->{cols}} ) {
+   foreach my $col_name ( keys %$cols ) {
       my $col_def = $table->{cols}->{$col_name};
-      die "I can't find a column named '$col_name'" unless $columns{$col_name};
+      die "I can't find a column named '$col_name' for '$table_name'" unless $columns{$col_name};
+      $columns{$col_name}->{referenced} = 1;
 
       foreach my $prop ( keys %col_props ) {
          # Each column gets non-existing values set from %columns or defaults from %col_props.
@@ -1148,40 +1360,29 @@
          }
       }
    }
+
+   # Ensure each column in visible exists in cols
+   foreach my $col_name ( @{$table->{visible}} ) {
+      if ( !exists $cols->{$col_name} ) {
+         die "Column '$col_name' is listed in visible for '$table_name', but doesn't exist";
+      }
+   }
+
    # Compile sort and color subroutines
    $table->{sort_func}  = make_sort_func($table);
    $table->{color_func} = make_color_func($table);
 }
 
-# ###########################################################################
-# Valid Term::ANSIColor color strings.
-# ###########################################################################
-my %ansicolors = map { $_ => 1 }
-   qw( black blink blue bold clear concealed cyan dark green magenta on_black
-       on_blue on_cyan on_green on_magenta on_red on_white on_yellow red reset
-       reverse underline underscore white yellow);
+# This is for code cleanup:
+{
+   my @unused_cols = grep { !$columns{$_}->{referenced} } sort keys %columns;
+   if ( @unused_cols ) {
+      die "The following columns are not used: "
+         . join(' ', @unused_cols);
+   }
+}
 
 # ###########################################################################
-# Valid comparison operators for color rules
-# ###########################################################################
-my %comp_ops = (
-   '==' => 'Numeric equality',
-   '>'  => 'Numeric greater-than',
-   '<'  => 'Numeric less-than',
-   '>=' => 'Numeric greater-than/equal',
-   '<=' => 'Numeric less-than/equal',
-   '!=' => 'Numeric not-equal',
-   'eq' => 'String equality',
-   'gt' => 'String greater-than',
-   'lt' => 'String less-than',
-   'ge' => 'String greater-than/equal',
-   'le' => 'String less-than/equal',
-   'ne' => 'String not-equal',
-   '=~' => 'Pattern match',
-   '!~' => 'Negated pattern match',
-);
-
-# ###########################################################################
 # Valid functions for transformations.
 # ###########################################################################
 my %trans_funcs = (
@@ -1190,8 +1391,8 @@
    no_ctrl_char => \&no_ctrl_char,
    percent      => \&percent,
    commify      => \&commify,
-   collapse_ws  => \&collapse_ws,
    dulint_to_int => \&dulint_to_int,
+   set_precision => \&set_precision,
 );
 
 # ###########################################################################
@@ -1199,12 +1400,13 @@
 # ###########################################################################
 my %modes = (
    B => {
-      hdr               => 'InnoDB Buf',
+      hdr               => 'InnoDB Buffers',
+      cust              => {},
       note              => 'Shows buffer info from InnoDB',
       action_for        => {
          i => {
             action => sub { toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
+            label  => 'Toggle incremental status display',
          },
       },
       display_sub       => \&display_B,
@@ -1216,8 +1418,13 @@
    },
    D => {
       hdr               => 'InnoDB Deadlocks',
+      cust              => {},
       note              => 'View InnoDB deadlock information',
       action_for        => {
+         c => {
+            action => sub { edit_table('deadlock_transactions') },
+            label  => 'Choose visible columns',
+         },
          w => {
             action => \&create_deadlock,
             label  => 'Wipe deadlock status info by creating a deadlock',
@@ -1232,6 +1439,7 @@
    },
    F => {
       hdr               => 'InnoDB FK Err',
+      cust              => {},
       note              => 'View the latest InnoDB foreign key error',
       action_for        => {},
       display_sub       => \&display_F,
@@ -1241,41 +1449,14 @@
       tables            => [qw(fk_error)],
       visible_tables    => [qw(fk_error)],
    },
-   G => {
-      hdr               => 'Load Graph',
-      note              => 'Shows query load graph',
-      action_for        => {
-         c => {
-            action => sub {
-               choose_var_set('G_set');
-               start_G_mode();
-            },
-            label => "Choose which set to display",
-         },
-         e => {
-            action => \&edit_current_var_set,
-            label  => 'Edit the current set of variables',
-         },
-         i => {
-            action => sub { $clear_screen_sub->(); toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
-         },
-      },
-      display_sub       => \&display_G,
-      no_clear_screen   => 1,
-      connections       => [],
-      server_group      => '',
-      one_connection    => 1,
-      tables            => [qw()],
-      visible_tables    => [qw()],
-   },
    I => {
       hdr               => 'InnoDB I/O Info',
+      cust              => {},
       note              => 'Shows I/O info (i/o, log...) from InnoDB',
       action_for        => {
          i => {
             action => sub { toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
+            label  => 'Toggle incremental status display',
          },
       },
       display_sub       => \&display_I,
@@ -1287,20 +1468,25 @@
    },
    M => {
       hdr               => 'Replication Status',
+      cust              => {},
       note              => 'Shows replication (master and slave) status',
       action_for        => {
          a => {
-            action => sub { send_cmd_to_servers('START SLAVE', 1, 'START SLAVE SQL_THREAD UNTIL MASTER_LOG_FILE = ?, MASTER_LOG_POS = ?'); },
+            action => sub { send_cmd_to_servers('START SLAVE', 0, 'START SLAVE SQL_THREAD UNTIL MASTER_LOG_FILE = ?, MASTER_LOG_POS = ?', []); },
             label  => 'Start slave(s)',
          },
          i => {
             action => sub { toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
+            label  => 'Toggle incremental status display',
          },
          o => {
-            action => sub { send_cmd_to_servers('STOP SLAVE', 1); },
+            action => sub { send_cmd_to_servers('STOP SLAVE', 0, '', []); },
             label  => 'Stop slave(s)',
          },
+         b => {
+            action => sub { purge_master_logs() },
+            label  => 'Purge unused master logs',
+         },
       },
       display_sub       => \&display_M,
       connections       => [],
@@ -1311,12 +1497,9 @@
    },
    O => {
       hdr               => 'Open Tables',
+      cust              => {},
       note              => 'Shows open tables in MySQL',
       action_for        => {
-         c => {
-            action => sub { get_config_interactive('O_fmt'); },
-            label => "Choose which columns to display",
-         },
          r => {
             action => sub { reverse_sort('open_tables'); },
             label  => 'Reverse sort order',
@@ -1335,15 +1518,16 @@
    },
    Q => {
       hdr        => 'Query List',
+      cust       => {},
       note       => 'Shows queries from SHOW FULL PROCESSLIST',
       action_for => {
          a => {
             action => sub { toggle_filter('processlist', 'hide_self') },
-            label  => 'Toggle hiding the innotop process',
+            label  => 'Toggle the innotop process',
          },
          c => {
-            action => sub { choose('Q_fmt'); },
-            label => "Choose which columns to display",
+            action => sub { edit_table('processlist') },
+            label  => 'Choose visible columns',
          },
          e => {
             action => sub { analyze_query('e'); },
@@ -1354,12 +1538,12 @@
             label  => "Show a thread's full query",
          },
          h => {
-            action => sub { toggle_config('show_QT_header') },
+            action => sub { toggle_visible_table('Q', 'q_header') },
             label  => 'Toggle the header on and off',
          },
          i => {
             action => sub { toggle_filter('processlist', 'hide_inactive') },
-            label  => 'Toggle showing or hiding idle (Sleep) processes',
+            label  => 'Toggle idle processes',
          },
          k => {
             action => sub { kill_query('CONNECTION') },
@@ -1375,7 +1559,7 @@
          },
          x => {
             action => sub { kill_query('QUERY') },
-            label => "Kill a query (not the connection; requires 5.0)",
+            label => "Kill a query",
          },
       },
       display_sub       => \&display_Q,
@@ -1387,11 +1571,12 @@
    },
    R => {
       hdr               => 'InnoDB Row Ops',
+      cust              => {},
       note              => 'Shows InnoDB row operation and semaphore info',
       action_for        => {
          i => {
             action => sub { toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
+            label  => 'Toggle incremental status display',
          },
       },
       display_sub       => \&display_R,
@@ -1402,9 +1587,18 @@
       visible_tables    => [qw(row_operations row_operation_misc semaphores wait_array)],
    },
    S => {
-      hdr               => 'Load Stats',
+      hdr               => 'Variables & Status',
+      cust              => {},
       note              => 'Shows query load statistics a la vmstat',
       action_for        => {
+         '>' => {
+            action => sub { switch_var_set('S_set', 1) },
+            label  => 'Switch to next variable set',
+         },
+         '<' => {
+            action => sub { switch_var_set('S_set', -1) },
+            label  => 'Switch to prev variable set',
+         },
          c => {
             action => sub {
                choose_var_set('S_set');
@@ -1418,7 +1612,7 @@
          },
          i => {
             action => sub { $clear_screen_sub->(); toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
+            label  => 'Toggle incremental status display',
          },
          '-' => {
             action => sub { set_display_precision(-1) },
@@ -1428,34 +1622,55 @@
             action => sub { set_display_precision(1) },
             label  => 'Increase fractional display precision',
          },
+         g => {
+            action => sub { set_s_mode('g') },
+            label  => 'Switch to graph (tload) view',
+         },
+         s => {
+            action => sub { set_s_mode('s') },
+            label  => 'Switch to standard (vmstat) view',
+         },
+         v => {
+            action => sub { set_s_mode('v') },
+            label  => 'Switch to pivoted view',
+         },
       },
       display_sub       => \&display_S,
       no_clear_screen   => 1,
       connections       => [],
       server_group      => '',
-      one_connection    => 0,
-      tables            => [qw()],
-      visible_tables    => [qw()],
+      one_connection    => 1,
+      tables            => [qw(var_status)],
+      visible_tables    => [qw(var_status)],
    },
    T => {
       hdr        => 'InnoDB Txns',
+      cust       => {},
       note       => 'Shows InnoDB transactions in top-like format',
       action_for => {
          a => {
             action => sub { toggle_filter('innodb_transactions', 'hide_self') },
-            label  => 'Toggle hiding the innotop process',
+            label  => 'Toggle the innotop process',
          },
          c => {
-            action => sub { get_config_interactive('T_fmt'); },
-            label => "Choose which columns to display",
+            action => sub { edit_table('innodb_transactions') },
+            label  => 'Choose visible columns',
          },
+         e => {
+            action => sub { analyze_query('e'); },
+            label  => "Explain a thread's query",
+         },
+         f => {
+            action => sub { analyze_query('f'); },
+            label  => "Show a thread's full query",
+         },
          h => {
-            action => sub { toggle_config('show_QT_header') },
+            action => sub { toggle_visible_table('T', 't_header') },
             label  => 'Toggle the header on and off',
          },
          i => {
             action => sub { toggle_filter('innodb_transactions', 'hide_inactive') },
-            label  => 'Toggle showing or hiding inactive transactions',
+            label  => 'Toggle inactive transactions',
          },
          k => {
             action => sub { kill_query('CONNECTION') },
@@ -1471,57 +1686,21 @@
          },
          x => {
             action => sub { kill_query('QUERY') },
-            label  => "Kill a query, not a connection (requires 5.0)",
+            label  => "Kill a query",
          },
       },
       display_sub       => \&display_T,
       connections       => [],
       server_group      => '',
       one_connection    => 0,
-      tables            => [qw(innodb_transactions)],
-      visible_tables    => [qw(innodb_transactions)],
+      tables            => [qw(t_header innodb_transactions)],
+      visible_tables    => [qw(t_header innodb_transactions)],
    },
-   V => {
-      hdr               => 'Variables & Status',
-      note              => 'Shows values from SHOW STATUS and SHOW VARIABLES',
-      action_for        => {
-         c => {
-            action => sub { choose_var_set('V_set') },
-            label  => 'Choose which set to display',
-         },
-         e => {
-            action => \&edit_current_var_set,
-            label  => 'Edit the current set of variables',
-         },
-         i => {
-            action => sub { toggle_config('status_inc') },
-            label  => 'Toggle overall/incremental status display',
-         },
-         '-' => {
-            action => sub { set_display_precision(-1) },
-            label  => 'Decrease fractional display precision',
-         },
-         '+' => {
-            action => sub { set_display_precision(1) },
-            label  => 'Increase fractional display precision',
-         },
-      },
-      display_sub    => \&display_V,
-      connections    => [],
-      server_group   => '',
-      one_connection => 1,
-      tables            => [qw()],
-      visible_tables    => [qw()],
-   },
    W => {
       hdr             => 'InnoDB Lock Waits',
+      cust            => {},
       note            => 'Shows transaction lock waits and OS wait array info',
-      action_for      => {
-         c => {
-            action => sub { get_config_interactive('W_fmt') },
-            label  => 'Choose which columns to show in the lock waits table',
-         },
-      },
+      action_for      => { },
       display_sub     => \&display_W,
       connections     => [],
       server_group    => '',
@@ -1548,11 +1727,11 @@
    },
    '!' => {
       action => \&display_license,
-      label  => 'Show license and warranty information',
+      label  => 'Show license and warranty',
    },
    '^' => {
       action => \&edit_table,
-      label  => "Edit columns, etc in the displayed table(s)",
+      label  => "Edit the displayed table(s)",
    },
    '#' => {
       action => \&choose_server_groups,
@@ -1562,6 +1741,18 @@
       action => \&choose_servers,
       label  => 'Select/create server connections',
    },
+   '/' => {
+      action => \&add_quick_filter,
+      label  => 'Quickly filter what you see',
+   },
+   '\\' => {
+      action => \&clear_quick_filters,
+      label  => 'Clear quick-filters',
+   },
+   '%' => {
+      action => \&choose_filters,
+      label  => 'Choose and edit table filters',
+   },
    "\t" => {
       action => \&next_server_group,
       label  => 'Switch to the next server group',
@@ -1569,65 +1760,63 @@
    },
    B => {
       action => sub { switch_mode('B') },
-      label  => 'Switch to B mode (InnoDB Buffer/Hash Index)',
+      label  => '',
    },
    D => {
       action => sub { switch_mode('D') },
-      label  => 'Switch to D mode (InnoDB Deadlock Information)',
+      label  => '',
    },
    F => {
       action => sub { switch_mode('F') },
-      label  => 'Switch to F mode (InnoDB Foreign Key Error)',
+      label  => '',
    },
-   G => {
-      action => \&start_G_mode,
-      label  => 'Switch to G mode (Load Graph)',
-   },
    I => {
       action => sub { switch_mode('I') },
-      label  => 'Switch to I mode (InnoDB I/O and Log)',
+      label  => '',
    },
    M => {
       action => sub { switch_mode('M') },
-      label  => 'Switch to M mode (MySQL Replication Status)',
+      label  => '',
    },
    O => {
       action => sub { switch_mode('O') },
-      label  => 'Switch to O mode (MySQL Open Tables)',
+      label  => '',
    },
    Q => {
       action => sub { switch_mode('Q') },
-      label  => 'Switch to Q mode (Query List, like mytop)',
+      label  => '',
    },
    R => {
       action => sub { switch_mode('R') },
-      label  => 'Switch to R mode (InnoDB Row Operations)',
+      label  => '',
    },
    S => {
       action => \&start_S_mode,
-      label  => 'Switch to S mode (Load Statistics)',
+      label  => '',
    },
    T => {
       action => sub { switch_mode('T') },
-      label  => 'Switch to T mode (InnoDB Transaction)',
+      label  => '',
    },
-   V => {
-      action => sub { switch_mode('V') },
-      label  => 'Switch to V mode (Variable & Status)',
-   },
    W => {
       action => sub { switch_mode('W') },
-      label  => 'Switch to W mode (InnoDB Lock Waits and OS Wait Info)',
+      label  => '',
    },
    d => {
       action => sub { get_config_interactive('interval') },
       label  => 'Change refresh interval',
    },
+   n => { action => \&next_server,       label => 'Switch to the next connection' },
    p => { action => \&pause,             label => 'Pause innotop', },
    q => { action => \&finish,            label => 'Quit innotop', },
 );
 
 # ###########################################################################
+# Sleep times after certain statements {{{3
+# ###########################################################################
+my %stmt_sleep_time_for = ();
+
+# ###########################################################################
 # Config editor key mappings {{{3
 # ###########################################################################
 my %cfg_editor_action = (
@@ -1647,6 +1836,10 @@
       note => 'Edit server groups',
       func => \&edit_server_groups,
    },
+   S => {
+      note => 'Edit SQL statement sleep delays',
+      func => \&edit_stmt_sleep_times,
+   },
    t => {
       note => 'Choose which table(s) to display in this mode',
       func => \&choose_mode_tables,
@@ -1709,6 +1902,7 @@
             arg   => $arg,
             color => $color
          };
+         $tbl_meta{$tbl}->{cust}->{colors} = 1;
 
          return $idx;
       },
@@ -1720,6 +1914,7 @@
          my @rules = @{ $tbl_meta{$tbl}->{colors} };
          return 0 unless @rules > 0 && $idx < @rules && $idx >= 0;
          splice(@{$tbl_meta{$tbl}->{colors}}, $idx, 1);
+         $tbl_meta{$tbl}->{cust}->{colors} = 1;
          return $idx == @rules ? $#rules : $idx;
       },
    },
@@ -1748,6 +1943,7 @@
          my $temp = $meta->{colors}->[$idx];
          $meta->{colors}->[$idx]  = $meta->{colors}->[$dest];
          $meta->{colors}->[$dest] = $temp;
+         $meta->{cust}->{colors} = 1;
          return $dest;
       },
    },
@@ -1760,6 +1956,7 @@
          my $temp = $meta->{colors}->[$idx];
          $meta->{colors}->[$idx]  = $meta->{colors}->[$dest];
          $meta->{colors}->[$dest] = $temp;
+         $meta->{cust}->{colors} = 1;
          return $dest;
       },
    },
@@ -1783,6 +1980,7 @@
             { map { $_ => $all_cols{$_}->{label} || $all_cols{$_}->{hdr} } keys %all_cols });
          if ( $all_cols{$choice} ) {
             push @{$tbl_meta{$tbl}->{visible}}, $choice;
+            $tbl_meta{$tbl}->{cust}->{visible} = 1;
             return $choice;
          }
          return $col;
@@ -1810,10 +2008,7 @@
          } while ( !$hdr );
 
          $clear_screen_sub->();
-         print word_wrap("Choose a source for the column's data.  You can either enter the name of an entry "
-               . "in the data available to the table (varies by context) or if you want to enter "
-               . "the name of an expression, specify nothing here.");
-         print "\n\n";
+         print "Choose a source for the column's data\n\n";
          my ( $src, $sub, $err );
          do {
             if ( $err ) {
@@ -1821,16 +2016,10 @@
             }
             $src = prompt("Enter column source");
             if ( $src ) {
-               ( $sub, $err ) = compile_expr($src, 1);
+               ( $sub, $err ) = compile_expr($src);
             }
-         } until ( !$src || !$err);
+         } until ( !$err);
 
-         my $exp;
-         if ( !$src ) {
-            $exp = get_expr();
-            return $col unless $exp && $exprs{$exp};
-         }
-
          $tbl_meta{$tbl}->{cols}->{$col} = {
             hdr   => $hdr,
             src   => $src,
@@ -1839,14 +2028,15 @@
             label => 'User-defined',
             user  => 1,
             tbl   => $tbl,
-            expr  => $exp ? $exp : '',
             minw  => 0,
             maxw  => 0,
             trans => [],
-            func  => $sub || $exprs{$exp}->{func},
+            func  => $sub,
+            dec   => 0,
          };
 
          $tbl_meta{$tbl}->{visible} = [ unique(@{$tbl_meta{$tbl}->{visible}}, $col) ];
+         $tbl_meta{$tbl}->{cust}->{visible} = 1;
          return $col;
       },
    },
@@ -1861,40 +2051,31 @@
             $idx++;
          }
          $tbl_meta{$tbl}->{visible} = [ grep { $_ ne $col } @visible_cols ];
+         $tbl_meta{$tbl}->{cust}->{visible} = 1;
          return $idx == $#visible_cols ? $visible_cols[$idx - 1] : $visible_cols[$idx + 1];
       },
    },
    e => {
       note => 'Edit selected column',
       func => sub {
+         # TODO: make this editor hotkey-driven and give readline support.
          my ( $tbl, $col ) = @_;
          $clear_screen_sub->();
          my $meta = $tbl_meta{$tbl}->{cols}->{$col};
-         my @prop = qw(hdr label src expr just num minw maxw trans);
+         my @prop = qw(hdr label src just num minw maxw trans); # TODO redundant
 
          my $answer;
          do {
             # Do what the user asked...
             if ( $answer && grep { $_ eq $answer } @prop ) {
-               if ( $answer eq 'expr' ) {
-                  $meta->{expr} = get_expr();
+               # Some properties are arrays, others scalars.
+               my $ini = ref $col_props{$answer} ? join(' ', @{$meta->{$answer}}) : $meta->{$answer};
+               my $val = prompt("New value for $answer", undef, $ini);
+               $val = [ split(' ', $val) ] if ref($col_props{$answer});
+               if ( $answer eq 'trans' ) {
+                  $val = [ unique(grep{ exists $trans_funcs{$_} } @$val) ];
                }
-               else {
-                  # Some properties are arrays, others scalars.
-                  my $ini = ref $col_props{$answer} ? join(' ', @{$meta->{$answer}}) : $meta->{$answer};
-                  my $val = prompt("New value for $answer", undef, $ini);
-                  $val = [ split(' ', $val) ] if ref($col_props{$answer});
-                  if ( $answer eq 'trans' ) {
-                     $val = [ unique(grep{ exists $trans_funcs{$_} } @$val) ];
-                  }
-                  @{$meta}{$answer, 'user', 'tbl' } = ( $val, 1, $tbl );
-                  if ( $answer eq 'src' ) {
-                     $meta->{expr} = '';
-                  }
-               }
-               if ( $meta->{expr} ) {
-                  $meta->{src}  = $exprs{$meta->{expr}};
-               }
+               @{$meta}{$answer, 'user', 'tbl' } = ( $val, 1, $tbl );
             }
 
             my @display_lines = (
@@ -1962,6 +2143,7 @@
             shift @{$meta->{visible}};
             push @{$meta->{visible}}, $col;
          }
+         $meta->{cust}->{visible} = 1;
          return $col;
       },
    },
@@ -1984,92 +2166,64 @@
             $visible_cols[$idx + 1] = $col;
             $meta->{visible}        = \@visible_cols;
          }
+         $meta->{cust}->{visible} = 1;
          return $col;
       },
    },
+   f => {
+      note => 'Choose filters',
+      func => sub {
+         my ( $tbl, $col ) = @_;
+         choose_filters($tbl);
+         return $col;
+      },
+   },
    o => {
-      note => 'Edit table meta-data (sort column, filters...)',
+      note => 'Edit color rules',
       func => sub {
          my ( $tbl, $col ) = @_;
-         $clear_screen_sub->();
-         my $meta         = $tbl_meta{$tbl};
-         my $sort_cols    = $meta->{sort_cols};
-         my $filters      = $meta->{filters};
-         my @prop         = qw(filters sort_cols);
-
-         my $answer;
-         do {
-            # Do whatever the user asked
-            if ( $answer && grep { $_ eq $answer } @prop ) {
-               my $ini = ref $meta->{$answer} ? join(' ', @{$meta->{$answer}}) : $meta->{$answer};
-               if ( $answer eq 'sort_cols' ) {
-                  choose_sort_cols($tbl);
-               }
-               elsif ( $answer eq 'filters' ) {
-                  $clear_screen_sub->();
-                  my $val = prompt_list(
-                     'Choose filters',
-                     $ini,
-                     sub { return keys %filters },
-                     {
-                        map  { $_ => $filters{$_}->{note} }
-                        grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} }
-                        keys %filters
-                     }
-                  );
-
-                  my @choices = unique(split(/\s+/, $val));
-                  foreach my $new ( grep { !exists($filters{$_}) } @choices ) {
-                     my $answer = prompt("There is no filter called '$new'.  Create it?", undef, 'y');
-                     if ( $answer eq 'y' ) {
-                        create_new_filter($new, $tbl);
-                     }
-                  }
-                  @choices = grep { exists $filters{$_} } @choices;
-                  @choices = grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} } @choices;
-                  $meta->{filters} = [ @choices ];
-               }
-            }
-
-            my @display_lines = "You are editing table $tbl.";
-
-            push @display_lines, '', create_caption('Properties', create_table2(
-               \@prop,
-               { map { $_ => $_ } @prop },
-               { map { $_ => ref $meta->{$_} eq 'ARRAY' ? join(' ', @{$meta->{$_}})
-                           :                              $meta->{$_}
-                     } @prop
-               },
-               { sep => '  ' }));
-            draw_screen(\@display_lines, { raw => 1 });
-            print "\n\n"; # One to add space, one to clear readline artifacts
-            $answer = prompt('Edit what? (q to quit)', undef, undef, sub { return @prop });
-         } while ( $answer ne 'q' );
-
+         edit_color_rules($tbl);
          return $col;
       },
    },
+   s => {
+      note => 'Choose sort columns',
+      func => sub {
+         my ( $tbl, $col ) = @_;
+         choose_sort_cols($tbl);
+         return $col;
+      },
+   },
 );
 
 # ###########################################################################
 # Global variables and environment {{{2
 # ###########################################################################
 
-# Set up required stuff for interactive mode...
-if ( !$opts{n} ) {
-   require Term::ReadKey;
-   import Term::ReadKey qw(ReadMode ReadKey);
-}
-
 my @this_term_size; # w_chars, h_chars, w_pix, h_pix
 my @last_term_size; # w_chars, h_chars, w_pix, h_pix
 my $char;
-my $windows       = $OSNAME =~ m/Win/i;
+my $windows       = $OSNAME =~ m/MSWin/;
 my $have_color    = 0;
 my $MAX_ULONG     = 4294967295; # 2^32-1
 my $num_regex     = qr/^[+-]?(?=\d|\.)\d*(?:\.\d+)?(?:E[+-]?\d+|)$/i;
+my $int_regex     = qr/^\d+$/;
+my $bool_regex    = qr/^[01]$/;
 my $term          = undef;
+my $innodb_parser = InnoDBParser->new;
 
+my $nonfatal_errs = join('|',
+   'Access denied for user',
+   'Unknown MySQL server host',
+   'Unknown database',
+   'Can\'t connect to local MySQL server through socket',
+   'Can\'t connect to MySQL server on',
+   'MySQL server has gone away',
+   'Cannot call SHOW INNODB STATUS',
+   'Access denied',
+   'AutoCommit',
+);
+
 if ( !$opts{n} ) {
    require Term::ReadLine;
    $term = Term::ReadLine->new('innotop');
@@ -2084,20 +2238,12 @@
 # Stores info on currently displayed queries: cxn, connection ID, query text.
 my @current_queries;
 
-my $hi_res              = 0;
 my $lines_printed       = 0;
 my @innodb_files        = ();
 my $innodb_file_counter = -1;
 my $clock               = 0;   # Incremented with every wake-sleep cycle
 my $clearing_deadlocks  = 0;
 
-# If hi-res time is available, use it.
-eval {
-   require Time::HiRes;
-   import Time::HiRes qw(time sleep);
-   $hi_res   = 1;
-};
-
 # Find the home directory; it's different on different OSes.
 my $homepath = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
 
@@ -2113,10 +2259,10 @@
       $have_color = 1;
    }
 };
-if ( $EVAL_ERROR ) {
+if ( $EVAL_ERROR || $opts{n} ) {
    # If there was an error, manufacture my own colored() function that does no
    # coloring.
-   *colored = sub { return shift; };
+   *colored = sub { pop @_; @_; };
 }
 
 if ( $opts{n} ) {
@@ -2134,29 +2280,65 @@
 # Config storage. {{{2
 # ###########################################################################
 my %config = (
+   show_percent => {
+      val  => 1,
+      note => 'Show the % symbol after percentages',
+      conf => 'ALL',
+      pat  => $bool_regex,
+   },
+   skip_innodb => {
+      val  => 0,
+      note => 'Disable SHOW INNODB STATUS',
+      conf => 'ALL',
+      pat  => $bool_regex,
+   },
+   S_func => {
+      val  => 's',
+      note => 'What to display in S mode: graph, status, pivoted status',
+      conf => [qw(S)],
+      pat  => qr/^[gsv]$/,
+   },
+   cxn_timeout => {
+      val  => 28800,
+      note => 'Connection timeout for keeping unused connections alive',
+      conf => 'ALL',
+      pat  => $int_regex,
+   },
+   graph_char => {
+      val  => '*',
+      note => 'Character for drawing graphs',
+      conf => [ qw(S) ],
+      pat  => qr/^.$/,
+   },
    show_cxn_errors_in_tbl => {
       val  => 1,
-      note => 'Whether to display connection errors at the end of every table',
-      conf => [ 'ALL' ],
-      pat  => qr/^[01]$/,
+      note => 'Whether to display connection errors as rows in the table',
+      conf => 'ALL',
+      pat  => $bool_regex,
    },
+   hide_hdr => {
+      val  => 0,
+      note => 'Whether to show column headers',
+      conf => 'ALL',
+      pat  => $bool_regex,
+   },
    show_cxn_errors => {
       val  => 1,
       note => 'Whether to print connection errors to STDOUT',
-      conf => [ 'ALL' ],
-      pat  => qr/^[01]$/,
+      conf => 'ALL',
+      pat  => $bool_regex,
    },
    readonly => {
       val  => 0,
       note => 'Whether the config file is read-only',
       conf => [ qw() ],
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
    },
    global => {
       val  => 1,
       note => 'Whether to show GLOBAL variables and status',
       conf => 'ALL',
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
    },
    header_highlight => {
       val  => 'bold',
@@ -2168,14 +2350,8 @@
       val  => 1,
       note => 'Whether to put captions on tables',
       conf => 'ALL',
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
    },
-   compact_hdr => {
-      val  => 1,
-      note => 'Whether to compact the headers in some modes',
-      conf => 'ALL',
-      pat  => qr/^[01]$/,
-   },
    charset => {
       val  => 'ascii',
       note => 'What type of characters should be displayed in queries (ascii, unicode, none)',
@@ -2186,7 +2362,7 @@
       val  => 0,
       note => 'Whether to auto-wipe InnoDB deadlocks',
       conf => 'ALL',
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
    },
    max_height => {
       val  => 30,
@@ -2195,29 +2371,23 @@
    },
    debug => {
       val  => 0,
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
       note => 'Debug mode (more verbose errors, uses more memory)',
-      conf => [ qw(D) ],
+      conf => 'ALL',
    },
    num_digits => {
       val  => 2,
-      pat  => qr/^\d$/,
+      pat  => $int_regex,
       note => 'How many digits to show in fractional numbers and percents',
       conf => 'ALL',
    },
-   show_QT_header => {
-      val  => 1,
-      pat  => qr/^[01]$/,
-      note => 'Whether to show the header in Q and T modes',
-      conf => [ qw(Q T) ],
-   },
    debugfile => {
       val  => "$homepath/.innotop_core_dump",
       note => 'A debug file in case you are interested in error output',
    },
    show_statusbar => {
       val  => 1,
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
       note => 'Whether to show the status bar in the display',
       conf => 'ALL',
    },
@@ -2229,35 +2399,23 @@
    status_inc => {
       val  => 0,
       note => 'Whether to show raw or incremental values for status variables',
-      pat  => qr/^[01]$/,
+      pat  => $bool_regex,
    },
    interval => {
       val  => 10,
       pat  => qr/^(?:(?:\d*?[1-9]\d*(?:\.\d*)?)|(?:\d*\.\d*?[1-9]\d*))$/,
       note => "The interval at which the display will be refreshed.  Fractional values allowed.",
    },
-   V_set => {
-      val  => 'general',
-      pat  => qr/^\w+$/,
-      note => 'Which set of variables to display in V (Variables/Status) mode',
-      conf => [ qw(V) ],
-   },
    num_status_sets => {
       val  => 9,
-      pat  => qr/^\d*?[1-9]\d*$/,
+      pat  => $int_regex,
       note => 'How many sets of STATUS and VARIABLES values to show',
-      conf => [ qw(V) ],
+      conf => [ qw(S) ],
    },
-   G_set => {
-      val  => 'general',
-      pat  => qr/^\w+$/,
-      note => 'Which set of variables to display in G (Load Graph) mode',
-      conf => [ qw(G) ],
-   },
    S_set => {
       val  => 'general',
       pat  => qr/^\w+$/,
-      note => 'Which set of variables to display in S (Load Statistics) mode',
+      note => 'Which set of variables to display in S (Variables & Status) mode',
       conf => [ qw(S) ],
    },
 );
@@ -2289,10 +2447,6 @@
       reader => \&load_config_active_columns,
       writer => \&save_config_active_columns,
    },
-   expressions => {
-      reader => \&load_config_expressions,
-      writer => \&save_config_expressions,
-   },
    tbl_meta => {
       reader => \&load_config_tbl_meta,
       writer => \&save_config_tbl_meta,
@@ -2329,65 +2483,73 @@
       reader => \&load_config_colors,
       writer => \&save_config_colors,
    },
+   stmt_sleep_times => {
+      reader => \&load_config_stmt_sleep_times,
+      writer => \&save_config_stmt_sleep_times,
+   },
 );
 
 # Config file sections have some dependencies, so they have to be read/written in order.
-my @ordered_config_file_sections = qw(filters active_filters expressions tbl_meta
-   general connections active_connections server_groups active_server_groups max_values_seen
-   active_columns sort_cols visible_tables varsets colors);
+my @ordered_config_file_sections = qw(general filters active_filters tbl_meta
+   connections active_connections server_groups active_server_groups max_values_seen
+   active_columns sort_cols visible_tables varsets colors stmt_sleep_times);
 
 # ###########################################################################
 # Contains logic to generate prepared statements for a given function for a
-# given DB connection.  $cxn is a key in %dbhs.  Returns a SQL string.
+# given DB connection.  Returns a $sth.
 # ###########################################################################
 my %stmt_maker_for = (
    INNODB_STATUS => sub {
-      my ( $cxn ) = @_;
-      my $meta = $dbhs{$cxn};
-      return ( $meta->{ver_major} >= 5 )
+      my ( $dbh ) = @_;
+      return $dbh->prepare(version_ge( $dbh, '5.0.0' )
              ? 'SHOW ENGINE INNODB STATUS'
-             : 'SHOW INNODB STATUS';
+             : 'SHOW INNODB STATUS');
    },
    SHOW_VARIABLES => sub {
-      my ( $cxn ) = @_;
-      my $meta = $dbhs{$cxn};
-      return ( $config{global}->{val} && $meta->{ver_major} >= 4 ) && ( $meta->{ver_rev} >= 3 )
+      my ( $dbh ) = @_;
+      return $dbh->prepare($config{global}->{val} && version_ge( $dbh, '4.0.3' )
              ? 'SHOW GLOBAL VARIABLES'
-             : 'SHOW VARIABLES';
+             : 'SHOW VARIABLES');
    },
    SHOW_STATUS => sub {
-      my ( $cxn ) = @_;
-      my $meta = $dbhs{$cxn};
-      return ( $config{global}->{val} && $meta->{ver_major} >= 5 ) && ( $meta->{ver_rev} >= 2 )
+      my ( $dbh ) = @_;
+      return $dbh->prepare($config{global}->{val} && version_ge( $dbh, '5.0.2' )
              ? 'SHOW GLOBAL STATUS'
-             : 'SHOW STATUS';
+             : 'SHOW STATUS');
    },
    KILL_QUERY => sub {
-      my ( $cxn ) = @_;
-      my $meta = $dbhs{$cxn};
-      return ( $meta->{ver_major} >= 5 )
+      my ( $dbh ) = @_;
+      return $dbh->prepare(version_ge( $dbh, '5.0.0' )
              ? 'KILL QUERY ?'
-             : 'KILL ?';
+             : 'KILL ?');
    },
+   SHOW_MASTER_LOGS => sub {
+      my ( $dbh ) = @_;
+      return $dbh->prepare('SHOW MASTER LOGS');
+   },
    SHOW_MASTER_STATUS => sub {
-      my ( $cxn ) = @_;
-      return 'SHOW MASTER STATUS';
+      my ( $dbh ) = @_;
+      return $dbh->prepare('SHOW MASTER STATUS');
    },
    SHOW_SLAVE_STATUS => sub {
-      my ( $cxn ) = @_;
-      return 'SHOW SLAVE STATUS';
+      my ( $dbh ) = @_;
+      return $dbh->prepare('SHOW SLAVE STATUS');
    },
    KILL_CONNECTION => sub {
-      my ( $cxn ) = @_;
-      return 'KILL CONNECTION ?';
+      my ( $dbh ) = @_;
+      return $dbh->prepare(version_ge( $dbh, '5.0.0' )
+             ? 'KILL CONNECTION ?'
+             : 'KILL ?');
    },
    OPEN_TABLES => sub {
-      my ( $cxn ) = @_;
-      return 'SHOW OPEN TABLES';
+      my ( $dbh ) = @_;
+      return version_ge($dbh, '4.0.0')
+         ? $dbh->prepare('SHOW OPEN TABLES')
+         : undef;
    },
    PROCESSLIST => sub {
-      my ( $cxn ) = @_;
-      return 'SHOW FULL PROCESSLIST';
+      my ( $dbh ) = @_;
+      return $dbh->prepare('SHOW FULL PROCESSLIST');
    },
 );
 
@@ -2418,9 +2580,23 @@
 }
 
 eval {
+
+   # In certain modes we might have to collect data for two cycles
+   # before printing anything out, so we need to bump up the count one.
+   if ( $opts{n} && $opts{count} && $config{status_inc}->{val}
+      && $config{mode}->{val} =~ m/[S]/ )
+   {
+      $opts{count}++;
+   }
+
    while (++$clock) {
 
-      my $mode = $config{mode}->{val};
+      my $mode = $config{mode}->{val} || 'T';
+      if ( !$modes{$mode} ) {
+         die "Mode '$mode' doesn't exist; try one of these:\n"
+            . join("\n", map { "  $_ $modes{$_}->{hdr}" }  sort keys %modes)
+            . "\n";
+      }
 
       if ( !$opts{n} ) {
          @last_term_size = @this_term_size;
@@ -2464,6 +2640,11 @@
       # Call the subroutine to display this mode.
       $modes{$mode}->{display_sub}->();
 
+      # It may be time to quit now.
+      if ( $opts{count} && $clock >= $opts{count} ) {
+         finish();
+      }
+
       # Wait for a bit.
       if ( $opts{n} ) {
          sleep($config{interval}->{val});
@@ -2630,13 +2811,15 @@
 sub kill_query {
    my ( $q_or_c ) = @_;
 
-   my ( $cxn ) = select_cxn('Kill on which server');
-   return unless $cxn && exists($connections{$cxn});
+   my $info = choose_thread(
+      sub { 1 },
+      'Select a thread to kill the ' . $q_or_c,
+   );
+   return unless $info;
+   return unless pause("Kill $info->{id}?") =~ m/y/i;
 
    eval {
-      my $thread = prompt("Choose which $q_or_c to kill");
-      return unless $thread && $thread =~ m/^\d+$/;
-      do_stmt($cxn, $q_or_c eq 'QUERY' ? 'KILL_QUERY' : 'KILL_CONNECTION', $thread);
+      do_stmt($info->{cxn}, $q_or_c eq 'QUERY' ? 'KILL_QUERY' : 'KILL_CONNECTION', $info->{id} );
    };
 
    if ( $EVAL_ERROR ) {
@@ -2645,17 +2828,24 @@
    }
 }
 
-# set_V_set {{{3
-sub set_V_set {
-   $config{V_set}->{val} = shift;
-}
-
 # set_display_precision {{{3
 sub set_display_precision {
    my $dir = shift;
    $config{num_digits}->{val} = min(9, max(0, $config{num_digits}->{val} + $dir));
 }
 
+sub toggle_visible_table {
+   my ( $mode, $table ) = @_;
+   my $visible = $modes{$mode}->{visible_tables};
+   if ( grep { $_ eq $table } @$visible ) {
+      $modes{$mode}->{visible_tables} = [ grep { $_ ne $table } @$visible ];
+   }
+   else {
+      unshift @$visible, $table;
+   }
+   $modes{$mode}->{cust}->{visible_tables} = 1;
+}
+
 # toggle_filter{{{3
 sub toggle_filter {
    my ( $tbl, $filter ) = @_;
@@ -2666,6 +2856,7 @@
    else {
       push @$filters, $filter;
    }
+   $tbl_meta{$tbl}->{cust}->{filters} = 1;
 }
 
 # toggle_config {{{3
@@ -2693,15 +2884,16 @@
 # deadlock_thread {{{3
 sub deadlock_thread {
    my ( $id, $tbl, $cxn ) = @_;
-   my @stmts = (
-      "set transaction isolation level serializable",
-      "start transaction",
-      "select * from $tbl where a = $id",
-      "update $tbl set a = $id where a <> $id",
-   );
 
    eval {
       my $dbh = get_new_db_connection($cxn, 1);
+      my @stmts = (
+         "set transaction isolation level serializable",
+         (version_ge($dbh, '4.0.11') ? "start transaction" : 'begin'),
+         "select * from $tbl where a = $id",
+         "update $tbl set a = $id where a <> $id",
+      );
+
       foreach my $stmt (@stmts[0..2]) {
          $dbh->do($stmt);
       }
@@ -2716,18 +2908,58 @@
    exit(0);
 }
 
+# Purges unused binlogs on the master, up to but not including the latest log.
+# TODO: guess which connections are slaves of a given master.
+sub purge_master_logs {
+   my @cxns = get_connections();
+
+   get_master_slave_status(@cxns);
+
+   # Toss out the rows that don't have master/slave status...
+   my @vars =
+      grep { $_ && ($_->{file} || $_->{master_host}) }
+      map  { $vars{$_}->{$clock} } @cxns;
+   @cxns = map { $_->{cxn} } @vars;
+
+   # Figure out which master to purge ons.
+   my @masters = map { $_->{cxn} } grep { $_->{file} } @vars;
+   my ( $master ) = select_cxn('Which master?', @masters );
+   return unless $master;
+   my ($master_status) = grep { $_->{cxn} eq $master } @vars;
+
+   # Figure out the result order (not lexical order) of master logs.
+   my @master_logs = get_master_logs($master);
+   my $i = 0;
+   my %master_logs = map { $_->{log_name} => $i++ } @master_logs;
+
+   # Ask which slave(s) are reading from this master.
+   my @slave_status = grep { $_->{master_host} } @vars;
+   my @slaves = map { $_->{cxn} } @slave_status;
+   @slaves = select_cxn("Which slaves are reading from $master?", @slaves);
+   @slave_status = grep { my $item = $_; grep { $item->{cxn} eq $_ } @slaves } @slave_status;
+   return unless @slave_status;
+
+   # Find the minimum binary log in use.
+   my $min_log = min(map { $master_logs{$_->{master_log_file}} } @slave_status);
+   my $log_name = $master_logs[$min_log]->{log_name};
+
+   my $stmt = "PURGE MASTER LOGS TO '$log_name'";
+   send_cmd_to_servers($stmt, 0, 'PURGE {MASTER | BINARY} LOGS {TO "log_name" | BEFORE "date"}', [$master]);
+}
+
 sub send_cmd_to_servers {
-   my ( $cmd, $all, $hint ) = @_;
-   my @cxns;
+   my ( $cmd, $all, $hint, $cxns ) = @_;
    if ( $all ) {
-      @cxns = get_connections();
+      @$cxns = get_connections();
    }
-   @cxns = select_cxn('Which servers?', @cxns);
+   elsif ( !@$cxns ) {
+      @$cxns = select_cxn('Which servers?', @$cxns);
+   }
    if ( $hint ) {
       print "\nHint: $hint\n";
    }
    $cmd = prompt('Command to send', undef, $cmd);
-   foreach my $cxn ( @cxns ) {
+   foreach my $cxn ( @$cxns ) {
       eval {
          my $sth = do_query($cxn, $cmd);
       };
@@ -2743,10 +2975,10 @@
 
 # Display functions {{{2
 
-# start_G_mode {{{3
-sub start_G_mode {
+sub set_s_mode {
+   my ( $func ) = @_;
    $clear_screen_sub->();
-   switch_mode('G');
+   $config{S_func}->{val} = $func;
 }
 
 # start_S_mode {{{3
@@ -2777,23 +3009,24 @@
 
    foreach my $cxn ( @cxns ) {
       my $set = $vars{$cxn}->{$clock};
+      my $pre = $vars{$cxn}->{$clock-1} || $set;
 
       if ( $set->{IB_bp_complete} ) {
          if ( $wanted{buffer_pool} ) {
-            push @buffer_pool, extract_values($set, 'buffer_pool');
+            push @buffer_pool, extract_values($set, $set, $pre, 'buffer_pool');
          }
          if ( $wanted{page_statistics} ) {
-            push @page_statistics, extract_values($set, 'page_statistics');
+            push @page_statistics, extract_values($set, $set, $pre, 'page_statistics');
          }
       }
       if ( $set->{IB_ib_complete} ) {
          if ( $wanted{insert_buffers} ) {
             push @insert_buffers, extract_values(
-               $config{status_inc}->{val} ? inc(0, $cxn) : $set,
+               $config{status_inc}->{val} ? inc(0, $cxn) : $set, $set, $pre,
                'insert_buffers');
          }
          if ( $wanted{adaptive_hash_index} ) {
-            push @adaptive_hash_index, extract_values($set, 'adaptive_hash_index');
+            push @adaptive_hash_index, extract_values($set, $set, $pre, 'adaptive_hash_index');
          }
       }
    }
@@ -2801,7 +3034,8 @@
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
@@ -2825,6 +3059,7 @@
 
    foreach my $cxn ( @cxns ) {
       my $innodb_status = $vars{$cxn}->{$clock};
+      my $prev_status   = $vars{$cxn}->{$clock-1} || $innodb_status;
 
       if ( $innodb_status->{IB_dl_timestring} ) {
 
@@ -2833,9 +3068,10 @@
          if ( %wanted ) {
             foreach my $txn_id ( keys %{$innodb_status->{IB_dl_txns}} ) {
                my $txn = $innodb_status->{IB_dl_txns}->{$txn_id};
+               my $pre = $prev_status->{IB_dl_txns}->{$txn_id} || $txn;
 
                if ( $wanted{deadlock_transactions} ) {
-                  my $hash = extract_values($txn->{tx}, 'deadlock_transactions');
+                  my $hash = extract_values($txn->{tx}, $txn->{tx}, $pre->{tx}, 'deadlock_transactions');
                   $hash->{cxn}        = $cxn;
                   $hash->{dl_txn_num} = $txn_id;
                   $hash->{victim}     = $txn_id == $victim ? 'Yes' : 'No';
@@ -2847,8 +3083,9 @@
                if ( $wanted{deadlock_locks} ) {
                   foreach my $what (qw(waits_for holds)) {
                      my $locks = $txn->{$what};
+                     my $pre_l = $pre->{$what};
                      if ( $locks ) {
-                        my $hash = extract_values($locks, 'deadlock_locks');
+                        my $hash = extract_values($locks, $locks, $pre_l, 'deadlock_locks');
                         $hash->{dl_txn_num}      = $txn_id;
                         $hash->{txn_status}      = $what;
                         $hash->{cxn}             = $cxn;
@@ -2866,7 +3103,8 @@
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
@@ -2895,7 +3133,7 @@
       my @fk_table = create_table2(
          $tbl_meta{fk_error}->{visible},
          meta_to_hdr('fk_error'),
-         extract_values($innodb_status, 'fk_error'),
+         extract_values($innodb_status, $innodb_status, $innodb_status, 'fk_error'),
          { just => '-', sep => '  '});
       push @display_lines, '', @fk_table;
 
@@ -2906,56 +3144,6 @@
    draw_screen(\@display_lines, { raw => 1 } );
 }
 
-# display_G {{{3
-sub display_G {
-   my ( $cxn ) = get_connections();
-   my $fmt     = get_var_set('G_set');
-   get_status_info($cxn);
-   get_innodb_status([$cxn]); # TODO: might not be needed.
-
-   if ( !exists $vars{$cxn}->{$clock - 1} ) {
-      return;
-   }
-
-   # Design a column format for the values.
-   my $num_cols = scalar(@$fmt);
-   my $width    = $opts{n} ? 0 : int(($this_term_size[0] - $num_cols + 1) / $num_cols);
-   my $format   = $opts{n} ? ( "%s\t" x $num_cols ) : ( "%-${width}s " x $num_cols );
-   $format      =~ s/\s$/\n/;
-
-   # Clear the screen if the display width changed.
-   if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
-      $lines_printed = 0;
-      $clear_screen_sub->();
-   }
-
-   # Get the values.
-   my $set = inc(0, $cxn);
-   $set = { map { $_ => ($set->{$_} || 1) / ( $set->{Uptime_hires} || 1) } @$fmt };
-
-   # Update max ever seen.
-   map { $mvs{$_} = max($mvs{$_} || 1, $set->{$_}) } @$fmt;
-
-   # Print headers every now and then.
-   if ( $opts{n} ) {
-      if ( $lines_printed == 0 ) {
-         print join("\t", @$fmt), "\n";
-         print join("\t", map { shorten($mvs{$_}) } @$fmt), "\n";
-      }
-   }
-   elsif ( $lines_printed % int( $this_term_size[1] - 2 ) == 0 ) {
-      printf($format, map { donut(crunch($_, $width), $width) } @$fmt);
-      printf($format, map { shorten($mvs{$_}) } @$fmt);
-   }
-   $lines_printed++;
-
-   # Scale the values against the max ever seen.
-   map { $set->{$_} /= $mvs{$_} } @$fmt;
-
-   # Print the values.
-   printf($format, map { ( '*' x int( $width * $set->{$_} )) || '.' } @$fmt );
-}
-
 # display_I {{{3
 sub display_I {
    my @display_lines;
@@ -2978,33 +3166,39 @@
 
    foreach my $cxn ( @cxns ) {
       my $set = $vars{$cxn}->{$clock};
+      my $pre = $vars{$cxn}->{$clock-1} || $set;
 
       if ( $set->{IB_io_complete} ) {
          if ( $wanted{io_threads} ) {
-            foreach my $thd ( values %{$set->{IB_io_threads}} ) {
-               my $hash = extract_values($thd, 'io_threads');
+            my $cur_threads = $set->{IB_io_threads};
+            my $pre_threads = $pre->{IB_io_threads} || $cur_threads;
+            foreach my $key ( sort keys %$cur_threads ) {
+               my $cur_thd = $cur_threads->{$key};
+               my $pre_thd = $pre_threads->{$key} || $cur_thd;
+               my $hash = extract_values($cur_thd, $cur_thd, $pre_thd, 'io_threads');
                $hash->{cxn} = $cxn;
                push @io_threads, $hash;
             }
          }
          if ( $wanted{pending_io} ) {
-            push @pending_io, extract_values($set, 'pending_io');
+            push @pending_io, extract_values($set, $set, $pre, 'pending_io');
          }
          if ( $wanted{file_io_misc} ) {
             push @file_io_misc, extract_values(
                $config{status_inc}->{val} ? inc(0, $cxn) : $set,
-               'file_io_misc');
+               $set, $pre, 'file_io_misc');
          }
       }
       if ( $set->{IB_lg_complete} && $wanted{log_statistics} ) {
-         push @log_statistics, extract_values($set, 'log_statistics');
+         push @log_statistics, extract_values($set, $set, $pre, 'log_statistics');
       }
    }
 
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
@@ -3031,21 +3225,23 @@
 
    foreach my $cxn ( @cxns ) {
       my $set  = $config{status_inc}->{val} ? inc(0, $cxn) : $vars{$cxn}->{$clock};
+      my $pre  = $vars{$cxn}->{$clock - 1} || $set;
       if ( $wanted{slave_sql_status} ) {
-         push @slave_sql_status, extract_values($set, 'slave_sql_status');
+         push @slave_sql_status, extract_values($set, $set, $pre, 'slave_sql_status');
       }
       if ( $wanted{slave_io_status} ) {
-         push @slave_io_status, extract_values($set, 'slave_io_status');
+         push @slave_io_status, extract_values($set, $set, $pre, 'slave_io_status');
       }
       if ( $wanted{master_status} ) {
-         push @master_status, extract_values($set, 'master_status');
+         push @master_status, extract_values($set, $set, $pre, 'master_status');
       }
    }
 
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
@@ -3056,7 +3252,7 @@
    my @display_lines = ('');
    my @cxns          = get_connections();
    my @open_tables   = get_open_tables(@cxns);
-   my @tables = map { extract_values($_, 'open_tables') } @open_tables;
+   my @tables = map { extract_values($_, $_, $_, 'open_tables') } @open_tables;
    push @display_lines, set_to_tbl(\@tables, 'open_tables'), get_cxn_errors(@cxns);
    draw_screen(\@display_lines);
 }
@@ -3075,9 +3271,6 @@
    my @visible = $opts{n} ? 'processlist' : get_visible_tables();
    my %wanted  = map { $_ => 1 } @visible;
 
-   # Config variable overrides %wanted here. TODO: this is hack-ish.
-   $wanted{q_header} = $config{show_QT_header}->{val};
-
    # Get the data
    my @cxns             = get_connections();
    my @full_processlist = get_full_processlist(@cxns);
@@ -3086,14 +3279,16 @@
    if ( $wanted{q_header} ) {
       get_status_info(@cxns);
       foreach my $cxn ( @cxns ) {
-         my $hash = extract_values($vars{$cxn}->{$clock}, 'q_header');
+         my $set = $vars{$cxn}->{$clock};
+         my $pre = $vars{$cxn}->{$clock-1} || $set;
+         my $hash = extract_values($set, $set, $pre, 'q_header');
          $hash->{cxn} = $cxn;
          $hash->{when} = 'Total';
          push @q_header, $hash;
 
          if ( exists $vars{$cxn}->{$clock - 1} ) {
             my $inc = inc(0, $cxn);
-            my $hash = extract_values($inc, 'q_header');
+            my $hash = extract_values($inc, $set, $pre, 'q_header');
             $hash->{cxn} = $cxn;
             $hash->{when} = 'Now';
             push @q_header, $hash;
@@ -3102,14 +3297,16 @@
    }
 
    if ( $wanted{processlist} ) {
-      push @processlist, map { extract_values($_, 'processlist') } @full_processlist;
+      # TODO: save prev values
+      push @processlist, map { extract_values($_, $_, $_, 'processlist') } @full_processlist;
    }
 
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       next unless $wanted{$tbl};
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    # Save queries in global variable for analysis.  The rows in %rows_for have been
@@ -3147,26 +3344,27 @@
 
    foreach my $cxn ( @cxns ) {
       my $set = $vars{$cxn}->{$clock};
+      my $pre = $vars{$cxn}->{$clock-1} || $set;
       my $inc; # Only assigned to if wanted
 
       if ( $set->{IB_ro_complete} ) {
          if ( $wanted{row_operations} ) {
             $inc ||= $incvar ? inc(0, $cxn) : $set;
-            push @row_operations, extract_values($inc, 'row_operations');
+            push @row_operations, extract_values($inc, $set, $pre, 'row_operations');
          }
          if ( $wanted{row_operation_misc} ) {
-            push @row_operation_misc, extract_values($set, 'row_operation_misc'),
+            push @row_operation_misc, extract_values($set, $set, $pre, 'row_operation_misc'),
          }
       }
 
       if ( $set->{IB_sm_complete} && $wanted{semaphores} ) {
          $inc ||= $incvar ? inc(0, $cxn) : $set;
-         push @semaphores, extract_values($inc, 'semaphores');
+         push @semaphores, extract_values($inc, $set, $pre, 'semaphores');
       }
 
       if ( $set->{IB_sm_wait_array_size} && $wanted{wait_array} ) {
          foreach my $wait ( @{$set->{IB_sm_waits}} ) {
-            my $hash = extract_values($wait, 'wait_array');
+            my $hash = extract_values($wait, $wait, $wait, 'wait_array');
             $hash->{cxn} = $cxn;
             push @wait_array, $hash;
          }
@@ -3176,151 +3374,211 @@
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
 }
 
-# display_S {{{3
-sub display_S {
-   my $min_width = 4;
-   my $inc       = $config{status_inc}->{val};
-   my ( $cxn )   = get_connections();
-   my $fmt       = get_var_set('S_set');
-   get_status_info( $cxn );
-   get_innodb_status([$cxn]); # TODO: might not be needed.
-
-   # Clear the screen if the display width changed.
-   if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
-      $lines_printed = 0;
-      $clear_screen_sub->();
-   }
-
-   # Decide how wide columns should be.
-   my $num_cols = scalar(@$fmt);
-   my $width    = $opts{n} ? 0 : max($min_width, int(($this_term_size[0] - $num_cols + 1) / $num_cols));
-
-   # Print headers every now and then.  Headers can get really long, so compact them.
-   my @hdr = @$fmt;
-   if ( $opts{n} ) {
-      if ( $lines_printed == 0 ) {
-         print join("\t", @hdr), "\n";
-      }
-   }
-   elsif ( $lines_printed % int( $this_term_size[1] - 2 ) == 0 ) {
-      @hdr = map { donut(crunch($_, $width), $width) } @hdr;
-      print join(' ', map { sprintf( "%${width}s", donut($_, $width)) } @hdr) . "\n";
-   }
-
-   # Design a column format for the values.
-   my $format
-      = $opts{n}
-      ? join("\t", map { '%s' } @$fmt) . "\n"
-      : join(' ',  map { "%${width}s" } @hdr) . "\n";
-
-   # Print the values.
-   my $set = $inc ? inc(0, $cxn) : $vars{$cxn}->{$clock};
-   printf($format,
-      map {
-            exists $set->{$_} ? $set->{$_}
-          : exists $exprs{$_} ? $exprs{$_}->{func}->($set)
-          :                     0
-      } @$fmt
-   );
-   $lines_printed++;
-}
-
 # display_T {{{3
 sub display_T {
    my @display_lines;
 
-   my @txns;
+   my @t_header;
+   my @innodb_transactions;
+   my %rows_for = (
+      t_header            => \@t_header,
+      innodb_transactions => \@innodb_transactions,
+   );
+
+   my @visible = $opts{n} ? 'innodb_transactions' : get_visible_tables();
+   my %wanted  = map { $_ => 1 } @visible;
+
    my @cxns = get_connections();
 
    # If the header is to be shown, buffer pool data is required.
-   my $hdr = ( !$opts{n} && 1 == scalar @cxns && $config{show_QT_header}->{val} );
+   get_innodb_status( \@cxns, [ $wanted{t_header} ? qw(bp) : () ] );
 
-   get_innodb_status( \@cxns, [ $hdr ? qw(bp) : () ] );
-
    foreach my $cxn ( get_connections() ) {
       my $set = $vars{$cxn}->{$clock};
+      my $pre = $vars{$cxn}->{$clock-1} || $set;
 
       next unless $set->{IB_tx_transactions};
 
-      if ( $set->{IB_tx_transactions} ) {
-         foreach my $txn ( @{$set->{IB_tx_transactions}} ) {
-            my $hash = extract_values($txn, 'innodb_transactions');
+      if ( $wanted{t_header} ) {
+         my $hash = extract_values($set, $set, $pre, 't_header');
+         push @t_header, $hash;
+      }
+
+      if ( $wanted{innodb_transactions} ) {
+         my $cur_txns = $set->{IB_tx_transactions};
+         my $pre_txns = $pre->{IB_tx_transactions} || $cur_txns;
+         my %cur_txns = map { $_->{mysql_thread_id} => $_ } @$cur_txns;
+         my %pre_txns = map { $_->{mysql_thread_id} => $_ } @$pre_txns;
+         foreach my $thd_id ( sort keys %cur_txns ) {
+            my $cur_txn = $cur_txns{$thd_id};
+            my $pre_txn = $pre_txns{$thd_id} || $cur_txn;
+            my $hash    = extract_values($cur_txn, $cur_txn, $pre_txn, 'innodb_transactions');
             $hash->{cxn} = $cxn;
-            push @txns, $hash;
+            push @innodb_transactions, $hash;
          }
       }
 
-      if ( $hdr ) {
-         push @display_lines, '', join(", ",
-            "History: $set->{IB_tx_history_list_len}",
-            "Versions: " . $exprs{OldVersions}->{func}->( $set ),
-            "Undo: $set->{IB_tx_purge_undo_for}",
-            "Dirty Bufs: " . percent($exprs{DirtyBufs}->{func}->( $set )) . '%',
-            "Used Bufs: " . percent($exprs{BufPoolFill}->{func}->( $set )) . '%',
-            "Max time: " . secs_to_time($exprs{MaxTxnTime}->{func}->( $set )),
-            "Lock structs: $set->{IB_tx_num_lock_structs}",
-         );
-      }
    }
 
-   push @display_lines, '', set_to_tbl(\@txns, 'innodb_transactions'), get_cxn_errors(@cxns);
+   my $first_table = 0;
+   foreach my $tbl ( @visible ) {
+      push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
+   }
 
+   # Save queries in global variable for analysis.  The rows in %rows_for have been
+   # filtered, etc as a side effect of set_to_tbl(), so they are the same as the rows
+   # that get pushed to the screen.
+   @current_queries = map {
+      my %hash;
+      @hash{ qw(cxn id db query) } = @{$_}{ qw(cxn mysql_thread_id db query_text) };
+      \%hash;
+   } @{$rows_for{innodb_transactions}};
+
    draw_screen(\@display_lines);
 }
 
-# display_V {{{3
-# TODO: when entering V mode, remove any non-contiguous stuff from %vars.
-sub display_V {
-   my @display_lines;
-   my ( $cxn ) = get_connections();
-   my $fmt     = get_var_set('V_set');
-   my $inc     = $config{status_inc}->{val};
-   my $num     = $config{num_status_sets}->{val};
+# display_S {{{3
+sub display_S {
+   my $fmt  = get_var_set('S_set');
+   my $func = $config{S_func}->{val};
+   my $inc  = $func eq 'g' || $config{status_inc}->{val};
 
-   get_status_info($cxn);
-   get_innodb_status([$cxn]); # TODO: might not be needed.
-
-   # Figure out how many past sets have actually been kept.
-   while ( !exists $vars{$cxn}->{$clock - $num} ) {
-      $num--;
+   # The table's meta-data is generated from the compiled var_set.
+   my ( $cols, $visible );
+   if ( $tbl_meta{var_status}->{fmt} && $fmt eq $tbl_meta{var_status}->{fmt} ) {
+      ( $cols, $visible ) = @{$tbl_meta{var_status}}{qw(cols visible)};
    }
-
-   # Build a meta dataset that can be used for a type-1 table
-   my $meta = { name => { hdr => 'Name', just => '-' } };
-   foreach my $set ( 0 .. $num ) {
-      $meta->{"set_$set"} = { hdr => "Set $set", just => '' };
+   else {
+      ( $cols, $visible ) = compile_select_stmt($fmt);
+      $tbl_meta{var_status}->{cols}    = $cols;
+      $tbl_meta{var_status}->{visible} = $visible;
+      $tbl_meta{var_status}->{fmt}     = $fmt;
+      map { $tbl_meta{var_status}->{cols}->{$_}->{just} = ''} @$visible;
    }
 
-   # Loop through them and do a 'pivot table' transformation on them.  Instead of
-   # sets becoming rows, sets must become columns, and variables become rows.
-   my @rows = map { { name => $_ } } @$fmt;
-   foreach my $set ( 0 .. $num ) {
-      my $vars = $inc ? inc($set, $cxn) : $vars{$cxn}->{$clock - $set};
-      foreach my $row ( 0.. @$fmt - 1 ) {
-         my $name = $fmt->[$row];
-         my $val = exists($vars->{$name}) ? $vars->{$name}
-                 : exists($exprs{$name})  ? $exprs{$name}->{func}->($vars)
-                 :                          0;
-         $rows[$row]->{"set_$set"} = defined $val ? $val : 0;
+   my @var_status;
+   my %rows_for = (
+      var_status => \@var_status,
+   );
+
+   my @visible = get_visible_tables();
+   my %wanted  = map { $_ => 1 } @visible;
+   my @cxns    = get_connections();
+
+   get_status_info(@cxns);
+   get_innodb_status(\@cxns);
+
+   # Set up whether to pivot and how many sets to extract.
+   $tbl_meta{var_status}->{pivot} = $func eq 'v';
+   my $num_sets = $func eq 'v' ? $config{num_status_sets}->{val} : 0;
+
+   foreach my $cxn ( @cxns ) {
+      foreach my $set ( 0 .. $num_sets ) {
+         my $vars = $inc ? inc($set, $cxn) : $vars{$cxn}->{$clock - $set};
+         my $cur  = $vars{$cxn}->{$clock-$set};
+         my $pre  = $vars{$cxn}->{$clock-$set-1} || $cur;
+         next unless $vars && %$vars;
+         my $hash = extract_values($vars, $cur, $pre, 'var_status');
+         push @var_status, $hash;
       }
    }
 
-   my @cols = 'name';
-   foreach my $set ( 0 .. $num ) {
-      push @cols, "set_$set";
-   }
+   # ################################################################
+   # Now there is specific display code based on $config{S_func}
+   # ################################################################
+   if ( $func =~ m/s|g/ ) {
+      my $min_width = 4;
 
-   push @display_lines, create_table( \@cols, $meta, \@rows);
+      # Clear the screen if the display width changed.
+      if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
+         $lines_printed = 0;
+         $clear_screen_sub->();
+      }
 
-   $clear_screen_sub->();
+      if ( $func eq 's' ) {
+         # Decide how wide columns should be.
+         my $num_cols = scalar(@$visible);
+         my $width    = $opts{n} ? 0 : max($min_width, int(($this_term_size[0] - $num_cols + 1) / $num_cols));
+         my $g_format = $opts{n} ? ( "%s\t" x $num_cols ) : ( "%-${width}s " x $num_cols );
 
-   draw_screen( \@display_lines );
+         # Print headers every now and then.  Headers can get really long, so compact them.
+         my @hdr = @$visible;
+         if ( $opts{n} ) {
+            if ( $lines_printed == 0 ) {
+               print join("\t", @hdr), "\n";
+               $lines_printed++;
+            }
+         }
+         elsif ( $lines_printed == 0 || $lines_printed > $this_term_size[1] - 2 ) {
+            @hdr = map { donut(crunch($_, $width), $width) } @hdr;
+            print join(' ', map { sprintf( "%${width}s", donut($_, $width)) } @hdr) . "\n";
+            $lines_printed = 1;
+         }
+
+         # Design a column format for the values.
+         my $format
+            = $opts{n}
+            ? join("\t", map { '%s' } @$visible) . "\n"
+            : join(' ',  map { "%${width}s" } @hdr) . "\n";
+
+         foreach my $row ( @var_status ) {
+            printf($format, map { defined $_ ? $_ : '' } @{$row}{ @$visible });
+            $lines_printed++;
+         }
+      }
+      else { # 'g' mode
+         # Design a column format for the values.
+         my $num_cols = scalar(@$visible);
+         my $width    = $opts{n} ? 0 : int(($this_term_size[0] - $num_cols + 1) / $num_cols);
+         my $format   = $opts{n} ? ( "%s\t" x $num_cols ) : ( "%-${width}s " x $num_cols );
+         $format      =~ s/\s$/\n/;
+
+         # Print headers every now and then.
+         if ( $opts{n} ) {
+            if ( $lines_printed == 0 ) {
+               print join("\t", @$visible), "\n";
+               print join("\t", map { shorten($mvs{$_}) } @$visible), "\n";
+            }
+         }
+         elsif ( $lines_printed == 0 || $lines_printed > $this_term_size[1] - 2 ) {
+            printf($format, map { donut(crunch($_, $width), $width) } @$visible);
+            printf($format, map { shorten($mvs{$_} || 0) } @$visible);
+            $lines_printed = 2;
+         }
+
+         # Update the max ever seen, and scale by the max ever seen.
+         my $set = $var_status[0];
+         foreach my $col ( @$visible ) {
+            $set->{$col}  = 1 unless defined $set->{$col} && $set->{$col} =~ m/$num_regex/;
+            $set->{$col}  = ($set->{$col} || 1) / ($set->{Uptime_hires} || 1);
+            $mvs{$col}    = max($mvs{$col} || 1, $set->{$col});
+            $set->{$col} /= $mvs{$col};
+         }
+         printf($format, map { ( $config{graph_char}->{val} x int( $width * $set->{$_} )) || '.' } @$visible );
+         $lines_printed++;
+
+      }
+   }
+   else { # 'v'
+      my $first_table = 0;
+      my @display_lines;
+      foreach my $tbl ( @visible ) {
+         push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
+         push @display_lines, get_cxn_errors(@cxns)
+            if ( $config{debug}->{val} || !$first_table++ );
+      }
+      $clear_screen_sub->();
+      draw_screen( \@display_lines );
+   }
 }
 
 # display_W {{{3
@@ -3342,26 +3600,37 @@
    # Get info on lock waits and OS wait array
    foreach my $cxn ( @cxns ) {
       my $set = $vars{$cxn}->{$clock} or next;
+      my $pre = $vars{$cxn}->{$clock-1} || $set;
 
-      if ( $wanted{lock_waits} && @{$set->{IB_tx_transactions}} ) {
+      if ( $wanted{lock_waits} && defined $set->{IB_tx_transactions} && @{$set->{IB_tx_transactions}} ) {
 
-         my @txns = @{$set->{IB_tx_transactions}};
-         foreach my $txn ( grep { $_->{lock_wait_status} } @txns ) {
-            my %lock_wait = map { $_ => $txn->{$_} }
-               qw(txn_id mysql_thread_id lock_wait_time active_secs);
-            my $wait_locks = $txn->{wait_locks};
-            map { $lock_wait{$_} = $wait_locks->{$_} }
-               qw(lock_type space_id page_no n_bits index db table txn_id
-                     lock_mode special insert_intention waiting num_locks);
-            $lock_wait{cxn} = $cxn;
-            push @lock_waits, extract_values(\%lock_wait, 'lock_waits');
+         my $cur_txns = $set->{IB_tx_transactions};
+         my $pre_txns = $pre->{IB_tx_transactions} || $cur_txns;
+         my %cur_txns = map { $_->{mysql_thread_id} => $_ } @$cur_txns;
+         my %pre_txns = map { $_->{mysql_thread_id} => $_ } @$pre_txns;
+         foreach my $thd_id ( map { $_->{mysql_thread_id} } grep { $_->{lock_wait_status} } @$cur_txns ) {
+            my $cur_txn = $cur_txns{$thd_id};
+            my $pre_txn = $pre_txns{$thd_id} || $cur_txn;
+
+            my %cur_lock_wait = map { $_ => $cur_txn->{$_} } qw(txn_id mysql_thread_id lock_wait_time active_secs);
+            my $cur_wait_locks = $cur_txn->{wait_locks};
+            map { $cur_lock_wait{$_} = $cur_wait_locks->{$_} } qw(lock_type space_id page_no n_bits index db table txn_id lock_mode special insert_intention waiting num_locks);
+            $cur_lock_wait{cxn} = $cxn;
+
+            my %pre_lock_wait = map { $_ => $pre_txn->{$_} } qw(txn_id mysql_thread_id lock_wait_time active_secs);
+            my $pre_wait_locks = $pre_txn->{wait_locks};
+            map { $pre_lock_wait{$_} = $pre_wait_locks->{$_} } qw(lock_type space_id page_no n_bits index db table txn_id lock_mode special insert_intention waiting num_locks);
+            $pre_lock_wait{cxn} = $cxn;
+
+            push @lock_waits, extract_values(\%cur_lock_wait, \%cur_lock_wait, \%pre_lock_wait, 'lock_waits');
          }
       }
 
       if ( $wanted{wait_array} && $set->{IB_sm_complete} ) {
          if ( $set->{IB_sm_wait_array_size} ) {
             foreach my $wait ( @{$set->{IB_sm_waits}} ) {
-               my $hash = extract_values($wait, 'wait_array');
+               # TODO: try to get prev values...
+               my $hash = extract_values($wait, $wait, $wait, 'wait_array');
                $hash->{cxn} = $cxn;
                push @wait_array, $hash;
             }
@@ -3372,7 +3641,8 @@
    my $first_table = 0;
    foreach my $tbl ( @visible ) {
       push @display_lines, '', set_to_tbl($rows_for{$tbl}, $tbl);
-      push @display_lines, get_cxn_errors(@cxns) unless $config{debug}->{val} || $first_table++;;
+      push @display_lines, get_cxn_errors(@cxns)
+         if ( $config{debug}->{val} || !$first_table++ );
    }
 
    draw_screen(\@display_lines);
@@ -3383,7 +3653,6 @@
    my $info = shift;
    my $cxn   = $info->{cxn};
    my $db    = $info->{db};
-   my $meta  = $dbhs{$cxn};
 
    my ( $mods, $query ) = rewrite_for_explain($info->{query});
 
@@ -3391,10 +3660,7 @@
 
    if ( $query ) {
 
-      my $part
-         = ( $meta->{ver_major} >= 5 && $meta->{ver_minor} >= 1 && $meta->{ver_rev} >= 5 )
-         ? 'PARTITIONS'
-         : '';
+      my $part = version_ge($dbhs{$cxn}->{dbh}, '5.1.5') ? 'PARTITIONS' : '';
       $query = "EXPLAIN $part\n" . $query;
 
       eval {
@@ -3410,7 +3676,7 @@
                create_table2(
                   $tbl_meta{explain}->{visible},
                   meta_to_hdr('explain'),
-                  extract_values($res, 'explain')));
+                  extract_values($res, $res, $res, 'explain')));
             @display_lines = stack_next(\@display_lines, \@this_table, { pad => '  ', vsep => 2 });
          }
       };
@@ -3433,7 +3699,8 @@
 }
 
 # rewrite_for_explain {{{3
-# Some replace/create/insert...select can be rewritten easily.
+# TODO: Some replace/create/insert...select can be rewritten easily.
+# TODO: update foo inner join bar can also be rewritten.
 sub rewrite_for_explain {
    my $query = shift;
 
@@ -3471,11 +3738,11 @@
          }
          do_query( $cxn, 'EXPLAIN EXTENDED ' . $query ) or die "Can't explain query";
          my $sth = do_query($cxn, 'SHOW WARNINGS');
-         my $res = $sth->fetchall_arrayref;
+         my $res = $sth->fetchall_arrayref({});
 
          if ( $res ) {
             foreach my $result ( @$res ) {
-               push @display_lines, 'Note:', no_ctrl_char($result->[2]);
+               push @display_lines, 'Note:', no_ctrl_char($result->{message});
             }
          }
          else {
@@ -3501,25 +3768,60 @@
 
    # Get globally mapped keys, then overwrite them with mode-specific ones.
    my %keys = map {
-         my $key = $action_for{$_}->{key} || $_;
-         $key => $action_for{$_}->{label}
+         $_ => $action_for{$_}->{label}
       } keys %action_for;
    foreach my $key ( keys %{$modes{$mode}->{action_for}} ) {
       $keys{$key} = $modes{$mode}->{action_for}->{$key}->{label};
    }
    delete $keys{'?'};
 
-   my @display_lines = ( '', 'The following keys are mapped in this mode:', '', );
-   push @display_lines,  create_table2(
-      [ sort keys %keys ],
-      { map { $_ => $_ } keys %keys },
-      \%keys,
-      { sep => '    ' }
-   );
-   push @display_lines, '', 'Any other key refreshes the display.', '';
+   # Split them into three kinds of keys: MODE keys, action keys, and
+   # magic (special character) keys.
+   my @modes   = sort grep { m/[A-Z]/   } keys %keys;
+   my @actions = sort grep { m/[a-z]/   } keys %keys;
+   my @magic   = sort grep { m/[^A-Z]/i } keys %keys;
+
+   my @display_lines = ( '', 'Switch to a different mode:' );
+
+   # Mode keys
+   my @all_modes = map { "$_  $modes{$_}->{hdr}" } @modes;
+   my @col1 = splice(@all_modes, 0, ceil(@all_modes/3));
+   my @col2 = splice(@all_modes, 0, ceil(@all_modes/2));
+   my $max1 = max(map {length($_)} @col1);
+   my $max2 = max(map {length($_)} @col2);
+   while ( @col1 ) {
+      push @display_lines, sprintf("   %-${max1}s  %-${max2}s  %s",
+         (shift @col1      || ''),
+         (shift @col2      || ''),
+         (shift @all_modes || ''));
+   }
+
+   # Action keys
+   my @all_actions = map { "$_  $keys{$_}" } @actions;
+   @col1 = splice(@all_actions, 0, ceil(@all_actions/2));
+   $max1 = max(map {length($_)} @col1);
+   push @display_lines, '', 'Actions:';
+   while ( @col1 ) {
+      push @display_lines, sprintf("   %-${max1}s  %s",
+         (shift @col1        || ''),
+         (shift @all_actions || ''));
+   }
+
+   # Magic keys
+   my @all_magic = map { sprintf('%4s', $action_for{$_}->{key} || $_) . "  $keys{$_}" } @magic;
+   @col1 = splice(@all_magic, 0, ceil(@all_magic/2));
+   $max1 = max(map {length($_)} @col1);
+   push @display_lines, '', 'Other:';
+   while ( @col1 ) {
+      push @display_lines, sprintf("%-${max1}s%s",
+         (shift @col1      || ''),
+         (shift @all_magic || ''));
+   }
+
    $clear_screen_sub->();
    draw_screen(\@display_lines, { show_all => 1 } );
    pause();
+   $clear_screen_sub->();
 }
 
 # show_full_query {{{3
@@ -3719,6 +4021,11 @@
    $prefs ||= {};
    $prefs->{no_hdr} ||= ($opts{n} && $clock != 1);
 
+   # Truncate rows that will surely be off screen even if this is the only table.
+   if ( !$opts{n} && !$prefs->{raw} && !$prefs->{show_all} && $this_term_size[1] < @$data-1 ) {
+      $data = [ @$data[0..$this_term_size[1] - 1] ];
+   }
+
    my @rows = ();
 
    if ( @$cols && %$info ) {
@@ -3735,6 +4042,15 @@
       if ( !$opts{n} ) {
          %width_for = map {
             my $col_name  = $_;
+            if ( $info->{$_}->{dec} ) {
+               # Align along the decimal point
+               my $max_rodp = max(0, map { $_->{$col_name} =~ m/([^\s\d-].*)$/ ? length($1) : 0 } @$data);
+               foreach my $row ( @$data ) {
+                  my $col = $row->{$col_name};
+                  my ( $l, $r ) = $col =~ m/^([\s\d]*)(.*)$/;
+                  $row->{$col_name} = sprintf("%s%-${max_rodp}s", $l, $r);
+               }
+            }
             my $max_width = max( length($info->{$_}->{hdr}), map { length($_->{$col_name}) } @$data);
             if ( $info->{$col_name}->{maxw} ) {
                $max_width = min( $max_width, $info->{$col_name}->{maxw} );
@@ -3747,7 +4063,7 @@
       }
 
       # The table header.
-      if ( !$prefs->{no_hdr} ) {
+      if ( !$config{hide_hdr}->{val} && !$prefs->{no_hdr} ) {
          push @rows, $opts{n}
             ? join( $col_sep, @$cols )
             : join( $col_sep, map { sprintf( "%-$width_for{$_}s", trunc($info->{$_}->{hdr}, $width_for{$_}) ) } @$cols );
@@ -3794,30 +4110,37 @@
       eval {
          @$rows = grep { $filters{$filter}->{func}->($_) } @$rows;
       };
+      if ( $EVAL_ERROR && $config{debug}->{val} ) {
+         die $EVAL_ERROR;
+      }
    }
 
-   # Sort.
-   if ( @$rows && $meta->{sort_func} ) {
-      if ( $meta->{sort_dir} > 0 ) {
-         @$rows = $meta->{sort_func}->( @$rows );
+   if ( !$meta->{pivot} ) {
+      # Sort.
+      if ( @$rows && $meta->{sort_func} ) {
+         if ( $meta->{sort_dir} > 0 ) {
+            @$rows = $meta->{sort_func}->( @$rows );
+         }
+         else {
+            @$rows = reverse $meta->{sort_func}->( @$rows );
+         }
       }
-      else {
-         @$rows = reverse $meta->{sort_func}->( @$rows );
-      }
    }
 
    # Stop altering arguments now.
    my @rows = @$rows;
 
-   # Colorize.  Adds a _color column to rows.
-   if ( @rows && $meta->{color_func} ) {
-      eval {
-         foreach my $row ( @rows ) {
-            $row->{_color} = $meta->{color_func}->($row);
+   if ( !$meta->{pivot} ) {
+      # Colorize.  Adds a _color column to rows.
+      if ( @rows && $meta->{color_func} ) {
+         eval {
+            foreach my $row ( @rows ) {
+               $row->{_color} = $meta->{color_func}->($row);
+            }
+         };
+         if ( $EVAL_ERROR ) {
+            pause($EVAL_ERROR);
          }
-      };
-      if ( $EVAL_ERROR ) {
-         pause($EVAL_ERROR);
       }
    }
 
@@ -3833,10 +4156,33 @@
       }
    }
 
-   @rows = create_table( $meta->{visible}, $meta->{cols}, \@rows);
-   if ( !$meta->{hide_hdr} && !$opts{n} && $config{display_table_captions}->{val} ) {
-      @rows = create_caption($meta->{hdr}, @rows)
+   my ($fmt_cols, $fmt_meta);
+
+   # Pivot.
+   if ( $meta->{pivot} ) {
+      my @vars = @{$meta->{visible}};
+      my @tmp  = map { { name => $_ } } @vars;
+      my @cols = 'name';
+      foreach my $i ( 0..@$rows-1 ) {
+         my $col = "set_$i";
+         push @cols, $col;
+         foreach my $j ( 0.. at vars-1 ) {
+            $tmp[$j]->{$col} = $rows[$i]->{$vars[$j]};
+         }
+      }
+      $fmt_meta = { map { $_ => { hdr => $_, just => '-'} } @cols };
+      $fmt_cols = \@cols;
+      @rows = @tmp;
    }
+   else {
+      $fmt_meta = $meta->{cols};
+      $fmt_cols = $meta->{visible};
+   }
+
+   @rows = create_table( $fmt_cols, $fmt_meta, \@rows);
+   if ( !$meta->{hide_caption} && !$opts{n} && $config{display_table_captions}->{val} ) {
+      @rows = create_caption($meta->{capt}, @rows)
+   }
    return @rows;
 }
 
@@ -3852,7 +4198,8 @@
 # commify {{{3
 # From perlfaq5: add commas.
 sub commify {
-   my $num = shift;
+   my ( $num ) = @_;
+   $num = 0 unless defined $num;
    $num =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
    return $num;
 }
@@ -3861,6 +4208,7 @@
 # Trim to desired precision.
 sub set_precision {
    my ( $num, $precision ) = @_;
+   $precision = $config{num_digits}->{val} if !defined $precision;
    sprintf("%.${precision}f", $num);
 }
 
@@ -3868,15 +4216,17 @@
 # Convert to percent
 sub percent {
    my ( $num ) = @_;
+   $num = 0 unless defined $num;
    my $digits = $config{num_digits}->{val};
-   sprintf("%.${digits}f", $num * 100);
+   return sprintf("%.${digits}f", $num * 100)
+      . ($config{show_percent}->{val} ? '%' : '');
 }
 
 # shorten {{{3
 sub shorten {
    my ( $num, $opts ) = @_;
 
-   return $num if !defined($num) || $opts{n} || $num =~ m/[^\d\.-]/;
+   return $num if !defined($num) || $opts{n} || $num !~ m/$num_regex/;
 
    $opts ||= {};
    my $pad = defined $opts->{pad} ? $opts->{pad} : '';
@@ -4049,29 +4399,24 @@
    }
    $clear_screen_sub->() unless $modes{$config{mode}->{val}}->{no_clear_screen};
    if ( $opts{n} || $prefs->{raw} ) {
+      my $num_lines = 0;
       print join("\n",
          map {
+            $num_lines++;
             ref $_
                ? colored($_->[0], $_->[1])
                : $_;
          }
-         grep { !$opts{n} || $_ } # When non-interactive, suppress empty lines
+         grep { !$opts{n} || $_ } # Suppress empty lines
          @$display_lines);
-      if ( $opts{n} ) {
+      if ( $opts{n} && $num_lines ) {
          print "\n";
       }
    }
-   elsif ( $prefs->{show_all} ) {
-      print join("\n",
-            map {
-               ref $_
-                  ? colored(substr($_->[0], 0, $this_term_size[0]), $_->[1])
-                  : substr($_, 0, $this_term_size[0]);
-            }
-         @$display_lines);
-   }
    else {
-      my $max_lines = min(scalar(@$display_lines), $this_term_size[1]);
+      my $max_lines = $prefs->{show_all}
+         ? scalar(@$display_lines)- 1
+         : min(scalar(@$display_lines), $this_term_size[1]);
       print join("\n",
          map {
             ref $_
@@ -4135,56 +4480,58 @@
    # The thingie in top-right that says what we're monitoring.
    my $cxn = '';
 
-   if ( 1 == @cxns ) {
-      $cxn = $dbhs{$cxns[0]}->{mysql_version};
+   if ( 1 == @cxns && $dbhs{$cxns[0]} && $dbhs{$cxns[0]}->{dbh} ) {
+      $cxn = $dbhs{$cxns[0]}->{dbh}->{mysql_serverinfo} || '';
    }
    else {
       if ( $modes{$mode}->{server_group} ) {
          $cxn = "Servers: " . $modes{$mode}->{server_group};
-         my $err_count = grep { $dbhs{$_}->{err_count} } @cxns;
+         my $err_count = grep { $dbhs{$_} && $dbhs{$_}->{err_count} } @cxns;
          if ( $err_count ) {
             $cxn .= "(" . ( scalar(@cxns) - $err_count ) . "/" . scalar(@cxns) . ")";
          }
       }
       else {
-         $cxn = join(' ', map { ($dbhs{$_}->{err_count} ? '!' : '') . $_ } @cxns);
+         $cxn = join(' ', map { ($dbhs{$_}->{err_count} ? '!' : '') . $_ }
+            grep { $dbhs{$_} } @cxns);
       }
    }
 
    if ( 1 == @cxns ) {
-      get_status_info(@cxns);
+      get_driver_status(@cxns);
       my $vars = $vars{$cxns[0]}->{$clock};
+      my $inc  = inc(0, $cxns[0]);
 
-      # Format server uptime human-readably.
-      my $uptime = secs_to_time( $vars->{Uptime} );
-      my $inc    = inc(0, $cxns[0]);
-      my $qps    = set_precision($exprs{QPS}->{func}->($inc), 2);
+      # Format server uptime human-readably, calculate QPS...
+      my $uptime = secs_to_time( $vars->{Uptime_hires} );
+      my $qps    = ($inc->{Questions}||0) / ($inc->{Uptime_hires}||1);
       my $ibinfo = '';
 
       if ( exists $vars->{IB_last_secs} ) {
-         $ibinfo .= "InnoDB $vars->{IB_last_secs} sec ";
+         $ibinfo .= "InnoDB $vars->{IB_last_secs}s ";
          if ( $vars->{IB_got_all} ) {
             if ( ($mode eq 'T' || $mode eq 'W')
                   && $vars->{IB_tx_is_truncated} ) {
-               $ibinfo .= ':^|, ';
+               $ibinfo .= ':^|';
             }
             else {
-               $ibinfo .= ':-), ';
+               $ibinfo .= ':-)';
             }
          }
          else {
-            $ibinfo .= ':-(, ';
+            $ibinfo .= ':-(';
          }
       }
       $result = sprintf(
          "%-${mode_width}s %${remaining_width}s",
          $modeline,
-         $ibinfo . join(', ',
-            "$qps QPS",
+         join(', ', grep { $_ } (
             $cxns[0],
-            ($vars->{Threads_connected} || 0) . " thd",
             $uptime,
-            $cxn));
+            $ibinfo,
+            shorten($qps) . " QPS",
+            ($vars->{Threads} || 0) . " thd",
+            $cxn)));
    }
    else {
       $result = sprintf(
@@ -4216,24 +4563,19 @@
    my $dsn;
    do {
       $clear_screen_sub->();
-      print "Typical DSN strings look like\n   DBI:mysql:db;host=hostname;port=port\n"
-         . "The db and port are optional and can typically be omitted.\n\n";
-      $dsn = prompt("Enter a DSN string", undef, "DBI:mysql:;host=$name");
+      print "Typical DSN strings look like\n   DBI:mysql:;host=hostname;port=port\n"
+         . "The db and port are optional and can usually be omitted.\n"
+         . "If you specify 'mysql_read_default_group=mysql' many options can be read\n"
+         . "from your mysql options files (~/.my.cnf, /etc/my.cnf).\n\n";
+      $dsn = prompt("Enter a DSN string", undef, "DBI:mysql:;mysql_read_default_group=mysql;host=$name");
    } until ( $dsn );
 
-   my $user = $ENV{USERNAME} || $ENV{USER} || getlogin() || getpwuid($REAL_USER_ID) || undef;
-   do {
-      $clear_screen_sub->();
-      $user = prompt("Enter a username for $name", undef, $user);
-   } until ( $user );
-
    $clear_screen_sub->();
    my $dl_table = prompt("Optional: enter a table (must not exist) to use when resetting InnoDB deadlock information",
-      undef, 'test.innodb_deadlock_maker');
+      undef, 'test.innotop_dl');
 
    $connections{$name} = {
       dsn      => $dsn,
-      user     => $user,
       dl_table => $dl_table,
    };
 }
@@ -4261,14 +4603,15 @@
    } until ( @cxns );
 
    $server_groups{$name} = \@cxns;
+   return $name;
 }
 
 sub get_var_set {
    my ( $name ) = @_;
-   while ( !exists($var_sets{$config{$name}->{val}}) ) {
+   while ( !$name || !exists($var_sets{$config{$name}->{val}}) ) {
       $name = choose_var_set($name);
    }
-   return $var_sets{$config{$name}->{val}};
+   return $var_sets{$config{$name}->{val}}->{text};
 }
 
 sub add_new_var_set {
@@ -4291,9 +4634,21 @@
       $variables = prompt("Enter variables for $name", undef );
    } until ( $variables );
 
-   $var_sets{$name} = [ unique(grep { $_ } split(/\s+/, $variables)) ];
+   $var_sets{$name} = { text => $variables, user => 1 };
 }
 
+sub next_server {
+   my $mode     = $config{mode}->{val};
+   my @cxns     = sort keys %connections;
+   my ($cur)    = get_connections($mode);
+   $cur         ||= $cxns[0];
+   my $pos      = grep { $_ lt $cur } @cxns;
+   my $newpos   = ($pos + 1) % @cxns;
+   $modes{$mode}->{server_group} = '';
+   $modes{$mode}->{connections} = [ $cxns[$newpos] ];
+   $clear_screen_sub->();
+}
+
 sub next_server_group {
    my $mode = shift || $config{mode}->{val};
    my @grps = sort keys %server_groups;
@@ -4371,8 +4726,10 @@
    my ( $cxn, $stmt_name, @args ) = @_;
 
    # Test if the cxn should not even be tried
-   return undef
-      if $dbhs{$cxn} && $dbhs{$cxn}->{err_count} && $dbhs{$cxn}->{wake_up} > $clock;
+   return undef if $dbhs{$cxn}
+      && $dbhs{$cxn}->{err_count} 
+      && ( !$dbhs{$cxn}->{dbh} || !$dbhs{$cxn}->{dbh}->{Active} || $dbhs{$cxn}->{mode} eq $config{mode}->{val} )
+      && $dbhs{$cxn}->{wake_up} > $clock;
 
    my $sth;
    my $retries = 1;
@@ -4385,30 +4742,21 @@
 
          # If the prepared query doesn't exist, make it.
          if ( !exists $dbhs{$cxn}->{stmts}->{$stmt_name} ) {
-            $dbhs{$cxn}->{stmts}->{$stmt_name}
-               = $dbh->prepare($stmt_maker_for{$stmt_name}->($cxn));
+            $dbhs{$cxn}->{stmts}->{$stmt_name} = $stmt_maker_for{$stmt_name}->($dbh);
          }
 
          $sth = $dbhs{$cxn}->{stmts}->{$stmt_name};
-         $sth->execute(@args);
+         if ( $sth ) {
+            $sth->execute(@args);
+         }
          $success = 1;
       };
       if ( $EVAL_ERROR ) {
-         my $errs = join('|',
-            'Access denied for user',
-            'Unknown MySQL server host',
-            'Unknown database',
-            'Can\'t connect to local MySQL server through socket',
-            'Can\'t connect to MySQL server on',
-            'MySQL server has gone away',
-            'Cannot call SHOW INNODB STATUS',
-            'Access denied',
-         );
-         if ( $EVAL_ERROR =~ m/$errs/ ) {
+         if ( $EVAL_ERROR =~ m/$nonfatal_errs/ ) {
             handle_cxn_error($cxn, $EVAL_ERROR);
          }
          else {
-            die $EVAL_ERROR;
+            die "$cxn $stmt_name: $EVAL_ERROR";
          }
          if ( $retries < 0 ) {
             $sth = undef;
@@ -4416,7 +4764,10 @@
       }
    }
 
-   return $sth;
+   if ( $sth && $sth->{NUM_OF_FIELDS} ) {
+      sleep($stmt_sleep_time_for{$stmt_name}) if $stmt_sleep_time_for{$stmt_name};
+      return $sth;
+   }
 }
 
 # Keeps track of error count, sleep times till retries, etc etc.
@@ -4427,6 +4778,11 @@
    my $meta = $dbhs{$cxn};
    $meta->{err_count}++;
 
+   # This is used so errors that have to do with permissions needed by the current
+   # mode will get displayed as long as we're in this mode, but get ignored if the
+   # mode changes.
+   $meta->{mode} = $config{mode}->{val};
+
    # Strip garbage from the error text if possible.
    $err =~ s/\s+/ /g;
    if ( $err =~ m/failed: (.*?) at \S*innotop line/ ) {
@@ -4449,8 +4805,10 @@
    my ( $cxn, $query ) = @_;
 
    # Test if the cxn should not even be tried
-   return undef
-      if $dbhs{$cxn} && $dbhs{$cxn}->{err_count} && $dbhs{$cxn}->{wake_up} > $clock;
+   return undef if $dbhs{$cxn}
+      && $dbhs{$cxn}->{err_count} 
+      && ( !$dbhs{$cxn}->{dbh} || !$dbhs{$cxn}->{dbh}->{Active} || $dbhs{$cxn}->{mode} eq $config{mode}->{val} )
+      && $dbhs{$cxn}->{wake_up} > $clock;
 
    my $sth;
    my $retries = 1;
@@ -4466,15 +4824,7 @@
          $success = 1;
       };
       if ( $EVAL_ERROR ) {
-         my $errs = join('|',
-            'Access denied for user',
-            'Unknown MySQL server host',
-            'Unknown database',
-            'Can\'t connect to local MySQL server through socket',
-            'Can\'t connect to MySQL server on',
-            'MySQL server has gone away',
-         );
-         if ( $EVAL_ERROR =~ m/$errs/ ) {
+         if ( $EVAL_ERROR =~ m/$nonfatal_errs/ ) {
             handle_cxn_error($cxn, $EVAL_ERROR);
          }
          else {
@@ -4502,42 +4852,86 @@
    };
    my $href = $dbhs{$cxn};
 
-   if ( !$href->{dbh} || !$href->{dbh}->ping ) {
+   if ( !$href->{dbh} || ref($href->{dbh}) !~ m/DBI/ || !$href->{dbh}->ping ) {
       my $dbh = get_new_db_connection($cxn);
-      $href->{dbh} = $dbh;
+      @{$href}{qw(dbh err_count wake_up this_sleep start_time prev_sleep)}
+               = ($dbh, 0, 0, 1, 0, 0);
 
-      # Get version and connection ID.  This is necessary to do repeatedly
-      # because we may disconnect and connect again.
-      my ($version, $connection_id)
-         = $dbh->selectrow_array("SELECT VERSION(), CONNECTION_ID()");
-      @{$href}{qw(mysql_version connection_id)} = ($version, $connection_id);
-      @{$href}{qw(ver_major ver_minor ver_rev)} = $version =~ m/^(\d+)\.(\d+)\.(\d+)/;
-
       # Derive and store the server's start time in hi-res
-      my $uptime = $dbh->selectrow_hashref("show status like 'Uptime'")->{Value};
+      my $uptime = $dbh->selectrow_hashref("show status like 'Uptime'")->{value};
       $href->{start_time} = time() - $uptime;
 
-      # Set timeouts to 8 hours so an unused connection stays alive
-      # (for example, a connection might be used in Q mode but idle in T mode).
-      $dbh->do("set session wait_timeout=28800, interactive_timeout=28800");
+      # Set timeouts so an unused connection stays alive.
+      # For example, a connection might be used in Q mode but idle in T mode.
+      if ( version_ge($dbh, '4.0.3')) {
+         my $timeout = $config{cxn_timeout}->{val};
+         $dbh->do("set session wait_timeout=$timeout, interactive_timeout=$timeout");
+      }
    }
    return $href->{dbh};
 }
 
+# Compares versions like 5.0.27 and 4.1.15-standard-log
+sub version_ge {
+   my ( $dbh, $target ) = @_;
+   my $version = sprintf('%03d%03d%03d', $dbh->{mysql_serverinfo} =~ m/(\d+)/g);
+   return $version ge sprintf('%03d%03d%03d', $target =~ m/(\d+)/g);
+}
+
+# Extracts status values that can be gleaned from the DBD driver without doing a whole query.
+sub get_driver_status {
+   my @cxns = @_;
+   if ( !$info_gotten{driver_status}++ ) {
+      foreach my $cxn ( @cxns ) {
+         next unless $dbhs{$cxn} && $dbhs{$cxn}->{dbh} && $dbhs{$cxn}->{dbh}->{Active};
+         $vars{$cxn}->{$clock} ||= {};
+         my $vars = $vars{$cxn}->{$clock};
+         my %res = map {  $_ =~ s/ +/_/g; $_ } $dbhs{$cxn}->{dbh}->{mysql_stat} =~ m/(\w[^:]+): ([\d\.]+)/g;
+         map { $vars->{$_} ||= $res{$_} } keys %res;
+         $vars->{Uptime_hires} ||= time() - $dbhs{$cxn}->{start_time};
+         $vars->{cxn} = $cxn;
+      }
+   }
+}
+
 sub get_new_db_connection {
    my ( $connection, $destroy ) = @_;
-   my $dsn = $connections{$connection};
-   if ( !$dsn->{pass} && !$dsn->{savepass} ) {
-      $dsn->{pass} = prompt_noecho("Enter password for $dsn->{user} on $connection");
+   my $dsn = $connections{$connection}
+      or die "No connection named '$connection' is defined in your configuration";
+
+   if ( !defined $dsn->{have_user} ) {
+      my $answer = prompt("Do you want to specify a username for $connection?", undef, 'n');
+      $dsn->{have_user} = $answer && $answer =~ m/1|y/i;
+   }
+
+   if ( !defined $dsn->{have_pass} ) {
+      my $answer = prompt("Do you want to specify a password for $connection?", undef, 'n');
+      $dsn->{have_pass} = $answer && $answer =~ m/1|y/i;
+   }
+
+   if ( !$dsn->{user} && $dsn->{have_user} ) {
+      my $user = $ENV{USERNAME} || $ENV{USER} || getlogin() || getpwuid($REAL_USER_ID) || undef;
+      $dsn->{user} = prompt("Enter username for $connection", undef, $user);
+   }
+
+   if ( !defined $dsn->{user} ) {
+      $dsn->{user} = '';
+   }
+
+   if ( !$dsn->{pass} && !$dsn->{savepass} && $dsn->{have_pass} ) {
+      $dsn->{pass} = prompt_noecho("Enter password for '$dsn->{user}' on $connection");
+      print "\n";
       if ( !defined($dsn->{savepass}) ) {
-         print "\n";
-         $dsn->{savepass} = prompt("Save password in plain text in the config file? 1 or 0", undef, 1);
+         my $answer = prompt("Save password in plain text in the config file?", undef, 'y');
+         $dsn->{savepass} = $answer && $answer =~ m/1|y/i;
       }
    }
+
    my $dbh = DBI->connect(
       $dsn->{dsn}, $dsn->{user}, $dsn->{pass},
       { RaiseError => 1, PrintError => 0, AutoCommit => 1 });
    $dbh->{InactiveDestroy} = 1 unless $destroy; # Can't be set in $db_options
+   $dbh->{FetchHashKeyName} = 'NAME_lc'; # Lowercases all column names for fetchrow_hashref
    return $dbh;
 }
 
@@ -4546,17 +4940,48 @@
    return () unless $config{show_cxn_errors_in_tbl}->{val};
    return
       map  { [ $_ . ': ' . $dbhs{$_}->{last_err}, 'red' ] }
-      grep { $dbhs{$_}->{err_count} }
+      grep { $dbhs{$_} && $dbhs{$_}->{err_count} && $dbhs{$_}->{mode} eq $config{mode}->{val} }
       @cxns;
 }
 
 # Setup and tear-down functions {{{2
+
+# Takes a string and turns it into a hashref you can apply to %tbl_meta tables.  The string
+# can be in the form 'foo, bar, foo/bar, foo as bar' much like a SQL SELECT statement.
+sub compile_select_stmt {
+   my ($str) = @_;
+   my @exps = $str =~ m/\s*([^,]+(?i:\s+as\s+[^,\s]+)?)\s*(?=,|$)/g;
+   my %cols;
+   my @visible;
+   foreach my $exp ( @exps ) {
+      my ( $text, $colname );
+      if ( $exp =~ m/as\s+(\w+)\s*/ ) {
+         $colname = $1;
+         $exp =~ s/as\s+(\w+)\s*//;
+         $text    = $exp;
+      }
+      else {
+         $text = $colname = $exp;
+      }
+      my ($func, $err) = compile_expr($text);
+      $cols{$colname} = {
+         src  => $text,
+         hdr  => $colname,
+         num  => 0,
+         func => $func,
+      };
+      push @visible, $colname;
+   }
+   return (\%cols, \@visible);
+}
+
 # compile_filter {{{3
 sub compile_filter {
    my ( $text ) = @_;
    my ( $sub, $err );
    eval "\$sub = sub { my \$set = shift; $text }";
    if ( $EVAL_ERROR ) {
+      $EVAL_ERROR =~ s/at \(eval.*$//;
       $sub = sub { return $EVAL_ERROR };
       $err = $EVAL_ERROR;
    }
@@ -4564,15 +4989,34 @@
 }
 
 # compile_expr {{{3
-# TODO: strip off "at (eval..." from error.
 sub compile_expr {
-   my ( $expr, $simple ) = @_;
-   if ( $simple ) {
-      $expr =~ s/([A-Za-z]\w+)/\$set->{$1}/g;
+   my ( $expr ) = @_;
+   # Leave built-in functions alone so they get called as Perl functions, unless
+   # they are the only word in $expr, in which case treat them as hash keys.
+   if ( $expr =~ m/\W/ ) {
+      $expr =~ s/(?<!\{|\$)\b([A-Za-z]\w{2,})\b/is_func($1) ? $1 : "\$set->{$1}"/eg;
    }
+   else {
+      $expr = "\$set->{$expr}";
+   }
    my ( $sub, $err );
-   eval "\$sub = sub { my \$set = shift; $expr }";
+   my $quoted = quotemeta($expr);
+   eval qq{
+      \$sub = sub {
+         my (\$set, \$cur, \$pre) = \@_;
+         my \$val = eval { $expr };
+         if ( \$EVAL_ERROR && \$config{debug}->{val} ) {
+            \$EVAL_ERROR =~ s/ at \\(eval.*//s;
+            die "\$EVAL_ERROR in expression $quoted";
+         }
+         return \$val;
+      }
+   };
    if ( $EVAL_ERROR ) {
+      if ( $config{debug}->{val} ) {
+         die $EVAL_ERROR;
+      }
+      $EVAL_ERROR =~ s/ at \(eval.*$//;
       $sub = sub { return $EVAL_ERROR };
       $err = $EVAL_ERROR;
    }
@@ -4630,10 +5074,10 @@
                   # If the config file is between the endpoints and innotop is greater than
                   # the endpoint, innotop has a newer config file format than the file.
                   if ( $cfg_ver ge $start && $cfg_ver lt $end && $innotop_ver ge $end ) {
-                     my $msg = "Innotop's config file format has changed.  Overwrite $filename?  y or n";
+                     my $msg = "innotop's config file format has changed.  Overwrite $filename?  y or n";
                      if ( pause($msg) eq 'n' ) {
                         $config{readonly}->{val} = 1;
-                        print "\nInnotop will not save any configuration changes you make.";
+                        print "\ninnotop will not save any configuration changes you make.";
                         pause();
                         print "\n";
                      }
@@ -4661,15 +5105,11 @@
 }
 
 # Do some post-processing on %tbl_meta: compile src properties into func etc.
-# TODO: allow 'foo/bar as bif' syntax.
 sub post_process_tbl_meta {
    foreach my $table ( values %tbl_meta ) {
       foreach my $col_name ( keys %{$table->{cols}} ) {
          my $col_def = $table->{cols}->{$col_name};
-         my ( $sub, $err )
-            = $col_def->{expr}
-            ? $col_def->{src}->{func} # Already compiled
-            : compile_expr($col_def->{src}, 1);
+         my ( $sub, $err ) = compile_expr($col_def->{src});
          $col_def->{func} = $sub;
       }
    }
@@ -4684,7 +5124,8 @@
       last if $line =~ m/^\[/;
 
       my ( $mode, $group ) = $line =~ m/^(.*?)=(.*)$/;
-      next unless $mode && $group;
+      next unless $mode && $group
+         && exists $modes{$mode} && exists $server_groups{$group};
       $modes{$mode}->{server_group} = $group;
    }
 }
@@ -4692,7 +5133,7 @@
 # save_config_active_server_groups {{{3
 sub save_config_active_server_groups {
    my $file = shift;
-   foreach my $mode ( keys %modes ) {
+   foreach my $mode ( sort keys %modes ) {
       print $file "$mode=$modes{$mode}->{server_group}\n";
    }
 }
@@ -4716,7 +5157,7 @@
 # save_config_server_groups {{{3
 sub save_config_server_groups {
    my $file = shift;
-   foreach my $set ( keys %server_groups ) {
+   foreach my $set ( sort keys %server_groups ) {
       print $file "$set=", join(' ', @{$server_groups{$set}}), "\n";
    }
 }
@@ -4731,17 +5172,19 @@
 
       my ( $name, $rest ) = $line =~ m/^(.*?)=(.*)$/;
       next unless $name && $rest;
-      my @vars = unique(map { $_ } split(/\s+/, $rest));
-      next unless @vars;
-      $var_sets{$name} = \@vars;
+      $var_sets{$name} = {
+         text => $rest,
+         user => 1,
+      };
    }
 }
 
 # save_config_varsets {{{3
 sub save_config_varsets {
    my $file = shift;
-   foreach my $varset ( keys %var_sets ) {
-      print $file "$varset=", join(' ', @{$var_sets{$varset}}), "\n";
+   foreach my $varset ( sort keys %var_sets ) {
+      next unless $var_sets{$varset}->{user};
+      print $file "$varset=$var_sets{$varset}->{text}\n";
    }
 }
 
@@ -4782,8 +5225,8 @@
 # save_config_filters {{{3
 sub save_config_filters {
    my $file = shift;
-   foreach my $key ( keys %filters ) {
-      next unless $filters{$key}->{user};
+   foreach my $key ( sort keys %filters ) {
+      next if !$filters{$key}->{user} || $filters{$key}->{quick};
       my $text = $filters{$key}->{text};
       $text =~ s/([\\'])/\\$1/g;
       my $tbls = join(" ", @{$filters{$key}->{tbls}});
@@ -4800,18 +5243,18 @@
       last if $line =~ m/^\[/;
 
       my ( $mode, $rest ) = $line =~ m/^(.*?)=(.*)$/;
-      next unless $mode;
-      if ( exists $modes{$mode} ) {
-         $modes{$mode}->{visible_tables} =
-            [ unique(grep { $_ && exists $tbl_meta{$_} } split(/\s+/, $rest)) ];
-      }
+      next unless $mode && exists $modes{$mode};
+      $modes{$mode}->{visible_tables} =
+         [ unique(grep { $_ && exists $tbl_meta{$_} } split(/\s+/, $rest)) ];
+      $modes{$mode}->{cust}->{visible_tables} = 1;
    }
 }
 
 # save_config_visible_tables {{{3
 sub save_config_visible_tables {
    my $file = shift;
-   foreach my $mode ( keys %modes ) {
+   foreach my $mode ( sort keys %modes ) {
+      next unless $modes{$mode}->{cust}->{visible_tables};
       my $tables = $modes{$mode}->{visible_tables};
       print $file "$mode=", join(' ', @$tables), "\n";
    }
@@ -4826,8 +5269,9 @@
       last if $line =~ m/^\[/;
 
       my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
-      next unless $key;
+      next unless $key && exists $tbl_meta{$key};
       $tbl_meta{$key}->{sort_cols} = $rest;
+      $tbl_meta{$key}->{cust}->{sort_cols} = 1;
       $tbl_meta{$key}->{sort_func} = make_sort_func($tbl_meta{$key});
    }
 }
@@ -4835,7 +5279,8 @@
 # save_config_sort_cols {{{3
 sub save_config_sort_cols {
    my $file = shift;
-   foreach my $tbl ( keys %tbl_meta ) {
+   foreach my $tbl ( sort keys %tbl_meta ) {
+      next unless $tbl_meta{$tbl}->{cust}->{sort_cols};
       my $col = $tbl_meta{$tbl}->{sort_cols};
       print $file "$tbl=$col\n";
    }
@@ -4854,13 +5299,16 @@
       my @parts = unique(grep { exists($filters{$_}) } split(/\s+/, $rest));
       @parts = grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} } @parts;
       $tbl_meta{$tbl}->{filters} = [ @parts ];
+      $tbl_meta{$tbl}->{cust}->{filters} = 1;
    }
 }
 
 # save_config_active_filters {{{3
 sub save_config_active_filters {
    my $file = shift;
-   foreach my $tbl ( keys %tbl_meta ) {
+   foreach my $tbl ( sort keys %tbl_meta ) {
+      next if $tbl_meta{$tbl}->{temp};
+      next unless $tbl_meta{$tbl}->{cust}->{filters};
       my $aref = $tbl_meta{$tbl}->{filters};
       print $file "$tbl=", join(' ', @$aref), "\n";
    }
@@ -4878,52 +5326,24 @@
       next unless $key && exists $tbl_meta{$key};
       my @parts = grep { exists($tbl_meta{$key}->{cols}->{$_}) } unique split(/ /, $rest);
       $tbl_meta{$key}->{visible} = [ @parts ];
+      $tbl_meta{$key}->{cust}->{visible} = 1;
    }
 }
 
 # save_config_active_columns {{{3
 sub save_config_active_columns {
    my $file = shift;
-   foreach my $tbl ( keys %tbl_meta ) {
+   foreach my $tbl ( sort keys %tbl_meta ) {
+      next unless $tbl_meta{$tbl}->{cust}->{visible};
       my $aref = $tbl_meta{$tbl}->{visible};
       print $file "$tbl=", join(' ', @$aref), "\n";
    }
 }
 
-# load_config_expressions {{{3
-sub load_config_expressions {
-   my ( $file ) = @_;
-   while ( my $line = <$file> ) {
-      chomp $line;
-      next if $line =~ m/^#/;
-      last if $line =~ m/^\[/;
-
-      my ( $key, $expr ) = $line =~ m/^(.+?)=(.*)$/;
-      next unless $key && $expr;
-
-      my ( $sub, $err ) = compile_expr($expr);
-      $exprs{$key} = {
-         func => $sub,
-         text => $expr,
-         user => 1,
-         name => $key,
-      }
-   }
-}
-
-# save_config_expressions {{{3
-sub save_config_expressions {
-   my $file = shift;
-   foreach my $key ( keys %exprs ) {
-      next unless $exprs{$key}->{user};
-      print $file "$key=$exprs{$key}->{text}\n";
-   }
-}
-
 # save_config_tbl_meta {{{3
 sub save_config_tbl_meta {
    my $file = shift;
-   foreach my $tbl ( keys %tbl_meta ) {
+   foreach my $tbl ( sort keys %tbl_meta ) {
       foreach my $col ( keys %{$tbl_meta{$tbl}->{cols}} ) {
          my $meta = $tbl_meta{$tbl}->{cols}->{$col};
          next unless $meta->{user};
@@ -4979,9 +5399,9 @@
 
    # Look in the command-line parameters for things stored in the same slot.
    my %cmdline =
-      map  { $opt_spec{$_}->{config} => $opts{$_} }
-      grep { exists $opt_spec{$_}->{config} && exists $opts{$_} }
-      keys %opt_spec;
+      map  { $_->{c} => $opts{$_->{k}} }
+      grep { exists $_->{c} && exists $opts{$_->{k}} }
+      @opt_spec;
 
    while ( my $line = <$file> ) {
       chomp $line;
@@ -5013,9 +5433,7 @@
       next if $line =~ m/^#/;
       last if $line =~ m/^\[/;
 
-      # Each tbl_meta section has all the properties defined in %col_props.  If expr
-      # is set, it gets looked up by name.  That's why load_config_expressions() has
-      # to be called before this.
+      # Each tbl_meta section has all the properties defined in %col_props.
       my ( $col , $rest ) = $line =~ m/^(.*?)=(.*)$/;
       next unless $col;
       my %parts = $rest =~ m/(\w+)='((?:(?!(?<!\\)').)*)'/g; # Properties are single-quoted
@@ -5054,10 +5472,6 @@
             $meta->{cols}->{$col}->{$prop} = $parts{$prop};
          }
       }
-      if ( $meta->{cols}->{$col}->{expr} ) {
-         $meta->{cols}->{$col}->{src} = $exprs{$parts{expr}}
-            or die "There's no expression named $parts{expr} for column $col in table $tbl";
-      }
 
    }
 }
@@ -5103,10 +5517,10 @@
 # save_config_connections {{{3
 sub save_config_connections {
    my $file = shift;
-   foreach my $conn ( keys %connections ) {
+   foreach my $conn ( sort keys %connections ) {
       my $href = $connections{$conn};
       my @keys = $href->{savepass} ? @conn_parts : grep { $_ ne 'pass' } @conn_parts;
-      print $file "$conn=", join(' ', map { "$_=$href->{$_}" } @keys), "\n";
+      print $file "$conn=", join(' ', map { "$_=$href->{$_}" } grep { defined $href->{$_} } @keys), "\n";
    }
 }
 
@@ -5138,14 +5552,16 @@
    foreach my $tbl ( keys %rule_set_for ) {
       $tbl_meta{$tbl}->{colors} = $rule_set_for{$tbl};
       $tbl_meta{$tbl}->{color_func} = make_color_func($tbl_meta{$tbl});
+      $tbl_meta{$tbl}->{cust}->{colors} = 1;
    }
 }
 
 # save_config_colors {{{3
 sub save_config_colors {
    my $file = shift;
-   foreach my $tbl ( keys %tbl_meta ) {
+   foreach my $tbl ( sort keys %tbl_meta ) {
       my $meta = $tbl_meta{$tbl};
+      next unless $meta->{cust}->{colors};
       foreach my $rule ( @{$meta->{colors}} ) {
          print $file "$tbl=", join(
             ' ',
@@ -5169,8 +5585,8 @@
       last if $line =~ m/^\[/;
 
       my ( $key , $rest ) = $line =~ m/^(.*?)=(.*)$/;
-      next unless $key;
-      my @parts = split(/ /, $rest);
+      next unless $key && exists $modes{$key};
+      my @parts = grep { exists $connections{$_} } split(/ /, $rest);
       $modes{$key}->{connections} = [ @parts ] if exists $modes{$key};
    }
 }
@@ -5178,12 +5594,34 @@
 # save_config_active_connections {{{3
 sub save_config_active_connections {
    my $file = shift;
-   foreach my $mode ( keys %modes ) {
+   foreach my $mode ( sort keys %modes ) {
       my @connections = get_connections($mode);
       print $file "$mode=", join(' ', @connections), "\n";
    }
 }
 
+# load_config_stmt_sleep_times {{{3
+sub load_config_stmt_sleep_times {
+   my ( $file ) = @_;
+   while ( my $line = <$file> ) {
+      chomp $line;
+      next if $line =~ m/^#/;
+      last if $line =~ m/^\[/;
+
+      my ( $key , $val ) = split('=', $line);
+      next unless $key && defined $val && $val =~ m/$num_regex/;
+      $stmt_sleep_time_for{$key} = $val;
+   }
+}
+
+# save_config_stmt_sleep_times {{{3
+sub save_config_stmt_sleep_times {
+   my $file = shift;
+   foreach my $key ( sort keys %stmt_sleep_time_for ) {
+      print $file "$key=$stmt_sleep_time_for{$key}\n";
+   }
+}
+
 # load_config_mvs {{{3
 sub load_config_mvs {
    my ( $file ) = @_;
@@ -5192,8 +5630,8 @@
       next if $line =~ m/^#/;
       last if $line =~ m/^\[/;
 
-      my ( $key , $val ) = $line =~ m/^(.*?)=(.*)$/;
-      next unless $key;
+      my ( $key , $val ) = split('=', $line);
+      next unless $key && defined $val && $val =~ m/$num_regex/;
       $mvs{$key} = $val;
    }
 }
@@ -5201,7 +5639,7 @@
 # save_config_mvs {{{3
 sub save_config_mvs {
    my $file = shift;
-   foreach my $key ( keys %mvs ) {
+   foreach my $key ( sort keys %mvs ) {
       print $file "$key=$mvs{$key}\n";
    }
 }
@@ -5256,49 +5694,12 @@
    }
 }
 
-# get_expr {{{3
-sub get_expr {
-   my $exp;
-   $clear_screen_sub->();
-   print word_wrap("Choose the name of an expression to be used to calculate the column's contents.  "
-      . "You can choose an existing expression, or type a new name to create a new one.");
-   do {
-      $exp = prompt_list(
-         "Enter expression name",
-         '',
-         sub { return keys %exprs },
-         { map { $_ => trunc(collapse_ws($exprs{$_}->{text}), 30) } keys %exprs },
-         { sep => ' ' });
-   } while ( !$exp );
-   if ( !exists $exprs{$exp} ) {
-      my ( $err, $sub, $body );
-      do {
-         $clear_screen_sub->();
-         print word_wrap("The expression you named doesn't exist yet.  Specify a Perl expression for the body of "
-               . "a subroutine that accepts a hashref called \$set and returns your desired value.");
-         print "\n\n";
-         if ( $err ) {
-            print "There's an error in your expression: $err\n\n";
-         }
-         $body = prompt("Enter subroutine body");
-         ( $sub, $err )  = compile_expr($body);
-      } while ( $err );
-
-      $exprs{$exp} = {
-         func => $sub,
-         text => $body,
-         user => 1,
-         name => $exp,
-      };
-   }
-   return $exp;
-}
-
 # edit_color_rules {{{3
 sub edit_color_rules {
+   my ( $tbl ) = @_;
    $clear_screen_sub->();
-   my $tbl = choose_visible_table();
-   if ( exists($tbl_meta{$tbl}) ) {
+   $tbl ||= choose_visible_table();
+   if ( $tbl && exists($tbl_meta{$tbl}) ) {
       my $meta = $tbl_meta{$tbl};
       my @cols = ('', qw(col op arg color));
       my $info = { map { $_ => { hdr => $_, just => '-', } }  @cols };
@@ -5348,7 +5749,7 @@
 
             # Draw the screen and wait for a command.
             unshift @display_lines, '',
-               "Editing color rules for $meta->{hdr}.  Press ? for help, q to "
+               "Editing color rules for $meta->{capt}.  Press ? for help, q to "
                . "quit.", '';
             draw_screen(\@display_lines);
             print "\n\n", word_wrap('Rules are applied in order from top to '
@@ -5361,13 +5762,59 @@
    }
 }
 
+# add_quick_filter {{{3
+sub add_quick_filter {
+   my $tbl = choose_visible_table();
+   if ( $tbl && exists($tbl_meta{$tbl}) ) {
+      print "\n";
+      my $response = prompt_list(
+         "Enter column name and filter text",
+         '',
+         sub { return keys %{$tbl_meta{$tbl}->{cols}} },
+         ()
+      );
+      my ( $col, $text ) = split(/\s+/, $response, 2);
+      return unless $col && $text && exists($tbl_meta{$tbl}->{cols}->{$col});
+      my ( $sub, $err ) = compile_filter( "defined \$set->{$col} && \$set->{$col} =~ m/$text/" );
+      return if !$sub || $err;
+      my $name = "quick_$tbl.$col";
+      $filters{$name} = {
+         func  => $sub,
+         text  => $text,
+         user  => 1,
+         quick => 1,
+         name  => $name,
+         note  => 'Quick-filter',
+         tbls  => [$tbl],
+      };
+      push @{$tbl_meta{$tbl}->{filters}}, $name;
+   }
+}
+
+# clear_quick_filters {{{3
+sub clear_quick_filters {
+   my $tbl = choose_visible_table(
+      # Only tables that have quick-filters
+      sub {
+         my ( $tbl ) = @_;
+         return scalar grep { $filters{$_}->{quick} } @{ $tbl_meta{$tbl}->{filters} };
+      }
+   );
+   if ( $tbl && exists($tbl_meta{$tbl}) ) {
+      my @current = @{$tbl_meta{$tbl}->{filters}};
+      @current = grep { !$filters{$_}->{quick} } @current;
+      $tbl_meta{$tbl}->{filters} = \@current;
+   }
+}
+
 # edit_table {{{3
 sub edit_table {
    $clear_screen_sub->();
-   my $tbl = choose_visible_table();
-   if ( exists($tbl_meta{$tbl}) ) {
+   my ( $tbl ) = @_;
+   $tbl ||= choose_visible_table();
+   if ( $tbl && exists($tbl_meta{$tbl}) ) {
       my $meta = $tbl_meta{$tbl};
-      my @cols = ('', qw(name hdr label src expr));
+      my @cols = ('', qw(name hdr label src));
       my $info = { map { $_ => { hdr => $_, just => '-', } }  @cols };
       $info->{label}->{maxw} = 30;
       my $key;
@@ -5421,7 +5868,7 @@
 
             # Draw the screen and wait for a command.
             unshift @display_lines, '',
-               "Editing table definition for $meta->{hdr}.  Press ? for help, q to quit.", '';
+               "Editing table definition for $meta->{capt}.  Press ? for help, q to quit.", '';
             draw_screen(\@display_lines);
             $key = pause('');
          }
@@ -5438,40 +5885,82 @@
       "Choose tables to display",
       join(' ', @tbls),
       sub { return @{$modes{$mode}->{tables}} },
-      { map { $_ => $tbl_meta{$_}->{hdr} } @{$modes{$mode}->{tables}} }
+      { map { $_ => $tbl_meta{$_}->{capt} } @{$modes{$mode}->{tables}} }
    );
    $modes{$mode}->{visible_tables} =
       [ unique(grep { $_ && exists $tbl_meta{$_} } split(/\s+/, $new)) ];
+   $modes{$mode}->{cust}->{visible_tables} = 1;
 }
 
 # choose_visible_table {{{3
 sub choose_visible_table {
+   my ( $grep_cond ) = @_;
    my $mode = $config{mode}->{val};
-   my @tbls = @{$modes{$mode}->{visible_tables}};
+   my @tbls
+      = grep { $grep_cond ? $grep_cond->($_) : 1 }
+        @{$modes{$mode}->{visible_tables}};
    my $tbl = $tbls[0];
    if ( @tbls > 1 ) {
       $tbl = prompt_list(
          "Choose a table",
          '',
          sub { return @tbls },
-         { map { $_ => $tbl_meta{$_}->{hdr} } @tbls }
+         { map { $_ => $tbl_meta{$_}->{capt} } @tbls }
       );
    }
    return $tbl;
 }
 
+sub choose_filters {
+   my ( $tbl ) = @_;
+   $tbl ||= choose_visible_table();
+   return unless $tbl && exists $tbl_meta{$tbl};
+   my $meta = $tbl_meta{$tbl};
+   $clear_screen_sub->();
+
+   print "Choose filters for $meta->{capt}:\n";
+
+   my $ini = join(' ', @{$meta->{filters}});
+   my $val = prompt_list(
+      'Choose filters',
+      $ini,
+      sub { return keys %filters },
+      {
+         map  { $_ => $filters{$_}->{note} }
+         grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} }
+         keys %filters
+      }
+   );
+
+   my @choices = unique(split(/\s+/, $val));
+   foreach my $new ( grep { !exists($filters{$_}) } @choices ) {
+      my $answer = prompt("There is no filter called '$new'.  Create it?", undef, 'y');
+      if ( $answer eq 'y' ) {
+         create_new_filter($new, $tbl);
+      }
+   }
+   @choices = grep { exists $filters{$_} } @choices;
+   @choices = grep { grep { $tbl eq $_ } @{$filters{$_}->{tbls}} } @choices;
+   $meta->{filters} = [ @choices ];
+   $meta->{cust}->{filters} = 1;
+}
+
 sub choose_sort_cols {
+   my ( $tbl ) = @_;
+   $tbl ||= choose_visible_table();
+   return unless $tbl && exists $tbl_meta{$tbl};
    $clear_screen_sub->();
-   my $tbl = shift;
-   return unless $tbl && exists $tbl_meta{$tbl};
    my $meta = $tbl_meta{$tbl};
    my $val = prompt_list(
-      'Choose sort columns (prefix a column with - to reverse sort)',
+      'Sort columns (reverse sort with -col)',
       $meta->{sort_cols},
       sub { return keys %{$meta->{cols}} },
       { map { $_ => $meta->{cols}->{$_}->{label} } keys %{$meta->{cols}} });
-   $meta->{sort_cols} = $val;
-   $tbl_meta{$tbl}->{sort_func} = make_sort_func($tbl_meta{$tbl});
+   if ( $meta->{sort_cols} ne $val ) {
+      $meta->{sort_cols} = $val;
+      $meta->{cust}->{sort_cols} = 1;
+      $tbl_meta{$tbl}->{sort_func} = make_sort_func($tbl_meta{$tbl});
+   }
 }
 
 # create_new_filter {{{3
@@ -5488,6 +5977,7 @@
       } while ( !$filter || $filter =~ m/\W/ );
    }
 
+   my $completion = sub { keys %{$tbl_meta{$tbl}->{cols}} };
    my ( $err, $sub, $body );
    do {
       $clear_screen_sub->();
@@ -5499,7 +5989,7 @@
       if ( $err ) {
          print "There's an error in your filter expression: $err\n\n";
       }
-      $body = prompt("Enter subroutine body");
+      $body = prompt("Enter subroutine body", undef, undef, $completion);
       ( $sub, $err ) = compile_filter($body);
    } while ( $err );
 
@@ -5527,32 +6017,32 @@
    $config{$key}->{val} = $new_value;
 }
 
-# TODO: make a list of all variables and what they come from.  Use that for
-# auto-completion here and in add_new_var_set, and for figuring out what data is needed.
-# Cache the list of what data is needed.
 sub edit_current_var_set {
    my $mode = $config{mode}->{val};
    my $name = $config{"${mode}_set"}->{val};
-   my $variables = join(' ', @{$var_sets{$name}});
+   my $variables = $var_sets{$name}->{text};
 
+   my $new = $variables;
    do {
       $clear_screen_sub->();
-      $variables = prompt("Enter variables for $name", undef, $variables );
-   } until ( $variables );
+      $new = prompt("Enter variables for $name", undef, $variables);
+   } until ( $new );
 
-   $var_sets{$name} = [ unique(grep { $_ } split(/\s+/, $variables)) ];
+   if ( $new ne $variables ) {
+      @{$var_sets{$name}}{qw(text user)} = ( $new, 1);
+   }
 }
 
 
 sub choose_var_set {
-   my $key = shift;
+   my ( $key ) = @_;
    $clear_screen_sub->();
 
    my $new_value = prompt_list(
       'Choose a set of values to display, or enter the name of a new one',
       $config{$key}->{val},
       sub { return keys %var_sets },
-      { map { $_ => join(' ', @{$var_sets{$_}}) } sort keys %var_sets });
+      { map { $_ => $var_sets{$_}->{text} } keys %var_sets });
 
    if ( !exists $var_sets{$new_value} ) {
       add_new_var_set($new_value);
@@ -5561,6 +6051,15 @@
    $config{$key}->{val} = $new_value if exists $var_sets{$new_value};
 }
 
+sub switch_var_set {
+   my ( $cfg_var, $dir ) = @_;
+   my @var_sets = sort keys %var_sets;
+   my $cur      = $config{$cfg_var}->{val};
+   my $pos      = grep { $_ lt $cur } @var_sets;
+   my $newpos   = ($pos + $dir) % @var_sets;
+   $config{$cfg_var}->{val} = $var_sets[$newpos];
+   $clear_screen_sub->();
+}
 
 # get_file {{{3
 sub get_file {
@@ -5579,6 +6078,22 @@
 
 # Online configuration and prompting functions {{{2
 
+# edit_stmt_sleep_times {{{3
+sub edit_stmt_sleep_times {
+   $clear_screen_sub->();
+   my $stmt = prompt_list('Specify a statement', '', sub { return sort keys %stmt_maker_for });
+   return unless $stmt && exists $stmt_maker_for{$stmt};
+   $clear_screen_sub->();
+   my $curr_val = $stmt_sleep_time_for{$stmt} || 0;
+   my $new_val  = prompt('Specify a sleep delay after calling this SQL', $num_regex, $curr_val);
+   if ( $new_val ) {
+      $stmt_sleep_time_for{$stmt} = $new_val;
+   }
+   else {
+      delete $stmt_sleep_time_for{$stmt};
+   }
+}
+
 # edit_server_groups {{{3
 # Choose which server connections are in a server group.  First choose a group,
 # then choose which connections are in it.
@@ -5588,11 +6103,11 @@
    my $group = $modes{$mode}->{server_group};
    my %curr  = %server_groups;
    my $new   = choose_or_create_server_group($group, 'to edit');
-   my $cxns  = join(' ', @{$server_groups{$new}});
    $clear_screen_sub->();
    if ( exists $curr{$new} ) {
       # Don't do this step if the user just created a new server group,
       # because part of that process was to choose connections.
+      my $cxns  = join(' ', @{$server_groups{$new}});
       my @conns = choose_or_create_connection($cxns, 'for this group');
       $server_groups{$new} = \@conns;
    }
@@ -5681,7 +6196,6 @@
 # Data-retrieval functions {{{2
 # get_status_info {{{3
 # Get SHOW STATUS and SHOW VARIABLES together.
-# TODO: figure out how to only get the needed parts.  Maybe split status/vars into two subs.
 sub get_status_info {
    my @cxns = @_;
    if ( !$info_gotten{status}++ ) {
@@ -5693,8 +6207,9 @@
          my $res = $sth->fetchall_arrayref();
          map { $vars->{$_->[0]} = $_->[1] || 0 } @$res;
 
-         # Calculate hi-res uptime and add cxn to the hash
-         $vars->{Uptime_hires} ||= $hi_res ? time() - $dbhs{$cxn}->{start_time} : $vars->{Uptime};
+         # Calculate hi-res uptime and add cxn to the hash.  This duplicates get_driver_status,
+         # but it's most important to have consistency.
+         $vars->{Uptime_hires} ||= time() - $dbhs{$cxn}->{start_time};
          $vars->{cxn} = $cxn;
 
          # Add SHOW VARIABLES to the hash
@@ -5705,32 +6220,64 @@
    }
 }
 
+# Chooses a thread for explaining, killing, etc...
+# First arg is a func that can be called in grep.
+sub choose_thread {
+   my ( $grep_cond, $prompt ) = @_;
+
+   # Narrow the list to queries that can be explained.
+   my %thread_for = map {
+      # Eliminate innotop's own threads.
+      $_ => $dbhs{$_}->{dbh} ? $dbhs{$_}->{dbh}->{mysql_thread_id} : 0
+   } keys %connections;
+
+   my @candidates = grep {
+      $_->{id} != $thread_for{$_->{cxn}} && $grep_cond->($_)
+   } @current_queries;
+   return unless @candidates;
+
+   # Find out which server.
+   my @cxns = unique map { $_->{cxn} } @candidates;
+   my ( $cxn ) = select_cxn('On which server', @cxns);
+   return unless $cxn && exists($connections{$cxn});
+
+   # Re-filter the list of candidates to only those on this server
+   @candidates = grep { $_->{cxn} eq $cxn } @candidates;
+
+   # Find out which thread to do.
+   my $info;
+   if ( @candidates > 1 ) {
+      my @threads = sort map { $_->{id} } @candidates;
+      my $thread = prompt_list($prompt,
+         $threads[0],
+         sub { return @threads });
+      return unless $thread && $thread =~ m/$int_regex/;
+
+      # Find the info hash of that query on that server.
+      ( $info ) = grep { $thread == $_->{id} } @candidates;
+   }
+   else {
+      $info = $candidates[0];
+   }
+   return $info;
+}
+
 # analyze_query {{{3
 # Allows the user to show fulltext, explain, show optimized...
 sub analyze_query {
-   my $action = shift;
+   my ( $action ) = @_;
+
+   my $info = choose_thread(
+      sub { $_[0]->{query} },
+      'Select a thread to analyze',
+   );
+   return unless $info;
+
    my %actions = (
       e => \&display_explain,
       f => \&show_full_query,
       o => \&show_optimized_query,
    );
-
-   # Find out which server.
-   my @cxns = unique map { $_->{cxn} } @current_queries;
-   my ( $cxn ) = select_cxn('On which server', @cxns);
-   return unless $cxn && exists($connections{$cxn});
-
-   # Find out which connection.
-   my @ids = sort map { $_->{id} } grep { $_->{cxn} eq $cxn } @current_queries;
-   return unless @ids;
-   my $id = prompt_list('Specify a connection ID to analyze',
-      $ids[0],
-      sub { return @ids });
-
-   # Find the info hash of that query on that server.
-   my ( $info ) = grep { $cxn eq $_->{cxn} && $id == $_->{id} } @current_queries;
-   return unless $info;
-
    do {
       $actions{$action}->($info);
       print "\n";
@@ -5763,8 +6310,11 @@
 }
 
 # extract_values {{{3
+# Arguments are a set of values (which may be incremental, derived from
+# current and previous), current, and previous values.
+# TODO: there are a few places that don't remember prev set so can't pass it.
 sub extract_values {
-   my ( $set, $tbl ) = @_;
+   my ( $set, $cur, $pre, $tbl ) = @_;
    my $result = {};
    my $meta   = $tbl_meta{$tbl};
    my $cols   = $meta->{cols};
@@ -5774,9 +6324,12 @@
       die "No func defined for '$key' in $tbl"
          unless $info->{func};
       eval {
-         $result->{$key} = $info->{func}->($set)
+         $result->{$key} = $info->{func}->($set, $cur, $pre)
       };
       if ( $EVAL_ERROR ) {
+         if ( $config{debug}->{val} ) {
+            die $EVAL_ERROR;
+         }
          $result->{$key} = $info->{num} ? 0 : '';
       }
    }
@@ -5810,13 +6363,12 @@
 # get_innodb_status {{{3
 sub get_innodb_status {
    my ( $cxns, $addl_sections ) = @_;
-   if ( !$info_gotten{innodb_status}++ ) {
-      my $parser = InnoDBParser->new;
+   if ( !$config{skip_innodb}->{val} && !$info_gotten{innodb_status}++ ) {
 
       # Determine which sections need to be parsed
       my %sections_required =
          map  { $tbl_meta{$_}->{innodb} => 1 }
-         grep { $_ }
+         grep { $_ && $tbl_meta{$_}->{innodb} }
          get_visible_tables();
 
       # Add in any other sections the caller requested.
@@ -5826,11 +6378,13 @@
 
       foreach my $cxn ( @$cxns ) {
          my $stmt = do_stmt($cxn, 'INNODB_STATUS') or next;
-         my $innodb_status_text = $stmt->fetchrow_hashref()->{Status};
+         my $innodb_status_text = $stmt->fetchrow_hashref()->{status};
+         next unless $innodb_status_text
+            && substr($innodb_status_text, 0, 100) =~ m/INNODB MONITOR OUTPUT/;
 
          # Parse and merge into %vars storage
          my %innodb_status = (
-            $parser->get_status_hash(
+            $innodb_parser->get_status_hash(
                $innodb_status_text,
                $config{debug}->{val},
                \%sections_required,
@@ -5846,7 +6400,7 @@
          my $hash = $vars{$cxn}->{$clock};
          @{$hash}{ keys %innodb_status } = values %innodb_status;
          $hash->{cxn} = $cxn;
-         $hash->{Uptime_hires} ||= $hi_res ? time() - $dbhs{$cxn}->{start_time} : $hash->{Uptime};
+         $hash->{Uptime_hires} ||= time() - $dbhs{$cxn}->{start_time};
       }
    }
 }
@@ -5860,8 +6414,9 @@
 
    eval {
       # Set up the table for creating a deadlock.
+      my $engine = version_ge($dbhs{$cxn}->{dbh}, '4.1.2') ? 'engine' : 'type';
       return unless do_query($cxn, "drop table if exists $tbl");
-      return unless do_query($cxn, "create table $tbl(a int) engine=innodb");
+      return unless do_query($cxn, "create table $tbl(a int) $engine=innodb");
       return unless do_query($cxn, "delete from $tbl");
       return unless do_query($cxn, "insert into $tbl(a) values(0), (1)");
       return unless do_query($cxn, "commit"); # Or the children will block against the parent
@@ -5890,14 +6445,25 @@
    };
    if ( $EVAL_ERROR ) {
       print $EVAL_ERROR;
+      pause();
    }
 
    $clearing_deadlocks = 0;
 }
 
+sub get_master_logs {
+   my @cxns = @_;
+   my @result;
+   if ( !$info_gotten{master_logs}++ ) {
+      foreach my $cxn ( @cxns ) {
+         my $stmt = do_stmt($cxn, 'SHOW_MASTER_LOGS') or next;
+         push @result, @{$stmt->fetchall_arrayref({})};
+      }
+   }
+   return @result;
+}
+
 # get_master_slave_status {{{3
-# TODO: apparently in version 4 the column names are different?
-# TODO: split into master/slave status...
 sub get_master_slave_status {
    my @cxns = @_;
    if ( !$info_gotten{replication_status}++ ) {
@@ -5912,11 +6478,18 @@
          $stmt = do_stmt($cxn, 'SHOW_SLAVE_STATUS') or next;
          $res = $stmt->fetchall_arrayref({})->[0];
          @{$vars}{ keys %$res } = values %$res;
-         $vars->{Uptime_hires} ||= $hi_res ? time() - $dbhs{$cxn}->{start_time} : $vars->{Uptime};
+         $vars->{Uptime_hires} ||= time() - $dbhs{$cxn}->{start_time};
       }
    }
 }
 
+sub is_func {
+   my ( $word ) = @_;
+   return defined(&$word)
+      || eval "my \$x= sub { $word  }; 1"
+      || $EVAL_ERROR !~ m/^Bareword/;
+}
+
 # Documentation {{{1
 # ############################################################################
 # I put this last as per the Dog book.
@@ -5925,97 +6498,183 @@
 
 =head1 NAME
 
-innotop - A MySQL and InnoDB monitor program.
+innotop - MySQL and InnoDB transaction/status monitor.
 
 =head1 DESCRIPTION
 
-innotop connects to one or many MySQL database servers and retrieves data, then
-displays it.  It can run interactively as a monitor, or serve as a source for
-UNIX pipe-and-filter style programming.  innotop uses the data from SHOW
-VARIABLES, SHOW GLOBAL STATUS, SHOW FULL PROCESSLIST, and SHOW ENGINE INNODB
-STATUS, among other things.  It refreshes the data at regular intervals, so you
-get a sense of what's happening inside your MySQL servers.  You can control how
-fast it refreshes.
+innotop monitors MySQL servers.  Each of its modes shows you a different aspect
+of what's happening in the server.  For example, there's a mode for monitoring
+replication, one for queries, and one for transactions.  innotop refreshes its
+data periodically, so you see an updating view.
 
-I originally wrote innotop to parse SHOW INNODB STATUS and show a list of
-current transactions in `top' style, hence the name.  It now has much more
-functionality.
+innotop has many features for power users, but you can start and run it with
+virtually no configuration.  If you're just getting started, see
+L<"QUICK-START">.  Press '?' at any time while running innotop for
+context-sensitive help.
 
-When innotop is running interactively, you control it with key presses.  You can
-find a complete list of all available keys at any time by pressing '?' for help.
-Keys change innotop from one mode to another, let you change configuration, and
-send commands to MySQL servers.
+=head1 QUICK-START
 
-=head1 OVERVIEW
+To start innotop, open a terminal or command prompt.  If you have installed
+innotop on your system, you should be able to just type "innotop" and press
+Enter; otherwise, you will need to change to innotop's directory and type "perl
+innotop".
 
-Within each of innotop's modes, innotop displays 'tables' of the current data.
-For example, in T (InnoDB Transactions) mode, it shows the transactions in a
-table.  In some modes there are many tables on screen at once.
+The first thing innotop needs to know is how to connect to a MySQL server.  You
+can just enter the hostname of the server, for example "localhost" or
+"127.0.0.1" if the server is on the same machine as innotop.  After this innotop
+will prompt you for a DSN (data source name).  You should be able to just accept
+the defaults by pressing Enter.
 
-You can choose which tables to display, in what order, which columns and in what
-order, how to sort the rows, colorize and filter the rows, and more.  Think of
-the tables as spreadsheets; you have quite a bit of control over what goes into
-the cells.  You can even define your own formulas and apply formatting.  For
-example, you can choose whether a cell should be right or left justified,
-specify minimum and maximum widths, shorten large numbers to familiar units like
-MB and GB, and turn an integer number of seconds into hours:minutes:seconds
-display.
+If you have a .my.cnf file with your MySQL connection defaults, innotop can read
+it, and you won't need to specify a username and password if it's in that file.
+Otherwise, you should answer 'y' to the next couple of prompts.
 
-Some modes allow you to see the incremental changes since last refresh.  This
-can be useful to see how many new queries have been issued during that time, for
-example.  You can toggle this on and off.
+After this, you should be connected, and innotop should show you something like
+the following:
 
-You can define many connections to servers, group the servers together, and
-switch between them easily to manage many MySQL instances conveniently.  See
-SERVER GROUPS for more.
+ InnoDB Txns (? for help) localhost, 01:11:19, InnoDB 10s :-), 50 QPS,
+ 
+ CXN        History  Versions  Undo  Dirty Buf  Used Bufs  Txns  MaxTxn
+ localhost        7      2035  0 0       0.00%     92.19%     1   07:34
+ 
+ CXN        ID     User   Host       Txn Status  Time   Undo  Query Tex
+ localhost  98379  user1  webserver  ACTIVE      07:34     0  SELECT `c
+ localhost  98450  user1  webserver  ACTIVE      01:06     0  INSERT IN
+ localhost  97750  user1  webserver  not starte  00:00     0      
+ localhost  98375  user1  appserver  not starte  00:00     0      
 
-Remember, press '?' to see what commands are available to you at any time.
+(This sample is truncated at the right so it will fit on a terminal when running
+'man innotop')
 
-=head1 CONFIGURATION
+This sample comes from a quiet server with few transactions active.  If your
+server is busy, you'll see more output.  Notice the first line on the screen,
+which tells you what mode you're in and what server you're connected to.  You
+can change to other modes with keystrokes; press 'Q' to switch to a list of
+currently running queries.
 
-innotop is completely configurable.  The default configuration is built into the
-program, but everything is written out to a configuration file when you exit
-innotop.  You can edit this file by hand as you wish, or just use the built-in
-configuration commands while innotop is running.
+Press the '?' key to see what keys are active in the current mode.  You can
+press any of these keys and innotop will either take the requested action or
+prompt you for more input.  If your system has Term::ReadLine support, you can
+use TAB and other keys to auto-complete and edit input.
 
-You can specify certain options on the command-line.  Run `innotop --help' for
-details.
+To quit innotop, press the 'q' key.
 
+=head1 OPTIONS
+
+You can negate some options by prefixing the option name with --no.  For
+example, --noinc negates L<"--inc">.
+
+=over
+
+=item --help
+
+Print a summary of command-line usage and exit.
+
+=item --config
+
+Specifies a configuration file to read.  This option is non-sticky, that is to
+say it does not persist to the configuration file itself.
+
+=item --nonint
+
+Enable non-interactive operation.  See L<"NON-INTERACTIVE OPERATION"> for more.
+
+=item --count
+
+Refresh only the specified number of times (ticks) before exiting.  Each refresh
+is a pause for L<"interval"> seconds, followed by requesting data from MySQL
+connections and printing it to the terminal.
+
+=item --delay
+
+Specifies the amount of time to pause between ticks (refreshes).  Corresponds to
+the configuration option L<"interval">.
+
+=item --mode
+
+Specifies the mode in which innotop should start.  Corresponds to the
+configuration option L<"mode">.
+
+=item --inc
+
+Specifies whether innotop should display absolute numbers or relative numbers
+(offsets from their previous values).  Corresponds to the configuration option
+L<"status_inc">.
+
+=item --version
+
+Output version information and exit.
+
+=back
+
+=head1 HOTKEYS
+
+innotop is interactive, and you control it with key-presses.
+
+=over
+
+=item *
+
+Uppercase keys switch between modes.
+
+=item *
+
+Lowercase keys initiate some action within the current mode.
+
+=item *
+
+Other keys do something special like change configuration or show the
+innotop license.
+
+=back
+
+Press '?' at any time to see the currently active keys and what they do.
+
 =head1 MODES
 
-innotop has many modes.  The following is a brief description of each in
-alphabetical order.  Remember, you can always get the authoritative help by
-pressing '?'.
+Each of innotop's modes retrieves and displays a particular type of data from
+the servers you're monitoring.  You switch between modes with uppercase keys.
+The following is a brief description of each mode, in alphabetical order.  To
+switch to the mode, press the key listed in front of its heading in the
+following list:
 
-=over 8
+=over
 
 =item B: InnoDB Buffers
 
-This mode displays the InnoDB buffer pool, page statistics, insert buffer, and
-adaptive hash index.
+This mode displays information about the InnoDB buffer pool, page statistics,
+insert buffer, and adaptive hash index.  The data comes from SHOW INNODB STATUS.
 
+This mode contains the L<"buffer_pool">, L<"page_statistics">,
+L<"insert_buffers">, and L<"adaptive_hash_index"> tables by default.
+
 =item D: InnoDB Deadlocks
 
 This mode shows the transactions involved in the last InnoDB deadlock.  A second
-table shows the locks each transaction held and waited for (recall that a
-deadlock is caused by a cycle in the waits-for graph).
+table shows the locks each transaction held and waited for.  A deadlock is
+caused by a cycle in the waits-for graph, so there should be two locks held and
+one waited for unless the deadlock information is truncated.
 
 InnoDB puts deadlock information before some other information in the SHOW
 INNODB STATUS output.  If there are a lot of locks, the deadlock information can
-grow very large indeed, and there is a limit on the size of the SHOW INNODB
+grow very large, and there is a limit on the size of the SHOW INNODB
 STATUS output.  A large deadlock can fill the entire output, or even be
 truncated, and prevent you from seeing other information at all.  If you are
 running innotop in another mode, for example T mode, and suddenly you don't see
 anything, you might want to check and see if a deadlock has wiped out the data
 you need.
 
-If it has, you can create a small deadlock to replace it.  Use the 'w' key to
-'wipe' the large deadlock with a small one.  This will not work unless you have
-defined a deadlock table for the connection -- look in your configuration file.
+If it has, you can create a small deadlock to replace the large one.  Use the
+'w' key to 'wipe' the large deadlock with a small one.  This will not work
+unless you have defined a deadlock table for the connection (see L<"SERVER
+CONNECTIONS">).
 
-You can also set innotop to automatically detect when a large deadlock needs to
-be replaced with a small one.  This feature is turned off by default.
+You can also configure innotop to automatically detect when a large deadlock
+needs to be replaced with a small one (see L<"auto_wipe_dl">).
 
+This mode displays the L<"deadlock_transactions"> and L<"deadlock_locks"> tables
+by default.
+
 =item F: InnoDB Foreign Key Errors
 
 This mode shows the last InnoDB foreign key error information, such as the
@@ -6027,27 +6686,24 @@
 likely never to be perfect in this regard.  If innotop doesn't show you what
 you need to see, just look at the status text directly.
 
-=item G: Load Graph
+This mode displays the L<"fk_error"> table by default.
 
-This mode calculates per-second statistics, such as queries per second, scales
-them against a maximum, and prints them out as a "bar graph."  It's similar to
-the Load Statistics mode, except it's a graph instead of numbers.
-
-Headers are abbreviated to fit on the screen if necessary.  This only happens in
-interactive operation, not while running unattended.
-
 =item I: InnoDB I/O Info
 
-This mode shows InnoDB's I/O statistics, including the I/O threads, pending
-I/O, file I/O miscellaneous, and log statistics.
+This mode shows InnoDB's I/O statistics, including the I/O threads, pending I/O,
+file I/O miscellaneous, and log statistics.  It displays the L<"io_threads">,
+L<"pending_io">, L<"file_io_misc">, and L<"log_statistics"> tables by default.
 
 =item M: Master/Slave Replication Status
 
 This mode shows the output of SHOW SLAVE STATUS and SHOW MASTER STATUS in three
 tables.  The first two divide the slave's status into SQL and I/O thread status,
 and the last shows master status.  Filters are applied to eliminate non-slave
-servers from the slave tables and vice versa.
+servers from the slave tables, and non-master servers from the master table.
 
+This mode displays the L<"slave_sql_status">, L<"slave_io_status">, and
+L<"master_status"> tables by default.
+
 =item O: Open Tables
 
 This section comes from MySQL's SHOW OPEN TABLES command.  By default it is
@@ -6055,53 +6711,72 @@
 get a quick look at which tables are 'hot'.  You can use this to guess which
 tables might be locked implicitly.
 
+This mode displays the L<"open_tables"> mode by default.
+
 =item Q: Query List
 
 This mode displays the output from SHOW FULL PROCESSLIST, much like B<mytop>'s
 query list mode.  This mode does B<not> show InnoDB-related information.  This
 is probably one of the most useful modes for general usage.
 
-You can toggle an informative header that shows general status information about
-your server.  There are default sorting, filtering, and colorization rules.
+There is an informative header that shows general status information about
+your server.  You can toggle it on and off with the 'h' key.  By default,
+innotop hides inactive processes and its own process.  You can toggle these on
+and off with the 'i' and 'a' keys.
 
-You can EXPLAIN a query from this mode.  This will allow you to see the query's
-full text, the results of EXPLAIN, and in newer MySQL versions, even see the
-optimized query resulting from EXPLAIN EXTENDED.
+You can EXPLAIN a query from this mode with the 'e' key.  This displays the
+query's full text, the results of EXPLAIN, and in newer MySQL versions, even
+the optimized query resulting from EXPLAIN EXTENDED.  innotop also tries to
+rewrite certain queries to make them EXPLAIN-able.  For example, INSERT/SELECT
+statements are rewritable.
 
+This mode displays the L<"q_header"> and L<"processlist"> tables by default.
+
 =item R: InnoDB Row Operations and Semaphores
 
 This mode shows InnoDB row operations, row operation miscellaneous, semaphores,
-and information from the wait array.
+and information from the wait array.  It displays the L<"row_operations">,
+L<"row_operation_misc">, L<"semaphores">, and L<"wait_array"> tables by default.
 
-=item S: Load Statistics
+=item S: Variables & Status
 
 This mode calculates statistics, such as queries per second, and prints them out
-in the style of <vmstat>.  It's similar to the Load Graph mode, except it's a
-numbers instead of a graph.  You can show absolute values or incremental values
-since the last refresh.  Like G mode, headers may be abbreviated to fit on the
-screen in interactive operation.  You choose which variables to display with the
-'c' key, which selects from predefined sets.  You can choose your own sets.
+in several different styles.  You can show absolute values, or incremental values
+between ticks.
 
+You can switch between the views by pressing a key.  The 's' key prints a
+single line each time the screen updates, in the style of B<vmstat>.  The 'g'
+key changes the view to a graph of the same numbers, sort of like B<tload>.
+The 'v' key changes the view to a pivoted table of variable names on the left,
+with successive updates scrolling across the screen from left to right.  You can
+choose how many updates to put on the screen with the L<"num_status_sets">
+configuration variable.
+
+Headers may be abbreviated to fit on the screen in interactive operation.  You
+choose which variables to display with the 'c' key, which selects from
+predefined sets, or lets you create your own sets.  You can edit the current set
+with the 'e' key.
+
+This mode doesn't really display any tables like other modes.  Instead, it uses
+a table definition to extract and format the data, but it then transforms the
+result in special ways before outputting it.  It uses the L<"var_status"> table
+definition for this.
+
 =item T: InnoDB Transactions
 
-This mode shows every transaction in the InnoDB monitor's output, in `top'
+This mode shows transactions from the InnoDB monitor's output, in B<top>-like
 format.  This mode is the reason I wrote innotop.
 
-By default, two filters are applied to the table to hide inactive transactions
-and hide innotop's own transaction.  You can toggle this on and off.  There are
-also default sort and colorization rules in this view.  You can customize these.
+You can kill queries or processes with the 'k' and 'x' keys, and EXPLAIN a query
+with the 'e' or 'f' keys.
 
-If you are only viewing one server's transactions, innotop can display an
-informational header.  This will show you things like how many entries there are
-in the InnoDB history list, how much of the buffer pool is used, and so forth.
+The informational header can be toggled on and off with the 'h' key.  By
+default, innotop hides inactive transactions and its own transaction.  You can
+toggle this on and off with the 'i' and 'a' keys.
 
-=item V: Variables & Status
+This mode displays the L<"t_header"> and L<"innodb_transactions"> tables by
+default.
 
-This mode displays any variables you please from SHOW GLOBAL STATUS and SHOW
-VARIABLES, as well as the values parsed from SHOW INNODB STATUS.  It displays
-not only the current values, but previous values too; you choose how many sets
-to keep on screen.
-
 =item W: InnoDB Lock Waits
 
 This mode shows information about current InnoDB lock waits.  This information
@@ -6110,58 +6785,1391 @@
 which tables and indexes are the "hot spot" for locks.  If your server is
 running pretty well, this mode should show nothing.
 
-A second table shows any waits in the OS wait array.  This comes from a separate
-section of the status text.  If you see frequent waits, your server is probably
-running under a high concurrency workload.  This is the same table displayed in
-R mode.
+A second table shows any waits in the OS wait array.  This comes from a
+different section of the status text.  If you see frequent waits, your server is
+probably running under a high concurrency workload.  This is the same table
+displayed in R mode.
 
+This mode displays the L<"lock_waits"> and L<"wait_array"> tables by default.
+
 =back
 
+=head1 INNOTOP STATUS
+
+The first line innotop displays is a "status bar" of sorts.  What it contains
+depends on the mode you're in, and what servers you're monitoring.  The first
+few words are always the innotop mode, such as "InnoDB Txns" for T mode,
+followed by a reminder to press '?' for help at any time.
+
+=head2 ONE SERVER
+
+The simplest case is when you're monitoring a single server.  In this case, the
+name of the connection is next on the status line.  This is the name you gave
+when you created the connection -- most likely the MySQL server's hostname.
+This is followed by the server's uptime.
+
+If you're in an InnoDB mode, such as T or B, the next word is "InnoDB" followed
+by some information about the SHOW INNODB STATUS output used to render the
+screen.  The first word is the number of seconds since the last SHOW INNODB
+STATUS, which InnoDB uses to calculate some per-second statistics.  The next is
+a smiley face indicating whether the InnoDB output is truncated.  If the smiley
+face is a :-), all is well; there is no truncation.  A :^| means the transaction
+list is so long, InnoDB has only printed out some of the transactions.  Finally,
+a frown :-( means the output is incomplete, which is probably due to a deadlock
+printing too much lock information (see L<"D: InnoDB Deadlocks">).
+
+The next two words indicate the server's queries per second (QPS) and how many
+threads (connections) exist.  Finally, the server's version number is the last
+thing on the line.
+
+=head2 MULTIPLE SERVERS
+
+If you are monitoring multiple servers (see L<"SERVER CONNECTIONS">), the status
+line does not show any details about individual servers.  Instead, it shows the
+names of the connections that are active.  Again, these are connection names you
+specified, which are likely to be the server's hostname.  A connection that has
+an error is prefixed with an exclamation point.
+
+If you are monitoring a group of servers (see L<"SERVER GROUPS">), the status
+line shows the name of the group.  If any connection in the group has an
+error, the group's name is followed by the fraction of the connections that
+don't have errors.
+
+See L<"ERROR HANDLING"> for more details about innotop's error handling.
+
+=head1 SERVER ADMINISTRATION
+
+While innotop is primarily a monitor that lets you watch and analyze your
+servers, it can also send commands to servers.  The most frequently useful
+commands are killing queries and stopping or starting slaves.
+
+You can kill a connection, or in newer versions of MySQL kill a query but not a
+connection, from L<"Q: Query List"> and L<"T: InnoDB Transactions"> modes.
+Press 'k' to issue a KILL command, or 'x' to issue a KILL QUERY command.
+innotop will prompt you for the server and/or connection ID to kill (innotop
+does not prompt you if there is only one possible choice for any input).
+Confirm the command with 'y'.
+
+In L<"M: Master/Slave Replication Status"> mode, you can start and stop slaves
+with the 'a' and 'o' keys, respectively.  You can send these commands to many
+slaves at once.  innotop fills in a default command of START SLAVE or STOP SLAVE
+for you, but you can actually edit the command and send anything you wish, such
+as SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1 to make the slave skip one binlog event
+when it starts.
+
+You can also ask innotop to calculate the earliest binlog in use by any slave
+and issue a PURGE MASTER LOGS on the master.  Use the 'b' key for this.  innotop
+will prompt you for a master to run the command on, then prompt you for the
+connection names of that master's slaves (there is no way for innotop to
+determine this reliably itself).  innotop will find the minimum binlog in use by
+these slave connections and suggest it as the argument to PURGE MASTER LOGS.
+
+=head1 SERVER CONNECTIONS
+
+When you create a server connection, innotop asks you for a series of inputs, as
+follows:
+
+=over
+
+=item DSN
+
+A DSN is a Data Source Name, which is the initial argument passed to the DBI
+module for connecting to a server.  It is usually of the form
+
+ DBI:mysql:;mysql_read_default_group=mysql;host=HOSTNAME
+
+Since this DSN is passed to the DBD::mysql driver, you should read the driver's
+documentation at L<"http://search.cpan.org/dist/DBD-mysql/lib/DBD/mysql.pm"> for
+the exact details on all the options you can pass the driver in the DSN.  You
+can read more about DBI at L<http://dbi.perl.org/docs/>, and especially at
+L<http://search.cpan.org/~timb/DBI/DBI.pm>.
+
+The mysql_read_default_group=mysql option lets the DBD driver read your MySQL
+options files, such as ~/.my.cnf on UNIX-ish systems.  You can use this to avoid
+specifying a username or password for the connection.
+
+=item InnoDB Deadlock Table
+
+This optional item tells innotop a table name it can use to deliberately create
+a small deadlock (see L<"D: InnoDB Deadlocks">).  If you specify this option,
+you just need to be sure the table doesn't exist, and that innotop can create
+and drop the table with the InnoDB storage engine.  You can safely omit or just
+accept the default if you don't intend to use this.
+
+=item Username
+
+innotop will ask you if you want to specify a username.  If you say 'y', it will
+then prompt you for a user name.  If you have a MySQL option file that specifies
+your username, you don't have to specify a username.
+
+The username defaults to your login name on the system you're running innotop on.
+
+=item Password
+
+innotop will ask you if you want to specify a password.  Like the username, the
+password is optional, but there's an additional prompt that asks if you want to
+save the password in the innotop configuration file.  If you don't save it in
+the configuration file, innotop will prompt you for a password each time it
+starts.  Passwords in the innotop configuration file are saved in plain text,
+not encrypted in any way.
+
+=back
+
+Once you finish answering these questions, you should be connected to a server.
+But innotop isn't limited to monitoring a single server; you can define many
+server connections and switch between them by pressing the '@' key.  See
+L<"SWITCHING BETWEEN CONNECTIONS">.
+
+To create a new connection, press the '@' key and type the name of the new
+connection, then follow the steps given above.
+
 =head1 SERVER GROUPS
 
-If you have a lot of MySQL instances, or even if you only have a few, you will
-probably find this functionality helpful.
+If you have multiple MySQL instances, you can put them into named groups, such
+as 'all', 'masters', and 'slaves', which innotop can monitor all together.
 
-To begin with, when you start innotop it will prompt you to define a connection
-to a server.  After that is done, you can tell it to monitor another server with
-the @ key.  This key actually brings up a list of connections you've defined.
-If you name one that doesn't exist, innotop will guide you through the process
-of defining it as a new connection, and it will be available from then on.
+You can choose which group to monitor with the '#' key, and you can press the
+TAB key to switch to the next group.  If you're not currently monitoring a
+group, pressing TAB selects the first group.
 
-You can name multiple connections in any mode.  For example, suppose you are in
-T mode, monitoring transactions on server1; if you press @, you can type
-'server1 server2' and see data from both.
+To create a group, press the '#' key and type the name of your new group, then
+type the names of the connections you want the group to contain.
 
-This becomes unwieldy after a bit though.  To address this, you can press the
-'#' key to create and select server groups.  Groups work just the same as
-connections: if you name one that doesn't exist, you can create it.
+=head1 SWITCHING BETWEEN CONNECTIONS
 
-As an example, you might have groups named 'all', 'masters', 'slaves', 'oltp'
-and 'olap'.  Many of the servers could belong to several of these groups.  It's
-just a quick way to toggle between various servers.
+innotop lets you quickly switch which servers you're monitoring.  The most basic
+way is by pressing the '@' key and typing the name(s) of the connection(s) you
+want to use.  This setting is per-mode, so you can monitor different connections
+in each mode, and innotop remembers which connections you choose.
 
-Once you have defined groups, you can press the TAB key to cycle between them.
+You can quickly switch to the 'next' connection in alphabetical order with the
+'n' key.  If you're monitoring a server group (see L<"SERVER GROUPS">) this will
+switch to the first connection.
 
-As of this writing innotop does NOT fetch data in parallel from different
-servers, so if your groups get large you may notice increased delay time when
-innotop refreshes.  You can address this by creating more small groups.  At some
-point I plan to make the data-fetching multi-threaded and this problem will not
-be so severe.
+You can also type many connection names, and innotop will fetch and display data
+from them all.  Just separate the connection names with spaces, for example
+"server1 server2."  Again, if you type the name of a connection that doesn't
+exist, innotop will prompt you for connection information and create the
+connection.
 
+Another way to monitor multiple connections at once is with server groups.  You
+can use the TAB key to switch to the 'next' group in alphabetical order, or if
+you're not monitoring any groups, TAB will switch to the first group.
+
+innotop does not fetch data in parallel from connections, so if you are
+monitoring a large group or many connections, you may notice increased delay
+between ticks.
+
+When you monitor more than one connection, innotop's status bar changes.  See
+L<"INNOTOP STATUS">.
+
+=head1 ERROR HANDLING
+
+Error handling is not that important when monitoring a single connection, but is
+crucial when you have many active connections.  A crashed server or lost
+connection should not crash innotop.  As a result, innotop will continue to run
+even when there is an error; it just won't display any information from the
+connection that had an error.  Because of this, innotop's behavior might confuse
+you.  It's a feature, not a bug!
+
+innotop does not continue to query connections that have errors, because they
+may slow innotop and make it hard to use, especially if the error is a problem
+connecting and causes a long time-out.  Instead, innotop retries the connection
+occasionally to see if the error still exists.  If so, it will wait until some
+point in the future.  The wait time increases in ticks as the Fibonacci series,
+so it tries less frequently as time passes.
+
+Since errors might only happen in certain modes because of the SQL commands
+issued in those modes, innotop keeps track of which mode caused the error.  If
+you switch to a different mode, innotop will retry the connection instead of
+waiting.
+
+By default innotop will display the problem in red text at the bottom of the
+first table on the screen.  You can disable this behavior with the
+L<"show_cxn_errors_in_tbl"> configuration option, which is enabled by default.
+If the L<"debug"> option is enabled, innotop will display the error at the
+bottom of every table, not just the first.  And if L<"show_cxn_errors"> is
+enabled, innotop will print the error text to STDOUT as well.  Error messages
+might only display in the mode that caused the error, depending on the mode and
+whether innotop is avoiding querying that connection.
+
+=head1 NON-INTERACTIVE OPERATION
+
+You can run innotop in non-interactive mode, in which case it is entirely
+controlled from the configuration file and command-line options.  To start
+innotop in non-interactive mode, give the L"<--nonint"> command-line option.
+This changes innotop's behavior in the following ways:
+
+=over
+
+=item *
+
+Certain Perl modules are not loaded.  Term::Readline is not loaded, since
+innotop doesn't prompt interactively.  Term::ANSIColor and Win32::Console::ANSI
+modules are not loaded.  Term::ReadKey is still used, since innotop may have to
+prompt for connection passwords when starting up.
+
+=item *
+
+innotop does not clear the screen after each tick.
+
+=item *
+
+innotop does not persist any changes to the configuration file.
+
+=item *
+
+If L<"--count"> is given and innotop is in incremental mode (see L<"status_inc">
+and L<"--inc">), innotop actually refreshes one more time than specified so it
+can print incremental statistics.  This suppresses output during the first
+tick, so innotop may appear to hang.
+
+=item *
+
+innotop only displays the first table in each mode.  This is so the output can
+be easily processed with other command-line utilities such as awk and sed.  To
+change which tables display in each mode, see L<"TABLES">.  Since L<"Q: Query
+List"> mode is so important, innotop automatically disables the L<"q_header">
+table.  This ensures you'll see the L<"processlist"> table, even if you have
+innotop configured to show the q_header table during interactive operation.
+Similarly, in L<"T: InnoDB Transactions"> mode, the L<"t_header"> table is
+suppressed so you see only the L<"innodb_transactions"> table.
+
+=item *
+
+All output is tab-separated instead of being column-aligned with whitespace, and
+innotop prints the full contents of each table instead of only printing one
+screenful at a time.
+
+=item *
+
+innotop only prints column headers once instead of every tick (see
+L<"hide_hdr">).  innotop does not print table captions (see
+L<"display_table_captions">).  innotop ensures there are no empty lines in the
+output.
+
+=item *
+
+innotop does not honor the L<"shorten"> transformation, which normally shortens
+some numbers to human-readable formats.
+
+=item *
+
+innotop does not print a status line (see L<"INNOTOP STATUS">).
+
+=back
+
+=head1 CONFIGURING
+
+Nearly everything about innotop is configurable.  Most things are possible to
+change with built-in commands, but you can also edit the configuration file.
+
+While running innotop, press the '$' key to bring up the configuration editing
+dialog.  Press another key to select the type of data you want to edit:
+
+=over
+
+=item S: Statement Sleep Times
+
+Edits SQL statement sleep delays, which make innotop pause for the specified
+amount of time after executing a statement.  See L<"SQL STATEMENTS"> for a
+definition of each statement and what it does.  By default innotop does not
+delay after any statements.
+
+This feature is included so you can customize the side-effects caused by
+monitoring your server.  You may not see any effects, but some innotop users
+have noticed that certain MySQL versions under very high load with InnoDB
+enabled take longer than usual to execute SHOW GLOBAL STATUS.  If innotop calls
+SHOW FULL PROCESSLIST immediately afterward, the processlist contains more
+queries than the machine actually averages at any given moment.  Configuring
+innotop to pause briefly after calling SHOW GLOBAL STATUS alleviates this
+effect.
+
+Sleep times are stored in the L<"stmt_sleep_times"> section of the configuration
+file.  Fractional-second sleeps are supported, subject to your hardware's
+limitations.
+
+=item c: Edit Columns
+
+Starts the table editor on one of the displayed tables.  See L<"TABLE EDITOR">.
+An alternative way to start the table editor without entering the configuration
+dialog is with the '^' key.
+
+=item g: General Configuration
+
+Starts the configuration editor to edit global and mode-specific configuration
+variables (see L<"MODES">).  innotop prompts you to choose a variable from among
+the global and mode-specific ones depending on the current mode.
+
+=item k: Row-Coloring Rules
+
+Starts the row-coloring rules editor on one of the displayed table(s).  See
+L<"COLORS"> for details.
+
+=item s: Server Groups
+
+Lets you create and edit server groups.  See L<"SERVER GROUPS">.
+
+=item t: Choose Displayed Tables
+
+Lets you choose which tables to display in this mode.  See L<"MODES"> and
+L<"TABLES">.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+innotop's default configuration file location is in $HOME/.innotop, but can be
+overridden with the L<"--config"> command-line option.  You can edit it by hand
+safely.  innotop reads the configuration file when it starts, and writes it out
+again when it exits, so any changes you make while innotop is running will be
+lost.
+
+innotop doesn't store its entire configuration in the configuration file.  It
+has a huge set of default configuration that it holds only in memory, and the
+configuration file only overrides these defaults.  When you customize a default
+setting, innotop notices, and then stores the customizations into the file.
+This keeps the file size down, makes it easier to edit, and makes upgrades
+easier.
+
+A configuration file can be made read-only.  See L<"readonly">.
+
+The configuration file is arranged into sections like an INI file.  Each
+section begins with [section-name] and ends with [/section-name].  Each
+section's entries have a different syntax depending on the data they need to
+store.  You can put comments in the file; any line that begins with a #
+character is a comment.  innotop will not read the comments, so it won't write
+them back out to the file when it exits.  Comments in read-only configuration
+files are still useful, though.
+
+The first line in the file is innotop's version number.  This lets innotop
+notice when the file format is not backwards-compatible, and upgrade smoothly
+without destroying your customized configuration.
+
+The following list describes each section of the configuration file and the data
+it contains:
+
+=over
+
+=item general
+
+The 'general' section contains global configuration variables and variables that
+may be mode-specific, but don't belong in any other section.  The syntax is a
+simple key=value list.  innotop writes a comment above each value to help you
+edit the file by hand.
+
+=over
+
+=item S_func
+
+Controls S mode presentation (see L<"S: Variables & Status">).  If g, values are
+graphed; if s, values are like vmstat; if p, values are in a pivoted table.
+
+=item S_set
+
+Specifies which set of variables to display in L<"S: Variables & Status"> mode.
+See L<"VARIABLE SETS">.
+
+=item auto_wipe_dl
+
+Instructs innotop to automatically wipe large deadlocks when it notices them.
+When this happens you may notice a slight delay.  At the next tick, you will
+usually see the information that was being truncated by the large deadlock.
+
+=item charset
+
+Specifies what kind of characters to allow through the L<"no_ctrl_char">
+transformation.  This keeps non-printable characters from confusing a
+terminal when you monitor queries that contain binary data, such as images.
+
+The default is 'ascii', which considers anything outside normal ASCII to be a
+control character.  The other allowable values are 'unicode' and 'none'.  'none'
+considers every character a control character, which can be useful for
+collapsing ALL text fields in queries.
+
+=item cxn_timeout
+
+On MySQL versions 4.0.3 and newer, this variable is used to set the connection's
+timeout, so MySQL doesn't close the connection if it is not used for a while.
+This might happen because a connection isn't monitored in a particular mode, for
+example.
+
+=item debug
+
+This option enables more verbose errors and makes innotop more strict in some
+places.  It can help in debugging filters and other user-defined code.  It also
+makes innotop write a lot of information to L<"debugfile"> when there is a
+crash.
+
+=item debugfile
+
+A file to which innotop will write information when there is a crash.  See
+L<"FILES">.
+
+=item display_table_captions
+
+innotop displays a table caption above most tables.  This variable suppresses or
+shows captions on all tables globally.  Some tables are configured with the
+hide_caption property, which overrides this.
+
+=item global
+
+Whether to show GLOBAL variables and status.  innotop only tries to do this on
+servers which support the GLOBAL option to SHOW VARIABLES and SHOW STATUS.  In
+some MySQL versions, you need certain privileges to do this; if you don't have
+them, innotop will not be able to fetch any variable and status data.  This
+configuration variable lets you run innotop and fetch what data you can even
+without the elevated privileges.
+
+I can no longer find or reproduce the situation where GLOBAL wasn't allowed, but
+I know there was one.
+
+=item graph_char
+
+Defines the character to use when drawing graphs in L<"S: Variables & Status">
+mode.
+
+=item header_highlight
+
+Defines how to highlight column headers.  This only works if Term::ANSIColor is
+available.  Valid values are 'bold' and 'underline'.
+
+=item hide_hdr
+
+Hides column headers globally.
+
+=item interval
+
+The interval at which innotop will refresh its data (ticks).  The interval is
+implemented as a sleep time between ticks, so the true interval will vary
+depending on how long it takes innotop to fetch and render data.
+
+This variable accepts fractions of a second.
+
+=item mode
+
+The mode in which innotop should start.  Allowable arguments are the same as the
+key presses that select a mode interactively.  See L<"MODES">.
+
+=item num_digits
+
+How many digits to show in fractional numbers and percents.  This variable's
+range is between 0 and 9 and can be set directly from L<"S: Variables & Status">
+mode with the '+' and '-' keys.  It is used in the L<"set_precision">,
+L<"shorten">, and L<"percent"> transformations.
+
+=item num_status_sets
+
+Controls how many sets of status variables to display in pivoted L<"S: Variables
+& Status"> mode.  It also controls the number of old sets of variables innotop
+keeps in its memory, so the larger this variable is, the more memory innotop
+uses.
+
+=item readonly
+
+Whether the configuration file is readonly.  This cannot be set interactively,
+because it would prevent itself from being written to the configuration file.
+
+=item show_cxn_errors
+
+Makes innotop print connection errors to STDOUT.  See L<"ERROR HANDLING">.
+
+=item show_cxn_errors_in_tbl
+
+Makes innotop display connection errors as rows in the first table on screen.
+See L<"ERROR HANDLING">.
+
+=item show_percent
+
+Adds a '%' character after the value returned by the L<"percent">
+transformation.
+
+=item show_statusbar
+
+Controls whether to show the status bar in the display.  See L<"INNOTOP
+STATUS">.
+
+=item skip_innodb
+
+Disables fetching SHOW INNODB STATUS, in case your server(s) do not have InnoDB
+enabled and you don't want innotop to try to fetch it.  This can also be useful
+when you don't have the SUPER privilege, required to run SHOW INNODB STATUS.
+
+=item status_inc
+
+Whether to show absolute or incremental values for status variables.
+Incremental values are calculated as an offset from the last value innotop saw
+for that variable.  This is a global setting, but will probably become
+mode-specific at some point.  Right now it is honored a bit inconsistently; some
+modes don't pay attention to it.
+
+=back
+
+=item filters
+
+This section holds user-defined filters (see L<"FILTERS">).  Each line is in the
+format filter_name=text='filter text' tbls='table list'.
+
+The filter text is the text of the subroutine's code.  The table list is a list
+of tables to which the filter can apply.  By default, user-defined filters apply
+to the table for which they were created, but you can manually override that by
+editing the definition in the configuration file.
+
+=item active_filters
+
+This section stores which filters are active on each table.  Each line is in the
+format table_name=filter_list.
+
+=item tbl_meta
+
+This section stores user-defined or user-customized columns (see L<"COLUMNS">).
+Each line is in the format col_name=properties, where the properties are a
+name=quoted-value list.
+
+=item connections
+
+This section holds the server connections you have defined.  Each line is in the
+format name=properties, where the properties are a name=value list.  The
+properties are self-explanatory, and the only one that is treated specially is
+'pass' which is only present if 'savepass' is set.  See L<"SERVER CONNECTIONS">.
+
+=item active_connections
+
+This section holds a list of which connections are active in each mode.  Each
+line is in the format mode_name=connection_list.
+
+=item server_groups
+
+This section holds server groups.  Each line is in the format
+name=connection_list.  See L<"SERVER GROUPS">.
+
+=item active_server_groups
+
+This section holds a list of which server group is active in each mode.  Each
+line is in the format mode_name=server_group.
+
+=item max_values_seen
+
+This section holds the maximum values seen for variables.  This is used to scale
+the graphs in L<"S: Variables & Status"> mode.  Each line is in the format
+name=value.
+
+=item active_columns
+
+This section holds table column lists.  Each line is in the format
+tbl_name=column_list.  See L<"COLUMNS">.
+
+=item sort_cols
+
+This section holds the sort definition.  Each line is in the format
+tbl_name=column_list.  If a column is prefixed with '-', that column sorts
+descending.  See L<"SORTING">.
+
+=item visible_tables
+
+This section defines which tables are visible in each mode.  Each line is in the
+format mode_name=table_list.  See L<"TABLES">.
+
+=item varsets
+
+This section defines variable sets for use in L<"S: Status & Variables"> mode.
+Each line is in the format name=variable_list.  See L<"VARIABLE SETS">.
+
+=item colors
+
+This section defines colorization rules.  Each line is in the format
+tbl_name=property_list.  See L<"COLORS">.
+
+=item stmt_sleep_times
+
+This section contains statement sleep times.  Each line is in the format
+statement_name=sleep_time.  See L<"S: Statement Sleep Times">.
+
+=back
+
+=head1 CUSTOMIZING
+
+You can customize innotop a great deal.  For example, you can:
+
+=over
+
+=item *
+
+Choose which tables to display, and in what order.
+
+=item *
+
+Choose which columns are in those tables, and create new columns.
+
+=item *
+
+Filter which rows display with built-in filters, user-defined filters, and
+quick-filters.
+
+=item *
+
+Sort the rows to put important data first or group together related rows.
+
+=item *
+
+Highlight rows with color.
+
+=item *
+
+Customize the alignment, width, and formatting of columns, and apply
+transformations to columns to extract parts of their values or format the values
+as you wish (for example, shortening large numbers to familiar units).
+
+=item *
+
+Design your own expressions to extract and combine data as you need.  This gives
+you unlimited flexibility.
+
+=back
+
+All these and more are explained in the following sections.
+
+=head2 TABLES
+
+A table is what you'd expect: a collection of columns.  It also has some other
+properties, such as a caption.  Filters, sorting rules, and colorization rules
+belong to tables and are covered in later sections.
+
+Internally, table meta-data is defined in a data structure called %tbl_meta.
+This hash holds all built-in table definitions, which contain a lot of default
+instructions to innotop.  The meta-data includes the caption, a list of columns
+the user has customized, a list of columns, a list of visible columns, a list of
+filters, color rules, a sort-column list, sort direction, and some information
+about the table's data sources.  Most of this is customizable via the table
+editor (see L<"TABLE EDITOR">).
+
+You can choose which tables to show by pressing the '$' key.  See L<"MODES"> and
+L<"TABLES">.
+
+The table life-cycle is as follows:
+
+=over
+
+=item *
+
+Each table begins with a data source, which is an array of hashes.  See below
+for details on data sources.
+
+=item *
+
+Each element of the data source becomes a row in the final table.
+
+=item *
+
+For each element in the data source, innotop extracts values from the source and
+creates a row.  This row is another hash, which later steps will refer to as
+$set.  The values innotop extracts are determined by the table's columns.  Each
+column has an extraction subroutine, compiled from an expression (see
+L<"EXPRESSIONS">).  The resulting row is a hash whose keys are named the same as
+the column name.
+
+=item *
+
+innotop filters the rows, removing those that don't need to be displayed.  See
+L<"FILTERS">.
+
+=item *
+
+innotop sorts the rows, unless they are to be pivoted.  See L<"SORTING">.
+
+=item *
+
+innotop colorizes the rows, again skipping this step if they are to be
+pivoted.  See L<"COLORS">.
+
+=item *
+
+innotop transforms the column values in each row.  See L<"TRANSFORMATIONS">.
+
+=item *
+
+innotop optionally pivots the rows (see L<"PIVOTING">).
+
+=item *
+
+innotop formats and justifies the rows as a table.  During this step, innotop
+applies further formatting to the column values, including alignment, maximum
+and minimum widths.  innotop also does final error checking to ensure there are
+no crashes due to undefined values.  innotop then adds a caption if specified,
+and the table is ready to print.
+
+=back
+
+Each built-in table is described below:
+
+=over
+
+=item adaptive_hash_index
+
+Displays data about InnoDB's adaptive hash index.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item buffer_pool
+
+Displays data about InnoDB's buffer pool.  Data source: L<"STATUS_VARIABLES">.
+
+=item deadlock_locks
+
+Shows which locks were held and waited for by the last detected deadlock.  Data
+source: L<"DEADLOCK_LOCKS">.
+
+=item deadlock_transactions
+
+Shows transactions involved in the last detected deadlock.  Data source:
+L<"DEADLOCK_TRANSACTIONS">.
+
+=item explain
+
+Shows the output of EXPLAIN.  Data source: L<"EXPLAIN">.
+
+=item file_io_misc
+
+Displays data about InnoDB's file and I/O operations.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item fk_error
+
+Displays various data about InnoDB's last foreign key error.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item innodb_transactions
+
+Displays data about InnoDB's current transactions.  Data source:
+L<"INNODB_TRANSACTIONS">.
+
+=item insert_buffers
+
+Displays data about InnoDB's insert buffer.  Data source: L<"STATUS_VARIABLES">.
+
+=item io_threads
+
+Displays data about InnoDB's I/O threads.  Data source: L<"IO_THREADS">.
+
+=item lock_waits
+
+Displays InnoDB lock waits, i.e. transactions which are waiting for a lock to be
+granted.  Data source: L<"LOCK_WAITS">.
+
+=item log_statistics
+
+Displays data about InnoDB's logging system.  Data source: L<"STATUS_VARIABLES">.
+
+=item master_status
+
+Displays replication master status.  Data source: L<"STATUS_VARIABLES">.
+
+=item open_tables
+
+Displays open tables.  Data source: L<"OPEN_TABLES">.
+
+=item page_statistics
+
+Displays InnoDB page statistics.  Data source: L<"STATUS_VARIABLES">.
+
+=item pending_io
+
+Displays InnoDB pending I/O operations.  Data source: L<"STATUS_VARIABLES">.
+
+=item processlist
+
+Displays current MySQL processes (threads/connections).  Data source:
+L<"PROCESSLIST">.
+
+=item q_header
+
+Displays various status values.  Data source: L<"STATUS_VARIABLES">.
+
+=item row_operation_misc
+
+Displays data about InnoDB's row operations.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item row_operations
+
+Displays data about InnoDB's row operations.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item semaphores
+
+Displays data about InnoDB's semaphores and mutexes.  Data source:
+L<"STATUS_VARIABLES">.
+
+=item slave_io_status
+
+Displays data about the slave I/O thread.  Data source: 
+L<"STATUS_VARIABLES">.
+
+=item slave_sql_status
+
+Displays data about the slave SQL thread.  Data source: L<"STATUS_VARIABLES">.
+
+=item t_header
+
+Displays various InnoDB status values.  Data source: L<"STATUS_VARIABLES">.
+
+=item var_status
+
+Displays user-configurable data.  Data source: L<"STATUS_VARIABLES">.
+
+=item wait_array
+
+Displays data about InnoDB's OS wait array.  Data source: L<"OS_WAIT_ARRAY">.
+
+=back
+
+=head2 COLUMNS
+
+Columns belong to tables.  You can choose a table's columns by pressing the '^'
+key, which starts the L<"TABLE EDITOR"> and lets you choose and edit columns.
+Pressing 'e' from within the table editor lets you edit the column's properties:
+
+=over
+
+=item *
+
+hdr: a column header.  This appears in the first row of the table.
+
+=item *
+
+just: justification.  '-' means left-justified and '' means right-justified,
+just as with printf formatting codes (not a coincidence).
+
+=item *
+
+dec: whether to further align the column on the decimal point.
+
+=item *
+
+num: whether the column is numeric.  This affects how values are sorted
+(lexically or numerically).
+
+=item *
+
+label: a small note about the column, which appears in dialogs that help the
+user choose columns.
+
+=item *
+
+src: an expression that innotop uses to extract the column's data from its
+source (see L<"DATA SOURCES">).  See L<"EXPRESSIONS"> for more on expressions.
+
+=item *
+
+minw: specifies a minimum display width.  This helps stabilize the display,
+which makes it easier to read if the data is changing frequently.
+
+=item *
+
+maxw: similar to minw.
+
+=item *
+
+trans: a list of column transformations.  See L<"TRANSFORMATIONS">.
+
+=back
+
+=head2 FILTERS
+
+Filters remove rows from the display.  They behave much like a WHERE clause in
+SQL.  innotop has several built-in filters, which remove irrelevant information
+like inactive queries, but you can define your own as well.  innotop also lets
+you create quick-filters, which do not get saved to the configuration file, and
+are just an easy way to quickly view only some rows.
+
+You can enable or disable a filter on any table.  Press the '%' key (mnemonic: %
+looks kind of like a line being filtered between two circles) and choose which
+table you want to filter, if asked.  You'll then see a list of possible filters
+and a list of filters currently enabled for that table.  Type the names of
+filters you want to apply and press Enter.
+
+=head3 USER-DEFINED FILTERS
+
+If you type a name that doesn't exist, innotop will prompt you to create the
+filter.  Filters are easy to create if you know Perl, and not hard if you don't.
+What you're doing is creating a subroutine that returns true if the row should
+be displayed.  The row is a hash reference passed to your subroutine as $set.
+
+For example, imagine you want to filter the processlist table so you only see
+queries that have been running more than five minutes.  Type a new name for your
+filter, and when prompted for the subroutine body, press TAB to initiate your
+terminal's auto-completion.  You'll see the names of the columns in the
+L<"processlist"> table (innotop generally tries to help you with auto-completion
+lists).  You want to filter on the 'time' column.  Type the text "$set->{time} >
+300" to return true when the query is more than five minutes old.  That's all
+you need to do.
+
+In other words, the code you're typing is surrounded by an implicit context,
+which looks like this:
+
+ sub filter {
+    my ( $set ) = @_;
+    # YOUR CODE HERE
+ }
+
+If your filter doesn't work, or if something else suddenly behaves differently,
+you might have made an error in your filter, and innotop is silently catching
+the error.  Try enabling L<"debug"> to make innotop throw an error instead.
+
+=head3 QUICK-FILTERS
+
+innotop's quick-filters are a shortcut to create a temporary filter that doesn't
+persist when you restart innotop.  To create a quick-filter, press the '/' key.
+innotop will prompt you for the column name and filter text.  Again, you can use
+auto-completion on column names.  The filter text can be just the text you want
+to "search for."  For example, to filter the L<"processlist"> table on queries
+that refer to the products table, type '/' and then 'info product'.
+
+The filter text can actually be any Perl regular expression, but of course a
+literal string like 'product' works fine as a regular expression.
+
+Behind the scenes innotop compiles the quick-filter into a specially tagged
+filter that is otherwise like any other filter.  It just isn't saved to the
+configuration file.
+
+To clear quick-filters, press the '\' key and innotop will clear them all at
+once.
+
+=head2 SORTING
+
+innotop has sensible built-in defaults to sort the most important rows to the
+top of the table.  Like anything else in innotop, you can customize how any
+table is sorted.
+
+To start the sort dialog, start the L<"TABLE EDITOR"> with the '^' key, choose a
+table if necessary, and press the 's' key.  You'll see a list of columns you can
+use in the sort expression and the current sort expression, if any.  Enter a
+list of columns by which you want to sort and press Enter.  If you want to
+reverse sort, prefix the column name with a minus sign.  For example, if you
+want to sort by column a ascending, then column b descending, type 'a -b'.  You
+can also explicitly add a + in front of columns you want to sort ascending, but
+it's not required.
+
+Some modes have keys mapped to open this dialog directly, and to quickly reverse
+sort direction.  Press '?' as usual to see which keys are mapped in any mode.
+
+=head2 PIVOTING
+
+innotop can pivot a table for more compact display, just like a Pivot Table in a
+spreadsheet (also known as a crosstab).  This is used in L<"S: Variables &
+Status"> mode, for example.
+
+Pivoting a table makes columns into rows.  Assume you start with this table:
+
+ foo bar
+ === ===
+ 1   3
+ 2   4
+
+After pivoting, the table will look like this:
+
+ name set0 set1
+ ==== ==== ====
+ foo  1    2
+ bar  3    4
+
+This functionality is currently limited because it becomes tricky to pivot when
+innotop is displaying data from multiple connections, each with a 'foo' and
+'bar' value.  In the future innotop will support some kind of aggregation,
+similar to SQL GROUP BY, to help solve this problem.
+
+=head2 COLORS
+
+By default, innotop highlights rows with color so you can see at a glance which
+rows are more important.  You can customize the colorization rules and add your
+own to any table.  Open the table editor with the '^' key, choose a table if
+needed, and press 'o' to open the color editor dialog.
+
+The color editor dialog displays the rules applied to the table, in the order
+they are evaluated.  Each row is evaluated against each rule to see if the rule
+matches the row; if it does, the row gets the specified color, and no further
+rules are evaluated.  The rules look like the following:
+
+ state  eq  Locked       black on_red
+ cmd    eq  Sleep        white       
+ user   eq  system user  white       
+ cmd    eq  Connect      white       
+ cmd    eq  Binlog Dump  white       
+ time   >   600          red         
+ time   >   120          yellow      
+ time   >   60           green       
+ time   >   30           cyan        
+
+This is the default rule set for the L<"processlist"> table.  In order of
+priority, these rules make locked queries black on a red background, "gray out"
+connections from replication and sleeping queries, and make queries turn from
+cyan to red as they run longer.
+
+(For some reason, the ANSI color code "white" is actually a light gray.  Your
+terminal's display may vary; experiment to find colors you like).
+
+You can use keystrokes to move the rules up and down, which re-orders their
+priority.  You can also delete rules and add new ones.  If you add a new rule,
+innotop prompts you for the column, an operator for the comparison, a value
+against which to compare the column, and a color to assign if the rule matches.
+There is auto-completion and prompting at each step.
+
+The value in the third step needs to be correctly quoted.  innotop does not try
+to quote the value because it doesn't know whether it should treat the value as
+a string or a number.  If you want to compare the column against a string, as
+for example in the first rule above, you should enter 'Locked' surrounded by
+quotes.  If you get an error message about a bareword, you probably should have
+quoted something.
+
+=head2 EXPRESSIONS
+
+Expressions are at the core of how innotop works, and are what enables you to
+extend innotop as you wish.  Recall the table lifecycle explained in
+L<"TABLES">.  Expressions are used in the earliest step, where it extracts
+values from a data source to form rows.
+
+It does this by calling a subroutine for each column, passing it the source data
+set, a set of current values, and a set of previous values.  These are all
+needed so the subroutine can calculate things like the difference between this
+tick and the previous tick.
+
+The subroutines that extract the data from the set are compiled from
+expressions.  This gives significantly more power than just naming the values to
+fill the columns, because it allows the column's value to be calculated from
+whatever data is necessary, but avoids the need to write complicated and lengthy
+Perl code.
+
+innotop begins with a string of text that can look as simple as a value's name
+or as complicated as a full-fledged Perl expression.  It looks at each
+'bareword' token in the string and decides whether it's supposed to be a key
+into the $set hash.  A bareword is an unquoted value that isn't already
+surrounded by code-ish things like dollar signs or curly brackets.  If innotop
+decides that the bareword isn't a function or other valid Perl code, it converts
+it into a hash access.  After the whole string is processed, innotop compiles a
+subroutine, like this:
+
+ sub compute_column_value {
+    my ( $set, $cur, $pre ) = @_;
+    my $val = # EXPANDED STRING GOES HERE
+    return $val;
+ }
+
+Here's a concrete example, taken from the header table L<"q_header"> in L<"Q:
+Query List"> mode.  This expression calculates the qps, or Queries Per Second,
+column's values, from the values returned by SHOW STATUS:
+
+ Questions/Uptime_hires
+
+innotop decides both words are barewords, and transforms this expression into
+the following Perl code:
+
+ $set->{Questions}/$set->{Uptime_hires}
+
+When surrounded by the rest of the subroutine's code, this is executable Perl
+that calculates a high-resolution queries-per-second value.
+
+The arguments to the subroutine are named $set, $cur, and $pre.  In most cases,
+$set and $cur will be the same values.  However, if L<"status_inc"> is set, $cur
+will not be the same as $set, because $set will already contain values that are
+the incremental difference between $cur and $pre.
+
+Every column in innotop is computed by subroutines compiled in the same fashion.
+There is no difference between innotop's built-in columns and user-defined
+columns.  This keeps things consistent and predictable.
+
+=head2 TRANSFORMATIONS
+
+Transformations change how a value is rendered.  For example, they can take a
+number of seconds and display it in H:M:S format.  The following transformations
+are defined:
+
+=over
+
+=item commify
+
+Adds commas to large numbers every three decimal places.
+
+=item dulint_to_int
+
+Accepts two unsigned integers and converts them into a single longlong.  This is
+useful for certain operations with InnoDB, which uses two integers as
+transaction identifiers, for example.
+
+=item no_ctrl_char
+
+Removes quoted control characters from the value.  This is affected by the
+L<"charset"> configuration variable.
+
+This transformation only operates within quoted strings, for example, values to
+a SET clause in an UPDATE statement.  It will not alter the UPDATE statement,
+but will collapse the quoted string to [BINARY] or [TEXT], depending on the
+charset.
+
+=item percent
+
+Converts a number to a percentage by multiplying it by two, formatting it with
+L<"num_digits"> digits after the decimal point, and optionally adding a percent
+sign (see L<"show_percent">).
+
+=item secs_to_time
+
+Formats a number of seconds as time in days+hours:minutes:seconds format.
+
+=item set_precision
+
+Formats numbers with L<"num_digits"> number of digits after the decimal point.
+
+=item shorten
+
+Formats a number as a unit of 1024 (k/M/G/T) and with L<"num_digits"> number of
+digits after the decimal point.
+
+=back
+
+=head2 TABLE EDITOR
+
+The innotop table editor lets you customize tables with keystrokes.  You start
+the table editor with the '^' key.  If there's more than one table on the
+screen, it will prompt you to choose one of them.  Once you do, innotop will
+show you something like this:
+
+ Editing table definition for Buffer Pool.  Press ? for help, q to quit.
+ 
+ name               hdr          label                  src          
+ cxn                CXN          Connection from which  cxn          
+ buf_pool_size      Size         Buffer pool size       IB_bp_buf_poo
+ buf_free           Free Bufs    Buffers free in the b  IB_bp_buf_fre
+ pages_total        Pages        Pages total            IB_bp_pages_t
+ pages_modified     Dirty Pages  Pages modified (dirty  IB_bp_pages_m
+ buf_pool_hit_rate  Hit Rate     Buffer pool hit rate   IB_bp_buf_poo
+ total_mem_alloc    Memory       Total memory allocate  IB_bp_total_m
+ add_pool_alloc     Add'l Pool   Additonal pool alloca  IB_bp_add_poo
+
+The first line shows which table you're editing, and reminds you again to press
+'?' for a list of key mappings.  The rest is a tabular representation of the
+table's columns, because that's likely what you're trying to edit.  However, you
+can edit more than just the table's columns; this screen can start the filter
+editor, color rule editor, and more.
+
+Each row in the display shows a single column in the table you're editing, along
+with a couple of its properties such as its header and source expression (see
+L<"EXPRESSIONS">).
+
+The key mappings are Vim-style, as in many other places.  Pressing 'j' and 'k'
+moves the highlight up or down.  You can then (d)elete or (e)dit the highlighted
+column.  You can also (a)dd a column to the table.  This actually just activates
+one of the columns already defined for the table; it prompts you to choose from
+among the columns available but not currently displayed.  Finally, you can
+re-order the columns with the '+' and '-' keys.
+
+If you want to really customize and create your own column, as opposed to just
+activating a built-in one that's not currently displayed, press the (n)ew key,
+and innotop will prompt you for the information it needs:
+
+=over
+
+=item *
+
+The column name: this needs to be a word without any funny characters, e.g. just
+letters, numbers and underscores.
+
+=item *
+
+The column header: this is the label that appears at the top of the column, in
+the table header.  This can have spaces and funny characters, but be careful not
+to make it too wide and waste space on-screen.
+
+=item *
+
+The column's data source: this is an expression that determines what data from
+the source (see L<"TABLES">) innotop will put into the column.  This can just be
+the name of an item in the source, or it can be a more complex expression, as
+described in L<"EXPRESSIONS">.
+
+=back
+
+Once you've entered the required data, your table has a new column.  There is no
+difference between this column and the built-in ones; it can have all the same
+properties and behaviors.  innotop will write the column's definition to the
+configuration file, so it will persist across sessions.
+
+=head1 VARIABLE SETS
+
+Variable sets are used in L<"S: Variables & Status"> mode to define more easily
+what variables you want to monitor.  Behind the scenes they are compiled to a
+list of expressions, and then into a column list so they can be treated just
+like columns in any other table, in terms of data extraction and
+transformations.  However, you're protected from the tedious details by a syntax
+that ought to feel very natural to you: a SQL SELECT list.
+
+The data source for variable sets, and indeed the entire S mode, is the
+combination of SHOW STATUS, SHOW VARIABLES, and SHOW INNODB STATUS.  Imagine
+that you had a huge table with one column per variable returned from those
+statements.  That's the data source for variable sets.  You can now query this
+data source just like you'd expect.  For example:
+
+ Questions, Uptime, Questions/Uptime as QPS
+
+Behind the scenes innotop will split that variable set into three expressions,
+compile them and turn them into a table definition, then extract as usual.  This
+becomes a "variable set," or a "list of variables you want to monitor."
+
+innotop lets you name and save your variable sets, and writes them to the
+configuration file.  You can choose which variable set you want to see with the
+'c' key, or activate the next and previous sets with the '>' and '<' keys.
+There are many built-in variable sets as well, which should give you a good
+start for creating your own.  Press 'e' to edit the current variable set, or
+just to see how it's defined.  To create a new one, just press 'c' and type its
+name.
+
+You may want to use some of the functions listed in L<"TRANSFORMATIONS"> to help
+format the results.  In particular, L<"set_precision"> is often useful to limit
+the number of digits you see.  Extending the above example, here's how:
+
+ Questions, Uptime, set_precision(Questions/Uptime) as QPS
+
+Actually, this still needs a little more work.  If your L<"interval"> is less
+than one second, you might be dividing by zero because Uptime is incremental in
+this mode by default.  Instead, use Uptime_hires:
+
+ Questions, Uptime, set_precision(Questions/Uptime_hires) as QPS
+
+This example is simple, but it shows how easy it is to choose which variables
+you want to monitor.
+
+=head1 SQL STATEMENTS
+
+innotop uses a limited set of SQL statements to retrieve data from MySQL for
+display.  The statements are customized depending on the server version against
+which they are executed; for example, on MySQL 5 and newer, INNODB_STATUS
+executes "SHOW ENGINE INNODB STATUS", while on earlier versions it executes
+"SHOW INNODB STATUS".  The statements are as follows:
+
+ Statement           SQL executed
+ =================== ===============================
+ INNODB_STATUS       SHOW [ENGINE] INNODB STATUS
+ KILL_CONNECTION     KILL
+ KILL_QUERY          KILL QUERY
+ OPEN_TABLES         SHOW OPEN TABLES
+ PROCESSLIST         SHOW FULL PROCESSLIST
+ SHOW_MASTER_LOGS    SHOW MASTER LOGS
+ SHOW_MASTER_STATUS  SHOW MASTER STATUS
+ SHOW_SLAVE_STATUS   SHOW SLAVE STATUS
+ SHOW_STATUS         SHOW [GLOBAL] STATUS
+ SHOW_VARIABLES      SHOW [GLOBAL] VARIABLES
+
+=head1 DATA SOURCES
+
+Each time innotop extracts values to create a table (see L<"EXPRESSIONS"> and
+L<"TABLES">), it does so from a particular data source.  Largely because of the
+complex data extracted from SHOW INNODB STATUS, this is slightly messy.  SHOW
+INNODB STATUS contains a mixture of single values and repeated values that form
+nested data sets.
+
+Whenever innotop fetches data from MySQL, it adds two extra bits to each set:
+cxn and Uptime_hires.  cxn is the name of the connection from which the data
+came.  Uptime_hires is a high-resolution version of the server's Uptime status
+variable, which is important if your L<"interval"> setting is sub-second.
+
+Here are the kinds of data sources from which data is extracted:
+
+=over
+
+=item STATUS_VARIABLES
+
+This is the broadest category, into which the most kinds of data fall.  It
+begins with the combination of SHOW STATUS and SHOW VARIABLES, but other sources
+may be included as needed, for example, SHOW MASTER STATUS and SHOW SLAVE
+STATUS, as well as many of the non-repeated values from SHOW INNODB STATUS.
+
+=item DEADLOCK_LOCKS
+
+This data is extracted from the transaction list in the LATEST DETECTED DEADLOCK
+section of SHOW INNODB STATUS.  It is nested two levels deep: transactions, then
+locks.
+
+=item DEADLOCK_TRANSACTIONS
+
+This data is from the transaction list in the LATEST DETECTED DEADLOCK
+section of SHOW INNODB STATUS.  It is nested one level deep.
+
+=item EXPLAIN
+
+This data is from the result set returned by EXPLAIN.
+
+=item INNODB_TRANSACTIONS
+
+This data is from the TRANSACTIONS section of SHOW INNODB STATUS.
+
+=item IO_THREADS
+
+This data is from the list of threads in the the FILE I/O section of SHOW INNODB
+STATUS.
+
+=item LOCK_WAITS
+
+This data is from the TRANSACTIONS section of SHOW INNODB STATUS and is nested
+two levels deep.
+
+=item OPEN_TABLES
+
+This data is from SHOW OPEN TABLES.
+
+=item PROCESSLIST
+
+This data is from SHOW FULL PROCESSLIST.
+
+=item OS_WAIT_ARRAY
+
+This data is from the SEMAPHORES section of SHOW INNODB STATUS and is nested one
+level deep.  It comes from the lines that look like this:
+
+ --Thread 1568861104 has waited at btr0cur.c line 424 ....
+
+=back
+
+=head1 MYSQL PRIVILEGES
+
+=over
+
+=item *
+
+You must connect to MySQL as a user who has the SUPER privilege for many of the
+functions.
+
+=item *
+
+If you don't have the SUPER privilege, you can still run some functions, but you
+won't necessarily see all the same data.
+
+=item *
+
+You need the PROCESS privilege to see the list of currently running queries in Q
+mode.
+
+=item *
+
+You need special privileges to start and stop slave servers.
+
+=item *
+
+You need appropriate privileges to create and drop the deadlock tables if needed
+(see L<"SERVER CONNECTIONS">).
+
+=back
+
 =head1 SYSTEM REQUIREMENTS
 
-You must connect to the DB server as a user who has the SUPER privilege for
-many of the functions.  If you don't have the SUPER privilege, you may still
-be able to run some functions.
+You need Perl to run innotop, of course.  You also need a few Perl modules: DBI,
+DBD::mysql,  Term::ReadKey, and Time::HiRes.  These should be included with most
+Perl distributions, but in case they are not, I recommend using versions
+distributed with your operating system or Perl distribution, not from CPAN.
+Term::ReadKey in particular has been known to cause problems if installed from
+CPAN.
 
-I think everything you need to run innotop is distributed either with Perl, or
-with innotop itself.  You need DBI and DBD::mysql.  You also need the
-InnoDBParser module, and Term::ReadKey.  If you have Time::HiRes, innotop will
-use it.  If you have Term::ANSIColor, innotop will use it to format headers more
-readably and compactly.  (Under Microsoft Windows, you also need
-Win32::Console::ANSI for terminal formatting codes to be honored).  If you
-install Term::ReadLine, preferably Term::ReadLine::Gnu, you'll get nice
-auto-completion support.
+If you have Term::ANSIColor, innotop will use it to format headers more readably
+and compactly.  (Under Microsoft Windows, you also need Win32::Console::ANSI for
+terminal formatting codes to be honored).  If you install Term::ReadLine,
+preferably Term::ReadLine::Gnu, you'll get nice auto-completion support.
 
 I run innotop on Gentoo GNU/Linux, Debian and Ubuntu, and I've had feedback from
 people successfully running it on Red Hat, CentOS, Solaris, and Mac OSX.  I
@@ -6169,19 +8177,69 @@
 I don't know for sure.  It also runs on Windows under ActivePerl without
 problem.
 
-I have perl v5.8.8 installed, and I've had reports of it working on 5.8.5 but
-I don't know about other versions.
+I use innotop on MySQL versions 3.23.58, 4.0.27, 4.1.0, 4.1.22, 5.0.26, 5.1.15,
+and 5.2.3.  If it doesn't run correctly for you, that is a bug and I hope you
+report it.
 
-I use innotop on MySQL version 4.1 and 5.0, and have heard of others using it
-on these same versions and 5.1.
-
 =head1 FILES
 
 $HOMEDIR/.innotop is used to store configuration information.
+$HOMEDIR/.innotop_core_dump contains verbose error messages if L<"debug"> is
+enabled.
 
+=head1 GLOSSARY OF TERMS
+
+=over
+
+=item tick
+
+A tick is a refresh event, when innotop re-fetches data from connections and
+displays it.
+
+=back
+
+=head1 ACKNOWLEDGEMENTS
+
+I'm grateful to the following people for various reasons, and hope I haven't
+forgotten to include anyone:
+
+Allen K. Smith,
+Aurimas Mikalauskas,
+Bartosz Fenski,
+Brian Miezejewski,
+Christian Hammers, 
+Cyril Scetbon,
+Dane Miller,
+David Multer,
+Dr. Frank Ullrich,
+Giuseppe Maxia,
+Google.com Site Reliability Engineers,
+Jan Pieter Kunst,
+Jari Aalto,
+Jay Pipes,
+Jeremy Zawodny,
+Johan Idren,
+Kristian Kohntopp,
+Lenz Grimmer,
+Maciej Dobrzanski,
+Michiel Betel,
+MySQL AB,
+Paul McCullagh,
+Sebastien Estienne,
+Sourceforge.net,
+Steven Kreuzer,
+The Gentoo MySQL Team,
+Trevor Price,
+Yaar Schnitman,
+and probably more people I've neglected to include.
+
+(If I misspelled your name, it's probably because I'm afraid of putting
+international characters into this documentation; earlier versions of Perl might
+not be able to compile it then).
+
 =head1 COPYRIGHT, LICENSE AND WARRANTY
 
-This program is copyright (c) 2006 Baron Schwartz, baron at xaprb dot com.
+This program is copyright (c) 2006 Baron Schwartz.
 Feedback and improvements are welcome.
 
 THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
@@ -6202,13 +8260,14 @@
 
 =head1 AUTHOR
 
-Baron Schwartz, baron at xaprb dot com.
+Baron Schwartz.
 
 =head1 BUGS
 
-If you find any problems with innotop, please contact me.  Specifically, if
-you find any problems with parsing the InnoDB monitor output, I would greatly
-appreciate you sending me the full text of the monitor output that caused the
-problem.
+You can report bugs, ask for improvements, and get other help and support at
+L<http://sourceforge.net/projects/innotop>.  There are mailing lists, forums,
+a bug tracker, etc.  Please use these instead of contacting me directly, as it
+makes my job easier and benefits others if the discussions are permanent and
+public.  Of course, if you need to contact me in private, please do.
 
 =cut

Modified: branches/sid-5.0/debian/changelog
===================================================================
--- branches/sid-5.0/debian/changelog	2007-11-08 18:39:40 UTC (rev 974)
+++ branches/sid-5.0/debian/changelog	2007-11-08 19:04:38 UTC (rev 975)
@@ -15,8 +15,9 @@
   * Suggest usage of an update statement on the user table to change the mysql
     root user password instead using mysqladmin, to catch all root users from
     all hosts. (closes: #435744)
+  * Updated innotop to 1.4.3 release.
 
- -- Norbert Tretkowski <nobse at debian.org>  Thu, 08 Nov 2007 19:29:38 +0100
+ -- Norbert Tretkowski <nobse at debian.org>  Thu, 08 Nov 2007 20:03:58 +0100
 
 mysql-dfsg-5.0 (5.0.45-1) unstable; urgency=low
 




More information about the Pkg-mysql-commits mailing list