Skip to content

Commit

Permalink
Add Documentation functionality (#6)
Browse files Browse the repository at this point in the history
Co-authored-by: ThomasLaPiana <tlapiana+github@pm.me>
  • Loading branch information
ThomasLaPiana and ThomasLaPiana authored Dec 30, 2023
1 parent 4de7b96 commit b111983
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 26 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ colored = "2.0.4"
rayon = "1.8.0"
serde = { version = "1.0.188", features = ["derive"] }
serde_yaml = "0.9.25"
termimad = "0.26.1"
webbrowser = "0.8.12"

[dev-dependencies]
assert_cmd = "2.0.12"
Expand Down
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ See [synthesizer](https://github.com/ThomasLaPiana/synthesizer) for an example o
- [Video Walkthrough](#video-walkthrough)
- [Installation](#installation)
- [Roxfile Syntax](#roxfile-syntax)
- [Docs](#docs)
- [Templates](#templates)
- [Tasks](#tasks)
- [Pipelines](#pipelines)
Expand All @@ -26,12 +27,12 @@ See [synthesizer](https://github.com/ThomasLaPiana/synthesizer) for an example o

Rox was created for the purpose of making building and developing applications easier. It is designed to focus on extensiblity, performance, and documentation. Here are a few of the key features that help Rox achieve that goal:

- __Dynamically Generated CLI__: Rox's tasks and pipelines are dynamically added as subcommands to the CLI at runtime. Configuration is handled entirely in YAML files.
- __Powerful Primitives__: Using a combination of Rox's primitives (`Tasks`, `Pipelines` and `Templates`) it is possible to handle virtually any use-case with elegance and minimal boilerplate.
- __Documentation as a First-Class Feature__: Names and descriptions are automatically injected into the CLI at runtime, so your `help` command is always accurate. This helps developers understand what various tasks and pipelines do without needing to dive into the code.
- __Performant__: Minimal overhead and native executables for a variety of architectures and operating systems.
- __Efficient__: By utilizing pipeline stages and parallel execution, developers are empowered to make use of multi-core machines to speed up build and development tasks.
- __User-Friendly__: Task results are shown to the user in an easy-to-consume table format along with useful metadata. This makes debugging easier, and shows potential bottlenecks in build steps.
- **Dynamically Generated CLI**: Rox's tasks and pipelines are dynamically added as subcommands to the CLI at runtime. Configuration is handled entirely in YAML files.
- **Primitives**: Using a combination of Rox's primitives (`Tasks`, `Pipelines` and `Templates`) it is possible to handle virtually any use-case with elegance and minimal boilerplate.
- **Documentation as a First-Class Feature**: Names and descriptions are automatically injected into the CLI at runtime, so your `help` command is always accurate. This helps developers understand what various tasks and pipelines do without needing to dive into the code.
- **Performant**: Minimal overhead and native executables for a variety of architectures and operating systems.
- **Efficient**: By utilizing pipeline stages and parallel execution, developers are empowered to make use of multi-core machines to speed up build and development tasks.
- **User-Friendly**: Task results are shown to the user in an easy-to-consume table format along with useful metadata. This makes debugging easier, and shows potential bottlenecks in build steps.

## Video Walkthrough

Expand All @@ -47,6 +48,22 @@ Rox can be installed via binaries provided with each release [here](https://gith

Rox requires a `YAML` file with the correct format and syntax to be parsed into a CLI. This file is expected to be at `./roxfile.yml` by default but that can be overriden with the `-f` flag at runtime.

### Docs

Specifying `docs` within your `roxfile` allows you to keep track of various documentation for your project, with multiple supported formats usable via the `kind` field. The supported values are as follows:

- url -> Opens a webbrowser pointed at the `URL` provided in the `path`
- markdown -> Opens the file at `path` in a special in-terminal Markdown viewer. This allows the developer to navigate around a Markdown document without leaving the terminal.
- text -> Prints a text file to the terminal.

```yaml
docs:
- name: testing
description: Docs around testing
kind: markdown
path: docs/testing.md
```
### Templates
Templates allow you to specify templated commands that can be reused by `tasks`. Values are injected positionally. These are intended to facilitate code reuse and uniformity across similar but different commands.
Expand Down
3 changes: 3 additions & 0 deletions docs/dev_docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Development

These docs are designed to help you get up-to-speed with the project's development workflows and processes.
3 changes: 3 additions & 0 deletions docs/dev_docs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
These are some dev docs.

No need for formatting here :shrug:
21 changes: 21 additions & 0 deletions roxfile.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
docs:
- name: "markdown"
description: "Development-related documentation"
kind: markdown
path: "docs/dev_docs.md"

- name: "readme"
description: "Development-related documentation"
kind: markdown
path: "README.md"

- name: "text"
description: "Development-related documentation"
kind: text
path: "docs/dev_docs.txt"

- name: "url"
description: "Development-related documentation"
kind: url
path: "http://google.com"

templates:
- name: docker_build
command: "docker build {path} -t rox:{image_tag}"
Expand Down
35 changes: 24 additions & 11 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
use crate::models::{Pipeline, Task};
use crate::models::{Docs, Pipeline, Task};
use clap::{crate_version, Arg, ArgAction, Command};

/// Dyanmically construct the CLI from the Roxfile
pub fn construct_cli(tasks: &[Task], pipelines: &Option<Vec<Pipeline>>) -> clap::Command {
pub fn construct_cli(
tasks: &[Task],
pipelines: &Option<Vec<Pipeline>>,
docs: &Option<Vec<Docs>>,
) -> clap::Command {
let mut cli = cli_builder();

// Docs
if let Some(docs) = docs {
let docs_subcommands = build_docs_subcommands(docs);
cli = cli.subcommands(vec![docs_subcommands]);
}

// Tasks
let task_subcommands = build_task_subcommands(tasks);
cli = cli.subcommands(vec![task_subcommands]);
Expand All @@ -25,22 +35,13 @@ pub fn cli_builder() -> Command {
.arg_required_else_help(true)
.allow_external_subcommands(true)
// TODO: Add a "watch" flag to run the command on file changes to a path?
// TODO: Add the option to log the command outputs into a file?
.arg(
Arg::new("roxfile")
.long("file")
.short('f')
.default_value("roxfile.yml")
.help("Path to a Roxfile"),
)
.arg(
Arg::new("skip-checks")
.long("skip-checks")
.short('s')
.required(false)
.action(ArgAction::SetTrue)
.help("Skip the version and file requirement checks."),
)
.subcommand(
Command::new("logs")
.about("View logs for Rox invocations.")
Expand All @@ -55,6 +56,18 @@ pub fn cli_builder() -> Command {
)
}

pub fn build_docs_subcommands(docs: &[Docs]) -> Command {
let subcommands: Vec<Command> = docs
.iter()
.map(|doc| Command::new(&doc.name).about(doc.description.clone().unwrap_or_default()))
.collect();

Command::new("docs")
.about("Display various kinds of documentation.")
.arg_required_else_help(true)
.subcommands(subcommands)
}

/// Build the `task` subcommand with individual tasks nested as subcommands
pub fn build_task_subcommands(tasks: &[Task]) -> Command {
let subcommands: Vec<Command> = tasks
Expand Down
107 changes: 107 additions & 0 deletions src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::models::{Docs, DocsKind};
use std::io::{stdout, Write};
use termimad::crossterm::{
cursor::{Hide, Show},
event::{self, Event, KeyCode, KeyEvent},
queue,
style::Color::*,
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use termimad::MadSkin;
use termimad::*;

static KEYBINDINGS: &str = r#"
# Navigation Keybindings
| Key(s) | Action |
| :---: | :------: |
| q/Esc | Exit |
| k/Up | Scroll up one line|
| j/Down | Scroll down one line |
| g | Jump to top |
| G | Jump to Bottom |
| u | Page Up |
| d | Page Down |
------
"#;

pub fn display_docs(docs: &Docs) {
match docs.kind {
DocsKind::Markdown => {
let markdown = std::fs::read_to_string(&docs.path).unwrap();
run_app(&markdown).unwrap();
}
DocsKind::Text => {
let contents = std::fs::read_to_string(&docs.path).unwrap();
println!("{}", contents);
}
DocsKind::URL => {
println!("> Opening '{}' in your browser...", docs.path);
webbrowser::open(&docs.path).unwrap()
}
}
}

/// Build and Run the terminal application
/// Taken from -> https://github.com/Canop/termimad/blob/main/examples/scrollable/main.rs
fn run_app(docs: &str) -> Result<(), Error> {
let mut w = stdout(); // we could also have used stderr
let skin = make_skin();
queue!(w, EnterAlternateScreen)?;
terminal::enable_raw_mode()?;
queue!(w, Hide)?; // hiding the cursor

let markdown = format!("{}\n{}", KEYBINDINGS, docs);
let mut view = MadView::from(markdown, view_area(), skin);
loop {
view.write_on(&mut w)?;
w.flush()?;
match event::read() {
Ok(Event::Key(KeyEvent { code, .. })) => match code {
// Negative number is up, Positive number is down
KeyCode::Up => view.try_scroll_lines(-1),
KeyCode::Down => view.try_scroll_lines(1),
KeyCode::Char('k') => view.try_scroll_lines(-1),
KeyCode::Char('j') => view.try_scroll_lines(1),

KeyCode::Char('u') => view.try_scroll_lines(-30),
KeyCode::Char('d') => view.try_scroll_lines(30),

KeyCode::Char('g') => view.try_scroll_lines(-100000),
KeyCode::Char('G') => view.try_scroll_lines(100000),

KeyCode::Char('q') => break,
KeyCode::Esc => break,
_ => continue,
},
Ok(Event::Resize(..)) => {
queue!(w, Clear(ClearType::All))?;
view.resize(&view_area());
}
_ => {}
}
}
terminal::disable_raw_mode()?;
queue!(w, Show)?; // we must restore the cursor
queue!(w, LeaveAlternateScreen)?;
w.flush()?;
Ok(())
}

fn make_skin() -> MadSkin {
let mut skin = MadSkin::default();
skin.table.align = Alignment::Center;
skin.set_headers_fg(AnsiValue(178));
skin.bold.set_fg(Yellow);
skin.italic.set_fg(Magenta);
skin.scrollbar.thumb.set_fg(AnsiValue(178));
skin.code_block.align = Alignment::Center;
skin
}

fn view_area() -> Area {
let mut area = Area::full_screen();
area.pad_for_max_width(120); // we don't want a too wide text column
area
}
27 changes: 20 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod cli;
mod docs;
mod execution;
mod model_injection;
pub mod models;
Expand Down Expand Up @@ -46,7 +47,8 @@ pub fn rox() -> RoxResult<()> {
// Build & Generate the CLI based on the loaded Roxfile
let tasks = inject_task_metadata(roxfile.tasks, &file_path);
let pipelines = inject_pipeline_metadata(roxfile.pipelines);
let cli = construct_cli(&tasks, &pipelines);
let docs = roxfile.docs;
let cli = construct_cli(&tasks, &pipelines, &docs);
let cli_matches = cli.get_matches();

// Build Hashmaps for Tasks, Templates and Pipelines
Expand All @@ -68,24 +70,35 @@ pub fn rox() -> RoxResult<()> {
})
.map(|task| (task.name.to_owned(), task)),
);
let pipeline_map: HashMap<String, models::Pipeline> = std::collections::HashMap::from_iter(
pipelines
.into_iter()
.flatten()
.map(|pipeline| (pipeline.name.to_owned(), pipeline)),
);

// Deconstruct the CLI commands and get the Pipeline object that was called
let (_, args) = cli_matches.subcommand().unwrap();
let subcommand_name = args.subcommand_name().unwrap_or("default");

// Execute the Task(s)
let results: Vec<Vec<TaskResult>> = match cli_matches.subcommand_name().unwrap() {
"docs" => {
let docs_map: HashMap<String, models::Docs> = std::collections::HashMap::from_iter(
docs.into_iter()
.flatten()
.map(|doc| (doc.name.to_owned(), doc)),
);
docs::display_docs(docs_map.get(subcommand_name).unwrap());
std::process::exit(0);
}
"logs" => {
let number = args.get_one::<i8>("number").unwrap();
output::display_logs(number);
std::process::exit(0);
}
"pl" => {
let pipeline_map: HashMap<String, models::Pipeline> =
std::collections::HashMap::from_iter(
pipelines
.into_iter()
.flatten()
.map(|pipeline| (pipeline.name.to_owned(), pipeline)),
);
let parallel = args.get_flag("parallel");
let execution_results = execute_stages(
&pipeline_map.get(subcommand_name).unwrap().stages,
Expand Down
17 changes: 17 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ impl std::fmt::Display for PassFail {
}
}

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DocsKind {
Markdown,
Text,
URL,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Docs {
pub name: String,
pub description: Option<String>,
pub kind: DocsKind,
pub path: String,
}

#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
pub struct TaskResult {
pub name: String,
Expand Down Expand Up @@ -162,6 +178,7 @@ pub struct Pipeline {
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct RoxFile {
pub docs: Option<Vec<Docs>>,
pub tasks: Vec<Task>,
pub pipelines: Option<Vec<Pipeline>>,
pub templates: Option<Vec<Template>>,
Expand Down
2 changes: 0 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ pub fn parse_file_contents(contents: String) -> RoxFile {
}

pub enum ColorEnum {
Green,
Red,
}

Expand All @@ -30,7 +29,6 @@ where
.fold("".to_string(), |x, y| format!("{}{}", x, y));

match color {
ColorEnum::Green => println!("{}", concat_output.green()),
ColorEnum::Red => println!("{}", concat_output.red()),
}
}
Expand Down

0 comments on commit b111983

Please sign in to comment.