├── .gitignore ├── array.json ├── src ├── lib.rs ├── main.rs ├── input.rs ├── utils.rs ├── table │ ├── style.rs │ ├── cell.rs │ └── mod.rs ├── application.rs └── data.rs ├── .github ├── images │ └── logo.png └── workflows │ └── release.yml ├── scripts ├── bootstrap.sh └── release.sh ├── completions ├── fish │ └── tv.fish ├── zsh │ └── _tv └── bash │ └── tv.bash ├── .commit_template ├── Cargo.toml ├── LICENSE ├── cliff.toml ├── README.md ├── example.json ├── CHANGELOG.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /array.json: -------------------------------------------------------------------------------- 1 | [1, 2, 3, 4] 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod table; 3 | pub(crate) mod utils; 4 | -------------------------------------------------------------------------------- /.github/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uzimaru0000/tv/HEAD/.github/images/logo.png -------------------------------------------------------------------------------- /scripts/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo install toml-cli 4 | cargo install semver-cli 5 | cargo install git-cliff 6 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::application::Application; 2 | use anyhow::Result; 3 | 4 | mod application; 5 | mod input; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let mut app = Application::new(); 10 | app.run().await 11 | } 12 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tokio::io::{stdin, AsyncReadExt, BufReader}; 3 | 4 | pub async fn read( 5 | reader: &mut BufReader, 6 | ) -> Result { 7 | let mut buf = String::new(); 8 | reader.read_to_string(&mut buf).await?; 9 | Ok(buf) 10 | } 11 | 12 | pub async fn read_stdin() -> Result { 13 | let mut reader = BufReader::new(stdin()); 14 | read(&mut reader).await 15 | } 16 | 17 | pub fn is_pipe() -> bool { 18 | atty::isnt(atty::Stream::Stdin) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TARGET=${1:-patch} 4 | 5 | CURRENT_VER=`toml get Cargo.toml package | jq -r '.version'` 6 | NEXT_VER=`semver-cli $CURRENT_VER --increment $TARGET` 7 | CARGO=`toml set Cargo.toml package.version $NEXT_VER` 8 | 9 | echo "$CARGO" > Cargo.toml 10 | 11 | cargo c 12 | git-cliff --output CHANGELOG.md --tag v$NEXT_VER 13 | 14 | git add Cargo.toml Cargo.lock CHANGELOG.md 15 | git commit -m ":bookmark: release v$NEXT_VER" 16 | git tag -a v$NEXT_VER -m "release v$NEXT_VER" 17 | 18 | cargo package 19 | cargo publish 20 | -------------------------------------------------------------------------------- /completions/fish/tv.fish: -------------------------------------------------------------------------------- 1 | complete -c tv -l style -d 'Table style' -r -f -a "ascii sharp rounded markdown plane" 2 | complete -c tv -s a -l align -d 'Table alignment' -r -f -a "left center right" 3 | complete -c tv -s s -l sort -d 'Options for sorting by key' -r -f 4 | complete -c tv -l no-headers -d 'Specify that the input has no header row' 5 | complete -c tv -s r -l recursive -d 'Recursive display' 6 | complete -c tv -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' 7 | complete -c tv -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' 8 | -------------------------------------------------------------------------------- /.commit_template: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ==================== Emojis ==================== 4 | # ✨ :sparkles: New Feature 5 | # 🐛 :bug: Bugfix 6 | # ♻️ :recycle: Refactoring 7 | # 🐎 :horse: Performance 8 | # 📚 :books: Documentation 9 | # 🔧 :wrench: Tooling 10 | # 🚨 :rotating_light: Tests 11 | # 💩 :hankey: Deprecation 12 | # 🗑️ :wastebasket: Removal 13 | # 🚧 :construction: WIP(Work In Progress) 14 | # ✎ :pencil2: Typo Fix 15 | # 👷 :construction_worker: Adding CI System 16 | # 💚 :green_heart: Fixing CI Build 17 | # 🩹 :adhesive_bandage: Simple fix for a non-critical issue 18 | # watch more (https://gitmoji.carloscuesta.me) 19 | 20 | 21 | # ==================== Format ==================== 22 | # :emoji: Subject 23 | # 24 | # Commit body... 25 | -------------------------------------------------------------------------------- /completions/zsh/_tv: -------------------------------------------------------------------------------- 1 | #compdef tv 2 | 3 | autoload -U is-at-least 4 | 5 | _tv() { 6 | typeset -a _arguments_options 7 | 8 | if is-at-least 5.2; then 9 | _arguments_options=(-s -S -C) 10 | else 11 | _arguments_options=(-s -C) 12 | fi 13 | 14 | _arguments "${_arguments_options[@]}" \ 15 | '-a+[Table alignment]' \ 16 | '--align=[Table alignment]: :(left center right)' \ 17 | '--style=[Specify the border style]: :(ascii sharp rounded larkdown)' \ 18 | '-s+[Options for sorting by key]' \ 19 | '--sort=[Options for sorting by key]' \ 20 | '-r[Recursive display]' \ 21 | '--recursive[Recursive display]' \ 22 | '--no-headers[Specify that the input has no header row]' \ 23 | '-h[Prints help information]' \ 24 | '--help[Prints help information]' \ 25 | '-V[Prints version information]' \ 26 | '--version[Prints version information]' \ 27 | '::FILE -- json file path:_files' \ 28 | } 29 | 30 | compdef _tv tv 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tv-cli" 3 | version = "0.7.0" 4 | authors = ["uzimaru0000 "] 5 | edition = "2018" 6 | description = "Format json into table view" 7 | homepage = "https://github.com/uzimaru0000/tv" 8 | keywords = [ 9 | "json", 10 | "viewer", 11 | "tool", 12 | ] 13 | readme = "README.md" 14 | license = "MIT" 15 | repository = "https://github.com/uzimaru0000/tv" 16 | exclude = [ 17 | ".github/*", 18 | "scripts/*", 19 | "array.json", 20 | "example.json", 21 | "cliff.toml" 22 | ] 23 | 24 | [[bin]] 25 | name = "tv" 26 | path = "src/main.rs" 27 | 28 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 29 | 30 | [dependencies] 31 | anyhow = "1.0.43" 32 | csv = "1.1.6" 33 | serde = { version = "1.0.129", features = ["derive"] } 34 | serde_json = { version = "1.0.66", features = ["preserve_order"] } 35 | tokio = { version = "1.10.1", features = ["full"] } 36 | unicode-width = "0.1.7" 37 | clap = "2.33.3" 38 | atty = "0.2.14" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shuji Oba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn multi_lines(str_vec: Vec>) -> Vec> { 2 | let split_values = str_vec.iter().map(|x| x.iter().map(|x| x.split("\n"))); 3 | let mapper: Vec> = split_values 4 | .clone() 5 | .map(|xs| { 6 | ( 7 | xs.len(), 8 | xs.map(|x| x.clone().collect::>().len()) 9 | .max() 10 | .unwrap_or_default(), 11 | ) 12 | }) 13 | .enumerate() 14 | .flat_map(|(idx, (h, v))| { 15 | (0..v).map(move |y| (0..h).map(move |x| (idx, x, y)).collect::>()) 16 | }) 17 | .collect(); 18 | let fields = mapper 19 | .iter() 20 | .map(|xs| { 21 | xs.iter() 22 | .map(|&(idx, ix, iy)| { 23 | split_values 24 | .clone() 25 | .nth(idx) 26 | .and_then(|x| x.clone().nth(ix)) 27 | .and_then(|x| x.clone().nth(iy)) 28 | .unwrap_or_default() 29 | }) 30 | .collect::>() 31 | }) 32 | .collect::>(); 33 | 34 | fields 35 | .into_iter() 36 | .map(|xs| xs.into_iter().map(String::from).collect::>()) 37 | .collect() 38 | } 39 | -------------------------------------------------------------------------------- /completions/bash/tv.bash: -------------------------------------------------------------------------------- 1 | _tv() { 2 | local i cur prev opts cmds 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | cmd="" 7 | opts="" 8 | 9 | for i in ${COMP_WORDS[@]} 10 | do 11 | case "${i}" in 12 | tv) 13 | cmd="tv" 14 | ;; 15 | 16 | *) 17 | ;; 18 | esac 19 | done 20 | 21 | case "${cmd}" in 22 | tv) 23 | opts=" -h -V -r -s -a --help --version --recursive --sort --align --style " 24 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 25 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 26 | return 0 27 | fi 28 | case "${prev}" in 29 | --style) 30 | COMPREPLY=($(compgen -W "ascii sharp rounded markdown plane" -- "${cur}")) 31 | return 0 32 | ;; 33 | 34 | --align) 35 | COMPREPLY=($(compgen -W "left center right" -- "${cur}")) 36 | return 0 37 | ;; 38 | *) 39 | COMPREPLY=() 40 | ;; 41 | esac 42 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 43 | return 0 44 | ;; 45 | 46 | esac 47 | } 48 | 49 | complete -F _tv -o bashdefault -o default tv -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version }}](https://github.com/uzimaru0000/tv/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/uzimaru0000/tv/commit/{{ commit.id }}))\ 21 | {% endfor %} 22 | {% endfor %}\n 23 | """ 24 | # remove the leading and trailing whitespaces from the template 25 | trim = true 26 | # changelog footer 27 | footer = """ 28 | 29 | """ 30 | 31 | [git] 32 | # allow only conventional commits 33 | # https://www.conventionalcommits.org 34 | conventional_commits = false 35 | # regex for parsing and grouping commits 36 | commit_parsers = [ 37 | { message = "^:sparkles:*", group = "Features"}, 38 | { message = "^:bug:*", group = "Bug Fixes"}, 39 | { message = "^:books:*", group = "Documentation"}, 40 | { message = "^:horse:*", group = "Performance"}, 41 | { message = "^:recycle:*", group = "Refactor"}, 42 | { message = "^:arts:*", group = "Styling"}, 43 | { message = "^:rotating_light:*", group = "Testing"}, 44 | { message = "^:bookmark:*", skip = true}, 45 | { message = "^:adhesive_bandage:*", group = "Miscellaneous Tasks"}, 46 | { body = ".*security", group = "Security"}, 47 | ] 48 | # filter out the commits that are not matched by commit parsers 49 | filter_commits = false 50 | # glob pattern for matching git tags 51 | tag_pattern = "v[0-9]*" 52 | # regex for skipping tags 53 | skip_tags = "v0.1.0-beta.1" 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

:tv: tv

2 | 3 |

4 | 5 |

6 | 7 |

Format json into table view

8 | 9 | [![](https://img.shields.io/github/license/uzimaru0000/tv?style=for-the-badge)](https://github.com/uzimaru0000/tv/blob/master/LICENSE) 10 | [![](https://img.shields.io/github/v/release/uzimaru0000/tv?style=for-the-badge)](https://github.com/uzimaru0000/tv/releases/latest) 11 | ![](https://img.shields.io/github/downloads/uzimaru0000/tv/total?style=for-the-badge) 12 | 13 | ## How to install 14 | 15 | ### For MacOS 16 | 17 | ```bash 18 | $ brew install uzimaru0000/tap/tv 19 | ``` 20 | 21 | ### Use cargo 22 | 23 | ```bash 24 | $ cargo install tv-cli 25 | ``` 26 | 27 | ## How to use 28 | 29 | ### USAGE 30 | ``` 31 | tv [FLAGS] [OPTIONS] [PATH] 32 | ``` 33 | 34 | ### FLAGS 35 | ``` 36 | -h, --help Prints help information 37 | --no-headers Specify that the input has no header row 38 | -r, --recursive Recursive display 39 | -V, --version Prints version information 40 | ``` 41 | 42 | ### OPTIONS 43 | ``` 44 | -a, --align Table alignment [default: none] 45 | -s, --sort Options for sorting by key 46 | --style Table style [default: ascii] 47 | ``` 48 | 49 | ### ARGS 50 | ``` 51 | json file path 52 | ``` 53 | 54 | ### Example 55 | 56 | ``` 57 | $ cat example.json 58 | [ 59 | { 60 | "id": 1, 61 | "name": "Leanne Graham", 62 | "username": "Bret", 63 | "email": "Sincere@april.biz", 64 | "address": { 65 | "street": "Kulas Light", 66 | "suite": "Apt. 556", 67 | "city": "Gwenborough", 68 | "zipcode": "92998-3874", 69 | "geo": { 70 | "lat": "-37.3159", 71 | "lng": "81.1496" 72 | } 73 | }, 74 | "phone": "1-770-736-8031 x56442", 75 | "website": "hildegard.org", 76 | "company": { 77 | "name": "Romaguera-Crona", 78 | "catchPhrase": "Multi-layered client-server neural-net", 79 | "bs": "harness real-time e-markets" 80 | } 81 | }, 82 | ... 83 | ] 84 | 85 | $ tv example.json 86 | +--+------------------------+----------------+-------------------------+-------+---------------------+-------------+-------+ 87 | |id|name |username |email |address|phone |website |company| 88 | +--+------------------------+----------------+-------------------------+-------+---------------------+-------------+-------+ 89 | |1 |Leanne Graham |Bret |Sincere@april.biz |... |1-770-736-8031 x56442|hildegard.org|... | 90 | |2 |Ervin Howell |Antonette |Shanna@melissa.tv |... |010-692-6593 x09125 |anastasia.net|... | 91 | |3 |Clementine Bauch |Samantha |Nathan@yesenia.net |... |1-463-123-4447 |ramiro.info |... | 92 | |4 |Patricia Lebsack |Karianne |Julianne.OConner@kory.org|... |493-170-9623 x156 |kale.biz |... | 93 | |5 |Chelsey Dietrich |Kamren |Lucio_Hettinger@annie.ca |... |(254)954-1289 |demarco.info |... | 94 | |6 |Mrs. Dennis Schulist |Leopoldo_Corkery|Karley_Dach@jasper.info |... |1-477-935-8478 x6430 |ola.org |... | 95 | |7 |Kurtis Weissnat |Elwyn.Skiles |Telly.Hoeger@billy.biz |... |210.067.6132 |elvis.io |... | 96 | |8 |Nicholas Runolfsdottir V|Maxime_Nienow |Sherwood@rosamond.me |... |586.493.6943 x140 |jacynthe.com |... | 97 | |9 |Glenna Reichert |Delphine |Chaim_McDermott@dana.io |... |(775)976-6794 x41206 |conrad.com |... | 98 | |10|Clementina DuBuque |Moriah.Stanton |Rey.Padberg@karina.biz |... |024-648-3804 |ambrose.net |... | 99 | +--+------------------------+----------------+-------------------------+-------+---------------------+-------------+-------+ 100 | 101 | ``` 102 | -------------------------------------------------------------------------------- /src/table/style.rs: -------------------------------------------------------------------------------- 1 | use super::cell::Align; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub enum Style { 5 | Plain, 6 | Ascii, 7 | Sharp, 8 | Rounded, 9 | Markdown, 10 | } 11 | 12 | impl Style { 13 | pub fn new(s: String) -> Self { 14 | match s.to_lowercase().as_str() { 15 | "plane" => Self::Plain, // deprecated 16 | "plain" => Self::Plain, 17 | "ascii" => Self::Ascii, 18 | "sharp" => Self::Sharp, 19 | "rounded" => Self::Rounded, 20 | "markdown" => Self::Markdown, 21 | _ => Self::Ascii, 22 | } 23 | } 24 | } 25 | 26 | pub struct Frame { 27 | pub has_cover: bool, 28 | pub border: String, 29 | pub separator: String, 30 | pub center: String, 31 | pub top: String, 32 | pub left: String, 33 | pub bottom: String, 34 | pub right: String, 35 | pub top_left: String, 36 | pub top_right: String, 37 | pub bottom_left: String, 38 | pub bottom_right: String, 39 | } 40 | 41 | impl Frame { 42 | pub fn align_border(&self, align: &Align, width: usize) -> String { 43 | match align { 44 | Align::None => self.border.repeat(width), 45 | Align::Left => format!(":{}", self.border.repeat(width - 1)), 46 | Align::Center => format!(":{}:", self.border.repeat(width - 2)), 47 | Align::Right => format!("{}:", self.border.repeat(width - 1)), 48 | } 49 | } 50 | } 51 | 52 | impl From