Add client.
[newgopher.git] / ngo
bloba08edbd21d809114e2695a6bb91e0ba0e532ccde
1 #!/usr/bin/perl
3 use strict;
4 use Curses::UI; # CPAN
5 use IO::Socket;
6 use File::Temp qw/ tempfile tempdir /;
7 use File::stat;
8 use File::Basename;
10 use NewGopher;
12 my $argo = 0;
13 my $dl_only = 0;
14 if (!$ARGV[0]) {
15 # show_banner();
16 show_usage();
17 exit 1;
19 if ($ARGV[0] =~ /^-/) {
20 if ($ARGV[0] eq '-o') {
21 $dl_only = 1;
22 $argo++;
26 my @history;
28 my ($host, $port, $selector) = ng_parse_uri($ARGV[$argo]);
30 if ($dl_only) {
31 my $shortname = fileparse($selector);
32 download_aux($host, $port, $selector, $shortname);
33 print "Saved (?) file $shortname\n";
34 exit 0;
37 my $cui = create_ui();
39 #if (!$selector) {
40 goto_dialog();
42 $cui->mainloop();
44 sub ngo_next_selector()
46 my $last_id = length(@history) - 1;
47 my $last = $history[ $last_id ];
48 my %item = %$last;
49 ng_advance($item{'host'}, $item{'port'}, $item{'selector'}, $item{'data'});
51 sub ngo_queue_selector
53 my ($host, $port, $selector, $post) = @_;
54 push @history, {
55 'uri' => 'gopher://'.$host.':'.$port.'/'.$selector,
56 'selector' => $selector,
57 'host' => $host,
58 'port' => $port,
59 'data' => $post,
63 sub create_ui()
65 my $cui = new Curses::UI( -color_support => 1 );
67 my @menu = (
68 { -label => 'File',
69 -submenu => [
70 { -label => 'Go To... g', -value => \&goto_dialog },
71 { -label => 'Exit ^C', -value => \&exit_dialog },
74 { -label => 'Link',
75 -submenu => [
76 { -label => 'Download... d', -value => \&download_dialog },
77 { -label => 'Follow f', -value => \&follow_cb },
78 #{ -label => 'Go Back b', -value => 0 },
83 my $menu = $cui->add(
84 'menu', 'Menubar',
85 -menu => \@menu,
86 -fg => "white",
89 $cui->set_binding(sub {$menu->focus}, "\cX");
90 $cui->set_binding(sub {$cui->layout}, "\cR");
91 $cui->set_binding( \&exit_dialog , "\cC");
92 $cui->set_binding( \&exit_dialog , "q");
93 $cui->set_binding( \&goto_dialog , "g");
94 $cui->set_binding( \&follow_cb , "f");
95 $cui->set_binding( \&download_dialog , "d");
97 my $win1 = $cui->add(
98 'win1', 'Window',
99 -border => 1,
100 -y => 1,
101 -padbottom => 1,
102 -bfg => 'red',
105 my $win2 = $cui->add(
106 'win2', 'Window',
107 -border => 0,
108 -y => -1,
109 -height => 1,
110 -bfg => 'blue',
113 my $statusbar = $win2->add(
114 'label', 'Label',
115 -width => -1,
116 -paddingspaces => 1,
117 -text => 'Ready. Use ^X to access Menu.',
118 -bg => "white",
119 -fg => "black",
122 $win1->focus;
124 return $cui;
127 sub titlebar
129 my $win = $cui->getobj('win1');
130 $win->title($_[0]);
131 #my $curlen = $label->width - length($_[0]) - 2;
132 #if ($curlen < 0) { $curlen = 0; }
133 #$label->text($_[0].''.sprintf('%'.$curlen.'s', ''));
134 #$label->draw;
137 sub statusbar
139 my $win = $cui->getobj('win2');
140 my $label = $win->getobj('label');
141 my $curlen = $label->width - length($_[0]) - 2;
142 if ($curlen < 0) { $curlen = 0; }
143 $label->text($_[0].''.sprintf('%'.$curlen.'s', ''));
144 $label->draw;
145 #$label->intellidraw;
148 sub exit_dialog()
150 my $return = $cui->dialog(
151 -message => "Do you really want to quit?",
152 -title => "Quit",
153 -buttons => ['yes', 'no'],
155 exit(0) if $return;
158 sub goto_dialog()
160 my $return = $cui->question(
161 -question => "'New Gopher' URI:",
162 -answer => $host.':'.$port.'/'.$selector,
163 #-title => "Are you sure???",
165 my ($host, $port, $selector) = ng_parse_uri($return);
167 ng_advance($host, $port, $selector);
168 #ngo_queue_selector($host, $port, $selector);
169 #$cui->schedule_event( \&ngo_next_selector );
173 sub ng_advance
175 my ($host, $port, $selector, $post) = @_;
177 statusbar("Getting $host:$port/$selector");
179 my $resp = gopher_grab($host, $port, $selector, $post);
181 if (!$resp) { return; }
183 my ($size, $mimetype, $data) = ng_parse_response($resp);
185 #Hack
186 statusbar("$selector [$mimetype] $size b");
188 if ($mimetype eq 'text/x-menu')
190 titlebar($selector);
191 gophmenu_dialog($size, $mimetype, $data);
193 elsif ($mimetype eq 'application/x-interactive')
195 handle_interactive($host, $port, $selector, $data);
197 elsif ($mimetype =~ /^text\//)
199 if (!$data) { $data = '<no data>'; }
200 my $title = $selector;
201 $title =~ s/\t.*//; # hide metadata
202 $cui->dialog(-message => $data, -title => $title, -bfg => "red");
204 else
206 $cui->dialog('File type ['.$mimetype.'] size '.$size.' should be downloaded'."\n".substr($data, 0, 1024));
208 #Hack
209 statusbar("$selector [$mimetype] $size b");
213 sub gophmenu_dialog {
214 my ($size, $type, $data) = @_;
216 my @menu = ng_parse_menu($data, $host, $port);
218 # Prepare ListBox
219 my (@values, %labels, $i);
220 $i = 0;
221 foreach (@menu) {
222 my %item = %$_;
223 $values[$i] = $i;
224 $labels{$i} = gophmenu_pretty($item{'type'}, $item{'name'}, $item{'selector'});
225 $i++;
228 my $win = $cui->getobj("win1");
229 my $lb = $win->getobj("mylistbox");
230 if ($lb) {
231 $lb->values( \@values );
232 $lb->labels( \%labels );
233 $lb->userdata( \@menu );
234 #$lb->focus();
235 return;
237 my $listbox = $win->add(
238 'mylistbox', 'Listbox',
239 -values => \@values,
240 -labels => \%labels,
241 -userdata => \@menu,
242 -onchange => \&gophmenu_select,
243 -onselchange=> \&gophmenu_hover,
245 #$listbox->focus();
248 sub gophmenu_getitem
250 my $listbox = $_[0];
251 my @udata = @{$listbox->userdata()};
252 my %item = %{$udata[ $listbox->get_active_id() ]};
253 return %item;
256 sub gophmenu_hover
258 my %item = gophmenu_getitem @_;
260 statusbar($item{'host'}.':'.$item{'port'}.'/'.$item{'selector'});
263 sub gophmenu_select
265 my $listbox = $_[0];
266 my %item = gophmenu_getitem $listbox;
268 $listbox->clear_selection();
270 ng_advance($item{'host'}, $item{'port'}, $item{'selector'});
271 #ngo_queue_selector($item{'host'}, $item{'port'}, $item{'selector'});
272 #$cui->schedule_event( \&ngo_next_selector );
275 sub follow_cb() {
276 my $list = $cui->getobj("win1")->getobj("mylistbox");
277 gophmenu_select($list);
280 sub download_dialog() {
281 my $list = $cui->getobj("win1")->getobj("mylistbox");
282 my %item = gophmenu_getitem $list;
284 if ($item{'type'} ne 'b') {
285 statusbar("Can't download non-binary selector ".$item{'selector'});
286 return;
289 my $shortname = fileparse($item{'selector'});
290 my $file = $cui->savefilebrowser(-file => $shortname);
292 download_aux($item{'host'}, $item{'port'}, $item{'selector'}, $file);
294 statusbar("Saved file ".$file);
297 sub gophmenu_pretty
299 my ($type, $name, $selector) = @_;
300 if ($type eq 'i') {
301 return sprintf(" -- %s", $name);
303 return sprintf("[%-4s] %s", $type, $name);
306 sub gopher_grab {
307 my ($host, $port, $selector, $post) = @_;
309 my $sock = new IO::Socket::INET(PeerAddr => $host, PeerPort => $port, Proto => 'tcp', );
310 if (!$sock) { $cui->dialog(-message =>"Can't connect to $host : $port", -title => "Error", -fg => "blue"); return ""; }
311 #return "Could not create socket: $!\n" unless $sock;
312 print $sock $selector."\r\n";
313 if ($post) {
314 print $sock length($post) . "\t\r\n";
315 print $sock $post;
317 my $data = '';
318 #$header = <$sock>
319 #my ($size, $mimetype) = split /\t/, $header;
320 binmode($sock);
321 while(<$sock>) {
322 $data .= $_;
324 close($sock);
325 # $cui->dialog("GOT:\n".$data);
326 return $data;
329 sub handle_interactive() {
331 my ($host, $port, $selector, $data) = @_;
332 my ($long_target, $rest) = split /\n/, $data, 2;
333 my $target = substr $long_target, 1;
334 undef $long_target;
336 my ($fh, $filename) = tempfile();
337 print $fh $rest;
338 close($fh);
340 my $sb = stat($filename);
341 my $mtime = $sb->mtime;
342 undef $sb;
344 system('$EDITOR '.$filename);
345 $cui->layout();
347 my $sb = stat($filename);
348 if ($mtime != $sb->mtime) {
349 my $fh; open $fh, $filename;
350 my $buf = '';
351 binmode($fh);
352 while (<$fh>) { $buf .= $_; }
353 close($fh);
355 ng_advance($host, $port, $target, $buf);
356 #ngo_queue_selector($host, $port, $target, $buf);
357 #$cui->schedule_event( \&ngo_next_selector );
358 } else {
359 statusbar("Interactive input aborted");
363 sub show_banner() {
366 sub show_usage() {
367 print "Usage: \n";
368 print " ./ngo [OPTIONS] HOST[:PORT][/SELECTOR] \n";
369 print " Options: \n";
370 print " -o Download file in non-interactive mode.\n";
371 #print " -p Post a Form file FILE.\n";
374 sub download_aux() {
375 my ($host, $port, $selector, $filename) = @_;
377 my $resp = gopher_grab($host, $port, $selector);
378 if (!$resp) { return; }
380 my ($size, $mimetype, $data) = ng_parse_response($resp);
382 open my $fh, '>'.$filename;
383 binmode $fh;
384 select $fh;
385 print $data;
386 close $fh;
387 select stdout;