unofficial mirror of meta@public-inbox.org
 help / color / mirror / Atom feed
* [PATCH 0/5] lei lcat - local cat (not lolcat :P)
@ 2021-04-27 11:07 Eric Wong
  2021-04-27 11:07 ` [PATCH 1/5] lei: add "ls-sync" command for listing sync folders Eric Wong
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

"lei lcat" is a convenience command to extract Message-IDs
from URLs and <$MSGID> or "id:$MSGID" args (or stdin) and
attempt to display them as text.

--format=text is now the default for lcat, and an option for
"lei q" for stdout users.  It decodes base64 and QP just like
the WWW interface.  It also supports ANSI terminal colors and
loads the diff ones from the users' existing git config.

It's actually my first time using Term::ANSIColor, even though
it's bundled with Perl since 5.6.

I got sidetracked on the sync stuff, but "ls-sync" exists, now.
I'm not sure how sync would work, especially since I want to
avoid reconnecting for imports...

Eric Wong (5):
  lei: add "ls-sync" command for listing sync folders
  lei blob: support retrieving attachments via $OID:$IDX
  lei: standardize on _lei_wq_eof callback for workers
  lei lcat: extract Message-IDs from URLs and show them
  lei q + lcat: support --format=text output

 MANIFEST                       |   5 +
 lib/PublicInbox/Hval.pm        |   2 +-
 lib/PublicInbox/LEI.pm         |  12 +-
 lib/PublicInbox/LeiBlob.pm     |  37 ++++-
 lib/PublicInbox/LeiConvert.pm  |   2 +-
 lib/PublicInbox/LeiExternal.pm |   2 +-
 lib/PublicInbox/LeiImport.pm   |   6 +-
 lib/PublicInbox/LeiLcat.pm     | 125 +++++++++++++++++
 lib/PublicInbox/LeiLsSync.pm   |  29 ++++
 lib/PublicInbox/LeiMirror.pm   |   6 +-
 lib/PublicInbox/LeiP2q.pm      |   2 +-
 lib/PublicInbox/LeiTag.pm      |   8 +-
 lib/PublicInbox/LeiToMail.pm   |  63 ++++++++-
 lib/PublicInbox/LeiViewText.pm | 237 +++++++++++++++++++++++++++++++++
 lib/PublicInbox/ViewDiff.pm    |   4 +-
 t/lei-import-imap.t            |   8 ++
 t/lei-import-maildir.t         |   3 +
 t/lei-lcat.t                   |  16 +++
 t/lei_lcat.t                   |  44 ++++++
 19 files changed, 584 insertions(+), 27 deletions(-)
 create mode 100644 lib/PublicInbox/LeiLcat.pm
 create mode 100644 lib/PublicInbox/LeiLsSync.pm
 create mode 100644 lib/PublicInbox/LeiViewText.pm
 create mode 100644 t/lei-lcat.t
 create mode 100644 t/lei_lcat.t


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/5] lei: add "ls-sync" command for listing sync folders
  2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
@ 2021-04-27 11:07 ` Eric Wong
  2021-04-27 11:07 ` [PATCH 2/5] lei blob: support retrieving attachments via $OID:$IDX Eric Wong
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

This will be useful, later.
---
 MANIFEST                       |  1 +
 lib/PublicInbox/LEI.pm         |  2 ++
 lib/PublicInbox/LeiExternal.pm |  2 +-
 lib/PublicInbox/LeiLsSync.pm   | 29 +++++++++++++++++++++++++++++
 t/lei-import-imap.t            |  2 ++
 t/lei-import-maildir.t         |  3 +++
 6 files changed, 38 insertions(+), 1 deletion(-)
 create mode 100644 lib/PublicInbox/LeiLsSync.pm

diff --git a/MANIFEST b/MANIFEST
index ce824fcf..d4e7d66f 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -202,6 +202,7 @@ lib/PublicInbox/LeiInput.pm
 lib/PublicInbox/LeiInspect.pm
 lib/PublicInbox/LeiLsLabel.pm
 lib/PublicInbox/LeiLsSearch.pm
+lib/PublicInbox/LeiLsSync.pm
 lib/PublicInbox/LeiMailSync.pm
 lib/PublicInbox/LeiMirror.pm
 lib/PublicInbox/LeiOverview.pm
diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm
index 39278de6..c170572b 100644
--- a/lib/PublicInbox/LEI.pm
+++ b/lib/PublicInbox/LEI.pm
@@ -161,6 +161,8 @@ our %CMD = ( # sorted in order of importance/use:
 'ls-external' => [ '[FILTER]', 'list publicinbox|extindex locations',
 	qw(format|f=s z|0 globoff|g invert-match|v local remote), @c_opt ],
 'ls-label' => [ '', 'list labels', qw(z|0 stats:s), @c_opt ],
+'ls-sync' => [ '', 'list sync folders',
+		qw(z|0 z|0 globoff|g invert-match|v local remote), @c_opt ],
 'forget-external' => [ 'LOCATION...|--prune',
 	'exclude further results from a publicinbox|extindex',
 	qw(prune), @c_opt ],
diff --git a/lib/PublicInbox/LeiExternal.pm b/lib/PublicInbox/LeiExternal.pm
index b0ebe947..3858085e 100644
--- a/lib/PublicInbox/LeiExternal.pm
+++ b/lib/PublicInbox/LeiExternal.pm
@@ -50,7 +50,7 @@ my %re_map = ( '*' => '[^/]*?', '?' => '[^/]',
 		'[' => '[', ']' => ']', ',' => ',' );
 
 sub glob2re {
-	my ($re) = @_;
+	my $re = $_[-1];
 	my $p = '';
 	my $in_bracket = 0;
 	my $qm = 0;
diff --git a/lib/PublicInbox/LeiLsSync.pm b/lib/PublicInbox/LeiLsSync.pm
new file mode 100644
index 00000000..71f111a9
--- /dev/null
+++ b/lib/PublicInbox/LeiLsSync.pm
@@ -0,0 +1,29 @@
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# front-end for the "lei ls-sync" sub-command
+package PublicInbox::LeiLsSync;
+use strict;
+use v5.10.1;
+use PublicInbox::LeiMailSync;
+
+sub lei_ls_sync {
+	my ($lei, $filter) = @_;
+	my $sto = $lei->_lei_store or return;
+	my $lms = $sto->search->lms or return;
+	my $opt = $lei->{opt};
+	my $re;
+	$re = defined($filter) ? qr/\Q$filter\E/ : qr/./ if $opt->{globoff};
+	$re //= $lei->glob2re($filter // '*');
+	my @f = $lms->folders;
+	@f = $opt->{'invert-match'} ? grep(!/$re/, @f) : grep(/$re/, @f);
+	if ($opt->{'local'} && !$opt->{remote}) {
+		@f = grep(!m!\A[a-z\+]+://!i, @f);
+	} elsif ($opt->{remote} && !$opt->{'local'}) {
+		@f = grep(m!\A[a-z\+]+://!i, @f);
+	}
+	my $ORS = $opt->{z} ? "\0" : "\n";
+	$lei->out(join($ORS, @f, ''));
+}
+
+1;
diff --git a/t/lei-import-imap.t b/t/lei-import-imap.t
index 4a3bd6d8..376a8b48 100644
--- a/t/lei-import-imap.t
+++ b/t/lei-import-imap.t
@@ -22,6 +22,8 @@ test_lei({ tmpdir => $tmpdir }, sub {
 	is_deeply(json_utf8->decode($lei_out), {}, 'no inspect stats, yet');
 
 	lei_ok('import', $url);
+	lei_ok 'ls-sync';
+	like($lei_out, qr!\A\Q$url\E;UIDVALIDITY=\d+\n\z!, 'ls-sync');
 
 	lei_ok('inspect', $url);
 	my $inspect = json_utf8->decode($lei_out);
diff --git a/t/lei-import-maildir.t b/t/lei-import-maildir.t
index 3e3d9188..808e1a73 100644
--- a/t/lei-import-maildir.t
+++ b/t/lei-import-maildir.t
@@ -40,6 +40,9 @@ test_lei(sub {
 	is_deeply($inspect, { sync => { "maildir:$md" => [ 'x:2,S' ] } },
 		'maildir sync info as expected');
 
+	lei_ok qw(ls-sync);
+	is($lei_out, "maildir:$md\n", 'ls-sync as expected');
+
 	lei_ok(qw(import), $md, \'import Maildir again');
 	$imp_err = $lei_err;
 	lei_ok(qw(q -d none s:boolean), \'lei q w/o dedupe');

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/5] lei blob: support retrieving attachments via $OID:$IDX
  2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
  2021-04-27 11:07 ` [PATCH 1/5] lei: add "ls-sync" command for listing sync folders Eric Wong
@ 2021-04-27 11:07 ` Eric Wong
  2021-04-27 11:07 ` [PATCH 3/5] lei: standardize on _lei_wq_eof callback for workers Eric Wong
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

We'll be supporting some sort of text view for pager or
piping to an $EDITOR buffer.
---
 lib/PublicInbox/LeiBlob.pm | 32 ++++++++++++++++++++++++++++++--
 t/lei-import-imap.t        |  6 ++++++
 2 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/lib/PublicInbox/LeiBlob.pm b/lib/PublicInbox/LeiBlob.pm
index e4cd4cca..4e52c8a5 100644
--- a/lib/PublicInbox/LeiBlob.pm
+++ b/lib/PublicInbox/LeiBlob.pm
@@ -84,12 +84,25 @@ sub do_solve_blob { # via wq_do
 	$solver->solve($lei->{env}, $log, $self->{oid_b}, $hints);
 }
 
+sub cat_attach_i { # Eml->each_part callback
+	my ($part, $depth, $idx) = @{$_[0]};
+	my $lei = $_[1];
+	my $want = $lei->{-attach_idx} // return;
+	return if $idx ne $want; # [0-9]+(?:\.[0-9]+)+
+	delete $lei->{-attach_idx};
+	$lei->out($part->body);
+}
+
 sub lei_blob {
 	my ($lei, $blob) = @_;
 	$lei->start_pager if -t $lei->{1};
 	my $opt = $lei->{opt};
 	my $has_hints = grep(defined, @$opt{qw(oid-a path-a path-b)});
 	my $lxs;
+	if ($blob =~ s/:([0-9\.]+)\z//) {
+		$lei->{-attach_idx} = $1;
+		$opt->{mail} = 1;
+	}
 
 	# first, see if it's a blob returned by "lei q" JSON output:k
 	if ($opt->{mail} // ($has_hints ? 0 : 1)) {
@@ -97,7 +110,7 @@ sub lei_blob {
 			$lxs = $lei->lxs_prepare;
 			$lei->ale->refresh_externals($lxs);
 		}
-		my $rdr = { 1 => $lei->{1} };
+		my $rdr = {};
 		if ($opt->{mail}) {
 			$rdr->{2} = $lei->{2};
 		} else {
@@ -105,7 +118,22 @@ sub lei_blob {
 		}
 		my $cmd = [ 'git', '--git-dir='.$lei->ale->git->{git_dir},
 				'cat-file', 'blob', $blob ];
-		waitpid(spawn($cmd, $lei->{env}, $rdr), 0);
+		if (defined $lei->{-attach_idx}) {
+			my $fh = popen_rd($cmd, $lei->{env}, $rdr);
+			require PublicInbox::Eml;
+			my $str = do { local $/; <$fh> };
+			if (close $fh) {
+				my $eml = PublicInbox::Eml->new(\$str);
+				$eml->each_part(\&cat_attach_i, $lei, 1);
+				my $idx = delete $lei->{-attach_idx};
+				defined($idx) and return $lei->fail(<<EOM);
+E: attachment $idx not found in $blob
+EOM
+			}
+		} else {
+			$rdr->{1} = $lei->{1};
+			waitpid(spawn($cmd, $lei->{env}, $rdr), 0);
+		}
 		return if $? == 0;
 		return $lei->child_error($?) if $opt->{mail};
 	}
diff --git a/t/lei-import-imap.t b/t/lei-import-imap.t
index 376a8b48..cf1fa49d 100644
--- a/t/lei-import-imap.t
+++ b/t/lei-import-imap.t
@@ -52,5 +52,11 @@ test_lei({ tmpdir => $tmpdir }, sub {
 	is(ref($x->{'lei/store'}), 'ARRAY', 'lei/store in inspect');
 	is(ref($x->{sync}), 'HASH', 'sync in inspect');
 	is(ref($x->{sync}->{$k[0]}), 'ARRAY', 'UID arrays in inspect');
+
+	my $psgi_attach = 'cfa3622cbeffc9bd6b0fc66c4d60d420ba74f60d';
+	lei_ok('blob', $psgi_attach);
+	like($lei_out, qr!^Content-Type: multipart/mixed;!sm, 'got full blob');
+	lei_ok('blob', "$psgi_attach:2");
+	is($lei_out, "b64\xde\xad\xbe\xef\n", 'got attachment');
 });
 done_testing;

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/5] lei: standardize on _lei_wq_eof callback for workers
  2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
  2021-04-27 11:07 ` [PATCH 1/5] lei: add "ls-sync" command for listing sync folders Eric Wong
  2021-04-27 11:07 ` [PATCH 2/5] lei blob: support retrieving attachments via $OID:$IDX Eric Wong
@ 2021-04-27 11:07 ` Eric Wong
  2021-04-27 11:07 ` [PATCH 4/5] lei lcat: extract Message-IDs from URLs and show them Eric Wong
  2021-04-27 11:07 ` [PATCH 5/5] lei q + lcat: support --format=text output Eric Wong
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

Simplify our internals a little bit.
---
 lib/PublicInbox/LEI.pm        | 2 +-
 lib/PublicInbox/LeiBlob.pm    | 5 ++---
 lib/PublicInbox/LeiConvert.pm | 2 +-
 lib/PublicInbox/LeiImport.pm  | 6 +++---
 lib/PublicInbox/LeiMirror.pm  | 6 ++----
 lib/PublicInbox/LeiP2q.pm     | 2 +-
 lib/PublicInbox/LeiTag.pm     | 8 ++++----
 7 files changed, 14 insertions(+), 17 deletions(-)

diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm
index c170572b..effc905a 100644
--- a/lib/PublicInbox/LEI.pm
+++ b/lib/PublicInbox/LEI.pm
@@ -531,7 +531,7 @@ sub workers_start {
 		'child_error' => [ \&child_error, $lei ],
 		($ops ? %$ops : ()),
 	};
-	$ops->{''} //= [ \&dclose, $lei ];
+	$ops->{''} //= [ $wq->can('_lei_wq_eof') || \&dclose, $lei ];
 	my $end = $lei->pkt_op_pair;
 	$wq->wq_workers_start($ident, $jobs, $lei->oldset, { lei => $lei });
 	delete $lei->{pkt_op_p};
diff --git a/lib/PublicInbox/LeiBlob.pm b/lib/PublicInbox/LeiBlob.pm
index 4e52c8a5..0b96bd04 100644
--- a/lib/PublicInbox/LeiBlob.pm
+++ b/lib/PublicInbox/LeiBlob.pm
@@ -10,7 +10,7 @@ use parent qw(PublicInbox::IPC);
 use PublicInbox::Spawn qw(spawn popen_rd which);
 use PublicInbox::DS;
 
-sub sol_done { # EOF callback for main daemon
+sub _lei_wq_eof { # EOF callback for main daemon
 	my ($lei) = @_;
 	my $sol = delete $lei->{sol} // return $lei->dclose; # already failed
 	$sol->wq_wait_old($lei->can('wq_done_wait'), $lei);
@@ -157,8 +157,7 @@ EOM
 	}
 	require PublicInbox::SolverGit;
 	my $self = bless { lxs => $lxs, oid_b => $blob }, __PACKAGE__;
-	my ($op_c, $ops) = $lei->workers_start($self, 'lei_solve', 1,
-		{ '' => [ \&sol_done, $lei ] });
+	my ($op_c, $ops) = $lei->workers_start($self, 'lei-blob', 1);
 	$lei->{sol} = $self;
 	$self->wq_io_do('do_solve_blob', []);
 	$self->wq_close(1);
diff --git a/lib/PublicInbox/LeiConvert.pm b/lib/PublicInbox/LeiConvert.pm
index 0ce49ea9..0c324169 100644
--- a/lib/PublicInbox/LeiConvert.pm
+++ b/lib/PublicInbox/LeiConvert.pm
@@ -52,7 +52,7 @@ sub lei_convert { # the main "lei convert" method
 	my $devfd = $lei->path_to_fd($ovv->{dst}) // return;
 	$lei->{opt}->{augment} = 1 if $devfd < 0;
 	$self->prepare_inputs($lei, \@inputs) or return;
-	my ($op_c, $ops) = $lei->workers_start($self, 'lei_convert', 1);
+	my ($op_c, $ops) = $lei->workers_start($self, 'lei-convert', 1);
 	$lei->{cnv} = $self;
 	$self->wq_io_do('process_inputs', []);
 	$self->wq_close(1);
diff --git a/lib/PublicInbox/LeiImport.pm b/lib/PublicInbox/LeiImport.pm
index daaa6753..e0d899cc 100644
--- a/lib/PublicInbox/LeiImport.pm
+++ b/lib/PublicInbox/LeiImport.pm
@@ -53,7 +53,7 @@ sub input_nntp_cb { # nntp_each
 	input_eml_cb($self, $eml, $self->{-import_kw} ? { kw => $kw } : undef);
 }
 
-sub import_done { # EOF callback for main daemon
+sub _lei_wq_eof { # EOF callback for main daemon
 	my ($lei) = @_;
 	my $imp = delete $lei->{imp} // return $lei->fail('BUG: {imp} gone');
 	$imp->wq_wait_old($lei->can('wq_done_wait'), $lei, 'non-fatal');
@@ -90,10 +90,10 @@ sub lei_import { # the main "lei import" method
 		my $nproc = $self->detect_nproc;
 		$j = $nproc if $j > $nproc;
 	}
-	my $ops = { '' => [ \&import_done, $lei ] };
+	my $ops = {};
 	$lei->{auth}->op_merge($ops, $self) if $lei->{auth};
 	$self->{-wq_nr_workers} = $j // 1; # locked
-	(my $op_c, $ops) = $lei->workers_start($self, 'lei_import', $j, $ops);
+	(my $op_c, $ops) = $lei->workers_start($self, 'lei-import', $j, $ops);
 	$lei->{imp} = $self;
 	net_merge_complete($self) unless $lei->{auth};
 	$op_c->op_wait_event($ops);
diff --git a/lib/PublicInbox/LeiMirror.pm b/lib/PublicInbox/LeiMirror.pm
index 15adb71b..50ab4c85 100644
--- a/lib/PublicInbox/LeiMirror.pm
+++ b/lib/PublicInbox/LeiMirror.pm
@@ -24,7 +24,7 @@ sub do_finish_mirror { # dwaitpid callback
 	$lei->dclose;
 }
 
-sub mirror_done { # EOF callback for main daemon
+sub _lei_wq_eof { # EOF callback for main daemon
 	my ($lei) = @_;
 	my $mrr = delete $lei->{mrr} or return;
 	$mrr->wq_wait_old(\&do_finish_mirror, $lei);
@@ -282,9 +282,7 @@ sub start {
 	require PublicInbox::Inbox;
 	require PublicInbox::Admin;
 	require PublicInbox::InboxWritable;
-	my ($op, $ops) = $lei->workers_start($self, 'lei_mirror', 1, {
-		'' => [ \&mirror_done, $lei ]
-	});
+	my ($op, $ops) = $lei->workers_start($self, 'lei_mirror', 1);
 	$lei->{mrr} = $self;
 	$self->wq_io_do('do_mirror', []);
 	$self->wq_close(1);
diff --git a/lib/PublicInbox/LeiP2q.pm b/lib/PublicInbox/LeiP2q.pm
index cb2309c7..3248afd7 100644
--- a/lib/PublicInbox/LeiP2q.pm
+++ b/lib/PublicInbox/LeiP2q.pm
@@ -188,7 +188,7 @@ sub lei_p2q { # the "lei patch-to-query" entry point
 	} else {
 		$self->{input} = $input;
 	}
-	my ($op, $ops) = $lei->workers_start($self, 'lei_p2q', 1);
+	my ($op, $ops) = $lei->workers_start($self, 'lei-p2q', 1);
 	$lei->{p2q} = $self;
 	$self->wq_io_do('do_p2q', []);
 	$self->wq_close(1);
diff --git a/lib/PublicInbox/LeiTag.pm b/lib/PublicInbox/LeiTag.pm
index f5791947..3cda2eca 100644
--- a/lib/PublicInbox/LeiTag.pm
+++ b/lib/PublicInbox/LeiTag.pm
@@ -19,9 +19,9 @@ sub input_eml_cb { # used by PublicInbox::LeiInput::input_fh
 
 sub input_mbox_cb { input_eml_cb($_[1], $_[0]) }
 
-sub tag_done { # EOF callback for main daemon
+sub _lei_wq_eof { # EOF callback for main daemon
 	my ($lei) = @_;
-	my $tag = delete $lei->{tag} or return;
+	my $tag = delete $lei->{tag} // return $lei->dclose;
 	$tag->wq_wait_old($lei->can('wq_done_wait'), $lei, 'non-fatal');
 }
 
@@ -52,11 +52,11 @@ sub lei_tag { # the "lei tag" method
 	$self->prepare_inputs($lei, \@argv) or return;
 	grep(defined, @$vmd_mod{qw(+kw +L -L -kw)}) or
 		return $lei->fail('no keywords or labels specified');
-	my $ops = { '' => [ \&tag_done, $lei ] };
+	my $ops = {};
 	$lei->{auth}->op_merge($ops, $self) if $lei->{auth};
 	$self->{vmd_mod} = $vmd_mod;
 	my $j = $self->{-wq_nr_workers} = 1; # locked for now
-	(my $op_c, $ops) = $lei->workers_start($self, 'lei_tag', $j, $ops);
+	(my $op_c, $ops) = $lei->workers_start($self, 'lei-tag', $j, $ops);
 	$lei->{tag} = $self;
 	net_merge_complete($self) unless $lei->{auth};
 	$op_c->op_wait_event($ops);

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 4/5] lei lcat: extract Message-IDs from URLs and show them
  2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
                   ` (2 preceding siblings ...)
  2021-04-27 11:07 ` [PATCH 3/5] lei: standardize on _lei_wq_eof callback for workers Eric Wong
@ 2021-04-27 11:07 ` Eric Wong
  2021-04-27 11:07 ` [PATCH 5/5] lei q + lcat: support --format=text output Eric Wong
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

It's a wrapper around "lei q" which extracts Message-IDs
from URLs, "<$MSGID>", "id:$MSGID" and attempts to display the
local version of the message.

Its main purpose is to extract Message-IDs out of
commonly-understood URLs to save users bandwidth and time
by displaying the message locally.  When reading from stdin,
it will discard things it doesn't understand, so you can just
pipe an entire "Link: $URL" line to it and it'll attempt to
pluck the Message-ID out of the URL.
---
 MANIFEST                   |   3 +
 lib/PublicInbox/LEI.pm     |   8 +++
 lib/PublicInbox/LeiLcat.pm | 125 +++++++++++++++++++++++++++++++++++++
 t/lei-lcat.t               |  16 +++++
 t/lei_lcat.t               |  44 +++++++++++++
 5 files changed, 196 insertions(+)
 create mode 100644 lib/PublicInbox/LeiLcat.pm
 create mode 100644 t/lei-lcat.t
 create mode 100644 t/lei_lcat.t

diff --git a/MANIFEST b/MANIFEST
index d4e7d66f..d3b46f8b 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -200,6 +200,7 @@ lib/PublicInbox/LeiImport.pm
 lib/PublicInbox/LeiInit.pm
 lib/PublicInbox/LeiInput.pm
 lib/PublicInbox/LeiInspect.pm
+lib/PublicInbox/LeiLcat.pm
 lib/PublicInbox/LeiLsLabel.pm
 lib/PublicInbox/LeiLsSearch.pm
 lib/PublicInbox/LeiLsSync.pm
@@ -400,6 +401,7 @@ t/lei-import-imap.t
 t/lei-import-maildir.t
 t/lei-import-nntp.t
 t/lei-import.t
+t/lei-lcat.t
 t/lei-mirror.t
 t/lei-p2q.t
 t/lei-q-kw.t
@@ -411,6 +413,7 @@ t/lei-tag.t
 t/lei.t
 t/lei_dedupe.t
 t/lei_external.t
+t/lei_lcat.t
 t/lei_mail_sync.t
 t/lei_overview.t
 t/lei_saved_search.t
diff --git a/lib/PublicInbox/LEI.pm b/lib/PublicInbox/LEI.pm
index effc905a..ef72758c 100644
--- a/lib/PublicInbox/LEI.pm
+++ b/lib/PublicInbox/LEI.pm
@@ -149,6 +149,14 @@ our %CMD = ( # sorted in order of importance/use:
 'up' => [ 'OUTPUT|--all', 'update saved search',
 	qw(jobs|j=s lock=s@ alert=s@ mua=s verbose|v+ all:s), @c_opt ],
 
+'lcat' => [ '--stdin|MSGID_OR_URL..', 'display local copy of message(s)',
+	'stdin|', # /|\z/ must be first for lone dash
+	# some of these options are ridiculous for lcat
+	@lxs_opt, qw(output|mfolder|o=s format|f=s dedupe|d=s threads|t+
+	sort|s=s reverse|r offset=i jobs|j=s globoff|g augment|a
+	import-before! lock=s@ rsyncable alert=s@ mua=s verbose|v+), @c_opt,
+	opt_dash('limit|n=i', '[0-9]+') ],
+
 'blob' => [ 'OID', 'show a git blob, reconstructing from mail if necessary',
 	qw(git-dir=s@ cwd! verbose|v+ mail! oid-a|A=s path-a|a=s path-b|b=s),
 	@lxs_opt, @c_opt ],
diff --git a/lib/PublicInbox/LeiLcat.pm b/lib/PublicInbox/LeiLcat.pm
new file mode 100644
index 00000000..f10452be
--- /dev/null
+++ b/lib/PublicInbox/LeiLcat.pm
@@ -0,0 +1,125 @@
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# lcat: local cat, display a local message by Message-ID or blob,
+# extracting from URL necessary
+# "lei lcat <URL|SPEC>"
+package PublicInbox::LeiLcat;
+use strict;
+use v5.10.1;
+use PublicInbox::LeiViewText;
+use URI::Escape qw(uri_unescape);
+use URI;
+use PublicInbox::MID qw($MID_EXTRACT);
+
+sub lcat_redispatch {
+	my ($lei, $out, $op_p) = @_;
+	my $l = bless { %$lei }, ref($lei);
+	delete $l->{sock};
+	$l->{''} = $op_p; # daemon only
+	eval {
+		$l->qerr("# updating $out");
+		up1($l, $out);
+		$l->qerr("# $out done");
+	};
+	$l->err($@) if $@;
+}
+
+sub extract_1 ($$) {
+	my ($lei, $x) = @_;
+	if ($x =~ m!\b([a-z]+?://\S+)!i) {
+		my $u = $1;
+		$u =~ s/[\>\]\)\,\.\;]+\z//;
+		$u = URI->new($u);
+		my $p = $u->path;
+		my $term;
+		if ($p =~ m!([^/]+\@[^/]+)!) { # common msgid pattern
+			$term = 'mid:'.uri_unescape($1);
+
+			# is it a URL which returns the full thread?
+			if ($u->scheme =~ /\Ahttps?/i &&
+				$p =~ m!/(?:T/?|t/?|t\.mbox\.gz|t\.atom)\b!) {
+
+				$lei->{mset_opt}->{threads} = 1;
+			}
+		} elsif ($u->scheme =~ /\Ahttps?/i &&
+				# some msgids don't have '@', see if it looks like
+				# a public-inbox URL:
+				$p =~ m!/([^/]+)/(raw|t/?|T/?|
+					t\.mbox\.gz|t\.atom)\z!x) {
+			$lei->{mset_opt}->{threads} = 1 if $2 && $2 ne 'raw';
+			$term = 'mid:'.uri_unescape($1);
+		}
+		$term;
+	} elsif ($x =~ $MID_EXTRACT) { # <$MSGID>
+		"mid:$1";
+	} elsif ($x =~ /\b((?:m|mid):\S+)/) { # our own prefixes (and mairix)
+		$1;
+	} elsif ($x =~ /\bid:(\S+)/) { # notmuch convention
+		"mid:$1";
+	} else {
+		undef;
+	}
+}
+
+sub extract_all {
+	my ($lei, @argv) = @_;
+	my $strict = !$lei->{opt}->{stdin};
+	my @q;
+	for my $x (@argv) {
+		if (my $term = extract_1($lei,$x)) {
+			push @q, $term;
+		} elsif ($strict) {
+			return $lei->fail(<<"");
+could not extract Message-ID from $x
+
+		}
+	}
+	@q ? join(' OR ', @q) : $lei->fail("no Message-ID in: @argv");
+}
+
+sub _stdin { # PublicInbox::InputPipe::consume callback for --stdin
+	my ($lei) = @_; # $_[1] = $rbuf
+	if (defined($_[1])) {
+		$_[1] eq '' and return eval {
+			if (my $dfd = $lei->{3}) {
+				chdir($dfd) or return $lei->fail("fchdir: $!");
+			}
+			my @argv = split(/\s+/, $lei->{mset_opt}->{qstr});
+			$lei->{mset_opt}->{qstr} = extract_all($lei, @argv)
+				or return;
+			$lei->_start_query;
+		};
+		$lei->{mset_opt}->{qstr} .= $_[1];
+	} else {
+		$lei->fail("error reading stdin: $!");
+	}
+}
+
+sub lei_lcat {
+	my ($lei, @argv) = @_;
+	my $lxs = $lei->lxs_prepare or return;
+	$lei->ale->refresh_externals($lxs);
+	my $sto = $lei->_lei_store(1);
+	$lei->{lse} = $sto->search;
+	my $opt = $lei->{opt};
+	my %mset_opt = map { $_ => $opt->{$_} } qw(threads limit offset);
+	$mset_opt{asc} = $opt->{'reverse'} ? 1 : 0;
+	$mset_opt{limit} //= 10000;
+	$opt->{sort} //= 'relevance';
+	$mset_opt{relevance} = 1;
+	$lei->{mset_opt} = \%mset_opt;
+	$opt->{'format'} //= 'mboxrd' unless defined($opt->{output});
+	if ($lei->{opt}->{stdin}) {
+		return $lei->fail(<<'') if @argv;
+no args allowed on command-line with --stdin
+
+		require PublicInbox::InputPipe;
+		PublicInbox::InputPipe::consume($lei->{0}, \&_stdin, $lei);
+		return;
+	}
+	$lei->{mset_opt}->{qstr} = extract_all($lei, @argv) or return;
+	$lei->_start_query;
+}
+
+1;
diff --git a/t/lei-lcat.t b/t/lei-lcat.t
new file mode 100644
index 00000000..e5f00706
--- /dev/null
+++ b/t/lei-lcat.t
@@ -0,0 +1,16 @@
+#!perl -w
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict; use v5.10.1; use PublicInbox::TestCommon;
+require_mods(qw(lei));
+
+test_lei(sub {
+	my $in = "\nMessage-id: <qp\@example.com>\n";
+	lei_ok([qw(lcat --stdin)], undef, { 0 => \$in, %$lei_opt });
+	unlike($lei_out, qr/\S/, 'nothing, yet');
+	lei_ok('import', 't/plack-qp.eml');
+	lei_ok([qw(lcat --stdin)], undef, { 0 => \$in, %$lei_opt });
+	like($lei_out, qr/qp\@example\.com/, 'got a result');
+});
+
+done_testing;
diff --git a/t/lei_lcat.t b/t/lei_lcat.t
new file mode 100644
index 00000000..536abdea
--- /dev/null
+++ b/t/lei_lcat.t
@@ -0,0 +1,44 @@
+#!perl -w
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+# unit test for "lei lcat" internals, see t/lei-lcat.t for functional test
+use strict;
+use v5.10.1;
+use Test::More;
+use_ok 'PublicInbox::LeiLcat';
+my $cb = \&PublicInbox::LeiLcat::extract_1;
+my $ck = sub {
+	my ($txt, $exp, $t) = @_;
+	my $lei = {};
+	is($cb->($lei, $txt), $exp, $txt);
+	($t ? is_deeply($lei, { mset_opt => { threads => 1 } }, "-t $exp")
+		: is_deeply($lei, {}, "no -t for $exp")) or diag explain($lei);
+};
+
+for my $txt (qw(https://example.com/inbox/foo@bar/
+		https://example.com/inbox/foo@bar
+		https://example.com/inbox/foo@bar/raw
+		id:foo@bar
+		mid:foo@bar
+		<foo@bar>
+		<https://example.com/inbox/foo@bar>
+		<https://example.com/inbox/foo@bar/raw>
+		<https://example.com/inbox/foo@bar/>
+		<nntp://example.com/foo@bar>)) {
+	$ck->($txt, 'mid:foo@bar');
+}
+
+for my $txt (qw(https://example.com/inbox/foo@bar/T/
+		https://example.com/inbox/foo@bar/t/
+		https://example.com/inbox/foo@bar/t.mbox.gz
+		<https://example.com/inbox/foo@bar/t.atom>
+		<https://example.com/inbox/foo@bar/t/>)) {
+	$ck->($txt, 'mid:foo@bar', '-t');
+}
+
+$ck->('https://example.com/x/foobar/T/', 'mid:foobar', '-t');
+$ck->('https://example.com/x/foobar/raw', 'mid:foobar');
+is($cb->(my $lei = {}, 'asdf'), undef, 'no Message-ID');
+is($cb->($lei = {}, 'm:x'), 'm:x', 'bare m: accepted');
+
+done_testing;

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 5/5] lei q + lcat: support --format=text output
  2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
                   ` (3 preceding siblings ...)
  2021-04-27 11:07 ` [PATCH 4/5] lei lcat: extract Message-IDs from URLs and show them Eric Wong
@ 2021-04-27 11:07 ` Eric Wong
  4 siblings, 0 replies; 6+ messages in thread
From: Eric Wong @ 2021-04-27 11:07 UTC (permalink / raw)
  To: meta

This is mainly for "lei lcat" where it's the default,
but I find it useful anyways compared to the JSON view.

Colors are loaded from ~/.config/lei/config, and fall back
to using diff colors from a normal git config
(e.g. ~/.gitconfig).
---
 MANIFEST                       |   1 +
 lib/PublicInbox/Hval.pm        |   2 +-
 lib/PublicInbox/LeiLcat.pm     |   2 +-
 lib/PublicInbox/LeiToMail.pm   |  63 ++++++++-
 lib/PublicInbox/LeiViewText.pm | 237 +++++++++++++++++++++++++++++++++
 lib/PublicInbox/ViewDiff.pm    |   4 +-
 6 files changed, 301 insertions(+), 8 deletions(-)
 create mode 100644 lib/PublicInbox/LeiViewText.pm

diff --git a/MANIFEST b/MANIFEST
index d3b46f8b..5933ddf4 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -218,6 +218,7 @@ lib/PublicInbox/LeiSucks.pm
 lib/PublicInbox/LeiTag.pm
 lib/PublicInbox/LeiToMail.pm
 lib/PublicInbox/LeiUp.pm
+lib/PublicInbox/LeiViewText.pm
 lib/PublicInbox/LeiXSearch.pm
 lib/PublicInbox/Linkify.pm
 lib/PublicInbox/Listener.pm
diff --git a/lib/PublicInbox/Hval.pm b/lib/PublicInbox/Hval.pm
index eab4738e..00b3c8b4 100644
--- a/lib/PublicInbox/Hval.pm
+++ b/lib/PublicInbox/Hval.pm
@@ -34,7 +34,7 @@ my %escape_sequence = (
 	"\x7f" => '\\x7f', # DEL
 );
 
-my %xhtml_map = (
+our %xhtml_map = (
 	'"' => '&#34;',
 	'&' => '&#38;',
 	"'" => '&#39;',
diff --git a/lib/PublicInbox/LeiLcat.pm b/lib/PublicInbox/LeiLcat.pm
index f10452be..87729acf 100644
--- a/lib/PublicInbox/LeiLcat.pm
+++ b/lib/PublicInbox/LeiLcat.pm
@@ -109,7 +109,7 @@ sub lei_lcat {
 	$opt->{sort} //= 'relevance';
 	$mset_opt{relevance} = 1;
 	$lei->{mset_opt} = \%mset_opt;
-	$opt->{'format'} //= 'mboxrd' unless defined($opt->{output});
+	$opt->{'format'} //= 'text' unless defined($opt->{output});
 	if ($lei->{opt}->{stdin}) {
 		return $lei->fail(<<'') if @argv;
 no args allowed on command-line with --stdin
diff --git a/lib/PublicInbox/LeiToMail.pm b/lib/PublicInbox/LeiToMail.pm
index 8b2f82dc..fa3af710 100644
--- a/lib/PublicInbox/LeiToMail.pm
+++ b/lib/PublicInbox/LeiToMail.pm
@@ -309,6 +309,26 @@ sub _imap_write_cb ($$) {
 	}
 }
 
+sub _text_write_cb ($$) {
+	my ($self, $lei) = @_;
+	my $dedupe = $lei->{dedupe};
+	$dedupe->prepare_dedupe if $dedupe;
+	my $lvt = $lei->{lvt};
+	my $ovv = $lei->{ovv};
+	$lei->{1} // die "no stdout ($ovv->{dst})"; # redirected earlier
+	$lei->{1}->autoflush(1);
+	binmode $lei->{1}, ':utf8';
+	my $lse = $lei->{lse}; # may be undef
+	sub { # for git_to_mail
+		my ($bref, $smsg, $eml) = @_;
+		$lse->xsmsg_vmd($smsg) if $lse;
+		$eml //= PublicInbox::Eml->new($bref); # copy bref
+		return if $dedupe && $dedupe->is_dup($eml, $smsg);
+		my $lk = $ovv->lock_for_scope;
+		$lei->out(${$lvt->eml_to_text($smsg, $eml)}, "\n");
+	}
+}
+
 sub write_cb { # returns a callback for git_to_mail
 	my ($self, $lei) = @_;
 	# _mbox_write_cb, _maildir_write_cb or _imap_write_cb
@@ -329,8 +349,6 @@ sub new {
 		$lei->{ovv}->{dst} = $dst .= '/' if substr($dst, -1) ne '/';
 	} elsif (substr($fmt, 0, 4) eq 'mbox') {
 		require PublicInbox::MboxReader;
-		(-d $dst || (-e _ && !-w _)) and die
-			"$dst exists and is not a writable file\n";
 		$self->can("eml2$fmt") or die "bad mbox format: $fmt\n";
 		$self->{base_type} = 'mbox';
 	} elsif ($fmt =~ /\Aimaps?\z/) { # TODO .onion support
@@ -347,9 +365,23 @@ sub new {
 		$dst = $lei->{ovv}->{dst} = $$uri; # canonicalized
 		$lei->{net} = $net;
 		$self->{base_type} = 'imap';
+	} elsif ($fmt eq 'text') {
+		require PublicInbox::LeiViewText;
+		$lei->{lvt} = PublicInbox::LeiViewText->new($lei);
+		$self->{base_type} = 'text';
 	} else {
 		die "bad mail --format=$fmt\n";
 	}
+	if ($self->{base_type} =~ /\A(?:text|mbox)\z/) {
+		(-d $dst || (-e _ && !-w _)) and die
+			"$dst exists and is not a writable file\n";
+	}
+	if ($self->{base_type} eq 'text') {
+		my @err = map {
+			defined($lei->{opt}->{$_}) ? "--$_" : ();
+		} (qw(mua save));
+		die "@err incompatible with $fmt\n" if @err;
+	}
 	$self->{dst} = $dst;
 	$lei->{dedupe} = $lei->{lss} // do {
 		my $dd_cls = 'PublicInbox::'.
@@ -429,6 +461,29 @@ sub _do_augment_imap {
 	}
 }
 
+sub _pre_augment_text {
+	my ($self, $lei) = @_;
+	my $dst = $lei->{ovv}->{dst};
+	my $out;
+	my $devfd = $lei->path_to_fd($dst) // die "bad $dst";
+	if ($devfd >= 0) {
+		$out = $lei->{$devfd};
+	} else { # normal-looking path
+		if (-p $dst) {
+			open $out, '>', $dst or die "open($dst): $!";
+		} elsif (-f _ || !-e _) {
+			# text allows augment, HTML/Atom won't
+			my $mode = $lei->{opt}->{augment} ? '>>' : '>';
+			open $out, $mode, $dst or die "open($mode, $dst): $!";
+		} else {
+			die "$dst is not a file or FIFO\n";
+		}
+	}
+	$lei->{ovv}->ovv_out_lk_init if !$lei->{ovv}->{lock_path};
+	$lei->{1} = $out;
+	undef;
+}
+
 sub _pre_augment_mbox {
 	my ($self, $lei) = @_;
 	my $dst = $lei->{ovv}->{dst};
@@ -523,8 +578,8 @@ sub pre_augment { # fast (1 disk seek), runs in same process as post_augment
 sub do_augment { # slow, runs in wq worker
 	my ($self, $lei) = @_;
 	# _do_augment_maildir, _do_augment_mbox, or _do_augment_imap
-	my $m = "_do_augment_$self->{base_type}";
-	$self->$m($lei);
+	my $m = $self->can("_do_augment_$self->{base_type}") or return;
+	$m->($self, $lei);
 }
 
 # fast (spawn compressor or mkdir), runs in same process as pre_augment
diff --git a/lib/PublicInbox/LeiViewText.pm b/lib/PublicInbox/LeiViewText.pm
new file mode 100644
index 00000000..6f5fca49
--- /dev/null
+++ b/lib/PublicInbox/LeiViewText.pm
@@ -0,0 +1,237 @@
+# Copyright (C) 2021 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# PublicInbox::Eml to (optionally colorized) text coverter for terminals
+# the non-HTML counterpart to PublicInbox::View
+package PublicInbox::LeiViewText;
+use strict;
+use v5.10.1;
+use PublicInbox::MsgIter qw(msg_part_text);
+use PublicInbox::ContentHash qw(git_sha);
+use PublicInbox::MID qw(references);
+use PublicInbox::View;
+use PublicInbox::Hval;
+use PublicInbox::ViewDiff;
+use PublicInbox::Spawn qw(popen_rd);
+use Term::ANSIColor;
+
+sub _xs {
+	# xhtml_map works since we don't search for HTML ([&<>'"])
+	$_[0] =~ s/([\x7f\x00-\x1f])/$PublicInbox::Hval::xhtml_map{$1}/sge;
+}
+
+my %DEFAULT_COLOR = (
+	# mutt names, loaded from ~/.config/lei/config
+	quoted => 'blue',
+	hdrdefault => 'cyan',
+	status => 'bright_cyan', # smsg stuff
+
+	# git names and defaults, falls back to ~/.gitconfig
+	new => 'green',
+	old => 'red',
+	meta => 'bold',
+	frag => 'cyan',
+	func => undef,
+	context => undef,
+);
+
+sub my_colored {
+	my ($self, $slot) = @_; # $_[2] = buffer
+	my $val = $self->{"color.$slot"} //=
+			$self->{-leicfg}->{"color.$slot"} //
+			$self->{-gitcfg}->{"color.diff.$slot"} //
+			$self->{-gitcfg}->{"diff.color.$slot"} //
+			$DEFAULT_COLOR{$slot};
+	$val = $val->[-1] if ref($val) eq 'ARRAY';
+	if (defined $val) {
+		# git doesn't use "_", Term::ANSIColor does
+		$val =~ s/\Abright([^_])/bright_$1/i;
+		${$self->{obuf}} .= Term::ANSIColor::colored($_[2], lc $val);
+	} else {
+		${$self->{obuf}} .= $_[2];
+	}
+}
+
+sub uncolored { ${$_[0]->{obuf}} .= $_[2] }
+
+sub new {
+	my ($cls, $lei) = @_;
+	my $self = bless { %{$lei->{opt}}, -colored => \&uncolored }, $cls;
+	return $self unless $self->{color} || -t $lei->{1};
+	my $cmd = [ qw(git config -z --includes -l) ];
+	my ($r, $pid) = popen_rd($cmd, undef, { 2 => $lei->{2} });
+	my $cfg = PublicInbox::Config::config_fh_parse($r, "\0", "\n");
+	waitpid($pid, 0);
+	if ($?) {
+		$lei->err("# git-config failed, no color (non-fatal)");
+		return $self;
+	}
+	$self->{-colored} = \&my_colored;
+	$self->{-gitcfg} = $cfg;
+	$self->{-leicfg} = $lei->{cfg};
+	$self;
+}
+
+sub hdr_buf ($$) {
+	my ($self, $eml) = @_;
+	my $hbuf = '';
+	for my $f (qw(From To Cc)) {
+		for my $v ($eml->header($f)) {
+			next if $v !~ /\S/;
+			PublicInbox::View::fold_addresses($v);
+			_xs($v);
+			$hbuf .= "$f: $v\n";
+		}
+	}
+	for my $f (qw(Subject Date Newsgroups Message-ID X-Message-ID)) {
+		for my $v ($eml->header($f)) {
+			_xs($v);
+			$hbuf .= "$f: $v\n";
+		}
+	}
+	if (my @irt = $eml->header_raw('In-Reply-To')) {
+		for my $v (@irt) {
+			_xs($v);
+			$hbuf .= "In-Reply-To: $v\n";
+		}
+	} else {
+		my $refs = references($eml);
+		if (defined(my $irt = pop @$refs)) {
+			_xs($irt);
+			$hbuf .= "In-Reply-To: <$irt>\n";
+		}
+		if (@$refs) {
+			my $max = $self->{-max_cols};
+			$hbuf .= 'References: ' .
+				join("\n\t", map { '<'._xs($_).'>' } @$refs) .
+				">\n";
+		}
+	}
+	$self->{-colored}->($self, 'hdrdefault', $hbuf .= "\n");
+}
+
+sub attach_note ($$$$;$) {
+	my ($self, $ct, $p, $fn, $err) = @_;
+	my ($part, $depth, $idx) = @$p;
+	my $obuf = $self->{obuf};
+	my $nl = $idx eq '1' ? '' : "\n"; # like join("\n", ...)
+	$$obuf .= <<EOF if $err;
+[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
+EOF
+	my $blob = $self->{-smsg}->{blob} // '';
+	$blob .= ':' if $blob ne '';
+	$$obuf .= "[-- Attachment $blob$idx ";
+	_xs($ct);
+	my $size = length($part->body);
+	my $ts = "Type: $ct, Size: $size bytes";
+	my $d = $part->header('Content-Description') // $fn // '';
+	_xs($d);
+	$$obuf .= $d eq '' ? "$ts --]\n" : "$d --]\n[-- $ts --]\n";
+	hdr_buf($self, $part) if $part->{is_submsg};
+}
+
+sub flush_text_diff ($$) {
+	my ($self, $cur) = @_;
+	my @top = split($PublicInbox::ViewDiff::EXTRACT_DIFFS, $$cur);
+	undef $$cur; # free memory
+	my $dctx;
+	my $obuf = $self->{obuf};
+	my $colored = $self->{-colored};
+	while (defined(my $x = shift @top)) {
+		if (scalar(@top) >= 4 &&
+				$top[1] =~ $PublicInbox::ViewDiff::IS_OID &&
+				$top[0] =~ $PublicInbox::ViewDiff::IS_OID) {
+			splice(@top, 0, 4);
+			$dctx = 1;
+			$colored->($self, 'meta', $x);
+		} elsif ($dctx) {
+			# Quiet "Complex regular subexpression recursion limit"
+			# warning.  Perl will truncate matches upon hitting
+			# that limit, giving us more (and shorter) scalars than
+			# would be ideal, but otherwise it's harmless.
+			#
+			# We could replace the `+' metacharacter with `{1,100}'
+			# to limit the matches ourselves to 100, but we can
+			# let Perl do it for us, quietly.
+			no warnings 'regexp';
+
+			for my $s (split(/((?:(?:^\+[^\n]*\n)+)|
+					(?:(?:^-[^\n]*\n)+)|
+					(?:^@@ [^\n]+\n))/xsm, $x)) {
+				if (!defined($dctx)) {
+					${$self->{obuf}} .= $s;
+				} elsif ($s =~ s/\A(@@ \S+ \S+ @@\s*)//) {
+					$colored->($self, 'frag', $1);
+					$colored->($self, 'func', $s);
+				} elsif ($s =~ /\A\+/) {
+					$colored->($self, 'new', $s);
+				} elsif ($s =~ /\A-- $/sm) { # email sig starts
+					$dctx = undef;
+					${$self->{obuf}} .= $s;
+				} elsif ($s =~ /\A-/) {
+					$colored->($self, 'old', $s);
+				} else {
+					$colored->($self, 'context', $s);
+				}
+			}
+		} else {
+			${$self->{obuf}} .= $x;
+		}
+	}
+}
+
+sub add_text_buf { # callback for Eml->each_part
+	my ($p, $self) = @_;
+	my ($part, $depth, $idx) = @$p;
+	my $ct = $part->content_type || 'text/plain';
+	my $fn = $part->filename;
+	my ($s, $err) = msg_part_text($part, $ct);
+	return attach_note($self, $ct, $p, $fn) unless defined $s;
+	hdr_buf($self, $part) if $part->{is_submsg};
+	$s =~ s/\r\n/\n/sg;
+	_xs($s);
+	$s .= "\n" unless substr($s, -1, 1) eq "\n";
+	my $diff = ($s =~ /^--- [^\n]+\n\+{3} [^\n]+\n@@ /ms);
+	my @sections = PublicInbox::MsgIter::split_quotes($s);
+	undef $s; # free memory
+	if (defined($fn) || ($depth > 0 && !$part->{is_submsg}) || $err) {
+		# badly-encoded message with $err? tell the world about it!
+		attach_note($self, $ct, $p, $fn, $err);
+		${$self->{obuf}} .= "\n";
+	}
+	my $colored = $self->{-colored};
+	for my $cur (@sections) {
+		if ($cur =~ /\A>/) {
+			$colored->($self, 'quoted', $cur);
+		} elsif ($diff) {
+			flush_text_diff($self, \$cur);
+		} else {
+			${$self->{obuf}} .= $cur;
+		}
+		undef $cur; # free memory
+	}
+}
+
+# returns an arrayref suitable for $lei->out or print
+sub eml_to_text {
+	my ($self, $smsg, $eml) = @_;
+	local $Term::ANSIColor::EACHLINE = "\n";
+	$self->{obuf} = \(my $obuf = '');
+	$self->{-smsg} = $smsg;
+	$self->{-max_cols} = ($self->{columns} //= 80) - 8; # for header wrap
+	my @h = ();
+	for my $f (qw(blob pct)) {
+		push @h, "$f:$smsg->{$f}" if defined $smsg->{$f};
+	}
+	@h = ("# @h\n") if @h;
+	for my $f (qw(kw L)) {
+		my $v = $smsg->{$f} or next;
+		push @h, "# $f:".join(',', @$v)."\n" if @$v;
+	}
+	$self->{-colored}->($self, 'status', join('', @h));
+	hdr_buf($self, $eml);
+	$eml->each_part(\&add_text_buf, $self, 1);
+	delete $self->{obuf};
+}
+
+1;
diff --git a/lib/PublicInbox/ViewDiff.pm b/lib/PublicInbox/ViewDiff.pm
index 8fe7261f..e9a7bf69 100644
--- a/lib/PublicInbox/ViewDiff.pm
+++ b/lib/PublicInbox/ViewDiff.pm
@@ -30,7 +30,7 @@ my $DIFFSTAT_COMMENT =
 my $NULL_TO_BLOB = qr/^(index $OID_NULL\.\.)($OID_BLOB)\b/ms;
 my $BLOB_TO_NULL = qr/^index ($OID_BLOB)(\.\.$OID_NULL)\b/ms;
 my $BLOB_TO_BLOB = qr/^index ($OID_BLOB)\.\.($OID_BLOB)/ms;
-my $EXTRACT_DIFFS = qr/(
+our $EXTRACT_DIFFS = qr/(
 		(?:	# begin header stuff, don't capture filenames, here,
 			# but instead wait for the --- and +++ lines.
 			(?:^diff\x20--git\x20$FN\x20$FN$LF)
@@ -41,7 +41,7 @@ my $EXTRACT_DIFFS = qr/(
 		^index\x20($OID_BLOB)\.\.($OID_BLOB)$ANY*$LF
 		^---\x20($FN)$LF
 		^\+{3}\x20($FN)$LF)/msx;
-my $IS_OID = qr/\A$OID_BLOB\z/s;
+our $IS_OID = qr/\A$OID_BLOB\z/s;
 
 # link to line numbers in blobs
 sub diff_hunk ($$$$) {

^ permalink raw reply related	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2021-04-27 11:07 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-04-27 11:07 [PATCH 0/5] lei lcat - local cat (not lolcat :P) Eric Wong
2021-04-27 11:07 ` [PATCH 1/5] lei: add "ls-sync" command for listing sync folders Eric Wong
2021-04-27 11:07 ` [PATCH 2/5] lei blob: support retrieving attachments via $OID:$IDX Eric Wong
2021-04-27 11:07 ` [PATCH 3/5] lei: standardize on _lei_wq_eof callback for workers Eric Wong
2021-04-27 11:07 ` [PATCH 4/5] lei lcat: extract Message-IDs from URLs and show them Eric Wong
2021-04-27 11:07 ` [PATCH 5/5] lei q + lcat: support --format=text output Eric Wong

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).