File Coverage

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