#!/usr/bin/perl -w
#
# Xanadu(R) DIMENSIA(tm) Hyperstructure Kit, $Revision: 0.49.0.8 $
#
# Designed by Ted Nelson
# Programmed by Andrew Pam ("xanni") and Bek Oberin ("gossamer")
# Copyright (c) 1997, 1998 Project Xanadu
#
# This is only a partial implementation, with only a few structure and view
# operations; however, they are enough to allow you to create, view and explore
# complex multidimensional structures in quantum hyperspace.
#
# A forthcoming tutorial will present you with strange spaces to explore,
# beginning with easy 2D concepts, the peculiar geography of this system and
# operating recommendations for getting around in it comfortably and changing
# your structures without losing cells or losing track of your stuff.  (This
# will emphasize pragmatic adaptations to the peculiarities of this limited
# implementation.)
#
# Forthcoming documentation will explain the space, the theory, structure
# operations, view operations, and the official planned extensions.
#
# For more information, visit http://www.xanadu.net/zz/
#
# ===================== Change Log
#
# Inital zigzag implementation
# $Id: zigzag,v 0.49.0.8 1998/07/12 17:02:01 gossamer Exp gossamer $
#
# $Log: zigzag,v $
# Revision 0.49.0.8  1998/07/12 17:02:01  gossamer
# Fixed bugs with clone insertion, and error-trapping
#
# Revision 0.49.0.7  1998/07/05 02:12:15  gossamer
# Statusbar now optional and off by default
#
# Revision 0.49.0.6  1998/07/04 13:53:28  gossamer
# Added backup of initial data file, fixed syncing before
# external operations.
#
# Revision 0.49.0.5  1998/06/21 06:14:41  gossamer
# Began to implement mark
#
# Revision 0.49.0.4  1998/05/28 10:09:05  gossamer
# Added DB syncing after x commands and  before all external ops
#
# Revision 0.49.0.3  1998/05/28 09:57:45  gossamer
# Fixed duration of status bar messages.
#
# Revision 0.49.0.2  1998/05/19 23:32:01  gossamer
# Renamed d.containment and d.contentlist to d.inside and d.contents
#
# Revision 0.49.0.1  1998/05/19 23:06:21  gossamer
# Constrained cell insertion in clone dimension, user_error copes
# with empty text messages.
#
# Revision 0.49  1998/04/11 16:50:46  xanni
# Implemented preliminary ZZmail support
#
# Revision 0.48  1998/04/11 15:20:01  xanni
# Reinstated $TRUE and $FALSE, improved error handling, fixed minor bugs
#
# Revision 0.47  1998/04/11 10:43:23  xanni
# Renamed d.contain to d.containment, generalised quadrant view
#
# Revision 0.46  1998/04/11 10:08:49  xanni
# Merged latest changes, renamed dimensions again
#
# Revision 0.45.0.2  1998/04/05 12:24:22  gossamer
# Made yellow-all-the-time optional for clones.
#
# Revision 0.45.0.1  1998/04/02 03:36:47  gossamer
# Status bar and better error reporting
#
# Revision 0.45  1998/03/18 03:28:28  xanni
# Fixed new dimension names, merged changes
#
# Revision 0.44.1.1  1998/03/13 03:36:27  gossamer
# Debugged, changed dimension names.
#
# Revision 0.44.1.0  1998/03/11 01:37:35  gossamer
# Renamed most functions to enhance readabililty and modularity
#
# Revision 0.44.0.1  1998/03/08 02:42:49  gossamer
# Rearranged things to make modularization more clear
# Added a few helper functions
#
# Revision 0.44  1998/03/05 06:35:06  xanni
# Merge colour changes with main branch
#
# Revision 0.43.0.1  1998/03/04 22:45:52  gossamer
# Cursor and clone colours added.  Dimension guide made smaller.
#
# Revision 0.43  1998/03/04 02:43:34  xanni
# Fixed containment bugs
#
# Revision 0.42  1998/03/02 15:31:28  xanni
# Implemented wordwrap and containment (display, execute and export)
#
# Revision 0.41  1998/02/27 17:35:37  xanni
# Changed initial dimensions to d.1-3, minor bugfixes and cleanup
#
# Revision 0.40  1998/02/25 20:59:52  xanni
# Removed cell types (redo as part of an attribute list later)
#
# Older revisions are listed in the CHANGES file
#

use integer;
use strict;
use Curses;        # Available at http://www.perl.org/CPAN/CPAN.html
use DB_File;
use Fcntl;
use POSIX;
use File::Copy;
#require 'sys/ioctl.ph';

# Note: We are using the following naming convention:
# Constants are named in ALLCAPS
# Global variables are named with Initial Caps
# Local variables and functions are named in lowercase
# Function calls to functions defined in this file all start with & except
#    the Curses ones.
# Put brackets around all function arguments.

# Define constants
my $VERSION = do { my @r=(q$Revision: 0.49.0.8 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
my $FALSE = 0;
my $TRUE = not $FALSE;
my $CELLS_PER_WIN = 5;           # Number of cells displayed across each window
my $CURSOR_HOME = 10;            # NOTE!  This assumes it stays fixed!
my $DELETE_HOME = 99;            # NOTE!  This assumes it stays fixed!
my $EDITOR = "/bin/vi";
   $EDITOR = "/usr/bin/emacs" if -x "/usr/bin/emacs";
   $EDITOR = "/usr/local/bin/mule" if -x "/usr/local/bin/mule";
   $EDITOR = "/usr/X11R6/bin/gvim -f" if -x "/usr/X11R6/bin/gvim";
my $FILENAME = "zigzag.data";    # Default filename for initial slice
my $TEMP_FILE = "/tmp/zigzag";   # Filename used for external editing
my $LOTS_OF_COLOURS = $FALSE;    # What style
my $USE_STATUSBAR = 0;           # Are we using the status bar?
my $ZZMAIL_SUPPORT = $TRUE;	   # Enable preliminary ZZmail support
my $COMMAND_SYNC_COUNT = 20;     # Sync the DB after this many commands
my $BACKUP_FILE_SUFFIX = ".bak";

# Declare globals
my %ZZ;                          # The Zigzag cells and links
my @Window;                      # Curses window handles
my $Status;                      # Status bar handle
my $StatusTimer;                 # Times messages' duration on the status bar
my @Window_Dirty;                # Flags to indicate windows need redrawing
my $Status_Dirty;                # Flags to indicate status bar needs redrawing
my $Input_Buffer;                # Cell number entered from the keyboard
my $Cursor_Len;                  # Current display width of the cursors
my $Display_Resized;             # True when window has been resized
my $ZigZag_Terminated;           # True when interrupted by a signal
my $Display_Has_Colour;          # True if colour is supported
my $Command_Count;               # Counts commands between DB syncs
my $DB_Ref;                      # We use this for sync'ing.

# Initialise global zigzag data structures
my %Keymap_Directions = ( # Direction key mappings
   "s"                => "0L",
   "f"                => "0R",
   "e"                => "0U",
   "c"                => "0D",
   "d"                => "0I",
   "D"                => "0O",
   "j"                => "1L",
   &KEY_LEFT          => "1L",
   "l"                => "1R",
   &KEY_RIGHT         => "1R",
   "i"                => "1U",
   &KEY_UP            => "1U",
   ","                => "1D",
   &KEY_DOWN          => "1D",
   "k"                => "1I",
   &KEY_PPAGE         => "1I",
   "K"                => "1O",
   &KEY_NPAGE         => "1O",
);

my %Keymap = (  # Keyboard mappings
   "\r"               => 'atcursor_execute(0);',
   "\n"               => 'atcursor_execute(0);',
   "<"                => '@_ = input_get_direction(); cells_import(@_) if $_[0];',
   ">"                => '@_ = input_get_direction(); cells_export(@_) if $_[0];',
   chr(127)           => 'atcursor_delete(1);',
   &KEY_DC            => 'atcursor_delete(1);',
   &KEY_BACKSPACE     => 'input_process_backspace();',
   &meta_key("s")     => 'atcursor_hop(0, "L");',
   &meta_key("f")     => 'atcursor_hop(0, "R");',
   &meta_key("e")     => 'atcursor_hop(0, "U");',
   &meta_key("c")     => 'atcursor_hop(0, "D");',
   &meta_key("D")     => 'atcursor_edit(0);',
   &meta_key("d")     => 'atcursor_edit(0);',
   "\cD"              => 'atcursor_edit(0);',
   &meta_key("j")     => 'atcursor_hop(1, "L");',
   &meta_key("l")     => 'atcursor_hop(1, "R");',
   &meta_key("i")     => 'atcursor_hop(1, "U");',
   &meta_key(",")     => 'atcursor_hop(1, "D");',
   &meta_key("K")     => 'atcursor_edit(1);',
   &meta_key("k")     => 'atcursor_edit(1);',
   "\cK"              => 'atcursor_edit(1);',
   "b"                => '@_ = input_get_direction(); atcursor_break_link(@_) if $_[0];',
   "G"                => 'cursor_jump(get_cursor(0));',
   "g"                => 'cursor_jump(get_cursor(1));',
   &KEY_HOME          => 'cursor_jump(get_cursor(1));',
   "h"                => '@_ = input_get_direction(); atcursor_hop(@_) if $_[0];',
   "N"                => 'cell_create(0);',
   &KEY_IC            => 'cell_create(0);',
   "n"                => 'cell_create(1);',
   "M"                => 'atcursor_mark(1);',
   "m"                => 'atcursor_mark(1);',
   "O"                => '$LOTS_OF_COLOURS = !$LOTS_OF_COLOURS; for ($_ = 0; $_ <= $#Window; $_++) { $Window_Dirty[$_] = $TRUE; }',
   "o"                => '$LOTS_OF_COLOURS = !$LOTS_OF_COLOURS; for ($_ = 0; $_ <= $#Window; $_++) { $Window_Dirty[$_] = $TRUE; }',
   "Q"                => 'view_quadrant_toggle(0);',
   "q"                => 'view_quadrant_toggle(1);',
   "T"                => 'atcursor_clone(0);',
   "t"                => 'atcursor_clone(1);',
   "V"                => 'view_raster_toggle(0);',
   "v"                => 'view_raster_toggle(1);',
   &meta_key("V")     => 'status_draw(&version());',
   &meta_key("v")     => 'status_draw(&version());',
   "X"                => 'view_rotate(0, "X");',
   "x"                => 'view_rotate(1, "X");',
   &meta_key("X")     => 'view_flip(0, "X");',
   &meta_key("x")     => 'view_flip(1, "X");',
   "Y"                => 'view_rotate(0, "Y");',
   "y"                => 'view_rotate(1, "Y");',
   &meta_key("Y")     => 'view_flip(0, "Y");',
   &meta_key("y")     => 'view_flip(1, "Y");',
   "Z"                => 'view_rotate(0, "Z");',
   "z"                => 'view_rotate(1, "Z");',
   &meta_key("Z")     => 'view_flip(0, "Z");',
   &meta_key("z")     => 'view_flip(1, "Z");',
);

sub initial_geometry()
{
  return (
    0 =>        "Home",
      "0-d.1" =>        99,
      "0+d.2" =>        30,
      "0+d.cursor" =>    11,
    1 =>        "d.1",
      "1-d.1" =>        10,
      "1+d.1" =>        99,
      "1-d.2" =>        8,
      "1+d.2" =>        2,
    2 =>        "d.2",
      "2-d.2" =>        1,
      "2+d.2" =>        3,
    3 =>        "d.3",
      "3-d.2" =>        2,
      "3+d.2" =>        4,
    4 =>        "d.inside",
      "4-d.2" =>        3,
      "4+d.2" =>        5,
    5 =>        "d.contents",
      "5-d.2" =>        4,
      "5+d.2" =>        6,
    6 =>        "d.mark",
      "6-d.2" =>        5,
      "6+d.2" =>        7,
    7 =>        "d.clone",
      "7-d.2" =>        6,
      "7+d.2" =>        8,
    8 =>        "d.cursor",
      "8-d.2" =>        7,
      "8+d.2" =>        1,
   10 =>        "Cursor home",
      "10+d.1" =>        1,
      "10+d.2" =>        11,
   11 =>        "Action",
      "11+d.1" =>        12,
      "11-d.2" =>        10,
      "11+d.2" =>        16,
      "11-d.cursor" =>    0,
      "11+d.cursor" =>    16,
   12 =>        "+d.1",
      "12-d.1" =>        11,
      "12+d.1" =>        13,
   13 =>        "+d.2",
      "13-d.1" =>        12,
      "13+d.1" =>        14,
   14 =>        "+d.3",
      "14-d.1" =>        13,
      "14+d.1" =>        15,
   15 =>        "I",
      "15-d.1" =>        14,
   16 =>        "Data",
      "16+d.1" =>        17,
      "16-d.2" =>        11,
      "16-d.cursor" =>    11,
   17 =>        "+d.1",
      "17-d.1" =>        16,
      "17+d.1" =>        18,
   18 =>        "+d.2",
      "18-d.1" =>        17,
      "18+d.1" =>        19,
   19 =>        "+d.3",
      "19-d.1" =>        18,
      "19+d.1" =>        20,
   20 =>        "I",
      "20-d.1" =>        19,
   30 =>        "#Edit\natcursor_edit(1);",
      "30-d.2" =>        0,
      "30+d.2" =>        40,
   40 =>        "#L-ins\natcursor_insert(1, 'L');",
      "40+d.1" =>        41,
      "40-d.2" =>        30,
      "40+d.2" =>        50,
   41 =>        "#R-ins\natcursor_insert(1, 'R');",
      "41-d.1" =>        40,
      "41+d.1" =>        42,
   42 =>        "#U-ins\natcursor_insert(1, 'U');",
      "42-d.1" =>        41,
      "42+d.1" =>        43,
   43 =>        "#D-ins\natcursor_insert(1, 'D');",
      "43-d.1" =>        42,
      "43+d.1" =>        44,
   44 =>        "#I-ins\natcursor_insert(1, 'I');",
      "44-d.1" =>        43,
      "44+d.1" =>        45,
   45 =>        "#O-ins\natcursor_insert(1, 'O');",
      "45-d.1" =>        44,
   50 =>        "#Delete\natcursor_delete(1);",
      "50+d.1" =>        51,
      "50-d.2" =>        40,
      "50+d.2" =>        60,
   51 =>        "#L-break\natcursor_break_link(1, 'L');",
      "51-d.1" =>        50,
      "51+d.1" =>        52,
   52 =>        "#R-break\natcursor_break_link(1, 'R');",
      "52-d.1" =>        51,
      "52+d.1" =>        53,
   53 =>        "#U-break\natcursor_break_link(1, 'U');",
      "53-d.1" =>        52,
      "53+d.1" =>        54,
   54 =>        "#D-break\natcursor_break_link(1, 'D');",
      "54-d.1" =>        53,
      "54+d.1" =>        55,
   55 =>        "#I-break\natcursor_break_link(1, 'I');",
      "55-d.1" =>        54,
      "55+d.1" =>        56,
   56 =>        "#O-break\natcursor_break_link(1, 'O');",
      "56-d.1" =>        55,
   60 =>        "#Mark",
      "60-d.2" =>        50,
      "60+d.2" =>        70,
   70 =>        "#L-Hop\natcursor_hop(1, 'L');",
      "70+d.1" =>        71,
      "70-d.2" =>        60,
      "70+d.2" =>        80,
   71 =>        "#R-Hop\natcursor_hop(1, 'R');",
      "71-d.1" =>        70,
      "71+d.1" =>        72,
   72 =>        "#U-Hop\natcursor_hop(1, 'U');",
      "72-d.1" =>        71,
      "72+d.1" =>        73,
   73 =>        "#D-Hop\natcursor_hop(1, 'D');",
      "73-d.1" =>        72,
      "73+d.1" =>        74,
   74 =>        "#I-Hop\natcursor_hop(1, 'I');",
      "74-d.1" =>        73,
      "74+d.1" =>        75,
   75 =>        "#O-Hop\natcursor_hop(1, 'O');",
      "75-d.1" =>        74,
   80 =>        "#Shear",
      "80-d.2" =>        70,
      "80+d.2" =>        85,
   85 =>        "#Chug",
      "85-d.2" =>        80,
      "85+d.2" =>        90,
   90 =>        "#A-View toggle\nview_raster_toggle(0);",
      "90+d.1" =>        91,
      "90-d.2" =>        85,
   91 =>        "#D-View toggle\nview_raster_toggle(1);",
      "91-d.1" =>        90,
   99 =>        "Midden",
      "99-d.1" =>        1,
      "99+d.1" =>        0,
   "n" =>        100
   );
}


#
# Some helper functions
#

sub version()
{
   return "ZigZag, version $VERSION";
}

sub meta_key($)
# This is just a little helper macro, returns the META/ALT key code
{ 
   return(chr(ord($_[0]) + 128)); 
}

sub reverse_sign($)
# Reverse the sign of the given cursor/dimension
{
   return ((substr($_[0], 0, 1) eq "+") ? "-" : "+") . substr($_[0], 1);
}

sub wordbreak($$)
# Returns a string up to the first line break or the end of the last word
# that finishes before the given character position
{
  $_ = substr($_[0], 0, $_[1]);
  if (/^(.*)\n/)
  {
     $_ = "$1 ";
  }
  elsif ((length eq $_[1]) && /^(.+)\s+\S*$/)
  {
     $_ = $1;
  }
  return $_;
}


#
# Testing cell type
# Named is_*
#
sub is_cursor($)
{
   my $cell = shift;
   return (defined($ZZ{"$cell-d.cursor"}) || defined($ZZ{"$cell+d.cursor"}));
}

sub is_clone($)
{
   my $cell = shift;
   return (defined($ZZ{"$cell-d.clone"}) || defined($ZZ{"$cell+d.clone"}));
}

#
# Retreiving Information
# Named get_*
#
sub get_lastcell($$)
# Find the last cell along a given dimension
{
  my ($cell, $dim) = @_;
  die "No cell $cell" unless defined($ZZ{"$cell"});
  die "Invalid direction $dim" unless ($dim =~ /^[+-]/);

  # Follow links to the end or until we return to where we started
  $cell = $_ while defined($_ = $ZZ{"$cell$dim"}) && ($_ != $_[0]);
  return $cell;
}

sub get_outline_parent($)
# Find the "outline parent" (first cell -d.1 along -d.2) of a cell
{
  my $cell = $_[0];
  die "No cell $cell" unless defined($ZZ{"$cell"});

  # Move -d.2 until we find a -d.1 link or return to where we started
  $cell = $_ while (!defined($ZZ{"$cell-d.1"}) &&
		    defined($_ = $ZZ{"$cell-d.2"}) && ($_ != $_[0]));
  $cell = $_ if defined($_ = $ZZ{"$cell-d.1"});
  return $cell;
}

sub get_cell_contents($)
# Return the contents of a cell
{
  my $cell = $_[0];
  die "No cell $cell" unless defined($ZZ{"$cell"});
  my $contents = $ZZ{&get_lastcell($cell, "-d.clone")};

  if ($ZZMAIL_SUPPORT && $contents =~ /^\[(\d+)\+(\d+)\]/)
  # Note 1: This should handle pointer lists, but currently only handles
  #         the first pointer.
  # Note 2: This performs extremely badly for large primedia.  It should
  #         make requests to an OSMIC server instead.
  {
    my $pos = $1;
    my $len = $2;
    my $PRIMEDIA = $ZZ{&get_outline_parent($cell)};
    if (open(PRIMEDIA, "<$PRIMEDIA"))
    {
      my $error = $FALSE;
      seek(PRIMEDIA, $pos, SEEK_SET) || ($error = "seeking");
      read(PRIMEDIA, $contents, $len) == $len || ($error = "reading");
      close PRIMEDIA;
      die "Error $error $PRIMEDIA" if $error;
    }
  }

  return $contents;
}

sub get_cursor($)
# Return the given cursor
{
  my $number = $_[0];
  my $cell = $CURSOR_HOME;

  # Count to the numbered cursor
  for ($_ = 0; defined($cell) && ($_ <= $number); $_++)
  {
     $cell = $ZZ{"$cell+d.2"};
  }
  die "No cursor $number" unless defined($cell);
  return $cell;
}

sub get_dimension($$)
# Get the dimension for a given cursor and direction
# Requires that each cursor have the current screen X, Y and Z axis
# dimension mappings linked +d.1 from the cursor cell
{
  my ($curs, $dir) = @_;
  die "Invalid direction $dir" unless ($dir =~ /^[LRUDIO]$/);

  $curs = $ZZ{"$curs+d.1"};
  if ($dir eq "L")
  {
     return reverse_sign($ZZ{$curs}); 
  }
  if ($dir eq "R")
  {
     return $ZZ{$curs}; 
  }
  $curs = $ZZ{"$curs+d.1"};
  if ($dir eq "U")
  {
     return reverse_sign($ZZ{$curs}); 
  }
  if ($dir eq "D")
  {
     return $ZZ{$curs}; 
  }
  $curs = $ZZ{"$curs+d.1"};
  if ($dir eq "I")
  {
     return reverse_sign($ZZ{$curs}); 
  }
  if ($dir eq "O")
  {
     return $ZZ{$curs}; 
  }
}

sub get_contained($)
# Return list of cells "contained" within a cell
# Performs a depth-first descend-only treewalk with loops broken
# +d.inside is depth, +d.contents is width.
{
  my %gen;
  my @stack;
  my $cell = $_[0];
  my $gen = 0;
  my ($index, $next, $start);

  $start = &get_lastcell($cell, "-d.contents");
  # If d.contents is not linked or is a loop, just use $cell
  $start = $cell if !defined($start) || defined($ZZ{"$start-d.contents"});

  # Mark the first generation
  $index = $start;
  do
  {
    $gen{$index} = 0;
    $index = $ZZ{"$index+d.contents"};
  }
  until (!defined($index) || ($index eq $start));

  undef @_;
  while (defined($cell))
  {
    push @_, $cell;

    if (($next = $ZZ{"$cell+d.inside"}) && 
        (!defined($gen{$next}) || ($gen{$next} > $gen)))
    {
      push @stack, $cell, $start;

      $start = &get_lastcell($next, "-d.contents");
      # If d.contents is not linked or is a loop, just use $next
      $start = $next if !defined($start) || defined($ZZ{"$start-d.contents"});

      # Mark the new generation
      $gen++;
      $index = $start;
      do
      {
        $gen{$index} = $gen;
        $index = $ZZ{"$index+d.contents"};
      }
      until (!defined($index) || ($index eq $start));

      $cell = $start;
    }
    else # Can't go +d.inside, so find somewhere to go +d.contents
    {
      while (defined($cell) &&
             ((!defined($cell = $ZZ{"$cell+d.contents"})) ||
              ($cell eq $start)))
      {
        $start = pop @stack;
        $cell = pop @stack;
      }
      $gen = $gen{$cell} if defined($cell);
    }
  }
  return @_;
}


#
# Functions that operate on links between cells
# Named link_*
#
sub link_break($$$)
# Break a link between two cells in a given dimension.
# This should be the only way links are ever broken to ensure consistency.
{
  my ($cell1, $cell2, $dim) = @_;
  die "No cell $cell1" unless defined $ZZ{"$cell1"};
  die "No cell $cell2" unless defined $ZZ{"$cell2"};
  die "Invalid direction $dim" unless ($dim =~ /^[+-]/);

  delete($ZZ{"$cell1$dim"});
  delete($ZZ{$cell2 . &reverse_sign($dim)});
}

sub link_make($$$)
# Make a link between two cells in a given dimension.
# This should be the only way links are ever made to ensure consistency.
{
  my ($cell1, $cell2, $dim) = @_;
  die "No cell $cell1" unless defined($ZZ{"$cell1"});
  die "No cell $cell2" unless defined($ZZ{"$cell2"});
  die "Invalid direction $dim" unless ($dim =~ /^[+-]/);
  my $back = &reverse_sign($dim);
  die "$cell1 already linked" if defined($ZZ{"$cell1$dim"});
  die "$cell2 already linked" if defined($ZZ{"$cell2$back"});

  $ZZ{"$cell1$dim"} = $cell2;
  $ZZ{"$cell2$back"} = $cell1;
}


#
# Functions that operate on individual cells
# Named cell_*
#
sub cell_create($)
# Create a new cell and optionally edit it
{
  my $edit = $_[0];
  my ($curs, $dir) = &input_get_direction();
  if ($curs)
  {
    &atcursor_insert($curs, $dir);
    &cursor_move_direction($curs, $dir);
    &atcursor_edit($curs) if $edit;
  }
}

sub cell_insert($$$)
# Insert a cell next to another cell along a given dimension
#
#           Original state                             New state
#           --------------                           -------------
#           $cell1---next
#           $cell2---$cell3                $cell2---$cell1---($cell3 or next)
{
  my ($cell1, $cell2, $dim) = @_;
  die "No cell $cell1" unless defined($ZZ{"$cell1"});
  die "No cell $cell2" unless defined($ZZ{"$cell2"});
  die "Invalid direction $dim" unless ($dim =~ /^[+-]/);
  my $cell3 = $ZZ{"$cell2$dim"};

  # Can't insert if $cell1 has inappropriate neighbours
  if (defined($ZZ{$cell1 . &reverse_sign($dim)}) ||
      ((defined($ZZ{"$cell1$dim"}) && defined($cell3))))
  { 
     &user_error(1, "$cell1 $dim $cell2"); 
  }
  else
  {
    if (defined($cell3))
    {
       &link_break($cell2, $cell3, $dim);
       &link_make($cell1, $cell3, $dim);
    }
    &link_make($cell2, $cell1, $dim);
  }
}

sub cell_excise($$)
# Remove a cell from a given dimension
{
  my ($cell, $dim) = @_;
  die "No cell $cell" unless defined $ZZ{"$cell"};
  my $prev = $ZZ{"$cell-$dim"};
  my $next = $ZZ{"$cell+$dim"};

  &link_break($cell, $prev, "-$dim") if defined($prev);
  &link_break($cell, $next, "+$dim") if defined($next);
  &link_make($prev, $next, "+$dim") if defined($prev) && defined($next);
}

#
# Functions that operate on the cursor cell
# Named cursor_*
#
sub cursor_move_dimension($$)
# Move cursor along a given dimension
#
#                 Original state                  New state
#                 --------------                -------------
# | +d.cursor         old-----new                 old-----new
# V dimension          |       |                  |       |
#                 XXX         YYY                 XXX     YYY
#                  |                          |       |
#                $curs                         ZZZ    $curs
#                  |
#                 ZZZ
#
# NOTE: If there are many cursors it would be more efficient to insert the
# cursor next to "new", but Ted prefers the visualisation that the most
# recent cursor is the one furthest along the cursor dimension.
{
  my ($curs, $dim) = @_;
  die "Invalid direction $dim" unless ($dim =~ /^[+-]/);
  my $cell = &get_lastcell($curs, "-d.cursor");

  # Don't bother if there's nowhere to go
  return if (!defined($_ = $ZZ{"$cell$dim"}) ||
             ($_ == $cell) || defined $ZZ{"$_-d.cursor"});

  # Now move the cursor
  $cell = &get_lastcell($_, "+d.cursor");
  &cell_excise($curs, "d.cursor");
  &cell_insert($curs, $cell, "+d.cursor");

  foreach (@Window_Dirty)
  {
     $_ = $TRUE;
  }
}

sub cursor_jump($)
# Jump cursor to $Input_Buffer
{
  my $curs = $_[0];
  $Input_Buffer = 0 if !defined $Input_Buffer;

  # Must jump to a valid non-cursor cell
  if (!defined $ZZ{$Input_Buffer} || defined $ZZ{"$Input_Buffer-d.cursor"})
  {
     &user_error(2, $Input_Buffer);
  }
  else
  {
    # Move the cursor
    &cell_excise($curs, "d.cursor");
    &cell_insert($curs, &get_lastcell($Input_Buffer, "+d.cursor"), "+d.cursor");

    foreach (@Window_Dirty)
    {
       $_ = $TRUE;
    }
  }
  undef $Input_Buffer;
  $Window_Dirty[0] = $TRUE;
}

sub cursor_move_direction($$)
# Move given cursor in a given direction
{
  my $curs = &get_cursor($_[0]);

  &cursor_move_dimension($curs, &get_dimension($curs, $_[1]));
}

#
# Functions that operate on the cell under a cursor
# Named atcursor_*
#
sub atcursor_execute($)
# Execute the contents of a progcell under a given cursor
{
  my $cell;
  foreach $cell (&get_contained(&get_lastcell(&get_cursor($_[0]), "-d.cursor")))
  {
#    $_ = $ZZ{&get_lastcell($cell, "-d.clone")};
    $_ = &get_cell_contents($cell);
    $@ = "Cell does not start with #";	# Error in case eval isn't done
    if (/^#/) {
      $DB_Ref->sync();
      $Command_Count = 0;   # Write cached data to file
      eval;
    }
    last if $@;
  }
  chomp $@;
  if ($@) {
     &user_error(3, $@);
     return 0;
  }

  return 1;
}

sub atcursor_clone($)
# Create a new clone cell at a given cursor
{
  my $curs = &get_cursor($_[0]);
  my $cell = &get_lastcell($curs, "-d.cursor");

  my $new = $ZZ{"n"}++;
  $ZZ{$new} = "Clone of $cell";
  &cell_insert($new, $cell, "+d.clone");
  &cursor_move_dimension($curs, "+d.clone");

  foreach (@Window_Dirty)
  {
     $_ = $TRUE;
  }
}

sub atcursor_mark($)
# mark the current cell
{
  my $cell = $_[0];

  my $new = $ZZ{"n"}++;
  $ZZ{$new} = "m.1";
  &cell_insert($new, $cell, "+d.mark");

  foreach (@Window_Dirty)
  {
     $_ = $TRUE;
  }
}

sub atcursor_insert($$)
# Insert a new cell at a given cursor in a given direction
{
  my $curs = &get_cursor($_[0]);
  my $dim = &get_dimension($curs, $_[1]);

  # Can't insert if it's the clone dimension
  if ($dim =~ /^[+-]d\.clone$/)
  {
     &user_error(8);
     return 0;
  }

  # Can't insert if it's the cursor dimension
  if ($dim =~ /^[+-]d\.cursor$/)
  {
     &user_error(9);
     return 0;
  }

  my $new = $ZZ{"n"}++;
  $ZZ{$new} = "$new";                # Initial contents will be cell number
  &cell_insert($new, &get_lastcell($curs, "-d.cursor"), $dim);

  foreach (@Window_Dirty)
  {
     $_ = $TRUE;
  }
}

sub atcursor_delete($)
# Delete the cell under a given cursor
{
  my $curs = &get_cursor($_[0]);
  my $cell = &get_lastcell($curs, "-d.cursor");
  my $index = $ZZ{"$CURSOR_HOME+d.1"}; # Dimension list is +d.1 from Cursor home
  my $done = $FALSE;
  my $dim;
  my $neighbour;

  # Pass the torch if this cell has clone(s)
  if (!defined $ZZ{"$cell-d.clone"} && ($_ = $ZZ{"$cell+d.clone"}))
  {
     $ZZ{$_} = $ZZ{$cell};
  }

  while (!$done)
  {
    $dim = $ZZ{$index};

    # Try and find any valid non-cursor neighbour
    $neighbour = $_ unless defined $neighbour
      || ((!defined($_ = $ZZ{"$cell-$dim"}) || 
           ($_ eq $cell) || 
           defined $ZZ{"$_-d.cursor"}) &&
          (!defined($_ = $ZZ{"$cell+$dim"}) || 
           ($_ eq $cell) || 
           defined $ZZ{"$_-d.cursor"}));

    # Excise $cell from dimension $dim
    &cell_excise($cell, $dim);
  }
  continue
  {
    # Proceed to the next dimension
    $index = $ZZ{"$index+d.2"};
    die "Dimension list broken" unless defined $index;
    $done = ($index == $ZZ{"$CURSOR_HOME+d.1"});
  }
  $neighbour = 0 unless defined $neighbour;

  # Move $cell to the deleted stack
  &cell_insert($cell, $DELETE_HOME, "+d.2");

  # Move the cursor to any $neighbour or home if none
  $cell = &get_lastcell($neighbour, "+d.cursor");
  &cell_insert($curs, $cell, "+d.cursor");

  foreach (@Window_Dirty)
  {
     $_ = $TRUE;
  }
}

sub atcursor_hop(@)
# Hop a cell at a given cursor in a given direction
#
#           Original state                             New state
#           --------------                           -------------
# $prev---$cell---$neighbour---$next        $prev---$neighbour---$cell---$next
{
  my $curs = &get_cursor($_[0]);
  my $dim = &get_dimension($curs, $_[1]);

  # Not in the Cursor dimension!
  return if $dim eq "d.cursor";

  my $cell = &get_lastcell($curs, "-d.cursor");
  my $neighbour = $ZZ{"$cell$dim"};
  if (!defined $neighbour)
  { 
     &user_error(4, "$cell$dim"); 
  }
  else
  {
    my $prev = $ZZ{$cell . &reverse_sign($dim)};
    my $next = $ZZ{"$neighbour$dim"};

    &link_break($cell, $neighbour, $dim);
    if (defined $prev)
    {
      &link_break($prev, $cell, $dim);
      &link_make($prev, $neighbour, $dim);
    }
    if (defined $next)
    {
      &link_break($neighbour, $next, $dim);
      &link_make($cell, $next, $dim);
    }
    &link_make($neighbour, $cell, $dim);
    
    foreach (@Window_Dirty)
    {
       $_ = $TRUE;
    }
  }
}

sub atcursor_edit($)
# Invoke an external text editor to edit the cell under a given cursor
{
  my $cell = &get_lastcell(&get_lastcell(&get_cursor($_[0]), "-d.cursor"), "-d.clone");

  # Save $cell contents in a temporary file
  open(TEMP, ">$TEMP_FILE") || die "Can't open \"$TEMP_FILE\": $!\n";
  print TEMP $ZZ{$cell}, "\n";
  close(TEMP);

  # Invoke the editor on the temporary file
  &display_close();
  if (!system("$EDITOR $TEMP_FILE"))
  {
    undef $/;                # Read entire file into cell
    open(TEMP, "<$TEMP_FILE") || die "Can't open \"$TEMP_FILE\": $!\n";
    $_ = <TEMP>;
    close(TEMP);
    $/="";                # Remove trailing blank lines
    chomp;
    $ZZ{$cell} = $_;
  }
  &display_open();
  unlink $TEMP_FILE;
}

sub atcursor_make_link($$)
# Link two cells along a given dimension
{
  my $curs = &get_cursor($_[0]);
  my $dim = &get_dimension($curs, $_[1]);

  # Not in the Cursor dimension!
  return if $dim eq "d.cursor";

  # If no cell number selected, just move the cursor instead
  if (!defined $Input_Buffer)
  {
     &cursor_move_dimension($curs, $dim);
  }
  elsif (!defined $ZZ{$Input_Buffer})
  {
     &user_error(5, $Input_Buffer);
  }
  else
  {
    &cell_insert($Input_Buffer, &get_lastcell($curs, "-d.cursor"), $dim);
    undef $Input_Buffer;

    foreach (@Window_Dirty)
    {
       $_ = $TRUE;
    }
  }
}

sub atcursor_break_link(@)
# Break a link in a given dimension
{
  my $curs = &get_cursor($_[0]);
  my $dim = &get_dimension($curs, $_[1]);
  my $cell = &get_lastcell($curs, "-d.cursor");

  # Not in the Cursor dimension!
  return if $dim eq "d.cursor";

  # First check that there is an existing link
  if (!defined($_ = $ZZ{"$cell$dim"}))
  {
     &user_error(6, "$cell$dim");
  }
  else
  {
    &link_break($cell, $_, $dim);

    foreach (@Window_Dirty)
    {
       $_ = $TRUE;
    }
  }
}

#
# Functions that operate on groups of cells in a given dimension
# Named: cells_*
# (bad name?)
#
sub cells_import(@)
# Import cells from a text file
{
  my $curs = &get_cursor($_[0]);
  my $cell = &get_lastcell($curs, "-d.cursor");
  my $dim = &get_dimension($curs, $_[1]);

  # Not in the Cursor dimension!
  return if $dim eq "d.cursor";

  # Invoke the editor on the temporary file
  &display_close();
  system("$EDITOR $TEMP_FILE");
  &display_open();

  $/ = "";                                # Blank lines are separators
  if (open(TEMP, "<$TEMP_FILE"))
  {
    while(<TEMP>)
    {
      chomp;
      if ($_)
      {
        my $new = $ZZ{"n"}++;
        s/\n\|/\n/g;                        # Strip off protective bars
        $ZZ{$new} = $_;
        &cell_insert($new, $cell, $dim);
        $cell = $new;
      }
    }
    close(TEMP);

    foreach (@Window_Dirty)
    {
       $_ = $TRUE;
    }
  }

  unlink $TEMP_FILE;
}

sub cells_export(@)
# Export cells to a text file
{
  my $curs = &get_cursor($_[0]);
  my $dim = &get_dimension($curs, $_[1]);
  my $start = &get_lastcell($curs, "-d.cursor");
  my $index = $start;
  my $loop = $FALSE;

  open(TEMP, ">$TEMP_FILE") || die "Can't open \"$TEMP_FILE\": $!\n";

  while (defined $index && !$loop)
  {
    if (!defined $ZZ{"$index-d.cursor"})        # Don't export cursor cells
    {
      my $cell;

      foreach $cell (&get_contained($index))
      {
        print TEMP "\n|" unless $cell eq $index; # Separate contained cells
        $_ = $ZZ{&get_lastcell($cell, "-d.clone")};
#	$_ = &get_cell_contents($cell);
        chomp;
        s/\n/\n\|/g;     # Protect blank lines in cells
        print TEMP;
      }
      print TEMP "\n\n";
    }
    $index = $ZZ{"$index$dim"};
    $loop = $index eq $start;
  }
  close(TEMP);

  # Invoke the editor on the temporary file
  &display_close();
  system("$EDITOR $TEMP_FILE");
  &display_open();
  unlink $TEMP_FILE;
}


#
# Functions that operate on the whole screen
# Named: display_*
#
sub display_open()
# (Re)initialise display
{
  initscr();                # Initialise Curses
  cbreak();                # Disable line buffering
  noecho();                # Disable echo
  intrflush(0);        # Disable buffer flush on interrupts
  keypad(1);        # Enable function keys
  timeout(0);                # Use non-blocking input
  nonl();                # Disable CRLF conversion
  leaveok(1);        # Disable cursor
  
  $Display_Has_Colour = has_colors(); # For Curses to know if it can use colour
  $LOTS_OF_COLOURS = $FALSE unless $Display_Has_Colour;
  # set up for colours
  if($Display_Has_Colour)
  {
    start_color();
    init_pair(1, COLOR_WHITE, COLOR_BLACK);
    init_pair(2, COLOR_YELLOW, COLOR_BLACK);
    init_pair(3, COLOR_BLACK, COLOR_WHITE);
    init_pair(4, COLOR_BLUE, COLOR_BLACK);
    init_pair(5, COLOR_GREEN, COLOR_BLACK);
    init_pair(6, COLOR_BLACK, COLOR_GREEN);
    init_pair(7, COLOR_BLACK, COLOR_BLUE);
    init_pair(8, COLOR_YELLOW, COLOR_GREEN);
    init_pair(9, COLOR_BLACK, COLOR_BLUE);
  }
  
  &display_clear();                # Clear screen
  &display_refresh();

  if ($USE_STATUSBAR)
  {
     $Window[0] = newwin($LINES - 1, $COLS / 2, 0, 0);
     $Window[1] = newwin($LINES - 1, $COLS / 2, 0, $COLS / 2);

     $Status = newwin(1, $COLS, $LINES - 1, 0);
     bkgd($Status, $Display_Has_Colour ? COLOR_PAIR(3) : A_REVERSE);
     &display_refresh($Status);
  } else {
     $Window[0] = newwin($LINES, $COLS / 2, 0, 0);
     $Window[1] = newwin($LINES, $COLS / 2, 0, $COLS / 2);
  }

  $Cursor_Len = int(($COLS - 1) / ($CELLS_PER_WIN * 2));

  # Mark all windows dirty to ensure they all get redrawn
  # We can't use foreach (@Window_Dirty) because the first time this code
  # is executed @Window_Dirty is empty and this code initialises it.
  for ($_ = 0; $_ <= $#Window; $_++)
  {
     $Window_Dirty[$_] = $TRUE;
  }
}

sub display_close()
# Free all windows and exit Curses
{
  foreach (@Window)
  {
     delwin($_);
  }
  delwin($Status) if $USE_STATUSBAR;
  endwin();
}

sub display_resize()
# Handle display size changes
{
# NOTE: Calling initscr() more than once is non-portable.  It seems to cause
#        strange visual effects, and sometimes even segfaults!  The commented
#        out code below is the proper way to do it, but unfortunately modifying
#        $LINES and $COLS doesn't seem to get passed back to curses by the Perl
#        Curses interface.  :-(
  &display_close();
#  my $getwinsz = &TIOCGWINSZ || die "No TIOCGWINSZ";
#  my $winsz = "ss";
#  ioctl(STDIN, $getwinsz, $winsz) || die "TIOCGWINSZ failed";
#  ($LINES, $COLS) = unpack("ss", $winsz);
  &display_open();
  $Display_Resized = $FALSE;
}

sub display_clear()
{
   clear();
}

sub display_refresh()
{
   my $win = shift;
   if ($win)
   {
      refresh($win);
   }
   else
   {
      refresh();
   }
}

#
# Functions that draw things in a given window.  The windows are a
# logical thing that may be any subsection of the display, or all
# of it.
# Named: window_draw_*
#
sub window_draw($$)
# Redraw given display window
{
  my ($number, $full) = @_;
  # Local variables
  my $win = $Window[$number];
  my $curs = &get_cursor($number);
  my $name = $ZZ{$curs};
  my $cell = &get_lastcell($curs, "-d.cursor");
  $curs = $ZZ{"$curs+d.1"};
  my $right = $ZZ{$curs};
  $curs = $ZZ{"$curs+d.1"};
  my $down = $ZZ{$curs};
  $curs = $ZZ{"$curs+d.1"};
  my $out = $ZZ{$curs};
  my $raster = $ZZ{$ZZ{"$curs+d.1"}};
  my $quad = ($raster =~ /Q$/);
  my $row = int(($LINES - 1) / 2);
  my $col = int($COLS / 4) - int($Cursor_Len / 2);

  # Draw window border, title and current cell number
  if ($full)
  {
    erase($win);
    attrset($win, A_NORMAL);
    box($win, 0, 0);
    addstr($win, 0, int($COLS / 4) - int(length($name) / 2) - 6,
           " $name Window ($raster) ");
    if ($USE_STATUSBAR) {
       addstr($win, $LINES - 2, int($COLS / 2) - length($cell) - 3, " $cell ");
    } else {
       addstr($Window[0], $LINES - 1, 1, " $Input_Buffer ")
         if (defined $Input_Buffer) && ($number == 0);
       addstr($win, $LINES - 1, int($COLS / 2) - length($cell) - 3, " $cell ");
    }
  }

  # Display window contents.
  if ($raster =~ /^H/)
  { &window_draw_Hraster($number, $cell, $row, $col, $right, $down, $quad, $full); }
  else
  { &window_draw_Iraster($number, $cell, $row, $col, $right, $down, $quad, $full); }

  attrset($win, A_BOLD);
  # Display clone flag if we aren't using lots of colours
  if (!$LOTS_OF_COLOURS && &is_clone($cell))
  { addch($win, $row - 1, $col, "c"); }

  # Display dimension guide
  addstr($win, 1, 1, "+---> $right ");
  addstr($win, 2, 1, "|\\   ");
  addstr($win, 3, 1, "|  \\| $out ");
  addstr($win, 4, 1, "V  -+ ");
  addstr($win, 5, 1, "$down ");
  attrset($win, A_BOLD);

  $Window_Dirty[$number] = $FALSE if $full;
  &display_refresh($win);
}

sub window_draw_Iraster($$$$$$$$)
# Horizontal ("I raster") window redraw starting at cell, row and column
{
  my ($number, $cell, $row, $col, $right, $down, $quad, $full) = @_;
  my $win = $Window[$number];
  my $left = &reverse_sign($right);
  my $up = &reverse_sign($down);
  my ($y, $i, $j);
  my $contents = "";
  my $width = ($COLS / 2 - 1) - $col;

  if ($quad)
  {
    @_ = &get_contained($cell);
    do
    {
      $contents .= substr(&get_cell_contents(shift), 0, $width * $row) . "\n";
    }
    until (($#_ < 0) || (length($contents) >= $width * $row));
    $_ = &wordbreak($contents, $width);
    $contents = substr($contents, length);
    &window_draw_cell_contents_wide($win, $_, $row, $col, $width);
  }
  else
  {
    &window_draw_cell($win, $cell, $row, $col);
    &window_draw_cells_horizontal($win, $cell, $row, $col, $right, 1);
  }

  &window_draw_cells_horizontal($win, $cell, $row, $col, $left, -1);
  return unless $full;

  # Draw up to half a screen each above and below if necessary
  for ($y = 1, $i = $j = $cell;
       (defined $i || defined $j || $quad) && ($y * 2 < $row);
       $y++)
  {
    # Find the next cell above, if any
    if (defined $i && defined ($i = $ZZ{"$i$up"}))
    {
      addch($win, $row - $y * 2 + 1, $col + int($Cursor_Len / 2), ACS_VLINE);
      &window_draw_cell($win, $i, $row - $y * 2, $col);
      &window_draw_cells_horizontal($win, $i, $row - $y * 2, $col, $left, -1);
      &window_draw_cells_horizontal($win, $i, $row - $y * 2, $col, $right, 1);
    }

    # Find the next cell below, if any
    if (defined $j && defined ($j = $ZZ{"$j$down"}))
    {
      if (!$quad)
      {
        addch($win, $row + $y * 2 - 1, $col + int($Cursor_Len / 2), ACS_VLINE);
        &window_draw_cell($win, $j, $row + $y * 2, $col);
        &window_draw_cells_horizontal($win, $j, $row + $y * 2, $col, $right, 1);
      }
      &window_draw_cells_horizontal($win, $j, $row + $y * 2, $col, $left, -1);
    }
  }

  if ($quad)
  {
    # Draw the remainder of the cell contents
    for ($y = 1; $y < $row; $y++)
    {
      $_ = &wordbreak($contents, $width);
      $contents = substr($contents, length);
      &window_draw_cell_contents_wide($win, $_, $row + $y, $col, $width);
    }
  }
}

sub window_draw_Hraster($$$$$$$$)
# Vertical ("H raster") window redraw starting at cell, row and column
{
  my ($number, $cell, $row, $col, $right, $down, $quad, $full) = @_;
  my $win = $Window[$number];
  my $left = &reverse_sign($right);
  my $up = &reverse_sign($down);
  my ($x, $i, $j);
  my $contents = "";
  my $width = ($COLS / 2 - 1) - $col;

  if ($quad)
  {
    @_ = &get_contained($cell);
    do
    {
      $contents .= substr(&get_cell_contents(shift), 0, $width * $row) . "\n";
    }
    until (($#_ < 0) || (length($contents) >= $width * $row));
    $_ = &wordbreak($contents, $width);
    $contents = substr($contents, length);
    &window_draw_cell_contents_wide($win, $_, $row, $col, $width);
  }
  else
  {
    &window_draw_cell($win, $cell, $row, $col);
    &window_draw_cells_vertical($win, $cell, $row, $col, $down, 1);
  }

  &window_draw_cells_vertical($win, $cell, $row, $col, $up, -1);
  return unless $full;

  # Draw up to half a screen each left and right if necessary
  for ($x = 1, $i = $j = $cell;
       (defined $i || defined $j) && ($x * 2 < $CELLS_PER_WIN); $x++)
  {
    # Find the next cell to the left, if any
    if (defined $i && defined ($i = $ZZ{"$i$left"}))
    {
      addch($win, $row, $col - ($x - 1) * $Cursor_Len - 1, ACS_HLINE);
      &window_draw_cell($win, $i, $row, $col - $x * $Cursor_Len);
      &window_draw_cells_vertical($win, $i, $row, $col - $x * $Cursor_Len, $up, -1);
      &window_draw_cells_vertical($win, $i, $row, $col - $x * $Cursor_Len, $down, 1);
    }

    # Find the next cell to the right, if any
    if (defined $j && defined ($j = $ZZ{"$j$right"}))
    {
      if (!$quad)
      {
        addch($win, $row, $col + $x * $Cursor_Len - 1, ACS_HLINE);
        &window_draw_cell($win, $j, $row, $col + $x * $Cursor_Len);
        &window_draw_cells_vertical($win, $j, $row, $col + $x * $Cursor_Len, $down, 1);
      }
      &window_draw_cells_vertical($win, $j, $row, $col + $x * $Cursor_Len, $up, -1);
    }
  }

  if ($quad)
  {
    # Draw the remainder of the cell contents
    for ($j = 1; $j < $row; $j++)
    {
      $_ = &wordbreak($contents, $width);
      $contents = substr($contents, length);
      &window_draw_cell_contents_wide($win, $_, $row + $j, $col, $width);
    }
  }
}

sub window_draw_cells_horizontal($$$$$$)
# Draw cells horizontally starting at cell, row and column
{
  my ($win, $cell, $row, $col, $dim, $sign) = @_;
  my $inc = $sign * $Cursor_Len;
  my $ofs = ($sign == 1) ? $Cursor_Len - 1 : -1;
  my $i = 1;

  # Draw at most half a screen of cells
  while (defined $cell && ($i++ < int(($CELLS_PER_WIN + 1) / 2)))
  {
    # Find the next cell to the left, if any
    if (defined ($cell = $ZZ{"$cell$dim"}))
    {
      addch($win, $row, $col + $ofs, ACS_HLINE);
      &window_draw_cell($win, $cell, $row, $col += $inc);
    }
  }
}

sub window_draw_cells_vertical($$$$$$)
# Draw cells vertically starting at cell, row and column
{
  my ($win, $cell, $row, $col, $dim, $sign) = @_;
  my $i = 1;

  # Draw at most half a screen of cells
  while (defined $cell && ($i++ < int(($LINES + 1) / 4)))
  {
    # Find the next cell above, if any
    if (defined ($cell = $ZZ{"$cell$dim"}))
    {
      addch($win, $row += $sign, $col + int($Cursor_Len / 2), ACS_VLINE);
      &window_draw_cell($win, $cell, $row += $sign, $col);
    }
  }
}

sub window_draw_cell($$$$)
# Draw cell at row and column
{
  my ($win, $cell, $row, $col) = @_;
  my $cursor = $ZZ{"$cell+d.cursor"} unless defined $ZZ{"$cell-d.cursor"};
  my $number = -1;
  my $content;

  while (defined $cursor)
  { $cursor = $ZZ{"$cursor-d.2"};  $number++; }
  ($content) = split(/\n/, &get_cell_contents($cell));	# Just the first line

  # Clone cells get a special colour in high-colour mode
  attron($win, COLOR_PAIR(2) | A_BOLD) 
    if $LOTS_OF_COLOURS && &is_clone($cell);
  # Colour/highlight the cursors
  attron($win, $Display_Has_Colour ? COLOR_PAIR(6) : A_UNDERLINE) if $number == 1;
  attron($win, $Display_Has_Colour ? COLOR_PAIR(7) : A_REVERSE) if $number > 1;
  addnstr($win, $row, $col,
          &window_draw_cell_contents($content, $Cursor_Len - 1), $Cursor_Len - 1);
  # End colour/highlight
  attrset($win, A_NORMAL);
}

sub window_draw_cell_contents($$)
# Return visualisation of one line of a cell space-padded to a given width
{
  my ($contents, $width) = @_;
  if (!defined $contents || ($contents eq ""))
  {
     $_ = " " x $width;
  }
  else
  {
    $contents =~ tr/\t/ /;        # Tab compression
    my $len = length($contents);

    # Trim and/or pad with trailing spaces as necessary
    if ($len == $width)
    {
       $_ = $contents;
    }
    elsif ($len > $width)
    {
       $_ = substr($contents, 0, $width);
    }
    else # $len < $width
    {
       $_ = substr($contents, 0, $len) . " " x ($width - $len);
    }
  }
}

sub window_draw_cell_contents_wide($$$$$)
# Draw wide cell showing given contents starting at row and column
{
  my ($win, $contents, $row, $col, $width) = @_;

  attrset($win, A_REVERSE);
  addnstr($win, $row, $col, &window_draw_cell_contents($contents, $width), $width);
  attrset($win, A_NORMAL);
}

#
# Status bar functions.
# Named: status_*
#
sub status_draw()
{
   my $text = shift;
 
   if ($USE_STATUSBAR)
   {
      erase($Status);
      attrset($Status, $Display_Has_Colour ? COLOR_PAIR(3) : A_REVERSE);
      #$text .=  " " x ($COLS - 10 - length($text)) .  $Input_Buffer if defined($Input_Buffer);
      addstr($Status, 0, 0, $text) if $text;
      addstr($Status, 0, $COLS - 10, $Input_Buffer) if defined($Input_Buffer);

      &display_refresh($Status);
   }
   #$Status_Dirty = $FALSE;  # XXX why doesn't that work?
}

#
# View functions.  These munge options which control how the
# window_draw_* functions work.
# Named: view_*
#
sub view_quadrant_toggle($)
# Toggle quadrant display style for given window
{
  my $cell = &get_cursor($_[0]);
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};

  if ($ZZ{$cell} =~ /Q$/)
  { $ZZ{$cell} = substr($ZZ{$cell}, 0, 1); }
  else
  { $ZZ{$cell} .= "Q"; }

  foreach (@Window_Dirty)
  { $_ = $TRUE; }
}

sub view_raster_toggle($)
# Toggle redraw style for given window
{
  my $cell = &get_cursor($_[0]);
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};
  $cell = $ZZ{"$cell+d.1"};

  # Toggle the value between "I" and "H"
  $ZZ{$cell} =~ tr/IH/HI/;

  foreach (@Window_Dirty)
  { $_ = $TRUE; }
}

sub view_rotate($$)
# Rotate dimensions of given cursor around given axis
{
  my ($number, $axis) = @_;
  my $curs = &get_cursor($number);
  die "Invalid axis $axis" unless $axis =~ /^[XYZ]$/;
  $curs = $ZZ{"$curs+d.1"};
  $curs = $ZZ{"$curs+d.1"} if $axis ne "X";
  $curs = $ZZ{"$curs+d.1"} if $axis eq "Z";
  my $dim = substr($ZZ{$curs}, 1);
  my $index = $ZZ{"$CURSOR_HOME+d.1"}; # Dimension list is +d.1 from Cursor home

  # Find the current dimension
  while ($ZZ{$index} ne $dim)
  {
    $index = $ZZ{"$index+d.2"};
    die "Dimension list broken" unless defined $index;
    die "Dimension $dim not found" if ($index == $ZZ{"$CURSOR_HOME+d.1"});
  }

  $ZZ{$curs} = substr($ZZ{$curs}, 0, 1) . $ZZ{$ZZ{"$index+d.2"}};
  $Window_Dirty[$number] = $TRUE;
}

sub view_flip($$)
# Invert sign of given cursor and dimension
{
  my ($number, $axis) = @_;
  my $curs = &get_cursor($number);
  $curs = $ZZ{"$curs+d.1"};
  $curs = $ZZ{"$curs+d.1"} if $axis ne "X";
  $curs = $ZZ{"$curs+d.1"} if $axis eq "Z";

  $ZZ{$curs} = &reverse_sign($ZZ{$curs});
  $Window_Dirty[$number] = $TRUE;
}

#
# Handle keyboard input.
# Named: input_*
#
sub input_get_any()
# Attempt to get any input
{
  my $key = getch();
  return $key eq -1 ? undef : $key;
}

sub input_get_direction()
# Attempt to get a direction key
{
  my $key;
  while (!defined($key = &input_get_any())) {}  # Idle until we get a key

  if ($_ = $Keymap_Directions{$key})
  { @_ = split //; }
  else
  {
    # The key isn't a direction key
    &user_error(0, $key);
    undef @_;
  }
}

sub input_process_digit($)
# Process numeric input from keyboard
{
  # If this is the first digit
  if (!defined $Input_Buffer)
  { $Input_Buffer = $_[0];  $Status_Dirty = $TRUE; }
  # Otherwise add another digit if the number isn't too large
  elsif ($Input_Buffer < 1000000000)
  { $Input_Buffer = $Input_Buffer * 10 + $_[0];  $Status_Dirty = $TRUE; }
  else
  { beep(); &display_refresh($Status); }
}

sub input_process_backspace()
# Handle backspace key
{
  if (defined $Input_Buffer)
  {
    # Remove the least significant digit if there's more than one digit
    if ($Input_Buffer > 9)
    { $Input_Buffer /= 10; }
    else # Otherwise remove the only digit
    { undef $Input_Buffer; }
    $Window_Dirty[0] = $TRUE;
  }
  else
  { beep(); &display_refresh($Status); }
}

#
# Handle signals and errors
#

sub catchsig()
# Handle fatal signals
{ $ZigZag_Terminated = $_[0]; }

sub catchwinch()
# Handle window size change signals
{ $Display_Resized = $_[0]; }

sub user_error($$)
# Handle user errors
{
   my $errno = shift;
   my $text = shift;

   # I feel like this next should be a global.  It's not.
   my @errors = (
      "Key is not a valid keystroke",
      "Cannot insert - invalid neigbours",
      "Cannot jump to invalid or cursor cell",
      "Error executing cell",
      "Cannot hop - no neigbours",
      "Cannot jump to cell that does not exist",
      "Cannot break link - none exists",
      "Error opening file",
      "Cannot insert cells in the clone dimension",
      "Cannot insert cells in the cursor dimension",
   );

   beep(); 
   if ($USE_STATUSBAR)
   {
      my $errmsg = "Error $errno: " . $errors[$errno] if $errno <= $#errors;
      if ($text)
      {
         &status_draw(substr("$errmsg ($text)", 0, $COLS));
      }
      else
      {
         &status_draw(substr("$errmsg", 0, $COLS));
      }
      &display_refresh($Status);
      $StatusTimer = 20;
   }
}


#
# Background functions, if any, can be executed here
#
sub idle()
{
  select(undef, undef, undef, 0.1);        # sleep for 1/10 second
  if (($StatusTimer-- <= 0) && $USE_STATUSBAR) {
     erase($Status);
     &display_refresh($Status);
  }
}


#
# Begin main.
#

# If there's a command line parameter, use it as the filename
my $Filename = shift || $FILENAME;
if (-e $Filename) {
   # we have an existing datafile - back it up!
   copy($Filename,"$Filename.bak");
   $DB_Ref = tie %ZZ, 'DB_File', $Filename, O_RDWR
     || die "Can't open data file \"$Filename\": $!\n";
} else {
   # no initial data file,  resort to initial geometry
   $DB_Ref = tie %ZZ, 'DB_File', $Filename, O_RDWR | O_CREAT
     || die "Can't create data file \"$Filename\": $!\n";
   %ZZ = &initial_geometry();
}

# Set the interrupt handlers
$SIG{INT} = $SIG{TERM} = \&catchsig;
$SIG{WINCH} = \&catchwinch;

&display_open();
eval
{
  my $key_pressed = &input_get_any();
  my $i;

  # Partially or fully redraw dirty windows
  for ($i = 0; $i <= $#Window_Dirty; $i++)
  {
     &window_draw($i, !defined($key_pressed)) if $Window_Dirty[$i];
  }
  if ($USE_STATUSBAR && $Status_Dirty)
  {
     &status_draw();
  }

  if (!defined($key_pressed))
  {
     &idle();
  }
  elsif ($key_pressed =~ /^\d$/)
  {
     &input_process_digit($key_pressed);
  }
  elsif ($_ = $Keymap_Directions{$key_pressed})
  {
     @_ = split //;
     &atcursor_make_link($_[0], $_[1]);
  }
  elsif ($_ = $Keymap{$key_pressed})
  {
     if (++$Command_Count > $COMMAND_SYNC_COUNT) {
        $DB_Ref->sync();
        $Command_Count = 0;
     }
     eval;
     die if $@;
  }
  else # The key isn't a digit, a direction key or in %Keymap
  {
     &user_error(0, $key_pressed);
  }
  &display_resize if $Display_Resized;
}
until ($@ || $ZigZag_Terminated);

&display_clear();
&display_refresh();
&display_close();
undef $DB_Ref;  # So untie doesn't complain
untie %ZZ;
die if $@;
print STDERR "Terminated by $ZigZag_Terminated signal\n" 
   if $ZigZag_Terminated;

#
# End.
#

