use
5.010001;
our
$AUTHORITY
=
'cpan:PERLANCAR'
;
our
$DATE
=
'2023-10-30'
;
our
$DIST
=
'Perinci-CmdLine-Classic'
;
our
$VERSION
=
'1.818'
;
our
$REQ_VERSION
= 0;
has
color_theme
=> (
is
=>
'rw'
,
trigger
=>
sub
{
my
(
$self
,
$val
) =
@_
;
my
$obj
=
Module::Load::Util::instantiate_class_with_optional_args(
{
ns_prefix
=>
'ColorTheme'
},
$val
);
Role::Tiny->apply_roles_to_object(
$obj
,
'ColorThemeRole::ANSI'
);
$self
->{color_theme_obj} =
$obj
;
},
);
has
undo
=> (
is
=>
'rw'
,
default
=>
sub
{0});
has
undo_dir
=> (
is
=>
'rw'
,
lazy
=> 1,
default
=>
sub
{
my
$self
=
shift
;
my
$dir
= File::HomeDir->my_home .
"/."
.
$self
->program_name;
mkdir
$dir
unless
-d
$dir
;
$dir
.=
"/.undo"
;
mkdir
$dir
unless
-d
$dir
;
$dir
;
}
);
has
riap_client
=> (
is
=>
'rw'
,
lazy
=> 1,
default
=>
sub
{
my
$self
=
shift
;
my
%args
= (
riap_version
=>
$self
->riap_version,
%{
$self
->riap_client_args // {}},
);
my
%opts
;
if
(
$self
->undo) {
$opts
{use_tx} = 1;
$opts
{custom_tx_manager} =
sub
{
my
$pa
=
shift
;
state
$txm
= Perinci::Tx::Manager->new(
data_dir
=>
$self
->undo_dir,
pa
=>
$pa
,
);
$txm
;
};
}
$args
{handlers} = {
pl
=> Perinci::Access::Perl->new(
%opts
),
''
=> Perinci::Access::Schemeless->new(
%opts
),
};
Perinci::Access->new(
%args
);
}
);
has
action_metadata
=> (
is
=>
'rw'
,
default
=>
sub
{
return
{
clear_history
=> {
},
help
=> {
use_utf8
=> 1,
},
history
=> {
},
subcommands
=> {
use_utf8
=> 1,
},
redo
=> {
},
call
=> {
},
undo
=> {
},
version
=> {
use_utf8
=> 1,
},
},
},
);
has
default_prompt_template
=> (
is
=>
'rw'
);
sub
VERSION {
my
(
$pkg
,
$req
) =
@_
;
$REQ_VERSION
=
$req
;
$pkg
->SUPER::VERSION(
@_
);
}
sub
BUILD {
my
(
$self
,
$args
) =
@_
;
my
$formats
= [
qw(
text text-simple text-pretty
json json-pretty yaml perl
ruby phpserialization)
];
if
(!
$self
->{default_prompt_template}) {
$self
->{default_prompt_template} = N__(
"Enter %s:"
) .
" "
;
}
if
(!
$self
->{actions}) {
$self
->{actions} = {
version
=> {
use_utf8
=> 1,
},
help
=> {
use_utf8
=> 1,
},
subcommands
=> {
use_utf8
=> 1,
},
call
=> {},
history
=> {},
clear_history
=> {},
redo
=> {},
undo
=> {},
};
}
my
$_t
=
sub
{
no
warnings;
my
$co_name
=
shift
;
my
$copt
=
$Perinci::CmdLine::Base::copts
{
$co_name
};
my
%res
;
for
(
keys
%$copt
) {
if
(
$_
eq
'summary'
||
$_
eq
'usage'
) {
$res
{
$_
} = N__(
$copt
->{
$_
});
}
else
{
$res
{
$_
} =
$copt
->{
$_
};
}
}
%res
;
};
if
(!
$self
->{common_opts}) {
my
$copts
= {};
$copts
->{version} = {
$_t
->(
'version'
),
show_in_options
=>
sub
{
$ENV
{VERBOSE} },
};
$copts
->{help} = {
$_t
->(
'help'
),
show_in_options
=>
sub
{
$ENV
{VERBOSE} },
};
unless
(
$self
->skip_format) {
$copts
->{
format
} = {
$_t
->(
'format'
),
schema
=> [
'str*'
=>
in
=>
$formats
],
};
$copts
->{json} = {
$_t
->(
'json'
),
summary
=> N__(
"Equivalent to --format=json-pretty"
),
};
$copts
->{naked_res} = {
$_t
->(
'naked_res'
),
summary
=> N__(
"When outputing as JSON, strip result envelope"
),
};
$copts
->{format_options} = {
getopt
=>
"format-options=s"
,
summary
=> N__(
"Pass options to formatter"
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
$r
->{format_options} = __json_decode(
$val
);
},
is_settable_via_config
=> 1,
tags
=> [
'category:output'
],
};
}
if
(
$self
->subcommands) {
$copts
->{subcommands} = {
$_t
->(
'subcommands'
),
show_in_options
=>
sub
{
my
(
$self
,
$r
) =
@_
;
$ENV
{VERBOSE} && !
$r
->{subcommand_name};
},
show_in_help
=> 0,
};
}
if
(
defined
$self
->default_subcommand) {
$copts
->{cmd} = {
$_t
->(
'cmd'
) };
}
if
(
$self
->read_config) {
$copts
->{config_path} = {
$_t
->(
'config_path'
) };
$copts
->{no_config} = {
$_t
->(
'no_config'
) };
$copts
->{config_profile} = {
$_t
->(
'config_profile'
) };
}
if
(
$self
->read_env) {
$copts
->{no_env} = {
$_t
->(
'no_env'
) };
}
if
(
$self
->
log
) {
$copts
->{log_level} = {
$_t
->(
'log_level'
), };
$copts
->{trace} = {
$_t
->(
'trace'
), };
$copts
->{debug} = {
$_t
->(
'debug'
), };
$copts
->{verbose} = {
$_t
->(
'verbose'
), };
$copts
->{quiet} = {
$_t
->(
'quiet'
), };
}
if
(
$self
->undo) {
$copts
->{history} = {
getopt
=>
'history'
,
summary
=> N__(
'List actions history'
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
$r
->{action} =
'history'
;
$r
->{skip_parse_subcommand_argv} = 1;
},
tags
=> [
'category:undo'
],
key
=>
'action'
,
};
$copts
->{clear_history} = {
getopt
=>
"clear-history"
,
summary
=> N__(
'Clear actions history'
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
$r
->{action} =
'clear_history'
;
$r
->{skip_parse_subcommand_argv} = 1;
},
tags
=> [
'category:undo'
],
key
=>
'action'
,
};
$copts
->{undo} = {
getopt
=>
'undo'
,
summary
=> N__(
'Undo previous action'
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
$r
->{action} =
'undo'
;
$r
->{skip_parse_subcommand_argv} = 1;
},
tags
=> [
'category:undo'
],
key
=>
'action'
,
};
$copts
->{
redo
} = {
getopt
=>
'redo'
,
summary
=> N__(
'Redo previous undone action'
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
$r
->{action} =
'redo'
;
$r
->{skip_parse_subcommand_argv} = 1;
},
tags
=> [
'category:undo'
],
key
=>
'action'
,
};
}
$self
->{common_opts} =
$copts
;
}
$self
->{formats} //=
$formats
;
$self
->{per_arg_json} //= 1;
unless
(
$ENV
{COMP_LINE}) {
my
$ct
=
$self
->{color_theme} //
$ENV
{PERINCI_CMDLINE_COLOR_THEME};
if
(!
$ct
) {
if
(
$self
->use_color) {
my
$bg
=
$self
->detect_terminal->{default_bgcolor} //
''
;
$ct
=
'Perinci::CmdLine::Classic::Default'
.
(
$bg
eq
'ffffff'
?
'WhiteBG'
:
''
);
}
else
{
$ct
=
'NoColor'
;
}
}
$self
->color_theme(
$ct
);
}
}
sub
__json_decode {
state
$json
=
do
{ JSON::MaybeXS->new->allow_nonref };
$json
->decode(
shift
);
}
sub
__json_encode {
state
$json
=
do
{ JSON::MaybeXS->new->allow_nonref };
$json
->encode(
shift
);
}
sub
_color {
my
(
$self
,
$item_name
,
$text
) =
@_
;
my
$color_code
=
$item_name
?
$self
->{color_theme_obj}->get_item_color_as_ansi(
$item_name
) :
""
;
my
$reset_code
=
$color_code
?
"\e[0m"
:
""
;
"$color_code$text$reset_code"
;
}
sub
hook_format_row {
state
$dfpc
=
do
{
Data::Format::Pretty::Console->new({
interactive
=>0});
};
my
(
$self
,
$r
,
$row
) =
@_
;
my
$ref
=
ref
(
$row
);
if
(!
$ref
) {
return
(
$row
//
""
) .
"\n"
;
}
elsif
(
$ref
eq
'ARRAY'
&& !(
grep
{
ref
(
$_
)}
@$row
)) {
return
join
(
"\t"
,
map
{
$dfpc
->_format_cell(
$_
) }
@$row
) .
"\n"
;
}
else
{
return
$dfpc
->_format(
$row
);
}
}
sub
hook_after_get_meta {
my
(
$self
,
$r
) =
@_
;
my
$metao
= risub(
$r
->{meta});
if
(
$metao
->can_dry_run) {
my
$default_dry_run
=
$metao
->default_dry_run //
$self
->default_dry_run;
$r
->{dry_run} = 1
if
$default_dry_run
;
$r
->{dry_run} = (
$ENV
{DRY_RUN} ? 1:0)
if
defined
$ENV
{DRY_RUN};
$self
->common_opts->{dry_run} = {
getopt
=>
$default_dry_run
?
'dry-run!'
:
'dry-run'
,
summary
=>
$default_dry_run
?
N__(
"Disable simulation mode (also via DRY_RUN=0)"
) :
N__(
"Run in simulation mode (also via DRY_RUN=1)"
),
handler
=>
sub
{
my
(
$go
,
$val
,
$r
) =
@_
;
if
(
$val
) {
log_debug(
"[pericmd] Dry-run mode is activated"
);
$r
->{dry_run} = 1;
}
else
{
log_debug(
"[pericmd] Dry-run mode is deactivated"
);
$r
->{dry_run} = 0;
}
},
};
}
}
my
(
$ph1
,
$ph2
);
my
$setup_progress
;
sub
_setup_progress_output {
my
$self
=
shift
;
if
(
$ENV
{PROGRESS} // (-t STDOUT)) {
my
$out
= Progress::Any::Output->set(
"TermProgressBarColor"
);
$setup_progress
= 1;
$ph1
= Monkey::Patch::Action::patch_package(
'Log::Log4perl::Appender::Screen'
,
'log'
,
'replace'
,
sub
{
my
(
$self
,
%params
) =
@_
;
my
$msg
=
$params
{message};
$msg
=~ s/\n//g;
if
(
$out
->{lastlen}) {
print
"\b"
x
$out
->{lastlen},
" "
x
$out
->{lastlen},
"\b"
x
$out
->{lastlen};
undef
$out
->{lastlen};
}
$Progress::Any::output_data
{
"$out"
}{force_update} = 1;
say
$msg
;
},
)
if
defined
&{
"Log::Log4perl::Appender::Screen::log"
};
$ph2
= Monkey::Patch::Action::patch_package(
'Log::Log4perl::Appender::ScreenColoredLevels'
,
'log'
,
'replace'
,
sub
{
my
(
$self
,
%params
) =
@_
;
my
$msg
=
$params
{message};
$msg
=~ s/\n//g;
if
(
my
$color
=
$self
->{color}->{
$params
{log4p_level}}) {
$msg
= Term::ANSIColor::colored(
$msg
,
$color
);
}
if
(
$out
->{lastlen}) {
print
"\b"
x
$out
->{lastlen},
" "
x
$out
->{lastlen},
"\b"
x
$out
->{lastlen};
undef
$out
->{lastlen};
}
$Progress::Any::output_data
{
"$out"
}{force_update} = 1;
say
$msg
;
}
)
if
defined
&{
"Log::Log4perl::Appender::ScreenColoredLevels::log"
};
}
}
sub
_unsetup_progress_output {
my
$self
=
shift
;
return
unless
$setup_progress
;
no
warnings
'once'
;
my
$out
=
$Progress::Any::outputs
{
''
}[0];
$out
->cleanup
if
$out
->can(
"cleanup"
);
undef
$ph1
;
undef
$ph2
;
$setup_progress
= 0;
}
sub
hook_before_run {
my
(
$self
,
$r
) =
@_
;
log_trace(
"Start of CLI run"
);
$r
->{orig_argv} = [
@ARGV
];
}
sub
hook_before_parse_argv {
}
sub
hook_after_parse_argv {
}
sub
hook_format_result {
return
if
$ENV
{COMP_LINE};
my
(
$self
,
$r
) =
@_
;
my
$res
=
$r
->{res};
my
$format
=
$r
->{
format
} //
'text'
;
my
$meta
=
$r
->{meta};
unless
(
grep
{
$_
eq
$format
} @{
$self
->formats }) {
warn
"Unknown output format '$format'"
;
$format
=
'text'
;
}
$res
->[3]{format_options} =
$r
->{format_options}
if
$r
->{format_options};
my
$fres
;
if
(
$res
->[3]{is_stream}) {
log_trace(
"Result is a stream"
);
return
;
}
elsif
(
$res
->[3]{
'x.hint.result_binary'
} &&
$format
=~ /text/) {
$fres
=
$res
->[2];
}
else
{
log_trace(
"Formatting output with %s"
,
$format
);
$fres
= Perinci::Result::Format::
format
(
$res
,
$format
,
$r
->{naked_res});
}
if
(
$format
=~/text/ &&
$r
->{res}[0] =~ /\A[45]/ &&
defined
(
$r
->{res}[1])) {
$fres
=
"$self->{program_name}: $fres"
;
}
$fres
;
}
sub
hook_display_result {
my
(
$self
,
$r
) =
@_
;
my
$res
=
$r
->{res};
my
$resmeta
=
$res
->[3] // {};
my
$handle
=
$r
->{output_handle};
if
(
$ENV
{COMP_LINE} ||
$res
->[3]{
"cmdline.skip_format"
}) {
print
$handle
$res
->[2];
return
;
}
my
$utf8
;
{
if
(
$resmeta
->{
'x.hint.result_binary'
}) {
$utf8
= 0;
last
;
}
my
$am
;
$am
=
$self
->action_metadata->{
$r
->{action}}
if
$r
->{action};
last
if
defined
(
$utf8
=
$am
->{use_utf8});
if
(
$r
->{subcommand_data}) {
last
if
defined
(
$utf8
=
$r
->{subcommand_data}{use_utf8});
}
$utf8
=
$self
->use_utf8;
}
binmode
(
$handle
,
":encoding(utf8)"
)
if
$utf8
;
$self
->display_result(
$r
);
}
sub
hook_after_run {
my
(
$self
,
$r
) =
@_
;
$self
->_unsetup_progress_output;
}
sub
action_subcommands {
my
(
$self
,
$r
) =
@_
;
if
(!
$self
->subcommands) {
return
[200,
"OK"
, __(
"There are no subcommands"
) .
"."
,
{
"cmdline.skip_format"
=>1}];
}
$r
->{_help_buf} =
''
;
my
$subcommands
=
$self
->list_subcommands;
my
%percat_subc
;
while
(
my
(
$scn
,
$sc
) =
each
%$subcommands
) {
my
$cat
=
""
;
for
my
$tag
(@{
$sc
->{tags} // []}) {
my
$tn
=
ref
(
$tag
) ?
$tag
->{name} :
$tag
;
next
unless
$tn
=~ /^category:(.+)/;
$cat
= $1;
last
;
}
$percat_subc
{
$cat
} //= {};
$percat_subc
{
$cat
}{
$scn
} =
$sc
;
}
my
$has_many_cats
=
scalar
(
keys
%percat_subc
) > 1;
my
$i
= 0;
for
my
$cat
(
sort
keys
%percat_subc
) {
if
(
$has_many_cats
) {
$self
->_help_add_heading(
$r
,
__x(
"{category} subcommands"
,
category
=>
ucfirst
(
$cat
|| __(
"main"
))));
}
my
$subc
=
$percat_subc
{
$cat
};
for
my
$scn
(
sort
keys
%$subc
) {
my
$sc
=
$subc
->{
$scn
};
my
$summary
= rimeta(
$sc
)->langprop(
"summary"
);
$self
->_help_add_row(
$r
,
[
$self
->_color(
'program_name'
,
$scn
),
$summary
],
{
column_widths
=>[-17, -40],
indent
=>1});
}
}
$self
->_help_draw_curtbl(
$r
);
[200,
"OK"
,
$r
->{_help_buf},
{
"cmdline.skip_format"
=>1}];
}
sub
action_version {
no
strict
'refs'
;
my
(
$self
,
$r
) =
@_
;
my
$url
=
$r
->{subcommand_data}{url} //
$self
->url;
my
@text
;
{
my
$meta
=
$self
->get_meta(
$r
,
$url
);
push
@text
, __x(
"{program} version {version}"
,
program
=>
$self
->_color(
'program_name'
,
$self
->get_program_and_subcommand_name),
version
=>
$self
->_color(
'emphasis'
, (
$meta
->{entity_v} //
"?"
)),
),
(
$meta
->{entity_date} ?
" ($meta->{entity_date})"
:
""
),
"\n"
;
for
my
$mod
(@{
$meta
->{
'x.dynamic_generator_modules'
} // [] }) {
push
@text
,
" "
, __x(
"{program} version {version}"
,
program
=>
$self
->_color(
'emphasis'
,
$mod
),
version
=>
$self
->_color(
'emphasis'
, (${
"$mod\::VERSION"
} //
"?"
)),
),
(${
"$mod\::DATE"
} ?
" ("
.${
"$mod\::DATE"
}.
")"
:
""
),
"\n"
;
}
}
for
my
$url
(@{
$self
->extra_urls_for_version // [] }) {
my
$meta
=
$self
->get_meta(
$r
,
$url
);
push
@text
,
" "
, __x(
"{program} version {version}"
,
program
=>
$self
->_color(
'emphasis'
,
$url
),
version
=>
$self
->_color(
'emphasis'
, (
$meta
->{entity_v} //
"?"
)),
),
(
$meta
->{entity_date} ?
" ($meta->{entity_date})"
:
''
),
"\n"
;
}
push
@text
,
" "
, __x(
"{program} version {version}"
,
program
=>
$self
->_color(
'emphasis'
,
"Perinci::CmdLine::Classic"
),
version
=>
$self
->_color(
'emphasis'
,
$Perinci::CmdLine::Classic::VERSION
||
"dev"
),
),
(
$Perinci::CmdLine::Classic::DATE
?
" ($Perinci::CmdLine::Classic::DATE)"
:
""
),
"\n"
;
[200,
"OK"
,
join
(
""
,
@text
), {
"cmdline.skip_format"
=>1}];
}
sub
action_call {
my
(
$self
,
$r
) =
@_
;
my
$scn
=
$r
->{subcommand_name};
my
$scd
=
$r
->{subcommand_data};
my
%fargs
= %{
$r
->{args} // {}};
my
$tx_id
;
my
$dry_run
=
$r
->{dry_run};
my
$using_tx
= !
$dry_run
&&
$self
->undo && (
$scd
->{undo} // 1);
if
(
$r
->{send_argv} && (
$dry_run
||
$using_tx
)) {
return
$r
->{parse_argv_res};
}
if
(
$using_tx
) {
$tx_id
= UUID::Random::generate();
$tx_id
=~ s/-.+//;
my
$summary
=
join
(
" "
, @{
$r
->{orig_argv} });
my
$tres
=
$self
->riap_client->request(
begin_tx
=>
"/"
, {
tx_id
=>
$tx_id
,
summary
=>
$summary
});
if
(
$tres
->[0] != 200) {
return
[
$tres
->[0],
"Can't start transaction '$tx_id': $tres->[1]"
];
}
}
if
(
$r
->{meta}{features}{progress}) {
$self
->_setup_progress_output;
}
my
$res
;
if
(
$r
->{send_argv}) {
$res
=
$self
->riap_client->request(
call
=>
$scd
->{url},
{
argv
=>
$r
->{orig_argv}},
);
}
else
{
$res
=
$self
->riap_client->request(
call
=>
$scd
->{url},
{
args
=>\
%fargs
,
tx_id
=>
$tx_id
,
dry_run
=>
$dry_run
});
}
log_trace(
"call res=%s"
,
$res
);
if
(
$using_tx
&&
$res
->[0] =~ /\A(?:200|304)\z/) {
my
$tres
=
$self
->riap_client->request(
commit_tx
=>
"/"
, {
tx_id
=>
$tx_id
});
if
(
$tres
->[0] != 200) {
return
[
$tres
->[0],
"Can't commit transaction '$tx_id': $tres->[1]"
];
}
}
$res
;
}
sub
action_history {
my
(
$self
,
$r
) =
@_
;
my
$res
=
$self
->riap_client->request(
list_txs
=>
"/"
, {
detail
=>1});
log_trace(
"list_txs res=%s"
,
$res
);
return
$res
unless
$res
->[0] == 200;
$res
->[2] = [
sort
{(
$b
->{tx_commit_time}//0) <=> (
$a
->{tx_commit_time}//0)}
@{
$res
->[2]}];
my
@txs
;
for
my
$tx
(@{
$res
->[2]}) {
next
unless
$tx
->{tx_status} =~ /[CUX]/;
push
@txs
, {
id
=>
$tx
->{tx_id},
start_time
=>
$tx
->{tx_start_time},
commit_time
=>
$tx
->{tx_commit_time},
status
=>
$tx
->{tx_status} eq
'X'
?
'error'
:
$tx
->{tx_status} eq
'U'
?
'undone'
:
''
,
summary
=>
$tx
->{tx_summary},
};
}
[200,
"OK"
, \
@txs
];
}
sub
action_clear_history {
my
(
$self
,
$r
) =
@_
;
$self
->riap_client->request(
discard_all_txs
=>
"/"
);
}
sub
action_undo {
my
(
$self
,
$r
) =
@_
;
$self
->riap_client->request(
undo
=>
"/"
);
}
sub
action_redo {
my
(
$self
,
$r
) =
@_
;
$self
->riap_client->request(
redo
=>
"/"
);
}
1;