use strict; use vars qw($VERSION %IRSSI); use Irssi; use IPC::Open3; use CPAN::Meta::YAML; use Text::ParseWords; use Text::Wrap; use Time::HiRes; use File::Glob qw/:bsd_glob/; $VERSION = '0.2'; %IRSSI = ( authors => 'bw1', contact => 'bw1@aol.at', name => 'gitscriptassist', description => 'script management with git', license => 'Public Domain', url => 'https://scripts.irssi.org/', changed => '2020-02-02', modules => 'IPC::Open3 CPAN::Meta::YAML Text::ParseWords '. 'Text::Wrap Time::HiRes', commands=> "gitscriptassist" ); my $help= << "END"; %9Name%9 /gitscriptassist - $IRSSI{description} %9Version%9 $VERSION %9Description%9 \$ mkdir ~/foo \$ cd ~/foo \$ git clone https://github.com/irssi/scripts.irssi.org.git \$ irssi [(status)] /script load ~/foo/scripts.irssi.org/scripts/gitscriptassist.pl [(status)] /set gitscriptassist_repo ~/foo/scripts.irssi.org [(status)] /gitscriptassist search script [(status)] /quit \$ echo "/script load ~/foo/scripts.irssi.org/scripts/gitscriptassist.pl" >> \\ > ~/.irssi/startup \$ irssi %9Settings%9 %Ugitscriptassist_repo%U path to the git workingdir %Ugitscriptassist_path%U path for the tempory files of /gitscriptassist %Ugitscriptassist_startup%U load the scripts on startup %Ugitscriptassist_integrate%U integrate in the script command %9Commands%9 END my %scmds=( 'fetch'=>{ 'short'=>"git fetch -all", }, 'gitload'=>{ 'short'=>"load a script from the repository", 'usage'=>"/gitscriptassist gitload {filename[.pl]|hash:filename[.pl]}", 'file'=>1, }, 'info'=>{ 'short'=>"view script info", 'usage'=>"/gitscriptassist info ", 'file'=>1, }, 'log'=>{ 'short'=>"git log", 'usage'=>"/gitscriptassist log [filename[.pl]]", 'file'=>1, }, 'pull'=>{ 'short'=>"git pull", }, 'search'=>{ 'short'=>"search for word in scripts.yaml", 'usage'=>"/gitscriptassist search ", }, 'status'=>{ 'short'=>"git status", }, 'help'=>{ 'short'=>"show help", }, 'autoload'=>{ 'short'=>"manage autoload", 'usage'=>"/gitscriptassist autoload ", 'sub' => { 'list' => { 'short'=>"show the list for startup", }, 'add' => { 'short'=>"add a list entry for /script load", 'file'=>1, }, 'gitadd' => { 'short'=>"add a list entry for /script load via git", 'file'=>1, }, 'write' => { 'short'=>"write list to file", }, 'load' => { 'short'=>"load the list from file", }, 'startup' => { 'short'=>"trigger the startup", }, 'remove' => { 'short'=>"remove a list entry", }, 'move' => { 'short'=>"move a list entry", }, }, }, 'new'=>{ 'short'=>"show last modified scripts", 'usage'=>"/gitscriptassist new [max]", }, ); my ($repo, $path, $startup, $integrate); my $subproc; my @nproc; my %scripts; my %time_scr; my @comp_start; my @autoload; my ($fh_in, $fh_out, $fh_err); sub load_autoload { my $fh; my $fn = $path.'/autoload.yaml'; if (-e $fn) { open $fh, "<:utf8", $fn; my $yt = do { local $/; <$fh> }; my $yml= CPAN::Meta::YAML->read_string($yt); if (defined $yml->[0]) { @autoload =@{$yml->[0]}; } close $fh; if ($startup) { ascmd_startup(); } } } sub write_autoload { my $fh; my $fn = $path.'/autoload.yaml'; if (scalar(@autoload) >0) { open $fh, ">:utf8", $fn; my $yml =CPAN::Meta::YAML->new(\@autoload); print $fh $yml->write_string; close $fh; } } sub load_scripts { my $fh; my $f =$repo.'/_data/scripts.yaml'; my $fn = bsd_glob $f, GLOB_TILDE; if (-e $fn) { %time_scr= (); %scripts= (); open $fh, "<:utf8", $fn; my $yt = do { local $/; <$fh> }; my $yml= CPAN::Meta::YAML->read_string($yt); my @l =@{$yml->[0]}; foreach my $s (@l) { $scripts{$s->{filename}}=$s; } foreach my $s (@l) { if (!exists $time_scr{$s->{modified}}) { $time_scr{$s->{modified}} =[]; } push @{$time_scr{$s->{modified}}}, $s; } close $fh; } } sub run { my (%arg) =@_; if (!defined $subproc) { $subproc={%arg}; use Symbol 'gensym'; $fh_err = gensym; my $pid = open3 ($fh_in, $fh_out, $fh_err, $subproc->{cmd}); if (defined $pid) { $subproc->{pid}=$pid; Irssi::pidwait_add($pid); } } else { push @nproc, {%arg} } } sub sig_run_end { my ($pid, $status) = @_; if (defined $subproc) { my $old = select $fh_out; { local $/; $subproc->{out} = <$fh_out>; $subproc->{out} =~ s/\n$//; select $old; } { select $fh_err; local $/; $subproc->{err} = <$fh_err>; $subproc->{err} =~ s/\n$//; select $old; } if (exists $subproc->{next}) { if (ref ($subproc->{next}) eq 'CODE') { &{$subproc->{next}}(); } elsif (ref ($subproc->{next}) eq 'ARRAY') { foreach my $p (@{$subproc->{next}}) { if (ref ($p) eq 'CODE') { &{$p}(); } } } } $subproc = undef; if (scalar(@nproc) >0 ){ my %arg = %{shift @nproc}; run(%arg); } } } sub draw_box { my ($title, $text, $footer, $colour) = @_; my $box = ''; $box .= '%R,--[%n%9%U'.$title.'%U%9%R]%n'."\n"; foreach (split(/\n/, $text)) { $box .= '%R|%n '.$_."\n"; } $box .= '%R`--<%n'.$footer.'%R>->%n'; $box =~ s/%.//g unless $colour; return $box; } sub print_msg { my ( @te ); if ($subproc->{out} ne '') { push @te, $subproc->{out}; } if ($subproc->{err} ne '') { push @te,'E:'.$subproc->{cmd}; push @te,'E:'.$subproc->{err}; } if (defined $subproc->{label} && ($subproc->{out} ne '' || $subproc->{err} ne '' )) { Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,$subproc->{label}, 1), , MSGLEVEL_CLIENTCRAP); } } sub next_gitload { if ($subproc->{err} eq '') { Irssi::command("script load $path/$subproc->{filename}"); } } sub scmd_script_info { my ($server, $witem, @args) =@_; my @te; my $s = $scripts{$args[0]}; if (!defined $s) { $s = $scripts{$args[0].'.pl'}; } if (defined $s) { push @te, "name: $s->{name}"; push @te, "authors: $s->{authors}"; push @te, "description:"; my $d; { local $Text::Wrap::columns = 60; local $Text::Wrap::unexpand= 0; $d =wrap(' ',' ',$s->{description}); } push @te, $d; push @te, "filename: $s->{filename}"; push @te, "version: $s->{version}"; push @te, "modified: $s->{modified}"; Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'info' , 1), , MSGLEVEL_CLIENTCRAP); } } sub scmd_script_search { my ($server, $witem, @args) =@_; my @te; my @scrs; my $ml=0; my $w=$args[0]; foreach my $fn (sort keys %scripts) { my $s=$scripts{$fn}; if ( $s->{name} =~ m/$w/i || $s->{authors} =~ m/$w/i || $s->{description} =~ m/$w/i || $s->{filename} =~ m/$w/i ) { push @scrs, $s; my $l=length($s->{filename}); $ml=$l if ( $ml < $l); } } foreach my $s (@scrs) { my $i = sprintf "%-*s ", $ml, $s->{filename}; my $dt=$s->{description}; $dt=~ s/\n/ /g; $dt=~ s/\s+/ /g; my $d; { local $Text::Wrap::columns = 60; local $Text::Wrap::unexpand= 0; $d =wrap($i, ' 'x($ml+1), $dt); } push @te, $d; } Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'search' , 1), , MSGLEVEL_CLIENTCRAP); } sub scmd_gitload { my ($server, $witem, @args) =@_; my ($po, $fn); if ($args[0] =~ m/^(.*):(.*)$/) { $po=$1; $fn=$2; } else { $po='master'; $fn=$args[0]; } $fn .= '.pl' if ($fn !~ m/\.pl$/); run( 'cmd' => "git -C $repo show $po:scripts/$fn >$path/$fn", 'label'=> 'gitload', 'filename'=>$fn, 'point'=>$po, 'next' => [\&next_gitload,\&print_msg]); } sub scmd_help { my ($server, $witem, @args) =@_; my @te; if (scalar(@args) ==0 ) { chomp $help; push @te, $help; foreach my $c (sort keys %scmds) { if (exists $scmds{$c}->{short}) { push @te, sprintf(" %%9%-10s%%9 %s", $c, $scmds{$c}->{short}); } if (scalar(keys %{$scmds{$c}->{sub}}) ) { push @te, ' '.join ' ',sort keys %{$scmds{$c}->{sub}}; } } Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'help' , 1), , MSGLEVEL_CLIENTCRAP); } elsif ( exists $scmds{$args[0]} ) { my $sa = $args[0]; push @te, "%9/$IRSSI{name} $sa%9"; if (exists $scmds{$sa}->{short}) { push @te, " $scmds{$sa}->{short}"; } if (exists $scmds{$sa}->{usage}) { push @te, "%9Usage:%9"; push @te, " $scmds{$sa}->{usage}"; } if (scalar(keys %{$scmds{$sa}->{sub}}) >0) { push @te, "%9Commands:%9"; foreach my $su (sort keys %{$scmds{$sa}->{sub}}) { if (exists $scmds{$sa}->{sub}->{$su}->{short}) { push @te, sprintf(" %-10s %s", $su, $scmds{$sa}->{sub}->{$su}->{short}); } } } Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'help' , 1), , MSGLEVEL_CLIENTCRAP); } } sub ascmd_startup { my ($server, $witem, @args) =@_; foreach my $s (@autoload) { if (exists $s->{load}) { Irssi::command("script load $s->{load}"); } elsif (exists $s->{gitload}) { scmd_gitload($server, $witem, $s->{gitload}); } } } sub ascmd_list { my ($server, $witem, @args) =@_; my @te; my $co=0; foreach (@autoload){ my ($k, $f); ($k) = keys %$_; $f = $_->{$k}; push @te,sprintf("%4d %-10s %s", $co, $k, $f); $co++; } Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'autoload list' , 1), , MSGLEVEL_CLIENTCRAP); } sub scmd_autoload { my ($server, $witem, @args) =@_; my $c = shift @args; if ($c eq 'list') { ascmd_list($server, $witem, @args); } elsif ( $c eq 'add') { push @autoload, { load=>$args[0]}; } elsif ( $c eq 'gitadd') { push @autoload, { gitload=>$args[0]}; } elsif ( $c eq 'remove') { splice @autoload,$args[0],1; } elsif ( $c eq 'move') { my $b =splice @autoload,$args[0],1; my @ab =splice @autoload,$args[1]; push @autoload, $b; push @autoload, @ab; } elsif ( $c eq 'write') { write_autoload(); } elsif ( $c eq 'load') { load_autoload(); } elsif ( $c eq 'startup') { ascmd_startup($server, $witem, @args); } } sub scmd_new { my ($server, $witem, @args) =@_; my @te; my $co=1; my $max=5; if (defined $args[0]) { $max= $args[0]; } foreach my $t (sort { $b cmp $a } keys %time_scr) { foreach my $s ( @{$time_scr{$t}}) { push @te,"$t $s->{filename}"; $co++; } last if ($co > $max); } Irssi::print( draw_box($IRSSI{name}, join( "\n",@te) ,'new' , 1), , MSGLEVEL_CLIENTCRAP); } sub cmd { my ($args, $server, $witem)=@_; my @args = grep { $_ ne ''} quotewords('\s+', 0, $args); my $c =shift @args; if ($c eq 'gitload') { scmd_gitload($server, $witem, @args); } elsif ($c eq 'status') { run( 'cmd' => "git -C $repo status -sbuno", 'label'=> 'status', 'next' => [\&print_msg, \&load_scripts]); } elsif ($c eq 'pull') { run( 'cmd' => "git -C $repo pull", 'label'=> 'pull', 'next' => [\&print_msg, \&load_scripts]); } elsif ($c eq 'fetch') { run( 'cmd' => "git -C $repo fetch --all", 'label'=> 'fetch', 'next' => \&print_msg); } elsif ($c eq 'log') { my $s; if (defined $args[0]) { $s = "scripts/$args[0]"; if ($s !~ m/\.pl$/) { $s .=".pl"; } } run( 'cmd' => "git -C $repo log master -n 10 ". "--invert-grep --grep='automatic scripts database update' ". "--no-decorate --no-merges ". "--date=short ". "--pretty='format:%cd %h %s' ". "$s", 'label'=> 'log', 'next' => \&print_msg); } elsif ($c eq 'info') { scmd_script_info($server, $witem, @args); } elsif ($c eq 'search') { scmd_script_search($server, $witem, @args); } elsif ($c eq 'help') { scmd_help($server, $witem, @args); } elsif ($c eq 'new') { scmd_new($server, $witem, @args); } elsif ($c eq 'autoload') { scmd_autoload($server, $witem, @args); } } sub sig_setup_changed { my $r = Irssi::settings_get_str('gitscriptassist_repo'); if ($r ne $repo ) { $r =~ s#/$##; $repo= $r; %scripts=(); load_scripts(); } my $p = Irssi::settings_get_str('gitscriptassist_path'); $p =~ s#/$##; if ($p !~ m#^[~/]#) { $path = Irssi::get_irssi_dir().'/'.$p; } if (! -e $path ) { Irssi::print('gitscriptassist: make working dir "'.$path.'"', MSGLEVEL_CLIENTCRAP); mkdir $path; } $startup = Irssi::settings_get_bool('gitscriptassist_startup'); my $bi= Irssi::settings_get_bool('gitscriptassist_integrate'); if ($bi==1 && $integrate != $bi) { $integrate=$bi; bind_cmd('script'); } } sub do_complete { my ($strings, $window, $word, $linestart, $want_space) = @_; my $ok; foreach (@comp_start) { $ok=1 if ($linestart =~ m/^$_/); } return unless $ok; if ($word =~ m/^(.*:)/) { @$strings = grep { m/^$word/} map {$1.$_} keys %scripts; } else { @$strings = grep { m/^$word/} keys %scripts; } $$want_space = 1; Irssi::signal_stop; } sub bind_cmd { my ($cm)=@_; Irssi::command_bind($cm ,\&cmd); foreach my $c (keys %scmds) { Irssi::command_bind($cm .' '.$c,\&cmd); foreach my $s (keys %{$scmds{$c}->{sub}}) { Irssi::command_bind($cm .' '.$c.' '.$s,\&cmd); } } foreach my $sc (keys %scmds) { if (exists $scmds{$sc}->{file}) { push @comp_start, "/$cm $sc"; } foreach my $s (keys %{$scmds{$sc}->{sub}}) { if (exists $scmds{$sc}->{sub}->{$s}->{file}) { push @comp_start, "/$cm $sc $s"; } } } } sub UNLOAD { write_autoload(); } Irssi::command_bind('help', sub { my @args = grep { $_ ne '' } quotewords('\s+', 0, $_[0]); my $s = shift @args; if ($s eq $IRSSI{name} ) { scmd_help(undef, undef, @args); Irssi::signal_stop; } } ); Irssi::signal_add_first('complete word', \&do_complete); Irssi::signal_add('pidwait', 'sig_run_end'); Irssi::signal_add('setup changed', 'sig_setup_changed'); Irssi::settings_add_str($IRSSI{name}, 'gitscriptassist_repo', '~/foo/script-irssi'); Irssi::settings_add_str($IRSSI{name}, 'gitscriptassist_path', 'gitscriptassist'); Irssi::settings_add_bool($IRSSI{name}, 'gitscriptassist_startup', 0); Irssi::settings_add_bool($IRSSI{name}, 'gitscriptassist_integrate', 0); bind_cmd($IRSSI{name}); sig_setup_changed(); load_autoload();