The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#! /usr/bin/perl

use strict;
use warnings;


=head1 NAME

psst - prompt string setting tool

=head1 VERSION

version 0.09

=head1 SYNOPSIS

In F<~/.bashrc> add

 # XXX: set up PATH and PERL5LIB as necessary
 
 # Set shell and environment variables and create a function for
 # prompt updating
 eval "$( psst prompt "$PS1" )"

Then use L<local::lib> as normal, by whatever means.

=head1 GOALS

This script is intended to run during the login profile.  It ought to
be reliable and failsafe, lightweight and require minimal early
configuration.

=head1 CONFIGURATION

Configuration goes in F<~/.psst.yaml> and should consist of key/value
pairs for one hash (dictionary).  It is entirely optional, but can
only be read if L<YAML> is installed.

=over 4

=item PS1_old

Write the base value for $PS1 here to avoid the need to pass it to
each call to C<psst prompt>.

=item PS1_substs

If present, should be a list of C<< [ $text, $replacement ] >> pairs.
These are applied in order to the C<$PERL_LOCAL_LIB_ROOT> by Bash,
after being munged into a set of Bash Variable Substitutions.

=back

=head1 CAVEATS

Author reserves the right to turn this into the Portable Sanity
Support Tool by adding other subcommands.  A symlink-farm in F<~/bin/>
is a likely next target.

=cut


sub main {
  syntax() if "@ARGV" =~ /^(--help|-h)\b/ || !@ARGV;

  my $subcmd = shift @ARGV;
  if ($subcmd eq 'prompt') {
    die "$0 prompt: takes one optional parameter, got ".@ARGV if @ARGV > 1;
    my $old_ps1 = prompt_base(PS1 => @ARGV);
    print prompt_set(PS1 => $old_ps1);
  } elsif ($subcmd =~ /^((--)?version|-v)$/) {
    require App::psst;
    print "psst $App::psst::VERSION\n";
  } else {
    die "Unknown subcommand '$subcmd'\n";
  }
}


sub syntax {
  die <<"MSG";
Syntax: $0 prompt [ "old prompt string" ]

Emits to STDOUT a text munging function and a new definition for \$PS1

The old prompt string is not required if it can be recovered by other
means,
 - from the environment, by export of some other variable made by psst
 - from the configuration file
 - from the environment, by export PS1; trimming off what we added before

These are taken in priority order.

MSG
}


sub prompt_base {
  my ($key, $arg_var) = @_;
  my $out;

  # 4. shell variable, if exported
  $out = prompt_strip($key, $ENV{$key}) if defined $ENV{$key};

  # 3. config file
  $out = config("${key}_old" => undef) if !defined $out;

  # 2. environment we exported.  YAGNI.

  # 1. given parameter
  $out = $arg_var if defined $arg_var;

  die "Cannot recover old prompt string $key, please supply it\n"
    unless defined $out;

  return $out;
}

sub prompt_strip {
  my ($key, $val) = @_;
  if ($val =~ /PERL_LOCAL_LIB_ROOT/) {
    return (); # XXX: removing our extra stuff is not implemented
  } else {
    return $val;
  }
}

sub prompt_set {
  my ($key, $base) = @_;

  my @out;
  push @out, <<'TXT';
_psst_format() {
    local out esc

    # Homedir replacement
    out="${PERL_LOCAL_LIB_ROOT//~/~}"
TXT

  my @substs = @{ config($key.'_substs', []) };
  if (@substs) {
    push @out, qq{\n  # Name substitutions\n};
    foreach my $subst (@substs) {
      my ($match, $replace) = @$subst;
      $match =~ s{([\/#%])}{\\$1}g;
      push @out, qq{    out="\${out//$match/$replace}"\n};
    }
  }

  push @out, <<"TXT";

    esc="\x1b" # One literal ESC (else we must generate or store it)
TXT

  push @out, <<'TXT';

    # Colour & space path :s
    out="${out//:/ $esc[36m:$esc[32m }"
    printf "%s" "$out"
}
# Calling this from PS1 is done by pipes & clone(2) i.e. subshell in
# its own process.  It seems to be the only way to nest substitutions.
# We don't call it unless PERL_LOCAL_LIB_ROOT is set.
TXT

  $base =~ s{'}{'"'"'}g;
  push @out, <<"TXT";

### Prefix the prompt with local::lib indicator
#
# First, reset to default prompt
PS1='$base'
TXT

  push @out, <<'TXT';
#
# Then prefix with indicator pokery
PS1='${PERL_LOCAL_LIB_ROOT:+\[\e7\r\e[3B\e[2K\e[B\e[2Kl:l=\e[32m$( _psst_format )\e8\e[32m\]LL)\[\e[0m\] }'"$PS1"

TXT

# Another option would be a minimal "LL) " indicator, plus a quick "What PERL_LOCAL_LIB_ROOT do I have?" command.  The default for `psst`?
#
#   /etc/profile gives '\u@\h:\w\$ '
#   /etc/skel/.bashrc and /etc/bash.bashrc give assorted debian_chroot indicators
#   /etc/bash_completion.d/git.dpkg-dist has recommendations for Git

  return join '', @out;
}


my %CONFIG;
sub config {
  my $key = shift;
  my $have_default = !!@_;
  my $default = shift;

  if (!keys %CONFIG) {
    my $ok = eval { config_load("$ENV{HOME}/.psst.yaml") };
    if (!$ok) {
      # Load failed.  Make the problem available for later.
      $CONFIG{_load_failed} = $@;
    }
  }

  if (!exists $CONFIG{$key}) {
    return $default if $have_default;
    die "No configuration for '$key'";
  } else {
    return $CONFIG{$key};
  }
}

sub config_load {
  my ($fn) = @_;
  require YAML;
  my @load = YAML::LoadFile($fn);
  die "$fn: Expected one hash item, got ".@load." items"
    unless 1==@load && ref($load[0]) eq 'HASH';
  %CONFIG = (%{ $load[0] }, _src => $fn);
  return 1;
}

main();

# XXX:? local lib names,
# s/dir/name patterns held in bash env
# updating needed to abbreviate the new


=head1 FILES

F<~/.psst.yaml> is the configuration file, taking C<~> from C<%ENV>.

=head1 AUTHOR

Copyright (C) 2011 Genome Research Limited

Author Matthew Astley L<mca@sanger.ac.uk>

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut