The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
#
# note - console notes management with database and encryption support.
# Copyright (C) 1999-2017 T.v.Dein   (see README for details!)
#
# 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; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# 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.
#
#                               - Thomas Linden <tom at linden dot at>
#
# latest version on:
# http://www.daemon.de/note/
#

use lib qw(blib/lib);

BEGIN {
  # works on unix or cygwin only!
  my $path = $0;
  $path =~ s#/[^/]*$##;
  unshift @INC, "$path/..";
}

use strict;
no strict 'refs';
use Getopt::Long;
use FileHandle;
use File::Spec;
use YAML;


#
# prototypes
#
sub usage;        # print usage message for us thumb userz :-)
sub find_editor;  # returns an external editor for use
sub output;       # used by &list and &display
sub C;            # print colourized 
sub num_bereich;  # returns array from "1-4" (1,2,3,4)
sub getdate;      # return pretty formatted day
sub new;          # crate new note
sub edit;         # edit a note
sub del;          # delete a note
sub display;      # display one or more notes
sub list;         # note-listing
sub help;         # interactive help screen
sub import;       # import from notedb-dump
sub display_tree; # show nice tree-view
sub tree;         # build the tree
sub print_tree;   # print the tree, contributed by Jens Heunemann <Jens dot Heunemann at consol dot de>. THX!
sub ticket;       # return a random string which is used as ticket number for new note entries

#
# globals
#
my (
    #
    # commandline options
    #
    $opt_, $opt_i, $opt_r, $opt_e, $opt_d, $opt_enc,
    $opt_s, $opt_t, $opt_T, $opt_l, $opt_L, $opt_c,
    $opt_D, $opt_I, $opt_o, $opt_h, $opt_n, $opt_v,

    #
    # set from commandline (or interactive)
    #
    $number, $searchstring, $dump_file, $ImportType, $NewType, $Raw, $TOPIC,

    #
    # configuration options
    %conf, %driver,

    #
    # processed colors
    #
    $BORDERC,  $_BORDERC, $NOTEC, $NUMC,  $_NUMC, $_NOTEC, $TIMEC,
    $_TIMEC, $TOPICC,  $_TOPICC,

    #
    # config presets
    #
    $DEFAULTDBNAME, $USER, $PATH, $CONF,

    #
    # internals
    #
    $TYPE, $mode, $NoteKey, %Color, @LastTopic, $timelen, $maxlen,
    $VERSION, $CurTopic, $CurDepth, $WantTopic, $db,
    $sizeof, %TP, $TreeType, $ListType, $SetTitle, $clearstring,
    @ArgTopics, $key, $typedef, @NumBlock, $has_nothing, @completion_topics, @completion_notes,
    @randomlist, $hardparams
   );


#
# DEFAULTS, allows one to use note without a config
# don't change them, instead use the config file!
#

%conf = (
     'numbercolor'       => 'blue',
     'bordercolor'       => 'black',
     'timecolor'         => 'black',
     'topiccolor'        => 'black',
     'notecolor'         => 'green',
     'alwaysinteractive' => 1,
     'keeptimestamp'     => 0,
     'readonly'          => 0,
     'shortcd'           => 1,
     'autoclear'         => 0,
     'maxlen'            => 'auto',
     'defaultlong'       => 0,
     'dbdriver'          => 'binary',
     'timeformat'        => 'DD.MM.YYYY hh:mm:ss',
     'usecolors'         => 0,
     'addticket'         => 0,
     'formattext'        => 0,
     'alwayseditor'      => 1,
     'useencryption'     => 0,
     'tempdirectory'     => File::Spec->tmpdir(),
     'topicseparator'    => '/',
     'printlines'        => 0,
     'cache'             => 0,
     'preferrededitor'   => '',
     'motd'              => ''
);

# these are not customizable at runtime!
$hardparams = "(readonly|maxlen|dbdriver|useencryption|cryptmethod)";
$CONF       = File::Spec->catfile($ENV{HOME}, ".noterc");
$USER       = getlogin || getpwuid($<); chomp $USER;
$TOPIC      = 1;
$VERSION    = "1.3.25";
$CurDepth   = 1;        # the current depth inside the topic "directory" structure...
$maxlen     = "auto";
$timelen    = 22;

@randomlist = ('a'..'z', 0..9, 'A'..'Z');

# colors available
# \033[1m%30s\033[0m
%Color = (            'black'         => '0;30',
                      'red'           => '0;31',
                      'green'         => '0;32',
                      'yellow'        => '0;33',
                      'blue'          => '0;34',
                      'magenta'       => '0;35',
                      'cyan'          => '0;36',
                      'white'         => '0;37',
                      'B'             => '1;30',
                      'BLACK'         => '1;30',
                      'RED'           => '1;31',
                      'GREEN'         => '1;32',
                      'YELLOW'        => '1;33',
                      'BLUE'          => '1;34',
                      'MAGENTA'       => '1;35',
                      'CYAN'          => '1;36',
                      'WHITE'         => '1;37',
                      'black_'        => '4;30',
                      'red_'          => '4;31',
                      'green_'        => '4;32',
                      'yellow_'       => '4;33',
                      'blue_'         => '4;34',
                      'magenta_'      => '4;35',
                      'cyan_'         => '4;36',
                      'white_'        => '4;37',
                      'blackI'        => '7;30',
                      'redI'          => '7;31',
                      'greenI'        => '7;32',
                      'yellowI'       => '7;33',
                      'blueI'         => '7;34',
                      'magentaI'      => '7;35',
                      'cyanI'         => '7;36',
                      'whiteI'        => '7;37',
                      'white_black'   => '40;37;01',
                      'bold'          => ';01',
                      'hide'          => '44;34'
         );

#
# process command line args
#
if ($ARGV[0] eq "") {
  $mode = "new";
}
elsif ($#ARGV == 0 && $ARGV[0] eq "-") {
  $mode = "new";
  $NewType = 1;            # read from STDIN until EOF
  shift;
  undef $has_nothing;
}
else {
  Getopt::Long::Configure( qw(no_ignore_case));
  GetOptions (
              "interactive|i!"    => \$opt_i, # no arg
              "config|c=s"        => \$opt_c, # string,  required
              "raw|r!"            => \$opt_r, # no arg
              "edit|e=i"          => \$opt_e, # integer, required
              "delete|d=s"        => \$opt_d, # integer, required
              "search|s=s"        => \$opt_s, # string,  required
              "tree|topic|t!"     => \$opt_t, # no arg
              "longtopic|T!"      => \$opt_T, # no arg
              "list|l:s"          => \$opt_l, # string,  optional
              "longlist|L:s"      => \$opt_L, # string,  optional
              "dump|Dump|D:s"     => \$opt_D, # string,  optional
              "import|Import|I:s" => \$opt_I, # string,  optional
              "overwrite|o!"      => \$opt_o, # no arg
              "help|h|?!"         => \$opt_h, # no arg
              "version|v!"        => \$opt_v, # no arg
              "encrypt=s"         => \$opt_enc, # string, required
             );
  $opt_n = shift;        # after that @ARGV contains eventually
  # a note-number
  # $opt_ is a single dash, in case of existence!
  #
  # determine mode
  #
  if ($opt_i) {
    $mode = "interactive";
  }
  elsif (defined $opt_l || defined $opt_L) {
    $mode = "list";
    if (defined $opt_l) {
      @ArgTopics = split /$conf{topicseparator}/, $opt_l;
    }
    else {
      $ListType = "LONG";
      @ArgTopics = split /$conf{topicseparator}/, $opt_L;
    }
    $CurDepth += $#ArgTopics + 1 if($opt_l || $opt_L);
    $CurTopic = $ArgTopics[$#ArgTopics];  # use the last element everytime...
  }
  elsif ($opt_t || $opt_T) {
    $mode = "tree";
    $mode = "display_tree";
    $TreeType = "LONG" if($opt_T);
  }
  elsif (defined $opt_s) {
    $mode = "search";
    $searchstring = $opt_s;
  }
  elsif ($opt_e) {
    $mode = "edit";
    $number = $opt_e;
  }
  elsif ($opt_d) {
    $mode = "del";
    $number = $opt_d;
  }
  elsif ($opt_enc) {
    $mode = "encrypt_passwd";
    $clearstring = $opt_enc;
  }
  elsif (defined $opt_D) {
    $mode = "dump";
    if (!$opt_) {
      if ($opt_D ne "") {
        $dump_file = $opt_D;
      }
      else {
        $dump_file = "note.dump.$$";
        print "no dumpfile specified, using $dump_file.\n";
      }
    }
    else {
      $dump_file = "-";        # use STDIN
    }
  }
  elsif (defined $opt_I) {
    $mode = "import";
    if (!$opt_) {
      if ($opt_I ne "") {
        $dump_file = $opt_I;
      }
      else {
        print "Import-error! No dump_file specified!\n";
        exit(1);
      }
    }
    else {
      $dump_file = "-";
    }
  }
  elsif ($opt_v) {
    print "This is note $VERSION by Thomas Linden <tom at linden dot at>.\n";
    exit(0);
  }
  elsif ($opt_h) {
    &usage;
  }
  else {
    if ($opt_c && $mode eq "" && !$opt_n) {
      $mode = "new";
    }
    elsif ($opt_c && $mode eq "") {
      $mode = "";        # huh?!
    }
    else {
      $has_nothing = 1;
    }
  }
  ### determine generic options
  if ($opt_n =~ /^[\d+\-?\,*]+$/) {
    # first arg is a digit!
    if ($mode eq "") {
      $number = $opt_n;
      $mode = "display";
      undef $has_nothing;
    }
    else {
      print "mode <$mode> does not take a numerical argument!\n";
      exit(1);
    }
  }
  elsif ($opt_n ne "") {
    print "Unknown option: $opt_n\n";
    &usage;
  }
  if ($opt_r) {
    $Raw = 1;
  }
  if ($opt_o) {
    $ImportType = "overwrite";
    if (!$opt_I) {
      print "--overwrite is only suitable for use with --import!\n";
      exit(1);
    }
  }
  #####
}
if ($has_nothing && $mode eq "") {
  &usage;
}


# read the configfile.
$CONF = $opt_c if($opt_c);    # if given by commandline, use this.
if (-e $CONF) {
  &getconfig($CONF);
}
elsif ($opt_c) {
  # only wrong, if specified by commandline! else use default values!
  print STDERR "Could not open \"$CONF\": file does not exist or permission denied!\n";
  exit(1);
}

# directly jump to encrypt, 'cause this sub does
# not require a database connection
if ($mode eq "encrypt_passwd") {
  &encrypt_passwd;
  exit;
}

# Always interactive?
if ($conf{alwaysinteractive} && $mode ne "dump" && $mode ne "import") {
  $mode = "interactive";
}

# OK ... Long-Listing shall be default ... You wanted it!!!
if ($conf{defaultlong}) {
  # takes only precedence in commandline mode
  $ListType="LONG";
}




# calculate some constants...
$BORDERC  = "<$conf{bordercolor}>";
$_BORDERC = "</$conf{bordercolor}>";
$NUMC     = "<$conf{numbercolor}>";
$_NUMC    = "</$conf{numbercolor}>";
$NOTEC    = "<$conf{notecolor}>";
$_NOTEC   = "</$conf{notecolor}>";
$TIMEC    = "<$conf{timecolor}>";
$_TIMEC   = "</$conf{timecolor}>";
$TOPICC   = "<$conf{topiccolor}>";
$_TOPICC  = "</$conf{topiccolor}>";

$NoteKey  = $conf{topicseparator} . "notes" . $conf{topicseparator};




# default permissions on new files (tmp)
umask 077;


# load the parent module
&load_driver(1);

# check wether the user wants to use encryption:
if ($conf{useencryption} && $NOTEDB::crypt_supported == 1) {
  if ($conf{cryptmethod} eq "") {
    $conf{cryptmethod} = "Crypt::IDEA";
  }
  if (!exists $ENV{'NOTE_PASSWD'}) {
    print "note password: ";
    eval {
      local($|) = 1;
      local(*TTY);
      open(TTY,"/dev/tty") or die "No /dev/tty!";
      system ("stty -echo </dev/tty") and die "stty failed!";
      chomp($key = <TTY>);
      print STDERR "\r\n";
      system ("stty echo </dev/tty") and die "stty failed!";
      close(TTY);
    };
    if ($@) {
      $key = <>;
    }
  }
  else {
    $key = $ENV{'NOTE_PASSWD'};
  }
  chomp $key;
  if ($conf{dbdriver} eq "mysql") {
    eval {
      require Crypt::CBC;
      my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
      # decrypt the dbpasswd, if it's encrypted!
      $driver{mysql}->{dbpasswd}  =
        $cipher->decrypt(unpack("u", $driver{mysql}->{dbpasswd})) if($driver{mysql}->{encrypt_passwd});
      &load_driver();
    };
    die "Could not connect to db: $@!\n" if($@);
  }
  else {
    &load_driver();
  }
  $db->use_crypt($key,$conf{cryptmethod});
  undef $key;
  # verify correctness of passwd
  my ($cnote, $cdate) = $db->get_single(1);
  if ($cdate ne "") {
    if ($cdate !~ /^\d+\.\d+?/) {
      print "access denied.\n"; # decrypted $date is not a number!
      exit(1);
    }
  } #else empty database!
}
elsif ($conf{useencryption} && $NOTEDB::crypt_supported == 0) {
  print STDERR "WARNING: You enabled database encryption but neither Crypt::CBC\n";
  print STDERR "WARNING: or Crypt::$conf{cryptmethod} are installed! Please turn\n";
  print STDERR "WARNING: off encryption or install the desired modules! Thanks!\n";
  exit 1;
}
else {
  # as of 1.3.5 we do not fall back to cleartext anymore
  # I consider this as unsecure, if you don't, fix your installation!

  &load_driver();
  $db->no_crypt;

  # does: NOTEDB::crypt_supported = 0;
  my %all = $db->get_all();
  if(scalar keys %all > 0) {
    my $id = (keys %all)[0];
    if($all{$id}->{date} !~ /^\d+\.\d+?/) {
      print "notedb seems to be encrypted!\n";
      exit(1);
    }
  }
}


# do we use the db cache?
if ($conf{cache}) {
  $db->use_cache();
}


# add the backend version to the note version:
$VERSION .= ", " . $conf{dbdriver} . " " . $db->version();


# main loop: ###############
&$mode;
exit(0);
################## EOP ################














############ encrypt a given password ##############
sub encrypt_passwd {
  my($key, $crypt_string);
  print "password: ";
  eval {
    local($|) = 1;
    local(*TTY);
    open(TTY,"/dev/tty") or die "No /dev/tty!";
    system ("stty -echo </dev/tty") and die "stty failed!";
    chomp($key = <TTY>);
    print STDERR "\r\n";
    system ("stty echo </dev/tty") and die "stty failed!";
    close(TTY);
  };
  if ($@) {
    $key = <>;
  }
  chomp $key;
  eval {
    require Crypt::CBC;
    my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
    $crypt_string  = pack("u", $cipher->encrypt($clearstring));
  };
  if ($@) {
    print "Something went wrong: $@\n";
    exit 1;
  }
  else {
    print "Encrypted password:\n$crypt_string\n";
  }
}
############################### MOTD ##################################
sub motd {
  my($N,$match,$note,$date,$num);
  # display a configured motd note, if any
  ($note, $date) = $db->get_single($conf{motd});
  if ($note) {
    print "\n\n$note\n\n";
  }
}

############################### DISPLAY ##################################
sub display {
    my($N,$match,$note,$date,$num);
    # display a certain note
    print "\n";
    &num_bereich;        # get @NumBlock from $numer
    my $count = scalar @NumBlock;
    foreach $N (@NumBlock) {
      ($note, $date) = $db->get_single($N);
      if ($note) {
    if ($Raw) {
      print "$N\n$date\n$note\n\n";
    }
    else {
      output($N, $note, $date, "SINGLE", $count);
      print "\n";
    }
    $match = 1;
      }
      $count--;
    }
    if (!$match) {
      print "no note with that number found!\n";
    }
  }

############################### SEARCH ##################################
sub search {
    my($n,$match,$note,$date,$num,%res);
    if ($searchstring eq "") {
      print "No searchstring specified!\n";
    }
    else {
      print "searching the database $conf{dbname} for \"$searchstring\"...\n\n";

      %res = $db->get_search($searchstring);
      my $nummatches = scalar keys %res;
      &determine_width;
      foreach $num (sort { $a <=> $b } keys %res) {
        if ($nummatches == 1) {
          output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "SINGLE");
        }
        else {
          output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "search");
        }
        $match = 1;
      }
      if (!$match) {
        print "no matching note found!\n";
      }
      print "\n";
    }
  }


############################### LIST ##################################
sub list {
    my(@topic,@RealTopic, $i,$t,$n,$num,@CurItem,$top,$in,%res);
    if ($mode ne "interactive" && !$Raw) {
      print "\nList of all existing notes:\n\n";
    }
    else {
      print "\n";
    }

    # list all available notes (number and firstline)
    %res = $db->get_all();

    if ($TOPIC) {
      undef %TP;
    }

    foreach $num (sort { $a <=> $b } keys %res) {
      $n = $res{$num}->{'note'};
      $t = $res{$num}->{'date'};
      if ($TOPIC) {
        # this allows us to have multiple topics (subtopics!)
        my ($firstline,$dummy) = split /\n/, $n, 2;
        if ($firstline =~ /^($conf{topicseparator})/) {
          @topic = split(/$conf{topicseparator}/,$firstline);
        }
        else {
          @topic = ();
        }
        # looks like: "\topic\"
        # collect a list of topics under the current topic
        if ($topic[$CurDepth-1] eq $CurTopic && $topic[$CurDepth] ne "") {
          if (exists $TP{$topic[$CurDepth]}) {
            $TP{$topic[$CurDepth]}++;
          }
          else {
            # only if the next item *is* a topic!
            $TP{$topic[$CurDepth]} = 1 if(($CurDepth) <= $#topic);
          }
        }
        elsif ($topic[$CurDepth-1] eq $CurTopic || ($topic[$CurDepth] eq "" && $CurDepth ==1)) {
          # cut the topic off the note-text
          if ($n =~ /^($conf{topicseparator})/) {
            $CurItem[$i]->{'note'} = $dummy;
          }
          else {
            $CurItem[$i]->{'note'} = $n;
          }
          # save for later output() call
          $CurItem[$i]->{'num'}  = $num;
          $CurItem[$i]->{'time'} = $t;
          $i++;
          # use this note for building the $PATH!
          if ($RealTopic[0] eq "") {
            @RealTopic = @topic;
          }
        }
      }
      else {
        output($num, $n, $t);
      }
    }
    if ($TOPIC) {
      if ($CurTopic ne "") {
        if ($i) {
          # only if there were notes under current topic
          undef $PATH;
          foreach (@RealTopic) {
            $PATH .= $_ . $conf{topicseparator};
            last if($_ eq $CurTopic);
          }
        }
        else {
          # it is an empty topic, no notes here
          $PATH = join $conf{topicseparator}, @LastTopic;
          $PATH .= $conf{topicseparator} . $CurTopic . $conf{topicseparator};
          $PATH =~ s/^\Q$conf{topicseparator}$conf{topicseparator}\E/$conf{topicseparator}/;
        }
      }
      else {
        $PATH = $conf{topicseparator};
      }

      @completion_topics = ();
      @completion_notes  = ();
      # we are at top level, print a list of topics...
      foreach $top (sort(keys %TP)) {
        push @completion_topics, $top;
        output("-", " => ". $top . "$conf{topicseparator} ($TP{$top} notes)",
               " Sub Topic         ");
      }
      #print Dumper(@CurItem);
      for ($in=0;$in<$i;$in++) {
        push @completion_notes, $CurItem[$in]->{'num'};
        output( $CurItem[$in]->{'num'},
                $CurItem[$in]->{'note'},
                $CurItem[$in]->{'time'} );
      }
    }

    print "\n";
  }

############################### NEW ##################################
sub new {
    my($TEMP,$editor, $date, $note, $WARN, $c, $line, $num, @topic);
    if ($conf{readonly}) {
    print "readonly\n";
    return;
    }
    $date = &getdate;
    return if $db->lock();
    if ($conf{alwayseditor}) {
      $TEMP = &gettemp;
      # security!
      unlink $TEMP;
      # let the user edit it...
      $editor = &find_editor;
      if ($editor) {
        # create the temp file
        open NEW, "> $TEMP" or die "Could not write $TEMP: $!\n";
        close NEW;
        system "chattr", "+s", $TEMP; # ignore errors, since only on ext2 supported!
        system $editor, $TEMP;
      }
      else {
        print "Could not find an editor to use!\n";
        $db->unlock();
        exit(0);
      }
      # read it in ($note)
      $note = "";
      open E, "<$TEMP" or $WARN = 1;
      if ($WARN) {
        print "...edit process interupted! No note has been saved.\n";
        undef $WARN;
        $db->unlock();
        return;
      }
      $c = 0;
      while (<E>) {
        $note = $note . $_;
      }
      chomp $note;
      close E;
      # privacy!
      unlink $TEMP;
    }
    else {
      $note = "";
      $line = "";
      # create a new note
      if ($NewType) {
        # be silent! read from STDIN until EOF.
        while (<STDIN>) {
          $note .= $_;
        }
      }
      else {
        print "enter the text of the note, end with a single .\n";
        do
          {
            $line = <STDIN>;
            $note = $note . $line;
          } until $line eq ".\n";
        # remove the . !
        chop $note;
        chop $note;
      }
    }
    # look if the note was empty, so don't store it!
    if ($note =~ /^\s*$/) {
      print "...your note was empty and will not be saved.\n";
      $db->unlock();
      return;
    }
    # since we have not a number, look for the next one available:
    $number = $db->get_nextnum();
    if ($TOPIC && $CurTopic ne "") {
      @topic = split(/$conf{topicseparator}/,$note);
      if ($topic[1] eq "") {
        $note = $PATH . "\n$note";
      }
    }
    $note = &add_ticket($note);

    $db->set_new($number,$note,$date);
    # everything ok until here!
    print "note stored. it has been assigned the number $number.\n\n";
    $db->unlock();
  }

sub add_ticket {
    my $orignote = shift;
    if ($conf{addticket}) {
      my ($topic, $title, $rest) = split /\n/, $orignote, 3;
      my $note = "";
      if ($topic =~ /^\//) {
        # topic path, keep it
        $note .= "$topic\n";
      }
      else {
        # no topic
        $rest  = "$title\n$rest";
        $title = $topic;
      }
      if ($title !~ /^\[[a-z0-9A-Z]+\]/) {
        # no ticket number, so create one
        my $ticket = &ticket();
        $title = "[" . ticket() . "] " . $title;
      }
      $note .= "$title\n$rest";
      return $note;
    }
    else {
      return $orignote;
    }
}


############################### DELETE ##################################
sub del {
    my($i,@count, $setnum, $pos, $ERR);
    if ($conf{readonly}) {
        print "readonly\n";
    return;
    }
    # delete a note
    &num_bereich;        # get @NumBlock from $number

    return if $db->lock();

    foreach $_ (@NumBlock) {
      $ERR = $db->set_del($_);
      if ($ERR) {
        print "no note with number $_ found!\n";
      }
      else {
        print "note number $_ has been deleted.\n";
      }
    }
    # recount the notenumbers:
    $db->set_recountnums();

    $db->unlock();
    @NumBlock = ();
  }

############################### EDIT ##################################
sub edit {
    my($keeptime, $date, $editor, $TEMP, $note, $t, $num, $match, $backup);
    if ($conf{readonly}) {
       print "readonly\n";
       return;
    }
    # edit a note
    $date = &getdate;

    return if $db->lock();

    ($note, $keeptime) = $db->get_single($number);
    if ($keeptime eq "") {
      print "no note with that number found ($number)!\n\n";
      if($mode ne "interactive") {
        $db->unlock();
        exit(0);
      }
      else {
        $db->unlock();
        return;
      }
    }

    $TEMP = &gettemp;
    open NOTE,">$TEMP" or die "Could not open $TEMP\n";
    select NOTE;

    system "chattr", "+s", $TEMP; # ignore errors, like in new()

    print $note;
    close NOTE;
    select STDOUT;
    $editor = &find_editor;

    $backup = $note;

    if ($editor) {
      system ($editor, $TEMP) and die "Could not execute $editor: $!\n";
    }
    else {
      print "Could not find an editor to use!\n";
      exit(0);
    }
    $note = "";
    open NOTE,"<$TEMP" or die "Could not open $TEMP\n";

    while (<NOTE>) {
      $note = $note . $_;
    }
    chomp $note;
    close NOTE;

    unlink $TEMP || die $!;

    if ($note ne $backup) {
      if ($conf{keeptimestamp}) {
        $t = $keeptime;
      }
      else {
        $t = $date;
      }
      # we got it, now save to db
      $db->set_edit($number, $note, $t);

      print "note number $number has been changed.\n";
    }
    else {
      print "note number $number has not changed, no save done.\n";
    }
    $db->unlock();
  }

sub dump {
    my(%res, $num, $DUMP);
    # $dump_file
    if ($dump_file eq "-") {
      $DUMP = *STDOUT;
    }
    else {
      open (DUMPFILE, ">$dump_file") or die "could not open $dump_file\n";
      $DUMP = *DUMPFILE;
    }
    select $DUMP;
    %res = $db->get_all();
    # FIXME: prepare hashing in NOTEDB class
    foreach $num (sort { $a <=> $b } keys %res) {
      print STDOUT "dumping note number $num to $dump_file\n" if($dump_file ne "-");
      my($title, $path, $body);
      if ($res{$num}->{note} =~ /^\//) {
        ($path, $title, $body) = split /\n/, $res{$num}->{note}, 3;
      }
      else {
        ($title, $body) = split /\n/, $res{$num}->{note}, 2;
        $path = '';
      }
      my $date = $res{$num}->{date};
      $res{$num} = { body => $body, title => $title, path => $path, date => $date};
    }
    print Dump(\%res);
    close(DUMPFILE);
    select STDOUT;
}

sub import {
    my($num, $start, $complete, $dummi, $note, $date, $time, $number, $stdin, $DUMP, %data);
    # open $dump_file and import it into the notedb
    $stdin = 1 if($dump_file eq "-");
    if ($stdin) {
      $DUMP = *STDIN;
    }
    else {
      open (DUMPFILE, "<$dump_file") or die "could not open $dump_file\n";
      $DUMP = *DUMPFILE;
    }

    my $yaml = join '', <$DUMP>;

    my $res = Load($yaml);

    foreach my $number (keys %{$res}) {
      my $note;
      if ($res->{$number}->{path}) {
        $note = "$res->{$number}->{path}\n$res->{$number}->{title}\n$res->{$number}->{body}";
      }
      else {
        $note = "$res->{$number}->{title}\n$res->{$number}->{body}";
      }
      $data{$number} = {
                        date => $res->{$number}->{date},
                        note => &add_ticket($note)
                       };
      print "fetched note number $number from $dump_file from $res->{$number}->{date}.\n" if(!$stdin);
      $number++;
    }

    $db->set_del_all() if($ImportType ne "");
    $db->import_data(\%data);
}



sub determine_width {
  # determine terminal wide, if possible
  if ($maxlen eq "auto") {
    eval {
      my $wide = `stty -a`;
      if ($wide =~ /columns (\d+?);/) {
        $maxlen = $1 - 32; # (32 = timestamp + borders)
      }
      elsif ($wide =~ /; (\d+?) columns;/) {
        # bsd
        $maxlen = $1 - 32; # (32 = timestamp + borders)
      }
      else {
        # stty didn't work
        $maxlen = 80 - 32;
      }
    };
  }
}

sub clear {
  # first, try to determine the terminal height
  return if(!$conf{autoclear});
  my $hoch;
  eval {
    my $height = `stty -a`;
    if ($height =~ /rows (\d+?);/) {
      $hoch = $1;
    }
    elsif ($height =~ /; (\d+?) rows;/) {
      # bsd
      $hoch = $1;
    }
  };
  if (!$hoch) {
    # stty didn't work
    $hoch = 25;
  }
  print "\n" x $hoch;
}

sub interactive {
    my($B, $BB, $menu, $char, $Channel);
    $Channel = $|;
    local $| = 1;
    # create menu:
    $B = "<white_black>";
    $BB = "</white_black>";
    $menu =     "[" .  $B . "L" . $BB . "-List ";
    if ($TOPIC) {
      $menu .= $B . "T" . $BB . "-Topics ";
    }
    $menu   .= $B . "N" . $BB . "-New "
             . $B . "D" . $BB . "-Delete "
             . $B . "S" . $BB . "-Search "
             . $B . "E" . $BB . "-Edit "
             . $B . "?" . $BB . "-Help "
             . $B . "Q" . $BB . "-Quit] "; # $CurTopic will be empty if $TOPIC is off!

    # per default let's list all the stuff:
    # Initially do a list command!
    &determine_width;
    $ListType = ($conf{defaultlong}) ? "LONG" : "";

    # show initial note entry
    if ($conf{motd}) {
      &motd;
    }

    # show initial listing
    &list;

    my ($term, $prompt, $attribs);
    eval { require Term::ReadLine; };
    if (!$@) {
      $term = new Term::ReadLine('');
      $attribs = $term->Attribs;
      $attribs->{completion_function} = \&complete;
    }

    for (;;) {
      $ListType = ($conf{defaultlong}) ? "LONG" : "";
      undef $SetTitle;
      if ($CurDepth > 2) {
        print C $menu . $TOPICC . "../" . $CurTopic . $_TOPICC;
      }
      else {
        print C $menu . $TOPICC . $CurTopic . $_TOPICC;
      }

      if ($conf{readonly}) {
        print " [readonly] ";
      }

      print ">";

      # endless until user press "Q" or "q"!
      if ($term) {
        if (defined ($char = $term->readline(" "))) {
          $term->addhistory($char) if $char =~ /\S/;
          $char =~ s/\s*$//; # remove trailing whitespace (could come from auto-completion)
        }
        else {
          # shutdown
          $| = $Channel;
          print "\n\ngood bye!\n";
          exit(0);
        }
      }
      else {
        $char = <STDIN>;
        chomp $char;
      }

      &determine_width;
      &clear;

      if ($char =~ /^\d+\s*[\di*?,*?\-*?]*$/) {
        $ListType = "";        #overrun
        # display notes
        $number = $char;
        &display;
      }
      elsif ($char =~ /^n$/i) {
        # create a new one
        &new;
      }
      elsif ($char =~ /^$/) {
        &list;
      }
      elsif ($char =~ /^l$/) {
        $ListType = "";
        &list;
      }
      elsif ($char =~ /^L$/) {
        $ListType = "LONG";
        &list;
        undef $SetTitle;
      }
      elsif ($char =~ /^h$/i || $char =~ /^\?/) {
        # zu dumm der Mensch ;-)
        &help;
      }
      elsif ($char =~ /^d\s+([\d*?,*?\-*?]*)$/i) {
        # delete one!
        $number = $1;
        &del;
      }
      elsif ($char =~ /^d$/i) {
        # we have to ask her:
        print "enter number(s) of note(s) you want to delete: ";
        $char = <STDIN>;
        chomp $char;
        $number = $char;
        &del;
      }
      elsif ($char =~ /^e\s+(\d+\-*\,*\d*)/i) {
        # edit one!
        $number = $1;
        &edit;
      }
      elsif ($char =~ /^e$/i) {
        # we have to ask her:
        print "enter number of the note you want to edit: ";
        $char = <STDIN>;
        chomp $char;
        $number = $char;
        &edit;
      }
      elsif ($char =~ /^s\s+/i) {
        # she want's to search
        $searchstring = $';
        chomp $searchstring;
        &search;
      }
      elsif ($char =~ /^s$/i) {
        # we have to ask her:
        print "enter the string you want to search for: ";
        $char = <STDIN>;
        chomp $char;
        $char =~ s/^\n//;
        $searchstring = $char;
        &search;
      }
      elsif ($char =~ /^q$/i) {
        # schade!!!
        $| = $Channel;
        print "\n\ngood bye!\n";
        exit(0);
      }
      elsif ($char =~ /^t$/) {
        $TreeType = "";
        &display_tree;
      }
      elsif ($char =~ /^T$/) {
        $TreeType = "LONG";
        &display_tree;
        $TreeType = "";
      }
      elsif ($char =~ /^c\s*$/) {
        print "Missing parameter (parameter=value), available ones:\n";
        foreach my $var (sort keys %conf) {
          if ($var !~ /^$hardparams/ && $var !~ /::/) {
            printf "%20s = %s\n", $var, $conf{$var};
          }
        }
      }
      elsif ($char =~ /^c\s*(.+?)\s*=\s*(.+?)/) {
        # configure
        my $param = $1;
        my $value = $2;
        if ($param !~ /^$hardparams/ && $param !~ /::/ && exists $conf{$param}) {
          print "Changing $param from $conf{$param} to $value\n";
          $conf{$param} = $value;
        }
        else {
          print "Unknown config parameter $param!\n";
        }
      }
      elsif ($char =~ /^\.\.$/ || $char =~ /^cd\s*\.\.$/) {
        $CurDepth-- if ($CurDepth > 1);
        $CurTopic = $LastTopic[$CurDepth];
        pop @LastTopic; # remove last element
        &list;
      }
      elsif ($char =~ /^l\s+(\w+)$/) {
        # list
        $WantTopic = $1;
        if (exists $TP{$WantTopic}) {
          my %SaveTP = %TP;
          $LastTopic[$CurDepth] = $CurTopic;
          $CurTopic = $1;
          $CurDepth++;
          &list;
          $CurTopic = $LastTopic[$CurDepth];
          $CurDepth--;
          %TP = %SaveTP;
        }
        else {
          print "\nunknown command!\n";
        }
      }
      else {
        # unknown
        my $unchar = $char;
        $unchar =~ s/^cd //;    # you may use cd <topic> now!
        if ($unchar =~ /^\d+?$/ && $conf{short_cd}) {
          # just a number!
          my @topic;
          my ($cnote, $cdate) = $db->get_single($unchar);
          my ($firstline,$dummy) = split /\n/, $cnote, 2;
          if ($firstline =~ /^($conf{topicseparator})/) {
            @topic = split(/$conf{topicseparator}/,$firstline);
          }
          else {
            @topic = ();
          }
          if (@topic) {
            # only jump, if, and only if there were at least one topic!
            $CurDepth = $#topic + 1;
            $CurTopic = pop @topic;
            @LastTopic = ("");
            push @LastTopic, @topic;
          }
          &list;
        }
        elsif ($unchar eq $conf{topicseparator}) {
          # cd /
          $CurDepth = 1;
          $CurTopic = "";
          &list;
        }
        elsif (exists $TP{$char} || exists $TP{$unchar}) {
          $char = $unchar if(exists $TP{$unchar});
          $LastTopic[$CurDepth] = $CurTopic;
          $CurTopic = $char;
          $CurDepth++;
          &list;
        }
        else {
          # try incomplete match
          my @matches;
          foreach my $topic (keys %TP) {
            if ($topic =~ /^$char/) {
              push @matches, $topic;
            }
          }
          my $nm = scalar @matches;
          if ($nm == 1) {
            # match on one incomplete topic, use this
            $LastTopic[$CurDepth] = $CurTopic;
            $CurTopic = $matches[0];
            $CurDepth++;
            &list;
          }
          elsif ($nm > 1) {
            print "available topics: " . join( "," , @matches) . "\n";
          }
          else {
            print "\nunknown command!\n";
          }
        }
        undef $unchar;
      }
    }
  }


sub usage
  {
    print qq~This is the program note $VERSION by T.v.Dein (c) 1999-2017.
It comes with absolutely NO WARRANTY. It is distributed under the
terms of the GNU General Public License. Use it at your own risk :-)

Usage:     note [ options ] [ number [,number...]]

Options:

       -c, --config file
           Use another config file than the default \$HOME/.noterc.

       -l, --list [topic]
           Lists all existing notes. If no topic were specified, it will
           display a list of all existing topics.  See the section TOPICS for
           details about topics.

       -L, --longlist [topic]
           The same as -l but prints also the timestamp of the notes.

       -t, --topic
           Prints a list of all topics as a tree.

       -T, --longtopic
           Prints the topic-tree with the notes under each topic.

       -s, --search string
           Searches for <string> trough the notes database. See the section
           SEARCHING for details about the search engine.

       -e, --edit number
           Edit the note with the number <number> using your default editor or
           the one you specified in the config file.

       -d, --delete number
           Delete the note with the number <number>. You can delete multiple
           notes with one command. "1-4" deletes the notes 1,2,3,4. And
           "1,5,7" deletes the specified ones.

       -D, --Dump [file | -]
           Dumps all notes to the textfile <file>. If <file> is a "-" it will
           be printed out to standard output (STDOUT).

       -I, --Import file | -
           Imports a previously dumped textfile into the note database. Data
           will be appended by default.  You can also specify a dash note -I -
           instead of a <file>, which causes note, silently to read in a dump
           from STDIN.

       -o, --overwrite
           Only suitable for use with --Import. Overwrites an existing notedb.
           Use with care.

       -r, --raw
           Raw mode, output will not be formatted. Works not in interactive
           mode, only on cmd-line for list and display. That means, no colors
           will be used and no lines or titles.

       -i, --interactive
           Start note in interactive mode. See the section INTERACTIVE MODE
           for details on this mode.

       --encrypt cleartext
           Encrypt the given clear text string. You would need that if you
           want to store the mysql password not in cleartext in the config(if
           you are using the mysql backend!).

       -h, --help
           Display this help screen.

       -v, --version
           Display the version number.

       -   If you run note just with one dash: note -, then it will read in a
           new note from STDIN until EOF. This makes it possible to pipe text
           into a new note, i.e.:

            cat sometextfile | note -

Read the note(1) manpage for more details.
~;
    exit 1;
  }

sub find_editor {
  return $conf{preferrededitor} || $ENV{"VISUAL"} || $ENV{"EDITOR"} || "vi";
}

#/

sub format {
  # make text bold/underlined/inverse using current $NOTEC
  my($note) = @_;
  if ($conf{formattext}) {
    # prepare colors to be used for replacement
    my $BN  = uc($NOTEC);
    my $_BN = uc($_NOTEC);
    my $UN  = $NOTEC;
    $UN     =~ s/<(.*)>/<$1_>/;
    my $_UN = $UN;
    $_UN    =~ s/<(.*)>/<\/$1>/;
    my $IN  = $NOTEC;
    my $_IN = $_NOTEC;
    $IN     =~ s/<(.*)>/<$1I>/;
    $_IN    =~ s/<(.*)>/<$1I>/;

    if ($conf{formattext} eq "simple") {
      $note =~ s/\*([^\*]*)\*/$BN$1$_BN/g;
      $note =~ s/_([^_]*)_/$UN$1$_UN/g;
      $note =~ s/{([^}]*)}/$IN$1$_IN/g;
      $note =~ s#(?<!<)/([^/]*)/#<hide>$1</hide>#g;
    }
    else {
      $note =~ s/\*\*([^\*]{2,})\*\*/$BN$1$_BN/g;
      $note =~ s/__([^_]{2,})__/$UN$1$_UN/g;
      $note =~ s/\{\{([^}]{2,})\}\}/$IN$1$_IN/g;
      $note =~ s#//([^/]{2,})//#<hide>$1</hide>#g;
    }

    $note =~ s/(<\/.*>)/$1$NOTEC/g;
  }
  $note;
}

sub output {
    my($SSS, $LINE, $num, $note, $time, $TYPE, $L, $LONGSPC, $R, $PathLen, $SP, $title, $CUTSPACE,
       $VersionLen, $len, $diff, $Space, $nlen, $txtlen, $count);
    ($num, $note, $time, $TYPE, $count) = @_;

    $txtlen = ($ListType eq "LONG") ? $maxlen : $timelen + $maxlen;
    $note = &format($note);

    $SSS = "-" x ($maxlen + 30);
    $nlen = length("$num");
    $LINE = "$BORDERC $SSS $_BORDERC\n";
    $LONGSPC = " " x (25 - $nlen);
    if ($conf{printlines}) {
      $L = $BORDERC . "[" . $_BORDERC;
      $R = $BORDERC . "]" . $_BORDERC;
    }
    $PathLen = length($PATH);    # will be ZERO, if not in TOPIC mode!
    $VersionLen = length($VERSION) + 7;

    if ($TYPE ne "SINGLE") {
      if (!$SetTitle) {
        $SP = "";
        # print only if it is the first line!
        $SP = " " x ($maxlen - 2 - $PathLen - $VersionLen);
        if (!$Raw) {
          # no title in raw-mode!
          print C $LINE if ($conf{printlines});
          print C "$L $NUMC#$_NUMC  ";
          if ($ListType eq "LONG") {
            print C " $TIMEC" . "creation date$_TIMEC          ";
          }
          else {
            print $LONGSPC if ($conf{printlines});
          }
          if ($TOPIC) {
            print C $TOPICC . "$PATH    $_TOPICC$SP" . " note $VERSION $R\n";
          }
          else {
            print C $NOTEC . "note$_NOTEC$SP" . " note $VERSION $R\n";
          }
          print C $LINE if ($conf{printlines});
        }
        $SetTitle = 1;
      }
      $title = "";
      $CUTSPACE = " " x $txtlen;
      if ($TYPE eq "search") {
        $note =~ s/^\Q$conf{topicseparator}\E.+?\Q$conf{topicseparator}\E\n//;
      }
      $note =~ s/\n/$CUTSPACE/g;
      $len   = length($note);
      if ($len < ($txtlen - 2 - $nlen)) {
        $diff = $txtlen - $len;
        if (!$Raw) {
          if ($num eq "-") {
            $Space = " " x $diff;
            $title = $BORDERC . $TOPICC . $note . " " . $_TOPICC . $Space . "$_BORDERC";
          }
          else {
            $Space = " " x ($diff - ($nlen - 1));
            $title = $BORDERC . $NOTEC  . $note . " " . $_NOTEC . $Space . "$_BORDERC";
          }
        }
        else {
          $title = $note;
        }
      }
      else {
        $title = substr($note,0,($txtlen - 2 - $nlen));
        if (!$Raw) {
          $title = $BORDERC . $NOTEC . $title . " $_NOTEC$_BORDERC";
        }
      }
      if ($Raw) {
        print "$num  ";
        print "$time  " if($ListType eq "LONG");
        if ($title =~ /^ => (.*)$conf{topicseparator} (.*)$/) {
          $title = "$1$conf{topicseparator} $2"; # seems to be a topic!
        }
        print "$title\n";
      }
      else {
        # $title should now look as: "A sample note                       "
        print C "$L $NUMC$num$_NUMC $R";
        if ($ListType eq "LONG") {
          print C "$L$TIMEC" . $time . " $_TIMEC$R"; 
        }
        print C "$L $NOTEC" . $title . "$_NOTEC $R\n";
        print C $LINE if ($conf{printlines});
      }
    }
    else {
      # we will not reach this in raw-mode, therefore no decision here!
      chomp $note;
      $Space = " " x (($maxlen + $timelen) - $nlen - 16);

      *CHANNEL = *STDOUT;
      my $usecol = $conf{usecolors};

      if ($conf{less}) {
        my $less = "less";
        if ($conf{less} ne 1) {
          # use given less command line
          $less = $conf{less};
        }
        if (open LESS, "|$less") {
          *CHANNEL = *LESS;
          $conf{usecolors} = 0;
        }
      }

      print CHANNEL C $LINE if ($conf{printlines});
      print CHANNEL C "$L $NUMC$num$_NUMC $R$L$TIMEC$time$_TIMEC $Space$R\n";
      print CHANNEL C "\n";
      print CHANNEL C $NOTEC . $note . $_NOTEC . "\n";
      print CHANNEL C $LINE if ($count == 1 && $conf{printlines});

      if ($conf{less}) {
        close LESS;
      }

      $conf{usecolors} = $usecol;
    }
  }



sub C {
    my($default, $S, $Col, $NC, $T);
    $default = "\033[0m";
    $S = $_[0];
    foreach $Col (%Color) {
      if ($S =~ /<$Col>/g) {
        if ($conf{usecolors}) {
          $NC = "\033[" . $Color{$Col} . "m";
          $S =~ s/<$Col>/$NC/g;
          $S =~ s/<\/$Col>/$default/g;
        }
        else {
          $S =~ s/<$Col>//g;
          $S =~ s/<\/$Col>//g;
        }
      }
    }
    return $S;
  }



sub num_bereich {
    my($m,@LR,@Sorted_LR,$i);
    # $number is the one we want to delete!
    # But does it contain commas?
    @NumBlock = ();        #reset
    $m = 0;
    if ($number =~ /\,/) {
      # accept -d 3,4,7
      @NumBlock = split(/\,/,$number);
    }
    elsif ($number =~ /^\d+\-\d+$/) {
      # accept -d 3-9
      @LR = split(/\-/,$number);
      @Sorted_LR = ();

      if ($LR[0] > $LR[1]) {
        @Sorted_LR = ($LR[1], $LR[0]);
      }
      elsif ($LR[0] == $LR[1]) {
        # 0 and 1 are the same
        @Sorted_LR = ($LR[0], $LR[1]);
      }
      else {
        @Sorted_LR = ($LR[0], $LR[1]);
      }

      for ($i=$Sorted_LR[0]; $i<=$Sorted_LR[1]; $i++) {
        # from 3-6 create @NumBlock (3,4,5,6)
        $NumBlock[$m] = $i;
        $m++;
      }
    }
    else {
      @NumBlock = ($number);
    }

  }

sub getdate {
    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
    $year += 1900;
    $mon  += 1;
    $mon  =~ s/^(\d)$/0$1/;
    $hour =~ s/^(\d)$/0$1/;
    $min  =~ s/^(\d)$/0$1/;
    $sec  =~ s/^(\d)$/0$1/;
    $mday =~ s/^(\d)$/0$1/;
    if ($conf{timeformat}) {
      my $back = $conf{timeformat};
      $back =~ s/YYYY/$year/;
      $back =~ s/YY/substr($year, 1, 2)/e;
      $back =~ s/MM/$mon/;
      $back =~ s/DD/$mday/;
      $back =~ s/mm/$min/;
      $back =~ s/hh/$hour/;
      $back =~ s/ss/$sec/;
      return $back;
    }
    return "$mday.$mon.$year $hour:$min:$sec";
  }


sub gettemp {
    my($random, @range);
    @range=('0'..'9','a'..'z','A'..'Z');
    srand(time||$$);
    for (0..10) {
      $random .= $range[rand(int($#range)+1)];
    }
    my $tempfile = File::Spec->catfile($conf{tempdirectory}, $USER . $random);
    if (-e $tempfile) {
      # avoid race conditions!
      unlink $tempfile;
    }
    return $tempfile;
  }



sub help {
    my $B = "<white_black>";
    my $BB = "</white_black>";
    my($S, $L, $T, $Q, $H, $N, $D, $E, $C);
    $L = $B . "L" . $BB . $NOTEC;
    $T = $B . "T" . $BB . $NOTEC;
    $Q = $B . "Q" . $BB . $NOTEC;
    $H = $B . "?" . $BB . $NOTEC;
    $N = $B . "N" . $BB . $NOTEC;
    $D = $B . "D" . $BB . $NOTEC;
    $E = $B . "E" . $BB . $NOTEC;
    $S = $B . "S" . $BB . $NOTEC;
    $C = $B . "C" . $BB . $NOTEC;

    print C qq~$BORDERC
----------------------------------------------------------------------$_BORDERC $TOPICC
HELP for interactive note       $VERSION
$_TOPICC $NOTEC
The following commands are available:
$L       List notes. L=long, with timestamp and l=short without timestamp.
        You can also just hit <enter> for short list.
        If you specify a subtopic, then list will display it's contents, 
        i.e.: "l mytopic" will dislpay notes under mytopic.
$N       Create a new note.
$D       Delete a note. You can either hit "d 1" or "d 1-4" or just hit "d".
        If you don't specify a number, you will be asked for.
$S       Search trough the notes database. Usage is similar to Delete, use
        a string instead of a number to search for.
$E       Edit a note. Usage is similar to Delete but you can only edit note
        a time.
$C       Change note config online. Use with care!
$H       This help screen.
$Q       Exit the program.~;
    if ($TOPIC) {
      print C qq~
$T       print a list of all existing topics as a tree. T prints the tree
        with all notes under each topic.~;
    }
    print C qq~

All commands except the List and Topic commands are case insensitive.
Read the note(1) manpage for more details.$BORDERC
----------------------------------------------------------------------$_BORDERC
~;
  }


sub display_tree {
  # displays a tree of all topics
  my(%TREE, %res, $n, $t, $num, @nodes, $firstline, $text, $untext);
  %res = $db->get_all();
  foreach $num (keys %res) {
    $n = $res{$num}->{'note'};
    $t = $res{$num}->{'date'};
    # this allows us to have multiple topics (subtopics!)
    my ($firstline,$text,$untext) = split /\n/, $n, 3;
    if ($firstline =~ /^($conf{topicseparator})/) {
      $firstline =~ s/($conf{topicseparator})*$//; #remove Topicseparator
      @nodes = split(/$conf{topicseparator}/,$firstline);
    }
    else {
      @nodes = (); #("$conf{topicseparator}");
      $text = $firstline;
    }
    &determine_width; # ensure $maxlen values for &tree in non interactive modes
    &tree($num, $text, \%TREE, @nodes);
  }
  #return if ($num == 0);
  # now that we have build our tree (in %TREE) go on t display it:
  print C $BORDERC . "\n[" . $conf{topicseparator} . $BORDERC . "]\n";
  &print_tree(\%{$TREE{''}},"") if(%TREE);
  print C $BORDERC . $_BORDERC . "\n";
}


sub tree {
  my($num, $text, $LocalTree, $node, @nodes) = @_;
  if (@nodes) {
    if (! exists $LocalTree->{$node}->{$NoteKey}) {
      $LocalTree->{$node}->{$NoteKey} = [];
    }
    &tree($num, $text, $LocalTree->{$node}, @nodes);
  }
  else {
    if (length($text) > ($maxlen - 5)) {
      $text = substr($text, 0, ($maxlen -5));
    }
    $text = $text . " (" . $NUMC . "#" . $num . $_NUMC . $NOTEC . ")" . $_NOTEC if($text ne "");
    push @{$LocalTree->{$node}->{$NoteKey}}, $text;
  }
}


sub print_tree {
  # thanks to Jens for his hints and this sub!
  my $hashref=shift;
  my $prefix=shift;
  my @notes=@{$hashref->{$NoteKey}};
  my @subnotes=sort grep { ! /^$NoteKey$/ } keys %$hashref;
  if ($TreeType eq "LONG") {
    for my $note (@notes) {
      if ($note ne "") {
        print C $BORDERC ;    # . $prefix. "|\n";
        print C "$prefix+---<" . $NOTEC . $note . $BORDERC . ">" . $_NOTEC . "\n";
      }
    }
  }
  for my $index (0..$#subnotes) {
    print C $BORDERC . $prefix. "|\n";
    print C "$prefix+---[" . $TOPICC . $subnotes[$index] . $BORDERC . "]\n";
    &print_tree($hashref->{$subnotes[$index]},($index == $#subnotes?"$prefix    ":"$prefix|   "));
  }
}


sub getconfig {
    my($configfile) = @_;
    my ($home, $value, $option);
    # checks are already done, so trust myself and just open it!
    open CONFIG, "<$configfile" || die $!;
    while (<CONFIG>) {
      chomp;
      next if(/^\s*$/ || /^\s*#/);
      my ($option,$value) = split /\s\s*=?\s*/, $_, 2;

      $value =~ s/\s*$//;
      $value =~ s/\s*#.*$//;
      if ($value =~ /^(~\/)(.*)$/) {
        $value = File::Spec->catfile($ENV{HOME}, $2);
      }

      if ($value =~ /^(yes|on|1)$/i) {
        $value = 1;
      }
      elsif ($value =~ /^(no|off|0)$/i) {
        $value = 0;
      }

      $option = lc($option);

      if ($option =~ /^(.+)::(.*)$/) {
        # driver option
        $driver{$1}->{$2} = $value;
      }
      else {
        # other option
        $conf{$option} = $value;
      }
    }

    close CONFIG;
}


sub complete {
  my ($text, $line, $start) = @_;

  if ($line =~ /^\s*$/) {
    # notes or topics allowed
    return @completion_topics, @completion_notes;
  }
  if ($line =~ /^cd/) {
    # only topics allowed
    return @completion_topics, "..";
  }
  if ($line =~ /^l/i) {
    # only topics allowed
    return @completion_topics;
  }
  if ($line =~ /^[ed]/) {
    # only notes allowed
    return @completion_notes;
  }
  if ($line =~ /^[snt\?q]/i) {
    # nothing allowed
    return ();
  }
}

sub load_driver {
  my ($parent) = @_;

  if ($parent) {
    my $pkg = "NOTEDB";
    eval "use $pkg;";
    if ($@) {
      die "Could not load the NOTEDB module: $@\n";
    }
  }
  else {
    # load the backend driver
    my $pkg = "NOTEDB::$conf{dbdriver}";
    eval "use $pkg;";
    if ($@) {
      die "$conf{dbdriver} backend unsupported: $@\n";
    }
    else {
      $db = $pkg->new(%{$driver{$conf{dbdriver}}});
    }
  }
}

sub ticket {
  return join "", (map { $randomlist[int(rand($#randomlist))] } (0 .. 10) );
}
__END__