1#!/usr/bin/perl
2#
3# note - console notes management with database and encryption support.
4# Copyright (C) 1999-2017 T.v.Dein   (see README for details!)
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License
8# as published by the Free Software Foundation; either version 2
9# of the License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
19#
20#                               - Thomas Linden <tom at linden dot at>
21#
22# latest version on:
23# http://www.daemon.de/note/
24#
25
26use lib qw(blib/lib);
27
28BEGIN {
29  # works on unix or cygwin only!
30  my $path = $0;
31  $path =~ s#/[^/]*$##;
32  unshift @INC, "$path/..";
33}
34
35use strict;
36no strict 'refs';
37use Getopt::Long;
38use FileHandle;
39use File::Spec;
40use YAML;
41
42
43#
44# prototypes
45#
46sub usage;        # print usage message for us thumb userz :-)
47sub find_editor;  # returns an external editor for use
48sub output;       # used by &list and &display
49sub C;            # print colourized
50sub num_bereich;  # returns array from "1-4" (1,2,3,4)
51sub getdate;      # return pretty formatted day
52sub new;          # crate new note
53sub edit;         # edit a note
54sub del;          # delete a note
55sub display;      # display one or more notes
56sub list;         # note-listing
57sub help;         # interactive help screen
58sub import;       # import from notedb-dump
59sub display_tree; # show nice tree-view
60sub tree;         # build the tree
61sub print_tree;   # print the tree, contributed by Jens Heunemann <Jens dot Heunemann at consol dot de>. THX!
62sub ticket;       # return a random string which is used as ticket number for new note entries
63
64#
65# globals
66#
67my (
68    #
69    # commandline options
70    #
71    $opt_, $opt_i, $opt_r, $opt_e, $opt_d, $opt_enc,
72    $opt_s, $opt_t, $opt_T, $opt_l, $opt_L, $opt_c,
73    $opt_D, $opt_I, $opt_o, $opt_h, $opt_n, $opt_v,
74
75    #
76    # set from commandline (or interactive)
77    #
78    $number, $searchstring, $dump_file, $ImportType, $NewType, $Raw, $TOPIC,
79
80    #
81    # configuration options
82    %conf, %driver,
83
84    #
85    # processed colors
86    #
87    $BORDERC,  $_BORDERC, $NOTEC, $NUMC,  $_NUMC, $_NOTEC, $TIMEC,
88    $_TIMEC, $TOPICC,  $_TOPICC,
89
90    #
91    # config presets
92    #
93    $DEFAULTDBNAME, $USER, $PATH, $CONF,
94
95    #
96    # internals
97    #
98    $TYPE, $mode, $NoteKey, %Color, @LastTopic, $timelen, $maxlen,
99    $VERSION, $CurTopic, $CurDepth, $WantTopic, $db,
100    $sizeof, %TP, $TreeType, $ListType, $SetTitle, $clearstring,
101    @ArgTopics, $key, $typedef, @NumBlock, $has_nothing, @completion_topics, @completion_notes,
102    @randomlist, $hardparams
103   );
104
105
106#
107# DEFAULTS, allows one to use note without a config
108# don't change them, instead use the config file!
109#
110
111%conf = (
112     'numbercolor'       => 'blue',
113     'bordercolor'       => 'black',
114     'timecolor'         => 'black',
115     'topiccolor'        => 'black',
116     'notecolor'         => 'green',
117     'alwaysinteractive' => 1,
118     'keeptimestamp'     => 0,
119     'readonly'          => 0,
120     'shortcd'           => 1,
121     'autoclear'         => 0,
122     'maxlen'            => 'auto',
123     'defaultlong'       => 0,
124     'dbdriver'          => 'binary',
125     'timeformat'        => 'DD.MM.YYYY hh:mm:ss',
126     'usecolors'         => 0,
127     'addticket'         => 0,
128     'formattext'        => 0,
129     'alwayseditor'      => 1,
130     'useencryption'     => 0,
131     'tempdirectory'     => File::Spec->tmpdir(),
132     'topicseparator'    => '/',
133     'printlines'        => 0,
134     'cache'             => 0,
135     'preferrededitor'   => '',
136     'motd'              => ''
137);
138
139# these are not customizable at runtime!
140$hardparams = "(readonly|maxlen|dbdriver|useencryption|cryptmethod)";
141$CONF       = File::Spec->catfile($ENV{HOME}, ".noterc");
142$USER       = getlogin || getpwuid($<); chomp $USER;
143$TOPIC      = 1;
144$VERSION    = "1.3.26";
145$CurDepth   = 1;        # the current depth inside the topic "directory" structure...
146$maxlen     = "auto";
147$timelen    = 22;
148
149@randomlist = ('a'..'z', 0..9, 'A'..'Z');
150
151# colors available
152# \033[1m%30s\033[0m
153%Color = (            'black'         => '0;30',
154                      'red'           => '0;31',
155                      'green'         => '0;32',
156                      'yellow'        => '0;33',
157                      'blue'          => '0;34',
158                      'magenta'       => '0;35',
159                      'cyan'          => '0;36',
160                      'white'         => '0;37',
161                      'B'             => '1;30',
162                      'BLACK'         => '1;30',
163                      'RED'           => '1;31',
164                      'GREEN'         => '1;32',
165                      'YELLOW'        => '1;33',
166                      'BLUE'          => '1;34',
167                      'MAGENTA'       => '1;35',
168                      'CYAN'          => '1;36',
169                      'WHITE'         => '1;37',
170                      'black_'        => '4;30',
171                      'red_'          => '4;31',
172                      'green_'        => '4;32',
173                      'yellow_'       => '4;33',
174                      'blue_'         => '4;34',
175                      'magenta_'      => '4;35',
176                      'cyan_'         => '4;36',
177                      'white_'        => '4;37',
178                      'blackI'        => '7;30',
179                      'redI'          => '7;31',
180                      'greenI'        => '7;32',
181                      'yellowI'       => '7;33',
182                      'blueI'         => '7;34',
183                      'magentaI'      => '7;35',
184                      'cyanI'         => '7;36',
185                      'whiteI'        => '7;37',
186                      'white_black'   => '40;37;01',
187                      'bold'          => ';01',
188                      'hide'          => '44;34'
189         );
190
191#
192# process command line args
193#
194if ($ARGV[0] eq "") {
195  $mode = "new";
196}
197elsif ($#ARGV == 0 && $ARGV[0] eq "-") {
198  $mode = "new";
199  $NewType = 1;            # read from STDIN until EOF
200  shift;
201  undef $has_nothing;
202}
203else {
204  Getopt::Long::Configure( qw(no_ignore_case));
205  GetOptions (
206              "interactive|i!"    => \$opt_i, # no arg
207              "config|c=s"        => \$opt_c, # string,  required
208              "raw|r!"            => \$opt_r, # no arg
209              "edit|e=i"          => \$opt_e, # integer, required
210              "delete|d=s"        => \$opt_d, # integer, required
211              "search|s=s"        => \$opt_s, # string,  required
212              "tree|topic|t!"     => \$opt_t, # no arg
213              "longtopic|T!"      => \$opt_T, # no arg
214              "list|l:s"          => \$opt_l, # string,  optional
215              "longlist|L:s"      => \$opt_L, # string,  optional
216              "dump|Dump|D:s"     => \$opt_D, # string,  optional
217              "import|Import|I:s" => \$opt_I, # string,  optional
218              "overwrite|o!"      => \$opt_o, # no arg
219              "help|h|?!"         => \$opt_h, # no arg
220              "version|v!"        => \$opt_v, # no arg
221              "encrypt=s"         => \$opt_enc, # string, required
222             );
223  $opt_n = shift;        # after that @ARGV contains eventually
224  # a note-number
225  # $opt_ is a single dash, in case of existence!
226  #
227  # determine mode
228  #
229  if ($opt_i) {
230    $mode = "interactive";
231  }
232  elsif (defined $opt_l || defined $opt_L) {
233    $mode = "list";
234    if (defined $opt_l) {
235      @ArgTopics = split /$conf{topicseparator}/, $opt_l;
236    }
237    else {
238      $ListType = "LONG";
239      @ArgTopics = split /$conf{topicseparator}/, $opt_L;
240    }
241    $CurDepth += $#ArgTopics + 1 if($opt_l || $opt_L);
242    $CurTopic = $ArgTopics[$#ArgTopics];  # use the last element everytime...
243  }
244  elsif ($opt_t || $opt_T) {
245    $mode = "tree";
246    $mode = "display_tree";
247    $TreeType = "LONG" if($opt_T);
248  }
249  elsif (defined $opt_s) {
250    $mode = "search";
251    $searchstring = $opt_s;
252  }
253  elsif ($opt_e) {
254    $mode = "edit";
255    $number = $opt_e;
256  }
257  elsif ($opt_d) {
258    $mode = "del";
259    $number = $opt_d;
260  }
261  elsif ($opt_enc) {
262    $mode = "encrypt_passwd";
263    $clearstring = $opt_enc;
264  }
265  elsif (defined $opt_D) {
266    $mode = "dump";
267    if (!$opt_) {
268      if ($opt_D ne "") {
269        $dump_file = $opt_D;
270      }
271      else {
272        $dump_file = "note.dump.$$";
273        print "no dumpfile specified, using $dump_file.\n";
274      }
275    }
276    else {
277      $dump_file = "-";        # use STDIN
278    }
279  }
280  elsif (defined $opt_I) {
281    $mode = "import";
282    if (!$opt_) {
283      if ($opt_I ne "") {
284        $dump_file = $opt_I;
285      }
286      else {
287        print "Import-error! No dump_file specified!\n";
288        exit(1);
289      }
290    }
291    else {
292      $dump_file = "-";
293    }
294  }
295  elsif ($opt_v) {
296    print "This is note $VERSION by Thomas Linden <tom at linden dot at>.\n";
297    exit(0);
298  }
299  elsif ($opt_h) {
300    &usage;
301  }
302  else {
303    if ($opt_c && $mode eq "" && !$opt_n) {
304      $mode = "new";
305    }
306    elsif ($opt_c && $mode eq "") {
307      $mode = "";        # huh?!
308    }
309    else {
310      $has_nothing = 1;
311    }
312  }
313  ### determine generic options
314  if ($opt_n =~ /^[\d+\-?\,*]+$/) {
315    # first arg is a digit!
316    if ($mode eq "") {
317      $number = $opt_n;
318      $mode = "display";
319      undef $has_nothing;
320    }
321    else {
322      print "mode <$mode> does not take a numerical argument!\n";
323      exit(1);
324    }
325  }
326  elsif ($opt_n ne "") {
327    print "Unknown option: $opt_n\n";
328    &usage;
329  }
330  if ($opt_r) {
331    $Raw = 1;
332  }
333  if ($opt_o) {
334    $ImportType = "overwrite";
335    if (!$opt_I) {
336      print "--overwrite is only suitable for use with --import!\n";
337      exit(1);
338    }
339  }
340  #####
341}
342if ($has_nothing && $mode eq "") {
343  &usage;
344}
345
346
347# read the configfile.
348$CONF = $opt_c if($opt_c);    # if given by commandline, use this.
349if (-e $CONF) {
350  &getconfig($CONF);
351}
352elsif ($opt_c) {
353  # only wrong, if specified by commandline! else use default values!
354  print STDERR "Could not open \"$CONF\": file does not exist or permission denied!\n";
355  exit(1);
356}
357
358# directly jump to encrypt, 'cause this sub does
359# not require a database connection
360if ($mode eq "encrypt_passwd") {
361  &encrypt_passwd;
362  exit;
363}
364
365# Always interactive?
366if ($conf{alwaysinteractive} && $mode ne "dump" && $mode ne "import") {
367  $mode = "interactive";
368}
369
370# OK ... Long-Listing shall be default ... You wanted it!!!
371if ($conf{defaultlong}) {
372  # takes only precedence in commandline mode
373  $ListType="LONG";
374}
375
376
377
378
379# calculate some constants...
380$BORDERC  = "<$conf{bordercolor}>";
381$_BORDERC = "</$conf{bordercolor}>";
382$NUMC     = "<$conf{numbercolor}>";
383$_NUMC    = "</$conf{numbercolor}>";
384$NOTEC    = "<$conf{notecolor}>";
385$_NOTEC   = "</$conf{notecolor}>";
386$TIMEC    = "<$conf{timecolor}>";
387$_TIMEC   = "</$conf{timecolor}>";
388$TOPICC   = "<$conf{topiccolor}>";
389$_TOPICC  = "</$conf{topiccolor}>";
390
391$NoteKey  = $conf{topicseparator} . "notes" . $conf{topicseparator};
392
393
394
395
396# default permissions on new files (tmp)
397umask 077;
398
399
400# load the parent module
401&load_driver(1);
402
403# check wether the user wants to use encryption:
404if ($conf{useencryption} && $NOTEDB::crypt_supported == 1) {
405  if ($conf{cryptmethod} eq "") {
406    $conf{cryptmethod} = "Crypt::IDEA";
407  }
408  if (!exists $ENV{'NOTE_PASSWD'}) {
409    print "note password: ";
410    eval {
411      local($|) = 1;
412      local(*TTY);
413      open(TTY,"/dev/tty") or die "No /dev/tty!";
414      system ("stty -echo </dev/tty") and die "stty failed!";
415      chomp($key = <TTY>);
416      print STDERR "\r\n";
417      system ("stty echo </dev/tty") and die "stty failed!";
418      close(TTY);
419    };
420    if ($@) {
421      $key = <>;
422    }
423  }
424  else {
425    $key = $ENV{'NOTE_PASSWD'};
426  }
427  chomp $key;
428  if ($conf{dbdriver} eq "mysql") {
429    eval {
430      require Crypt::CBC;
431      my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
432      # decrypt the dbpasswd, if it's encrypted!
433      $driver{mysql}->{dbpasswd}  =
434        $cipher->decrypt(unpack("u", $driver{mysql}->{dbpasswd})) if($driver{mysql}->{encrypt_passwd});
435      &load_driver();
436    };
437    die "Could not connect to db: $@!\n" if($@);
438  }
439  else {
440    &load_driver();
441  }
442  $db->use_crypt($key,$conf{cryptmethod});
443  undef $key;
444  # verify correctness of passwd
445  my ($cnote, $cdate) = $db->get_single(1);
446  if ($cdate ne "") {
447    if ($cdate !~ /^\d+\.\d+?/) {
448      print "access denied.\n"; # decrypted $date is not a number!
449      exit(1);
450    }
451  } #else empty database!
452}
453elsif ($conf{useencryption} && $NOTEDB::crypt_supported == 0) {
454  print STDERR "WARNING: You enabled database encryption but neither Crypt::CBC\n";
455  print STDERR "WARNING: or Crypt::$conf{cryptmethod} are installed! Please turn\n";
456  print STDERR "WARNING: off encryption or install the desired modules! Thanks!\n";
457  exit 1;
458}
459else {
460  # as of 1.3.5 we do not fall back to cleartext anymore
461  # I consider this as unsecure, if you don't, fix your installation!
462
463  &load_driver();
464  $db->no_crypt;
465
466  # does: NOTEDB::crypt_supported = 0;
467  my %all = $db->get_all();
468  if(scalar keys %all > 0) {
469    my $id = (keys %all)[0];
470    if($all{$id}->{date} !~ /^\d+\.\d+?/) {
471      print "notedb seems to be encrypted!\n";
472      exit(1);
473    }
474  }
475}
476
477
478# do we use the db cache?
479if ($conf{cache}) {
480  $db->use_cache();
481}
482
483
484# add the backend version to the note version:
485$VERSION .= ", " . $conf{dbdriver} . " " . $db->version();
486
487
488# main loop: ###############
489&$mode;
490exit(0);
491################## EOP ################
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506############ encrypt a given password ##############
507sub encrypt_passwd {
508  my($key, $crypt_string);
509  print "password: ";
510  eval {
511    local($|) = 1;
512    local(*TTY);
513    open(TTY,"/dev/tty") or die "No /dev/tty!";
514    system ("stty -echo </dev/tty") and die "stty failed!";
515    chomp($key = <TTY>);
516    print STDERR "\r\n";
517    system ("stty echo </dev/tty") and die "stty failed!";
518    close(TTY);
519  };
520  if ($@) {
521    $key = <>;
522  }
523  chomp $key;
524  eval {
525    require Crypt::CBC;
526    my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
527    $crypt_string  = pack("u", $cipher->encrypt($clearstring));
528  };
529  if ($@) {
530    print "Something went wrong: $@\n";
531    exit 1;
532  }
533  else {
534    print "Encrypted password:\n$crypt_string\n";
535  }
536}
537############################### MOTD ##################################
538sub motd {
539  my($N,$match,$note,$date,$num);
540  # display a configured motd note, if any
541  ($note, $date) = $db->get_single($conf{motd});
542  if ($note) {
543    print "\n\n$note\n\n";
544  }
545}
546
547############################### DISPLAY ##################################
548sub display {
549    my($N,$match,$note,$date,$num);
550    # display a certain note
551    print "\n";
552    &num_bereich;        # get @NumBlock from $numer
553    my $count = scalar @NumBlock;
554    foreach $N (@NumBlock) {
555      ($note, $date) = $db->get_single($N);
556      if ($note) {
557    if ($Raw) {
558      print "$N\n$date\n$note\n\n";
559    }
560    else {
561      output($N, $note, $date, "SINGLE", $count);
562      print "\n";
563    }
564    $match = 1;
565      }
566      $count--;
567    }
568    if (!$match) {
569      print "no note with that number found!\n";
570    }
571  }
572
573############################### SEARCH ##################################
574sub search {
575    my($n,$match,$note,$date,$num,%res);
576    if ($searchstring eq "") {
577      print "No searchstring specified!\n";
578    }
579    else {
580      print "searching the database $conf{dbname} for \"$searchstring\"...\n\n";
581
582      %res = $db->get_search($searchstring);
583      my $nummatches = scalar keys %res;
584      &determine_width;
585      foreach $num (sort { $a <=> $b } keys %res) {
586        if ($nummatches == 1) {
587          output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "SINGLE");
588        }
589        else {
590          output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "search");
591        }
592        $match = 1;
593      }
594      if (!$match) {
595        print "no matching note found!\n";
596      }
597      print "\n";
598    }
599  }
600
601
602############################### LIST ##################################
603sub list {
604    my(@topic,@RealTopic, $i,$t,$n,$num,@CurItem,$top,$in,%res);
605    if ($mode ne "interactive" && !$Raw) {
606      print "\nList of all existing notes:\n\n";
607    }
608    else {
609      print "\n";
610    }
611
612    # list all available notes (number and firstline)
613    %res = $db->get_all();
614
615    if ($TOPIC) {
616      undef %TP;
617    }
618
619    foreach $num (sort { $a <=> $b } keys %res) {
620      $n = $res{$num}->{'note'};
621      $t = $res{$num}->{'date'};
622      if ($TOPIC) {
623        # this allows us to have multiple topics (subtopics!)
624        my ($firstline,$dummy) = split /\n/, $n, 2;
625        if ($firstline =~ /^($conf{topicseparator})/) {
626          @topic = split(/$conf{topicseparator}/,$firstline);
627        }
628        else {
629          @topic = ();
630        }
631        # looks like: "\topic\"
632        # collect a list of topics under the current topic
633        if ($topic[$CurDepth-1] eq $CurTopic && $topic[$CurDepth] ne "") {
634          if (exists $TP{$topic[$CurDepth]}) {
635            $TP{$topic[$CurDepth]}++;
636          }
637          else {
638            # only if the next item *is* a topic!
639            $TP{$topic[$CurDepth]} = 1 if(($CurDepth) <= $#topic);
640          }
641        }
642        elsif ($topic[$CurDepth-1] eq $CurTopic || ($topic[$CurDepth] eq "" && $CurDepth ==1)) {
643          # cut the topic off the note-text
644          if ($n =~ /^($conf{topicseparator})/) {
645            $CurItem[$i]->{'note'} = $dummy;
646          }
647          else {
648            $CurItem[$i]->{'note'} = $n;
649          }
650          # save for later output() call
651          $CurItem[$i]->{'num'}  = $num;
652          $CurItem[$i]->{'time'} = $t;
653          $i++;
654          # use this note for building the $PATH!
655          if ($RealTopic[0] eq "") {
656            @RealTopic = @topic;
657          }
658        }
659      }
660      else {
661        output($num, $n, $t);
662      }
663    }
664    if ($TOPIC) {
665      if ($CurTopic ne "") {
666        if ($i) {
667          # only if there were notes under current topic
668          undef $PATH;
669          foreach (@RealTopic) {
670            $PATH .= $_ . $conf{topicseparator};
671            last if($_ eq $CurTopic);
672          }
673        }
674        else {
675          # it is an empty topic, no notes here
676          $PATH = join $conf{topicseparator}, @LastTopic;
677          $PATH .= $conf{topicseparator} . $CurTopic . $conf{topicseparator};
678          $PATH =~ s/^\Q$conf{topicseparator}$conf{topicseparator}\E/$conf{topicseparator}/;
679        }
680      }
681      else {
682        $PATH = $conf{topicseparator};
683      }
684
685      @completion_topics = ();
686      @completion_notes  = ();
687      # we are at top level, print a list of topics...
688      foreach $top (sort(keys %TP)) {
689        push @completion_topics, $top;
690        output("-", " => ". $top . "$conf{topicseparator} ($TP{$top} notes)",
691               " Sub Topic         ");
692      }
693      #print Dumper(@CurItem);
694      for ($in=0;$in<$i;$in++) {
695        push @completion_notes, $CurItem[$in]->{'num'};
696        output( $CurItem[$in]->{'num'},
697                $CurItem[$in]->{'note'},
698                $CurItem[$in]->{'time'} );
699      }
700    }
701
702    print "\n";
703  }
704
705############################### NEW ##################################
706sub new {
707    my($TEMP,$editor, $date, $note, $WARN, $c, $line, $num, @topic);
708    if ($conf{readonly}) {
709    print "readonly\n";
710    return;
711    }
712    $date = &getdate;
713    return if $db->lock();
714    if ($conf{alwayseditor}) {
715      $TEMP = &gettemp;
716      # security!
717      unlink $TEMP;
718      # let the user edit it...
719      $editor = &find_editor;
720      if ($editor) {
721        # create the temp file
722        open NEW, "> $TEMP" or die "Could not write $TEMP: $!\n";
723        close NEW;
724        system "chattr", "+s", $TEMP; # ignore errors, since only on ext2 supported!
725        system $editor, $TEMP;
726      }
727      else {
728        print "Could not find an editor to use!\n";
729        $db->unlock();
730        exit(0);
731      }
732      # read it in ($note)
733      $note = "";
734      open E, "<$TEMP" or $WARN = 1;
735      if ($WARN) {
736        print "...edit process interupted! No note has been saved.\n";
737        undef $WARN;
738        $db->unlock();
739        return;
740      }
741      $c = 0;
742      while (<E>) {
743        $note = $note . $_;
744      }
745      chomp $note;
746      close E;
747      # privacy!
748      unlink $TEMP;
749    }
750    else {
751      $note = "";
752      $line = "";
753      # create a new note
754      if ($NewType) {
755        # be silent! read from STDIN until EOF.
756        while (<STDIN>) {
757          $note .= $_;
758        }
759      }
760      else {
761        print "enter the text of the note, end with a single .\n";
762        do
763          {
764            $line = <STDIN>;
765            $note = $note . $line;
766          } until $line eq ".\n";
767        # remove the . !
768        chop $note;
769        chop $note;
770      }
771    }
772    # look if the note was empty, so don't store it!
773    if ($note =~ /^\s*$/) {
774      print "...your note was empty and will not be saved.\n";
775      $db->unlock();
776      return;
777    }
778    # since we have not a number, look for the next one available:
779    $number = $db->get_nextnum();
780    if ($TOPIC && $CurTopic ne "") {
781      @topic = split(/$conf{topicseparator}/,$note);
782      if ($topic[1] eq "") {
783        $note = $PATH . "\n$note";
784      }
785    }
786    $note = &add_ticket($note);
787
788    $db->set_new($number,$note,$date);
789    # everything ok until here!
790    print "note stored. it has been assigned the number $number.\n\n";
791    $db->unlock();
792  }
793
794sub add_ticket {
795    my $orignote = shift;
796    if ($conf{addticket}) {
797      my ($topic, $title, $rest) = split /\n/, $orignote, 3;
798      my $note = "";
799      if ($topic =~ /^\//) {
800        # topic path, keep it
801        $note .= "$topic\n";
802      }
803      else {
804        # no topic
805        $rest  = "$title\n$rest";
806        $title = $topic;
807      }
808      if ($title !~ /^\[[a-z0-9A-Z]+\]/) {
809        # no ticket number, so create one
810        my $ticket = &ticket();
811        $title = "[" . ticket() . "] " . $title;
812      }
813      $note .= "$title\n$rest";
814      return $note;
815    }
816    else {
817      return $orignote;
818    }
819}
820
821
822############################### DELETE ##################################
823sub del {
824    my($i,@count, $setnum, $pos, $ERR);
825    if ($conf{readonly}) {
826        print "readonly\n";
827    return;
828    }
829    # delete a note
830    &num_bereich;        # get @NumBlock from $number
831
832    return if $db->lock();
833
834    foreach $_ (@NumBlock) {
835      $ERR = $db->set_del($_);
836      if ($ERR) {
837        print "no note with number $_ found!\n";
838      }
839      else {
840        print "note number $_ has been deleted.\n";
841      }
842    }
843    # recount the notenumbers:
844    $db->set_recountnums();
845
846    $db->unlock();
847    @NumBlock = ();
848  }
849
850############################### EDIT ##################################
851sub edit {
852    my($keeptime, $date, $editor, $TEMP, $note, $t, $num, $match, $backup);
853    if ($conf{readonly}) {
854       print "readonly\n";
855       return;
856    }
857    # edit a note
858    $date = &getdate;
859
860    return if $db->lock();
861
862    ($note, $keeptime) = $db->get_single($number);
863    if ($keeptime eq "") {
864      print "no note with that number found ($number)!\n\n";
865      if($mode ne "interactive") {
866        $db->unlock();
867        exit(0);
868      }
869      else {
870        $db->unlock();
871        return;
872      }
873    }
874
875    $TEMP = &gettemp;
876    open NOTE,">$TEMP" or die "Could not open $TEMP\n";
877    select NOTE;
878
879    system "chattr", "+s", $TEMP; # ignore errors, like in new()
880
881    print $note;
882    close NOTE;
883    select STDOUT;
884    $editor = &find_editor;
885
886    $backup = $note;
887
888    if ($editor) {
889      system ($editor, $TEMP) and die "Could not execute $editor: $!\n";
890    }
891    else {
892      print "Could not find an editor to use!\n";
893      exit(0);
894    }
895    $note = "";
896    open NOTE,"<$TEMP" or die "Could not open $TEMP\n";
897
898    while (<NOTE>) {
899      $note = $note . $_;
900    }
901    chomp $note;
902    close NOTE;
903
904    unlink $TEMP || die $!;
905
906    if ($note ne $backup) {
907      if ($conf{keeptimestamp}) {
908        $t = $keeptime;
909      }
910      else {
911        $t = $date;
912      }
913      # we got it, now save to db
914      $db->set_edit($number, $note, $t);
915
916      print "note number $number has been changed.\n";
917    }
918    else {
919      print "note number $number has not changed, no save done.\n";
920    }
921    $db->unlock();
922  }
923
924sub dump {
925    my(%res, $num, $DUMP);
926    # $dump_file
927    if ($dump_file eq "-") {
928      $DUMP = *STDOUT;
929    }
930    else {
931      open (DUMPFILE, ">$dump_file") or die "could not open $dump_file\n";
932      $DUMP = *DUMPFILE;
933    }
934    select $DUMP;
935    %res = $db->get_all();
936    # FIXME: prepare hashing in NOTEDB class
937    foreach $num (sort { $a <=> $b } keys %res) {
938      print STDOUT "dumping note number $num to $dump_file\n" if($dump_file ne "-");
939      my($title, $path, $body);
940      if ($res{$num}->{note} =~ /^\//) {
941        ($path, $title, $body) = split /\n/, $res{$num}->{note}, 3;
942      }
943      else {
944        ($title, $body) = split /\n/, $res{$num}->{note}, 2;
945        $path = '';
946      }
947      my $date = $res{$num}->{date};
948      $res{$num} = { body => $body, title => $title, path => $path, date => $date};
949    }
950    print Dump(\%res);
951    close(DUMPFILE);
952    select STDOUT;
953}
954
955sub import {
956    my($num, $start, $complete, $dummi, $note, $date, $time, $number, $stdin, $DUMP, %data);
957    # open $dump_file and import it into the notedb
958    $stdin = 1 if($dump_file eq "-");
959    if ($stdin) {
960      $DUMP = *STDIN;
961    }
962    else {
963      open (DUMPFILE, "<$dump_file") or die "could not open $dump_file\n";
964      $DUMP = *DUMPFILE;
965    }
966
967    my $yaml = join '', <$DUMP>;
968
969    my $res = Load($yaml);
970
971    foreach my $number (keys %{$res}) {
972      my $note;
973      if ($res->{$number}->{path}) {
974        $note = "$res->{$number}->{path}\n$res->{$number}->{title}\n$res->{$number}->{body}";
975      }
976      else {
977        $note = "$res->{$number}->{title}\n$res->{$number}->{body}";
978      }
979      $data{$number} = {
980                        date => $res->{$number}->{date},
981                        note => &add_ticket($note)
982                       };
983      print "fetched note number $number from $dump_file from $res->{$number}->{date}.\n" if(!$stdin);
984      $number++;
985    }
986
987    $db->set_del_all() if($ImportType ne "");
988    $db->import_data(\%data);
989}
990
991
992
993sub determine_width {
994  # determine terminal wide, if possible
995  if ($maxlen eq "auto") {
996    eval {
997      my $wide = `stty -a`;
998      if ($wide =~ /columns (\d+?);/) {
999        $maxlen = $1 - 32; # (32 = timestamp + borders)
1000      }
1001      elsif ($wide =~ /; (\d+?) columns;/) {
1002        # bsd
1003        $maxlen = $1 - 32; # (32 = timestamp + borders)
1004      }
1005      else {
1006        # stty didn't work
1007        $maxlen = 80 - 32;
1008      }
1009    };
1010  }
1011}
1012
1013sub clear {
1014  # first, try to determine the terminal height
1015  return if(!$conf{autoclear});
1016  my $hoch;
1017  eval {
1018    my $height = `stty -a`;
1019    if ($height =~ /rows (\d+?);/) {
1020      $hoch = $1;
1021    }
1022    elsif ($height =~ /; (\d+?) rows;/) {
1023      # bsd
1024      $hoch = $1;
1025    }
1026  };
1027  if (!$hoch) {
1028    # stty didn't work
1029    $hoch = 25;
1030  }
1031  print "\n" x $hoch;
1032}
1033
1034sub interactive {
1035    my($B, $BB, $menu, $char, $Channel);
1036    $Channel = $|;
1037    local $| = 1;
1038    # create menu:
1039    $B = "<white_black>";
1040    $BB = "</white_black>";
1041    $menu =     "[" .  $B . "L" . $BB . "-List ";
1042    if ($TOPIC) {
1043      $menu .= $B . "T" . $BB . "-Topics ";
1044    }
1045    $menu   .= $B . "N" . $BB . "-New "
1046             . $B . "D" . $BB . "-Delete "
1047             . $B . "S" . $BB . "-Search "
1048             . $B . "E" . $BB . "-Edit "
1049             . $B . "?" . $BB . "-Help "
1050             . $B . "Q" . $BB . "-Quit] "; # $CurTopic will be empty if $TOPIC is off!
1051
1052    # per default let's list all the stuff:
1053    # Initially do a list command!
1054    &determine_width;
1055    $ListType = ($conf{defaultlong}) ? "LONG" : "";
1056
1057    # show initial note entry
1058    if ($conf{motd}) {
1059      &motd;
1060    }
1061
1062    # show initial listing
1063    &list;
1064
1065    my ($term, $prompt, $attribs);
1066    eval { require Term::ReadLine; };
1067    if (!$@) {
1068      $term = new Term::ReadLine('');
1069      $attribs = $term->Attribs;
1070      $attribs->{completion_function} = \&complete;
1071    }
1072
1073    for (;;) {
1074      $ListType = ($conf{defaultlong}) ? "LONG" : "";
1075      undef $SetTitle;
1076      if ($CurDepth > 2) {
1077        print C $menu . $TOPICC . "../" . $CurTopic . $_TOPICC;
1078      }
1079      else {
1080        print C $menu . $TOPICC . $CurTopic . $_TOPICC;
1081      }
1082
1083      if ($conf{readonly}) {
1084        print " [readonly] ";
1085      }
1086
1087      print ">";
1088
1089      # endless until user press "Q" or "q"!
1090      if ($term) {
1091        if (defined ($char = $term->readline(" "))) {
1092          $term->addhistory($char) if $char =~ /\S/;
1093          $char =~ s/\s*$//; # remove trailing whitespace (could come from auto-completion)
1094        }
1095        else {
1096          # shutdown
1097          $| = $Channel;
1098          print "\n\ngood bye!\n";
1099          exit(0);
1100        }
1101      }
1102      else {
1103        $char = <STDIN>;
1104        chomp $char;
1105      }
1106
1107      &determine_width;
1108      &clear;
1109
1110      if ($char =~ /^\d+\s*[\di*?,*?\-*?]*$/) {
1111        $ListType = "";        #overrun
1112        # display notes
1113        $number = $char;
1114        &display;
1115      }
1116      elsif ($char =~ /^n$/i) {
1117        # create a new one
1118        &new;
1119      }
1120      elsif ($char =~ /^$/) {
1121        &list;
1122      }
1123      elsif ($char =~ /^l$/) {
1124        $ListType = "";
1125        &list;
1126      }
1127      elsif ($char =~ /^L$/) {
1128        $ListType = "LONG";
1129        &list;
1130        undef $SetTitle;
1131      }
1132      elsif ($char =~ /^h$/i || $char =~ /^\?/) {
1133        # zu dumm der Mensch ;-)
1134        &help;
1135      }
1136      elsif ($char =~ /^d\s+([\d*?,*?\-*?]*)$/i) {
1137        # delete one!
1138        $number = $1;
1139        &del;
1140      }
1141      elsif ($char =~ /^d$/i) {
1142        # we have to ask her:
1143        print "enter number(s) of note(s) you want to delete: ";
1144        $char = <STDIN>;
1145        chomp $char;
1146        $number = $char;
1147        &del;
1148      }
1149      elsif ($char =~ /^e\s+(\d+\-*\,*\d*)/i) {
1150        # edit one!
1151        $number = $1;
1152        &edit;
1153      }
1154      elsif ($char =~ /^e$/i) {
1155        # we have to ask her:
1156        print "enter number of the note you want to edit: ";
1157        $char = <STDIN>;
1158        chomp $char;
1159        $number = $char;
1160        &edit;
1161      }
1162      elsif ($char =~ /^s\s+/i) {
1163        # she want's to search
1164        $searchstring = $';
1165        chomp $searchstring;
1166        &search;
1167      }
1168      elsif ($char =~ /^s$/i) {
1169        # we have to ask her:
1170        print "enter the string you want to search for: ";
1171        $char = <STDIN>;
1172        chomp $char;
1173        $char =~ s/^\n//;
1174        $searchstring = $char;
1175        &search;
1176      }
1177      elsif ($char =~ /^q$/i) {
1178        # schade!!!
1179        $| = $Channel;
1180        print "\n\ngood bye!\n";
1181        exit(0);
1182      }
1183      elsif ($char =~ /^t$/) {
1184        $TreeType = "";
1185        &display_tree;
1186      }
1187      elsif ($char =~ /^T$/) {
1188        $TreeType = "LONG";
1189        &display_tree;
1190        $TreeType = "";
1191      }
1192      elsif ($char =~ /^c\s*$/) {
1193        print "Missing parameter (parameter=value), available ones:\n";
1194        foreach my $var (sort keys %conf) {
1195          if ($var !~ /^$hardparams/ && $var !~ /::/) {
1196            printf "%20s = %s\n", $var, $conf{$var};
1197          }
1198        }
1199      }
1200      elsif ($char =~ /^c\s*(.+?)\s*=\s*(.+?)/) {
1201        # configure
1202        my $param = $1;
1203        my $value = $2;
1204        if ($param !~ /^$hardparams/ && $param !~ /::/ && exists $conf{$param}) {
1205          print "Changing $param from $conf{$param} to $value\n";
1206          $conf{$param} = $value;
1207        }
1208        else {
1209          print "Unknown config parameter $param!\n";
1210        }
1211      }
1212      elsif ($char =~ /^\.\.$/ || $char =~ /^cd\s*\.\.$/) {
1213        $CurDepth-- if ($CurDepth > 1);
1214        $CurTopic = $LastTopic[$CurDepth];
1215        pop @LastTopic; # remove last element
1216        &list;
1217      }
1218      elsif ($char =~ /^l\s+(\w+)$/) {
1219        # list
1220        $WantTopic = $1;
1221        if (exists $TP{$WantTopic}) {
1222          my %SaveTP = %TP;
1223          $LastTopic[$CurDepth] = $CurTopic;
1224          $CurTopic = $1;
1225          $CurDepth++;
1226          &list;
1227          $CurTopic = $LastTopic[$CurDepth];
1228          $CurDepth--;
1229          %TP = %SaveTP;
1230        }
1231        else {
1232          print "\nunknown command!\n";
1233        }
1234      }
1235      else {
1236        # unknown
1237        my $unchar = $char;
1238        $unchar =~ s/^cd //;    # you may use cd <topic> now!
1239        if ($unchar =~ /^\d+?$/ && $conf{short_cd}) {
1240          # just a number!
1241          my @topic;
1242          my ($cnote, $cdate) = $db->get_single($unchar);
1243          my ($firstline,$dummy) = split /\n/, $cnote, 2;
1244          if ($firstline =~ /^($conf{topicseparator})/) {
1245            @topic = split(/$conf{topicseparator}/,$firstline);
1246          }
1247          else {
1248            @topic = ();
1249          }
1250          if (@topic) {
1251            # only jump, if, and only if there were at least one topic!
1252            $CurDepth = $#topic + 1;
1253            $CurTopic = pop @topic;
1254            @LastTopic = ("");
1255            push @LastTopic, @topic;
1256          }
1257          &list;
1258        }
1259        elsif ($unchar eq $conf{topicseparator}) {
1260          # cd /
1261          $CurDepth = 1;
1262          $CurTopic = "";
1263          &list;
1264        }
1265        elsif (exists $TP{$char} || exists $TP{$unchar}) {
1266          $char = $unchar if(exists $TP{$unchar});
1267          $LastTopic[$CurDepth] = $CurTopic;
1268          $CurTopic = $char;
1269          $CurDepth++;
1270          &list;
1271        }
1272        else {
1273          # try incomplete match
1274          my @matches;
1275          foreach my $topic (keys %TP) {
1276            if ($topic =~ /^$char/) {
1277              push @matches, $topic;
1278            }
1279          }
1280          my $nm = scalar @matches;
1281          if ($nm == 1) {
1282            # match on one incomplete topic, use this
1283            $LastTopic[$CurDepth] = $CurTopic;
1284            $CurTopic = $matches[0];
1285            $CurDepth++;
1286            &list;
1287          }
1288          elsif ($nm > 1) {
1289            print "available topics: " . join( "," , @matches) . "\n";
1290          }
1291          else {
1292            print "\nunknown command!\n";
1293          }
1294        }
1295        undef $unchar;
1296      }
1297    }
1298  }
1299
1300
1301sub usage
1302  {
1303    print qq~This is the program note $VERSION by T.v.Dein (c) 1999-2017.
1304It comes with absolutely NO WARRANTY. It is distributed under the
1305terms of the GNU General Public License. Use it at your own risk :-)
1306
1307Usage:     note [ options ] [ number [,number...]]
1308
1309Options:
1310
1311       -c, --config file
1312           Use another config file than the default \$HOME/.noterc.
1313
1314       -l, --list [topic]
1315           Lists all existing notes. If no topic were specified, it will
1316           display a list of all existing topics.  See the section TOPICS for
1317           details about topics.
1318
1319       -L, --longlist [topic]
1320           The same as -l but prints also the timestamp of the notes.
1321
1322       -t, --topic
1323           Prints a list of all topics as a tree.
1324
1325       -T, --longtopic
1326           Prints the topic-tree with the notes under each topic.
1327
1328       -s, --search string
1329           Searches for <string> trough the notes database. See the section
1330           SEARCHING for details about the search engine.
1331
1332       -e, --edit number
1333           Edit the note with the number <number> using your default editor or
1334           the one you specified in the config file.
1335
1336       -d, --delete number
1337           Delete the note with the number <number>. You can delete multiple
1338           notes with one command. "1-4" deletes the notes 1,2,3,4. And
1339           "1,5,7" deletes the specified ones.
1340
1341       -D, --Dump [file | -]
1342           Dumps all notes to the textfile <file>. If <file> is a "-" it will
1343           be printed out to standard output (STDOUT).
1344
1345       -I, --Import file | -
1346           Imports a previously dumped textfile into the note database. Data
1347           will be appended by default.  You can also specify a dash note -I -
1348           instead of a <file>, which causes note, silently to read in a dump
1349           from STDIN.
1350
1351       -o, --overwrite
1352           Only suitable for use with --Import. Overwrites an existing notedb.
1353           Use with care.
1354
1355       -r, --raw
1356           Raw mode, output will not be formatted. Works not in interactive
1357           mode, only on cmd-line for list and display. That means, no colors
1358           will be used and no lines or titles.
1359
1360       -i, --interactive
1361           Start note in interactive mode. See the section INTERACTIVE MODE
1362           for details on this mode.
1363
1364       --encrypt cleartext
1365           Encrypt the given clear text string. You would need that if you
1366           want to store the mysql password not in cleartext in the config(if
1367           you are using the mysql backend!).
1368
1369       -h, --help
1370           Display this help screen.
1371
1372       -v, --version
1373           Display the version number.
1374
1375       -   If you run note just with one dash: note -, then it will read in a
1376           new note from STDIN until EOF. This makes it possible to pipe text
1377           into a new note, i.e.:
1378
1379            cat sometextfile | note -
1380
1381Read the note(1) manpage for more details.
1382~;
1383    exit 1;
1384  }
1385
1386sub find_editor {
1387  return $conf{preferrededitor} || $ENV{"VISUAL"} || $ENV{"EDITOR"} || "vi";
1388}
1389
1390#/
1391
1392sub format {
1393  # make text bold/underlined/inverse using current $NOTEC
1394  my($note) = @_;
1395  if ($conf{formattext}) {
1396    # prepare colors to be used for replacement
1397    my $BN  = uc($NOTEC);
1398    my $_BN = uc($_NOTEC);
1399    my $UN  = $NOTEC;
1400    $UN     =~ s/<(.*)>/<$1_>/;
1401    my $_UN = $UN;
1402    $_UN    =~ s/<(.*)>/<\/$1>/;
1403    my $IN  = $NOTEC;
1404    my $_IN = $_NOTEC;
1405    $IN     =~ s/<(.*)>/<$1I>/;
1406    $_IN    =~ s/<(.*)>/<$1I>/;
1407
1408    if ($conf{formattext} eq "simple") {
1409      $note =~ s/\*([^\*]*)\*/$BN$1$_BN/g;
1410      $note =~ s/_([^_]*)_/$UN$1$_UN/g;
1411      $note =~ s/{([^}]*)}/$IN$1$_IN/g;
1412      $note =~ s#(?<!<)/([^/]*)/#<hide>$1</hide>#g;
1413    }
1414    else {
1415      $note =~ s/\*\*([^\*]{2,})\*\*/$BN$1$_BN/g;
1416      $note =~ s/__([^_]{2,})__/$UN$1$_UN/g;
1417      $note =~ s/\{\{([^}]{2,})\}\}/$IN$1$_IN/g;
1418      $note =~ s#//([^/]{2,})//#<hide>$1</hide>#g;
1419    }
1420
1421    $note =~ s/(<\/.*>)/$1$NOTEC/g;
1422  }
1423  $note;
1424}
1425
1426sub output {
1427    my($SSS, $LINE, $num, $note, $time, $TYPE, $L, $LONGSPC, $R, $PathLen, $SP, $title, $CUTSPACE,
1428       $VersionLen, $len, $diff, $Space, $nlen, $txtlen, $count);
1429    ($num, $note, $time, $TYPE, $count) = @_;
1430
1431    $txtlen = ($ListType eq "LONG") ? $maxlen : $timelen + $maxlen;
1432    $note = &format($note);
1433
1434    $SSS = "-" x ($maxlen + 30);
1435    $nlen = length("$num");
1436    $LINE = "$BORDERC $SSS $_BORDERC\n";
1437    $LONGSPC = " " x (25 - $nlen);
1438    if ($conf{printlines}) {
1439      $L = $BORDERC . "[" . $_BORDERC;
1440      $R = $BORDERC . "]" . $_BORDERC;
1441    }
1442    $PathLen = length($PATH);    # will be ZERO, if not in TOPIC mode!
1443    $VersionLen = length($VERSION) + 7;
1444
1445    if ($TYPE ne "SINGLE") {
1446      if (!$SetTitle) {
1447        $SP = "";
1448        # print only if it is the first line!
1449        $SP = " " x ($maxlen - 2 - $PathLen - $VersionLen);
1450        if (!$Raw) {
1451          # no title in raw-mode!
1452          print C $LINE if ($conf{printlines});
1453          print C "$L $NUMC#$_NUMC  ";
1454          if ($ListType eq "LONG") {
1455            print C " $TIMEC" . "creation date$_TIMEC          ";
1456          }
1457          else {
1458            print $LONGSPC if ($conf{printlines});
1459          }
1460          if ($TOPIC) {
1461            print C $TOPICC . "$PATH    $_TOPICC$SP" . " note $VERSION $R\n";
1462          }
1463          else {
1464            print C $NOTEC . "note$_NOTEC$SP" . " note $VERSION $R\n";
1465          }
1466          print C $LINE if ($conf{printlines});
1467        }
1468        $SetTitle = 1;
1469      }
1470      $title = "";
1471      $CUTSPACE = " " x $txtlen;
1472      if ($TYPE eq "search") {
1473        $note =~ s/^\Q$conf{topicseparator}\E.+?\Q$conf{topicseparator}\E\n//;
1474      }
1475      $note =~ s/\n/$CUTSPACE/g;
1476      $len   = length($note);
1477      if ($len < ($txtlen - 2 - $nlen)) {
1478        $diff = $txtlen - $len;
1479        if (!$Raw) {
1480          if ($num eq "-") {
1481            $Space = " " x $diff;
1482            $title = $BORDERC . $TOPICC . $note . " " . $_TOPICC . $Space . "$_BORDERC";
1483          }
1484          else {
1485            $Space = " " x ($diff - ($nlen - 1));
1486            $title = $BORDERC . $NOTEC  . $note . " " . $_NOTEC . $Space . "$_BORDERC";
1487          }
1488        }
1489        else {
1490          $title = $note;
1491        }
1492      }
1493      else {
1494        $title = substr($note,0,($txtlen - 2 - $nlen));
1495        if (!$Raw) {
1496          $title = $BORDERC . $NOTEC . $title . " $_NOTEC$_BORDERC";
1497        }
1498      }
1499      if ($Raw) {
1500        print "$num  ";
1501        print "$time  " if($ListType eq "LONG");
1502        if ($title =~ /^ => (.*)$conf{topicseparator} (.*)$/) {
1503          $title = "$1$conf{topicseparator} $2"; # seems to be a topic!
1504        }
1505        print "$title\n";
1506      }
1507      else {
1508        # $title should now look as: "A sample note                       "
1509        print C "$L $NUMC$num$_NUMC $R";
1510        if ($ListType eq "LONG") {
1511          print C "$L$TIMEC" . $time . " $_TIMEC$R";
1512        }
1513        print C "$L $NOTEC" . $title . "$_NOTEC $R\n";
1514        print C $LINE if ($conf{printlines});
1515      }
1516    }
1517    else {
1518      # we will not reach this in raw-mode, therefore no decision here!
1519      chomp $note;
1520      $Space = " " x (($maxlen + $timelen) - $nlen - 16);
1521
1522      *CHANNEL = *STDOUT;
1523      my $usecol = $conf{usecolors};
1524
1525      if ($conf{less}) {
1526        my $less = "less";
1527        if ($conf{less} ne 1) {
1528          # use given less command line
1529          $less = $conf{less};
1530        }
1531        if (open LESS, "|$less") {
1532          *CHANNEL = *LESS;
1533          $conf{usecolors} = 0;
1534        }
1535      }
1536
1537      print CHANNEL C $LINE if ($conf{printlines});
1538      print CHANNEL C "$L $NUMC$num$_NUMC $R$L$TIMEC$time$_TIMEC $Space$R\n";
1539      print CHANNEL C "\n";
1540      print CHANNEL C $NOTEC . $note . $_NOTEC . "\n";
1541      print CHANNEL C $LINE if ($count == 1 && $conf{printlines});
1542
1543      if ($conf{less}) {
1544        close LESS;
1545      }
1546
1547      $conf{usecolors} = $usecol;
1548    }
1549  }
1550
1551
1552
1553sub C {
1554    my($default, $S, $Col, $NC, $T);
1555    $default = "\033[0m";
1556    $S = $_[0];
1557    foreach $Col (%Color) {
1558      if ($S =~ /<$Col>/g) {
1559        if ($conf{usecolors}) {
1560          $NC = "\033[" . $Color{$Col} . "m";
1561          $S =~ s/<$Col>/$NC/g;
1562          $S =~ s/<\/$Col>/$default/g;
1563        }
1564        else {
1565          $S =~ s/<$Col>//g;
1566          $S =~ s/<\/$Col>//g;
1567        }
1568      }
1569    }
1570    return $S;
1571  }
1572
1573
1574
1575sub num_bereich {
1576    my($m,@LR,@Sorted_LR,$i);
1577    # $number is the one we want to delete!
1578    # But does it contain commas?
1579    @NumBlock = ();        #reset
1580    $m = 0;
1581    if ($number =~ /\,/) {
1582      # accept -d 3,4,7
1583      @NumBlock = split(/\,/,$number);
1584    }
1585    elsif ($number =~ /^\d+\-\d+$/) {
1586      # accept -d 3-9
1587      @LR = split(/\-/,$number);
1588      @Sorted_LR = ();
1589
1590      if ($LR[0] > $LR[1]) {
1591        @Sorted_LR = ($LR[1], $LR[0]);
1592      }
1593      elsif ($LR[0] == $LR[1]) {
1594        # 0 and 1 are the same
1595        @Sorted_LR = ($LR[0], $LR[1]);
1596      }
1597      else {
1598        @Sorted_LR = ($LR[0], $LR[1]);
1599      }
1600
1601      for ($i=$Sorted_LR[0]; $i<=$Sorted_LR[1]; $i++) {
1602        # from 3-6 create @NumBlock (3,4,5,6)
1603        $NumBlock[$m] = $i;
1604        $m++;
1605      }
1606    }
1607    else {
1608      @NumBlock = ($number);
1609    }
1610
1611  }
1612
1613sub getdate {
1614    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
1615    $year += 1900;
1616    $mon  += 1;
1617    $mon  =~ s/^(\d)$/0$1/;
1618    $hour =~ s/^(\d)$/0$1/;
1619    $min  =~ s/^(\d)$/0$1/;
1620    $sec  =~ s/^(\d)$/0$1/;
1621    $mday =~ s/^(\d)$/0$1/;
1622    if ($conf{timeformat}) {
1623      my $back = $conf{timeformat};
1624      $back =~ s/YYYY/$year/;
1625      $back =~ s/YY/substr($year, 1, 2)/e;
1626      $back =~ s/MM/$mon/;
1627      $back =~ s/DD/$mday/;
1628      $back =~ s/mm/$min/;
1629      $back =~ s/hh/$hour/;
1630      $back =~ s/ss/$sec/;
1631      return $back;
1632    }
1633    return "$mday.$mon.$year $hour:$min:$sec";
1634  }
1635
1636
1637sub gettemp {
1638    my($random, @range);
1639    @range=('0'..'9','a'..'z','A'..'Z');
1640    srand(time||$$);
1641    for (0..10) {
1642      $random .= $range[rand(int($#range)+1)];
1643    }
1644    my $tempfile = File::Spec->catfile($conf{tempdirectory}, $USER . $random);
1645    if (-e $tempfile) {
1646      # avoid race conditions!
1647      unlink $tempfile;
1648    }
1649    return $tempfile;
1650  }
1651
1652
1653
1654sub help {
1655    my $B = "<white_black>";
1656    my $BB = "</white_black>";
1657    my($S, $L, $T, $Q, $H, $N, $D, $E, $C);
1658    $L = $B . "L" . $BB . $NOTEC;
1659    $T = $B . "T" . $BB . $NOTEC;
1660    $Q = $B . "Q" . $BB . $NOTEC;
1661    $H = $B . "?" . $BB . $NOTEC;
1662    $N = $B . "N" . $BB . $NOTEC;
1663    $D = $B . "D" . $BB . $NOTEC;
1664    $E = $B . "E" . $BB . $NOTEC;
1665    $S = $B . "S" . $BB . $NOTEC;
1666    $C = $B . "C" . $BB . $NOTEC;
1667
1668    print C qq~$BORDERC
1669----------------------------------------------------------------------$_BORDERC $TOPICC
1670HELP for interactive note       $VERSION
1671$_TOPICC $NOTEC
1672The following commands are available:
1673$L       List notes. L=long, with timestamp and l=short without timestamp.
1674        You can also just hit <enter> for short list.
1675        If you specify a subtopic, then list will display it's contents,
1676        i.e.: "l mytopic" will dislpay notes under mytopic.
1677$N       Create a new note.
1678$D       Delete a note. You can either hit "d 1" or "d 1-4" or just hit "d".
1679        If you don't specify a number, you will be asked for.
1680$S       Search trough the notes database. Usage is similar to Delete, use
1681        a string instead of a number to search for.
1682$E       Edit a note. Usage is similar to Delete but you can only edit note
1683        a time.
1684$C       Change note config online. Use with care!
1685$H       This help screen.
1686$Q       Exit the program.~;
1687    if ($TOPIC) {
1688      print C qq~
1689$T       print a list of all existing topics as a tree. T prints the tree
1690        with all notes under each topic.~;
1691    }
1692    print C qq~
1693
1694All commands except the List and Topic commands are case insensitive.
1695Read the note(1) manpage for more details.$BORDERC
1696----------------------------------------------------------------------$_BORDERC
1697~;
1698  }
1699
1700
1701sub display_tree {
1702  # displays a tree of all topics
1703  my(%TREE, %res, $n, $t, $num, @nodes, $firstline, $text, $untext);
1704  %res = $db->get_all();
1705  foreach $num (keys %res) {
1706    $n = $res{$num}->{'note'};
1707    $t = $res{$num}->{'date'};
1708    # this allows us to have multiple topics (subtopics!)
1709    my ($firstline,$text,$untext) = split /\n/, $n, 3;
1710    if ($firstline =~ /^($conf{topicseparator})/) {
1711      $firstline =~ s/($conf{topicseparator})*$//; #remove Topicseparator
1712      @nodes = split(/$conf{topicseparator}/,$firstline);
1713    }
1714    else {
1715      @nodes = (); #("$conf{topicseparator}");
1716      $text = $firstline;
1717    }
1718    &determine_width; # ensure $maxlen values for &tree in non interactive modes
1719    &tree($num, $text, \%TREE, @nodes);
1720  }
1721  #return if ($num == 0);
1722  # now that we have build our tree (in %TREE) go on t display it:
1723  print C $BORDERC . "\n[" . $conf{topicseparator} . $BORDERC . "]\n";
1724  &print_tree(\%{$TREE{''}},"") if(%TREE);
1725  print C $BORDERC . $_BORDERC . "\n";
1726}
1727
1728
1729sub tree {
1730  my($num, $text, $LocalTree, $node, @nodes) = @_;
1731  if (@nodes) {
1732    if (! exists $LocalTree->{$node}->{$NoteKey}) {
1733      $LocalTree->{$node}->{$NoteKey} = [];
1734    }
1735    &tree($num, $text, $LocalTree->{$node}, @nodes);
1736  }
1737  else {
1738    if (length($text) > ($maxlen - 5)) {
1739      $text = substr($text, 0, ($maxlen -5));
1740    }
1741    $text = $text . " (" . $NUMC . "#" . $num . $_NUMC . $NOTEC . ")" . $_NOTEC if($text ne "");
1742    push @{$LocalTree->{$node}->{$NoteKey}}, $text;
1743  }
1744}
1745
1746
1747sub print_tree {
1748  # thanks to Jens for his hints and this sub!
1749  my $hashref=shift;
1750  my $prefix=shift;
1751  my @notes=@{$hashref->{$NoteKey}};
1752  my @subnotes=sort grep { ! /^$NoteKey$/ } keys %$hashref;
1753  if ($TreeType eq "LONG") {
1754    for my $note (@notes) {
1755      if ($note ne "") {
1756        print C $BORDERC ;    # . $prefix. "|\n";
1757        print C "$prefix+---<" . $NOTEC . $note . $BORDERC . ">" . $_NOTEC . "\n";
1758      }
1759    }
1760  }
1761  for my $index (0..$#subnotes) {
1762    print C $BORDERC . $prefix. "|\n";
1763    print C "$prefix+---[" . $TOPICC . $subnotes[$index] . $BORDERC . "]\n";
1764    &print_tree($hashref->{$subnotes[$index]},($index == $#subnotes?"$prefix    ":"$prefix|   "));
1765  }
1766}
1767
1768
1769sub getconfig {
1770    my($configfile) = @_;
1771    my ($home, $value, $option);
1772    # checks are already done, so trust myself and just open it!
1773    open CONFIG, "<$configfile" || die $!;
1774    while (<CONFIG>) {
1775      chomp;
1776      next if(/^\s*$/ || /^\s*#/);
1777      my ($option,$value) = split /\s\s*=?\s*/, $_, 2;
1778
1779      $value =~ s/\s*$//;
1780      $value =~ s/\s*#.*$//;
1781      if ($value =~ /^(~\/)(.*)$/) {
1782        $value = File::Spec->catfile($ENV{HOME}, $2);
1783      }
1784
1785      if ($value =~ /^(yes|on|1)$/i) {
1786        $value = 1;
1787      }
1788      elsif ($value =~ /^(no|off|0)$/i) {
1789        $value = 0;
1790      }
1791
1792      $option = lc($option);
1793
1794      if ($option =~ /^(.+)::(.*)$/) {
1795        # driver option
1796        $driver{$1}->{$2} = $value;
1797      }
1798      else {
1799        # other option
1800        $conf{$option} = $value;
1801      }
1802    }
1803
1804    close CONFIG;
1805}
1806
1807
1808sub complete {
1809  my ($text, $line, $start) = @_;
1810
1811  if ($line =~ /^\s*$/) {
1812    # notes or topics allowed
1813    return @completion_topics, @completion_notes;
1814  }
1815  if ($line =~ /^cd/) {
1816    # only topics allowed
1817    return @completion_topics, "..";
1818  }
1819  if ($line =~ /^l/i) {
1820    # only topics allowed
1821    return @completion_topics;
1822  }
1823  if ($line =~ /^[ed]/) {
1824    # only notes allowed
1825    return @completion_notes;
1826  }
1827  if ($line =~ /^[snt\?q]/i) {
1828    # nothing allowed
1829    return ();
1830  }
1831}
1832
1833sub load_driver {
1834  my ($parent) = @_;
1835
1836  if ($parent) {
1837    my $pkg = "NOTEDB";
1838    eval "use $pkg;";
1839    if ($@) {
1840      die "Could not load the NOTEDB module: $@\n";
1841    }
1842  }
1843  else {
1844    # load the backend driver
1845    my $pkg = "NOTEDB::$conf{dbdriver}";
1846    eval "use $pkg;";
1847    if ($@) {
1848      die "$conf{dbdriver} backend unsupported: $@\n";
1849    }
1850    else {
1851      $db = $pkg->new(%{$driver{$conf{dbdriver}}});
1852    }
1853  }
1854}
1855
1856sub ticket {
1857  return join "", (map { $randomlist[int(rand($#randomlist))] } (0 .. 10) );
1858}
1859__END__
1860