#!/usr/bin/perl

use strict;
use warnings FATAL => 'all';

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
use DBI;
use English qw(-no_match_vars);
use List::Util qw(max min);
use InnoDBParser;
use Term::ReadKey;

# ###########################################################################
# Version, license and warranty information.
# ###########################################################################
our $VERSION = '1.0';

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.
Feedback and improvements are gratefully received.

THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 2; OR the Perl Artistic License.  On UNIX and similar
systems, you can issue `man perlgpl' or `man perlartistic' to read these
licenses.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA  02111-1307  USA.
LICENSE

# ###########################################################################
# End version, license and warranty information.
# ###########################################################################

# TODO: add a mode that gives a general status overview, like mysqlreport
# TODO: allow using Ctrl-C to cancel answering a question
# TODO: allow sorting by multiple columns (factor sorting out into a sub)
# TODO: factor filtering out into a sub

# ###########################################################################
# Really, really, super-global variables.
# ###########################################################################
my $clear_screen_sub;

# ###########################################################################
# Lookup tables and definitions etc.
# ###########################################################################

# This hash's elements may get 'filter' qr// elements added, to filter out
# rows in the results where that column doesn't match the filter.
# TODO: this and locks, and maybe other things too, are too redundant.
my %T_cols = (
   active_secs        => { hdr => 'Time',          just => '',  num => 1 },
   has_read_view      => { hdr => 'Rd View',       just => '',  num => 1 },
   heap_size          => { hdr => 'Heap',          just => '',  num => 1 },
   hostname           => { hdr => 'Host',          just => '-', num => 0 },
   ip                 => { hdr => 'IP',            just => '-', num => 0 },
   lock_wait_status   => { hdr => 'Lock Wait?',    just => '-', num => 0 },
   lock_wait_time     => { hdr => 'Wait',          just => '',  num => 1 },
   lock_structs       => { hdr => 'LStrcts',       just => '',  num => 1 },
   mysql_thread_id    => { hdr => 'Thread',        just => '',  num => 1 },
   os_thread_id       => { hdr => 'OS Thread',     just => '',  num => 1 },
   proc_no            => { hdr => 'Proc',          just => '',  num => 1 },
   query_id           => { hdr => 'Query ID',      just => '',  num => 1 },
   query_status       => { hdr => 'Query Status',  just => '-', num => 0 },
   query_text         => { hdr => 'Query Text',    just => '-', num => 0 },
   tables_in_use      => { hdr => 'Tbl Used',      just => '',  num => 1 },
   tables_locked      => { hdr => 'Tbl Lck',       just => '',  num => 1 },
   thread_decl_inside => { hdr => 'Thread Inside', just => '-', num => 0 },
   thread_status      => { hdr => 'Thread Status', just => '-', num => 0 },
   'time'             => { hdr => 'Time',          just => '',  num => 0 },
   txn_doesnt_see_ge  => { hdr => "Txn Won't See", just => '-', num => 0 },
   txn_id             => { hdr => 'ID',            just => '-', num => 0 },
   txn_sees_lt        => { hdr => 'Txn Sees',      just => '',  num => 0 },
   txn_status         => { hdr => 'Txn Status',    just => '-', num => 0 },
   undo_log_entries   => { hdr => 'Undo',          just => '',  num => 1 },
   user               => { hdr => 'User',          just => '-', num => 0 },
);

my %O_cols = (
   Database => {
      hdr   => 'Database',
      just  => '-',
      num   => 0,
      label => 'Database (schema)',
   },
   Table => {
      hdr   => 'Table',
      just  => '-',
      num   => 0,
      label => 'Table',
   },
   In_use => {
      hdr    => 'In Use',
      just   => '',
      num    => 1,
      label  => 'How many times the table is opened for use',
      filter => qr/[^0]/,
   },
   Name_locked => {
      hdr   => 'Name Locked',
      just  => '',
      num   => 1,
      label => 'Whether the table is name locked',
   },
);

my %Q_cols = (
   mysql_thread_id => {
      hdr   => 'ID',
      just  => '',
      num   => 1,
      label => 'The MySQL connection (thread) ID',
   },
   user => {
      hdr   => 'User',
      just  => '-',
      num   => 0,
      label => 'The database username',
   },
   host => {
      hdr   => 'Host',
      just  => '-',
      num   => 0,
      label => 'The hostname or IP address',
   },
   port => {
      hdr   => 'Client Port',
      just  => '',
      num   => 1,
      label => 'The client port number',
   },
   host_and_port => {
      hdr   => 'Host/IP',
      just  => '-',
      num   => 0,
      label => 'The hostname or IP address, and port number',
   },
   db => {
      hdr   => 'DB',
      just  => '-',
      num   => 0,
      label => 'The database, if any',
   },
   cmd => {
      hdr   => 'Cmd',
      just  => '-',
      num   => 0,
      label => 'The type of command being executed',
   },
   secs => {
      hdr   => 'Time',
      just  => '',
      num   => 1,
      label => 'The number of seconds since the last event',
   },
   'time' => {
      hdr   => 'Time',
      just  => '',
      num   => 0,
      label => 'The time since the last event',
   },
   state => {
      hdr   => 'State',
      just  => '-',
      num   => 0,
      label => 'The connection state',
   },
   info => {
      hdr   => 'Info',
      just  => '-',
      num   => 0,
      label => 'Info or the current query',
   },
   info_or_state => {
      hdr   => 'Query or State',
      just  => '-',
      num   => 0,
      label => 'The connection status or current query',
   },
);

# Operating modes for innotop.
my %modes = (
   A => {
      hdr               => 'Analyze',
      note              => 'Analyze queries with EXPLAIN and more',
      interval          => 0,
      action_for        => {
         e => {
            action => \&display_explain,
            label  => 'Analyze the query with EXPLAIN',
         },
         f => {
            action => \&show_full_query,
            label  => "Show the full query",
         },
         o => {
            action => \&show_optimized_query,
            label  => 'Show the optimized query',
         },
         q => {
            action => \&restore_mode,
            label  => 'Switch back to previous mode',
         },
      },
      data_required => [ qw(status) ],
      display_sub       => sub { draw_screen(["Press 'q' to resume operation."]) },
   },
   B => {
      hdr               => 'InnoDB Buf',
      note              => 'Shows buffer info from InnoDB',
      action_for        => {},
      data_required => [ qw(status innodb) ],
      display_sub       => \&display_B,
      innodb_required   => { bp => 1, ib => 1 },
   },
   D => {
      hdr               => 'InnoDB D/L',
      note              => 'View InnoDB deadlock information',
      action_for        => {
         c => {
            action => sub { get_config_interactive('dl_txn_fmt') },
            label  => 'Choose columns for the deadlock transaction display',
         },
         w => {
            action => \&create_deadlock,
            label  => 'Wipe deadlock status info by creating a deadlock',
         },
      },
      data_required => [ qw(status innodb) ],
      display_sub       => \&display_D,
      interval          => 0,
      innodb_required   => { dl => 1 },
   },
   F => {
      hdr               => 'InnoDB FK Err',
      note              => 'View the latest InnoDB foreign key error',
      action_for        => {},
      data_required => [ qw(status innodb) ],
      display_sub       => \&display_F,
      interval          => 0,
      innodb_required   => { fk => 1 },
   },
   G => {
      hdr               => 'Load Graph',
      note              => 'Shows query load graph',
      action_for        => {
         c => {
            action => sub {
               get_config_interactive('G_fmt');
               start_G_mode();
            },
            label => "Choose which columns to display",
         },
         i => {
            action => sub { $clear_screen_sub->(); toggle_value('status_inc') },
            label  => 'Toggle overall/incremental status display',
         },
         a => {
            action => sub { $clear_screen_sub->(); toggle_value('status_avg') },
            label  => 'Toggle accumulated/average status display',
         },
      },
      data_required     => [ qw(status) ],
      display_sub       => \&display_G,
      no_clear_screen   => 1,
   },
   I => {
      hdr               => 'InnoDB I/O Info',
      note              => 'Shows I/O info (i/o, log...) from InnoDB',
      action_for        => {},
      data_required     => [ qw(status innodb) ],
      display_sub       => \&display_I,
      innodb_required   => { io => 1, lg => 1 },
   },
   O => {
      hdr               => 'Open Tables',
      note              => 'Shows open tables in MySQL',
      action_for        => {
         c => {
            action => sub { get_config_interactive('O_fmt'); },
            label => "Choose which columns to display",
         },
         r => {
            action => \&reverse_sort,
            label  => 'Reverse sort order',
         },
         s => {
            action => sub { get_config_interactive('O_sort'); },
            label => "Choose sort column",
         },
         w => {
            action => sub { add_remove_filter(\%O_cols) },
            label  => "Add or remove filters (mnemonic: WHERE clause)",
         },
      },
      data_required     => [ qw(opentables status) ],
      display_sub       => \&display_O,
      innodb_required   => { },
   },
   Q => {
      hdr        => 'Query List',
      note       => 'Shows queries from SHOW FULL PROCESSLIST',
      action_for => {
         a => {
            action => sub { toggle_value('hide_self') },
            label  => 'Toggle hiding the innotop process',
         },
         c => {
            action => sub { get_config_interactive('Q_fmt'); },
            label => "Choose which columns to display",
         },
         e => {
            action => \&start_A_mode,
            label  => "Explain a thread's query",
         },
         f => {
            action => \&start_A_mode,
            label  => "Show a thread's full query",
         },
         h => {
            action => sub { toggle_value('show_QT_header') },
            label  => 'Toggle the header on and off',
         },
         i => {
            action => sub { toggle_value('hide_inactive') },
            label  => 'Toggle showing or hiding idle (Sleep) processes',
         },
         k => {
            action => sub { kill_query('CONNECTION') },
            label => "Kill a query's connection",
         },
         r => {
            action => \&reverse_sort,
            label  => 'Reverse sort order',
         },
         s => {
            action => sub { get_config_interactive('Q_sort'); },
            label => "Change the display's sort column",
         },
         w => {
            action => sub { add_remove_filter(\%Q_cols) },
            label  => "Add or remove filters (mnemonic: WHERE clause)",
         },
         x => {
            action => sub { kill_query('QUERY') },
            label => "Kill a query (not the connection; requires 5.0)",
         },
      },
      data_required => [ qw(status process) ],
      display_sub       => \&display_Q,
   },
   R => {
      hdr               => 'InnoDB Row Ops',
      note              => 'Shows InnoDB row operation and semaphore info',
      action_for        => {},
      data_required => [ qw(status innodb) ],
      display_sub       => \&display_R,
      innodb_required   => { ro => 1, sm => 1 },
   },
   S => {
      hdr               => 'Load Stats',
      note              => 'Shows query load statistics a la vmstat',
      action_for        => {
         c => {
            action => sub {
               get_config_interactive('S_fmt');
               start_S_mode();
            },
            label => "Choose which columns to display",
         },
         i => {
            action => sub { $clear_screen_sub->(); toggle_value('status_inc') },
            label  => 'Toggle overall/incremental status display',
         },
         a => {
            action => sub { $clear_screen_sub->(); toggle_value('status_avg') },
            label  => 'Toggle accumulated/average status display',
         },
         '-' => {
            action => sub { set_display_precision(-1) },
            label  => 'Decrease fractional display precision',
         },
         '+' => {
            action => sub { set_display_precision(1) },
            label  => 'Increase fractional display precision',
         },
      },
      data_required => [ qw(status) ],
      display_sub       => \&display_S,
      no_clear_screen   => 1,
   },
   T => {
      hdr        => 'InnoDB Txns',
      note       => 'Shows InnoDB transactions in top-like format',
      action_for => {
         a => {
            action => sub { toggle_value('hide_self') },
            label  => 'Toggle hiding the innotop process',
         },
         c => {
            action => sub { get_config_interactive('T_fmt'); },
            label => "Choose which columns to display",
         },
         e => {
            action => \&start_A_mode,
            label  => "Explain a transaction's query",
         },
         f => {
            action => \&show_full_tx,
            label  => "Show a transaction's full info",
         },
         h => {
            action => sub { toggle_value('show_QT_header') },
            label  => 'Toggle the header on and off',
         },
         i => {
            action => sub { toggle_value('hide_inactive') },
            label  => 'Toggle showing or hiding inactive transactions',
         },
         k => {
            action => sub { kill_query('CONNECTION') },
            label  => "Kill a transaction's connection",
         },
         r => {
            action => \&reverse_sort,
            label  => 'Reverse sort order',
         },
         s => {
            action => sub { get_config_interactive('T_sort'); },
            label  => "Change the display's sort column",
         },
         w => {
            action => sub { add_remove_filter(\%T_cols) },
            label  => "Add or remove filters (mnemonic: WHERE clause)",
         },
         x => {
            action => sub { kill_query('QUERY') },
            label  => "Kill a query, not a connection (requires 5.0)",
         },
      },
      data_required => [ qw(status innodb) ],
      display_sub       => \&display_T,
      innodb_required   => { tx => 1 },
   },
   V => {
      hdr               => 'Variables & Status',
      note              => 'Shows values from SHOW STATUS and SHOW VARIABLES',
      action_for        => {
         c => {
            action => sub { choose_V_columns() },
            label  => 'Choose which values to show in the display',
         },
         i => {
            action => sub { $clear_screen_sub->(); toggle_value('status_inc') },
            label  => 'Toggle overall/incremental status display',
         },
         a => {
            action => sub { $clear_screen_sub->(); toggle_value('status_avg') },
            label  => 'Toggle accumulated/average status display',
         },
         '-' => {
            action => sub { set_display_precision(-1) },
            label  => 'Decrease fractional display precision',
         },
         '+' => {
            action => sub { set_display_precision(1) },
            label  => 'Increase fractional display precision',
         },
         0 => {
            action => sub { set_V_set(0) },
            label  => 'View the 0th stored configuration',
         },
         1 => {
            action => sub { set_V_set(1) },
            label  => 'View the 1st stored configuration',
         },
         2 => {
            action => sub { set_V_set(2) },
            label  => 'View the 2nd stored configuration',
         },
         3 => {
            action => sub { set_V_set(3) },
            label  => 'View the 3rd stored configuration',
         },
         4 => {
            action => sub { set_V_set(4) },
            label  => 'View the 4th stored configuration',
         },
         5 => {
            action => sub { set_V_set(5) },
            label  => 'View the 5th stored configuration',
         },
         6 => {
            action => sub { set_V_set(6) },
            label  => 'View the 6th stored configuration',
         },
         7 => {
            action => sub { set_V_set(7) },
            label  => 'View the 7th stored configuration',
         },
         8 => {
            action => sub { set_V_set(8) },
            label  => 'View the 8th stored configuration',
         },
         9 => {
            action => sub { set_V_set(9) },
            label  => 'View the 9th stored configuration',
         },
      },
      data_required => [ qw(status) ],
      display_sub       => \&display_V,
   },
   W => {
      hdr             => 'InnoDB Lock Waits',
      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',
         },
      },
      data_required   => [qw(status innodb)],
      display_sub     => \&display_W,
      innodb_required => { tx => 1, sm => 1 },
   },
);

my %derived_val = (
   KeyCacheHitRate   => sub {
      my ( $set, $inc, $avg ) = @_;
      my $num = get_union_s_v( 'Key_reads', $set, $inc, 0, { vbtm => 1 } ) || 0;
      my $denom = get_union_s_v( 'Key_read_requests', $set, $inc, 0, { vbtm => 1 }) || 1;
      return 1 - ( $num / $denom );
   },
   QueryCacheHitRate => sub {
      my ( $set, $inc, $avg ) = @_;
      my $num = get_union_s_v( 'Qcache_hits', $set, $inc, 0, { vbtm => 1 } ) || 0;
      my $denom = $num + get_union_s_v( 'Com_select', $set, $inc, 0, { vbtm => 1 }) || 1;
      return $num / $denom;
   },
);

# ###########################################################################
# Key mappings.  Keyed on a single character, which is read from the keyboard.
# Uppercase letters switch modes.  Lowercase letters access commands when in a
# mode.  These can be overridden by action_for in %modes.
# ###########################################################################
my %action_for = (
   '$' => {
      action => \&display_everything_configurable,
      label  => 'Edit configuration settings',
   },
   '?' => {
      action => \&display_help,
      label  => 'Show help',
   },
   '!' => {
      action => \&display_license,
      label  => 'Show license and warranty information',
   },
   B => {
      action => sub { switch_mode('B') },
      label  => 'Switch to B mode (InnoDB Buffer/Hash Index)',
   },
   D => {
      action => sub { switch_mode('D') },
      label  => 'Switch to D mode (InnoDB Deadlock Information)',
   },
   F => {
      action => sub { switch_mode('F') },
      label  => 'Switch to F mode (InnoDB Foreign Key Error)',
   },
   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)',
   },
   O => {
      action => sub { switch_mode('O') },
      label  => 'Switch to O mode (MySQL Open Tables)',
   },
   Q => {
      action => sub { switch_mode('Q') },
      label  => 'Switch to Q mode (Query List, like mytop)',
   },
   R => {
      action => sub { switch_mode('R') },
      label  => 'Switch to R mode (InnoDB Row Operations)',
   },
   S => {
      action => \&start_S_mode,
      label  => 'Switch to S mode (Load Statistics)',
   },
   T => {
      action => sub { switch_mode('T') },
      label  => 'Switch to T mode (InnoDB Transaction)',
   },
   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)',
   },
   d => {
      action => sub { get_config_interactive('interval') },
      label  => 'Change refresh interval',
   },
   l => {
      action => sub { toggle_value('long_numbers') },
      label  => 'Toggle long/short number format',
   },
   p => { action => \&pause,             label => 'Pause innotop', },
   q => { action => \&finish,            label => 'Quit innotop', },
   u => { action => \&dump_current_info, label => 'Dump current data', },
);

# ###########################################################################
# Global variables.
# ###########################################################################
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 $innodb_status;
my $innodb_status_text;
my $full_processlist;
my $open_tables;
my @sql_status;
my @sql_variables;
my @sql_status_times;
my $want_status_info = 0;
my $mysql_version;
my $ver_major;
my $ver_minor;
my $ver_rev;
my $connection_id;
my $char;
my $previous_mode = "Q";
my $this_time;
my $last_time;
my $time_sub;
my $line_counter = 0;
my $query_to_analyze;
my $have_color = 0;
my @innodb_files;
my $innodb_file_counter = -1;
my $windows = $OSNAME eq 'MSWin32';

# If hi-res time is available, use it.
eval {
   require Time::HiRes;
   $time_sub = sub { &Time::HiRes::gettimeofday(); };
};
if ( $EVAL_ERROR ) {
   $time_sub = sub { time(); };
}

# Find the home directory; it's different on different OSes.
my $homepath = $ENV{'HOME'} || $ENV{'HOMEPATH'} || $ENV{'USERPROFILE'} || '.';
die "Your home directory ($homepath) doesn't exist or isn't writable"
   unless -d $homepath && -w $homepath;

# If terminal coloring is available, use it.  The only function I want from
# the module is the colored() function.
eval {
   if ( $windows ) {
      require Win32::Console::ANSI;
   }
   require Term::ANSIColor;
   import Term::ANSIColor qw(colored);
   $have_color = 1;
};
if ( $EVAL_ERROR ) {
   # If there was an error, manufacture my own colored() function that does no
   # coloring.
   *colored = sub { return shift; };
}

if ( $windows ) {
   $clear_screen_sub = sub { $line_counter = 0; system("cls") };
}
else {
   my $clear = `clear`;
   $clear_screen_sub = sub { $line_counter = 0; print $clear };
}

my %data_subs_for = (
   status     => \&get_status_info,
   innodb     => \&get_innodb_status,
   process    => \&get_full_processlist,
   opentables => \&get_open_tables,
);

# ###########################################################################
# Config storage.  Create info for every config variable the program cares
# about, with initial values.  After these are read in, they are marked as
# 'read' so any un-read values can be prompted for.  If they are read from the
# command-line and are already set, they are marked as 'dontsave' so they
# don't get written to the config file at exit.
# ###########################################################################
my %config = (
   unicode => {
      val  => 0,
      note => 'Whether to allow Unicode characters to be displayed in queries',
      conf => 'ALL',
      pat  => qr/^0|1$/,
   },
   innodb_status_from_dir => {
      val  => '',
      note => 'A directory that holds InnoDB status files (for testing)',
      conf => [],
   },
   max_height => {
      val  => 30,
      note => '[Win32] Max window height',
      conf => 'ALL',
   },
   dl_lock_fmt => {
      val  => [ qw(num what lock_mode db table index heap_no
         special insert_intention) ],
      note => 'The column format for locks in D (InnoDB Deadlock) mode',
      meta => {
         num              => { hdr => 'Txn',        just => '' },
         what             => { hdr => 'What',       just => '-' },
         txn_id           => { hdr => 'Txn ID',     just => '-' },
         lock_type        => { hdr => 'Type',       just => '-' },
         space_id         => { hdr => 'Space',      just => '' },
         page_no          => { hdr => 'Page',       just => '' },
         heap_no          => { hdr => 'Heap',       just => '' },
         n_bits           => { hdr => '# Bits',     just => '' },
         index            => { hdr => 'Index',      just => '-' },
         db               => { hdr => 'DB',         just => '-' },
         table            => { hdr => 'Tbl',        just => '-' },
         lock_mode        => { hdr => 'Mode',       just => '-' },
         special          => { hdr => 'Special',    just => '-' },
         insert_intention => { hdr => 'Ins Intent', just => '' },
         waiting          => { hdr => 'Wait',       just => '' },
         num_locks        => { hdr => 'Num Lcks',   just => '' },
      },
      conf => [ qw(D) ],
   },
   dl_txn_fmt => {
      val  => [ qw(num mysql_thread_id user hostname active_secs undo_log_entries lock_structs) ],
      note => 'The column format for D (InnoDB Deadlock) mode',
      meta => {
         # These are similar to T mode.
         active_secs        => { hdr => 'Time',          just => ''},
         has_read_view      => { hdr => 'Rd View',       just => ''},
         heap_size          => { hdr => 'Heap',          just => ''},
         hostname           => { hdr => 'Host',          just => '-'},
         ip                 => { hdr => 'IP',            just => '-'},
         lock_wait_time     => { hdr => 'Wait',          just => ''},
         lock_structs       => { hdr => 'LStrcts',       just => ''},
         mysql_thread_id    => { hdr => 'Thread',        just => ''},
         query_text         => { hdr => 'Query Text',    just => '-'},
         tables_in_use      => { hdr => 'Tbl Used',      just => ''},
         tables_locked      => { hdr => 'Tbl Lck',       just => ''},
         txn_id             => { hdr => 'ID',            just => '-'},
         undo_log_entries   => { hdr => 'Undo',          just => ''},
         user               => { hdr => 'User',          just => '-'},
         num                => { hdr => 'Txn',           just => ''},
      },
      conf => [ qw(D) ],
   },
   dl_table => {
      val  => 'test.innodb_deadlock_maker',
      pat  => qr/\w+\.\w+/,
      note => 'DB.TABLE name of the table, which must not exist, used to reset deadlock info',
      conf => [ qw(D) ],
   },
   debug => {
      val  => 0,
      pat  => qr/^0|1$/,
      note => 'Debug mode (more verbose errors, uses more memory)',
      conf => [ qw(D) ],
   },
   long_numbers => {
      val  => 0,
      pat  => qr/^0|1$/,
      note => 'Whether to show long numbers in headers',
      conf => [ qw(Q) ],
   },
   num_digits => {
      val  => 2,
      pat  => qr/^\d$/,
      note => 'How many digits to show in fractional numbers',
      conf => [ qw(V) ],
   },
   show_QT_header => {
      val  => 1,
      pat  => qr/^0|1$/,
      note => 'Whether to show the header in Q and T modes',
      conf => [ qw(Q T) ],
   },
   hide_inactive => {
      val  => 0,
      pat  => qr/^0|1$/,
      note => 'Whether to show idle (sleeping) processes',
      conf => [ qw(T Q) ],
   },
   debugfile => {
      val  => "$homepath/.innotop_core_dump",
      note => 'A debug file in case you are interested in error output',
   },
   port => {
      val    => 3306,
      note   => "Which port to connect to",
      prompt => 1,
      pat    => qr/^[1-9]\d*$/,
      cmdline=> 1,
   },
   host => {
      val    => "localhost",
      note   => "Which server to connect to",
      prompt => 1,
      cmdline=> 1,
   },
   user => {
      prompt => 1,
      note   => "The DB user (must have SUPER privilege for some modes)",
      cmdline=> 1,
   },
   db => {
      prompt => 1,
      note   => "Which DB to connect to",
      cmdline=> 1,
   },
   password => {
      note   => "The password of a user with the SUPER privilege",
      prompt => 1,
      noecho => 1,
      cmdline=> 1,
   },
   savepass => {
      val    => 0,
      pat    => qr/^0|1$/,
      prompt => 1,
      note   => "Whether to save your DB password in the config file",
      hint   => "this must be a 1 or 0",
   },
   show_statusbar => {
      val  => 1,
      pat  => qr/^0|1$/,
      note => 'Whether to show the status bar in the display',
      conf => 'ALL',
   },
   hide_self => {
      val  => 1,
      note => 'Whether to hide the innotop query process in T and Q modes',
      pat  => qr/^0|1$/,
      conf => [ qw(Q T) ],
   },
   mode => {
      val  => "T",
      note => "Which mode to start in",
      meta => \%modes,
      cmdline => 1,
   },
   T_fmt => {
      val  => [ qw( mysql_thread_id user hostname txn_status time
                    lock_wait_time lock_structs tables_in_use tables_locked
                    heap_size undo_log_entries query_text)],
      note => "The column format for T (InnoDB Transaction) mode",
      meta => \%T_cols,
      conf => [ qw(T) ],
   },
   T_sort => {
      val  => 'mysql_thread_id',
      note => "The sort column for T (InnoDB Transaction) mode",
      meta => \%T_cols,
      conf => [ qw(T) ],
   },
   O_fmt => {
      val  => [ qw(Database Table In_use Name_locked)],
      note => 'The column format for O (Open Tables) mode',
      meta => \%O_cols,
      conf => [ qw(O) ],
   },
   O_sort => {
      val  => '',
      note => "The sort column for O (Open Tables) mode",
      meta => \%O_cols,
      conf => [ qw(O) ],
   },
   Q_fmt => {
      val  => [ qw(mysql_thread_id user host db time cmd info_or_state)],
      note => 'The column format for Q (Query List) mode',
      meta => \%Q_cols,
      conf => [ qw(Q) ],
   },
   Q_sort => {
      val  => 'mysql_thread_id',
      note => "The sort column for Q (Query List) mode",
      meta => \%Q_cols,
      conf => [ qw(Q) ],
   },
   W_fmt => {
      val => [ qw(mysql_thread_id lock_wait_time lock_mode db table
            index insert_intention special ) ],
      note => 'The column format for W (InnoDB Lock Wait) mode',
      meta => {
         db               => { hdr => 'DB',         just => '-' },
         index            => { hdr => 'Index',      just => '-' },
         insert_intention => { hdr => 'Ins Intent', just => '' },
         lock_mode        => { hdr => 'Mode',       just => '-' },
         lock_type        => { hdr => 'Type',       just => '-' },
         lock_wait_time   => { hdr => 'Time',       just => '' },
         mysql_thread_id  => { hdr => 'Thread',     just => '' },
         n_bits           => { hdr => '# Bits',     just => '' },
         num_locks        => { hdr => 'Num Lcks',   just => '' },
         page_no          => { hdr => 'Page',       just => '' },
         space_id         => { hdr => 'Space',      just => '' },
         special          => { hdr => 'Special',    just => '-' },
         table            => { hdr => 'Tbl',        just => '-' },
         txn_id           => { hdr => 'Txn ID',     just => '-'},
         waiting          => { hdr => 'Wait',       just => '' },
      },
      conf => [ qw(W) ],
   },
   iusd_fmt => {
      val  => [ qw( h i u r d ) ],
      note => "The column format for R (Row Operation) mode for the Ins/Upd/Read/Del table",
      meta => {
         h => { hdr => 'What', just => '-', label => 'Row header' },
         i => { hdr => 'Ins',  just => '',  label => 'Number of inserts' },
         u => { hdr => 'Upd',  just => '',  label => 'Number of updates' },
         r => { hdr => 'Read', just => '',  label => 'Number of reads' },
         d => { hdr => 'Del',  just => '',  label => 'Number of deletes' },
      },
      conf => [ qw(R) ],
   },
   sm_fmt => {
      val  => [  qw(mutex_os_waits mutex_spin_rounds mutex_spin_waits
                  reservation_count rw_excl_os_waits rw_excl_spins
                  rw_shared_os_waits rw_shared_spins signal_count
                  wait_array_size)
            ],
      note => "The values to show in the InnoDB Semaphores display",
      meta => {
         mutex_os_waits     => { hdr => 'Mutex OS Waits' },
         mutex_spin_rounds  => { hdr => 'Mutex Spin Rounds' },
         mutex_spin_waits   => { hdr => 'Mutex Spin Waits' },
         reservation_count  => { hdr => 'Reservation Count' },
         rw_excl_os_waits   => { hdr => 'R/W Excl. OS Waits' },
         rw_excl_spins      => { hdr => 'R/W Excl. Spins' },
         rw_shared_os_waits => { hdr => 'R/W Shared OS Waits' },
         rw_shared_spins    => { hdr => 'R/W Shared Spins' },
         signal_count       => { hdr => 'Signal Count' },
         wait_array_size    => { hdr => 'Wait Array Size' },
      },
      conf => [ qw(R) ],
   },
   wait_array_fmt => {
      val => [
         qw(thread waited_secs waited_at_filename waited_at_line
            request_type num_readers lock_var waiters_flag cell_waiting
            cell_event_set)
      ],
      note => 'The columns to show in the wait array table',
      conf => [qw(R W)],
      meta => {
         thread             => { hdr => 'Thread',        just => '' },
         waited_at_filename => { hdr => 'File',          just => '-' },
         waited_at_line     => { hdr => 'Line',          just => '' },
         waited_secs        => { hdr => 'Time',          just => '' },
         request_type       => { hdr => 'Type',          just => '-' },
         lock_mem_addr      => { hdr => 'Addr',          just => '-' },
         lock_cfile_name    => { hdr => 'Crtd File',     just => '-' },
         lock_cline         => { hdr => 'Crtd Line',     just => '' },
         writer_thread      => { hdr => 'Wrtr Thread',   just => '' },
         writer_lock_mode   => { hdr => 'Wrtr Lck Mode', just => '-' },
         num_readers        => { hdr => 'Readers',       just => '' },
         lock_var           => { hdr => 'Lck Var',       just => '' },
         waiters_flag       => { hdr => 'Waiters',       just => '' },
         last_s_file_name   => { hdr => 'S-File',        just => '-' },
         last_s_line        => { hdr => 'S-Line',        just => '' },
         last_x_file_name   => { hdr => 'X-File',        just => '-' },
         last_x_line        => { hdr => 'X-Line',        just => '' },
         cell_waiting       => { hdr => 'Waiting?',      just => '' },
         cell_event_set     => { hdr => 'Ending?',       just => '' },
      },
   },
   explain_fmt => {
      val => [qw(select_type table partitions type possible_keys key key_len ref rows Extra)],
      note => 'The values to show in the Explain display',
      meta => {
         id            => { hdr => 'Part ID',     label => '' },
         select_type   => { hdr => 'Select Type', label => '' },
         table         => { hdr => 'Table',       label => '' },
         partitions    => { hdr => 'Partitions',  label => '' },
         type          => { hdr => 'Type',        label => '' },
         possible_keys => { hdr => 'Poss. Keys',  label => '' },
         key           => { hdr => 'Key Chosen',  label => '' },
         key_len       => { hdr => 'Key Length',  label => '' },
         'ref'         => { hdr => 'Index Ref',   label => '' },
         rows          => { hdr => 'Row Count',   label => '' },
         Extra         => { hdr => 'Extra Info',  label => '' },
      },
      conf => [qw(A)],
   },
   bp_fmt => {
      val  => [  qw(total_mem_alloc awe_mem_alloc add_pool_alloc
            buf_pool_size buf_free buf_pool_hit_rate buf_pool_reads
            buf_pool_hits pages_total pages_modified) ],
      note => 'The values to show in the Buffer Pool Misc display',
      meta => {
         total_mem_alloc            => { hdr =>  'Memory Allocated' },
         awe_mem_alloc              => { hdr =>  'AWE Memory Allocated' },
         add_pool_alloc             => { hdr =>  'Add\'l Pool Alloc' },
         buf_pool_size              => { hdr =>  'Buffer Pool Size' },
         buf_free                   => { hdr =>  'Buffers Free' },
         buf_pool_hit_rate          => { hdr =>  'Buffer Pool Hit Rate' },
         buf_pool_reads             => { hdr =>  'Buffer Pool Reads' },
         buf_pool_hits              => { hdr =>  'Buffer Pool Hits' },
         pages_total                => { hdr =>  'Database Pages' },
         pages_modified             => { hdr =>  'Modified DB Pages' },
         reads_pending              => { hdr =>  'Pending Reads' },
         writes_pending             => { hdr =>  'Pending Writes' },
         writes_pending_lru         => { hdr =>  'Pending LRU Writes' },
         writes_pending_flush_list  => { hdr =>  'Pending Flush List Writes' },
         writes_pending_single_page => { hdr =>  'Pending Single-Page Writes' },
         page_creates_sec           => { hdr =>  'Page Creates/Sec' },
         page_reads_sec             => { hdr =>  'Page Reads/Sec' },
         page_writes_sec            => { hdr =>  'Page Writes/Sec' },
         pages_created              => { hdr =>  'Pages Created' },
         pages_read                 => { hdr =>  'Pages Read' },
         pages_written              => { hdr =>  'Pages Written' },
      },
      conf => [ qw(B) ],
   },
   file_io_misc_fmt => {
      val  => [ qw(os_file_reads os_file_writes os_fsyncs reads_s writes_s
            avg_bytes_s ) ],
      note => 'The values to show in the File I/O Misc display',
      meta => {
         avg_bytes_s                 => { hdr => 'Avg Bytes/Sec', },
         flush_type                  => { hdr => 'Flush Type', },
         fsyncs_s                    => { hdr => 'fsyncs/sec', },
         os_file_reads               => { hdr => 'OS File Reads', },
         os_file_writes              => { hdr => 'OS File Writes', },
         os_fsyncs                   => { hdr => 'OS fsyncs', },
         reads_s                     => { hdr => 'Avg Reads/Sec', },
         writes_s                    => { hdr => 'Avg Writes/Sec', },
         pending_aio_writes          => { hdr => 'Pending Async I/O Writes', },
         pending_buffer_pool_flushes => { hdr => 'Pending Buffer Pool Flushes', },
         pending_ibuf_aio_reads      => { hdr => 'Pending Insert Buf Async I/O Reads', },
         pending_log_flushes         => { hdr => 'Pending Log Flushes', },
         pending_log_ios             => { hdr => 'Pending Log I/Os', },
         pending_normal_aio_reads    => { hdr => 'Pending Async I/O Reads', },
         pending_preads              => { hdr => 'Pending preads', },
         pending_pwrites             => { hdr => 'Pending pwrites', },
         pending_sync_ios            => { hdr => 'Pending Sync I/Os', },
      },
      conf => [ qw(I) ],
   },
   log_stats_fmt => {
      val  => [ qw(log_seq_no log_flushed_to last_chkp pending_log_writes
                  pending_chkp_writes log_ios_done log_ios_s) ],
      note => 'The values to show in the Log Statistics display',
      meta => {
         last_chkp           => { hdr => 'Last Checkpoint', },
         log_flushed_to      => { hdr => 'Flushed To', },
         log_ios_done        => { hdr => 'I/Os Done', },
         log_ios_s           => { hdr => 'Avg I/Os per Sec', },
         log_seq_no          => { hdr => 'Sequence No.', },
         pending_chkp_writes => { hdr => 'Pending Chkpt Writes', },
         pending_log_writes  => { hdr => 'Pending Log Writes', },
      },
      conf => [ qw(I) ],
   },
   io_pending_fmt => {
      val  => [ qw(pending_normal_aio_reads pending_aio_writes
            pending_ibuf_aio_reads pending_sync_ios pending_log_flushes
            pending_log_ios) ],
      note => 'The values to show in the Pending File I/O display',
      meta => {
         pending_normal_aio_reads    => { hdr => 'Async I/O Reads', },
         pending_aio_writes          => { hdr => 'Async I/O Writes', },
         pending_ibuf_aio_reads      => { hdr => 'Insert Buf Async I/O Reads', },
         pending_sync_ios            => { hdr => 'Sync I/Os', },
         pending_buffer_pool_flushes => { hdr => 'Buffer Pool Flushes', },
         pending_log_flushes         => { hdr => 'Log Flushes', },
         pending_log_ios             => { hdr => 'Log I/Os', },
         pending_preads              => { hdr => 'preads', },
         pending_pwrites             => { hdr => 'pwrites', },
      },
      conf => [ qw(I) ],
   },
   crw_fmt => {
      val  => [ qw(h c r w)],
      note => "The column format for the Buffer Pool Pages Create/Read/Write table",
      meta => {
         h => { hdr => 'What',    just => '-', label => 'Row header'},
         c => { hdr => 'Creates', just => '' , label => 'How many creates/sec'},
         r => { hdr => 'Reads',   just => '' , label => 'How many reads/sec'},
         w => { hdr => 'Writes',  just => '' , label => 'How many writes/sec'},
      },
      conf => [ qw(B) ],
   },
   status_inc => {
      val  => 1,
      note => 'Whether to show raw or incremental values for status variables',
      pat  => qr/^0|1$/,
   },
   status_avg => {
      val  => 0,
      note => 'Whether to average status variables over elapsed time',
      pat  => qr/^0|1$/,
   },
   q_header_fmt => {
      val  => [qw(when qps slow cache_hit key_buffer_hit bps_in bps_out)],
      note => "The format for the Q (Query List) header",
      meta => {
         when => {
            hdr   => 'When',
            just  => '-',
            label => 'What time period the row describes',
         },
         questions => {
            hdr   => 'Questions',
            just  => '',
            label => 'How many queries',
         },
         qps => {
            hdr   => 'QPS',
            just  => '',
            label => 'How many queries/sec',
         },
         slow => {
            hdr   => 'Slow',
            just  => '',
            label => 'How many slow queries',
         },
         cache_hit => {
            hdr   => 'QCacheHit',
            just  => '',
            label => 'Query cache hit ratio',
         },
         key_buffer_hit => {
            hdr   => 'KCacheHit',
            just  => '',
            label => 'Key cache hit ratio',
         },
         bps_in => {
            hdr   => 'BpsIn',
            just  => '',
            label => 'Bytes per second received by the server',
         },
         bps_out => {
            hdr   => 'BpsOut',
            just  => '',
            label => 'Bytes per second sent by the server',
         },
      },
      conf => [ qw(Q) ],
   },
   io_thread_fmt => {
      val  => [qw(thread purpose state event_set)],
      note => 'The column format for the I/O Thread State table',
      meta => {
         thread    => { hdr => 'ID',      just => '',  label => "The thread's ID" },
         purpose   => { hdr => 'Purpose', just => '-', label => "The thread's purpose" },
         state     => { hdr => 'State',   just => '-', label => "The thread's state" },
         event_set => {
            hdr   => 'Evt Set?',
            just  => '',
            label => '[Win32] Whether a wait event is set',
         },
      },
      conf => [qw(I)],
   },
   fk_fmt => {
      val  => [ qw(timestring child_db child_table child_index parent_db
                  parent_table parent_col parent_index fk_name attempted_op) ],
      note => 'The column format for the Foreign Key Error display',
      meta => {
         timestring => {
            hdr   => 'Time',
            just  => '-',
            label => 'Time the error occurred'
         },
         child_db => {
            hdr   => 'Child DB',
            just  => '-',
            label => 'The database of the child table'
         },
         child_table => {
            hdr   => 'Child Table',
            just  => '-',
            label => 'The child table'
         },
         child_index => {
            hdr   => 'Child Index',
            just  => '-',
            label => 'The index in the child table'
         },
         fk_name => {
            hdr   => 'Constraint',
            just  => '-',
            label => 'The name of the FK constraint'
         },
         parent_db => {
            hdr   => 'Parent DB',
            just  => '-',
            label => 'The database of the parent table'
         },
         parent_table => {
            hdr   => 'Parent Table',
            just  => '-',
            label => 'The parent table'
         },
         parent_col => {
            hdr   => 'Parent Column',
            just  => '-',
            label => 'The referred column in the parent table',
         },
         parent_index => {
            hdr   => 'Parent Index',
            just  => '-',
            label => 'The referred index in the parent table'
         },
         attempted_op => {
            hdr   => 'Attempted Action',
            just  => '-',
            label => 'The action that caused the error'
         },
      },
      conf => [ qw(F) ],
   },
   ib_ts_fmt => {
      val  => [ qw(size free_list_len seg_size inserts merged_recs merges is_empty) ],
      note => 'The column format for the Insert Buffer table',
      meta => {
         size => {
            hdr   => 'Size',
            just  => '',
            label => 'Size of the tablespace'
         },
         free_list_len => {
            hdr   => 'Free List Len',
            just  => '',
            label => 'Length of the free list'
         },
         seg_size => {
            hdr   => 'Seg. Size',
            just  => '',
            label => 'Segment size'
         },
         inserts => {
            hdr   => 'Inserts',
            just  => '',
            label => 'Inserts'
         },
         merged_recs => {
            hdr   => 'Merged Recs',
            just  => '',
            label => 'Merged records'
         },
         merges => {
            hdr   => 'Merges',
            just  => '',
            label => 'Merges'
         },
         is_empty => {
            hdr   => 'Empty?',
            just  => '-',
            label => 'Whether the tablespace is empty'
         },
      },
      conf => [ qw(B) ],
   },
   ahi_fmt => {
      val  => [ qw(hash_table_size used_cells bufs_in_node_heap
            hash_searches_s non_hash_searches_s) ],
      note => 'The format for the Buffer Pool/Adaptive Hash Index table',
      meta => {
         hash_table_size     => { hdr => 'Hash Table Size', },
         used_cells          => { hdr => 'Hash Table Used Cells', },
         bufs_in_node_heap   => { hdr => 'Node Heap Buffers', },
         hash_searches_s     => { hdr => 'Hash Searches/Sec', },
         non_hash_searches_s => { hdr => 'Non-Hash Searches/Sec', },
      },
      conf => [ qw(B) ],
   },
   R_fmt => {
      val  => [  qw(queries_in_queue queries_inside read_views_open
                  main_thread_id main_thread_proc_no main_thread_state
                  n_reserved_extents)
            ],
      note => 'The values to show in the Row Operation Misc display',
      meta => {
         queries_in_queue    => { hdr => 'Queries In Queue', },
         queries_inside      => { hdr => 'Queries In InnoDB', },
         read_views_open     => { hdr => 'Read Views Open', },
         main_thread_id      => { hdr => 'Main Thread ID', },
         main_thread_proc_no => { hdr => 'Main Thread Proc', },
         main_thread_state   => { hdr => 'Main Thread State', },
         n_reserved_extents  => { hdr => 'Extents Reserved for B-Tree', },
      },
      conf => [ qw(R) ],
   },
   sort_dir => {
      val  => 1,
      pat  => qr/^-?1$/,
      note => "Whether to sort ascending or descending (1 or -1)",
   },
   interval => {
      val  => 10,
      pat  => qr/^[1-9]\d*$/,
      note => "The interval at which the display will be refreshed",
   },
   V_set => {
      val  => 0,
      pat  => qr/^\d$/,
      note => 'Which set of variables you want to display in V mode',
      conf => [ qw(V) ],
   },
   # General stats about queries
   V_fmt_0 => {
      val  => [ 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 ) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Stuff about how queries are performed
   V_fmt_1 => {
      val  => [ 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) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # InnoDB stuff
   V_fmt_2 => {
      val  => [ 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) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Transaction information
   V_fmt_3 => {
      val  => [ 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) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Stuff about key cache
   V_fmt_4 => {
      val  => [ qw( Uptime Key_blocks_not_flushed Key_blocks_unused
            Key_blocks_used Key_read_requests Key_reads Key_write_requests
            Key_writes ) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Stuff about query cache
   V_fmt_5 => {
      val  => [ 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 ) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Stuff about handler
   V_fmt_6 => {
      val  => [ 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) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Stuff about connections, open files, threads, etc.
   V_fmt_7 => {
      val  => [ 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) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   # Prepared statements
   V_fmt_8 => {
      val  => [ 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 ) ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   V_fmt_9 => {
      val  => [ qw() ],
      note => 'One of ten SHOW VARIABLES and SHOW STATUS values table formats',
      hint => 'You can enter the name of any value in SHOW VARIABLES or SHOW STATUS.',
   },
   num_status_sets => {
      val  => 3,
      pat  => qr/^[2-9]$/,
      note => 'How many sets of STATUS and VARIABLES values to show',
      conf => [ qw(V) ],
   },
   G_fmt => {
      val  => [ qw(Questions Com_select Com_insert Com_update Com_delete)],
      note => 'The values to display in G (Load Graph) mode',
      hint => 'You can use any STATUS or VARIABLE values, plus ('
              . join(' ', sort keys %derived_val) . ')',
      conf => [ qw(G) ],
   },
   S_fmt => {
      val  => [ qw(Questions Com_select Com_insert Com_update Com_delete)],
      note => 'The values to display in S (Load Statistics) mode',
      hint => 'You can use any STATUS or VARIABLE values, plus ('
              . join(' ', sort keys %derived_val) . ')',
      conf => [ qw(S) ],
   },
   max_per_sec_ever_seen => {
      val  => {
         Com_select   => 500,
         Com_insert   => 500,
         Com_update   => 500,
         Com_delete   => 500,
         Questions    => 1_000,
      },
      note => 'Maximum bucketized SHOW STATUS values ever seen',
   },
);

if ( !$windows ) {
   delete $config{'max_height'};
}

# ###########################################################################
# It all happens here, folks.  This is it.
# ###########################################################################

# Check whether the user wants the help screen, before starting the actual
# program.
if ( @ARGV && $ARGV[0] =~ m/^-{0,2}h(?:elp)?$/ ) {
   show_help();
}

# Try to lower my priority.
eval { setpriority(0, 0, getpriority(0, 0) + 10); };
# Print stuff to the screen immediately, don't wait for a newline.
$OUTPUT_AUTOFLUSH = 1;

$clear_screen_sub->();
load_config();

# In case we're in test mode, get a list of files that will hold InnoDB
# monitor text.
if ( $config{'innodb_status_from_dir'}->{'val'} ) {
   my $dirname = $config{'innodb_status_from_dir'}->{'val'};
   if ( opendir my $dir, $dirname ) {
      @innodb_files
         = map { "$dirname/$_" }
         grep { -f "$dirname/$_" && -r "$dirname/$_" } readdir($dir);
      closedir($dir);
   }
   else {
      $config{'innodb_status_from_dir'}->{'val'} = '';
   }
}

eval {
   main_loop();
};
if ( $EVAL_ERROR ) {
   core_dump( $EVAL_ERROR );
}
finish();

# ###########################################################################
# Subroutines.
# ###########################################################################

sub main_loop {
   while (1) {
      my $mode = $config{'mode'}->{'val'};
      get_time();
      @last_term_size = @this_term_size;
      @this_term_size = Term::ReadKey::GetTerminalSize(\*STDOUT);
      if ( $windows ) {
         # TODO: what if it's 64bit?
         $this_term_size[0]--;
         $this_term_size[1]
            = min($this_term_size[1], $config{'max_height'}->{'val'});
      }
      die("Can't read terminal size") unless @this_term_size;

      # Handle whatever action the key indicates.
      do_key_action();

      # Re-read the mode, which fully determines the behavior for the rest of
      # the loop.
      $mode = $config{'mode'}->{'val'};

      # Fetch and display the required data.
      $want_status_info = $config{'show_statusbar'}->{'val'};
      foreach my $data_required ( @{ $modes{$mode}->{'data_required'} } ) {
         $data_subs_for{$data_required}->();
      }
      $clear_screen_sub->() unless $modes{$mode}->{'no_clear_screen'};
      $modes{$mode}->{'display_sub'}->();
      ReadMode('cbreak');
      $char = ReadKey(
         defined($modes{$mode}->{'interval'})
         ? $modes{$mode}->{'interval'}
         : $config{'interval'}->{'val'});
      ReadMode('normal');
   }
}

sub finish {
   disconnect_from_db();
   save_config();
   ReadMode('normal');
   print "\n";
   exit(0);
}

# Depending on whether a key was read, do something.  Keys have certain
# actions defined in lookup tables.  Each mode may have its own lookup
# table, which trumps the global table -- so keys can be
# context-sensitive.  The key may be read and written in a subroutine, so it's a
# global.
sub do_key_action {
   if ( defined $char ) {
      my $mode = $config{'mode'}->{'val'};
      my $action
         = defined($modes{$mode}->{'action_for'}->{$char})
         ? $modes{$mode}->{'action_for'}->{$char}->{'action'}
         : defined($action_for{$char})
         ? $action_for{$char}->{'action'}
         : sub{};
      $action->();
   }
}

sub get_time {
   my $current_time = $time_sub->();
   $last_time = $this_time || $current_time;
   $this_time = $current_time;
}

sub core_dump {
   my $msg = shift;
   if ($config{'debugfile'}->{'val'} && $config{'debug'}->{'val'}) {
      eval {
         open my $file, '>>', $config{'debugfile'}->{'val'};
         print $file "Error:\n$msg\n=== parsed status ===\n";
         print $file Dumper($innodb_status);
         if ( $full_processlist ) {
            print $file "Current processlist:\n"
               . Dumper($full_processlist);
         }
         if ( @sql_status ) {
            print $file "Current status:\n"
               . Dumper($sql_status[0]);
         }
         if ( @sql_variables ) {
            print $file "Current variables:\n"
               . Dumper($sql_variables[0]);
         }
         close $file;
      };
   }
   print $msg;
}

sub display_help {
   my $mode = $config{'mode'}->{'val'};
   my %keys = map { $_ => $action_for{$_}->{'label'} } keys %action_for;
   foreach my $key ( keys %{$modes{$mode}->{'action_for'}} ) {
      $keys{$key} = $modes{$mode}->{'action_for'}->{$key}->{'label'};
   }
   my @display_lines = (
      '',
      'Uppercase keys change modes, lowercase keys do other stuff.',
      '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.', '';
   $clear_screen_sub->();
   draw_screen(\@display_lines, { show_all => 1 } );
   pause();
}

sub pause {
   my $msg = shift;
   print defined($msg) ? $msg : "\nPress any key to continue";
   ReadMode('cbreak');
   my $char = ReadKey(0);
   ReadMode('normal');
   return $char;
}

sub dump_current_info {
   my $filename = prompt("\nDumping current info. Enter a filename");
   eval {
      open my $file, '>', $filename or die $OS_ERROR;
      print $file join("\n",
         'InnoDB monitor text',
         $innodb_status_text || '',
         'Parsed InnoDB monitor text',
         Dumper($innodb_status),
         'Full process list',
         Dumper($full_processlist));
      close $file or die $OS_ERROR;
   };
   if ( $EVAL_ERROR ) {
      print "\n$EVAL_ERROR";
   }
   else {
      print "\nSuccess.";
   }
   pause();
}

sub reverse_sort {
   $config{'sort_dir'}->{'val'} *= -1;
}

sub get_status_info {
   if ( $want_status_info ) {
      unshift @sql_status_times, $this_time;
      my $dbh = connect_to_db();
      my $stmt
         = ( $ver_major >= 5 ) && ( $ver_rev >= 2 )
         ? 'SHOW GLOBAL STATUS'
         : 'SHOW STATUS';
      my $res = $dbh->selectall_arrayref($stmt);
      unshift @sql_status, { map { $_->[0] => $_->[1] } @$res };
      $res = $dbh->selectall_arrayref('SHOW VARIABLES');
      unshift @sql_variables, { map { $_->[0] => $_->[1] } @$res };
   }
   while ( @sql_status > $config{'num_status_sets'}->{'val'} ) {
      pop @sql_status;
      pop @sql_status_times;
      pop @sql_variables;
   }
}

# Accepts a value, such as 'Key_reads', a numeric set, and a mode and whether
# to average the value over time.  The value is either in SHOW STATUS, SHOW
# VARIABLES, or one of the derived values from a formula.  The set is which to
# retrieve from, since innotop keeps an array of sets of the data.  0 is most
# recent.  $inc is 0 for the raw value, 1 for the difference between the
# value and its value in the previous set.  Set $avg for the difference
# divided by either uptime or the time difference between sets, depending on
# $mode.
sub get_union_s_v {
   my ( $key, $set, $inc, $avg, $opts ) = @_;

   my %opts = (
      vbtm => 0,
   );
   $opts ||= {};
   map { $opts{$_} = $opts->{$_} } keys %$opts;

   # The numbers that come from the derived calculations are too complex to do
   # one-size-fits-all arithmetic on, so here I do some special-case stuff.
   # This prevents the math below doing wacky things like taking the average
   # of the increment or so forth.
   if (exists($derived_val{$key}) ) {
      return shorten($derived_val{$key}->($set, $inc, $avg), \%opts);
   }

   # Find the current and previous values.
   my ( $now, $old, $interval );
   $now = exists($sql_status[$set]->{$key})    ? $sql_status[$set]->{$key}
        : exists($sql_variables[$set]->{$key}) ? $sql_variables[$set]->{$key}
        : undef;
   if ( $inc && $set + 1 < @sql_status ) {
      $old = exists($sql_status[$set + 1]->{$key})    ? $sql_status[$set + 1]->{$key}
           : exists($sql_variables[$set + 1]->{$key}) ? $sql_variables[$set + 1]->{$key}
           : undef;
   }

   if ( $inc && $set + 1 < @sql_status ) {
      $interval = $sql_status_times[$set] - $sql_status_times[$set + 1];
   }
   else {
      $interval = $sql_status[$set]->{'Uptime'};
   }
   $interval ||= 1;

   my $val = $now;
   if ( defined($now) ) {
      if ( 0 == $inc ) {
         if ( $avg && $now =~ m/^-?[0-9\.]+$/ ) {
            $val = $now / $interval;
         }
      }
      else { # $inc is 1, 'incremental'
         if ( defined($old) && $now =~ m/^-?[0-9\.]+$/ && $old =~ m/^-?[0-9\.]+$/ ) {
            $val = $now - $old;
         }
         if ( $avg && $val =~ m/^-?[0-9\.]+$/ ) {
            $val /= $interval;
         }
      }
   }
   else {
      return $now;
   }

   return $opts{'vbtm'}
      ? $val
      : shorten($val, \%opts);
}

sub get_full_processlist {
   my $dbh = connect_to_db();
   $full_processlist = $dbh->selectall_hashref('SHOW FULL PROCESSLIST', 'Id');
}

sub get_open_tables {
   my $dbh = connect_to_db();
   $open_tables = $dbh->selectall_arrayref('SHOW OPEN TABLES', { Slice => {} } );
}

sub kill_query {
   my $kill_what = shift;
   print "\n";
   my $thread = prompt(
      'Enter the thread to kill',
      make_regex_validator(qr/^\d+$/),
   );

   eval {
      my $dbh = connect_to_db();
      # if version > 5 and specified, kill query, not connection
      $dbh->do( $ver_major >= 5 ? "KILL $kill_what $thread" : "KILL $thread" );
   };

   if ( $EVAL_ERROR ) {
      print "\nThread $thread could not be killed.";
   }
   pause();
}

# Given a thread ID or transaction ID, find the query it was executing.
sub get_query_info_for_analysis {
   my $mode = $config{'mode'}->{'val'};

   # If we're in T mode, immediately fetch the qu information so we can
   # possibly figure out the full query better.  Possibility of race
   # conditions, but oh well.
   if ( $mode eq 'T' ) {
      get_full_processlist();
   }

   my %prompt_for = (
      Q => "\nEnter the ID of the query to examine",
      T => "\nEnter the ID or Thread of the transaction to examine",
   );
   my %validator_for = (
      T => make_regex_validator(qr/^\d+(?: \d+)?$/),
      Q => make_regex_validator(qr/^\d+$/),
   );
   my $query_id = prompt($prompt_for{$mode}, $validator_for{$mode});

   # Find the query text.
   my $query    = "";
   my $db       = "";
   if ( $mode eq 'T' ) {
      my $txn;
      my $txns = $innodb_status->{'sections'}->{'tx'}->{'transactions'};
      if ( $query_id =~ m/ / ) {
         # The user entered a transaction ID, not a MySQL thread ID.
         ( $txn ) = grep { $_->{'txn_id'} eq $query_id } @$txns;
      }
      else {
         ( $txn ) = grep { $_->{'mysql_thread_id'} eq $query_id } @$txns;
      }
      if ( $txn ) {
         $query    = $txn->{'query_text'};
         $query_id = $txn->{'mysql_thread_id'};
         if ( $full_processlist->{$query_id} ) {
            $db = $full_processlist->{$query_id}{'db'};
            # If possible, get the full(er) query from the processlist.
            my $processlist_query = $full_processlist->{$query_id}{'Info'};
            if ( $processlist_query && length($query) < length($processlist_query)
               && substr($processlist_query, 0, length($query)) eq $query)
            {
               $query = $processlist_query;
            }
         }
      }
   }
   elsif ( $full_processlist->{$query_id} ) {
      $query = $full_processlist->{$query_id}{'Info'};
      $db    = $full_processlist->{$query_id}{'db'};
   }

   $query_id ||= "";
   $query    ||= "";
   $db       ||= "";

   $query_to_analyze = {
      id   => $query_id,
      text => $query,
      db   => $db,
   };
}

sub show_full_query {
   my @display_lines;

   my $query_text = $query_to_analyze->{'text'};
   if ( $query_text ) {
      push @display_lines, $query_text;
   }
   else {
      push @display_lines, "Unable to find any info on that thread.";
   }

   $clear_screen_sub->();
   draw_screen(\@display_lines, { raw => 1 } );
   $char = pause('');
   do_key_action();
}

sub show_full_tx {
   my $txn_id = prompt(
      "\nEnter the ID or Thread of the transaction to examine",
      make_regex_validator(qr/^\d+(?: \d+)?$/),
   );

   my $txn;
   my $txns = $innodb_status->{'sections'}->{'tx'}->{'transactions'};
   if ( $txn_id =~ m/ / ) {
      # The user entered a transaction ID, not a MySQL thread ID.
      ( $txn ) = grep { $_->{'txn_id'} eq $txn_id } @$txns;
   }
   else {
      ( $txn ) = grep { $_->{'mysql_thread_id'} eq $txn_id } @$txns;
   }

   $clear_screen_sub->();
   if ( $txn ) {
      my $stuff = $txn->{'fulltext'} || Dumper($txn);
      print $stuff . "\n";
   }
   else {
      print "Unable to find any info on that transaction.\n";
   }

   pause();
}

sub start_A_mode {
   get_query_info_for_analysis();
   switch_mode('A');
   do_key_action();
}

sub display_explain {

   my ( $query, $db ) = @{$query_to_analyze}{qw(text db)};
   # Some replace/create/insert...select can be rewritten easily.
   my $mods = 0;
   $mods += $query =~ s/^\s*(?:replace|insert).*?select/select/is;
   $mods += $query =~ s/^
      \s*create\s+(?:temporary\s+)?table
      \s+(?:\S+\s+)as\s+select/select/xis;

   my @display_lines;

   if ( $query ) {

      my $part
         = ( $ver_major >= 5 && $ver_minor >= 1 && $ver_rev >= 5 )
         ? 'PARTITIONS'
         : '';
      $query = "EXPLAIN $part\n" . $query;

      eval {
         my $dbh = connect_to_db();
         if ( $db ) {
            $dbh->do("use $db");
         }
         my $sth = $dbh->prepare( $query );
         $sth->execute();

         my $res;
         while ( $res = $sth->fetchrow_hashref() ) {
            map { $res->{$_} ||= '' } ( 'partitions', keys %$res);
            my @this_table = create_caption("Sub-Part $res->{id}",
               create_table2(
                  $config{'explain_fmt'}->{'val'},
                  meta_to_hdr($config{'explain_fmt'}->{'meta'}),
                  $res));
            @display_lines = stack_next(\@display_lines, \@this_table);
         }
      };

      if ( $EVAL_ERROR ) {
         print $EVAL_ERROR . "\n" if $config{'debug'}->{'val'};
         push @display_lines, 'The query could not be explained.';
      }
   }
   else {
      push @display_lines, 'The query could not be explained.';
   }

   if ( $mods ) {
      push @display_lines, '[This query has been re-written to be explainable]';
   }

   unshift @display_lines, $query;

   $clear_screen_sub->();
   draw_screen(\@display_lines, { raw => 1 } );
   $char = pause('');
   do_key_action();
}

sub show_optimized_query {
   my ( $query, $db ) = @{$query_to_analyze}{qw(text db)};

   # Some replace/create/insert...select can be rewritten easily.
   my $mods = 0;
   my $orig = $query;
   $mods += $query =~ s/^\s*(?:replace|insert).*?select/select/is;
   $mods += $query =~ s/^
      \s*create\s+(?:temporary\s+)?table
      \s+(?:\S+\s+)as\s+select/select/xis;

   my @display_lines;

   if ( $mods ) {
      push @display_lines, '[This query has been re-written to be explainable]';
   }

   if ( $query ) {

      push @display_lines, $orig;

      eval {
         my $dbh = connect_to_db();
         if ( $db ) {
            $dbh->do("use $db");
         }
         $dbh->do( 'EXPLAIN EXTENDED ' . $query );
         my $res = $dbh->selectall_arrayref('SHOW WARNINGS');

         if ( $res ) {
            foreach my $result ( @$res ) {
               push @display_lines, 'Note:', $result->[2];
            }
         }
         else {
            push @display_lines, 'The query optimization could not be generated.';
         }
      };

      if ( $EVAL_ERROR ) {
         print $EVAL_ERROR . "\n" if $config{'debug'}->{'val'};
         push @display_lines, 'The query optimization could not be generated.';
      }
   }
   else {
      push @display_lines, 'The query optimization could not be generated.';
   }

   $clear_screen_sub->();
   draw_screen(\@display_lines, { raw => 1 } );
   $char = pause('');
   do_key_action();
}

sub display_O {
   my @display_lines = ('');

   if ( $open_tables ) {
      my @tables;

      # Apply filters
      my %filters =
         map { $_ => $O_cols{$_}->{'filter'} }
         grep { $O_cols{$_}->{'filter'} } keys %O_cols;
      if ( %filters ) {
         @tables = grep {
            my $table = $_;
            # Grepping for matches will return true if ANY match.  We want ALL
            # to match, so we grep for the absence of any that don't match.
            0 == grep { $table->{$_} !~ m/$filters{$_}/ } keys %filters;
         } @$open_tables;
      }
      else {
         @tables = @$open_tables;
      }

      # Sort the entries if desired
      my $sort_col = $config{'O_sort'}->{'val'};
      if ( $sort_col ) {
         my $sort_dir = $config{'sort_dir'}->{'val'};
         if ( $O_cols{$sort_col}->{'num'} ) {
            @tables = sort {
               $sort_dir * ( $a->{$sort_col} <=> $b->{$sort_col} )
            } @tables;
         }
         else {
            @tables = sort {
               $sort_dir * ( $a->{$sort_col} cmp $b->{$sort_col} )
            } @tables;
         }
      }

      my @table = create_table($config{'O_fmt'}->{'val'}, \%O_cols, \@tables);
      if ( @table ) {
         push @display_lines, @table;
      }
      else {
         push @display_lines, 'Nothing to display.';
      }
   }
   else {
      push @display_lines, 'No data on open tables.';
   }

   draw_screen(\@display_lines);
}

sub display_W {
   my @display_lines = ('');
   my $tx = $innodb_status->{'sections'}->{'tx'};
   my $sm = $innodb_status->{'sections'}->{'sm'};

   if ( $tx && @{$tx->{'transactions'}} ) {

      my @txns = @{$tx->{'transactions'}};
      my @lock_waits;
      foreach my $txn ( grep { $_->{'lock_wait_status'} } @txns ) {
         my %lock_wait = map { $_ => $txn->{$_} }
            qw(txn_id mysql_thread_id lock_wait_time );
         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);
         push @lock_waits, \%lock_wait;
      }

      # Sort by wait time descending
      @lock_waits
         = reverse sort { $a->{'lock_wait_time'} <=> $b->{'lock_wait_time'} }
         @lock_waits;

      my @table = create_table($config{'W_fmt'}->{'val'},
                  $config{'W_fmt'}->{'meta'}, \@lock_waits);
      if ( @table ) {
         push @display_lines, create_caption('Lock Waits', @table);
      }
      else {
         push @display_lines, 'No InnoDB lock waits.';
      }

   }
   else {
      push @display_lines, 'No lock wait data.  Try clearing deadlocks.';
   }

   if ( $sm ) {
      if ( $sm->{'wait_array_size'} ) {
         push @display_lines, '', create_caption(
            'Wait Array Information',
            create_table(
               $config{'wait_array_fmt'}->{'val'},
               $config{'wait_array_fmt'}->{'meta'},
               $sm->{'waits'} ));
      }
      else {
         push @display_lines, '', 'No reserved cells in the wait array.';
      }
   }
   else {
      push @display_lines, '', 'No semaphore data.  Try clearing deadlocks.';
   }

   draw_screen(\@display_lines);
}

sub set_V_set {
   $config{'V_set'}->{'val'} = shift;
}

sub choose_V_columns {
   get_config_interactive( 'V_fmt_' . $config{'V_set'}->{'val'} );
}

sub display_V {
   my @display_lines;

   my $var_set = $config{'V_set'}->{'val'};
   my @values  = @{ $config{"V_fmt_$var_set"}->{'val'} };

   if (@values) {

      my $num_sets = scalar(@sql_status);
      my $inc      = $config{'status_inc'}->{'val'};
      my $avg      = $config{'status_avg'}->{'val'};

      # Build a meta dataset that can be used for a type-1 table
      my %meta = ( name => { hdr => 'Name', just => '-' } );
      foreach my $set ( 0 .. $num_sets - 1 ) {
         $meta{"set_$set"} = { hdr => "Set $set", just => '' };
      }

      # Loop through them and do a 'pivot table' transformation on them.
      my @rows = map {
         my $row = { name => $_ };
         foreach my $set ( 0 .. $num_sets - 1 ) {
            my $val = get_union_s_v( $_, $set, $inc, $avg );
            $row->{"set_$set"} = defined $val ? $val : '';
         }
         $row;
      } @values;

      my @cols = 'name';
      foreach my $set ( 0 .. $num_sets - 1 ) {
         push @cols, "set_$set";
      }

      push @display_lines, "Options: incremental=$inc, per-sec avg=$avg";
      push @display_lines, create_table( \@cols, \%meta, \@rows);
   }
   else {
      @display_lines = "No variables chosen.  Use the 'c' key to choose.";
   }

   $clear_screen_sub->();

   draw_screen( \@display_lines );
}

sub create_grid {
   my @vals = @_;
   my @result;

   # Slice and stack, baby.
   my $i = 0;
   while ($i < @vals) {
      # Do 5 at a time
      my $max_index = min( scalar(@vals), $i + 5 );
      my @slice = @vals[$i..$max_index - 1];
      my $max_width = max( map{ length($_) } @slice );
      @slice  = map { sprintf("%-${max_width}s", $_) } @slice;
      @result = stack_next(\@result, \@slice);
      $i += 5;
   }
   return @result;
}

# Add or remove filters from a hashref
sub add_remove_filter {
   my $meta = shift;
   my $validator = make_meta_validator($meta);

   # Make a grid of columns, with the filter appended if there is any
   my @display_lines
      = create_caption("You can add or remove filters on these columns",
         create_grid(
            map { "$_ " . ($meta->{$_}->{'filter'} || '') } sort keys %$meta
      ));
   $clear_screen_sub->();
   draw_screen(\@display_lines);

   my $col = prompt("\nChoose a column", $validator);
   my $pat = prompt("Enter a regex '$col' must match (blank to unfilter)");
   if ( defined($meta->{$col}) ) {
      if ( defined($pat) && length($pat) ) {
         $meta->{$col}->{'filter'} = qr/$pat/;
      }
      else {
         delete($meta->{$col}->{'filter'});
      }
   }
}

sub display_Q {
   my @display_lines;
   if ( $full_processlist ) {

      # You have to handle possible NULL values at every step.  I'm fairly
      # certain the Id won't be NULL, but for safety... I'm paranoid at this
      # point.
      my @procs = map {
         $_->{'Host'} ||= '';
         my ( $host, $port ) = $_->{'Host'} =~ m/^([^:]*):?(.*)$/;
         my $info_or_state = $_->{'Info'} || $_->{'State'} || '';
         {  mysql_thread_id => $_->{'Id'} || 0,
            user            => $_->{'User'} || '',
            host            => $host || '',
            port            => $port || 0,
            host_and_port   => $_->{'Host'} || '',
            db              => $_->{'db'} || '',
            cmd             => $_->{'Command'} || '',
            secs            => $_->{'Time'} || 0,
            'time'          => secs_to_time($_->{'Time'} || 0),
            state           => $_->{'State'} || '',
            info            => no_ctrl_char($_->{'Info'}),
            info_or_state   => no_ctrl_char($info_or_state),
         }
      } values %$full_processlist;

      # Hide own process
      if ( $config{'hide_self'}->{'val'} ) {
         @procs = grep { $_->{'mysql_thread_id'} != $connection_id } @procs;
      }

      # Remove idle queries from the list
      if ( $config{'hide_inactive'}->{'val'} ) {
         @procs = grep { $_->{'cmd'} ne 'Sleep' && $_->{'cmd'} ne 'Binlog Dump' } @procs;
      }

      # Apply filters
      my %filters =
         map { $_ => $Q_cols{$_}->{'filter'} }
         grep { $Q_cols{$_}->{'filter'} } keys %Q_cols;
      if ( %filters ) {
         @procs = grep {
            my $proc = $_;
            # Grepping for matches will return true if ANY match.  We want ALL
            # to match, so we grep for the absence of any that don't match.
            0 == grep { $proc->{$_} !~ m/$filters{$_}/ } keys %filters;
         } @procs;
      }

      # Sort the entries
      my $sort_col = $config{'Q_sort'}->{'val'} || 'mysql_thread_id';
      my $sort_dir = $config{'sort_dir'}->{'val'};
      if ( $Q_cols{$sort_col}->{'num'} ) {
         @procs = sort {
            $sort_dir * ( $a->{$sort_col} <=> $b->{$sort_col} )
         } @procs;
      }
      else {
         @procs = sort {
            $sort_dir * ( $a->{$sort_col} cmp $b->{$sort_col} )
         } @procs;
      }

      # Create header
      push @display_lines, create_Q_header();

      my @table = create_table($config{'Q_fmt'}->{'val'}, \%Q_cols, \@procs);
      if ( @table ) {
         push @display_lines, @table;
      }
      else {
         push @display_lines, 'Nothing to display.';
      }
   }
   else {
      push @display_lines, 'No process list information found.';
   }
   draw_screen(\@display_lines);
}

sub set_display_precision {
   my $dir = shift;
   $config{'num_digits'}->{'val'}
      = min(9, max(0, $config{'num_digits'}->{'val'} + $dir));
}

sub toggle_value {
   my $key = shift;
   $config{$key}->{'val'} ^= 1;
}

sub create_Q_header {
   my @result;

   my $opts = { num_digits => 2, pad => ' ', force => 1 };

   if ( $config{'show_QT_header'}->{'val'} ) {

      my $rows = [{
         when           => 'Total',
         questions      => get_union_s_v('Questions', 0, 0, 0, $opts),
         qps            => get_union_s_v('Questions', 0, 0, 1, $opts),
         slow           => get_union_s_v('Slow_queries', 0, 0, 0, $opts),
         cache_hit      => sprintf("%.2f%%",
                           get_union_s_v('QueryCacheHitRate', 0, 0, 0) * 100),
         key_buffer_hit => sprintf("%.2f%%",
                           get_union_s_v('KeyCacheHitRate', 0, 0, 0) * 100),
         bps_in         => get_union_s_v('Bytes_received', 0, 0, 1, $opts),
         bps_out        => get_union_s_v('Bytes_sent', 0, 0, 2, $opts),
      },
      {
         when           => 'Now',
         questions      => get_union_s_v('Questions', 0, 1, 0, $opts),
         qps            => get_union_s_v('Questions', 0, 1, 1, $opts),
         slow           => get_union_s_v('Slow_queries', 0, 1, 0, $opts),
         cache_hit      => sprintf("%.2f%%",
                           get_union_s_v('QueryCacheHitRate', 0, 1, 0) * 100),
         key_buffer_hit => sprintf("%.2f%%",
                           get_union_s_v('KeyCacheHitRate', 0, 1, 0) * 100),
         bps_in         => get_union_s_v('Bytes_received', 0, 1, 1, $opts),
         bps_out        => get_union_s_v('Bytes_sent', 0, 1, 2, $opts),
      }];

      # Format the data
      my $meta = $config{'q_header_fmt'}->{'meta'};
      foreach my $row ( @$rows ) {
         foreach my $col ( keys %$row) {
            $row->{$col} ||= 0;
         }
      }

      @result = create_table($config{'q_header_fmt'}->{'val'}, $meta, $rows);

      # Add some space above and below the header.
      if ( @result ) {
         push @result, '';
         unshift @result, '';
      }

   }
   return @result;
}

sub shorten {
   my ( $num, $opts ) = @_;

   return $num if $num =~ m/[^\d\.-]/;

   $opts ||= {};
   my $pad = defined $opts->{'pad'} ? $opts->{'pad'} : '';
   my $num_digits = defined $opts->{'num_digits'}
      ? $opts->{'num_digits'}
      : $config{'num_digits'}->{'val'};
   my $force = defined $opts->{'force'};

   if ( $config{'long_numbers'}->{'val'} ) {
      # From perlfaq5: add commas.
      $num =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
      # Trim to desired precision.
      $num =~ s/(^[-+]?[\d,]+\.\d{$num_digits})\d+/$1/;
      return $num;
   }
   else {
      my $n = 0;
      while ( $num > 1_024 ) {
         $num /= 1_024;
         ++$n;
      }
      return sprintf(
         $num =~ m/\./ || $n || $force
            ? "%.${num_digits}f%s"
            : '%d',
         $num, ($pad,'k','M','G', 'T')[$n]);
   }
}

sub display_D {
   my $dl = $innodb_status->{'sections'}->{'dl'};

   if ( $dl ) {

      my @display_lines;
      if ( !$innodb_status->{'got_all'} ) {
         push @display_lines,
            '',
            'The InnoDB monitor text seems to be truncated.  The',
            'information below is probably incomplete.';
      }

      $dl->{'rolled_back'} ||= '?';

      push @display_lines,
           '',
           "The last deadlock was at $dl->{timestring}.  "
            . "Transaction $dl->{rolled_back} was the victim.";

      my @txns;
      my @txn_query;

      # Table-ify the transactions, with the locks they were waiting for.
      foreach my $txn_id ( sort { int($a) <=> int($b) } keys %{$dl->{'txns'}} ) {
         my $txn = $dl->{'txns'}->{$txn_id};

         # Pull the stuff out of the transaction
         my %row = map {
            $_ => $txn->{'tx'}->{$_}
         } keys %{$txn->{'tx'}};

         push @txn_query, $txn->{'tx'}->{'query_text'};
         $row{'num'} = $txn_id;
         push @txns, \%row;
      }

      push @display_lines,
         '',
         create_table(
         $config{'dl_txn_fmt'}->{'val'},
         $config{'dl_txn_fmt'}->{'meta'},
         \@txns),
         '';

      # Each transaction may have info about the locks it holds and waits for.
      my @lock_rows;
      foreach my $txn_id ( sort { int($a) <=> int($b) } keys %{$dl->{'txns'}} ) {
         my $txn = $dl->{'txns'}->{$txn_id};

         foreach my $what (qw(waits_for holds)) {
            my $locks = $txn->{$what};
            if ( $locks ) {
               $locks->{'num'} = $txn_id;
               $locks->{'what'} = $what;
               # If there's only one record, get its heap number
               if ( scalar @{$locks->{'locks'}} == 1 ) {
                  $locks->{'heap_no'} = $locks->{'locks'}->[0]->{'heap_no'};
               }
               else {
                  $locks->{'heap_no'} = '+';
               }
               push @lock_rows, $locks;
            }
         }
      }
      if ( @lock_rows ) {
         push @display_lines,
            create_caption('Locks Held and Waited For', create_table(
            $config{'dl_lock_fmt'}->{'val'},
            $config{'dl_lock_fmt'}->{'meta'},
            \@lock_rows)),
            '';
      }

      draw_screen(\@display_lines);

      # Print out each one's query
      foreach my $i ( 1..@txn_query ) {
         print "\nTransaction $i was executing this query:\n" . $txn_query[$i - 1];
      }
   }
   else {
      print "\nNo deadlock data to display.\n";
   }

}

sub create_deadlock {
   if ( !$config{'dl_table'}->{'val'} ) {
      get_config_interactive('dl_table');
   }
   my $tbl = $config{'dl_table'}->{'val'};

   $clear_screen_sub->();

   print "This function will deliberately cause a deadlock in $tbl, "
      . "clearing deadlock information from the InnoDB monitor.\n\n";

   my $answer = prompt("Are you sure you want to proceed?  Say 'y' if you do");
   return 0 unless $answer eq 'y';

   eval {
      # Set up the table for creating a deadlock.
      my $dbh = get_new_db_connection();
      $dbh->do("create table $tbl(a int) engine=innodb");
      $dbh->do("delete from $tbl");
      $dbh->do("insert into $tbl(a) values(0), (1)");
      $dbh->do("commit"); # Or the children will block against the parent

      # Fork off two children to deadlock against each other.
      print "Forking children to deadlock against each other\n";
      my %children;
      foreach my $child ( 0..1 ) {
         my $pid = fork();
         if ( defined($pid) && $pid == 0 ) { # I am a child
            deadlock_thread( $child, $tbl );
         }
         elsif ( !defined($pid) ) {
            die("Unable to fork!\n");
         }
         # I already exited if I'm a child, so I'm the parent.
         $children{$child} = $pid;
      }

      # Wait for the children to exit.
      print "Waiting for children to exit\n";
      foreach my $child ( keys %children ) {
         my $pid = waitpid($children{$child}, 0);
         print "Child process $child ($children{$child}) exited: $pid\n";
      }

      # Clean up.  The child process had an error on the DB handle, so the DB
      # connection has gone away.  Re-connect.
      disconnect_from_db();
      $dbh = connect_to_db();
      $dbh->do("drop table $tbl");
   };
   if ( $EVAL_ERROR ) {
      print $EVAL_ERROR;
   }
   pause();
}

sub deadlock_thread {
   my ( $id, $tbl ) = @_;
   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();
      foreach my $stmt (@stmts[0..2]) {
         print_thread( $id, $stmt );
         $dbh->do($stmt);
      }
      print_thread( $id, "sleeping" );
      sleep(1 + $id);
      print_thread( $id, $stmts[-1] );
      $dbh->do($stmts[-1]);
   };
   if ( $EVAL_ERROR ) {
      if ( $EVAL_ERROR =~ m/Deadlock found/ ) {
         print_thread( $id, "deadlocked successfully" );
      }
      else {
         print_thread( $id, $EVAL_ERROR );
      }
   }
   exit(0);
}

sub print_thread {
   my ( $thread, $msg ) = @_;
   print " --> Child $thread: $msg\n";
}

# Draw the screen for the 'I/O' display mode.
sub display_I {
   my @display_lines;
   my $io = $innodb_status->{'sections'}->{'io'};
   my $lg = $innodb_status->{'sections'}->{'lg'};

   my $prefs = { pad => '  ', vsep => 1 };

   if ( $io ) {
      # Format the threads section
      my @io_thread_data
         = sort { $a->{'thread'} <=> $b->{'thread'} }
            values %{$io->{'threads'}};

      push @display_lines, create_caption('I/O Threads',
         create_table(
            $config{'io_thread_fmt'}->{'val'},
            $config{'io_thread_fmt'}->{'meta'},
            \@io_thread_data,
            { nocolor => 1} ));

      # Format the pending I/O stuff.
      my @io_pending = create_caption('Pending I/O',
         create_table2(
            $config{'io_pending_fmt'}->{'val'},
            meta_to_hdr($config{'io_pending_fmt'}->{'meta'}),
            $io,
            { just => '-', sep => ' ', just1 => '' }));
      @display_lines = stack_next(\@display_lines, \@io_pending, $prefs );

      # Format the misc stuff.
      my @file_io_misc = create_caption('File I/O Misc',
         create_table2(
            $config{'file_io_misc_fmt'}->{'val'},
            meta_to_hdr($config{'file_io_misc_fmt'}->{'meta'}),
            $io,
            { just => '-', sep => ' ', just1 => '' }));
      @display_lines = stack_next(\@display_lines, \@file_io_misc, $prefs );

   }
   else {
      push @display_lines, 'No I/O data.  Try clearing deadlocks.';
   }

   if ( $lg ) {
      my @log_stats = create_caption('Log Statistics',
         create_table2(
            $config{'log_stats_fmt'}->{'val'},
            meta_to_hdr($config{'log_stats_fmt'}->{'meta'}),
            $lg,
            { just => '-', sep => ' ', just1 => '' }));
      @display_lines = stack_next(\@display_lines, \@log_stats, $prefs );
   }
   else {
      push @display_lines, 'No log data.  Try clearing deadlocks.';
   }

   draw_screen(\@display_lines);
}

sub start_S_mode {
   $clear_screen_sub->();
   switch_mode('S');
}

sub start_G_mode {
   $clear_screen_sub->();
   switch_mode('G');
}

sub display_S {
   my $S_fmt = $config{'S_fmt'}->{'val'};
   my $min_width = 9;

   my $inc = $config{'status_inc'}->{'val'};
   my $avg  = $config{'status_avg'}->{'val'};

   # Clear the screen if the display width changed.
   if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
      $line_counter = 0;
      $clear_screen_sub->();
   }

   # Design a column format for the values.
   my $format = join(' ',
      map { '%' . max($min_width, length($_)) . 's' } @$S_fmt
      ) . "\n";

   # Print headers every now and then.
   if ( $line_counter++ % int( $this_term_size[1] - 5 ) == 0 ) {
      print "Options: incremental=$inc, per-sec avg=$avg\n";
      print join(' ',
         map { sprintf( '%-' . max($min_width, length($_)) . 's', $_) } @$S_fmt
         ) . "\n";
   }

   # Print the values.
   printf($format, map { get_union_s_v($_, 0, $inc, $avg) || 0 } @$S_fmt );
}

sub display_G {
   my $G_fmt = $config{'G_fmt'}->{'val'};
   my $inc = $config{'status_inc'}->{'val'};
   my $avg = $config{'status_avg'}->{'val'};
   my $mvs = $config{'max_per_sec_ever_seen'}->{'val'};

   if ( $inc && @sql_status < 2 ) {
      return;
   }

   # Design a column format for the values.
   my $num_cols = scalar(@$G_fmt);
   my $width
      = int(($this_term_size[0] - $num_cols + 1) / $num_cols);
   my $format = ( "%-${width}s " x $num_cols );
   $format =~ s/ $/\n/;

   # Clear the screen if the display width changed.
   if ( @last_term_size && $this_term_size[0] != $last_term_size[0] ) {
      $line_counter = 0;
      $clear_screen_sub->();
   }

   # Print headers every now and then.
   if ( $line_counter++ % int( $this_term_size[1] - 5 ) == 0 ) {
      printf($format,
         map {
            my $wid = $width - length($_) - 1;
            "$_ " . shorten($mvs->{$_})
         } @$G_fmt
      );
   }

   # Get the values.
   my %diffs = map {
      $_ => get_union_s_v($_, 0, $inc, $avg, { vbtm => 1 } ) || 0
   } @$G_fmt;

   # Update max ever seen.
   map { $mvs->{$_}
      = max($mvs->{$_} || 0, $diffs{$_})
   } keys %diffs;

   # Scale the values against the max ever seen.
   map {
      $diffs{$_} /= $mvs->{$_};
   } keys %diffs;

   # Print the values.
   printf($format,
      map { ( '*' x int( $width * $diffs{$_} )) || '.' } @$G_fmt );
}

# Draws the screen for the 'Buffer' display mode.
sub display_B {
   my @display_lines;
   my $bp = $innodb_status->{'sections'}->{'bp'};
   my $ib = $innodb_status->{'sections'}->{'ib'};

   if ( $bp ) {
      # Format the misc stuff.
      my @bp_misc = create_table2(
         $config{'bp_fmt'}->{'val'},
         meta_to_hdr($config{'bp_fmt'}->{'meta'}),
         $bp,
         { just => '-', sep => ' ', just1 => '' });

      # Format the Created/Read/Written section.
      my $crw_data = [
         {  h => 'Total Pages',
            c => $bp->{'pages_created'},
            r => $bp->{'pages_read'},
            w => $bp->{'pages_written'},
         },
         {  h => 'Per-Sec Avg',
            c => $bp->{'page_creates_sec'},
            r => $bp->{'page_reads_sec'},
            w => $bp->{'page_writes_sec'},
         },
      ];
      my @bp_pages = create_table(
         $config{'crw_fmt'}->{'val'},
         $config{'crw_fmt'}->{'meta'},
         $crw_data,
         { nocolor => 1 });

      push @display_lines,
         create_caption('Buffer Pool and Memory',
            stack_next(\@bp_misc, \@bp_pages, { pad => ' | '}));
   }
   else {
      push @display_lines, 'No buffer pool data.  Try clearing deadlocks.';
   }

   if ( $ib ) {

      # Format the tablespace section.
      my @ib_ts_table;
      my $meta = $config{'ib_ts_fmt'}->{'meta'};

      @ib_ts_table = create_caption(
         'Insert Buffer',
         create_table2(
         $config{'ib_ts_fmt'}->{'val'},
         meta_to_hdr($meta),
         $ib,
         { just => '-', sep => ' ', just1 => '' }));

      # Format the hash index stuff.
      my @hash_index = create_caption( 'Adaptive Hash Index',
         create_table2(
            $config{'ahi_fmt'}->{'val'},
            meta_to_hdr($config{'ahi_fmt'}->{'meta'}),
            $ib,
            { just => '-', sep => ' ', just1 => '' }));

      push @display_lines, '',
         create_caption('Insert Buffer and Adaptive Hash Index',
            stack_next(\@ib_ts_table, \@hash_index));
   }
   else {
      push @display_lines,
        'No insert buffer/adaptive hash index data.  Try clearing deadlocks.';
   }

   draw_screen(\@display_lines);
}

# Draw the screen for the 'row operation' display mode.  Draws:
# Row operations
# Semaphores
sub display_R {
   my @display_lines;
   my $ro = $innodb_status->{'sections'}->{'ro'};
   my $sm = $innodb_status->{'sections'}->{'sm'};

   if ( $ro ) {
      # Format the Ins/Upd/Read/Del section
      my $iusd_data = [
         {  h => 'Total Rows',
            i => $ro->{'num_rows_ins'},
            u => $ro->{'num_rows_upd'},
            r => $ro->{'num_rows_read'},
            d => $ro->{'num_rows_del'},
         },
         {  h => 'Per-Sec Avg',
            i => $ro->{'ins_sec'},
            u => $ro->{'upd_sec'},
            r => $ro->{'read_sec'},
            d => $ro->{'del_sec'},
         },
      ];
      push @display_lines,
         create_caption(
            'InnoDB Row Operations',
            create_table(
               $config{'iusd_fmt'}->{'val'},
               $config{'iusd_fmt'}->{'meta'},
               $iusd_data, { nocolor => 1}));

      # Format the main thread info and misc stuff.
      my @ro_misc = create_caption(
         'Row Operation Misc',
         create_table2(
            $config{'R_fmt'}->{'val'},
            meta_to_hdr($config{'R_fmt'}->{'meta'}),
            $ro,
            { just => '-', sep => ' ', just1 => '' }));
      push @display_lines, '', @ro_misc;

   }
   else {
      push @display_lines, 'No row operation data.  Try clearing deadlocks.';
   }

   # Format the semaphore data
   if ($sm) {
      my @sem = create_caption(
         'InnoDB Semaphores',
         create_table2(
            $config{'sm_fmt'}->{'val'},
            meta_to_hdr($config{'sm_fmt'}->{'meta'}),
            $sm,
            { just => '-', sep => ' ', just1 => '' }));
      @display_lines = stack_next(\@display_lines, \@sem);

      if ( $sm->{'wait_array_size'} ) {
         push @display_lines, '', create_caption(
            'Wait Array Information',
            create_table(
               $config{'wait_array_fmt'}->{'val'},
               $config{'wait_array_fmt'}->{'meta'},
               $sm->{'waits'}));
      }
      else {
         push @display_lines, '', 'No reserved cells in the wait array.';
      }

   }
   else {
      push @display_lines, 'No semaphore data.  Try clearing deadlocks.';
   }

   draw_screen(\@display_lines);
}

# Makes a two-column table, labels on left, data on right.
# Takes refs of @cols, %labels and %data, %user_prefs
sub create_table2 {
   my ( $cols, $labels, $data, $user_prefs ) = @_;
   my @rows;

   if ( @$cols && %$data ) {

      # Override defaults
      my $p = {
         just  => '',
         sep   => ':',
         just1 => '-',
      };
      if ( $user_prefs ) {
         map { $p->{$_} = $user_prefs->{$_} } keys %$user_prefs;
      }

      # Fix undef values
      map { $data->{$_} = '' unless defined $data->{$_} } @$cols;

      # Format the table
      my $max_l = max(map{ length($labels->{$_}) } @$cols);
      my $max_v = max(map{ length($data->{$_}) } @$cols);
      my $format    = "%$p->{just}${max_l}s$p->{sep} %$p->{just1}${max_v}s";
      foreach my $col ( @$cols ) {
         push @rows, sprintf($format, $labels->{$col}, $data->{$col});
      }
   }
   return @rows;
}

# Stacks one display section next to the other.  Accepts left-hand arrayref,
# right-hand arrayref, and options hashref.  Tries to stack as high as
# possible, so
# aaaaaa
# bbb
# can stack ccc next to the bbb.
# NOTE: this DOES modify its arguments, even though it returns a new array.
sub stack_next {
   my ( $left, $right, $user_prefs ) = @_;
   my @result;

   my $p = {
      pad   => ' ',
      vsep  => 0,
   };
   if ( $user_prefs ) {
      map { $p->{$_} = $user_prefs->{$_} } keys %$user_prefs;
   }

   # Find out how wide the LHS can be and still let the RHS fit next to it.
   my $pad   = $p->{'pad'};
   my $max_r = max( map { length($_) } @$right) || 0;
   my $max_l = $this_term_size[0] - $max_r - length($pad);

   # Find the minimum row on the LHS that the RHS will fit next to.
   my $i = scalar(@$left) - 1;
   while ( $i >= 0 && length($left->[$i]) <= $max_l ) {
      $i--;
   }
   $i++;
   my $offset = $i;

   if ( $i < scalar(@$left) ) {
      # Find the max width of the section of the LHS against which the RHS
      # will sit.
      my $max_i_in_common = min($i + scalar(@$right) - 1, scalar(@$left) - 1);
      my $max_width = max( map { length($_) } @{$left}[$i..$max_i_in_common]);

      # Append the RHS onto the LHS until one runs out.
      while ( $i < @$left && $i - $offset < @$right ) {
         my $format = "%-${max_width}s$pad%${max_r}s";
         $left->[$i] = sprintf($format, $left->[$i], $right->[$i - $offset]);
         $i++;
      }
      while ( $i - $offset < @$right ) {
         # There is more RHS to push on the end of the array
         push @$left,
            sprintf("%${max_width}s$pad%${max_r}s", ' ', $right->[$i - $offset]);
         $i++;
      }
      push @result, @$left;
   }
   else {
      # There is no room to put them side by side.  Add them below, with
      # a blank line above them if specified.
      push @result, @$left;
      push @result, (' ' x $this_term_size[0]) if $p->{'vsep'} && @$left;
      push @result, @$right;
   }
   return @result;
}

# Titles a section of text.
sub create_caption {
   my ( $caption, @rows ) = @_;
   if ( @rows ) {

      # Calculate the width of what will be displayed, so it can be centered
      # in that space.  When the thing is wider than the display, center the
      # caption in the display.
      my $width = min($this_term_size[0], max(map { length($_) } @rows));

      my $cap_len = length($caption);

      # It may be narrow enough to pad the sides with underscores and save a
      # line on the screen.
      if ( $cap_len <= $width - 6 ) {
         my $left = int(($width - 2 - $cap_len) / 2);
         unshift @rows,
            ("_" x $left) . " $caption " . ("_" x ($width - $left - $cap_len - 2));
      }

      # OK, it's not, so we have to add a line underneath to separate the
      # caption from whatever it's captioning.
      elsif ( $cap_len < $width ) {
         my $left = int(($width - $cap_len) / 2);
         unshift @rows, ('-' x $width);
         unshift @rows,
            (" " x $left) . $caption . (" " x ($width - $left - $cap_len));
      }

      # The caption is wider than the thing it labels, so we have to pad the
      # thing it labels to a consistent width.
      else {
         @rows = map {
            sprintf('%-' . $cap_len . 's', $_);
         } @rows;
         unshift @rows, ('-' x $cap_len);
         unshift @rows, $caption;
      }
   }
   return @rows;
}

# Input: an arrayref of columns, hashref of col info, and an arrayref of hashes
# Example: [ 'a', 'b' ]
#          { a => spec, b => spec }
#          [ { a => 1, b => 2}, { a => 3, b => 4 } ]
# The 'spec' is a hashref of hdr => label, just => ('-' or '')
# Output: an array of strings, one per row.
# Example:
# Column One Column Two
# ---------- ----------
# 1          2
# 3          4
sub create_table {
   my ( $cols, $info, $data, $prefs ) = @_;
   $prefs ||= {};

   my @rows = ();

   if ( @$cols && %$info && @$data ) {

      # Fix undef values
      foreach my $row ( @$data ) {
         map { $row->{$_} = '' unless defined $row->{$_} } @$cols;
      }

      # Find each column's max width.
      my %width_for = map {
         my $col_name = $_;
         $col_name => max(
            length($info->{$_}->{'hdr'}),
            map { length($_->{$col_name}) } @$data
         )
      } @$cols;

      # The table header.
      push @rows, join( " ",
         map {
            sprintf( "%-$width_for{$_}s", $info->{$_}->{'hdr'} )
         } @$cols );
      if ( $have_color && !$prefs->{'nocolor'} ) {
         push @rows, [ pop @rows, 'bold' ];
      }
      else {
         push @rows, join( " ", map { "-" x $width_for{$_} } @$cols );
      }

      # The table data.
      my $format = join( ' ',
         map { "%$info->{$_}->{just}$width_for{$_}s" } @$cols );
      foreach my $item ( @$data ) {
         push @rows, sprintf($format, map { no_nl($item->{$_}) } @$cols );
      }
   }

   return @rows;
}

sub no_nl {
   my $text = shift;
   $text =~ s/\s+/ /g;
   return $text;
}

# Strips out non-printable characters within fields, which freak terminals out.
sub no_ctrl_char {
   my ( $text ) = @_;
   return '' unless defined $text;
   if ( $config{unicode}->{val} ) {
      $text =~ s/
         ("(?:(?!(?<!\\)").)*"  # Double-quoted string
         |'(?:(?!(?<!\\)').)*') # Or single-quoted string
         /$1 =~ m#\p{IsC}# ? "[BINARY]" : $1/egx;
   }
   else {
      $text =~ s/
         ("(?:(?!(?<!\\)").)*"
         |'(?:(?!(?<!\\)').)*')
         /$1 =~ m#[^\040-\176]# ? "[BINARY]" : $1/egx;
   }
   return $text;
}

sub display_F {
   my @display_lines;
   my $fk = $innodb_status->{'sections'}->{'fk'};

   if ( $fk ) {

      push @display_lines, 'Reason: ' . $fk->{'reason'};

      # Display FK errors caused by invalid DML.
      if ( exists $fk->{'txn'} ) {
         my $txn = $fk->{'txn'};
         push @display_lines,
            '',
            "User $txn->{user} from $txn->{hostname}, thread $txn->{mysql_thread_id} was executing:",
            '', $txn->{'query_text'};
      }

      my @fk_table = create_table2(
         [ grep { exists($fk->{$_}) } @{$config{'fk_fmt'}->{'val'}} ],
         meta_to_hdr($config{'fk_fmt'}->{'meta'}),
         $fk,
         { just => '-', sep => '  '});
      push @display_lines, '', @fk_table;

   }
   else {
      push @display_lines, 'No foreign key error data.';
   }
   draw_screen(\@display_lines, { raw => 1 } );
}

sub display_T {
   my @display_lines;

   my $section = $innodb_status->{'sections'}->{'tx'};

   if ( $section && @{$section->{'transactions'}} ) {
      my @txns = @{$section->{'transactions'}};

      if ( $config{'show_QT_header'}->{'val'} ) {
         ( my $t = $section->{'trx_id_counter'} ) =~ s/\d* //;
         ( my $p = $section->{'purge_done_for'} ) =~ s/\d* //;
         my $maxtx = max( map { $_->{'active_secs'} } @txns );
         push @display_lines, '', join(", ",
            "History: $section->{history_list_len}",
            "Versions: " . ( $t - $p ),
            "Undo: $section->{purge_undo_for}",
            "Max time: " . secs_to_time($maxtx),
            "Lock structs: $section->{num_lock_structs}",
         );
      }

      push @display_lines, '';

      # Remove own query from the list
      if ( $config{'hide_self'}->{'val'} ) {
         @txns = grep { ( $_->{'mysql_thread_id'} || 0 ) != $connection_id } @txns;
      }

      # Remove idle queries from the list
      if ( $config{'hide_inactive'}->{'val'} ) {
         @txns = grep { ( $_->{'txn_status'} || '' ) ne 'not started' } @txns;
      }

      # Add formatted time to each element of the list.
      # Prevent binary crap from freaking out terminals.
      map {
         $_->{'time'} = secs_to_time($_->{'active_secs'});
         $_->{query_text} = no_ctrl_char($_->{query_text});
      } @txns;

      # Apply filters to the list
      my %filters =
         map { $_ => $T_cols{$_}->{'filter'} }
         grep { $T_cols{$_}->{'filter'} } keys %T_cols;
      if ( %filters ) {
         @txns = grep {
            my $txn = $_;
            # Grepping for matches will return true if ANY match.  We want ALL
            # to match, so we grep for the absence of any that don't match.
            0 == grep {( $txn->{$_} || '') !~ m/$filters{$_}/ } keys %filters;
         } @txns;
      }

      # Sort the entries
      my $sort_col = $config{'T_sort'}->{'val'} || 'txn_id';
      my $sort_dir = $config{'sort_dir'}->{'val'};
      if ( $T_cols{$sort_col}->{'num'} ) {
         @txns = sort {
            $sort_dir * ( $a->{$sort_col} <=> $b->{$sort_col} )
         } @txns;
      }
      else {
         @txns = sort {
            $sort_dir * ( $a->{$sort_col} cmp $b->{$sort_col} )
         } @txns;
      }

      my @table = create_table($config{'T_fmt'}->{'val'}, \%T_cols, \@txns);
      if ( @table ) {
         push @display_lines, @table;
      }
      else {
         push @display_lines, 'Nothing to display.';
      }

   }
   else {
      push @display_lines, 'No transaction data.  Try clearing deadlocks.';
   }
   draw_screen(\@display_lines);
}

# Prints lines to the screen.  The first argument is an arrayref.  Each
# element of the array is either a string or an arrayref.  If it's a string it
# just gets printed.  If it's an arrayref, the first element is the string to
# print, and the second is args to colored().
sub draw_screen {
   my ( $display_lines, $prefs ) = @_;
   if ( $config{'show_statusbar'}->{'val'} && !$prefs->{'nostatus'} ) {
      unshift @$display_lines, create_statusbar();
   }
   if ( $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);
   }
   elsif ( $prefs->{'raw'} ) {
      print join("\n",
         map {
            ref $_
               ? colored($_->[0], $_->[1])
               : $_;
         } @$display_lines);
   }
   else {
      my $max_lines = min(scalar(@$display_lines), $this_term_size[1]);
      print join("\n",
         map {
            ref $_
               ? colored(substr($_->[0], 0, $this_term_size[0]), $_->[1])
               : substr($_, 0, $this_term_size[0]);
         } @$display_lines[0..$max_lines - 1]);
   }
}

sub secs_to_time {
   my ( $secs, $fmt ) = @_;
   $secs ||= 0;

   # Decide what format to use, if not given
   $fmt ||= $secs >= 86_400 ? 'd'
          : $secs >= 3_600  ? 'h'
          :                   'm';

   return
      $fmt eq 'd' ? sprintf(
         "%d+%02d:%02d:%02d",
         int($secs / 86_400),
         int(($secs % 86_400) / 3_600),
         int(($secs % 3_600) / 60),
         $secs % 60)
      : $fmt eq 'h' ? sprintf(
         "%02d:%02d:%02d",
         int(($secs % 86_400) / 3_600),
         int(($secs % 3_600) / 60),
         $secs % 60)
      : sprintf(
         "%02d:%02d",
         int(($secs % 3_600) / 60),
         $secs % 60);
}

sub create_statusbar {
   my $mode = $config{'mode'}->{'val'};

   # Format server uptime human-readably.
   my $time   = $sql_status[0]->{'Uptime'};
   my $uptime = secs_to_time( $time );
   
   my $qps = get_union_s_v('Questions', 0, 1, 1, { num_digits => 2 });

   my $innodb_info = $config{'innodb_status_from_dir'}->{'val'}
      ? filename($innodb_files[$innodb_file_counter]) . ' '
      : '';

   if ( grep { $_ eq 'innodb' } @{ $modes{$mode}->{'data_required'} } ) {
      $innodb_info .= "InnoDB $innodb_status->{last_secs} sec ";
      if ( $innodb_status->{'got_all'} ) {
         if ( ($mode eq 'T' || $mode eq 'W')
               && $innodb_status->{'sections'}->{'tx'}->{'is_truncated'} ) {
            $innodb_info .= ':^|, ';
         }
         else {
            $innodb_info .= ':-), ';
         }
      }
      else {
         $innodb_info .= ':-(, ';
      }
   }

   my $mode_width = length($modes{$mode}->{'hdr'});
   my $remaining_width = $this_term_size[0] - $mode_width - 1;
   my $result = sprintf(
      "%-${mode_width}s %${remaining_width}s",
      $modes{$mode}->{'hdr'},
      $innodb_info . join(', ',
         "$qps QPS",
         $config{'host'}->{'val'},
         "$sql_status[0]->{Threads_connected} thd",
         $uptime,
         $mysql_version,
      ));
   return [ $result, 'bold reverse' ];
}

sub get_innodb_status {
   # Get the status text.
   my $parser = InnoDBParser->new;
   if ( $config{'innodb_status_from_dir'}->{'val'} ) {
      # Read the next file in the array...
      $innodb_file_counter = ( $innodb_file_counter + 1 ) % @innodb_files;
      my $file_contents = get_file($innodb_files[$innodb_file_counter]);
      $innodb_status = $parser->parse_status_text(
         $file_contents,
         $config{'debug'}->{'val'},
         $modes{$config{'mode'}->{'val'}}->{'innodb_required'});
      $innodb_status_text = $file_contents;
   }
   else {
      my $stmt
         = $ver_major >= 5
         ? 'SHOW ENGINE INNODB STATUS'
         : 'SHOW INNODB STATUS';
      my $dbh = connect_to_db();
      my $res = (@{$dbh->selectall_arrayref($stmt, { Slice => {} })})[0];

      $innodb_status = $parser->parse_status_text(
         $res->{'Status'},
         $config{'debug'}->{'val'},
         $modes{$config{'mode'}->{'val'}}->{'innodb_required'});
      $innodb_status_text = $res->{'Status'};
   }
}

{
   my $dbh;
   my @dbhs;
   sub connect_to_db {
      if ( !$dbh ) {
         $dbh = get_new_db_connection();

         # Get version and connection ID.  This is necessary to do repeatedly
         # because we may disconnect and connect again.
         ( $mysql_version, $connection_id )
            = $dbh->selectrow_array("SELECT VERSION(), CONNECTION_ID()");
         ( $ver_major, $ver_minor, $ver_rev )
            = $mysql_version =~ m/^(\d+)\.(\d+)\.(\d+)/;

         # 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");
      }
      return $dbh;
   }

   sub disconnect_from_db {
      foreach my $dbh ( @dbhs ) {
         eval {
            $dbh->disconnect();
         };
      }
      $dbh = 0;
   }

   # For whatever reason, setting AutoCommit to 0 can cause DBI problems when
   # connecting.  It will get it into its head that MySQL doesn't support
   # transactions even though everything is fine.  So, I set it to 1, and use
   # explicit SQL to handle transactions.  That seems to work OK.  I've looked
   # around the Internet and lots of people have this problem, with no real
   # idea why.
   sub get_new_db_connection {
      my $dbh = DBI->connect(
         "DBI:mysql:$config{db}->{val};host=$config{host}->{val};port=$config{port}->{val}",
         $config{'user'}->{'val'},
         $config{'password'}->{'val'},
         { RaiseError => 1, PrintError => 0, AutoCommit => 1 },
         ) or die $DBI::errstr;
      push @dbhs, $dbh;
      return $dbh;
   }
}

sub show_help {
   print "This is innotop, a MySQL and InnoDB monitor.\n\n",
      "Command-line options are key-value pairs.  Valid keys are:\n\n";
   foreach my $key ( sort grep { $config{$_}->{'cmdline'} } keys %config ) {
      my $val = $config{$key};
      print "$key\t$val->{note}\n";
   }

   print "\nFor example, 'innotop port 3900' connects to port 3900.\n"
       . "You can also prefix each name with one or two dashes, e.g.\n"
       . "   'innotop --port 3900'\n";
   exit(0);
}

sub load_config {
   # Get command-line arguments.  They will override arguments elsewhere.
   my %cmd_line_args;
   eval {
      while ( my ($name, $val) = splice(@ARGV, 0, 2) ) {
         $name =~ s/^-*//;
         $cmd_line_args{$name} = $val;
      }
   };

   my $filename = "$homepath/.innotop";
   if ( -f $filename ) {
      open my $file, "<", $filename or die("Can't open $filename: $OS_ERROR");
      my $upgraded = 1;
      while ( my $line = <$file> ) {
         chomp $line;
         next if $line =~ m/^#/;
         my ( $name, $val ) = $line =~ m/^(.+?)=(.*)$/;

         if ( $name eq 'version' ) {
            $upgraded = $val gt $VERSION;
            next;
         }

         # Override with cmd-line arguments.
         if ( exists($cmd_line_args{$name}) && $config{$name}->{'cmdline'} ) {
            $val = $cmd_line_args{$name};
         }

         # Validate the incoming values...
         if ( $name && exists( $config{$name} ) ) {

            my $meta = $config{$name}->{'meta'};
            my $validator_sub
               = $config{$name}->{'pat'}               ? make_regex_validator($config{$name}->{'pat'})
               : ref($config{$name}->{'val'}) && $meta ? make_list_validator ($meta)
               : $meta                                 ? make_meta_validator ($meta)
               :                                        undef;

            if ( !$validator_sub || $validator_sub->($val) ) {
               # If it's an array and we just upgraded, make sure the user's
               # existing config file doesn't hide newly introduced values.
               if ( $upgraded && ref($config{$name}->{'val'}) eq 'ARRAY' ) {
                  $val .= ' ' . join(" ", @{$config{$name}->{'val'}});
               }
               $config{$name}->{'val'}
                  = get_config_from_string( $name, $meta, $val );
               $config{$name}->{'read'} = 1;
            }
         }

      }
      close $file or die("Can't close $filename: $OS_ERROR");
   }
   else {
      display_license();
   }

   foreach my $key ( keys %config ) {
      if ( !$config{$key}->{'read'} ) {
         
         # Look for it in cmd-line arguments.
         if ( exists($cmd_line_args{$key}) && $config{$key}->{'cmdline'} ) {
            my $meta = $config{$key}->{'meta'};
            my $validator_sub
               = $config{$key}->{'pat'}               ? make_regex_validator($config{$key}->{'pat'})
               : ref($config{$key}->{'val'}) && $meta ? make_list_validator ($meta)
               : $meta                                ? make_meta_validator ($meta)
               :                                        undef;

            my $val = $cmd_line_args{$key};
            if ( !$validator_sub || $validator_sub->($val) ) {
               $config{$key}->{'val'}
                  = get_config_from_string( $key, $meta, $val );
               $config{$key}->{'read'} = 1;
            }
         }

         # If it's still not set, prompt...
         if ( !$config{$key}->{'read'} && $config{$key}->{'prompt'} ) {
            get_config_interactive($key);
         }
      }
   }
}

sub save_config {
   my $filename = "$homepath/.innotop";
   open my $file, "+>", $filename
      or die("Can't write to $filename: $OS_ERROR");
   print $file "version=$VERSION\n";
   foreach my $key ( sort keys %config ) {
      if ( $key ne 'password' || $config{'savepass'}->{'val'} ) {
         print $file "# $config{$key}->{note}\n"
            or die "Can't write $filename: $OS_ERROR";
         if ( ref( $config{$key}->{'val'} ) eq 'ARRAY' ) {
            print $file "$key="
               . join( " ", @{ $config{$key}->{'val'} } ) . "\n"
               or die "Can't write $filename: $OS_ERROR";
         }
         elsif ( ref( $config{$key}->{'val'} ) eq 'HASH' ) {
            print $file "$key="
               . join( " ",
                  map { "$_:$config{$key}->{val}->{$_}" } keys %{ $config{$key}->{'val'} }
               ) . "\n"
               or die "Can't write $filename: $OS_ERROR";
         }
         else {
            print $file "$key=$config{$key}->{val}\n"
               or die "Can't write $filename: $OS_ERROR";
         }
      }
   }
   close $file or die("Can't close $filename: $OS_ERROR");
}

# Prints out a prompt and reads from the keyboard, then validates with the
# validation function until the input is correct.
sub prompt {
   my ( $prompt, $validator, $noecho ) = @_;
   print "$prompt: ";
   my $response;
   my $success = 0;

   ReadMode('noecho') if $noecho;

   do {
      $response = <STDIN>;
      chomp($response);
      if ( $validator && !$validator->($response) ) {
         print "Invalid response.\nTry again: ";
      }
      else {
         $success = 1;
      }
   } while ( !$success );

   ReadMode('normal');

   return $response;
}

sub restore_mode {
   switch_mode($previous_mode);
}

sub switch_mode {
   my $mode = shift;
   if ( $config{'mode'}->{'val'} ne $mode && config_valid_for('mode', $mode) ) {
      $previous_mode = $config{'mode'}->{'val'};
      $config{'mode'}->{'val'} = $mode;
   }
}

sub config_valid_for {
   my ( $key, $val ) = @_;
   if ( exists( $config{$key} ) ) {
      if ( $config{$key}->{'meta'} ) {
         return grep { $val eq $_ } keys %{$config{$key}->{'meta'}};
      }
      elsif ( $config{$key}->{'pat'} ) {
         return $val =~ $config{$key}->{'pat'};
      }
      return 1;
   }
   return 0;
}

sub display_license {
   $clear_screen_sub->();

   print $innotop_license;

   pause();
}

sub display_everything_configurable {
   my $mode = $config{'mode'}->{'val'};

   my @display_lines
      = ( '', 'Here are all configurable variables for this mode:', '');

   my %config_choices
      = map  { $_ => $config{$_}->{'note'} || '' }
        # Only config values that are marked as applying to this mode.
        grep {
           my $key = $_;
           $config{$key}->{'conf'} &&
              ( $config{$key}->{'conf'} eq 'ALL'
              || grep { $mode eq $_ } @{$config{$key}->{'conf'}} )
        } keys %config;

   push @display_lines, create_table2(
      [ sort keys %config_choices ],
      { map { $_ => $_ } keys %config_choices },
      \%config_choices,
      { sep => '  ', just => '-' } );
   push @display_lines, '';
   push @display_lines, '';

   $clear_screen_sub->();
   draw_screen(\@display_lines );
   my $key = prompt("Enter the name of the variable you wish to configure");
   if ( exists($config_choices{$key}) ) {
      get_config_interactive($key);
   }
}

sub make_regex_validator {
   my $pat = shift;
   return sub {
      return $_[0] =~ m/$pat/;
   };
}

sub make_list_validator {
   my $meta = shift;
   return sub {
      my @vals = split(/ +/, shift);
      foreach my $val ( @vals ) {
         return 0 unless grep { $_ eq $val } keys %$meta;
      }
      return 1;
   };
}

sub make_meta_validator {
   my $meta = shift;
   return sub {
      my $val = shift;
      return grep { $_ eq $val } keys %$meta
   };
}

sub get_config_interactive {
   my $key = shift;
   $clear_screen_sub->();

   # Print help first.
   print "Enter a new value for '$key' ($config{$key}->{note}).\n";

   my $meta = $config{$key}->{'meta'};
   if ( $meta ) {
      my @meta_rows = create_table2(
               [ sort keys %$meta ],
               { map { $_ => $_ } keys %$meta },
               { map { $_ => $meta->{$_}->{'label'} || $meta->{$_}->{'hdr'} } keys %$meta },
               { sep => '  ' });
      if (@meta_rows > 10) {
         # Try to split and stack the meta rows next to each other
         my $split = int(@meta_rows / 2);
         @meta_rows = stack_next(
            [@meta_rows[0..$split - 1]],
            [@meta_rows[$split..$#meta_rows]],
            { pad => ' | '},
         );
      }
      my $caption = ref($config{$key}->{'val'})
         ? "Choose one or more of these values:"
         : "Choose one of these values:";
      print join("\n", '', create_caption($caption, @meta_rows), ''), "\n";
   }

   if ( $config{$key}->{'hint'} ) {
      print "\nHint: $config{$key}->{hint}\n\n";
   }

   if ( defined($config{$key}->{'val'}) ) {
      if ( ref($config{$key}->{'val'}) ) {
         print "Current value: " . join(" ", @{$config{$key}->{'val'}}) . "\n";
      }
      else {
         print "Current value: $config{$key}->{val}\n";
      }
   }

   my $validator_sub
      = $config{$key}->{'pat'}               ? make_regex_validator($config{$key}->{'pat'})
      : ref($config{$key}->{'val'}) && $meta ? make_list_validator ($meta)
      : $meta                                ? make_meta_validator ($meta)
      :                                        undef;

   my $new_value = prompt('Enter a value',
         $validator_sub, $config{$key}->{'noecho'} );
   $config{$key}->{'val'}
      = get_config_from_string( $key, $meta, $new_value );
}

sub get_config_from_string {
   my ( $key, $meta, $string ) = @_;

   # It's an array
   if ( ref( $config{$key}->{'val'} ) eq 'ARRAY' ) {
      my %seen; # unique-ify
      return [
         grep {
            my $val = $_;
            !$seen{$_}++ && (!$meta || grep { $val eq $_ } keys %$meta )
         } split(/ +/, $string)
      ];
   }

   # It's a hash
   elsif ( ref( $config{$key}->{'val'} ) eq 'HASH' ) {
      return { $string =~ m/(\S+):(\S+)/g };
   }

   # It's a scalar, but constrained
   elsif ( $meta ) {
      if ( grep { $string eq $_ } keys %$meta ) {
         return $string;
      }
   }

   # It's a free-form scalar
   else {
      return $string;
   }

}

sub get_file {
   my $filename = shift;
   open my $file, "<", "$filename" or die "Can't open $filename: $!";
   my $file_contents = do { local $/; <$file>; };
   close $file;
   return $file_contents;
}

sub meta_to_hdr {
   my $meta = shift;
   my %labels = map { $_ => $meta->{$_}->{'hdr'} } keys %$meta;
   return \%labels;
}

sub filename {
   ( my $filename = shift ) =~ s#^.*[/\\]##;
   return $filename;
}

# ############################################################################
# Perldoc section.  I put this last as per the Dog book.
# ############################################################################
=pod

=head1 NAME

innotop - A MySQL and InnoDB monitor program.

=head1 DESCRIPTION

innotop connects to a MySQL database server and retrieves information from it,
then displays it in a manner similar to the UNIX top program.  innotop uses
the data from SHOW VARIABLES, SHOW GLOBAL STATUS, SHOW FULL PROCESSLIST, and
SHOW ENGINE INNODB STATUS, among other things.

innotop operates in one of several modes, and you control it by key presses.
You can find a complete list of all available keys at any time by pressing '?'
for help.  See below for a list of modes.

Note: if there's been a huge deadlock, the InnoDB monitor text may be
truncated, and the transaction and other information may not be available.  In
this case, you should enter Deadlock mode, make a note of anything you care
about, and use the 'w' command to deliberately create a very small deadlock,
so the monitor text won't be truncated anymore.

=head1 ABOUT THIS DOCUMENTATION

This documentation is text that can get out of date.  The help text displayed
when you press '?' is generated by innotop looking at itself, and will never
be out of date or incomplete.

=head1 CONFIGURATION

Fortunately or unfortunately, depending on your point of view, innotop is
highly configurable.  You can choose what to display, what order to sort it
in, and so on.  Every tabular display allows you to choose the columns and
their order.  You can remove a tabular display section entirely by removing
all its columns.

There are no required command-line arguments.  Just start the program.  It
will create a default configuration file for you, asking you to fill in a
couple variables it needs.  After that, the configuration file gets saved
every time you exit innotop.  You can hand-edit it when innotop isn't running.

You can specify certain options on the command-line.  Run `innotop -help' for
details.

You can also configure everything that matters while it's running.  Just press
the '$' key, and everything relevant to the current mode will be displayed.
Type the name of the variable you want to configure, and a dialog will show
you all sorts of info about the variable, its current value, and possible
values.  This is probably more convenient than editing the file by hand, since
you won't get all that helpful information when editing by hand.

=head1 MODES

innotop has a bunch of different modes.  The following is a brief, incomplete
description of each.  Remember, you can always get the authoritative help by
pressing '?'.

=over 8

=item InnoDB Transactions

This mode shows every transaction in the InnoDB monitor's output, in
query-list format.  This mode is the reason I wrote innotop.  It is the most
important for my daily monitoring activities.

It has tons of options, including the ability to choose a sort column and sort
order, hide inactive transactions, hide innotop's own transaction, and choose
columns for the display.

You can also add filters to each column (including ones not displayed), much
like a SQL WHERE clause.  Filters are Perl regular expressions.

From this mode you can also enter the Query Analysis mode, to EXPLAIN a query.

=item InnoDB Lock Waits

This mode shows you information about any current InnoDB lock waits.  This
information comes from the TRANSACTIONS section of the InnoDB status text.  If
you have a very busy server, you may have frequent lock waits; it helps to be
able to see which table and index is the "hot spot" for locks.  If your server
is running pretty well, this mode will usually have nothing to display.

A second section of the display 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 running in high concurrency.  This section deals with thread
semaphores, not transactional lock waits.

=item Open Tables

This section comes from MySQL's SHOW OPEN TABLES command.  By default it is
filtered to show tables which are in use by one or more queries, so you can
get a quick look at which tables are 'hot'.  You can use this to guess which
tables might be locked implicitly.

You enter this mode with the 'O' key.

=item Query List

This mode is very much like B<mytop>'s query list mode.  It shows a list of
all processes, garnered from SHOW FULL PROCESSLIST.  This mode does B<not>
show InnoDB-related information.  This is probably one of the most useful
modes for general usage.

It has a bunch of options, including an informative header.  You can also show
or hide many things, such as the header, innotop's own process, and sleeping
processes.  You can choose a sort column and reverse the sort.

You can also add filters to each column (including ones not displayed), much
like a SQL WHERE clause.  Filters are Perl regular expressions.

From this mode you can also enter the Query Analysis mode, to EXPLAIN a query.

=item InnoDB Deadlocks

This mode shows the last detected InnoDB deadlock.  It displays the
transactions involved, the locks that were held and waited for, and the query
each was running.  It also allows you to deliberately create a small deadlock,
to obliterate the (potentially very large) deadlock information.

=item InnoDB Foreign Key Errors

This mode shows the last InnoDB foreign key error information, such as the
table where it happened, when and who and what query caused it, and so on.

InnoDB has a huge variety of foreign key error messages, and many of them are
just hard to parse.  innotop doesn't always do the best job here, but there's
so much code devoted to parsing this messy, unparseable output that innotop is
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 InnoDB Row Operations and Semaphores

This mode shows the InnoDB row operations and semaphore information.  It shows
a synopsis of per-second average information, and what the status of various
things is.  It also shows when there's information in the wait array.

=item InnoDB Buffers

This mode shows the InnoDB buffer pool, buffer memory usage, insert buffer,
and adaptive hash index.

=item 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.

=item Load Graph

This mode calculates several types of per-second statistics, such as queries
per second, scales them against a maximum, and prints them out as a "graph" in
the style of <tload>.  It's similar to the Load Statistics mode, except it's a
graph instead of numbers.

=item Load Statistics

This mode calculates several types of per-second 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 choose whether to show the statistics over all time or the interval at
which innotop refreshes.  You can choose whether to show absolute values or
per-second averages.  You can also increase and decrease the digits of
precision, and toggle between short and long numbers modes (e.g. 1k vs.  1,024).

=item Variables & Status

This mode displays any variables you please from SHOW GLOBAL STATUS and SHOW
VARIABLES.  It displays not only the current values, but previous values too;
you choose how many (two through nine).  You have the same options as in Load
Statistics mode.

You can save 10 presets and toggle between them with the keys 0 through 9.
I've included some samples with innotop already.  They show you things about
the query cache, how many index and table scans are happening, and so forth.
You can make your own too, of course.

=item Analyze Query

This mode is useful in Query List and Transaction modes.  You enter it from
those modes with the 'e' or 'f' keys, to 'explain' or 'show full text' of a
query.  Use 'q' to exit this mode and return to the previous mode.

In this mode, you can 'explain' a query, which executes EXPLAIN EXTENDED
whatever query you selected in Query List or Transaction mode.  You can flip
back and forth between explaining the query, looking at its full text, and
viewing the MySQL optimization of the query (only available in newer versions
of MySQL, where SHOW WARNINGS after EXPLAIN EXTENDED shows the optimization).
You will also see partition information, which will be blank unless your
version of MySQL supports partitions.

=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 that don't require InnoDB information.

I think everything you need to run innotop is distributed either with Perl, or
with innotop itself.  You need DBI, obviously.  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).

I run innotop on Gentoo GNU/Linux, and I've had feedback from people
successfully running it on Red Hat, CentOS, Solaris, and Mac OSX.  I don't see
any reason why it won't work on other UNIX-ish operating systems, but I don't
know for sure.  As of version 0.1.155, 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 version 4.1 and 5.0
and have heard of others using it on these same versions.  I don't know of any
reasons it shouldn't work with other versions of perl and MySQL, but I don't
know for sure.  Please test and help me fix any problems you find.

=head1 FILES

$homepath/.innotop is used to store configuration information.

You may also choose to have innotop do "core dumps" if there are errors.  In
that case you can choose a file in which to put the dump.  See the
configuration file.

=head1 COPYRIGHT, LICENSE AND WARRANTY

This program is copyright (c) 2006 Baron Schwartz, baron at xaprb dot com.
Feedback and improvements are gratefully received.

THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 2; OR the Perl Artistic License.  On UNIX and similar
systems, you can issue `man perlgpl' or `man perlartistic' to read these
licenses.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA  02111-1307  USA.

Execute innotop and press '!' to see this information at any time.

=head1 AUTHOR

Baron Schwartz, baron at xaprb dot com.

=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.

It does way too much, and tries to be all things to all people.

Not really a bug, but asking InnoDB for its monitor output has a side effect.
Certain InnoDB statistics are calculated from the last time the request was
issued, so multiple users may have an effect on the data each other sees.

=cut
