diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/App/Raps2.pm | 418 | ||||
| -rw-r--r-- | lib/App/Raps2/Password.pm | 227 | ||||
| -rw-r--r-- | lib/App/Raps2/UI.pm | 208 | 
3 files changed, 436 insertions, 417 deletions
| diff --git a/lib/App/Raps2.pm b/lib/App/Raps2.pm index 0f1fdc4..4a204aa 100644 --- a/lib/App/Raps2.pm +++ b/lib/App/Raps2.pm @@ -1,32 +1,5 @@  package App::Raps2; -=head1 NAME - -App::Raps2 - A Password safe - -=head1 SYNOPSIS - -    use App::Raps2; - -    my $raps2 = App::Raps2->new(); -    my ($action, @args) = @ARGV; - -    $raps2->sanity_check(); -    $raps2->load_config(); - -    given ($action) { -        when ('add')  { $raps2->cmd_add(@args) } -        when ('dump') { $raps2->cmd_dump(@args) } -        when ('get')  { $raps2->cmd_get(@args) } -        when ('info') { $raps2->cmd_info(@args) } -    } - -=head1 DESCRIPTION - -B<App::Raps2> is the backend for B<raps2>, a simple commandline password safe. - -=cut -  use strict;  use warnings;  use 5.010; @@ -40,28 +13,8 @@ use File::Slurp qw(read_dir slurp write_file);  our $VERSION = '0.3'; -=head1 METHODS - -=over - -=item $raps2 = App::Raps2->new(I<%conf>) - -Returns a new B<App::Raps2> object. - -Accepted configuration parameters are: - -=over - -=item B<cost> => I<int> - -B<cost> of key setup, passed on to App::Raps2::Password(3pm). - -=back - -=cut -  sub new { -	my ($obj, %conf) = @_; +	my ( $obj, %conf ) = @_;  	my $ref = {};  	$ref->{xdg_conf} = config_home('raps2'); @@ -71,24 +24,17 @@ sub new {  	$ref->{default} = \%conf; -	return bless($ref, $obj); +	return bless( $ref, $obj );  } -=item $raps2->file_to_hash(I<$file>) - -Reads $file (lines with key/value separated by whitespace) and returns a hash -with its key/value pairs. - -=cut -  sub file_to_hash { -	my ($self, $file) = @_; +	my ( $self, $file ) = @_;  	my %ret; -	for my $line (slurp($file)) { -		my ($key, $value) = split(qr{\s+}, $line); +	for my $line ( slurp($file) ) { +		my ( $key, $value ) = split( qr{ \s+ }x, $line ); -		if (not ($key and $value)) { +		if ( not( $key and $value ) ) {  			next;  		} @@ -97,63 +43,44 @@ sub file_to_hash {  	return %ret;  } -=item $raps2->sanity_check() - -Create working directories (~/.config/raps2 and ~/.local/share/raps2, or the -respective XDG environment variable contents), if they don't exist yet. - -Calls B<create_config> if no raps2 config was found. - -=cut -  sub sanity_check {  	my ($self) = @_; -	make_path($self->{xdg_conf}); -	make_path($self->{xdg_data}); +	make_path( $self->{xdg_conf} ); +	make_path( $self->{xdg_data} ); -	if (not -e $self->{xdg_conf} . '/password') { +	if ( not -e $self->{xdg_conf} . '/password' ) {  		$self->create_config();  	}  	return;  } -=item $raps2->get_master_password() - -Asks the user for the master passphrase. - -=cut -  sub get_master_password {  	my ($self) = @_; -	my $pass = $self->ui->read_pw('Master Password', 0); +	my $pass = $self->ui->read_pw( 'Master Password', 0 );  	$self->{pass} = App::Raps2::Password->new( -		cost => $self->{default}->{cost}, -		salt => $self->{master_salt}, +		cost       => $self->{default}->{cost}, +		salt       => $self->{master_salt},  		passphrase => $pass,  	); -	$self->{pass}->verify($self->{master_hash}); -} - -=item $raps2->create_config() - -Creates a default config and asks the user to set a master password. +	$self->{pass}->verify( $self->{master_hash} ); -=cut +	return; +}  sub create_config {  	my ($self) = @_;  	my $cost = 12; -	my $pass = $self->ui->read_pw('Master Password', 1); +	my $pass = $self->ui->read_pw( 'Master Password', 1 );  	$self->{pass} = App::Raps2::Password->new( -		cost => $cost, +		cost       => $cost,  		passphrase => $pass,  	); -	my $hash = $self->pw->crypt(); +	my $hash = $self->pw->bcrypt();  	my $salt = $self->pw->salt();  	write_file( @@ -162,61 +89,47 @@ sub create_config {  		"salt ${salt}\n",  		"hash ${hash}\n",  	); -} - -=item $raps2->load_config() -Load config - -=cut +	return; +}  sub load_config {  	my ($self) = @_; -	my %cfg = $self->file_to_hash($self->{xdg_conf} . '/password'); +	my %cfg = $self->file_to_hash( $self->{xdg_conf} . '/password' );  	$self->{master_hash} = $cfg{hash};  	$self->{master_salt} = $cfg{salt};  	$self->{default}->{cost} //= $cfg{cost}; -} - -=item $raps2->pw() - -Returns the App::Raps2::Password(3pm) object. -=cut +	return; +}  sub pw {  	my ($self) = @_; -	if (defined $self->{pass}) { +	if ( defined $self->{pass} ) {  		return $self->{pass};  	}  	else { -		confess('No App::Raps2::Password object, did you call get_master_password?'); +		confess( +			'No App::Raps2::Password object, did you call get_master_password?' +		);  	} -} - -=item $raps2->ui() - -Returns the App::Raps2::UI(3pm) object. -=cut +	return; +}  sub ui {  	my ($self) = @_; +  	return $self->{ui};  } -=item $raps2->cmd_add(I<$name>) - -Adds a new password file called $name. - -=cut -  sub cmd_add { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}"; -	if (-e $pwfile) { +	if ( -e $pwfile ) {  		confess('Password file already exists');  	} @@ -225,39 +138,34 @@ sub cmd_add {  	my $salt  = $self->pw->create_salt();  	my $url   = $self->ui->read_line('URL');  	my $login = $self->ui->read_line('Login'); -	my $pass  = $self->ui->read_pw('Password', 1); +	my $pass  = $self->ui->read_pw( 'Password', 1 );  	my $extra = $self->ui->read_multiline('Additional content');  	$self->pw->salt($salt); -	my $pass_hash = $self->pw->encrypt($pass); +	my $pass_hash  = $self->pw->encrypt($pass);  	my $extra_hash = ( -		$extra ? -		$self->pw->encrypt($extra) : -		q{} +		  $extra +		? $self->pw->encrypt($extra) +		: q{}  	); -  	write_file( -		$pwfile, -		"url ${url}\n", +		$pwfile, "url ${url}\n",  		"login ${login}\n",  		"salt ${salt}\n",  		"hash ${pass_hash}\n",  		"extra ${extra_hash}\n",  	); -} - -=item $raps2->cmd_dump(I<$account>) -Dumps the content of I<account> - -=cut +	return; +}  sub cmd_dump { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}"; -	if (not -e $pwfile) { +	if ( not -e $pwfile ) {  		confess('Password file does not exist');  	} @@ -265,45 +173,42 @@ sub cmd_dump {  	$self->get_master_password(); -	$self->pw->salt($key{salt}); +	$self->pw->salt( $key{salt} );  	$self->ui()->output( -		['URL', $key{url}], -		['Login', $key{login}], -		['Password', $self->pw->decrypt($key{hash})], +		[ 'URL',      $key{url} ], +		[ 'Login',    $key{login} ], +		[ 'Password', $self->pw->decrypt( $key{hash} ) ],  	); -	if ($key{extra}) { -		print $self->pw->decrypt($key{extra}); +	if ( $key{extra} ) { +		print $self->pw->decrypt( $key{extra} );  	} -} - -=item $raps2->cmd_edit(I<$acount>) -Edit I<account>. - -=cut +	return; +}  sub cmd_edit { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}";  	my $pass_hash; -	if (not -e $pwfile) { +	if ( not -e $pwfile ) {  		confess('Password file does not exist');  	}  	my %key = $self->file_to_hash($pwfile);  	$self->get_master_password(); -	$self->pw->salt($key{salt}); +	$self->pw->salt( $key{salt} ); -	my $salt = $key{salt}; -	my $url   = $self->ui->read_line('URL', $key{url}); -	my $login = $self->ui->read_line('Login', $key{login}); -	my $pass  = $self->ui->read_pw('New password (empty to keep old)', 1); +	my $salt  = $key{salt}; +	my $url   = $self->ui->read_line( 'URL', $key{url} ); +	my $login = $self->ui->read_line( 'Login', $key{login} ); +	my $pass  = $self->ui->read_pw( 'New password (empty to keep old)', 1 );  	my $extra = $key{extra} // q{}; -	if (length($pass)) { +	if ( length($pass) ) {  		$pass_hash = $self->pw->encrypt($pass);  	}  	else { @@ -311,26 +216,22 @@ sub cmd_edit {  	}  	write_file( -		$pwfile, -		"url ${url}\n", +		$pwfile, "url ${url}\n",  		"login ${login}\n",  		"salt ${salt}\n",  		"hash ${pass_hash}\n",  		"extra ${extra}\n",  	); -} - -=item $raps2->cmd_get(I<$name>) -Puts the password saved in $name into the X clipboard. - -=cut +	return; +}  sub cmd_get { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}"; -	if (not -e $pwfile) { +	if ( not -e $pwfile ) {  		confess('Password file does not exist');  	} @@ -338,79 +239,190 @@ sub cmd_get {  	$self->get_master_password(); -	$self->pw->salt($key{salt}); +	$self->pw->salt( $key{salt} ); -	$self->ui()->to_clipboard($self->pw->decrypt($key{hash})); +	$self->ui()->to_clipboard( $self->pw->decrypt( $key{hash} ) ); -	if ($key{extra}) { -		print $self->pw->decrypt($key{extra}) +	if ( $key{extra} ) { +		print $self->pw->decrypt( $key{extra} );  	} -} - -=item $raps2->cmd_info(I<$name>) -Prints unencrypted information about $name. - -=cut +	return; +}  sub cmd_info { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}"; -	if (not -e $pwfile) { +	if ( not -e $pwfile ) {  		confess('Password file does not exist');  	}  	my %key = $self->file_to_hash($pwfile); -	$self->ui()->output( -		['URL', $key{url}], -		['Login', $key{login}], -	); -} +	$self->ui()->output( [ 'URL', $key{url} ], [ 'Login', $key{login} ], ); -=item $raps2->cmd_list() - -Lists all saved passwords and their logins and urls - -=cut +	return; +}  sub cmd_list {  	my ($self) = @_; -	my @files = read_dir($self->{xdg_data}); -	for my $file (sort @files) { -		my %key = $self->file_to_hash($self->{xdg_data} . "/${file}"); +	my @files = read_dir( $self->{xdg_data} ); + +	for my $file ( sort @files ) { +		my %key = $self->file_to_hash( $self->{xdg_data} . "/${file}" );  		$self->ui->list( -			['Account', $file], -			['Login', $key{login}], -			['URL', $key{url}], +			[ 'Account', $file ], +			[ 'Login',   $key{login} ], +			[ 'URL',     $key{url} ],  		);  	} -} - -=item $raps2->cmd_remove(I<$name>) - -Remove (unlink) the account I<name>. -=cut +	return; +}  sub cmd_remove { -	my ($self, $name) = @_; +	my ( $self, $name ) = @_; +  	my $pwfile = $self->{xdg_data} . "/${name}"; -	if (-e $pwfile) { +	if ( -e $pwfile ) {  		unlink($pwfile);  	}  	else {  		say STDERR 'File did not exist, so could not be removed';  	} + +	return;  } +1; + +__END__ + +=head1 NAME + +App::Raps2 - A Password safe + +=head1 SYNOPSIS + +    use App::Raps2; + +    my $raps2 = App::Raps2->new(); +    my ($action, @args) = @ARGV; + +    $raps2->sanity_check(); +    $raps2->load_config(); + +    given ($action) { +        when ('add')  { $raps2->cmd_add(@args) } +        when ('dump') { $raps2->cmd_dump(@args) } +        when ('get')  { $raps2->cmd_get(@args) } +        when ('info') { $raps2->cmd_info(@args) } +    } + +=head1 DESCRIPTION + +B<App::Raps2> is the backend for B<raps2>, a simple commandline password safe. + +=head1 VERSION + +This manual documents App::Raps2 version 0.3 + +=head1 METHODS + +=over + +=item $raps2 = App::Raps2->new(I<%conf>) + +Returns a new B<App::Raps2> object. + +Accepted configuration parameters are: + +=over + +=item B<cost> => I<int> + +B<cost> of key setup, passed on to App::Raps2::Password(3pm). +  =back +=item $raps2->file_to_hash(I<$file>) + +Reads $file (lines with key/value separated by whitespace) and returns a hash +with its key/value pairs. + +=item $raps2->sanity_check() + +Create working directories (~/.config/raps2 and ~/.local/share/raps2, or the +respective XDG environment variable contents), if they don't exist yet. + +Calls B<create_config> if no raps2 config was found. + +=item $raps2->get_master_password() + +Asks the user for the master passphrase. + +=item $raps2->create_config() + +Creates a default config and asks the user to set a master password. + +=item $raps2->load_config() + +Load config + +=item $raps2->pw() + +Returns the App::Raps2::Password(3pm) object. + +=item $raps2->ui() + +Returns the App::Raps2::UI(3pm) object. + +=item $raps2->cmd_add(I<$name>) + +Adds a new password file called $name. + +=item $raps2->cmd_dump(I<$account>) + +Dumps the content of I<account> + +=item $raps2->cmd_edit(I<$acount>) + +Edit I<account>. + +=item $raps2->cmd_get(I<$name>) + +Puts the password saved in $name into the X clipboard. + +=item $raps2->cmd_info(I<$name>) + +Prints unencrypted information about $name. + +=item $raps2->cmd_list() + +Lists all saved passwords and their logins and urls + +=item $raps2->cmd_remove(I<$name>) + +Remove (unlink) the account I<name>. + +=back + +=head1 DIAGNOSTICS + +If anything goes wrong, B<App::Raps2> will die with a backtrace (using +B<confess> from Carp(3pm)). +  =head1 DEPENDENCIES -L<App::Raps2::Password>, L<App::Raps2::UI>, L<File::BaseDir>, L<File::Slurp>. +App::Raps2::Password(3pm), App::Raps2::UI(3pm), File::BaseDir(3pm), +File::Slurp(3pm). + +=head1 BUGS AND LIMITATIONS + +Unknown.  =head1 AUTHOR diff --git a/lib/App/Raps2/Password.pm b/lib/App/Raps2/Password.pm index 1e33f58..1a3ab63 100644 --- a/lib/App/Raps2/Password.pm +++ b/lib/App/Raps2/Password.pm @@ -11,6 +11,103 @@ use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64 de_base64);  our $VERSION = '0.3'; +sub new { +	my ( $obj, %conf ) = @_; + +	$conf{cost} //= 12; + +	if ( not defined $conf{salt} ) { +		$conf{salt} = create_salt(); +	} + +	if ( length( $conf{salt} ) != 16 ) { +		confess('incorrect salt length'); +	} + +	if ( not( defined $conf{passphrase} and length $conf{passphrase} ) ) { +		confess('no passphrase given'); +	} + +	my $ref = \%conf; + +	return bless( $ref, $obj ); +} + +sub create_salt { +	my ($self) = @_; +	my $salt = q{}; + +	for ( 1 .. 16 ) { +		$salt .= chr( 0x21 + int( rand(90) ) ); +	} + +	return $salt; +} + +sub salt { +	my ( $self, $salt ) = @_; + +	if ( defined $salt ) { +		if ( length($salt) != 16 ) { +			confess('incorrect salt length'); +		} + +		$self->{salt} = $salt; +	} + +	return $self->{salt}; +} + +sub encrypt { +	my ( $self, $in ) = @_; + +	my $eksblowfish = Crypt::Eksblowfish->new( $self->{cost}, $self->{salt}, +		$self->{passphrase}, ); +	my $cbc = Crypt::CBC->new( -cipher => $eksblowfish ); + +	return $cbc->encrypt_hex($in); +} + +sub decrypt { +	my ( $self, $in ) = @_; + +	my $eksblowfish = Crypt::Eksblowfish->new( $self->{cost}, $self->{salt}, +		$self->{passphrase}, ); +	my $cbc = Crypt::CBC->new( -cipher => $eksblowfish ); + +	return $cbc->decrypt_hex($in); +} + +sub bcrypt { +	my ($self) = @_; + +	return en_base64( +		bcrypt_hash( +			{ +				key_nul => 1, +				cost    => $self->{cost}, +				salt    => $self->{salt}, +			}, +			$self->{passphrase}, +		) +	); +} + +sub verify { +	my ( $self, $testhash ) = @_; + +	my $myhash = $self->bcrypt(); + +	if ( $testhash eq $myhash ) { +		return 1; +	} +	confess('Passwords did not match'); +} + +1; + +__END__ +  =head1 NAME  App::Raps2::Password - Password class for App::Raps2 @@ -23,7 +120,7 @@ App::Raps2::Password - Password class for App::Raps2          passphrase => 'secret',      ); -    my $oneway_hash = $raps2->crypt(); +    my $oneway_hash = $raps2->bcrypt();      $raps2->verify($oneway_hash);      my $twoway_hash = $raps2->encrypt('data'); @@ -34,6 +131,10 @@ App::Raps2::Password - Password class for App::Raps2  This manual documents B<App::Raps2::Password> version 0.3 +=head1 DESCRIPTION + +App::Raps2::Pasword is a wrapper around Crypt::Eksblowfish. +  =head1 METHODS  =over @@ -60,153 +161,47 @@ generates its own.  =back -=cut - -sub new { -	my ($obj, %conf) = @_; - -	$conf{cost} //= 12; - -	if (not defined $conf{salt}) { -		$conf{salt} = create_salt(); -	} - -	if (length($conf{salt}) != 16) { -		confess('incorrect salt length'); -	} - -	if (not (defined $conf{passphrase} and length $conf{passphrase})) { -		confess('no passphrase given'); -	} - -	my $ref = \%conf; - -	return bless($ref, $obj); -} -  =item $pass->create_salt()  Returns a new 16-byte salt. Contains only printable characters. -=cut - -sub create_salt { -	my ($self) = @_; -	my $salt = q{}; - -	for (1 .. 16) { -		$salt .= chr(0x21 + int(rand(90))); -	} - -	return $salt; -} -  =item $pass->salt([I<salt>])  Returns the currently used salt and optionally changes it to I<salt>. -=cut - -sub salt { -	my ($self, $salt) = @_; - -	if (defined $salt) { -		if (length($salt) != 16) { -			confess('incorrect salt length'); -		} - -		$self->{salt} = $salt; -	} - -	return $self->{salt}; -} -  =item $pass->encrypt(I<data>)  Encrypts I<data> with the passphrase saved in the object, returns the  corresponding hexadecimal hash (as string). -=cut - -sub encrypt { -	my ($self, $in) = @_; - -	my $eksblowfish = Crypt::Eksblowfish->new( -		$self->{cost}, -		$self->{salt}, -		$self->{passphrase}, -	); -	my $cbc = Crypt::CBC->new(-cipher => $eksblowfish); - -	return $cbc->encrypt_hex($in); -} -  =item $pass->decrypt(I<hexstr>)  Decrypts I<hexstr> (as created by B<encrypt>), returns its original content. -=cut - -sub decrypt { -	my ($self, $in) = @_; - -	my $eksblowfish = Crypt::Eksblowfish->new( -		$self->{cost}, -		$self->{salt}, -		$self->{passphrase}, -	); -	my $cbc = Crypt::CBC->new(-cipher => $eksblowfish); - -	return $cbc->decrypt_hex($in); -} - -=item $pass->crypt() +=item $pass->bcrypt()  Return a base64 bcrypt hash of the password, salted with the salt. -=cut - -sub crypt { -	my ($self) = @_; - -	return en_base64( -		bcrypt_hash({ -				key_nul => 1, -				cost => $self->{cost}, -				salt => $self->{salt}, -			}, -			$self->{passphrase}, -	)); -} -  =item $pass->verify(I<hash>)  Verify a hash as returned by B<crypt>.  Returns true if it matches, dies if it doesn't. -=cut - -sub verify { -	my ($self, $testhash) = @_; - -	my $myhash = $self->crypt(); +=back -	if ($testhash eq $myhash) { -		return 1; -	} -	confess('Passwords did not match'); -} +=head1 DIAGNOSTICS -1; +When anything goes wrong, App::Raps2::Password will use Carp(3pm)'s B<confess> +method to die with a backtrace. -__END__ +=head1 DEPENDENCIES -=back +Crypt::CBC(3pm), Crypt::Eksblowfish(3pm). -=head1 DEPENDENCIES +=head1 BUGS AND LIMITATIONS -B<Crypt::CBC>, B<Crypt::Eksblowfish>. +Unknown.  =head1 SEE ALSO diff --git a/lib/App/Raps2/UI.pm b/lib/App/Raps2/UI.pm index 4d7f3d0..b588222 100644 --- a/lib/App/Raps2/UI.pm +++ b/lib/App/Raps2/UI.pm @@ -10,113 +10,61 @@ use Term::ReadLine;  our $VERSION = '0.3'; -=head1 NAME - -App::Raps2::UI - App::Raps2 User Interface - -=head1 SYNOPSIS - -    my $ui = App::Raps2::UI->new(); - -    my $input = $ui->read_line('Say something'); - -    my $password = $ui->read_pw('New password', 1); - -    $ui->to_clipboard('stuff!'); - -=head1 VERSION - -This manual documents B<App::Raps2::UI> version 0.3 - -=head1 METHODS - -=over - -=item $ui = App::Raps2::UI->new() - -Returns a new App::Raps2::UI object. - -=cut -  sub new {  	my ($obj) = @_; -	my $ref = {}; -	$ref->{term_readline} = Term::ReadLine->new('App::Raps2'); -	return bless($ref, $obj); -} -=item $ui->list(I<\@item1>, I<\@item2>, I<\@item3>) +	my $ref = {}; -Print the list items neatly formatted to stdout. Each I<item> looks like B<[> -I<key>, I<value> B<]>. When B<list> is called for the first time, it will -print the keys as well as the values. +	$ref->{term_readline} = Term::ReadLine->new('App::Raps2'); -=cut +	return bless( $ref, $obj ); +}  sub list { -	my ($self, @list) = @_; +	my ( $self, @list ) = @_; +  	my $format = "%-20s %-20s %s\n"; -	if (not $self->{list}->{header}) { -		printf($format, map { $_->[0] } @list); +	if ( not $self->{list}->{header} ) { +		printf( $format, map { $_->[0] } @list );  		$self->{list}->{header} = 1;  	} -	printf($format, map { $_->[1] // q{} } @list); -} - -=item $ui->read_line(I<$question>, [I<$prefill>]) - -Print "I<question>: " to stdout and wait for the user to input text followed -by a newline.  I<prefill> sets the default content of the answer field. - -Returns the user's reply, excluding the newline. +	printf( $format, map { $_->[1] // q{} } @list ); -=cut +	return; +}  sub read_line { -	my ($self, $str, $pre) = @_; +	my ( $self, $str, $pre ) = @_; -	my $input = $self->{term_readline}->readline("${str}: ", $pre); +	my $input = $self->{term_readline}->readline( "${str}: ", $pre );  	return $input;  } -=item $ui->read_multiline(I<$message>) - -Like B<read_line>, but repeats I<message> each time the user hits return. -Input is terminated by EOF (Ctrl+D).  Returns a string concatenation of all -lines (including newlines). - -=cut -  sub read_multiline { -	my ($self, $str) = @_; +	my ( $self, $str ) = @_; +  	my $in;  	say "${str} (^D to quit)"; -	while (my $line = $self->read_line('multiline')) { +	while ( my $line = $self->read_line('multiline') ) {  		$in .= "${line}\n";  	} +  	return $in;  } -=item $ui->read_pw(I<$message>, I<$verify>) - -Prompt the user for a password. I<message> is displayed, the user's input is -noch echoed.  If I<verify> is set, the user has to enter the same input twice, -otherwise B<read_pw> dies.  Returns the input. - -=cut -  sub read_pw { -	my ($self, $str, $verify) = @_; -	my ($in1, $in2); +	my ( $self, $str, $verify ) = @_; +	my ( $in1, $in2 );  	my $term = POSIX::Termios->new(); +  	$term->getattr(0); -	$term->setlflag($term->getlflag() & ~POSIX::ECHO); -	$term->setattr(0, POSIX::TCSANOW); +	$term->setlflag( $term->getlflag() & ~POSIX::ECHO ); +	$term->setattr( 0, POSIX::TCSANOW );  	print "${str}: ";  	$in1 = readline(STDIN); @@ -128,49 +76,39 @@ sub read_pw {  		print "\n";  	} -	$term->setlflag($term->getlflag() | POSIX::ECHO); -	$term->setattr(0, POSIX::TCSANOW); +	$term->setlflag( $term->getlflag() | POSIX::ECHO ); +	$term->setattr( 0, POSIX::TCSANOW ); -	if ($verify and $in1 ne $in2) { +	if ( $verify and $in1 ne $in2 ) {  		confess('Input lines did not match');  	}  	chomp $in1; +  	return $in1;  } -=item $ui->to_clipboard(I<$string>) +sub to_clipboard { +	my ( $self, $str ) = @_; -Place I<string> in the primary X Clipboard. +	open( my $clipboard, q{|-}, 'xclip -l 1' ) +		or confess("Failed to execute xclip -l 1: $!"); -=cut +	print $clipboard $str; -sub to_clipboard { -	my ($self, $str) = @_; +	close($clipboard) +		or confess("Failed to close pipe to xclip: $!"); -	open(my $clipboard, '|-', 'xclip -l 1'); -	print $clipboard $str; -	close($clipboard);  	return;  } -=item $ui->output(I<\@pair>, I<...>) - -I<pair> consinsts of B<[> I<key>, I<value> B<]>. For each I<pair>, prints -"     key : value" to stdout. - -=cut -  sub output { -	my ($self, @out) = @_; +	my ( $self, @out ) = @_;  	for my $pair (@out) { -		printf( -			"%-8s : %s\n", -			$pair->[0], -			$pair->[1] // q{}, -		); +		printf( "%-8s : %s\n", $pair->[0], $pair->[1] // q{}, );  	} +  	return;  } @@ -178,12 +116,86 @@ sub output {  __END__ +=head1 NAME + +App::Raps2::UI - App::Raps2 User Interface + +=head1 SYNOPSIS + +    my $ui = App::Raps2::UI->new(); + +    my $input = $ui->read_line('Say something'); + +    my $password = $ui->read_pw('New password', 1); + +    $ui->to_clipboard('stuff!'); + +=head1 VERSION + +This manual documents B<App::Raps2::UI> version 0.3 + +=head1 DESCRIPTION + +App::Raps2::UI is used by App::Raps2 to interface with the user, i.e. do input +and output on the terminal. + +=head1 METHODS + +=over + +=item $ui = App::Raps2::UI->new() + +Returns a new App::Raps2::UI object. + +=item $ui->list(I<\@item1>, I<\@item2>, I<\@item3>) + +Print the list items neatly formatted to stdout. Each I<item> looks like B<[> +I<key>, I<value> B<]>. When B<list> is called for the first time, it will +print the keys as well as the values. + +=item $ui->read_line(I<$question>, [I<$prefill>]) + +Print "I<question>: " to stdout and wait for the user to input text followed +by a newline.  I<prefill> sets the default content of the answer field. + +Returns the user's reply, excluding the newline. + +=item $ui->read_multiline(I<$message>) + +Like B<read_line>, but repeats I<message> each time the user hits return. +Input is terminated by EOF (Ctrl+D).  Returns a string concatenation of all +lines (including newlines). + +=item $ui->read_pw(I<$message>, I<$verify>) + +Prompt the user for a password. I<message> is displayed, the user's input is +noch echoed.  If I<verify> is set, the user has to enter the same input twice, +otherwise B<read_pw> dies.  Returns the input. + +=item $ui->to_clipboard(I<$string>) + +Place I<string> in the primary X Clipboard. + +=item $ui->output(I<\@pair>, I<...>) + +I<pair> consinsts of B<[> I<key>, I<value> B<]>. For each I<pair>, prints +"     key : value" to stdout. +  =back +=head1 DIAGNOSTICS + +When App::Raps2::UI encounters an error, it uses Carp(3pm)'s B<confess> +function to die with a backtrace. +  =head1 DEPENDENCIES  This module requires B<Term::ReadLine> and the B<xclip> executable. +=head1 BUGS AND LIMITATIONS + +Unknown. +  =head1 SEE ALSO  App::Raps2(3pm). | 
