mirror of
https://github.com/tealdeer-rs/tealdeer.git
synced 2026-08-23 16:44:18 +02:00
Merge pull request #43 from Voultapher/tealdeer
This commit is contained in:
commit
278a2bbac9
17 changed files with 540 additions and 56 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -136,7 +136,7 @@ dependencies = [
|
|||
"semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
|
@ -672,6 +672,7 @@ dependencies = [
|
|||
"tar 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
|
@ -724,7 +725,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
@ -937,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
|
||||
"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
|
||||
"checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e"
|
||||
"checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9"
|
||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
|
||||
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ serde = "1.0.21"
|
|||
serde_derive = "1.0.21"
|
||||
tar = "0.4.14"
|
||||
time = "0.1.38"
|
||||
toml = "0.4.6"
|
||||
walkdir = "2.0.1"
|
||||
xdg = "2.1.0"
|
||||
|
||||
|
|
|
|||
70
README.md
70
README.md
|
|
@ -9,13 +9,14 @@
|
|||
A very fast implementation of [tldr](https://github.com/tldr-pages/tldr) in
|
||||
Rust: Simplified, example based and community-driven man pages.
|
||||
|
||||

|
||||
<img src="screenshot-default.png" alt="Screenshot of tldr command" width="600">
|
||||
|
||||
If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :)
|
||||
|
||||
In case you're in a hurry and just want to quickly try tealdeer, you can find static
|
||||
binaries on the [GitHub releases page](https://github.com/dbrgn/tealdeer/releases/)!
|
||||
|
||||
|
||||
## Goals
|
||||
|
||||
High level project goals:
|
||||
|
|
@ -54,6 +55,7 @@ These are the clients I tried but failed to compile or run:
|
|||
[Go client by anoopengineer](https://github.com/anoopengineer/tldr/),
|
||||
[PHP client](https://github.com/BrainMaestro/tldr-php).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
tldr [options] <command>
|
||||
|
|
@ -61,13 +63,15 @@ These are the clients I tried but failed to compile or run:
|
|||
|
||||
Options:
|
||||
|
||||
-h --help Show this screen
|
||||
-v --version Show version information
|
||||
-l --list List all commands in the cache
|
||||
-f --render <file> Render a specific markdown file
|
||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
||||
-u --update Update the local cache
|
||||
-c --clear-cache Clear the local cache
|
||||
-h --help Show this screen
|
||||
-v --version Show version information
|
||||
-l --list List all commands in the cache
|
||||
-f --render <file> Render a specific markdown file
|
||||
-o --os <type> Override the operating system [linux, osx, sunos]
|
||||
-u --update Update the local cache
|
||||
-c --clear-cache Clear the local cache
|
||||
--config-path Show config file path
|
||||
--seed-config Create a basic config
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
@ -116,14 +120,58 @@ To enable the log output, set the `RUST_LOG` env variable:
|
|||
|
||||
$ export RUST_LOG=tldr=debug
|
||||
|
||||
|
||||
### From AUR (Arch Linux)
|
||||
|
||||
If you're an Arch Linux user, you can also install the package from the AUR:
|
||||
|
||||
$ yaourt -S tealdeer
|
||||
- [tealdeer](https://aur.archlinux.org/packages/tealdeer/)
|
||||
- [tealdeer-git](https://aur.archlinux.org/packages/tealdeer-git/)
|
||||
|
||||
(Or `tealdeer-git` if you prefer the current development version.)
|
||||
|
||||
## Configuration
|
||||
|
||||
The tldr command can be customized with a config file called `config.toml`.
|
||||
Creating the config file can be done manually or with the help of tldr:
|
||||
|
||||
$ tldr --seed-config
|
||||
|
||||
The configuration file path follows OS conventions. It can be queried with the following command:
|
||||
|
||||
$ tldr --config-path
|
||||
|
||||
### Style
|
||||
|
||||
Using the config file, the style (e.g. colors or underlines) can be customized.
|
||||
|
||||
Possible styles:
|
||||
|
||||
- `description`: The initial description text
|
||||
- `command_name`: The command name as part of the example code
|
||||
- `example_text`: The text that describes an example
|
||||
- `example_code`: The example itself, except the `command_name` and `example_variable`
|
||||
- `example_variable`: The variables in the example
|
||||
|
||||
Currently supported attributes:
|
||||
|
||||
- `foreground` (color string, see below)
|
||||
- `background` (color string, see below)
|
||||
- `underline` (`true` or `false`)
|
||||
- `bold` (`true` or `false`)
|
||||
|
||||
The currently supported colors are:
|
||||
|
||||
- `black`
|
||||
- `red`
|
||||
- `green`
|
||||
- `yellow`
|
||||
- `blue`
|
||||
- `purple`
|
||||
- `cyan`
|
||||
- `white`
|
||||
|
||||
Example customization:
|
||||
|
||||
<img src="screenshot-custom.png" alt="Screenshot of customized version" width="600">
|
||||
|
||||
|
||||
## Bash Autocompletion
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ _tealdeer()
|
|||
_init_completion || return
|
||||
|
||||
case $prev in
|
||||
-h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache)
|
||||
-h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|--config-path|--seed-config)
|
||||
return
|
||||
;;
|
||||
-f|--render)
|
||||
|
|
|
|||
BIN
screenshot-custom.png
Normal file
BIN
screenshot-custom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
screenshot-default.png
Normal file
BIN
screenshot-default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
255
src/config.rs
Normal file
255
src/config.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{Error as IoError, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ansi_term::{Color, Style};
|
||||
use toml;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use error::TealdeerError::{self, ConfigError};
|
||||
|
||||
pub const CONFIG_FILE_NAME: &'static str = "config.toml";
|
||||
|
||||
fn default_underline() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_bold() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum RawColor {
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Purple,
|
||||
Cyan,
|
||||
White,
|
||||
}
|
||||
|
||||
impl From<RawColor> for Color {
|
||||
fn from(raw_color: RawColor) -> Color {
|
||||
match raw_color {
|
||||
RawColor::Black => Color::Black,
|
||||
RawColor::Red => Color::Red,
|
||||
RawColor::Green => Color::Green,
|
||||
RawColor::Yellow => Color::Yellow,
|
||||
RawColor::Blue => Color::Blue,
|
||||
RawColor::Purple => Color::Purple,
|
||||
RawColor::Cyan => Color::Cyan,
|
||||
RawColor::White => Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct RawStyle {
|
||||
pub foreground: Option<RawColor>,
|
||||
pub background: Option<RawColor>,
|
||||
#[serde(default = "default_underline")]
|
||||
pub underline: bool,
|
||||
#[serde(default = "default_bold")]
|
||||
pub bold: bool,
|
||||
}
|
||||
|
||||
impl Default for RawStyle {
|
||||
fn default() -> RawStyle {
|
||||
RawStyle{
|
||||
foreground: None,
|
||||
background: None,
|
||||
underline: false,
|
||||
bold: false,
|
||||
}
|
||||
}
|
||||
} // impl RawStyle
|
||||
|
||||
impl From<RawStyle> for Style {
|
||||
fn from(raw_style: RawStyle) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
if let Some(foreground) = raw_style.foreground {
|
||||
style = style.fg(Color::from(foreground));
|
||||
}
|
||||
|
||||
if let Some(background) = raw_style.background {
|
||||
style = style.on(Color::from(background));
|
||||
}
|
||||
|
||||
if raw_style.underline {
|
||||
style = style.underline();
|
||||
}
|
||||
|
||||
if raw_style.bold {
|
||||
style = style.bold();
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct RawStyleConfig {
|
||||
#[serde(default)]
|
||||
pub description: RawStyle,
|
||||
#[serde(default)]
|
||||
pub command_name: RawStyle,
|
||||
#[serde(default)]
|
||||
pub example_text: RawStyle,
|
||||
#[serde(default)]
|
||||
pub example_code: RawStyle,
|
||||
#[serde(default)]
|
||||
pub example_variable: RawStyle,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
struct RawConfig {
|
||||
style: RawStyleConfig,
|
||||
}
|
||||
|
||||
impl RawConfig {
|
||||
fn new() -> RawConfig {
|
||||
let mut raw_config = RawConfig::default();
|
||||
|
||||
// Set default config
|
||||
raw_config.style.example_text.foreground = Some(RawColor::Green);
|
||||
raw_config.style.command_name.foreground = Some(RawColor::Cyan);
|
||||
raw_config.style.example_code.foreground = Some(RawColor::Cyan);
|
||||
raw_config.style.example_variable.foreground = Some(RawColor::Cyan);
|
||||
raw_config.style.example_variable.underline = true;
|
||||
|
||||
raw_config
|
||||
}
|
||||
} // impl RawConfig
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct StyleConfig {
|
||||
pub description: Style,
|
||||
pub command_name: Style,
|
||||
pub example_text: Style,
|
||||
pub example_code: Style,
|
||||
pub example_variable: Style,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct Config {
|
||||
pub style: StyleConfig,
|
||||
}
|
||||
|
||||
impl From<RawConfig> for Config {
|
||||
fn from(raw_config: RawConfig) -> Config {
|
||||
Config{
|
||||
style: StyleConfig{
|
||||
command_name: raw_config.style.command_name.into(),
|
||||
description: raw_config.style.description.into(),
|
||||
example_text: raw_config.style.example_text.into(),
|
||||
example_code: raw_config.style.example_code.into(),
|
||||
example_variable: raw_config.style.example_variable.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_io_err_to_config_err(e: IoError) -> TealdeerError {
|
||||
ConfigError(format!("Io Error: {}", e))
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Config, TealdeerError> {
|
||||
let raw_config = match get_config_path() {
|
||||
Ok(config_file_path) => {
|
||||
let mut config_file = fs::File::open(config_file_path)
|
||||
.map_err(map_io_err_to_config_err)?;
|
||||
let mut contents = String::new();
|
||||
let _rc = config_file.read_to_string(&mut contents)
|
||||
.map_err(map_io_err_to_config_err)?;
|
||||
|
||||
toml::from_str(&contents).map_err(|err| ConfigError(format!("Failed to parse config file: {}", err)))?
|
||||
}
|
||||
Err(ConfigError(_)) => RawConfig::new(),
|
||||
Err(_) => {
|
||||
return Err(ConfigError("Unknown error while looking up config path".into()));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Config::from(raw_config))
|
||||
}
|
||||
} // impl Config
|
||||
|
||||
/// Return the path to the config directory.
|
||||
pub fn get_config_dir() -> Result<PathBuf, TealdeerError> {
|
||||
// Allow overriding the config directory by setting the
|
||||
// $TEALDEER_CONFIG_DIR env variable.
|
||||
if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") {
|
||||
let path = PathBuf::from(value);
|
||||
|
||||
if path.exists() && path.is_dir() {
|
||||
return Ok(path)
|
||||
} else {
|
||||
return Err(ConfigError(
|
||||
"Path specified by $TEALDEER_CONFIG_DIR \
|
||||
does not exist or is not a directory.".into()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Otherwise, fall back to $XDG_CONFIG_HOME/tealdeer.
|
||||
let xdg_dirs = match BaseDirectories::with_prefix(::NAME) {
|
||||
Ok(dirs) => dirs,
|
||||
Err(_) => return Err(ConfigError("Could not determine XDG base directory.".into())),
|
||||
};
|
||||
Ok(xdg_dirs.get_config_home())
|
||||
}
|
||||
|
||||
/// Return the path to the config file.
|
||||
pub fn get_config_path() -> Result<PathBuf, TealdeerError> {
|
||||
let config_dir = get_config_dir()?;
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
|
||||
if config_file_path.is_file() {
|
||||
Ok(config_file_path)
|
||||
} else {
|
||||
Err(ConfigError(format!("{} is not a file path", config_file_path.to_str().unwrap())))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create default config file.
|
||||
pub fn make_default_config() -> Result<PathBuf, TealdeerError> {
|
||||
let config_dir = get_config_dir()?;
|
||||
if !config_dir.is_dir() {
|
||||
if let Err(e) = fs::create_dir_all(&config_dir) {
|
||||
return Err(ConfigError(format!("Could not create config directory: {}", e)));
|
||||
}
|
||||
}
|
||||
|
||||
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
|
||||
if config_file_path.is_file() {
|
||||
return Err(ConfigError(format!(
|
||||
"A configuration file already exists at {}, no action was taken.",
|
||||
config_file_path.to_str().unwrap()
|
||||
)));
|
||||
}
|
||||
|
||||
let serialized_config = toml::to_string(&RawConfig::new())
|
||||
.map_err(|err| ConfigError(format!("Failed to serialize default config: {}", err)))?;
|
||||
|
||||
let mut config_file = fs::File::create(&config_file_path)
|
||||
.map_err(map_io_err_to_config_err)?;
|
||||
let _wc = config_file.write(serialized_config.as_bytes())
|
||||
.map_err(map_io_err_to_config_err)?;
|
||||
|
||||
Ok(config_file_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize() {
|
||||
let raw_config = RawConfig::new();
|
||||
let serialized = toml::to_string(&raw_config).unwrap();
|
||||
let deserialized: RawConfig = toml::from_str(&serialized).unwrap();
|
||||
assert_eq!(raw_config, deserialized);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use curl::Error as CurlError;
|
|||
#[derive(Debug)]
|
||||
pub enum TealdeerError {
|
||||
CacheError(String),
|
||||
ConfigError(String),
|
||||
UpdateError(String),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,35 +2,64 @@
|
|||
|
||||
use std::io::BufRead;
|
||||
|
||||
use ansi_term::{Colour, ANSIStrings};
|
||||
use ansi_term::{ANSIString, ANSIStrings};
|
||||
|
||||
use config::Config;
|
||||
use tokenizer::Tokenizer;
|
||||
use types::LineType;
|
||||
|
||||
/// Provide formatting for {{ curly braces }} in ExampleCode lines
|
||||
fn format_braces(text: &str) -> String {
|
||||
let parts = text.split("{{").flat_map(|s| s.split("}}"))
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
if i % 2 == 0 {
|
||||
Colour::Cyan.paint(v)
|
||||
} else {
|
||||
Colour::Cyan.underline().paint(v)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
fn highlight_command<'a>(
|
||||
command: &'a str,
|
||||
example_code: &'a str,
|
||||
config: &Config,
|
||||
parts: &mut Vec<ANSIString<'a>>
|
||||
) {
|
||||
let mut code_part_end_pos = 0;
|
||||
while let Some(command_start) = example_code[code_part_end_pos..].find(&command) {
|
||||
let code_part = &example_code[code_part_end_pos..code_part_end_pos + command_start];
|
||||
parts.push(config.style.example_code.paint(code_part));
|
||||
parts.push(config.style.command_name.paint(command));
|
||||
|
||||
code_part_end_pos += command_start + command.len();
|
||||
}
|
||||
parts.push(config.style.example_code.paint(&example_code[code_part_end_pos..]));
|
||||
}
|
||||
|
||||
/// Format and highlight code examples including variables in {{ curly braces }}.
|
||||
fn format_code(command: &str, text: &str, config: &Config) -> String {
|
||||
let mut parts = Vec::new();
|
||||
for between_variables in text.split("}}") {
|
||||
if let Some(variable_start) = between_variables.find("{{") {
|
||||
let example_code = &between_variables[..variable_start];
|
||||
let example_variable = &between_variables[variable_start + 2..];
|
||||
|
||||
highlight_command(&command, &example_code, &config, &mut parts);
|
||||
parts.push(config.style.example_variable.paint(example_variable));
|
||||
} else {
|
||||
highlight_command(&command, &between_variables, &config, &mut parts);
|
||||
}
|
||||
}
|
||||
|
||||
ANSIStrings(&parts).to_string()
|
||||
}
|
||||
|
||||
/// Print a token stream to an ANSI terminal.
|
||||
pub fn print_lines<R>(tokenizer: &mut Tokenizer<R>) where R: BufRead {
|
||||
pub fn print_lines<R>(tokenizer: &mut Tokenizer<R>, config: &Config) where R: BufRead {
|
||||
let mut command = String::new();
|
||||
while let Some(token) = tokenizer.next_token() {
|
||||
match token {
|
||||
LineType::Empty => println!(""),
|
||||
LineType::Title(_) => debug!("Ignoring title"),
|
||||
LineType::Description(text) => println!(" {}", text),
|
||||
LineType::ExampleText(text) => println!(" {}", Colour::Green.paint(text)),
|
||||
LineType::ExampleCode(text) => println!(" {}", &format_braces(&text)),
|
||||
LineType::Title(title) => {
|
||||
debug!("Ignoring title");
|
||||
|
||||
// This is safe as long as the parsed title is only the command,
|
||||
// and tokenizer yields values in order of appearance.
|
||||
command = title;
|
||||
debug!("Detected command name: {}", &command);
|
||||
},
|
||||
LineType::Description(text) => println!(" {}", config.style.description.paint(text)),
|
||||
LineType::ExampleText(text) => println!(" {}", config.style.example_text.paint(text)),
|
||||
LineType::ExampleCode(text) => println!(" {}", &format_code(&command, &text, &config)),
|
||||
LineType::Other(text) => debug!("Unknown line type: {:?}", text),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
src/main.rs
85
src/main.rs
|
|
@ -31,6 +31,7 @@ extern crate tar;
|
|||
extern crate xdg;
|
||||
extern crate curl;
|
||||
extern crate time;
|
||||
extern crate toml;
|
||||
extern crate walkdir;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
|
@ -41,17 +42,19 @@ use std::path::{Path, PathBuf};
|
|||
use std::process;
|
||||
|
||||
use docopt::Docopt;
|
||||
use ansi_term::Colour;
|
||||
use ansi_term::Color;
|
||||
|
||||
mod types;
|
||||
mod tokenizer;
|
||||
mod formatter;
|
||||
mod cache;
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
use tokenizer::Tokenizer;
|
||||
use cache::Cache;
|
||||
use error::TealdeerError::{UpdateError, CacheError};
|
||||
use config::{get_config_path, make_default_config, Config};
|
||||
use error::TealdeerError::{CacheError, ConfigError, UpdateError};
|
||||
use formatter::print_lines;
|
||||
use types::OsType;
|
||||
|
||||
|
|
@ -72,6 +75,8 @@ Options:
|
|||
-o --os <type> Override the operating system [linux, osx, sunos]
|
||||
-u --update Update the local cache
|
||||
-c --clear-cache Clear the local cache
|
||||
--config-path Show config file path
|
||||
--seed-config Create a basic config
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
@ -100,6 +105,8 @@ struct Args {
|
|||
flag_os: Option<OsType>,
|
||||
flag_update: bool,
|
||||
flag_clear_cache: bool,
|
||||
flag_config_path: bool,
|
||||
flag_seed_config: bool,
|
||||
}
|
||||
|
||||
/// Print page by path
|
||||
|
|
@ -110,9 +117,22 @@ fn print_page(path: &Path) -> Result<(), String> {
|
|||
);
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
// Look up config file, if none is found fall back to default config.
|
||||
let config = match Config::load() {
|
||||
Ok(config) => config,
|
||||
Err(ConfigError(msg)) => {
|
||||
eprintln!("Failed to create config: {}", msg);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Unknown error while creating config");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Create tokenizer and print output
|
||||
let mut tokenizer = Tokenizer::new(reader);
|
||||
print_lines(&mut tokenizer);
|
||||
print_lines(&mut tokenizer, &config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -122,14 +142,14 @@ fn check_cache(args: &Args, cache: &Cache) {
|
|||
if !args.flag_update {
|
||||
match cache.last_update() {
|
||||
Some(ago) if ago > MAX_CACHE_AGE => {
|
||||
println!("{}", Colour::Red.paint(format!(
|
||||
println!("{}", Color::Red.paint(format!(
|
||||
"Cache wasn't updated in {} days.\n\
|
||||
You should probably run `tldr --update` soon.\n",
|
||||
You should probably run `tldr --update` soon.",
|
||||
MAX_CACHE_AGE / 24 / 3600
|
||||
)));
|
||||
},
|
||||
None => {
|
||||
println!("Cache not found. Please run `tldr --update`.");
|
||||
eprintln!("Cache not found. Please run `tldr --update`.");
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {},
|
||||
|
|
@ -182,7 +202,8 @@ fn main() {
|
|||
if args.flag_clear_cache {
|
||||
cache.clear().unwrap_or_else(|e| {
|
||||
match e {
|
||||
UpdateError(msg) | CacheError(msg) => println!("Could not delete cache: {}", msg),
|
||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) =>
|
||||
eprintln!("Could not delete cache: {}", msg),
|
||||
};
|
||||
process::exit(1);
|
||||
});
|
||||
|
|
@ -193,7 +214,8 @@ fn main() {
|
|||
if args.flag_update {
|
||||
cache.update().unwrap_or_else(|e| {
|
||||
match e {
|
||||
UpdateError(msg) | CacheError(msg) => println!("Could not update cache: {}", msg),
|
||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) =>
|
||||
eprintln!("Could not update cache: {}", msg),
|
||||
};
|
||||
process::exit(1);
|
||||
});
|
||||
|
|
@ -204,7 +226,7 @@ fn main() {
|
|||
if let Some(ref file) = args.flag_render {
|
||||
let path = PathBuf::from(file);
|
||||
if let Err(msg) = print_page(&path) {
|
||||
println!("{}", msg);
|
||||
eprintln!("{}", msg);
|
||||
process::exit(1);
|
||||
} else {
|
||||
process::exit(0);
|
||||
|
|
@ -219,7 +241,8 @@ fn main() {
|
|||
// Get list of pages
|
||||
let pages = cache.list_pages().unwrap_or_else(|e| {
|
||||
match e {
|
||||
UpdateError(msg) | CacheError(msg) => println!("Could not get list of pages: {}", msg),
|
||||
CacheError(msg) | ConfigError(msg) | UpdateError(msg) =>
|
||||
eprintln!("Could not get list of pages: {}", msg),
|
||||
}
|
||||
process::exit(1);
|
||||
});
|
||||
|
|
@ -237,7 +260,7 @@ fn main() {
|
|||
// Search for command in cache
|
||||
if let Some(path) = cache.find_page(&command) {
|
||||
if let Err(msg) = print_page(&path) {
|
||||
println!("{}", msg);
|
||||
eprintln!("{}", msg);
|
||||
process::exit(1);
|
||||
} else {
|
||||
process::exit(0);
|
||||
|
|
@ -245,14 +268,50 @@ fn main() {
|
|||
} else {
|
||||
println!("Page {} not found in cache", &command);
|
||||
println!("Try updating with `tldr --update`, or submit a pull request to:");
|
||||
println!("https://github.com/tldr-pages/tldr");
|
||||
eprintln!("https://github.com/tldr-pages/tldr");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Show config file and path and exit
|
||||
if args.flag_config_path {
|
||||
match get_config_path() {
|
||||
Ok(config_file_path) => {
|
||||
println!("Config path is: {}", config_file_path.to_str().unwrap());
|
||||
process::exit(0);
|
||||
},
|
||||
Err(ConfigError(msg)) => {
|
||||
eprintln!("Could not look up config_path: {}", msg);
|
||||
process::exit(1);
|
||||
},
|
||||
Err(_) => {
|
||||
eprintln!("Unknown error");
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create a basic config and exit
|
||||
if args.flag_seed_config {
|
||||
match make_default_config() {
|
||||
Ok(config_file_path) => {
|
||||
println!("Successfully created seed config file here: {}", config_file_path.to_str().unwrap());
|
||||
process::exit(0);
|
||||
},
|
||||
Err(ConfigError(msg)) => {
|
||||
eprintln!("Could not create seed config: {}", msg);
|
||||
process::exit(1);
|
||||
},
|
||||
Err(_) => {
|
||||
eprintln!("Unkown error");
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Some flags can be run without a command.
|
||||
if !(args.flag_update || args.flag_clear_cache) {
|
||||
println!("{}", USAGE);
|
||||
eprintln!("{}", USAGE);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
tests/config.toml
Normal file
17
tests/config.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[style.highlight]
|
||||
foreground = "green"
|
||||
underline = false
|
||||
bold = false
|
||||
|
||||
[style.description]
|
||||
underline = false
|
||||
bold = false
|
||||
|
||||
[style.example_text]
|
||||
foreground = "black"
|
||||
background = "blue"
|
||||
underline = false
|
||||
|
||||
[style.example_variable]
|
||||
underline = true
|
||||
bold = false
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
[32mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
[36minkscape [4mfilename.svg[0m[36m --export-pdf=[4mfilename.pdf[0m[36m --export-text-to-path[0m
|
||||
[36minkscape [4mfilename.svg[0m[36m | inkscape | inkscape --export-pdf=[4minkscape.pdf[0m[36m | inkscape | inkscape --export-text-to-path[0m
|
||||
|
||||
[32mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
- Export an SVG document to PDF, converting all texts to paths:
|
||||
|
||||
`inkscape {{filename.svg}} --export-pdf={{filename.pdf}} --export-text-to-path`
|
||||
`inkscape {{filename.svg}} | inkscape | inkscape --export-pdf={{inkscape.pdf}} | inkscape | inkscape --export-text-to-path`
|
||||
|
||||
- Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Export a single object, given its ID, into a bitmap:
|
|||
|
||||
Export an SVG document to PDF, converting all texts to paths:
|
||||
|
||||
inkscape {{filename.svg}} --export-pdf={{filename.pdf}} --export-text-to-path
|
||||
inkscape {{filename.svg}} | inkscape | inkscape --export-pdf={{inkscape.pdf}} | inkscape | inkscape --export-text-to-path
|
||||
|
||||
Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:
|
||||
|
||||
|
|
|
|||
28
tests/inkscape-with-config.expected
Normal file
28
tests/inkscape-with-config.expected
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
An SVG (Scalable Vector Graphics) editing program.
|
||||
Use -z to not open the GUI and only process files in the console.
|
||||
|
||||
[44;30mOpen an SVG file in the Inkscape GUI:[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m
|
||||
|
||||
[44;30mExport an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI):[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m -e [4mfilename.png[0m
|
||||
|
||||
[44;30mExport an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur):[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m -e [4mfilename.png[0m -w [4m600[0m -h [4m400[0m
|
||||
|
||||
[44;30mExport a single object, given its ID, into a bitmap:[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m -i [4mid[0m -e [4mobject.png[0m
|
||||
|
||||
[44;30mExport an SVG document to PDF, converting all texts to paths:[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m | inkscape | inkscape --export-pdf=[4minkscape.pdf[0m | inkscape | inkscape --export-text-to-path
|
||||
|
||||
[44;30mDuplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape:[0m
|
||||
|
||||
inkscape [4mfilename.svg[0m --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit
|
||||
|
||||
59
tests/lib.rs
59
tests/lib.rs
|
|
@ -11,21 +11,25 @@ use tempdir::TempDir;
|
|||
|
||||
struct TestEnv {
|
||||
pub cache_dir: TempDir,
|
||||
pub config_dir: TempDir,
|
||||
pub input_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Self {
|
||||
let cache_dir = TempDir::new(".tldr.test.cache").unwrap();
|
||||
let input_dir = TempDir::new(".tldr.test.input").unwrap();
|
||||
TestEnv { cache_dir, input_dir }
|
||||
TestEnv {
|
||||
cache_dir: TempDir::new(".tldr.test.cache").unwrap(),
|
||||
config_dir: TempDir::new(".tldr.test.config").unwrap(),
|
||||
input_dir: TempDir::new(".tldr.test.input").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a new [`Assert`](../assert_cli/struct.Assert.html) instance for
|
||||
/// the main binary with env vars set.
|
||||
fn assert(&self) -> Assert {
|
||||
let env = Environment::inherit()
|
||||
.insert("TEALDEER_CACHE_DIR", self.cache_dir.path().to_str().unwrap());
|
||||
.insert("TEALDEER_CACHE_DIR", self.cache_dir.path().to_str().unwrap())
|
||||
.insert("TEALDEER_CONFIG_DIR", self.config_dir.path().to_str().unwrap());
|
||||
Assert::main_binary()
|
||||
.with_env(env)
|
||||
}
|
||||
|
|
@ -37,7 +41,7 @@ fn test_missing_cache() {
|
|||
.assert()
|
||||
.with_args(&["sl"])
|
||||
.fails()
|
||||
.stdout().contains("Cache not found. Please run `tldr --update`.")
|
||||
.stderr().contains("Cache not found. Please run `tldr --update`.")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +52,7 @@ fn test_update_cache() {
|
|||
testenv.assert()
|
||||
.with_args(&["sl"])
|
||||
.fails()
|
||||
.stdout().contains("Cache not found. Please run `tldr --update`.")
|
||||
.stderr().contains("Cache not found. Please run `tldr --update`.")
|
||||
.unwrap();
|
||||
|
||||
testenv.assert()
|
||||
|
|
@ -63,6 +67,17 @@ fn test_update_cache() {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setup_seed_config() {
|
||||
let testenv = TestEnv::new();
|
||||
|
||||
testenv.assert()
|
||||
.with_args(&["--seed-config"])
|
||||
.succeeds()
|
||||
.stdout().contains("Successfully created seed config file")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn _test_correct_rendering(input_file: &str, filename: &str) {
|
||||
let testenv = TestEnv::new();
|
||||
|
||||
|
|
@ -73,7 +88,7 @@ fn _test_correct_rendering(input_file: &str, filename: &str) {
|
|||
file.write_all(input_file.as_bytes()).unwrap();
|
||||
|
||||
// Load expected output
|
||||
let expected = include_str!("inkscape.expected");
|
||||
let expected = include_str!("inkscape-default.expected");
|
||||
|
||||
testenv.assert()
|
||||
.with_args(&["-f", &file_path.to_str().unwrap()])
|
||||
|
|
@ -93,3 +108,33 @@ fn test_correct_rendering_v1() {
|
|||
fn test_correct_rendering_v2() {
|
||||
_test_correct_rendering(include_str!("inkscape-v2.md"), "inkscape-v2.md");
|
||||
}
|
||||
|
||||
/// An end-to-end integration test for rendering with constom syntax config.
|
||||
#[test]
|
||||
fn test_correct_rendering_with_config() {
|
||||
let testenv = TestEnv::new();
|
||||
|
||||
// Setup config file
|
||||
// TODO should be config::CONFIG_FILE_NAME
|
||||
let config_file_path = testenv.config_dir.path().join("config.toml");
|
||||
println!("Config path: {:?}", &config_file_path);
|
||||
|
||||
let mut config_file = File::create(&config_file_path).unwrap();
|
||||
config_file.write(include_str!("config.toml").as_bytes()).unwrap();
|
||||
|
||||
// Create input file
|
||||
let file_path = testenv.input_dir.path().join("inkscape-v2.md");
|
||||
println!("Testfile path: {:?}", &file_path);
|
||||
|
||||
let mut file = File::create(&file_path).unwrap();
|
||||
file.write_all(include_str!("inkscape-v2.md").as_bytes()).unwrap();
|
||||
|
||||
// Load expected output
|
||||
let expected = include_str!("inkscape-with-config.expected");
|
||||
|
||||
testenv.assert()
|
||||
.with_args(&["-f", &file_path.to_str().unwrap()])
|
||||
.succeeds()
|
||||
.stdout().is(expected)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue