diff options
author | Denys Séguret <cano.petrole@gmail.com> | 2023-01-03 18:16:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-03 18:16:39 +0100 |
commit | 3b4bff25b4971b24f0ec009858b112af0bde6dff (patch) | |
tree | 7523098730248e7c6d38298674bb2c20cdc7aafa | |
parent | 4fcd56b59c1fc2fdd5b87040d2e1f87c2e3370be (diff) |
Nushell br function (#647)
Install the br function for nushell.
To have this function installed on a previously installed version of broot, run `broot --install`
Fix #375
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | bacon.toml | 1 | ||||
-rw-r--r-- | src/cli/mod.rs | 9 | ||||
-rw-r--r-- | src/errors.rs | 61 | ||||
-rw-r--r-- | src/shell_install/bash.rs | 14 | ||||
-rw-r--r-- | src/shell_install/fish.rs | 4 | ||||
-rw-r--r-- | src/shell_install/mod.rs | 76 | ||||
-rw-r--r-- | src/shell_install/nushell.rs | 123 | ||||
-rw-r--r-- | src/shell_install/util.rs | 32 | ||||
-rw-r--r-- | website/broot_theme/css/base.css | 4 | ||||
-rw-r--r-- | website/broot_theme/css/bootstrap-custom.min.css | 2 | ||||
-rw-r--r-- | website/docs/css/extra.css | 4 |
12 files changed, 268 insertions, 64 deletions
@@ -12,7 +12,7 @@ license = "MIT" categories = ["command-line-utilities"] readme = "README.md" build = "build.rs" -rust-version = "1.60" +rust-version = "1.65" exclude = ["website", "broot*.zip"] [features] @@ -42,6 +42,7 @@ command = [ "-A", "clippy::module_inception", "-A", "clippy::neg_multiply", "-A", "clippy::vec_init_then_push", + "-A", "clippy::if_same_then_else", ] need_stdout = false diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6b56c36..7456337 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -72,7 +72,12 @@ pub fn run() -> Result<Option<Launchable>, ProgramError> { // configuration if specific_conf.is_none() && install_args.install != Some(false) { let mut shell_install = ShellInstall::new(install_args.install == Some(true)); - shell_install.check()?; + // TODO clean the next few lines when inspect_err is stable + let res = shell_install.check(); + if let Err(e) = &res { + shell_install.comment_error(e); + } + res?; if shell_install.should_quit { return Ok(None); } @@ -137,7 +142,7 @@ pub fn run() -> Result<Option<Launchable>, ProgramError> { } /// wait for user input, return `true` if they didn't answer 'n' -pub fn ask_authorization() -> Result<bool, ProgramError> { +pub fn ask_authorization() -> io::Result<bool> { let mut answer = String::new(); io::stdin().read_line(&mut answer)?; let answer = answer.trim(); diff --git a/src/errors.rs b/src/errors.rs index e1571bb..5ae94d5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,28 +8,59 @@ use { }; custom_error! {pub ProgramError - Io {source: io::Error} = "IO Error : {source}", - Termimad {source: termimad::Error} = "Termimad Error : {source}", + AmbiguousVerbName {name: String} = "Ambiguous name: More than one verb matches {name:?}", + ArgParse {bad: String, valid: String} = "{bad:?} can't be parsed (valid values: {valid:?})", Conf {source: ConfError} = "Bad configuration: {source}", ConfFile {path:String, details: ConfError} = "Bad configuration file {path:?} : {details}", - ArgParse {bad: String, valid: String} = "{bad:?} can't be parsed (valid values: {valid:?})", - UnknownVerb {name: String} = "No verb matches {name:?}", - AmbiguousVerbName {name: String} = "Ambiguous name: More than one verb matches {name:?}", - UnmatchingVerbArgs {name: String} = "No matching argument found for verb {name:?}", - TreeBuild {source: TreeBuildError} = "{source}", - LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}", - UnknowShell {shell: String} = "Unknown shell: {shell}", + ImageError {source: ImageError } = "{source}", InternalError {details: String} = "Internal error: {details}", // should not happen InvalidGlobError {pattern: String} = "Invalid glob: {pattern}", - Unrecognized {token: String} = "Unrecognized: {token}", - NetError {source: NetError} = "{source}", - ImageError {source: ImageError } = "{source}", + Io {source: io::Error} = "IO Error : {source}", + LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}", Lfs {details: String} = "Failed to fetch mounts: {details}", - ZeroLenFile = "File seems empty", + NetError {source: NetError} = "{source}", + OpenError { source: opener::OpenError } = "Open error: {source}", + ShelInstall { source: ShellInstallError } = "{source}", + SyntectCrashed { details: String } = "Syntect crashed on {details:?}", + Termimad {source: termimad::Error} = "Termimad Error : {source}", + TreeBuild {source: TreeBuildError} = "{source}", + UnknowShell {shell: String} = "Unknown shell: {shell}", + UnknownVerb {name: String} = "No verb matches {name:?}", UnmappableFile = "File can't be mapped", + UnmatchingVerbArgs {name: String} = "No matching argument found for verb {name:?}", UnprintableFile = "File can't be printed", // has characters that can't be printed without escaping - SyntectCrashed { details: String } = "Syntect crashed on {details:?}", - OpenError { source: opener::OpenError } = "Open error: {source}", + Unrecognized {token: String} = "Unrecognized: {token}", + ZeroLenFile = "File seems empty", +} + +custom_error!{pub ShellInstallError + Io {source: io::Error, when: String} = "IO Error {source} on {when}", +} +impl ShellInstallError { + pub fn is_permission_denied(&self) -> bool { + match self { + Self::Io { source, .. } => { + if source.kind() == io::ErrorKind::PermissionDenied { + true + } else if cfg!(windows) && source.raw_os_error().unwrap_or(0) == 1314 { + // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699- + true + } else { + false + } + } + } + } +} +pub trait IoToShellInstallError<Ok> { + fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError>; +} +impl<Ok> IoToShellInstallError<Ok> for Result<Ok, io::Error> { + fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError> { + self.map_err(|source| ShellInstallError::Io { + source, when: f() + }) + } } custom_error! {pub TreeBuildError diff --git a/src/shell_install/bash.rs b/src/shell_install/bash.rs index 1ebae33..e730e03 100644 --- a/src/shell_install/bash.rs +++ b/src/shell_install/bash.rs @@ -13,12 +13,12 @@ use { super::{util, ShellInstall}, crate::{ conf, - errors::ProgramError, + errors::*, }, directories::UserDirs, lazy_regex::regex, regex::Captures, - std::{env, fs::OpenOptions, io::Write, path::PathBuf}, + std::{env, path::PathBuf}, termimad::{ mad_print_inline, }, @@ -117,7 +117,7 @@ fn get_sourcing_paths() -> Vec<PathBuf> { /// check whether the shell function is installed, install /// it if it wasn't refused before or if broot is launched /// with --install. -pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { let script_path = get_script_path(); si.write_script(&script_path, BASH_FUNC)?; let link_path = get_link_path(); @@ -139,13 +139,7 @@ pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { &sourcing_path_str, ); } else { - let mut shellrc = OpenOptions::new() - .write(true) - .append(true) - .open(sourcing_path)?; - shellrc.write_all(b"\n")?; - shellrc.write_all(source_line.as_bytes())?; - shellrc.write_all(b"\n")?; + util::append_to_file(sourcing_path, format!("\n{source_line}\n"))?; let is_zsh = sourcing_path_str.contains(".zshrc"); if is_zsh { mad_print_inline!( diff --git a/src/shell_install/fish.rs b/src/shell_install/fish.rs index e56343e..720db20 100644 --- a/src/shell_install/fish.rs +++ b/src/shell_install/fish.rs @@ -15,7 +15,7 @@ use { super::ShellInstall, - crate::{conf, errors::ProgramError}, + crate::{conf, errors::*}, directories::BaseDirs, directories::ProjectDirs, std::path::PathBuf, @@ -94,7 +94,7 @@ fn get_script_path() -> PathBuf { /// /// As fish isn't frequently used, we first check that it seems /// to be installed. If not, we just do nothing. -pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { let fish_dir = get_fish_dir(); if !fish_dir.exists() { debug!("no fish config directory. Assuming fish isn't used."); diff --git a/src/shell_install/mod.rs b/src/shell_install/mod.rs index f3de0ac..356c714 100644 --- a/src/shell_install/mod.rs +++ b/src/shell_install/mod.rs @@ -2,11 +2,12 @@ use { crate::{ cli::{self, ShellInstallState}, conf, - errors::ProgramError, + errors::*, skin, }, std::{ - fs, io, os, + fs, + os, path::{Path, PathBuf}, }, termimad::{mad_print_inline, MadSkin}, @@ -14,6 +15,7 @@ use { mod bash; mod fish; +mod nushell; mod util; const MD_INSTALL_REQUEST: &str = r#" @@ -34,6 +36,13 @@ If you want the `br` shell function, you may either "#; +const MD_PERMISSION_DENIED: &str = r#" +Installation check resulted in **Permission Denied**. +Please relaunch with elevated privilege. +This is typically only needed once. +Error details: +"#; + const MD_INSTALL_DONE: &str = r#" The **br** function has been successfully installed. You may have to restart your shell or source your shell init files. @@ -68,23 +77,29 @@ pub struct ShellInstall { /// or test scripts when we don't want the user to be prompted /// to install the function, or in case something doesn't properly /// work in shell detections -pub fn write_state(state: ShellInstallState) -> Result<(), ProgramError> { +pub fn write_state(state: ShellInstallState) -> Result<(), ShellInstallError> { let refused_path = get_refused_path(); let installed_path = get_installed_path(); if installed_path.exists() { - fs::remove_file(&installed_path)?; + fs::remove_file(&installed_path) + .context(&|| format!("removing {:?}", &installed_path))?; } if refused_path.exists() { - fs::remove_file(&refused_path)?; + fs::remove_file(&refused_path) + .context(&|| format!("removing {:?}", &refused_path))?; } match state { ShellInstallState::Refused => { - fs::create_dir_all(refused_path.parent().unwrap())?; - fs::write(&refused_path, REFUSED_FILE_CONTENT)?; + fs::create_dir_all(refused_path.parent().unwrap()) + .context(&|| format!("creating parents of {refused_path:?}"))?; + fs::write(&refused_path, REFUSED_FILE_CONTENT) + .context(&|| format!("writing in {refused_path:?}"))?; } ShellInstallState::Installed => { - fs::create_dir_all(installed_path.parent().unwrap())?; - fs::write(&installed_path, INSTALLED_FILE_CONTENT)?; + fs::create_dir_all(installed_path.parent().unwrap()) + .context(&|| format!("creating parents of {installed_path:?}"))?; + fs::write(&installed_path, INSTALLED_FILE_CONTENT) + .context(&|| format!("writing in {installed_path:?}"))?; } _ => {} } @@ -116,6 +131,7 @@ impl ShellInstall { match shell { "bash" | "zsh" => println!("{}", bash::get_script()), "fish" => println!("{}", fish::get_script()), + "nushell" => println!("{}", nushell::get_script()), _ => { return Err(ProgramError::UnknowShell { shell: shell.to_string(), @@ -128,7 +144,7 @@ impl ShellInstall { /// check whether the shell function is installed, install /// it if it wasn't refused before or if broot is launched /// with --install. - pub fn check(&mut self) -> Result<(), ProgramError> { + pub fn check(&mut self) -> Result<(), ShellInstallError> { let installed_path = get_installed_path(); if self.force_install { self.skin.print_text("You requested a clean (re)install."); @@ -152,6 +168,7 @@ impl ShellInstall { debug!("Starting install"); bash::install(self)?; fish::install(self)?; + nushell::install(self)?; self.should_quit = true; if self.done { self.skin.print_text(MD_INSTALL_DONE); @@ -159,19 +176,28 @@ impl ShellInstall { Ok(()) } - pub fn remove(&self, path: &Path) -> io::Result<()> { + /// print some additional information on the error (typically before + /// the error itself is dumped) + pub fn comment_error(&self, err: &ShellInstallError) { + if err.is_permission_denied() { + self.skin.print_text(MD_PERMISSION_DENIED); + } + } + + pub fn remove(&self, path: &Path) -> Result<(), ShellInstallError> { // path.exists() doesn't work when the file is a link (it checks whether // the link destination exists instead of checking the link exists // so we first check whether the link exists if fs::read_link(path).is_ok() || path.exists() { mad_print_inline!(self.skin, "Removing `$0`.\n", path.to_string_lossy()); - fs::remove_file(path)?; + fs::remove_file(path) + .context(&|| format!("removing {path:?}"))?; } Ok(()) } /// check whether we're allowed to install. - fn can_do(&mut self) -> Result<bool, ProgramError> { + fn can_do(&mut self) -> Result<bool, ShellInstallError> { if let Some(authorization) = self.authorization { return Ok(authorization); } @@ -181,7 +207,8 @@ impl ShellInstall { return Ok(false); } self.skin.print_text(MD_INSTALL_REQUEST); - let proceed = cli::ask_authorization()?; + let proceed = cli::ask_authorization() + .context(&|| "asking user".to_string())?; // read_line failure debug!("proceed: {:?}", proceed); self.authorization = Some(proceed); if !proceed { @@ -192,7 +219,7 @@ impl ShellInstall { } /// write the script at the given path - fn write_script(&self, script_path: &Path, content: &str) -> Result<(), ProgramError> { + fn write_script(&self, script_path: &Path, content: &str) -> Result<(), ShellInstallError> { self.remove(script_path)?; info!("Writing `br` shell function in `{:?}`", &script_path); mad_print_inline!( @@ -200,13 +227,16 @@ impl ShellInstall { "Writing *br* shell function in `$0`.\n", script_path.to_string_lossy(), ); - fs::create_dir_all(script_path.parent().unwrap())?; - fs::write(script_path, content)?; + fs::create_dir_all(script_path.parent().unwrap()) + .context(&|| format!("creating parent dirs to {script_path:?}"))?; + fs::write(script_path, content) + .context(&|| format!("writing script in {script_path:?}"))?; Ok(()) } + /// create a link - fn create_link(&self, link_path: &Path, script_path: &Path) -> Result<(), ProgramError> { + fn create_link(&self, link_path: &Path, script_path: &Path) -> Result<(), ShellInstallError> { info!("Creating link from {:?} to {:?}", &link_path, &script_path); self.remove(link_path)?; let link_path_str = link_path.to_string_lossy(); @@ -217,11 +247,15 @@ impl ShellInstall { &link_path_str, &script_path_str, ); - fs::create_dir_all(link_path.parent().unwrap())?; + let parent = link_path.parent().unwrap(); + fs::create_dir_all(parent) + .context(&|| format!("creating directory {parent:?}"))?; #[cfg(unix)] - os::unix::fs::symlink(script_path, link_path)?; + os::unix::fs::symlink(script_path, link_path) + .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?; #[cfg(windows)] - os::windows::fs::symlink_file(&script_path, &link_path)?; + os::windows::fs::symlink_file(&script_path, &link_path) + .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?; Ok(()) } } diff --git a/src/shell_install/nushell.rs b/src/shell_install/nushell.rs new file mode 100644 index 0000000..228a28f --- /dev/null +++ b/src/shell_install/nushell.rs @@ -0,0 +1,123 @@ +//! The goal of this mod is to ensure the launcher shell function +//! is available for nushell i.e. the `br` shell function can +//! be used to launch broot (and thus make it possible to execute +//! some commands, like `cd`, from the starting shell. +//! +//! In a correct installation, we have: +//! - a function declaration script in ~/.local/share/broot/launcher/nushell/br/1 +//! - a link to that script in ~/.config/broot/launcher/nushell/br/1 +//! - a line to source the link in ~/.config/nushell/config.nu +//! (exact paths depend on XDG variables) +//! +//! More info at +//! https://github.com/Canop/broot/issues/375 + +use { + super::{util, ShellInstall}, + crate::{ + conf, + errors::*, + }, + directories::BaseDirs, + std::path::PathBuf, + termimad::{ + mad_print_inline, + }, +}; + +const NAME: &str = "nushell"; +const VERSION: &str = "1"; + +const NU_FUNC: &str = r#" +# This script was automatically generated by the broot program +# More information can be found in https://github.com/Canop/broot +# This function starts broot and executes the command +# it produces, if any. +# It's needed because some shell commands, like `cd`, +# have no useful effect if executed in a subshell. +def _br_cmd [] { + let cmd_file = ([ $nu.temp-path, $"broot-(random chars).tmp" ] | path join) + touch $cmd_file + ^broot --outcmd $cmd_file + let target_dir = (open $cmd_file | to text | str replace "^cd\\s+" "" | str trim) + rm -p -f $cmd_file + + $target_dir +} +alias br = cd (_br_cmd) +"#; + +pub fn get_script() -> &'static str { + NU_FUNC +} + +/// return the path to the link to the function script +fn get_link_path() -> PathBuf { + conf::dir().join("launcher").join(NAME).join("br") +} + +/// return the root of +fn get_nushell_dir() -> Option<PathBuf> { + BaseDirs::new() + .map(|base_dirs| base_dirs.config_dir().join("nushell")) + .filter(|dir| dir.exists()) +} + +/// return the path to the script containing the function. +/// +/// In XDG_DATA_HOME (typically ~/.local/share on linux) +fn get_script_path() -> PathBuf { + conf::app_dirs() + .data_dir() + .join("launcher") + .join(NAME) + .join(VERSION) +} + +/// Check for nushell. +/// +/// Check whether the shell function is installed, install +/// it if it wasn't refused before or if broot is launched +/// with --install. +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { + debug!("install {NAME}"); + let Some(nushell_dir) = get_nushell_dir() else { + debug!("no nushell config directory. Assuming nushell isn't used."); + return Ok(()); + }; + info!("nushell seems to be installed"); + let script_path = get_script_path(); + si.write_script(&script_path, NU_FUNC)?; + let link_path = get_link_path(); + si.create_link(&link_path, &script_path)?; + + let escaped_path = link_path.to_string_lossy().replace(' ', "\\ "); + let source_line = format!("source {}", &escaped_path); + + let sourcing_path = nushell_dir.join("config.nu"); + if !sourcing_path.exists() { + warn!("Unexpected lack of config.nu file"); + return Ok(()); + } + if sourcing_path.is_dir() { + warn!("config.nu file"); + return Ok(()); + } + let sourcing_path_str = sourcing_path.to_string_lossy(); + if util::file_contains_line(&sourcing_path, &source_line)? { + mad_print_inline!( + &si.skin, + "`$0` already patched, no change made.\n", + &sourcing_path_str, + ); + } else { + util::append_to_file(&sourcing_path, format!("\n{source_line}\n"))?; + mad_print_inline!( + &si.skin, + "`$0` successfully patched, you can make the function immediately available with `source $0`\n", + &sourcing_path_str, + ); + } + si.done = true; + Ok(()) +} diff --git a/src/shell_install/util.rs b/src/shell_install/util.rs index 13ad5d0..34d11d1 100644 --- a/src/shell_install/util.rs +++ b/src/shell_install/util.rs @@ -1,14 +1,30 @@ -use std::{ - fs, - io::{self, BufRead, BufReader}, - path::Path, +use { + crate::errors::*, + std::{ + fs::{self, OpenOptions}, + io::{BufRead, BufReader, Write}, + path::Path, + }, }; - -pub fn file_contains_line(path: &Path, searched_line: &str) -> io::Result<bool> { - for line in BufReader::new(fs::File::open(path)?).lines() { - if line? == searched_line { +pub fn file_contains_line(path: &Path, searched_line: &str) -> Result<bool, ShellInstallError> { + let file = fs::File::open(path) + .context(&|| format!("opening {path:?}"))?; + for line in BufReader::new(file).lines() { + let line = line.context(&|| format!("reading line in {path:?}"))?; + if line == searched_line { return Ok(true); } } Ok(false) } + +pub fn append_to_file<S: AsRef<str>>(path: &Path, content: S) -> Result<(), ShellInstallError> { + let mut shellrc = OpenOptions::new() + .write(true) + .append(true) + .open(path) + .context(&|| format!("opening {path:?} for append"))?; + shellrc.write_all(content.as_ref().as_bytes()) + .context(&|| format!("writing in {path:?}"))?; + Ok(()) +} diff --git a/website/broot_theme/css/base.css b/website/broot_theme/css/base.css index ab8471b..d569453 100644 --- a/website/broot_theme/css/base.css +++ b/website/broot_theme/css/base.css @@ -22,7 +22,7 @@ body > .container { } ul.nav .main { - font-weight: bold; + /*font-weight: bold;*/ } .col-md-3 { @@ -177,7 +177,7 @@ footer { .bs-sidebar .nav > .active > a, .bs-sidebar .nav > .active:hover > a, .bs-sidebar .nav > .active:focus > a { - font-weight: bold; + /*font-weight: bold;*/ background-color: transparent; border-right: 1px solid; } diff --git a/website/broot_theme/css/bootstrap-custom.min.css b/website/broot_theme/css/bootstrap-custom.min.css index d85b1dc..a0576ba 100644 --- a/website/broot_theme/css/bootstrap-custom.min.css +++ b/website/broot_theme/css/bootstrap-custom.min.css @@ -1 +1 @@ -/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#555;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#2fa4e7;text-decoration:none}a:hover,a:focus{color:#157ab5;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:#317eac}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#2fa4e7}.text-primary:hover{color:#178acc}.text-warning{color:#c09853}.text-warning:hover{color:#a47e3c}.text-danger{color:#b94a48}.text-danger:hover{color:#953b39}.text-success{color:#468847}.text-success:hover{color:#356635}.text-info{color:#3a87ad}.text-info:hover{color:#2d6987}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{marg |