2016-10-01 15:35:06 +00:00
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
2016-01-20 19:46:42 +00:00
use std ::path ::PathBuf ;
2016-03-21 09:19:03 +00:00
use std ::process ::Command ;
use std ::env ;
2016-03-24 11:09:45 +00:00
use std ::io ::stderr ;
use std ::io ::Write ;
2016-01-20 19:46:42 +00:00
pub use clap ::App ;
use clap ::{ Arg , ArgMatches } ;
use log ;
use log ::LogLevelFilter ;
use configuration ::Configuration ;
2016-01-21 17:43:03 +00:00
use error ::RuntimeError ;
use error ::RuntimeErrorKind ;
2016-06-27 16:23:54 +00:00
use error ::MapErrInto ;
2016-01-20 19:46:42 +00:00
use logger ::ImagLogger ;
use libimagstore ::store ::Store ;
2016-04-18 12:02:57 +00:00
#[ derive(Debug) ]
2016-01-20 19:46:42 +00:00
pub struct Runtime < ' a > {
rtp : PathBuf ,
2016-03-05 12:01:09 +00:00
configuration : Option < Configuration > ,
2016-02-20 20:15:09 +00:00
cli_matches : ArgMatches < ' a > ,
2016-01-20 19:46:42 +00:00
store : Store ,
}
impl < ' a > Runtime < ' a > {
/**
* Gets the CLI spec for the program and retreives the config file path ( or uses the default on
* in $HOME / . imag / config , $XDG_CONFIG_DIR / imag / config or from env ( " $IMAG_CONFIG " )
* and builds the Runtime object with it .
*
* The cli_spec object should be initially build with the ::get_default_cli_builder ( ) function .
*
* /
2016-10-31 21:31:03 +00:00
pub fn new ( mut cli_spec : App < ' a , ' a > ) -> Result < Runtime < ' a > , RuntimeError > {
2016-01-29 19:36:37 +00:00
use std ::env ;
2016-10-28 07:42:44 +00:00
use std ::io ::stdout ;
use clap ::Shell ;
2016-01-29 19:36:37 +00:00
2016-07-15 21:36:21 +00:00
use libimagstore ::hook ::position ::HookPosition as HP ;
use libimagstore ::hook ::Hook ;
2016-04-05 14:47:51 +00:00
use libimagstore ::error ::StoreErrorKind ;
2016-03-24 11:09:45 +00:00
use libimagstorestdhook ::debug ::DebugHook ;
2016-07-15 21:24:28 +00:00
use libimagstorestdhook ::vcs ::git ::delete ::DeleteHook as GitDeleteHook ;
use libimagstorestdhook ::vcs ::git ::update ::UpdateHook as GitUpdateHook ;
2016-09-20 13:48:23 +00:00
use libimagstorestdhook ::vcs ::git ::store_unload ::StoreUnloadHook as GitStoreUnloadHook ;
2016-05-16 16:59:02 +00:00
use libimagerror ::trace ::trace_error ;
use libimagerror ::trace ::trace_error_dbg ;
2016-05-27 08:27:06 +00:00
use libimagerror ::into ::IntoError ;
2016-03-24 11:09:45 +00:00
2016-03-05 12:01:09 +00:00
use configuration ::error ::ConfigErrorKind ;
2016-10-31 21:31:03 +00:00
let matches = cli_spec . clone ( ) . get_matches ( ) ;
2016-04-05 15:46:51 +00:00
2016-03-25 18:42:10 +00:00
let is_debugging = matches . is_present ( " debugging " ) ;
2016-04-05 15:46:51 +00:00
let is_verbose = matches . is_present ( " verbosity " ) ;
2016-08-02 09:39:01 +00:00
let colored = ! matches . is_present ( " no-color-output " ) ;
2016-04-05 15:46:51 +00:00
2016-08-02 09:39:01 +00:00
Runtime ::init_logger ( is_debugging , is_verbose , colored ) ;
2016-03-25 18:42:10 +00:00
2016-10-28 07:42:44 +00:00
match matches . value_of ( Runtime ::arg_generate_compl ( ) ) {
Some ( shell ) = > {
debug! ( " Generating shell completion script, writing to stdout " ) ;
2016-11-02 12:22:58 +00:00
let shell = shell . parse ::< Shell > ( ) . unwrap ( ) ; // clap has our back here.
let appname = String ::from ( cli_spec . get_name ( ) ) ;
cli_spec . gen_completions_to ( appname , shell , & mut stdout ( ) ) ;
2016-10-28 07:42:44 +00:00
} ,
_ = > debug! ( " Not generating shell completion script " ) ,
}
2016-01-29 19:36:37 +00:00
let rtp : PathBuf = matches . value_of ( " runtimepath " )
2016-05-03 21:10:32 +00:00
. map_or_else ( | | {
2016-01-29 19:36:37 +00:00
env ::var ( " HOME " )
. map ( PathBuf ::from )
. map ( | mut p | { p . push ( " .imag " ) ; p } )
. unwrap_or_else ( | _ | {
panic! ( " You seem to be $HOME-less. Please get a $HOME before using this software. We are sorry for you and hope you have some accommodation anyways. " ) ;
} )
2016-05-03 21:10:32 +00:00
} , PathBuf ::from ) ;
2016-01-20 19:46:42 +00:00
let storepath = matches . value_of ( " storepath " )
2016-05-03 21:10:32 +00:00
. map_or_else ( | | {
2016-01-20 19:46:42 +00:00
let mut spath = rtp . clone ( ) ;
2016-01-28 19:05:43 +00:00
spath . push ( " store " ) ;
2016-01-20 19:46:42 +00:00
spath
2016-05-03 21:10:32 +00:00
} , PathBuf ::from ) ;
2016-03-05 11:36:11 +00:00
2016-05-28 21:04:10 +00:00
let configpath = matches . value_of ( " config " )
2016-07-27 17:15:50 +00:00
. map_or_else ( | | rtp . clone ( ) , PathBuf ::from ) ;
2016-05-28 21:04:10 +00:00
let cfg = match Configuration ::new ( & configpath ) {
2016-05-14 17:47:34 +00:00
Err ( e ) = > if e . err_type ( ) ! = ConfigErrorKind ::NoConfigFileFound {
2016-05-27 08:27:06 +00:00
return Err ( RuntimeErrorKind ::Instantiate . into_error_with_cause ( Box ::new ( e ) ) ) ;
2016-03-05 12:01:09 +00:00
} else {
2016-04-22 13:06:44 +00:00
warn! ( " No config file found. " ) ;
warn! ( " Continuing without configuration file " ) ;
2016-03-05 12:01:09 +00:00
None
2016-05-14 17:47:34 +00:00
} ,
2016-07-24 16:06:01 +00:00
Ok ( mut cfg ) = > {
if let Err ( e ) = cfg . override_config ( get_override_specs ( & matches ) ) {
error! ( " Could not apply config overrides " ) ;
trace_error ( & e ) ;
// TODO: continue question (interactive)
}
Some ( cfg )
}
2016-03-05 12:01:09 +00:00
} ;
2016-03-05 11:36:11 +00:00
2016-05-14 17:47:34 +00:00
let store_config = match cfg {
Some ( ref c ) = > c . store_config ( ) . cloned ( ) ,
None = > None ,
2016-03-05 17:42:59 +00:00
} ;
2016-03-24 11:09:45 +00:00
if is_debugging {
2016-04-17 18:48:17 +00:00
write! ( stderr ( ) , " Config: {:?} \n " , cfg ) . ok ( ) ;
write! ( stderr ( ) , " Store-config: {:?} \n " , store_config ) . ok ( ) ;
2016-03-24 11:09:45 +00:00
}
2016-07-16 13:59:08 +00:00
Store ::new ( storepath . clone ( ) , store_config ) . map ( | mut store | {
2016-03-24 11:09:45 +00:00
// If we are debugging, generate hooks for all positions
if is_debugging {
2016-07-15 21:36:21 +00:00
let hooks : Vec < ( Box < Hook > , & str , HP ) > = vec! [
( Box ::new ( DebugHook ::new ( HP ::PreCreate ) ) , " debug " , HP ::PreCreate ) ,
( Box ::new ( DebugHook ::new ( HP ::PostCreate ) ) , " debug " , HP ::PostCreate ) ,
( Box ::new ( DebugHook ::new ( HP ::PreRetrieve ) ) , " debug " , HP ::PreRetrieve ) ,
( Box ::new ( DebugHook ::new ( HP ::PostRetrieve ) ) , " debug " , HP ::PostRetrieve ) ,
( Box ::new ( DebugHook ::new ( HP ::PreUpdate ) ) , " debug " , HP ::PreUpdate ) ,
( Box ::new ( DebugHook ::new ( HP ::PostUpdate ) ) , " debug " , HP ::PostUpdate ) ,
( Box ::new ( DebugHook ::new ( HP ::PreDelete ) ) , " debug " , HP ::PreDelete ) ,
( Box ::new ( DebugHook ::new ( HP ::PostDelete ) ) , " debug " , HP ::PostDelete ) ,
2016-03-24 11:09:45 +00:00
] ;
2016-07-15 21:36:21 +00:00
// If hook registration fails, trace the error and warn, but continue.
for ( hook , aspectname , position ) in hooks {
if let Err ( e ) = store . register_hook ( position , & String ::from ( aspectname ) , hook ) {
2016-04-05 14:47:51 +00:00
if e . err_type ( ) = = StoreErrorKind ::HookRegisterError {
trace_error_dbg ( & e ) ;
warn! ( " Registering debug hook with store failed " ) ;
} else {
trace_error ( & e ) ;
} ;
2016-03-24 11:09:45 +00:00
}
}
}
2016-07-29 08:01:02 +00:00
let sp = storepath ;
let hooks : Vec < ( Box < Hook > , & str , HP ) > = vec! [
2016-09-20 13:48:23 +00:00
( Box ::new ( GitDeleteHook ::new ( sp . clone ( ) , HP ::PostDelete ) ) , " vcs " , HP ::PostDelete ) ,
( Box ::new ( GitUpdateHook ::new ( sp . clone ( ) , HP ::PostUpdate ) ) , " vcs " , HP ::PostUpdate ) ,
( Box ::new ( GitStoreUnloadHook ::new ( sp ) ) , " vcs " , HP ::StoreUnload ) ,
2016-07-29 08:01:02 +00:00
] ;
for ( hook , aspectname , position ) in hooks {
if let Err ( e ) = store . register_hook ( position , & String ::from ( aspectname ) , hook ) {
if e . err_type ( ) = = StoreErrorKind ::HookRegisterError {
trace_error_dbg ( & e ) ;
2016-09-18 12:44:08 +00:00
warn! ( " Registering git hook with store failed " ) ;
2016-07-29 08:01:02 +00:00
} else {
trace_error ( & e ) ;
} ;
}
}
2016-01-21 17:43:03 +00:00
Runtime {
cli_matches : matches ,
2016-03-05 11:36:11 +00:00
configuration : cfg ,
2016-01-21 17:43:03 +00:00
rtp : rtp ,
store : store ,
}
} )
2016-06-27 16:23:54 +00:00
. map_err_into ( RuntimeErrorKind ::Instantiate )
2016-01-20 19:46:42 +00:00
}
/**
2016-01-21 20:24:20 +00:00
* Get a commandline - interface builder object from ` clap `
*
* This commandline interface builder object already contains some predefined interface flags :
* * - v | - - verbose for verbosity
* * - - debug for debugging
* * - c < file > | - - config < file > for alternative configuration file
* * - r < path > | - - rtp < path > for alternative runtimepath
* * - - store < path > for alternative store path
* Each has the appropriate help text included .
*
* The ` appname ` shall be " imag-<command> " .
2016-01-20 19:46:42 +00:00
* /
pub fn get_default_cli_builder ( appname : & ' a str ,
version : & ' a str ,
about : & ' a str )
2016-02-20 20:15:09 +00:00
-> App < ' a , ' a >
2016-01-20 19:46:42 +00:00
{
App ::new ( appname )
. version ( version )
. author ( " Matthias Beyer <mail@beyermatthias.de> " )
. about ( about )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_verbosity_name ( ) )
2016-01-20 19:46:42 +00:00
. short ( " v " )
. long ( " verbose " )
. help ( " Enables verbosity " )
. required ( false )
. takes_value ( false ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_debugging_name ( ) )
2016-01-20 19:46:42 +00:00
. long ( " debug " )
. help ( " Enables debugging output " )
. required ( false )
. takes_value ( false ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_no_color_output_name ( ) )
2016-08-02 09:39:01 +00:00
. long ( " no-color " )
. help ( " Disable color output " )
. required ( false )
. takes_value ( false ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_config_name ( ) )
2016-01-20 19:46:42 +00:00
. long ( " config " )
. help ( " Path to alternative config file " )
. required ( false )
. takes_value ( true ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_config_override_name ( ) )
2016-07-24 16:05:29 +00:00
. long ( " override-config " )
. help ( " Override a configuration settings. Use 'key=value' pairs, where the key is a path in the TOML configuration. The value must be present in the configuration and be convertible to the type of the configuration setting. If the argument does not contain a '=', it gets ignored. Setting Arrays and Tables is not yet supported. " )
. required ( false )
. takes_value ( true ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_runtimepath_name ( ) )
2016-01-20 19:46:42 +00:00
. long ( " rtp " )
. help ( " Alternative runtimepath " )
. required ( false )
. takes_value ( true ) )
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_storepath_name ( ) )
2016-01-20 19:46:42 +00:00
. long ( " store " )
. help ( " Alternative storepath. Must be specified as full path, can be outside of the RTP " )
. required ( false )
. takes_value ( true ) )
2016-03-21 09:19:03 +00:00
2016-08-06 15:43:14 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_editor_name ( ) )
2016-03-21 09:19:03 +00:00
. long ( " editor " )
. help ( " Set editor " )
. required ( false )
. takes_value ( true ) )
2016-10-28 07:42:44 +00:00
. arg ( Arg ::with_name ( Runtime ::arg_generate_compl ( ) )
. long ( " generate-commandline-completion " )
. help ( " Generate the commandline completion for bash or zsh or fish " )
. required ( false )
. takes_value ( true )
. value_name ( " SHELL " )
. possible_values ( & [ " bash " , " fish " , " zsh " ] ) )
2016-01-20 19:46:42 +00:00
}
2016-08-06 15:43:14 +00:00
pub fn arg_names ( ) -> Vec < & 'static str > {
vec! [
Runtime ::arg_verbosity_name ( ) ,
Runtime ::arg_debugging_name ( ) ,
Runtime ::arg_no_color_output_name ( ) ,
Runtime ::arg_config_name ( ) ,
Runtime ::arg_config_override_name ( ) ,
Runtime ::arg_runtimepath_name ( ) ,
Runtime ::arg_storepath_name ( ) ,
Runtime ::arg_editor_name ( ) ,
]
}
pub fn arg_verbosity_name ( ) -> & 'static str {
" verbosity "
}
pub fn arg_debugging_name ( ) -> & 'static str {
" debugging "
}
pub fn arg_no_color_output_name ( ) -> & 'static str {
" no-color-output "
}
pub fn arg_config_name ( ) -> & 'static str {
" config "
}
pub fn arg_config_override_name ( ) -> & 'static str {
" config-override "
}
pub fn arg_runtimepath_name ( ) -> & 'static str {
" runtimepath "
}
pub fn arg_storepath_name ( ) -> & 'static str {
" storepath "
}
pub fn arg_editor_name ( ) -> & 'static str {
" editor "
}
2016-10-28 07:42:44 +00:00
pub fn arg_generate_compl ( ) -> & 'static str {
" generate-completion "
}
2016-01-21 20:24:20 +00:00
/**
* Initialize the internal logger
* /
2016-08-02 09:39:01 +00:00
fn init_logger ( is_debugging : bool , is_verbose : bool , colored : bool ) {
2016-05-24 14:43:04 +00:00
use std ::env ::var as env_var ;
use env_logger ;
2016-01-20 19:46:42 +00:00
2016-05-24 14:43:04 +00:00
if env_var ( " IMAG_LOG_ENV " ) . is_ok ( ) {
env_logger ::init ( ) . unwrap ( ) ;
} else {
let lvl = if is_debugging {
LogLevelFilter ::Debug
} else if is_verbose {
LogLevelFilter ::Info
} else {
2016-07-16 22:49:39 +00:00
LogLevelFilter ::Warn
2016-05-24 14:43:04 +00:00
} ;
log ::set_logger ( | max_log_lvl | {
max_log_lvl . set ( lvl ) ;
debug! ( " Init logger with {} " , lvl ) ;
2016-08-02 09:39:01 +00:00
Box ::new ( ImagLogger ::new ( lvl . to_log_level ( ) . unwrap ( ) ) . with_color ( colored ) )
2016-05-24 14:43:04 +00:00
} )
. map_err ( | _ | {
panic! ( " Could not setup logger " ) ;
} )
. ok ( ) ;
}
2016-01-20 19:46:42 +00:00
}
2016-01-21 20:24:20 +00:00
/**
* Get the verbosity flag value
* /
2016-01-20 19:46:42 +00:00
pub fn is_verbose ( & self ) -> bool {
self . cli_matches . is_present ( " verbosity " )
}
2016-01-21 20:24:20 +00:00
/**
* Get the debugging flag value
* /
2016-01-20 19:46:42 +00:00
pub fn is_debugging ( & self ) -> bool {
self . cli_matches . is_present ( " debugging " )
}
2016-01-21 20:24:20 +00:00
/**
* Get the runtimepath
* /
2016-01-20 19:46:42 +00:00
pub fn rtp ( & self ) -> & PathBuf {
& self . rtp
}
2016-01-21 20:24:20 +00:00
/**
* Get the commandline interface matches
* /
2016-01-20 19:46:42 +00:00
pub fn cli ( & self ) -> & ArgMatches {
& self . cli_matches
}
2016-03-26 18:50:13 +00:00
/**
* Get the configuration object
* /
pub fn config ( & self ) -> Option < & Configuration > {
self . configuration . as_ref ( )
}
2016-01-21 20:24:20 +00:00
/**
* Get the store object
* /
2016-01-20 19:46:42 +00:00
pub fn store ( & self ) -> & Store {
& self . store
}
2016-03-21 09:19:03 +00:00
pub fn editor ( & self ) -> Option < Command > {
self . cli ( )
. value_of ( " editor " )
. map ( String ::from )
. or ( {
2016-05-03 21:10:32 +00:00
match self . configuration {
Some ( ref c ) = > c . editor ( ) . cloned ( ) ,
2016-03-21 09:19:03 +00:00
_ = > None ,
}
} )
. or ( env ::var ( " EDITOR " ) . ok ( ) )
. map ( Command ::new )
}
2016-01-20 19:46:42 +00:00
}
2016-07-24 16:06:01 +00:00
fn get_override_specs ( matches : & ArgMatches ) -> Vec < String > {
matches
. values_of ( " config-override " )
. map ( | values | {
values
. filter ( | s | {
let b = s . contains ( " = " ) ;
if ! b { warn! ( " override '{}' does not contain '=' - will be ignored! " , s ) ; }
b
} )
. map ( String ::from )
. collect ( )
} )
. unwrap_or ( vec! [ ] )
}