Just posting this here for now to see if people are interested. If they are, I'll put it on SourceForge. Try it! -- TarQuin
I tested it. Very simple, but works fine as a WikiAsNotepad. I'd like to see it on SourceForge offcially. HelgeMeier
Thanks :) I've improved it some more lately. I'm looking into putting it on SF. "kiwi" is, as expected, already taken. I'm pondering "Apteryx". -- TarQuin
You got to be kidding about the name! That name would kill the project in its childhood. I like your script because I am looking for a WikiAsNotepad for my PocketPC. There is a perl-TK implementation at perl.org. But to use it, the menus in KiwiScript would have to go away. They are not supported. I could also use EmacsWiki, but probably your script is better because EmacsWiki will probably not have support for clicking on links with the PDA-pen. At least for me one feature would be very important for KiwiScript: Square-bracket wiki links. HelgeMeier
button2 runs emacs-wiki-follow-name-at-mouse -- merriam
#!/usr/bin/perl # kiwi - a wiki notepad application # by tarquin use warnings; use strict; use Tk; use Tk::Text; use Tk::Dialog; use Tk::Font; # use File::Spec::Functions; # see Camel p 624 use File::Basename; # see Camel p 624 use Data::Dumper; ################################ variables my $configFile = 'kiwiconfig.txt'; my $fileExtension = '.txt'; my $linkpattern = '[[:upper:]]+[[:lower:]]+[[:upper:]][[:alpha:]]*'; my $currentLongFile; # better version my $wikitext = <<'EOT'; Welcome to Kiwi. Just clear this text and start typing. Then save this file into the directory where you want to store your wiki. EOT ################################ main widgets my $mw = new MainWindow -title => "kiwi"; my $font = $mw->Font( -family => 'Times', -size => '12', ); # Text widget my $text = $mw->Scrolled( 'Text', -scrollbars => 'osoe', -width => 60, -height => 30, -wrap => 'word', -font => $font ); # status bar widget my $statustext; my $statusbar = $mw->Label( -textvariable => \$statustext, -anchor => 'w', #-background => '#aaaaaa', -relief => 'groove', -padx => '4', -font => $mw->Font( -family => 'Helvetica', -size => '12', -weight => 'bold', ) ); # geometry setup =pod $statusbar->pack( -side => 'top' ); $text->pack( -fill => 'both', -expand => 1 ); =cut $statusbar->grid(-sticky => 'new'); $text-> grid(-sticky => 'nsew'); $mw->gridRowconfigure( 1, -weight => 1); $mw->gridColumnconfigure(0, -weight => 1); ################################ configuration if ( -f $configFile ) { print "found config\n"; open CONFIG, "<$configFile"; while (<CONFIG>) { /^(File=)/ and do { s/$1//; chomp; $currentLongFile = $_; print "Current file: >$currentLongFile<\n"; }; } close CONFIG; } sub storeConfig { open CONFIG, ">>$configFile"; # create if doesn't exist open CONFIG, "+<$configFile"; my %config; while (<CONFIG>) { print "read: $_"; chomp; %config = (%config, split /=/); } # now put in the current config $config{File}=$currentLongFile; open CONFIG, ">$configFile"; # why won't +< work? foreach (keys %config) { print CONFIG "$_=$config{$_}\n"; print "storing: $_=$config{$_}\n"; } close CONFIG; } sub setCurrentFileFromName { my $name = shift; # obsolete } ################################ dialogs my $helpdialog = $mw->Dialog( -title => 'Kiwi help', -text => qq[Just start typing. You'll need to save your current page before you go any further. This will set the working directory for your current wiki.\n\n You can move to a new wiki at any time by opening a new file in another directory.\n\n Make links to pages by typing words in CamelCase. Click on them to either go to the existing page, or create a new page with this title.\n\n Links will be made automatically as you type, but in case they're not, you can select Wiki->Refresh links to update them.\n\n THINGS TO AVOID: don't type within links.], -buttons => ['OK'], -font => $mw->Font( -family => 'Helvetica', -size => '10', ) ); ################################ menus unless ( grep { $_ eq '-nomenu' } @ARGV ) { sub buildMainMenu { my $menu = $mw->Menu(-type => 'menubar'); $mw->configure(-menu => $menu ); # menu items my $file = $menu->cascade(-label => '~File', -tearoff => 0); $file->command( -label => '~New', -command => \&newCommand, -accelerator =>"Ctrl+N" ); $file->command( -label => '~Open ...', -command => \&openCommand, -accelerator =>"Ctrl+O" ); #$file->command(-label => 'New', -command => [\&menus_error, 'New']); $file->command( -label => '~Save', -command => \&saveCommand, -accelerator =>"Ctrl+S" ); $file->command( -label => 'Save ~As ...', -command => \&saveAsFile, -accelerator =>"Ctrl+Shift+S" ); $file->command( -label => 'E~xit', -accelerator =>"Ctrl+Q", -command => \&exitCommand ); #my $edit = $menu->cascade(-label => '~Edit', -tearoff => 0); # $edit->command(-label => 'Find ...', -command => \&testfind ); my $wiki = $menu->cascade(-label => '~Wiki', -tearoff => 0); $wiki->command( -label => 'Refresh links', -command => \&makeWikiLinks, -accelerator =>"Ctrl+R" ); $wiki->command( -label => 'Rebuild page list', -command => sub{ deletePageList(); buildPageList(); }, ); $wiki->separator(); my $help = $menu->cascade(-label => '~Help', -tearoff => 0); $help->command(-label => 'Help ...', -command => sub { $helpdialog->Show } ); $help->command(-label => 'About ...', -command => sub { $mw->messageBox( -title => 'About Kiwi', -message => 'Kiwi is a WikiWiki-style notepad application written in Perl/Tk.', -type => 'OK') } ); my $debug = $menu->cascade(-label => '~Debug', -tearoff => 0); $debug->command(-label => 'Dump', -command => \&DumpMenus ); $debug->command(-label => 'Clear page list', -command => \&deletePageList ); $debug->command(-label => 'Insert to page list', -command => \&insertinPageList ); $debug->command(-label => 'Build here', -command => sub { print "building...\n"} ); return $menu; } sub buildContextMenu { my $menu = $mw->Menu(-type => 'menubar'); $menu->command(-label => 'Refresh links', -command => \&makeWikiLinks ); $menu->command(-label => 'Open ...', -command => \&openCommand ); return $menu; } # menu widgets my $mainmenubar = buildMainMenu(); #$mw->configure(-menu => buildMainMenu() ); $text->menu( buildContextMenu() ); # examining menu sub DumpMenus { #print $mw->menu, "\n"; #print $mainmenubar, "\n"; my $submenu = $mainmenubar->entrycget('Debug', -menu); print "$submenu\n"; $submenu->add('command', -label => 'Added', -command => sub { print "cool...\n" } ); print "menu from mw: ", $mw->cget('-menu'), "\n"; print "menubar direct: $mainmenubar\n"; } #DumpMenus(); #exit; } # end -nomenu conditional sub buildPageList { # when should this be called? # app load if a file is loaded from config setting # open new page in new directory # save file into a new directory # user request return unless $currentLongFile; return if ( grep { $_ eq '-nomenu' } @ARGV ); # no menus - not relevant # we suppose the page list is currently empty my $submenu = $mw->cget('-menu')->entrycget('Wiki', -menu); my ($name,$path,$ext) = fileparse( $currentLongFile, $fileExtension); opendir THISDIR, $path; my @wikipages; foreach (readdir THISDIR) { /($linkpattern)$fileExtension/ and push @wikipages, $1; } closedir THISDIR; print "pages: @wikipages\n"; for my $page (sort @wikipages) { $submenu->add('command', -label => $page, -command => [\&linkClick, '', $page] ); } } sub deletePageList { my $submenu = $mw->cget('-menu')->entrycget('Wiki', -menu); $submenu->delete(3, 'end'); # remember to keep the 3 synchronized with the menu contents! } sub insertinPageList { my $newitem = shift; my $submenu = $mw->cget('-menu')->entrycget('Wiki', -menu); my @menuitems; for (3 .. $submenu->index('end') ) { # remember to keep the 3 synchronized with the menu contents! # print "got item ", $submenu->entrycget($_, -label), "\n"; if ( $submenu->entrycget($_, -label) gt $newitem ) { # print "stopping at $_\n"; $submenu->insert($_, 'command', -label => $newitem, -command => [\&linkClick, '', $newitem]); last; } } } ################################ construction ######### bindings # menu shortcuts $mw->bind("<Control-n>" => \&newCommand ); $mw->bind("<Control-o>" => \&openCommand ); $mw->bind("<Control-s>" => \&saveCommand ); $mw->bind("<Control-S>" => \&saveAsFile ); $mw->bind("<Control-r>" => \&makeWikiLinks ); # HELP! not good, inserts a control character $mw->bind("<Control-q>" => \&exitCommand ); $mw->protocol('WM_DELETE_WINDOW', \&exitCommand); # auto wiki-linking $mw->bind("<space>" => \&checkLatestLink ); $mw->bind("<Return>" => \&checkLatestLink ); # insert document given in config or default text if( $currentLongFile && -f $currentLongFile) { openFile(); # load the file (will also set status text and make links) buildPageList(); # built the page list in the menu } else { $text->insert('0.0' , $wikitext); makeWikiLinks(); $statustext = 'Welcome (no document)'; } ########## tag setup # configure general tag $text->tagConfigure('wikilink', -relief => 'raised', -foreground => '#8888ff', -underline => '1', ); $text->tagBind('wikilink', '<Key>', \&killLink ); ############################### mark wiki links { my %storedlinks; # stores the wikilinks we've already seen and bound sub makeWikiLinks { # scans entire text for links. tags them and registers new tags my($current, $length) = ('1.0', 0); my $linkpattern = '[[:upper:]]+[[:lower:]]+[[:upper:]][[:alpha:]]*'; my @foundLinks; # find and tag # search routine from widget demo while (1) { $current = $text->search( -count => \$length, -regexp => $linkpattern, $current, 'end'); last if not $current; my $linktext = $text->get($current, "$current + $length char"); print "$current - $current + $length char - $linktext\n"; tagTextAsLink($linktext, $current, $length); push @foundLinks, $linktext; $current = $text->index("$current + $length char"); } bindTaggedLinks( grep !exists $storedlinks{$_}, @foundLinks ); # check we've not already seen this link return; } sub checkLatestLink { # check if the last entered word is a link # insert method my $wordstart = $text->index('insert -2 chars wordstart'); my $wordend = $text->index('insert -2 chars wordend'); print "worstart: ", $text->get('insert -1 chars wordstart'), "\n"; my $lastword = $text->get('insert -2 chars wordstart', 'insert -2 chars wordend'); print "Spacebar, got: >$lastword<\n"; # search method my $length; my $index = $text->search( '-backwards', -count => \$length, -regexp => '\b\w+\b', # grab last word 'insert', '0.0'); my $found = $text->get($index, "$index + $length char"); print "search found: >$found<\n"; return unless $lastword =~ m/[[:upper:]]+[[:lower:]]+[[:upper:]][[:alpha:]]*/; tagTextAsLink($lastword, $wordstart, $wordend); bindTaggedLinks($lastword) unless exists $storedlinks{$lastword}; } # helper subs sub tagTextAsLink { my ( $linktext, $start, $end ) = @_; unless ( $end =~ m/\./ ) { # if $end is not "line.char" then it's an offset $end = $text->index("$start + $end char"); } # tag as general and specific $text->tag('add', 'wikilink', $start, $end); $text->tag('add', $linktext, $start, $end); print "Tagging $linktext\n"; } sub bindTaggedLinks { for my $taggedlink (@_) { $text->tagBind($taggedlink, '<1>', [ \&linkClick, $taggedlink, Ev('x'), Ev('y'), Ev('@') ] ); $text->tagBind($taggedlink, '<Any-Enter>' => sub { $text->tagConfigure($taggedlink, -background => '#ffff00'); $text->configure(qw/-cursor hand2/); } ); $text->tagBind($taggedlink, '<Any-Leave>' => sub { $text->tagConfigure($taggedlink, -background => undef); $text->configure(qw'-cursor xterm'); } ); $storedlinks{$taggedlink} = ''; print "Binding $taggedlink\n"; } } sub killLink { print "typing in link\n"; =pod For typing within tags, need to: get position of insert mark $text->tagNames(?index?) # find the tag @x,y wordstart and wordend to grab whole word $text->tagRanges(tagName); # grab ranges find which range is there =cut } } # end of scoped block for wiki link making subs ############################### page navigation sub linkClick { # user clicks a wiki link shift; my $link = shift; print "clicked link $link\n"; print 'Ev: ', join ', ', @_, "\n"; # shift, ', ', shift; unless ( $currentLongFile ) { my $noDirDialog = $mw->Dialog( -title => q[Can't follow wiki link!], -text => q[Can't follow links until you've saved this page to set the working directory. Do you want to save this page now and then follow the link?)], -buttons => ['Save page', 'Cancel'], # 'Save and follow link', -bitmap => 'info' ); my $answer = $noDirDialog->Show; return if $answer eq 'Cancel'; print "saving...\n"; saveAsFile() or return; # in case of cancellation # return if $answer eq 'Save page'; # now continue with clicking... } saveFile(); # work out what to open my ($name,$path,$ext) = fileparse( $currentLongFile, $fileExtension); $currentLongFile = $path . $link . $ext; if (-f $currentLongFile) { # determine if file exists or not openFile(); $statustext = $link; } else { newFile(); $statustext = $link . ' - unsaved'; insertinPageList($link); } return; } sub newCommand { # ask to save first? $text->delete('1.0','end'); $currentLongFile = ''; $statustext = 'New file (untitled, unsaved)'; } sub openCommand { # user selects 'Open' from a menu my $open = $mw->getOpenFile(-filetypes=>[['Wiki pages',"*$fileExtension"],['All Files','*.*']]) or return; # for user cancelation my $needRefresh; if( $currentLongFile ) { saveFile(); # later ask user if they want to lose changes # delete page list if new path... if( [fileparse( $open, $fileExtension)]->[1] ne [fileparse( $currentLongFile, $fileExtension)]->[1] ) { print "changed dir!\n"; deletePageList(); $needRefresh = 1; } } $currentLongFile = $open; buildPageList() if $needRefresh; openFile(); } sub openFile { # with $currentLongFile set, open it and present the text return unless $currentLongFile; # should never come here without it? print "opening $currentLongFile ...\n"; $text->delete('1.0','end'); open FILE, "<$currentLongFile"; while (<FILE>) { $text->insert('end',$_); } close FILE; $statustext = [fileparse( $currentLongFile, $fileExtension)]->[0] ; #$link; makeWikiLinks(); } sub saveCommand { if ( $currentLongFile ) { saveFile(); } else { saveAsFile() or return; } $statustext = [fileparse( $currentLongFile, $fileExtension)]->[0] ; #$link; # makeWikiLinks(); # needed here? } sub saveFile { open FILE , ">$currentLongFile"; my $wikitext = $text->get('1.0','end'); chomp $wikitext; # otherwise we add a \n to every saved file - DIRTY HACK! print FILE $wikitext; print "saving: $currentLongFile\n"; close FILE; # should determine if this is the first save of a file # if so, add to page list } sub saveAsFile { my $savedFile = $mw->getSaveFile(-defaultextension => 'txt'); # messy! -defaultextension doesn't take the initial dot! return 0 unless $savedFile; if( $currentLongFile ) { # delete page list if new path... if( [fileparse( $savedFile, $fileExtension)]->[1] ne [fileparse( $currentLongFile, $fileExtension)]->[1] ) { print "changed dir!\n"; deletePageList(); } else { # same directory # should add this page to the menu list } } $currentLongFile = $savedFile; # need to set page name too print "save as $savedFile\n"; saveFile(); $statustext = [fileparse( $currentLongFile, $fileExtension)]->[0] ; } sub newFile { $text->delete('1.0','end'); } sub exitCommand { storeConfig(); if ( $currentLongFile ) { saveFile(); } print "bye!\n"; exit; } ############################### MainLoop;