File Coverage

blib/lib/TiddlyWeb/Wikrad/Window.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             package TiddlyWeb::Wikrad::Window;
2 1     1   4616 use strict;
  1         3  
  1         40  
3 1     1   6 use warnings;
  1         2  
  1         29  
4 1     1   8 use base 'Curses::UI::Window';
  1         1  
  1         975  
5             use Curses qw/KEY_ENTER/;
6             use TiddlyWeb::Wikrad qw/$App/; # XXX cyclic
7             use TiddlyWeb::EditPage;
8             use JSON;
9             use Data::Dumper;
10             use YAML ();
11              
12             sub new {
13             my $class = shift;
14             my $self = $class->SUPER::new(@_);
15              
16             $self->_create_ui_widgets;
17             $self->read_config;
18              
19             my ($v, $p, $w, $t, $md, $mr) = map { $self->{$_} }
20             qw/viewer page_box workspace_box tag_box modified_box modifier_box/;
21             $v->focus;
22             $v->set_binding( \&show_help, '?' );
23             $v->set_binding( \&recently_changed, 'r' );
24             $v->set_binding( \&show_uri, 'u' );
25             if ($self->{config}{vim_insert_keys_start_vim}) {
26             for my $key (qw(i a o A)) {
27             $v->set_binding( sub { editor(
28             command => $key,
29             line => $v->{-ypos} + 1,
30             col => $v->{-xpos} + 1,
31             ) }, $key );
32             }
33             }
34             $v->set_binding( \&clone_page, 'c' );
35             $v->set_binding( \&show_metadata, 'm' );
36             $v->set_binding( \&change_server, 'S' );
37             $v->set_binding( \&save_to_file, 'W' );
38             $v->set_binding( \&search, 's' );
39             $v->set_binding( \&tag_page, 'T' );
40             $v->set_binding( \&process_macros, 'M' );
41              
42             $v->set_binding( sub { editor() }, 'e' );
43             $v->set_binding( sub { $v->focus }, 'v' );
44             $v->set_binding( sub { $p->focus; $self->{cb}{page}->($p) }, 'p' );
45             $v->set_binding( sub { $w->focus; $self->{cb}{workspace}->($w) }, 'w' );
46              
47             $v->set_binding( sub { $v->viewer_enter }, KEY_ENTER );
48             $v->set_binding( sub { $App->go_back }, 'b' );
49              
50             # this n/N messes up search next/prev
51             $v->set_binding( sub { $v->next_link }, 'n' );
52             $v->set_binding( sub { $v->prev_link }, 'N' );
53              
54             $v->set_binding( sub { $v->cursor_down }, 'j' );
55             $v->set_binding( sub { $v->cursor_up }, 'k' );
56             $v->set_binding( sub { $v->cursor_right }, 'l' );
57             $v->set_binding( sub { $v->cursor_left }, 'h' );
58             $v->set_binding( sub { $v->cursor_to_home }, '0' );
59             $v->set_binding( sub { $v->cursor_to_end }, 'G' );
60              
61             return $self;
62             }
63              
64             sub process_macros {
65             my $r = $App->{rester};
66             my $viewer = $App->{win}{viewer};
67             $App->{cui}->status('Processing Macros ...');
68             my $page_text = $viewer->text;
69             # deal with <>
70             while($page_text =~ m/(<>)/g) {
71             my $matched = $1;
72             my $command = $2;
73             my $args = $3;
74             $r->filter("$command:$args");
75             $r->accept('text/plain');
76             my @pages = split(/\n/, $r->get_pages());
77             $r->filter('');
78             my $new_text = '* ' . join("\n* ", map {"[[$_]]"} @pages);
79             $page_text =~ s/\Q$matched\E/$command:$args\n$new_text/;
80             }
81             # deal with <>
82             while($page_text =~ m/<>/g) {
83             my $included_page = $1;
84             $r->accept('perl_hash');
85             my $included_page_info = $r->get_page($included_page);
86             my $included_text = $included_page_info->{text};
87             my $new_text = "-----Included Tiddler----- [[$included_page]]\n"
88             . "$included_text\n"
89             . "-----End Include----- \n";
90             $page_text =~ s/<>/$new_text/;
91             }
92             $viewer->text($page_text);
93             $App->{cui}->nostatus;
94             }
95              
96             sub show_help {
97             $App->{cui}->dialog(
98             -fg => 'yellow',
99             -bg => 'blue',
100             -title => 'Help:',
101             -message => <
102             Basic Commands:
103             j/k/h/l/arrow keys - move cursor
104             n/N - move to next/previous link
105             ENTER - jump to page [under cursor]
106             space/- - page down/up
107             b - go back
108             e - open page for edit
109             r - choose from recently changed pages
110              
111             Awesome Commands:
112             0/G - move to beginning/end of page
113             w - set workspace
114             p - set page
115             s - search
116             u - show the uri for the current page
117             m - show page metadata (tags, revision)
118             M - process macros (tiddler, list)
119             T - Tag page
120             c - clone this page
121             S - Change REST server
122              
123             Find:
124             / - find forward
125             ? - find backwards
126             (Bad: find n/N conflicts with next/prev link)
127              
128             Ctrl-q / Ctrl-c / q - quit
129             EOT
130             }
131              
132             sub tag_page {
133             my $r = $App->{rester};
134             $r->accept('perl_hash');
135             my $page_name = $App->get_page;
136             my @tags = split(/\s*,\s*/, $App->{win}{tag_box}->text);
137             my $question = "Enter new tags, separate with commas, prefix with '-' to remove\n ";
138             if (@tags) {
139             $question .= join(", ", @tags) . "\n";
140             }
141             my $newtags = $App->{cui}->question($question) || '';
142             my @new_tags = split(/\s*,\s*/, $newtags);
143             my @store_tags;
144             if (@new_tags) {
145             $App->{cui}->status("Tagging $page_name with @new_tags...");
146             for my $t (@new_tags) {
147             unless ($t =~ m/^-/) {
148             push(@store_tags, $t);
149             }
150             }
151             my $page = $r->get_page($page_name);
152             $page->{tags} = \@store_tags;
153             eval { $r->put_page($page_name, $page); };
154             if ($@) {
155             $App->{cui}->dialog("Error: $@");
156             }
157             $App->{cui}->nostatus;
158             $App->set_page($page_name);
159             }
160             }
161              
162             sub show_metadata {
163             my $r = $App->{rester};
164             $App->{cui}->status('Fetching page metadata ...');
165             $r->accept('application/json');
166             my $page_name = $App->get_page;
167             my $json_text = $r->get_page($page_name);
168             my $page_data = from_json($json_text);
169             $App->{cui}->nostatus;
170             $App->{cui}->dialog(
171             -title => "$page_name metadata",
172             -message => Dumper $page_data,
173             );
174             }
175              
176             sub show_uri {
177             my $r = $App->{rester};
178             my $uri = $r->server . '/recipes/' . $r->workspace . '/tiddlers/'
179             . $App->get_page;
180             $App->{cui}->dialog( -title => "Current page:", -message => " $uri" );
181             }
182              
183             sub clone_page {
184             my @args = @_; # obj, key, args
185             my $template_page = $args[2] || $App->get_page;
186             my $r = $App->{rester};
187             $r->accept('perl_hash');
188             my $template = $r->get_page($template_page);
189             my $new_page = $App->{cui}->question("Title for new page:");
190             if ($new_page) {
191             $App->{cui}->status("Creating page ...");
192             eval { $r->put_page($new_page, $template); };
193             if ($@) {
194             $App->{cui}->dialog("Error: $@");
195             $App->set_page($template_page);
196             } else {
197             $App->set_page($new_page);
198             }
199             }
200             }
201              
202             sub recently_changed {
203             my $r = $App->{rester};
204             $App->{cui}->status('Fetching recent changes ...');
205             $r->accept('text/plain');
206             $r->count(250);
207             $r->order('-modified');
208             my @recent = $r->get_pages();
209             $r->count(0);
210             $r->order('');
211             $App->{cui}->nostatus;
212             $App->{win}->listbox(
213             -title => 'Choose a page link',
214             -values => \@recent,
215             change_cb => sub {
216             my $link = shift;
217             $App->set_page($link) if $link;
218             },
219             );
220             }
221              
222             sub choose_link {
223             my $method = shift;
224             my $text = shift;
225             my $arg = shift;
226             my $page = $App->get_page;
227             $App->{cui}->status("Fetching ${text}s");
228             $App->{rester}->accept('text/plain');
229             my @links = $App->{rester}->$method($page, $arg);
230             $App->{cui}->nostatus;
231             if (@links) {
232             $App->{win}->listbox(
233             -title => "Choose a $text",
234             -values => \@links,
235             change_cb => sub {
236             my $link = shift;
237             $App->set_page($link) if $link;
238             },
239             );
240             }
241             else {
242             $App->{cui}->error("No ${text}s");
243             }
244             }
245              
246             sub editor {
247             my %extra_args = @_;
248             $App->{cui}->status('Editing page');
249             $App->{cui}->leave_curses;
250              
251             my $ep = TiddlyWeb::EditPage->new(
252             rester => $App->{rester},
253             %extra_args,
254             );
255              
256             my $page = $App->get_page;
257             eval {
258             $ep->edit_page(
259             page => $page,
260             );
261             };
262             if ($@) {
263             my ($message) = ($@ =~ /(.*?)\n/);
264             $App->{cui}->reset_curses;
265             $App->{cui}->dialog("Error, so: $message");
266             $App->load_page;
267             } else {
268             $App->{cui}->reset_curses;
269             $App->load_page;
270             }
271              
272             }
273              
274             sub workspace_change {
275             my $new_wksp = $App->{win}{workspace_box}->text;
276             my $r = $App->{rester};
277             if ($new_wksp) {
278             $App->set_page(undef, $new_wksp);
279             }
280             else {
281             $App->{cui}->status('Fetching list of workspaces ...');
282             $r->accept('text/plain');
283             my @workspaces = $r->get_workspaces;
284             $App->{cui}->nostatus;
285             $App->{win}->listbox(
286             -title => 'Choose a workspace',
287             -values => \@workspaces,
288             change_cb => sub {
289             my $wksp = shift;
290             $App->set_page(undef, $wksp);
291             },
292             );
293             }
294             }
295              
296             sub search {
297             my $r = $App->{rester};
298              
299             my $query = $App->{cui}->question(
300             -question => "Search"
301             ) || return;
302              
303             $App->{cui}->status("Looking for pages matching your query");
304             $r->accept('text/plain');
305             $r->query($query);
306             $r->order('-modified');
307             my @matches = $r->get_search;
308             $r->query('');
309             $r->order('');
310             $App->{cui}->nostatus;
311             $App->{win}->listbox(
312             -title => 'Choose a page link',
313             -values => \@matches,
314             change_cb => sub {
315             my $link = shift;
316             $App->set_page($link) if $link;
317             },
318             );
319             }
320              
321             sub change_server {
322             my $r = $App->{rester};
323             my $old_server = $r->server;
324             my $question = <
325             Enter the REST server you'd like to use:
326             (Current server: $old_server)
327             EOT
328             my $new_server = $App->{cui}->question(
329             -question => $question,
330             -answer => $old_server,
331             ) || '';
332             if ($new_server and $new_server ne $old_server) {
333             $r->server($new_server);
334             }
335             }
336              
337             sub save_to_file {
338             my $r = $App->{rester};
339             my $filename;
340             eval {
341             my $page_name = $App->get_page;
342             $filename = $App->save_dir . "/$page_name.wiki";
343              
344             open(my $fh, ">$filename") or die "Can't open $filename: $!";
345             print $fh $App->{win}{viewer}->text;
346             close $fh or die "Couldn't write $filename: $!";
347             };
348             my $msg = $@ ? "Error: $@" : "Saved to $filename";
349             $App->{cui}->dialog(
350             -title => "Saved page to disk",
351             -message => $msg,
352             );
353             }
354              
355             sub toggle_editable {
356             my $w = shift;
357             my $cb = shift;
358             my $readonly = $w->{'-readonly'};
359              
360             my $new_text = $w->text;
361             $new_text =~ s/^\s*(.+?)\s*$/$1/;
362             $w->text($new_text);
363              
364             if ($readonly) {
365             $w->{last_text} = $new_text;
366             $w->cursor_to_home;
367             $w->focus;
368             }
369             else {
370             $App->{win}{viewer}->focus;
371             }
372              
373             $cb->() if $cb and !$readonly;
374              
375             if (! $readonly and $w->text =~ m/^\s*$/) {
376             $w->text($w->{last_text}) if $w->{last_text};
377             }
378              
379             $w->readonly(!$readonly);
380             $w->set_binding( sub { toggle_editable($w, $cb) }, KEY_ENTER );
381             }
382              
383             sub _create_ui_widgets {
384             my $self = shift;
385             my %widget_positions = (
386             workspace_field => {
387             -width => 18,
388             -x => 1,
389             },
390             page_field => {
391             -width => 44,
392             -x => 32,
393             },
394             tag_field => {
395             -width => 70,
396             -x => 1,
397             -y => 1,
398             label_padding => 5,
399             },
400             modified_field => {
401             -width => 18,
402             -x => 1,
403             -y => 2,
404             label_padding => 1,
405             },
406             modifier_field => {
407             -width => 39,
408             -x => 32,
409             -y => 2,
410             label_padding => 1,
411             },
412             help_label => {
413             -x => 1,
414             -y => 3,
415             },
416             page_viewer => {
417             -y => 4,
418             },
419             );
420            
421             my $win_width = $self->width;
422             # if ($win_width < 110 and $win_width >= 80) {
423             # $widget_positions{tag_field} = {
424             # -width => 18,
425             # -x => 1,
426             # -y => 1,
427             # label_padding => 6,
428             # };
429             # $widget_positions{help_label} = {
430             # -x => 32,
431             # -y => 1,
432             # };
433             # $widget_positions{page_viewer}{-y} = 2;
434             # }
435              
436             #######################################
437             # Create the Workspace label and field
438             #######################################
439             my $wksp_cb = sub { toggle_editable( shift, \&workspace_change ) };
440             $self->{cb}{workspace} = $wksp_cb;
441             $self->{workspace_box} = $self->add_field('Workspace:', $wksp_cb,
442             -text => $App->{rester}->workspace,
443             %{ $widget_positions{workspace_field} },
444             );
445              
446             #######################################
447             # Create the Page label and field
448             #######################################
449             my $page_cb = sub { toggle_editable( shift, sub { $App->load_page } ) };
450             $self->{cb}{page} = $page_cb;
451             $self->{page_box} = $self->add_field('Page:', $page_cb,
452             %{ $widget_positions{page_field} },
453             );
454              
455             #######################################
456             # Create the Tag label and field
457             #######################################
458             my $tag_cb = sub { return };
459             $self->{cb}{tag} = $tag_cb;
460             $self->{tag_box} = $self->add_field('Tags:', $tag_cb,
461             %{ $widget_positions{tag_field} },
462             );
463              
464             #######################################
465             # Create the modified label and field
466             #######################################
467             my $modified_cb = sub { return };
468             $self->{cb}{modified} = $modified_cb;
469             $self->{modified_box} = $self->add_field('Modified:', $modified_cb,
470             %{ $widget_positions{modified_field} },
471             );
472              
473             #######################################
474             # Create the modifier label and field
475             #######################################
476             my $modifier_cb = sub { return };
477             $self->{cb}{modifier} = $modifier_cb;
478             $self->{modifier_box} = $self->add_field('Modifier:', $modifier_cb,
479             %{ $widget_positions{modifier_field} },
480             );
481              
482             $self->add(undef, 'Label',
483             -bold => 1,
484             -text => "Help: hit '?'",
485             %{ $widget_positions{help_label} },
486             );
487              
488             #######################################
489             # Create the page Viewer
490             #######################################
491             $self->{viewer} = $self->add(
492             'viewer', 'TiddlyWeb::Wikrad::PageViewer',
493             -border => 1,
494             %{ $widget_positions{page_viewer} },
495             );
496             }
497              
498             sub listbox {
499             my $self = shift;
500             $App->{win}->add('listbox', 'TiddlyWeb::Wikrad::Listbox', @_)->focus;
501             }
502              
503             sub add_field {
504             my $self = shift;
505             my $desc = shift;
506             my $cb = shift;
507             my %args = @_;
508             my $x = $args{-x} || 0;
509             my $y = $args{-y} || 0;
510             my $label_padding = $args{label_padding} || 0;
511              
512             $self->add(undef, 'Label',
513             -bold => 1,
514             -text => $desc,
515             -x => $x,
516             -y => $y,
517             );
518             $args{-x} = $x + length($desc) + 1 + $label_padding;
519             my $w = $self->add(undef, 'TextEntry',
520             -singleline => 1,
521             -sbborder => 1,
522             -readonly => 1,
523             %args,
524             );
525             $w->set_binding( sub { $cb->($w) }, KEY_ENTER );
526             return $w;
527             }
528              
529             sub read_config {
530             my $self = shift;
531             my $file = "$ENV{HOME}/.wikradrc";
532              
533             return unless -r $file;
534             $self->{config} = YAML::LoadFile($file);
535             }
536              
537             1;