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