mirror of
https://codeberg.org/scip/note.git
synced 2025-12-16 20:21:04 +01:00
- fixed https://rt.cpan.org/Ticket/Display.html?id=155578: adding a new entry from STDIN now works regardless of interactive seting. - added new flag -n - fixed note number argument - code reorganized
1916 lines
51 KiB
Perl
Executable File
1916 lines
51 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
#
|
|
# note - console notes management with database and encryption support.
|
|
# Copyright (C) 1999-2024 T.v.Dein (see README for details!)
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
#
|
|
# - Thomas von Dein <tom at vondein dot org>
|
|
#
|
|
# latest version on:
|
|
# http://www.daemon.de/note/
|
|
# https://github.com/TLINDEN/note/
|
|
#
|
|
|
|
use lib qw(blib/lib);
|
|
|
|
BEGIN {
|
|
# works on unix or cygwin only!
|
|
my $path = $0;
|
|
$path =~ s#/[^/]*$##;
|
|
unshift @INC, "$path/..";
|
|
}
|
|
|
|
use strict;
|
|
no strict 'refs';
|
|
use Getopt::Long;
|
|
use FileHandle;
|
|
use File::Spec;
|
|
use YAML;
|
|
use JSON::PP;
|
|
|
|
|
|
#
|
|
# prototypes
|
|
#
|
|
sub usage; # print usage message for us thumb userz :-)
|
|
sub find_editor; # returns an external editor for use
|
|
sub output; # used by &list and &display
|
|
sub C; # print colourized
|
|
sub num_bereich; # returns array from "1-4" (1,2,3,4)
|
|
sub getdate; # return pretty formatted day
|
|
sub new; # crate new note
|
|
sub edit; # edit a note
|
|
sub del; # delete a note
|
|
sub display; # display one or more notes
|
|
sub list; # note-listing
|
|
sub help; # interactive help screen
|
|
sub import; # import from notedb-dump
|
|
sub display_tree; # show nice tree-view
|
|
sub tree; # build the tree
|
|
sub print_tree; # print the tree, contributed by Jens Heunemann <Jens dot Heunemann at consol dot de>. THX!
|
|
sub ticket; # return a random string which is used as ticket number for new note entries
|
|
|
|
#
|
|
# globals
|
|
#
|
|
my (
|
|
#
|
|
# commandline options
|
|
#
|
|
$opt_, $opt_i, $opt_r, $opt_e, $opt_d, $opt_enc,
|
|
$opt_s, $opt_t, $opt_T, $opt_l, $opt_L, $opt_c,
|
|
$opt_D, $opt_I, $opt_o, $opt_h, $opt_n, $opt_v,
|
|
$opt_j, $opt_new,
|
|
|
|
#
|
|
# set from commandline (or interactive)
|
|
#
|
|
$number, $searchstring, $dump_file, $ImportType, $StdinMode, $Raw, $TOPIC,
|
|
|
|
#
|
|
# configuration options
|
|
%conf, %driver,
|
|
|
|
#
|
|
# processed colors
|
|
#
|
|
$BORDERC, $_BORDERC, $NOTEC, $NUMC, $_NUMC, $_NOTEC, $TIMEC,
|
|
$_TIMEC, $TOPICC, $_TOPICC,
|
|
|
|
#
|
|
# config presets
|
|
#
|
|
$DEFAULTDBNAME, $USER, $PATH, $CONF,
|
|
|
|
#
|
|
# internals
|
|
#
|
|
$TYPE, $mode, $NoteKey, %Color, @LastTopic, $timelen, $maxlen,
|
|
$VERSION, $CurTopic, $CurDepth, $WantTopic, $db,
|
|
$sizeof, %TP, $TreeType, $ListType, $SetTitle, $clearstring,
|
|
@ArgTopics, $key, $typedef, @NumBlock, $has_nothing, @completion_topics, @completion_notes,
|
|
@randomlist, $hardparams
|
|
);
|
|
|
|
|
|
#
|
|
# DEFAULTS, allows one to use note without a config
|
|
# don't change them, instead use the config file!
|
|
#
|
|
|
|
%conf = (
|
|
'numbercolor' => 'blue',
|
|
'bordercolor' => 'black',
|
|
'timecolor' => 'black',
|
|
'topiccolor' => 'black',
|
|
'notecolor' => 'green',
|
|
'alwaysinteractive' => 1,
|
|
'keeptimestamp' => 0,
|
|
'readonly' => 0,
|
|
'shortcd' => 1,
|
|
'autoclear' => 0,
|
|
'maxlen' => 'auto',
|
|
'defaultlong' => 0,
|
|
'dbdriver' => 'binary', # will be depcrecated in 1.5.0 and replaced by dumper.
|
|
'timeformat' => 'DD.MM.YYYY hh:mm:ss',
|
|
'usecolors' => 0,
|
|
'addticket' => 0,
|
|
'formattext' => 0,
|
|
'alwayseditor' => 1,
|
|
'useencryption' => 0,
|
|
'tempdirectory' => File::Spec->tmpdir(),
|
|
'topicseparator' => '/',
|
|
'printlines' => 0,
|
|
'cache' => 0,
|
|
'preferrededitor' => '',
|
|
'motd' => '',
|
|
'usejson' => 0, # will be the default in the future
|
|
);
|
|
|
|
# these are not customizable at runtime!
|
|
$hardparams = "(readonly|maxlen|dbdriver|useencryption|cryptmethod)";
|
|
$CONF = File::Spec->catfile($ENV{HOME}, ".noterc");
|
|
$USER = getlogin || getpwuid($<); chomp $USER;
|
|
$TOPIC = 1;
|
|
$VERSION = "1.4.2";
|
|
$CurDepth = 1; # the current depth inside the topic "directory" structure...
|
|
$maxlen = "auto";
|
|
$timelen = 22;
|
|
|
|
@randomlist = ('a'..'z', 0..9, 'A'..'Z');
|
|
|
|
# colors available
|
|
# \033[1m%30s\033[0m
|
|
%Color = ( 'black' => '0;30',
|
|
'red' => '0;31',
|
|
'green' => '0;32',
|
|
'yellow' => '0;33',
|
|
'blue' => '0;34',
|
|
'magenta' => '0;35',
|
|
'cyan' => '0;36',
|
|
'white' => '0;37',
|
|
'B' => '1;30',
|
|
'BLACK' => '1;30',
|
|
'RED' => '1;31',
|
|
'GREEN' => '1;32',
|
|
'YELLOW' => '1;33',
|
|
'BLUE' => '1;34',
|
|
'MAGENTA' => '1;35',
|
|
'CYAN' => '1;36',
|
|
'WHITE' => '1;37',
|
|
'black_' => '4;30',
|
|
'red_' => '4;31',
|
|
'green_' => '4;32',
|
|
'yellow_' => '4;33',
|
|
'blue_' => '4;34',
|
|
'magenta_' => '4;35',
|
|
'cyan_' => '4;36',
|
|
'white_' => '4;37',
|
|
'blackI' => '7;30',
|
|
'redI' => '7;31',
|
|
'greenI' => '7;32',
|
|
'yellowI' => '7;33',
|
|
'blueI' => '7;34',
|
|
'magentaI' => '7;35',
|
|
'cyanI' => '7;36',
|
|
'whiteI' => '7;37',
|
|
'white_black' => '40;37;01',
|
|
'bold' => ';01',
|
|
'hide' => '44;34'
|
|
);
|
|
|
|
#
|
|
# process command line args
|
|
#
|
|
if ($ARGV[0] eq "") {
|
|
$mode = "new";
|
|
}
|
|
elsif ($#ARGV == 0 && $ARGV[0] eq "-") {
|
|
$mode = "new";
|
|
$StdinMode = 1; # read from STDIN until EOF
|
|
shift;
|
|
undef $has_nothing;
|
|
}
|
|
else {
|
|
Getopt::Long::Configure( qw(no_ignore_case));
|
|
GetOptions (
|
|
"interactive|i!" => \$opt_i, # no arg
|
|
"config|c=s" => \$opt_c, # string, required
|
|
"raw|r!" => \$opt_r, # no arg
|
|
"new|n:s" => \$opt_new, # no arg or optional string
|
|
"edit|e=i" => \$opt_e, # integer, required
|
|
"delete|d=s" => \$opt_d, # integer, required
|
|
"search|s=s" => \$opt_s, # string, required
|
|
"tree|topic|t!" => \$opt_t, # no arg
|
|
"longtopic|T!" => \$opt_T, # no arg
|
|
"list|l:s" => \$opt_l, # string, optional
|
|
"longlist|L:s" => \$opt_L, # string, optional
|
|
"dump|Dump|D:s" => \$opt_D, # string, optional
|
|
"json|j" => \$opt_j, # bool, optional
|
|
"import|Import|I:s" => \$opt_I, # string, optional
|
|
"overwrite|o!" => \$opt_o, # no arg
|
|
"help|h|?!" => \$opt_h, # no arg
|
|
"version|v!" => \$opt_v, # no arg
|
|
"encrypt=s" => \$opt_enc, # string, required
|
|
);
|
|
|
|
# after that @ARGV contains eventually a note-number or a single dash
|
|
$opt_n = shift;
|
|
|
|
#
|
|
# determine mode
|
|
#
|
|
if ($opt_i) {
|
|
$mode = "interactive";
|
|
}
|
|
elsif (defined $opt_new) {
|
|
$mode = "new";
|
|
if ($opt_new eq "-") {
|
|
$StdinMode = 1; # read from STDIN
|
|
}
|
|
}
|
|
elsif (defined $opt_l || defined $opt_L) {
|
|
$mode = "list";
|
|
if (defined $opt_l) {
|
|
@ArgTopics = split /$conf{topicseparator}/, $opt_l;
|
|
}
|
|
else {
|
|
$ListType = "LONG";
|
|
@ArgTopics = split /$conf{topicseparator}/, $opt_L;
|
|
}
|
|
$CurDepth += $#ArgTopics + 1 if($opt_l || $opt_L);
|
|
$CurTopic = $ArgTopics[$#ArgTopics]; # use the last element everytime...
|
|
}
|
|
elsif ($opt_t || $opt_T) {
|
|
$mode = "tree";
|
|
$mode = "display_tree";
|
|
$TreeType = "LONG" if($opt_T);
|
|
}
|
|
elsif (defined $opt_s) {
|
|
$mode = "search";
|
|
$searchstring = $opt_s;
|
|
}
|
|
elsif ($opt_e) {
|
|
$mode = "edit";
|
|
$number = $opt_e;
|
|
}
|
|
elsif ($opt_d) {
|
|
$mode = "del";
|
|
$number = $opt_d;
|
|
}
|
|
elsif ($opt_enc) {
|
|
$mode = "encrypt_passwd";
|
|
$clearstring = $opt_enc;
|
|
}
|
|
elsif (defined $opt_D) {
|
|
$mode = "dump";
|
|
if (!$opt_) {
|
|
if ($opt_D ne "") {
|
|
$dump_file = $opt_D;
|
|
}
|
|
else {
|
|
$dump_file = "note.dump.$$";
|
|
print "no dumpfile specified, using $dump_file.\n";
|
|
}
|
|
}
|
|
else {
|
|
$dump_file = "-"; # use STDIN
|
|
}
|
|
|
|
if (defined $opt_j) {
|
|
$conf{usejson} = 1; # force JSON
|
|
}
|
|
}
|
|
elsif (defined $opt_I) {
|
|
$mode = "import";
|
|
if (!$opt_) {
|
|
if ($opt_I ne "") {
|
|
$dump_file = $opt_I;
|
|
}
|
|
else {
|
|
print "Import-error! No dump_file specified!\n";
|
|
exit(1);
|
|
}
|
|
}
|
|
else {
|
|
$dump_file = "-";
|
|
}
|
|
}
|
|
elsif ($opt_v) {
|
|
print "This is note $VERSION by Thomas von Dein <tom at vondein dot org>.\n";
|
|
exit(0);
|
|
}
|
|
elsif ($opt_h) {
|
|
&usage;
|
|
}
|
|
else {
|
|
if ($opt_c && $mode eq "" && !$opt_n) {
|
|
$mode = "new";
|
|
}
|
|
elsif ($opt_c && $mode eq "") {
|
|
$mode = ""; # huh?!
|
|
}
|
|
else {
|
|
$has_nothing = 1;
|
|
}
|
|
}
|
|
|
|
### determine generic options
|
|
if ($opt_n =~ /^\d[\d\-?\,]*$/) {
|
|
# first arg is a digit!
|
|
if ($mode eq "") {
|
|
$number = $opt_n;
|
|
$mode = "display";
|
|
undef $has_nothing;
|
|
}
|
|
else {
|
|
print "mode <$mode> does not take a numerical argument!\n";
|
|
exit(1);
|
|
}
|
|
}
|
|
elsif ($opt_n eq "-") {
|
|
$StdinMode = 1; # read from STDIN
|
|
$mode = "new";
|
|
}
|
|
elsif ($opt_n ne "") {
|
|
print "Unknown option: $opt_n\n";
|
|
&usage;
|
|
}
|
|
if ($opt_r) {
|
|
$Raw = 1;
|
|
}
|
|
if ($opt_o) {
|
|
$ImportType = "overwrite";
|
|
if (!$opt_I) {
|
|
print "--overwrite is only suitable for use with --import!\n";
|
|
exit(1);
|
|
}
|
|
}
|
|
#####
|
|
}
|
|
if ($has_nothing && $mode eq "") {
|
|
&usage;
|
|
}
|
|
|
|
# read the configfile.
|
|
$CONF = $opt_c if($opt_c); # if given by commandline, use this.
|
|
if (-e $CONF) {
|
|
&getconfig($CONF);
|
|
}
|
|
elsif ($opt_c) {
|
|
# only wrong, if specified by commandline! else use default values!
|
|
print STDERR "Could not open \"$CONF\": file does not exist or permission denied!\n";
|
|
exit(1);
|
|
}
|
|
|
|
# directly jump to encrypt, 'cause this sub does
|
|
# not require a database connection
|
|
if ($mode eq "encrypt_passwd") {
|
|
&encrypt_passwd;
|
|
exit;
|
|
}
|
|
|
|
# Always interactive? with the exception if stdin was requested
|
|
if ($conf{alwaysinteractive} && $mode ne "dump" && $mode ne "import" && !$StdinMode && !defined $opt_new) {
|
|
$mode = "interactive";
|
|
}
|
|
|
|
# OK ... Long-Listing shall be default ... You wanted it!!!
|
|
if ($conf{defaultlong}) {
|
|
# takes only precedence in commandline mode
|
|
$ListType="LONG";
|
|
}
|
|
|
|
|
|
|
|
|
|
# calculate some constants...
|
|
$BORDERC = "<$conf{bordercolor}>";
|
|
$_BORDERC = "</$conf{bordercolor}>";
|
|
$NUMC = "<$conf{numbercolor}>";
|
|
$_NUMC = "</$conf{numbercolor}>";
|
|
$NOTEC = "<$conf{notecolor}>";
|
|
$_NOTEC = "</$conf{notecolor}>";
|
|
$TIMEC = "<$conf{timecolor}>";
|
|
$_TIMEC = "</$conf{timecolor}>";
|
|
$TOPICC = "<$conf{topiccolor}>";
|
|
$_TOPICC = "</$conf{topiccolor}>";
|
|
|
|
$NoteKey = $conf{topicseparator} . "notes" . $conf{topicseparator};
|
|
|
|
|
|
|
|
|
|
# default permissions on new files (tmp)
|
|
umask 077;
|
|
|
|
|
|
# load the parent module
|
|
&load_driver(1);
|
|
|
|
# check wether the user wants to use encryption:
|
|
if ($conf{useencryption} && $NOTEDB::crypt_supported == 1) {
|
|
if ($conf{cryptmethod} eq "") {
|
|
$conf{cryptmethod} = "Crypt::IDEA";
|
|
}
|
|
if (!exists $ENV{'NOTE_PASSWD'}) {
|
|
print "note password: ";
|
|
eval {
|
|
local($|) = 1;
|
|
local(*TTY);
|
|
open(TTY,"/dev/tty") or die "No /dev/tty!";
|
|
system ("stty -echo </dev/tty") and die "stty failed!";
|
|
chomp($key = <TTY>);
|
|
print STDERR "\r\n";
|
|
system ("stty echo </dev/tty") and die "stty failed!";
|
|
close(TTY);
|
|
};
|
|
if ($@) {
|
|
$key = <>;
|
|
}
|
|
}
|
|
else {
|
|
$key = $ENV{'NOTE_PASSWD'};
|
|
}
|
|
chomp $key;
|
|
if ($conf{dbdriver} eq "mysql") {
|
|
eval {
|
|
require Crypt::CBC;
|
|
my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
|
|
# decrypt the dbpasswd, if it's encrypted!
|
|
$driver{mysql}->{dbpasswd} =
|
|
$cipher->decrypt(unpack("u", $driver{mysql}->{dbpasswd})) if($driver{mysql}->{encrypt_passwd});
|
|
&load_driver();
|
|
};
|
|
die "Could not connect to db: $@!\n" if($@);
|
|
}
|
|
else {
|
|
&load_driver();
|
|
}
|
|
$db->use_crypt($key,$conf{cryptmethod});
|
|
undef $key;
|
|
# verify correctness of passwd
|
|
my ($cnote, $cdate) = $db->get_single(1);
|
|
if ($cdate ne "") {
|
|
if ($cdate !~ /^\d+\.\d+?/) {
|
|
print "access denied.\n"; # decrypted $date is not a number!
|
|
exit(1);
|
|
}
|
|
} #else empty database!
|
|
}
|
|
elsif ($conf{useencryption} && $NOTEDB::crypt_supported == 0) {
|
|
print STDERR "WARNING: You enabled database encryption but neither Crypt::CBC\n";
|
|
print STDERR "WARNING: or Crypt::$conf{cryptmethod} are installed! Please turn\n";
|
|
print STDERR "WARNING: off encryption or install the desired modules! Thanks!\n";
|
|
exit 1;
|
|
}
|
|
else {
|
|
# as of 1.3.5 we do not fall back to cleartext anymore
|
|
# I consider this as unsecure, if you don't, fix your installation!
|
|
|
|
&load_driver();
|
|
$db->no_crypt;
|
|
|
|
# does: NOTEDB::crypt_supported = 0;
|
|
my %all = $db->get_all();
|
|
if(scalar keys %all > 0) {
|
|
my $id = (keys %all)[0];
|
|
if($all{$id}->{date} !~ /^\d+\.\d+?/) {
|
|
print "Warning! Encryption is not enabled, but notedb seems to be encrypted or in a non-text binary format. Please check your db file and your config!\n";
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# do we use the db cache?
|
|
if ($conf{cache}) {
|
|
$db->use_cache();
|
|
}
|
|
|
|
|
|
# add the backend version to the note version:
|
|
$VERSION .= ", " . $conf{dbdriver} . " " . $db->version();
|
|
|
|
|
|
# main loop: ###############
|
|
&$mode;
|
|
exit(0);
|
|
################## EOP ################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
############ encrypt a given password ##############
|
|
sub encrypt_passwd {
|
|
my($key, $crypt_string);
|
|
print "password: ";
|
|
eval {
|
|
local($|) = 1;
|
|
local(*TTY);
|
|
open(TTY,"/dev/tty") or die "No /dev/tty!";
|
|
system ("stty -echo </dev/tty") and die "stty failed!";
|
|
chomp($key = <TTY>);
|
|
print STDERR "\r\n";
|
|
system ("stty echo </dev/tty") and die "stty failed!";
|
|
close(TTY);
|
|
};
|
|
if ($@) {
|
|
$key = <>;
|
|
}
|
|
chomp $key;
|
|
eval {
|
|
require Crypt::CBC;
|
|
my $cipher = new Crypt::CBC($key, $conf{cryptmethod});
|
|
$crypt_string = pack("u", $cipher->encrypt($clearstring));
|
|
};
|
|
if ($@) {
|
|
print "Something went wrong: $@\n";
|
|
exit 1;
|
|
}
|
|
else {
|
|
print "Encrypted password:\n$crypt_string\n";
|
|
}
|
|
}
|
|
############################### MOTD ##################################
|
|
sub motd {
|
|
my($N,$match,$note,$date,$num);
|
|
# display a configured motd note, if any
|
|
($note, $date) = $db->get_single($conf{motd});
|
|
if ($note) {
|
|
print "\n\n$note\n\n";
|
|
}
|
|
}
|
|
|
|
############################### DISPLAY ##################################
|
|
sub display {
|
|
my($N,$match,$note,$date,$num);
|
|
# display a certain note
|
|
print "\n";
|
|
&num_bereich; # get @NumBlock from $numer
|
|
my $count = scalar @NumBlock;
|
|
foreach $N (@NumBlock) {
|
|
($note, $date) = $db->get_single($N);
|
|
if ($note) {
|
|
if ($Raw) {
|
|
print "$N\n$date\n$note\n\n";
|
|
}
|
|
else {
|
|
output($N, $note, $date, "SINGLE", $count);
|
|
print "\n";
|
|
}
|
|
$match = 1;
|
|
}
|
|
$count--;
|
|
}
|
|
if (!$match) {
|
|
print "no note with that number found!\n";
|
|
}
|
|
}
|
|
|
|
############################### SEARCH ##################################
|
|
sub search {
|
|
my($n,$match,$note,$date,$num,%res);
|
|
if ($searchstring eq "") {
|
|
print "No searchstring specified!\n";
|
|
}
|
|
else {
|
|
print "searching the database $conf{dbname} for \"$searchstring\"...\n\n";
|
|
|
|
%res = $db->get_search($searchstring);
|
|
my $nummatches = scalar keys %res;
|
|
&determine_width;
|
|
foreach $num (sort { $a <=> $b } keys %res) {
|
|
if ($nummatches == 1) {
|
|
output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "SINGLE");
|
|
}
|
|
else {
|
|
output($num, $res{$num}->{'note'}, $res{$num}->{'date'}, "search");
|
|
}
|
|
$match = 1;
|
|
}
|
|
if (!$match) {
|
|
print "no matching note found!\n";
|
|
}
|
|
print "\n";
|
|
}
|
|
}
|
|
|
|
|
|
############################### LIST ##################################
|
|
sub list {
|
|
my(@topic,@RealTopic, $i,$t,$n,$num,@CurItem,$top,$in,%res);
|
|
if ($mode ne "interactive" && !$Raw) {
|
|
print "\nList of all existing notes:\n\n";
|
|
}
|
|
else {
|
|
print "\n";
|
|
}
|
|
|
|
# list all available notes (number and firstline)
|
|
%res = $db->get_all();
|
|
|
|
if ($TOPIC) {
|
|
undef %TP;
|
|
}
|
|
|
|
foreach $num (sort { $a <=> $b } keys %res) {
|
|
$n = $res{$num}->{'note'};
|
|
$t = $res{$num}->{'date'};
|
|
if ($TOPIC) {
|
|
# this allows us to have multiple topics (subtopics!)
|
|
my ($firstline,$dummy) = split /\n/, $n, 2;
|
|
if ($firstline =~ /^($conf{topicseparator})/) {
|
|
@topic = split(/$conf{topicseparator}/,$firstline);
|
|
}
|
|
else {
|
|
@topic = ();
|
|
}
|
|
# looks like: "\topic\"
|
|
# collect a list of topics under the current topic
|
|
if ($topic[$CurDepth-1] eq $CurTopic && $topic[$CurDepth] ne "") {
|
|
if (exists $TP{$topic[$CurDepth]}) {
|
|
$TP{$topic[$CurDepth]}++;
|
|
}
|
|
else {
|
|
# only if the next item *is* a topic!
|
|
$TP{$topic[$CurDepth]} = 1 if(($CurDepth) <= $#topic);
|
|
}
|
|
}
|
|
elsif ($topic[$CurDepth-1] eq $CurTopic || ($topic[$CurDepth] eq "" && $CurDepth ==1)) {
|
|
# cut the topic off the note-text
|
|
if ($n =~ /^($conf{topicseparator})/) {
|
|
$CurItem[$i]->{'note'} = $dummy;
|
|
}
|
|
else {
|
|
$CurItem[$i]->{'note'} = $n;
|
|
}
|
|
# save for later output() call
|
|
$CurItem[$i]->{'num'} = $num;
|
|
$CurItem[$i]->{'time'} = $t;
|
|
$i++;
|
|
# use this note for building the $PATH!
|
|
if ($RealTopic[0] eq "") {
|
|
@RealTopic = @topic;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
output($num, $n, $t);
|
|
}
|
|
}
|
|
if ($TOPIC) {
|
|
if ($CurTopic ne "") {
|
|
if ($i) {
|
|
# only if there were notes under current topic
|
|
undef $PATH;
|
|
foreach (@RealTopic) {
|
|
$PATH .= $_ . $conf{topicseparator};
|
|
last if($_ eq $CurTopic);
|
|
}
|
|
}
|
|
else {
|
|
# it is an empty topic, no notes here
|
|
$PATH = join $conf{topicseparator}, @LastTopic;
|
|
$PATH .= $conf{topicseparator} . $CurTopic . $conf{topicseparator};
|
|
$PATH =~ s/^\Q$conf{topicseparator}$conf{topicseparator}\E/$conf{topicseparator}/;
|
|
}
|
|
}
|
|
else {
|
|
$PATH = $conf{topicseparator};
|
|
}
|
|
|
|
@completion_topics = ();
|
|
@completion_notes = ();
|
|
# we are at top level, print a list of topics...
|
|
foreach $top (sort(keys %TP)) {
|
|
push @completion_topics, $top;
|
|
output("-", " => ". $top . "$conf{topicseparator} ($TP{$top} notes)",
|
|
" Sub Topic ");
|
|
}
|
|
#print Dumper(@CurItem);
|
|
for ($in=0;$in<$i;$in++) {
|
|
push @completion_notes, $CurItem[$in]->{'num'};
|
|
output( $CurItem[$in]->{'num'},
|
|
$CurItem[$in]->{'note'},
|
|
$CurItem[$in]->{'time'} );
|
|
}
|
|
}
|
|
|
|
print "\n";
|
|
}
|
|
|
|
############################### NEW ##################################
|
|
sub new {
|
|
my($TEMP,$editor, $date, $note, $WARN, $c, $line, $num, @topic);
|
|
if ($conf{readonly}) {
|
|
print "readonly\n";
|
|
return;
|
|
}
|
|
|
|
$date = &getdate;
|
|
$note = "";
|
|
$line = "";
|
|
|
|
return if $db->lock();
|
|
|
|
if ($StdinMode) {
|
|
# create a new note from STDIN
|
|
print STDERR "Reading from STDIN ...\n";
|
|
while (<STDIN>) {
|
|
$note .= $_;
|
|
}
|
|
}
|
|
elsif ($conf{alwayseditor} && &is_interactive()) {
|
|
# read a new note interactively or by using the editor
|
|
$TEMP = &gettemp;
|
|
# security!
|
|
unlink $TEMP;
|
|
|
|
# let the user edit it...
|
|
$editor = &find_editor;
|
|
if ($editor) {
|
|
# create the temp file
|
|
open NEW, "> $TEMP" or die "Could not write $TEMP: $!\n";
|
|
close NEW;
|
|
system "chattr", "+s", $TEMP; # ignore errors, since only on ext2 supported!
|
|
system $editor, $TEMP;
|
|
}
|
|
else {
|
|
print "Could not find an editor to use!\n";
|
|
$db->unlock();
|
|
exit(0);
|
|
}
|
|
|
|
# read it in ($note)
|
|
$note = "";
|
|
open E, "<$TEMP" or $WARN = 1;
|
|
if ($WARN) {
|
|
print "...edit process interupted! No note has been saved.\n";
|
|
undef $WARN;
|
|
$db->unlock();
|
|
return;
|
|
}
|
|
|
|
$c = 0;
|
|
while (<E>) {
|
|
$note = $note . $_;
|
|
}
|
|
chomp $note;
|
|
close E;
|
|
|
|
# privacy!
|
|
unlink $TEMP;
|
|
}
|
|
else {
|
|
print "enter the text of the note, end with a single .\n";
|
|
do {
|
|
$line = <STDIN>;
|
|
$note = $note . $line;
|
|
} until $line eq ".\n";
|
|
# remove the . !
|
|
chop $note;
|
|
chop $note;
|
|
}
|
|
|
|
# look if the note was empty, so don't store it!
|
|
if ($note =~ /^\s*$/) {
|
|
print "...your note was empty and will not be saved.\n";
|
|
$db->unlock();
|
|
return;
|
|
}
|
|
# since we have not a number, look for the next one available:
|
|
$number = $db->get_nextnum();
|
|
if ($TOPIC && $CurTopic ne "") {
|
|
@topic = split(/$conf{topicseparator}/,$note);
|
|
if ($topic[1] eq "") {
|
|
$note = $PATH . "\n$note";
|
|
}
|
|
}
|
|
$note = &add_ticket($note);
|
|
|
|
$db->set_new($number,$note,$date);
|
|
# everything ok until here!
|
|
print "note stored. it has been assigned the number $number.\n\n";
|
|
$db->unlock();
|
|
}
|
|
|
|
sub add_ticket {
|
|
my $orignote = shift;
|
|
if ($conf{addticket}) {
|
|
my ($topic, $title, $rest) = split /\n/, $orignote, 3;
|
|
my $note = "";
|
|
if ($topic =~ /^\//) {
|
|
# topic path, keep it
|
|
$note .= "$topic\n";
|
|
}
|
|
else {
|
|
# no topic
|
|
$rest = "$title\n$rest";
|
|
$title = $topic;
|
|
}
|
|
if ($title !~ /^\[[a-z0-9A-Z]+\]/) {
|
|
# no ticket number, so create one
|
|
my $ticket = &ticket();
|
|
$title = "[" . ticket() . "] " . $title;
|
|
}
|
|
$note .= "$title\n$rest";
|
|
return $note;
|
|
}
|
|
else {
|
|
return $orignote;
|
|
}
|
|
}
|
|
|
|
|
|
############################### DELETE ##################################
|
|
sub del {
|
|
my($i,@count, $setnum, $pos, $ERR);
|
|
if ($conf{readonly}) {
|
|
print "readonly\n";
|
|
return;
|
|
}
|
|
# delete a note
|
|
&num_bereich; # get @NumBlock from $number
|
|
|
|
return if $db->lock();
|
|
|
|
foreach $_ (@NumBlock) {
|
|
$ERR = $db->set_del($_);
|
|
if ($ERR) {
|
|
print "no note with number $_ found!\n";
|
|
}
|
|
else {
|
|
print "note number $_ has been deleted.\n";
|
|
}
|
|
}
|
|
# recount the notenumbers:
|
|
$db->set_recountnums();
|
|
|
|
$db->unlock();
|
|
@NumBlock = ();
|
|
}
|
|
|
|
############################### EDIT ##################################
|
|
sub edit {
|
|
my($keeptime, $date, $editor, $TEMP, $note, $t, $num, $match, $backup);
|
|
if ($conf{readonly}) {
|
|
print "readonly\n";
|
|
return;
|
|
}
|
|
# edit a note
|
|
$date = &getdate;
|
|
|
|
return if $db->lock();
|
|
|
|
($note, $keeptime) = $db->get_single($number);
|
|
if ($keeptime eq "") {
|
|
print "no note with that number found ($number)!\n\n";
|
|
if($mode ne "interactive") {
|
|
$db->unlock();
|
|
exit(0);
|
|
}
|
|
else {
|
|
$db->unlock();
|
|
return;
|
|
}
|
|
}
|
|
|
|
$TEMP = &gettemp;
|
|
open NOTE,">$TEMP" or die "Could not open $TEMP\n";
|
|
select NOTE;
|
|
|
|
system "chattr", "+s", $TEMP; # ignore errors, like in new()
|
|
|
|
print $note;
|
|
close NOTE;
|
|
select STDOUT;
|
|
$editor = &find_editor;
|
|
|
|
$backup = $note;
|
|
|
|
if ($editor) {
|
|
system ($editor, $TEMP) and die "Could not execute $editor: $!\n";
|
|
}
|
|
else {
|
|
print "Could not find an editor to use!\n";
|
|
exit(0);
|
|
}
|
|
$note = "";
|
|
open NOTE,"<$TEMP" or die "Could not open $TEMP\n";
|
|
|
|
while (<NOTE>) {
|
|
$note = $note . $_;
|
|
}
|
|
chomp $note;
|
|
close NOTE;
|
|
|
|
unlink $TEMP || die $!;
|
|
|
|
if ($note ne $backup) {
|
|
if ($conf{keeptimestamp}) {
|
|
$t = $keeptime;
|
|
}
|
|
else {
|
|
$t = $date;
|
|
}
|
|
# we got it, now save to db
|
|
$db->set_edit($number, $note, $t);
|
|
|
|
print "note number $number has been changed.\n";
|
|
}
|
|
else {
|
|
print "note number $number has not changed, no save done.\n";
|
|
}
|
|
$db->unlock();
|
|
}
|
|
|
|
sub dump {
|
|
my(%res, $num, $DUMP);
|
|
|
|
# $dump_file
|
|
if ($dump_file eq "-") {
|
|
$DUMP = *STDOUT;
|
|
}
|
|
else {
|
|
open (DUMPFILE, ">$dump_file") or die "could not open $dump_file\n";
|
|
$DUMP = *DUMPFILE;
|
|
}
|
|
|
|
select $DUMP;
|
|
|
|
%res = $db->get_all();
|
|
|
|
# FIXME: prepare hashing in NOTEDB class
|
|
foreach $num (sort { $a <=> $b } keys %res) {
|
|
print STDOUT "dumping note number $num to $dump_file\n" if($dump_file ne "-");
|
|
my($title, $path, $body);
|
|
if ($res{$num}->{note} =~ /^\//) {
|
|
($path, $title, $body) = split /\n/, $res{$num}->{note}, 3;
|
|
}
|
|
else {
|
|
($title, $body) = split /\n/, $res{$num}->{note}, 2;
|
|
$path = '';
|
|
}
|
|
my $date = $res{$num}->{date};
|
|
$res{$num} = { body => $body, title => $title, path => $path, date => $date};
|
|
}
|
|
|
|
if($conf{usejson}) {
|
|
my $json = JSON::PP->new->utf8->pretty;
|
|
print $json->encode(\%res);
|
|
}
|
|
else {
|
|
warn "Deprecation notice: YAML export format will not be supported in the future!
|
|
Enable JSON using the UseJSON config parameter or the -j commandline parameter!";
|
|
print Dump(\%res);
|
|
}
|
|
|
|
close(DUMPFILE);
|
|
select STDOUT;
|
|
}
|
|
|
|
sub import {
|
|
my($num, $start, $complete, $dummi, $note, $date, $time, $number, $stdin, $DUMP, %data);
|
|
# open $dump_file and import it into the notedb
|
|
$stdin = 1 if($dump_file eq "-");
|
|
if ($stdin) {
|
|
$DUMP = *STDIN;
|
|
}
|
|
else {
|
|
open (DUMPFILE, "<$dump_file") or die "could not open $dump_file\n";
|
|
$DUMP = *DUMPFILE;
|
|
}
|
|
|
|
my $serialized = join '', <$DUMP>;
|
|
|
|
my $res;
|
|
|
|
if($serialized =~ /^\{/) {
|
|
$res = decode_json($serialized);
|
|
}
|
|
else {
|
|
$res = Load($serialized);
|
|
}
|
|
|
|
foreach my $number (keys %{$res}) {
|
|
my $note;
|
|
if ($res->{$number}->{path}) {
|
|
$note = "$res->{$number}->{path}\n$res->{$number}->{title}\n$res->{$number}->{body}";
|
|
}
|
|
else {
|
|
$note = "$res->{$number}->{title}\n$res->{$number}->{body}";
|
|
}
|
|
$data{$number} = {
|
|
date => $res->{$number}->{date},
|
|
note => &add_ticket($note)
|
|
};
|
|
print "fetched note number $number from $dump_file from $res->{$number}->{date}.\n" if(!$stdin);
|
|
$number++;
|
|
}
|
|
|
|
$db->set_del_all() if($ImportType ne "");
|
|
$db->import_data(\%data);
|
|
}
|
|
|
|
|
|
|
|
sub determine_width {
|
|
# determine terminal wide, if possible
|
|
if ($maxlen eq "auto") {
|
|
eval {
|
|
my $wide = `stty -a`;
|
|
if ($wide =~ /columns (\d+?);/) {
|
|
$maxlen = $1 - 32; # (32 = timestamp + borders)
|
|
}
|
|
elsif ($wide =~ /; (\d+?) columns;/) {
|
|
# bsd
|
|
$maxlen = $1 - 32; # (32 = timestamp + borders)
|
|
}
|
|
else {
|
|
# stty didn't work
|
|
$maxlen = 80 - 32;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
sub clear {
|
|
# first, try to determine the terminal height
|
|
return if(!$conf{autoclear});
|
|
my $hoch;
|
|
eval {
|
|
my $height = `stty -a`;
|
|
if ($height =~ /rows (\d+?);/) {
|
|
$hoch = $1;
|
|
}
|
|
elsif ($height =~ /; (\d+?) rows;/) {
|
|
# bsd
|
|
$hoch = $1;
|
|
}
|
|
};
|
|
if (!$hoch) {
|
|
# stty didn't work
|
|
$hoch = 25;
|
|
}
|
|
print "\n" x $hoch;
|
|
}
|
|
|
|
sub interactive {
|
|
my($B, $BB, $menu, $char, $Channel);
|
|
$Channel = $|;
|
|
local $| = 1;
|
|
# create menu:
|
|
$B = "<white_black>";
|
|
$BB = "</white_black>";
|
|
$menu = "[" . $B . "L" . $BB . "-List ";
|
|
if ($TOPIC) {
|
|
$menu .= $B . "T" . $BB . "-Topics ";
|
|
}
|
|
$menu .= $B . "N" . $BB . "-New "
|
|
. $B . "D" . $BB . "-Delete "
|
|
. $B . "S" . $BB . "-Search "
|
|
. $B . "E" . $BB . "-Edit "
|
|
. $B . "?" . $BB . "-Help "
|
|
. $B . "Q" . $BB . "-Quit] "; # $CurTopic will be empty if $TOPIC is off!
|
|
|
|
# per default let's list all the stuff:
|
|
# Initially do a list command!
|
|
&determine_width;
|
|
$ListType = ($conf{defaultlong}) ? "LONG" : "";
|
|
|
|
# show initial note entry
|
|
if ($conf{motd}) {
|
|
&motd;
|
|
}
|
|
|
|
# show initial listing
|
|
&list;
|
|
|
|
my ($term, $prompt, $attribs);
|
|
eval { require Term::ReadLine; };
|
|
if (!$@) {
|
|
$term = new Term::ReadLine('');
|
|
$attribs = $term->Attribs;
|
|
$attribs->{completion_function} = \&complete;
|
|
}
|
|
|
|
for (;;) {
|
|
$ListType = ($conf{defaultlong}) ? "LONG" : "";
|
|
undef $SetTitle;
|
|
if ($CurDepth > 2) {
|
|
print C $menu . $TOPICC . "../" . $CurTopic . $_TOPICC;
|
|
}
|
|
else {
|
|
print C $menu . $TOPICC . $CurTopic . $_TOPICC;
|
|
}
|
|
|
|
if ($conf{readonly}) {
|
|
print " [readonly] ";
|
|
}
|
|
|
|
print ">";
|
|
|
|
# endless until user press "Q" or "q"!
|
|
if ($term) {
|
|
if (defined ($char = $term->readline(" "))) {
|
|
$term->addhistory($char) if $char =~ /\S/;
|
|
$char =~ s/\s*$//; # remove trailing whitespace (could come from auto-completion)
|
|
}
|
|
else {
|
|
# shutdown
|
|
$| = $Channel;
|
|
print "\n\ngood bye!\n";
|
|
exit(0);
|
|
}
|
|
}
|
|
else {
|
|
$char = <STDIN>;
|
|
chomp $char;
|
|
}
|
|
|
|
&determine_width;
|
|
&clear;
|
|
|
|
if ($char =~ /^\d+\s*[\di*?,*?\-*?]*$/) {
|
|
$ListType = ""; #overrun
|
|
# display notes
|
|
$number = $char;
|
|
&display;
|
|
}
|
|
elsif ($char =~ /^n$/i) {
|
|
# create a new one
|
|
&new;
|
|
}
|
|
elsif ($char =~ /^$/) {
|
|
&list;
|
|
}
|
|
elsif ($char =~ /^l$/) {
|
|
$ListType = "";
|
|
&list;
|
|
}
|
|
elsif ($char =~ /^L$/) {
|
|
$ListType = "LONG";
|
|
&list;
|
|
undef $SetTitle;
|
|
}
|
|
elsif ($char =~ /^h$/i || $char =~ /^\?/) {
|
|
# zu dumm der Mensch ;-)
|
|
&help;
|
|
}
|
|
elsif ($char =~ /^d\s+([\d*?,*?\-*?]*)$/i) {
|
|
# delete one!
|
|
$number = $1;
|
|
&del;
|
|
}
|
|
elsif ($char =~ /^d$/i) {
|
|
# we have to ask her:
|
|
print "enter number(s) of note(s) you want to delete: ";
|
|
$char = <STDIN>;
|
|
chomp $char;
|
|
$number = $char;
|
|
&del;
|
|
}
|
|
elsif ($char =~ /^e\s+(\d+\-*\,*\d*)/i) {
|
|
# edit one!
|
|
$number = $1;
|
|
&edit;
|
|
}
|
|
elsif ($char =~ /^e$/i) {
|
|
# we have to ask her:
|
|
print "enter number of the note you want to edit: ";
|
|
$char = <STDIN>;
|
|
chomp $char;
|
|
$number = $char;
|
|
&edit;
|
|
}
|
|
elsif ($char =~ /^s\s+/i) {
|
|
# she want's to search
|
|
$searchstring = $';
|
|
chomp $searchstring;
|
|
&search;
|
|
}
|
|
elsif ($char =~ /^s$/i) {
|
|
# we have to ask her:
|
|
print "enter the string you want to search for: ";
|
|
$char = <STDIN>;
|
|
chomp $char;
|
|
$char =~ s/^\n//;
|
|
$searchstring = $char;
|
|
&search;
|
|
}
|
|
elsif ($char =~ /^q$/i) {
|
|
# schade!!!
|
|
$| = $Channel;
|
|
print "\n\ngood bye!\n";
|
|
exit(0);
|
|
}
|
|
elsif ($char =~ /^t$/) {
|
|
$TreeType = "";
|
|
&display_tree;
|
|
}
|
|
elsif ($char =~ /^T$/) {
|
|
$TreeType = "LONG";
|
|
&display_tree;
|
|
$TreeType = "";
|
|
}
|
|
elsif ($char =~ /^c\s*$/) {
|
|
print "Missing parameter (parameter=value), available ones:\n";
|
|
foreach my $var (sort keys %conf) {
|
|
if ($var !~ /^$hardparams/ && $var !~ /::/) {
|
|
printf "%20s = %s\n", $var, $conf{$var};
|
|
}
|
|
}
|
|
}
|
|
elsif ($char =~ /^c\s*(.+?)\s*=\s*(.+?)/) {
|
|
# configure
|
|
my $param = $1;
|
|
my $value = $2;
|
|
if ($param !~ /^$hardparams/ && $param !~ /::/ && exists $conf{$param}) {
|
|
print "Changing $param from $conf{$param} to $value\n";
|
|
$conf{$param} = $value;
|
|
}
|
|
else {
|
|
print "Unknown config parameter $param!\n";
|
|
}
|
|
}
|
|
elsif ($char =~ /^\.\.$/ || $char =~ /^cd\s*\.\.$/) {
|
|
$CurDepth-- if ($CurDepth > 1);
|
|
$CurTopic = $LastTopic[$CurDepth];
|
|
pop @LastTopic; # remove last element
|
|
&list;
|
|
}
|
|
elsif ($char =~ /^l\s+(\w+)$/) {
|
|
# list
|
|
$WantTopic = $1;
|
|
if (exists $TP{$WantTopic}) {
|
|
my %SaveTP = %TP;
|
|
$LastTopic[$CurDepth] = $CurTopic;
|
|
$CurTopic = $1;
|
|
$CurDepth++;
|
|
&list;
|
|
$CurTopic = $LastTopic[$CurDepth];
|
|
$CurDepth--;
|
|
%TP = %SaveTP;
|
|
}
|
|
else {
|
|
print "\nunknown command!\n";
|
|
}
|
|
}
|
|
else {
|
|
# unknown
|
|
my $unchar = $char;
|
|
$unchar =~ s/^cd //; # you may use cd <topic> now!
|
|
if ($unchar =~ /^\d+?$/ && $conf{short_cd}) {
|
|
# just a number!
|
|
my @topic;
|
|
my ($cnote, $cdate) = $db->get_single($unchar);
|
|
my ($firstline,$dummy) = split /\n/, $cnote, 2;
|
|
if ($firstline =~ /^($conf{topicseparator})/) {
|
|
@topic = split(/$conf{topicseparator}/,$firstline);
|
|
}
|
|
else {
|
|
@topic = ();
|
|
}
|
|
if (@topic) {
|
|
# only jump, if, and only if there were at least one topic!
|
|
$CurDepth = $#topic + 1;
|
|
$CurTopic = pop @topic;
|
|
@LastTopic = ("");
|
|
push @LastTopic, @topic;
|
|
}
|
|
&list;
|
|
}
|
|
elsif ($unchar eq $conf{topicseparator}) {
|
|
# cd /
|
|
$CurDepth = 1;
|
|
$CurTopic = "";
|
|
&list;
|
|
}
|
|
elsif (exists $TP{$char} || exists $TP{$unchar}) {
|
|
$char = $unchar if(exists $TP{$unchar});
|
|
$LastTopic[$CurDepth] = $CurTopic;
|
|
$CurTopic = $char;
|
|
$CurDepth++;
|
|
&list;
|
|
}
|
|
else {
|
|
# try incomplete match
|
|
my @matches;
|
|
foreach my $topic (keys %TP) {
|
|
if ($topic =~ /^$char/) {
|
|
push @matches, $topic;
|
|
}
|
|
}
|
|
my $nm = scalar @matches;
|
|
if ($nm == 1) {
|
|
# match on one incomplete topic, use this
|
|
$LastTopic[$CurDepth] = $CurTopic;
|
|
$CurTopic = $matches[0];
|
|
$CurDepth++;
|
|
&list;
|
|
}
|
|
elsif ($nm > 1) {
|
|
print "available topics: " . join( "," , @matches) . "\n";
|
|
}
|
|
else {
|
|
print "\nunknown command!\n";
|
|
}
|
|
}
|
|
undef $unchar;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sub usage
|
|
{
|
|
print qq~This is the program note $VERSION by T.v.Dein (c) 1999-2017.
|
|
It comes with absolutely NO WARRANTY. It is distributed under the
|
|
terms of the GNU General Public License. Use it at your own risk :-)
|
|
|
|
Usage: note [ options ] [ number [,number...]]
|
|
|
|
Options:
|
|
|
|
-c, --config file
|
|
Use another config file than the default \$HOME/.noterc.
|
|
|
|
-n, --new
|
|
Create a new note entry.
|
|
|
|
-l, --list [topic]
|
|
Lists all existing notes. If no topic were specified, it will
|
|
display a list of all existing topics. See the section TOPICS for
|
|
details about topics.
|
|
|
|
-L, --longlist [topic]
|
|
The same as -l but prints also the timestamp of the notes.
|
|
|
|
-t, --topic
|
|
Prints a list of all topics as a tree.
|
|
|
|
-T, --longtopic
|
|
Prints the topic-tree with the notes under each topic.
|
|
|
|
-s, --search string
|
|
Searches for <string> trough the notes database. See the section
|
|
SEARCHING for details about the search engine.
|
|
|
|
-e, --edit number
|
|
Edit the note with the number <number> using your default editor or
|
|
the one you specified in the config file.
|
|
|
|
-d, --delete number
|
|
Delete the note with the number <number>. You can delete multiple
|
|
notes with one command. "1-4" deletes the notes 1,2,3,4. And
|
|
"1,5,7" deletes the specified ones.
|
|
|
|
-D, --Dump [file | -]
|
|
Dumps all notes to the textfile <file>. If <file> is a "-" it will
|
|
be printed out to standard output (STDOUT).
|
|
|
|
-I, --Import file | -
|
|
Imports a previously dumped textfile into the note database. Data
|
|
will be appended by default. You can also specify a dash note -I -
|
|
instead of a <file>, which causes note, silently to read in a dump
|
|
from STDIN.
|
|
|
|
-o, --overwrite
|
|
Only suitable for use with --Import. Overwrites an existing notedb.
|
|
Use with care.
|
|
|
|
-r, --raw
|
|
Raw mode, output will not be formatted. Works not in interactive
|
|
mode, only on cmd-line for list and display. That means, no colors
|
|
will be used and no lines or titles.
|
|
|
|
-i, --interactive
|
|
Start note in interactive mode. See the section INTERACTIVE MODE
|
|
for details on this mode.
|
|
|
|
--encrypt cleartext
|
|
Encrypt the given clear text string. You would need that if you
|
|
want to store the mysql password not in cleartext in the config(if
|
|
you are using the mysql backend!).
|
|
|
|
-h, --help
|
|
Display this help screen.
|
|
|
|
-v, --version
|
|
Display the version number.
|
|
|
|
- If you run note just with one dash: note -, then it will read in a
|
|
new note from STDIN until EOF. This makes it possible to pipe text
|
|
into a new note, i.e.:
|
|
|
|
cat sometextfile | note -
|
|
|
|
Read the note(1) manpage for more details.
|
|
~;
|
|
exit 1;
|
|
}
|
|
|
|
sub find_editor {
|
|
return $conf{preferrededitor} || $ENV{"VISUAL"} || $ENV{"EDITOR"} || "vi";
|
|
}
|
|
|
|
#/
|
|
|
|
sub format {
|
|
# make text bold/underlined/inverse using current $NOTEC
|
|
my($note) = @_;
|
|
if ($conf{formattext}) {
|
|
# prepare colors to be used for replacement
|
|
my $BN = uc($NOTEC);
|
|
my $_BN = uc($_NOTEC);
|
|
my $UN = $NOTEC;
|
|
$UN =~ s/<(.*)>/<$1_>/;
|
|
my $_UN = $UN;
|
|
$_UN =~ s/<(.*)>/<\/$1>/;
|
|
my $IN = $NOTEC;
|
|
my $_IN = $_NOTEC;
|
|
$IN =~ s/<(.*)>/<$1I>/;
|
|
$_IN =~ s/<(.*)>/<$1I>/;
|
|
|
|
if ($conf{formattext} eq "simple") {
|
|
$note =~ s/\*([^\*]*)\*/$BN$1$_BN/g;
|
|
$note =~ s/_([^_]*)_/$UN$1$_UN/g;
|
|
$note =~ s/{([^}]*)}/$IN$1$_IN/g;
|
|
$note =~ s#(?<!<)/([^/]*)/#<hide>$1</hide>#g;
|
|
}
|
|
else {
|
|
$note =~ s/\*\*([^\*]{2,})\*\*/$BN$1$_BN/g;
|
|
$note =~ s/__([^_]{2,})__/$UN$1$_UN/g;
|
|
$note =~ s/\{\{([^}]{2,})\}\}/$IN$1$_IN/g;
|
|
$note =~ s#//([^/]{2,})//#<hide>$1</hide>#g;
|
|
}
|
|
|
|
$note =~ s/(<\/.*>)/$1$NOTEC/g;
|
|
}
|
|
$note;
|
|
}
|
|
|
|
sub output {
|
|
my($SSS, $LINE, $num, $note, $time, $TYPE, $L, $LONGSPC, $R, $PathLen, $SP, $title, $CUTSPACE,
|
|
$VersionLen, $len, $diff, $Space, $nlen, $txtlen, $count);
|
|
($num, $note, $time, $TYPE, $count) = @_;
|
|
|
|
$txtlen = ($ListType eq "LONG") ? $maxlen : $timelen + $maxlen;
|
|
$note = &format($note);
|
|
|
|
$SSS = "-" x ($maxlen + 30);
|
|
$nlen = length("$num");
|
|
$LINE = "$BORDERC $SSS $_BORDERC\n";
|
|
$LONGSPC = " " x (25 - $nlen);
|
|
if ($conf{printlines}) {
|
|
$L = $BORDERC . "[" . $_BORDERC;
|
|
$R = $BORDERC . "]" . $_BORDERC;
|
|
}
|
|
$PathLen = length($PATH); # will be ZERO, if not in TOPIC mode!
|
|
$VersionLen = length($VERSION) + 7;
|
|
|
|
if ($TYPE ne "SINGLE") {
|
|
if (!$SetTitle) {
|
|
$SP = "";
|
|
# print only if it is the first line!
|
|
$SP = " " x ($maxlen - 2 - $PathLen - $VersionLen);
|
|
if (!$Raw) {
|
|
# no title in raw-mode!
|
|
print C $LINE if ($conf{printlines});
|
|
print C "$L $NUMC#$_NUMC ";
|
|
if ($ListType eq "LONG") {
|
|
print C " $TIMEC" . "creation date$_TIMEC ";
|
|
}
|
|
else {
|
|
print $LONGSPC if ($conf{printlines});
|
|
}
|
|
if ($TOPIC) {
|
|
print C $TOPICC . "$PATH $_TOPICC$SP" . " note $VERSION $R\n";
|
|
}
|
|
else {
|
|
print C $NOTEC . "note$_NOTEC$SP" . " note $VERSION $R\n";
|
|
}
|
|
print C $LINE if ($conf{printlines});
|
|
}
|
|
$SetTitle = 1;
|
|
}
|
|
$title = "";
|
|
$CUTSPACE = " " x $txtlen;
|
|
if ($TYPE eq "search") {
|
|
$note =~ s/^\Q$conf{topicseparator}\E.+?\Q$conf{topicseparator}\E\n//;
|
|
}
|
|
$note =~ s/\n/$CUTSPACE/g;
|
|
$len = length($note);
|
|
if ($len < ($txtlen - 2 - $nlen)) {
|
|
$diff = $txtlen - $len;
|
|
if (!$Raw) {
|
|
if ($num eq "-") {
|
|
$Space = " " x $diff;
|
|
$title = $BORDERC . $TOPICC . $note . " " . $_TOPICC . $Space . "$_BORDERC";
|
|
}
|
|
else {
|
|
$Space = " " x ($diff - ($nlen - 1));
|
|
$title = $BORDERC . $NOTEC . $note . " " . $_NOTEC . $Space . "$_BORDERC";
|
|
}
|
|
}
|
|
else {
|
|
$title = $note;
|
|
}
|
|
}
|
|
else {
|
|
$title = substr($note,0,($txtlen - 2 - $nlen));
|
|
if (!$Raw) {
|
|
$title = $BORDERC . $NOTEC . $title . " $_NOTEC$_BORDERC";
|
|
}
|
|
}
|
|
if ($Raw) {
|
|
print "$num ";
|
|
print "$time " if($ListType eq "LONG");
|
|
if ($title =~ /^ => (.*)$conf{topicseparator} (.*)$/) {
|
|
$title = "$1$conf{topicseparator} $2"; # seems to be a topic!
|
|
}
|
|
print "$title\n";
|
|
}
|
|
else {
|
|
# $title should now look as: "A sample note "
|
|
print C "$L $NUMC$num$_NUMC $R";
|
|
if ($ListType eq "LONG") {
|
|
print C "$L$TIMEC" . $time . " $_TIMEC$R";
|
|
}
|
|
print C "$L $NOTEC" . $title . "$_NOTEC $R\n";
|
|
print C $LINE if ($conf{printlines});
|
|
}
|
|
}
|
|
else {
|
|
# we will not reach this in raw-mode, therefore no decision here!
|
|
chomp $note;
|
|
$Space = " " x (($maxlen + $timelen) - $nlen - 16);
|
|
|
|
*CHANNEL = *STDOUT;
|
|
my $usecol = $conf{usecolors};
|
|
|
|
if ($conf{less}) {
|
|
my $less = "less";
|
|
if ($conf{less} ne 1) {
|
|
# use given less command line
|
|
$less = $conf{less};
|
|
}
|
|
if (open LESS, "|$less") {
|
|
*CHANNEL = *LESS;
|
|
$conf{usecolors} = 0;
|
|
}
|
|
}
|
|
|
|
print CHANNEL C $LINE if ($conf{printlines});
|
|
print CHANNEL C "$L $NUMC$num$_NUMC $R$L$TIMEC$time$_TIMEC $Space$R\n";
|
|
print CHANNEL C "\n";
|
|
print CHANNEL C $NOTEC . $note . $_NOTEC . "\n";
|
|
print CHANNEL C $LINE if ($count == 1 && $conf{printlines});
|
|
|
|
if ($conf{less}) {
|
|
close LESS;
|
|
}
|
|
|
|
$conf{usecolors} = $usecol;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sub C {
|
|
my($default, $S, $Col, $NC, $T);
|
|
$default = "\033[0m";
|
|
$S = $_[0];
|
|
foreach $Col (%Color) {
|
|
if ($S =~ /<$Col>/g) {
|
|
if ($conf{usecolors}) {
|
|
$NC = "\033[" . $Color{$Col} . "m";
|
|
$S =~ s/<$Col>/$NC/g;
|
|
$S =~ s/<\/$Col>/$default/g;
|
|
}
|
|
else {
|
|
$S =~ s/<$Col>//g;
|
|
$S =~ s/<\/$Col>//g;
|
|
}
|
|
}
|
|
}
|
|
return $S;
|
|
}
|
|
|
|
|
|
|
|
sub num_bereich {
|
|
my($m,@LR,@Sorted_LR,$i);
|
|
# $number is the one we want to delete!
|
|
# But does it contain commas?
|
|
@NumBlock = (); #reset
|
|
$m = 0;
|
|
if ($number =~ /\,/) {
|
|
# accept -d 3,4,7
|
|
@NumBlock = split(/\,/,$number);
|
|
}
|
|
elsif ($number =~ /^\d+\-\d+$/) {
|
|
# accept -d 3-9
|
|
@LR = split(/\-/,$number);
|
|
@Sorted_LR = ();
|
|
|
|
if ($LR[0] > $LR[1]) {
|
|
@Sorted_LR = ($LR[1], $LR[0]);
|
|
}
|
|
elsif ($LR[0] == $LR[1]) {
|
|
# 0 and 1 are the same
|
|
@Sorted_LR = ($LR[0], $LR[1]);
|
|
}
|
|
else {
|
|
@Sorted_LR = ($LR[0], $LR[1]);
|
|
}
|
|
|
|
for ($i=$Sorted_LR[0]; $i<=$Sorted_LR[1]; $i++) {
|
|
# from 3-6 create @NumBlock (3,4,5,6)
|
|
$NumBlock[$m] = $i;
|
|
$m++;
|
|
}
|
|
}
|
|
else {
|
|
@NumBlock = ($number);
|
|
}
|
|
|
|
}
|
|
|
|
sub getdate {
|
|
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
|
$year += 1900;
|
|
$mon += 1;
|
|
$mon =~ s/^(\d)$/0$1/;
|
|
$hour =~ s/^(\d)$/0$1/;
|
|
$min =~ s/^(\d)$/0$1/;
|
|
$sec =~ s/^(\d)$/0$1/;
|
|
$mday =~ s/^(\d)$/0$1/;
|
|
if ($conf{timeformat}) {
|
|
my $back = $conf{timeformat};
|
|
$back =~ s/YYYY/$year/;
|
|
$back =~ s/YY/substr($year, 1, 2)/e;
|
|
$back =~ s/MM/$mon/;
|
|
$back =~ s/DD/$mday/;
|
|
$back =~ s/mm/$min/;
|
|
$back =~ s/hh/$hour/;
|
|
$back =~ s/ss/$sec/;
|
|
return $back;
|
|
}
|
|
return "$mday.$mon.$year $hour:$min:$sec";
|
|
}
|
|
|
|
|
|
sub gettemp {
|
|
my($random, @range);
|
|
@range=('0'..'9','a'..'z','A'..'Z');
|
|
srand(time||$$);
|
|
for (0..10) {
|
|
$random .= $range[rand(int($#range)+1)];
|
|
}
|
|
my $tempfile = File::Spec->catfile($conf{tempdirectory}, $USER . $random);
|
|
if (-e $tempfile) {
|
|
# avoid race conditions!
|
|
unlink $tempfile;
|
|
}
|
|
return $tempfile;
|
|
}
|
|
|
|
|
|
|
|
sub help {
|
|
my $B = "<white_black>";
|
|
my $BB = "</white_black>";
|
|
my($S, $L, $T, $Q, $H, $N, $D, $E, $C);
|
|
$L = $B . "L" . $BB . $NOTEC;
|
|
$T = $B . "T" . $BB . $NOTEC;
|
|
$Q = $B . "Q" . $BB . $NOTEC;
|
|
$H = $B . "?" . $BB . $NOTEC;
|
|
$N = $B . "N" . $BB . $NOTEC;
|
|
$D = $B . "D" . $BB . $NOTEC;
|
|
$E = $B . "E" . $BB . $NOTEC;
|
|
$S = $B . "S" . $BB . $NOTEC;
|
|
$C = $B . "C" . $BB . $NOTEC;
|
|
|
|
print C qq~$BORDERC
|
|
----------------------------------------------------------------------$_BORDERC $TOPICC
|
|
HELP for interactive note $VERSION
|
|
$_TOPICC $NOTEC
|
|
The following commands are available:
|
|
$L List notes. L=long, with timestamp and l=short without timestamp.
|
|
You can also just hit <enter> for short list.
|
|
If you specify a subtopic, then list will display it's contents,
|
|
i.e.: "l mytopic" will dislpay notes under mytopic.
|
|
$N Create a new note.
|
|
$D Delete a note. You can either hit "d 1" or "d 1-4" or just hit "d".
|
|
If you don't specify a number, you will be asked for.
|
|
$S Search trough the notes database. Usage is similar to Delete, use
|
|
a string instead of a number to search for.
|
|
$E Edit a note. Usage is similar to Delete but you can only edit note
|
|
a time.
|
|
$C Change note config online. Use with care!
|
|
$H This help screen.
|
|
$Q Exit the program.~;
|
|
if ($TOPIC) {
|
|
print C qq~
|
|
$T print a list of all existing topics as a tree. T prints the tree
|
|
with all notes under each topic.~;
|
|
}
|
|
print C qq~
|
|
|
|
All commands except the List and Topic commands are case insensitive.
|
|
Read the note(1) manpage for more details.$BORDERC
|
|
----------------------------------------------------------------------$_BORDERC
|
|
~;
|
|
}
|
|
|
|
|
|
sub display_tree {
|
|
# displays a tree of all topics
|
|
my(%TREE, %res, $n, $t, $num, @nodes, $firstline, $text, $untext);
|
|
%res = $db->get_all();
|
|
foreach $num (keys %res) {
|
|
$n = $res{$num}->{'note'};
|
|
$t = $res{$num}->{'date'};
|
|
# this allows us to have multiple topics (subtopics!)
|
|
my ($firstline,$text,$untext) = split /\n/, $n, 3;
|
|
if ($firstline =~ /^($conf{topicseparator})/) {
|
|
$firstline =~ s/($conf{topicseparator})*$//; #remove Topicseparator
|
|
@nodes = split(/$conf{topicseparator}/,$firstline);
|
|
}
|
|
else {
|
|
@nodes = (); #("$conf{topicseparator}");
|
|
$text = $firstline;
|
|
}
|
|
&determine_width; # ensure $maxlen values for &tree in non interactive modes
|
|
&tree($num, $text, \%TREE, @nodes);
|
|
}
|
|
#return if ($num == 0);
|
|
# now that we have build our tree (in %TREE) go on t display it:
|
|
print C $BORDERC . "\n[" . $conf{topicseparator} . $BORDERC . "]\n";
|
|
&print_tree(\%{$TREE{''}},"") if(%TREE);
|
|
print C $BORDERC . $_BORDERC . "\n";
|
|
}
|
|
|
|
|
|
sub tree {
|
|
my($num, $text, $LocalTree, $node, @nodes) = @_;
|
|
if (@nodes) {
|
|
if (! exists $LocalTree->{$node}->{$NoteKey}) {
|
|
$LocalTree->{$node}->{$NoteKey} = [];
|
|
}
|
|
&tree($num, $text, $LocalTree->{$node}, @nodes);
|
|
}
|
|
else {
|
|
if (length($text) > ($maxlen - 5)) {
|
|
$text = substr($text, 0, ($maxlen -5));
|
|
}
|
|
$text = $text . " (" . $NUMC . "#" . $num . $_NUMC . $NOTEC . ")" . $_NOTEC if($text ne "");
|
|
push @{$LocalTree->{$node}->{$NoteKey}}, $text;
|
|
}
|
|
}
|
|
|
|
|
|
sub print_tree {
|
|
# thanks to Jens for his hints and this sub!
|
|
my $hashref=shift;
|
|
my $prefix=shift;
|
|
my @notes=@{$hashref->{$NoteKey}};
|
|
my @subnotes=sort grep { ! /^$NoteKey$/ } keys %$hashref;
|
|
if ($TreeType eq "LONG") {
|
|
for my $note (@notes) {
|
|
if ($note ne "") {
|
|
print C $BORDERC ; # . $prefix. "|\n";
|
|
print C "$prefix+---<" . $NOTEC . $note . $BORDERC . ">" . $_NOTEC . "\n";
|
|
}
|
|
}
|
|
}
|
|
for my $index (0..$#subnotes) {
|
|
print C $BORDERC . $prefix. "|\n";
|
|
print C "$prefix+---[" . $TOPICC . $subnotes[$index] . $BORDERC . "]\n";
|
|
&print_tree($hashref->{$subnotes[$index]},($index == $#subnotes?"$prefix ":"$prefix| "));
|
|
}
|
|
}
|
|
|
|
|
|
sub getconfig {
|
|
my($configfile) = @_;
|
|
my ($home, $value, $option);
|
|
# checks are already done, so trust myself and just open it!
|
|
open CONFIG, "<$configfile" || die $!;
|
|
while (<CONFIG>) {
|
|
chomp;
|
|
next if(/^\s*$/ || /^\s*#/);
|
|
my ($option,$value) = split /\s\s*=?\s*/, $_, 2;
|
|
|
|
$value =~ s/\s*$//;
|
|
$value =~ s/\s*#.*$//;
|
|
if ($value =~ /^(~\/)(.*)$/) {
|
|
$value = File::Spec->catfile($ENV{HOME}, $2);
|
|
}
|
|
|
|
if ($value =~ /^(yes|on|1)$/i) {
|
|
$value = 1;
|
|
}
|
|
elsif ($value =~ /^(no|off|0)$/i) {
|
|
$value = 0;
|
|
}
|
|
|
|
$option = lc($option);
|
|
|
|
if ($option =~ /^(.+)::(.*)$/) {
|
|
# driver option
|
|
$driver{$1}->{$2} = $value;
|
|
}
|
|
else {
|
|
# other option
|
|
$conf{$option} = $value;
|
|
}
|
|
}
|
|
|
|
close CONFIG;
|
|
}
|
|
|
|
|
|
sub complete {
|
|
my ($text, $line, $start) = @_;
|
|
|
|
if ($line =~ /^\s*$/) {
|
|
# notes or topics allowed
|
|
return @completion_topics, @completion_notes;
|
|
}
|
|
if ($line =~ /^cd/) {
|
|
# only topics allowed
|
|
return @completion_topics, "..";
|
|
}
|
|
if ($line =~ /^l/i) {
|
|
# only topics allowed
|
|
return @completion_topics;
|
|
}
|
|
if ($line =~ /^[ed]/) {
|
|
# only notes allowed
|
|
return @completion_notes;
|
|
}
|
|
if ($line =~ /^[snt\?q]/i) {
|
|
# nothing allowed
|
|
return ();
|
|
}
|
|
}
|
|
|
|
sub load_driver {
|
|
my ($parent) = @_;
|
|
|
|
if ($parent) {
|
|
my $pkg = "NOTEDB";
|
|
eval "use $pkg;";
|
|
if ($@) {
|
|
die "Could not load the NOTEDB module: $@\n";
|
|
}
|
|
}
|
|
else {
|
|
# load the backend driver
|
|
my $pkg = "NOTEDB::$conf{dbdriver}";
|
|
eval "use $pkg;";
|
|
if ($@) {
|
|
die "$conf{dbdriver} backend unsupported: $@\n";
|
|
}
|
|
else {
|
|
$db = $pkg->new(%{$driver{$conf{dbdriver}}});
|
|
}
|
|
}
|
|
}
|
|
|
|
sub ticket {
|
|
return join "", (map { $randomlist[int(rand($#randomlist))] } (0 .. 10) );
|
|
}
|
|
|
|
sub is_interactive {
|
|
return -t STDIN && -t STDOUT;
|
|
}
|
|
|
|
__END__
|