├── FUNDING.yml ├── .gitattributes ├── .gitignore ├── rust-toolchain.toml ├── src ├── table │ ├── mod.rs │ ├── row.rs │ ├── style.rs │ └── printer.rs ├── util.rs ├── cli.rs └── main.rs ├── release.toml ├── .github ├── dependabot.yml ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── release.yml │ └── CICD.yml ├── rustfmt.toml ├── Cargo.toml ├── LICENSE-MIT ├── completions ├── fish │ └── csview.fish ├── elvish │ └── csview.elv ├── zsh │ └── _csview ├── bash │ └── csview.bash └── powershell │ └── _csview.ps1 ├── .pre-commit-config.yaml ├── cliff.toml ├── README.md ├── LICENSE-APACHE ├── Cargo.lock └── CHANGELOG.md /FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: wfxr 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /completions/**/* linguist-generated 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /flamegraph.svg 4 | /perf.data 5 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt", "clippy", "rust-analyzer"] 4 | -------------------------------------------------------------------------------- /src/table/mod.rs: -------------------------------------------------------------------------------- 1 | mod printer; 2 | mod row; 3 | mod style; 4 | 5 | pub use printer::TablePrinter; 6 | pub use style::{RowSep, Style, StyleBuilder}; 7 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["master"] 2 | pre-release-hook = ["sh", "-c", "git cliff -o CHANGELOG.md --tag {{version}} && git add CHANGELOG.md"] 3 | pre-release-commit-message = "chore: release {{crate_name}} version {{version}}" 4 | tag-message = "chore: release {{crate_name}} version {{version}}" 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | reviewers: 9 | - wfxr 10 | labels: 11 | - dependencies 12 | commit-message: 13 | prefix: "chore" 14 | prefix-development: "chore" 15 | include: "scope" 16 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 120 3 | reorder_imports = true 4 | struct_lit_width = 60 5 | struct_variant_width = 60 6 | 7 | # struct_field_align_threshold = 40 8 | # comment_width = 120 9 | # fn_single_line = false 10 | # imports_layout = "HorizontalVertical" 11 | # match_arm_blocks = false 12 | # overflow_delimited_expr = true 13 | # imports_granularity = "Crate" 14 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## Check list 6 | 7 | - [ ] I have read through the [README](https://github.com/wfxr/csview/blob/master/README.md) 8 | - [ ] I have searched through the existing issues 9 | 10 | ## Environment info 11 | 12 | - OS 13 | - [ ] Linux 14 | - [ ] Mac OS X 15 | - [ ] Windows 16 | - [ ] Others: 17 | 18 | ## Version 19 | 20 | 21 | 22 | ## Problem / Steps to reproduce 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Check list 4 | 5 | - [ ] I have read through the [README](https://github.com/wfxr/csview/blob/master/README.md) (especially F.A.Q section) 6 | - [ ] I have searched through the existing issues or pull requests 7 | - [ ] I have performed a self-review of my code and commented hard-to-understand areas 8 | - [ ] I have made corresponding changes to the documentation (when necessary) 9 | 10 | ## Description 11 | 12 | 13 | 14 | ## Type of change 15 | 16 | - [ ] Bug fix 17 | - [ ] New feature 18 | - [ ] Refactor 19 | - [ ] Breaking change 20 | - [ ] Documentation change 21 | - [ ] CICD related improvement 22 | 23 | ## Test environment 24 | 25 | - OS 26 | - [ ] Linux 27 | - [ ] Mac OS X 28 | - [ ] Windows 29 | - [ ] Others: 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | changelog: 10 | name: Generate and publish changelog 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Generate a changelog 21 | uses: orhun/git-cliff-action@v3 22 | id: git-cliff 23 | with: 24 | config: cliff.toml 25 | args: -v --latest --strip header 26 | env: 27 | OUTPUT: CHANGES.md 28 | GITHUB_REPO: ${{ github.repository }} 29 | 30 | - name: Polish changelog 31 | shell: bash 32 | run: sed -i '1,2d' CHANGES.md 33 | 34 | - name: Upload the changelog 35 | uses: ncipollo/release-action@v1 36 | with: 37 | # draft: true 38 | allowUpdates: true 39 | bodyFile: CHANGES.md 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "csview" 3 | version = "1.3.4" 4 | authors = ["Wenxuan Zhang "] 5 | description = "A high performance csv viewer with cjk/emoji support." 6 | categories = ["command-line-utilities"] 7 | homepage = "https://github.com/wfxr/csview" 8 | keywords = ["csv", "pager", "viewer", "tool"] 9 | readme = "README.md" 10 | license = "MIT OR Apache-2.0" 11 | exclude = ["/completions"] 12 | repository = "https://github.com/wfxr/csview" 13 | edition = "2021" 14 | build = "build.rs" 15 | 16 | [features] 17 | default = ["pager"] 18 | pager = ["dep:pager"] 19 | 20 | [dependencies] 21 | csv = "1.3" 22 | clap = { version = "4", features = ["wrap_help", "derive"] } 23 | exitcode = "1.1" 24 | anyhow = "1.0" 25 | unicode-width = "0" 26 | unicode-truncate = "2.0" 27 | itertools = "0.14" 28 | [target.'cfg(target_family = "unix")'.dependencies] 29 | pager = { version = "0.16", optional = true } 30 | 31 | [build-dependencies] 32 | clap = { version = "4", features = ["wrap_help", "derive"] } 33 | clap_complete = "4" 34 | 35 | [profile.release] 36 | lto = true 37 | codegen-units = 1 38 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Wenxuan Zhang (https://wfxr.mit-license.org/2020). 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /completions/fish/csview.fish: -------------------------------------------------------------------------------- 1 | complete -c csview -s d -l delimiter -d 'Specify the field delimiter' -r 2 | complete -c csview -s s -l style -d 'Specify the border style' -r -f -a "{none\t'',ascii\t'',ascii2\t'',sharp\t'',rounded\t'',reinforced\t'',markdown\t'',grid\t''}" 3 | complete -c csview -s p -l padding -d 'Specify padding for table cell' -r 4 | complete -c csview -s i -l indent -d 'Specify global indent for table' -r 5 | complete -c csview -l sniff -d 'Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit' -r 6 | complete -c csview -l header-align -d 'Specify the alignment of the table header' -r -f -a "{left\t'',center\t'',right\t''}" 7 | complete -c csview -l body-align -d 'Specify the alignment of the table body' -r -f -a "{left\t'',center\t'',right\t''}" 8 | complete -c csview -s H -l no-headers -d 'Specify that the input has no header row' 9 | complete -c csview -s n -l number -d 'Prepend a column of line numbers to the table' 10 | complete -c csview -s t -l tsv -d 'Use \'\\t\' as delimiter for tsv' 11 | complete -c csview -s P -l disable-pager -d 'Disable pager' 12 | complete -c csview -s h -l help -d 'Print help' 13 | complete -c csview -s V -l version -d 'Print version' 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: check-yaml 9 | - id: check-added-large-files 10 | - id: mixed-line-ending 11 | - id: check-toml 12 | - repo: local 13 | hooks: 14 | - id: cargo-fmt 15 | name: cargo fmt 16 | pass_filenames: false 17 | always_run: true 18 | language: system 19 | entry: cargo fmt 20 | - id: cargo-check 21 | name: cargo check 22 | pass_filenames: false 23 | always_run: true 24 | language: system 25 | entry: cargo check 26 | - id: cargo-clippy 27 | name: cargo clippy 28 | pass_filenames: false 29 | language: system 30 | always_run: true 31 | entry: cargo clippy 32 | args: ["--", "-D", "warnings"] 33 | - id: update-completions 34 | name: update shell completions 35 | pass_filenames: false 36 | language: system 37 | always_run: true 38 | entry: > 39 | sh -c ' 40 | touch build.rs && 41 | SHELL_COMPLETIONS_DIR=completions cargo build && 42 | git add completions 43 | ' 44 | -------------------------------------------------------------------------------- /completions/elvish/csview.elv: -------------------------------------------------------------------------------- 1 | 2 | use builtin; 3 | use str; 4 | 5 | set edit:completion:arg-completer[csview] = {|@words| 6 | fn spaces {|n| 7 | builtin:repeat $n ' ' | str:join '' 8 | } 9 | fn cand {|text desc| 10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc 11 | } 12 | var command = 'csview' 13 | for word $words[1..-1] { 14 | if (str:has-prefix $word '-') { 15 | break 16 | } 17 | set command = $command';'$word 18 | } 19 | var completions = [ 20 | &'csview'= { 21 | cand -d 'Specify the field delimiter' 22 | cand --delimiter 'Specify the field delimiter' 23 | cand -s 'Specify the border style' 24 | cand --style 'Specify the border style' 25 | cand -p 'Specify padding for table cell' 26 | cand --padding 'Specify padding for table cell' 27 | cand -i 'Specify global indent for table' 28 | cand --indent 'Specify global indent for table' 29 | cand --sniff 'Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit' 30 | cand --header-align 'Specify the alignment of the table header' 31 | cand --body-align 'Specify the alignment of the table body' 32 | cand -H 'Specify that the input has no header row' 33 | cand --no-headers 'Specify that the input has no header row' 34 | cand -n 'Prepend a column of line numbers to the table' 35 | cand --number 'Prepend a column of line numbers to the table' 36 | cand -t 'Use ''\t'' as delimiter for tsv' 37 | cand --tsv 'Use ''\t'' as delimiter for tsv' 38 | cand -P 'Disable pager' 39 | cand --disable-pager 'Disable pager' 40 | cand -h 'Print help' 41 | cand --help 'Print help' 42 | cand -V 'Print version' 43 | cand --version 'Print version' 44 | } 45 | ] 46 | $completions[$command] 47 | } 48 | -------------------------------------------------------------------------------- /completions/zsh/_csview: -------------------------------------------------------------------------------- 1 | #compdef csview 2 | 3 | autoload -U is-at-least 4 | 5 | _csview() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" : \ 18 | '-d+[Specify the field delimiter]:DELIMITER:_default' \ 19 | '--delimiter=[Specify the field delimiter]:DELIMITER:_default' \ 20 | '-s+[Specify the border style]:STYLE:(none ascii ascii2 sharp rounded reinforced markdown grid)' \ 21 | '--style=[Specify the border style]:STYLE:(none ascii ascii2 sharp rounded reinforced markdown grid)' \ 22 | '-p+[Specify padding for table cell]:PADDING:_default' \ 23 | '--padding=[Specify padding for table cell]:PADDING:_default' \ 24 | '-i+[Specify global indent for table]:INDENT:_default' \ 25 | '--indent=[Specify global indent for table]:INDENT:_default' \ 26 | '--sniff=[Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit]:LIMIT:_default' \ 27 | '--header-align=[Specify the alignment of the table header]:HEADER_ALIGN:(left center right)' \ 28 | '--body-align=[Specify the alignment of the table body]:BODY_ALIGN:(left center right)' \ 29 | '-H[Specify that the input has no header row]' \ 30 | '--no-headers[Specify that the input has no header row]' \ 31 | '-n[Prepend a column of line numbers to the table]' \ 32 | '--number[Prepend a column of line numbers to the table]' \ 33 | '(-d --delimiter)-t[Use '\''\\t'\'' as delimiter for tsv]' \ 34 | '(-d --delimiter)--tsv[Use '\''\\t'\'' as delimiter for tsv]' \ 35 | '-P[Disable pager]' \ 36 | '--disable-pager[Disable pager]' \ 37 | '-h[Print help]' \ 38 | '--help[Print help]' \ 39 | '-V[Print version]' \ 40 | '--version[Print version]' \ 41 | '::FILE -- File to view:_files' \ 42 | && ret=0 43 | } 44 | 45 | (( $+functions[_csview_commands] )) || 46 | _csview_commands() { 47 | local commands; commands=() 48 | _describe -t commands 'csview commands' commands "$@" 49 | } 50 | 51 | if [ "$funcstack[1]" = "_csview" ]; then 52 | _csview "$@" 53 | else 54 | compdef _csview csview 55 | fi 56 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use cli::Alignment; 2 | 3 | use crate::{ 4 | cli::{self, TableStyle}, 5 | table::{RowSep, Style, StyleBuilder}, 6 | }; 7 | 8 | pub fn table_style( 9 | style: TableStyle, 10 | padding: usize, 11 | indent: usize, 12 | header_align: Alignment, 13 | body_align: Alignment, 14 | ) -> Style { 15 | let builder = match style { 16 | TableStyle::None => StyleBuilder::new().clear_seps(), 17 | TableStyle::Ascii => StyleBuilder::new().col_sep('|').row_seps( 18 | RowSep::new('-', '+', '+', '+'), 19 | RowSep::new('-', '+', '+', '+'), 20 | None, 21 | RowSep::new('-', '+', '+', '+'), 22 | ), 23 | TableStyle::Ascii2 => { 24 | StyleBuilder::new() 25 | .col_seps(' ', '|', ' ') 26 | .row_seps(None, RowSep::new('-', ' ', '+', ' '), None, None) 27 | } 28 | TableStyle::Sharp => StyleBuilder::new().col_sep('│').row_seps( 29 | RowSep::new('─', '┌', '┬', '┐'), 30 | RowSep::new('─', '├', '┼', '┤'), 31 | None, 32 | RowSep::new('─', '└', '┴', '┘'), 33 | ), 34 | TableStyle::Rounded => StyleBuilder::new().col_sep('│').row_seps( 35 | RowSep::new('─', '╭', '┬', '╮'), 36 | RowSep::new('─', '├', '┼', '┤'), 37 | None, 38 | RowSep::new('─', '╰', '┴', '╯'), 39 | ), 40 | TableStyle::Reinforced => StyleBuilder::new().col_sep('│').row_seps( 41 | RowSep::new('─', '┏', '┬', '┓'), 42 | RowSep::new('─', '├', '┼', '┤'), 43 | None, 44 | RowSep::new('─', '┗', '┴', '┛'), 45 | ), 46 | TableStyle::Markdown => { 47 | StyleBuilder::new() 48 | .col_sep('|') 49 | .row_seps(None, RowSep::new('-', '|', '|', '|'), None, None) 50 | } 51 | TableStyle::Grid => StyleBuilder::new().col_sep('│').row_seps( 52 | RowSep::new('─', '┌', '┬', '┐'), 53 | RowSep::new('─', '├', '┼', '┤'), 54 | RowSep::new('─', '├', '┼', '┤'), 55 | RowSep::new('─', '└', '┴', '┘'), 56 | ), 57 | }; 58 | builder 59 | .padding(padding) 60 | .indent(indent) 61 | .header_align(header_align.into()) 62 | .body_align(body_align.into()) 63 | .build() 64 | } 65 | 66 | impl From for unicode_truncate::Alignment { 67 | fn from(a: Alignment) -> Self { 68 | match a { 69 | Alignment::Left => unicode_truncate::Alignment::Left, 70 | Alignment::Center => unicode_truncate::Alignment::Center, 71 | Alignment::Right => unicode_truncate::Alignment::Right, 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::ValueHint; 4 | use clap::{ 5 | builder::{ 6 | styling::{AnsiColor, Effects}, 7 | Styles, 8 | }, 9 | Parser, ValueEnum, 10 | }; 11 | 12 | #[derive(Parser)] 13 | #[clap(about, version)] 14 | #[clap(disable_help_subcommand = true)] 15 | #[clap(next_line_help = true)] 16 | #[clap( 17 | styles(Styles::styled() 18 | .header(AnsiColor::Yellow.on_default() | Effects::BOLD) 19 | .usage(AnsiColor::Yellow.on_default() | Effects::BOLD) 20 | .literal(AnsiColor::Green.on_default() | Effects::BOLD) 21 | .placeholder(AnsiColor::Cyan.on_default()) 22 | ) 23 | )] 24 | pub struct App { 25 | /// File to view. 26 | #[arg(name = "FILE", value_hint = ValueHint::FilePath)] 27 | pub file: Option, 28 | 29 | /// Specify that the input has no header row. 30 | #[arg(short = 'H', long = "no-headers")] 31 | pub no_headers: bool, 32 | 33 | /// Prepend a column of line numbers to the table. 34 | #[arg(short, long, alias = "seq")] 35 | pub number: bool, 36 | 37 | /// Use '\t' as delimiter for tsv. 38 | #[arg(short, long, conflicts_with = "delimiter")] 39 | pub tsv: bool, 40 | 41 | /// Specify the field delimiter. 42 | #[arg(short, long, default_value_t = ',')] 43 | pub delimiter: char, 44 | 45 | /// Specify the border style. 46 | #[arg(short, long, value_enum, default_value_t = TableStyle::Sharp, ignore_case = true)] 47 | pub style: TableStyle, 48 | 49 | /// Specify padding for table cell. 50 | #[arg(short, long, default_value_t = 1)] 51 | pub padding: usize, 52 | 53 | /// Specify global indent for table. 54 | #[arg(short, long, default_value_t = 0)] 55 | pub indent: usize, 56 | 57 | /// Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit. 58 | #[arg(long, default_value_t = 1000, name = "LIMIT")] 59 | pub sniff: usize, 60 | 61 | /// Specify the alignment of the table header. 62 | #[arg(long, value_enum, default_value_t = Alignment::Center, ignore_case = true)] 63 | pub header_align: Alignment, 64 | 65 | /// Specify the alignment of the table body. 66 | #[arg(long, value_enum, default_value_t = Alignment::Left, ignore_case = true)] 67 | pub body_align: Alignment, 68 | 69 | #[cfg(all(feature = "pager", unix))] 70 | /// Disable pager. 71 | #[arg(long, short = 'P')] 72 | pub disable_pager: bool, 73 | } 74 | 75 | #[derive(Copy, Clone, ValueEnum)] 76 | pub enum TableStyle { 77 | None, 78 | Ascii, 79 | Ascii2, 80 | Sharp, 81 | Rounded, 82 | Reinforced, 83 | Markdown, 84 | Grid, 85 | } 86 | 87 | #[derive(Copy, Clone, ValueEnum)] 88 | pub enum Alignment { 89 | Left, 90 | Center, 91 | Right, 92 | } 93 | -------------------------------------------------------------------------------- /completions/bash/csview.bash: -------------------------------------------------------------------------------- 1 | _csview() { 2 | local i cur prev opts cmd 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 "${cmd},${i}" in 12 | ",$1") 13 | cmd="csview" 14 | ;; 15 | *) 16 | ;; 17 | esac 18 | done 19 | 20 | case "${cmd}" in 21 | csview) 22 | opts="-H -n -t -d -s -p -i -P -h -V --no-headers --number --tsv --delimiter --style --padding --indent --sniff --header-align --body-align --disable-pager --help --version [FILE]" 23 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 24 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 25 | return 0 26 | fi 27 | case "${prev}" in 28 | --delimiter) 29 | COMPREPLY=($(compgen -f "${cur}")) 30 | return 0 31 | ;; 32 | -d) 33 | COMPREPLY=($(compgen -f "${cur}")) 34 | return 0 35 | ;; 36 | --style) 37 | COMPREPLY=($(compgen -W "none ascii ascii2 sharp rounded reinforced markdown grid" -- "${cur}")) 38 | return 0 39 | ;; 40 | -s) 41 | COMPREPLY=($(compgen -W "none ascii ascii2 sharp rounded reinforced markdown grid" -- "${cur}")) 42 | return 0 43 | ;; 44 | --padding) 45 | COMPREPLY=($(compgen -f "${cur}")) 46 | return 0 47 | ;; 48 | -p) 49 | COMPREPLY=($(compgen -f "${cur}")) 50 | return 0 51 | ;; 52 | --indent) 53 | COMPREPLY=($(compgen -f "${cur}")) 54 | return 0 55 | ;; 56 | -i) 57 | COMPREPLY=($(compgen -f "${cur}")) 58 | return 0 59 | ;; 60 | --sniff) 61 | COMPREPLY=($(compgen -f "${cur}")) 62 | return 0 63 | ;; 64 | --header-align) 65 | COMPREPLY=($(compgen -W "left center right" -- "${cur}")) 66 | return 0 67 | ;; 68 | --body-align) 69 | COMPREPLY=($(compgen -W "left center right" -- "${cur}")) 70 | return 0 71 | ;; 72 | *) 73 | COMPREPLY=() 74 | ;; 75 | esac 76 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 77 | return 0 78 | ;; 79 | esac 80 | } 81 | 82 | if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then 83 | complete -F _csview -o nosort -o bashdefault -o default csview 84 | else 85 | complete -F _csview -o bashdefault -o default csview 86 | fi 87 | -------------------------------------------------------------------------------- /src/table/row.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Write}; 2 | 3 | use itertools::Itertools; 4 | use unicode_truncate::{Alignment, UnicodeTruncateStr}; 5 | 6 | use crate::table::Style; 7 | 8 | /// Represent a table row made of cells 9 | #[derive(Clone, Debug)] 10 | pub struct Row<'a> { 11 | cells: Vec<&'a str>, 12 | } 13 | 14 | impl<'a> FromIterator<&'a str> for Row<'a> { 15 | fn from_iter>(iter: I) -> Self { 16 | Self { cells: iter.into_iter().collect() } 17 | } 18 | } 19 | 20 | impl Row<'_> { 21 | pub fn write(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> { 22 | let sep = fmt.colseps.mid.map(|c| c.to_string()).unwrap_or_default(); 23 | write!(wtr, "{:indent$}", "", indent = fmt.indent)?; 24 | fmt.colseps.lhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?; 25 | Itertools::intersperse( 26 | self.cells 27 | .iter() 28 | .zip(widths) 29 | .map(|(cell, &width)| cell.unicode_pad(width, align, true)) 30 | .map(|s| format!("{:pad$}{}{:pad$}", "", s, "", pad = fmt.padding)), 31 | sep, 32 | ) 33 | .try_for_each(|s| write!(wtr, "{}", s))?; 34 | fmt.colseps.rhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?; 35 | Ok(()) 36 | } 37 | 38 | pub fn writeln(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> { 39 | self.write(wtr, fmt, widths, align).and_then(|_| writeln!(wtr)) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use super::*; 46 | use anyhow::Result; 47 | use std::str; 48 | 49 | #[test] 50 | fn write_ascii_row() -> Result<()> { 51 | let row = Row::from_iter(["a", "b"]); 52 | let buf = &mut Vec::new(); 53 | let fmt = Style::default(); 54 | let widths = [3, 4]; 55 | 56 | row.writeln(buf, &fmt, &widths, Alignment::Left)?; 57 | assert_eq!("| a | b |\n", str::from_utf8(buf)?); 58 | Ok(()) 59 | } 60 | 61 | #[test] 62 | fn write_cjk_row() -> Result<()> { 63 | let row = Row::from_iter(["李磊(Jack)", "四川省成都市", "💍"]); 64 | let buf = &mut Vec::new(); 65 | let fmt = Style::default(); 66 | let widths = [10, 8, 2]; 67 | 68 | row.writeln(buf, &fmt, &widths, Alignment::Left)?; 69 | assert_eq!("| 李磊(Jack) | 四川省成 | 💍 |\n", str::from_utf8(buf)?); 70 | Ok(()) 71 | } 72 | 73 | #[test] 74 | fn write_align_center() -> Result<()> { 75 | let row = Row::from_iter(["a", "b"]); 76 | let buf = &mut Vec::new(); 77 | let fmt = Style::default(); 78 | let widths = [3, 4]; 79 | 80 | row.writeln(buf, &fmt, &widths, Alignment::Center)?; 81 | assert_eq!("| a | b |\n", str::from_utf8(buf)?); 82 | Ok(()) 83 | } 84 | 85 | #[test] 86 | fn write_align_right() -> Result<()> { 87 | let row = Row::from_iter(["a", "b"]); 88 | let buf = &mut Vec::new(); 89 | let fmt = Style::default(); 90 | let widths = [3, 4]; 91 | 92 | row.writeln(buf, &fmt, &widths, Alignment::Right)?; 93 | assert_eq!("| a | b |\n", str::from_utf8(buf)?); 94 | Ok(()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod table; 3 | mod util; 4 | 5 | use anyhow::bail; 6 | use clap::Parser; 7 | use cli::App; 8 | use csv::{ErrorKind, ReaderBuilder}; 9 | use std::{ 10 | fs::File, 11 | io::{self, BufWriter, IsTerminal, Read}, 12 | process, 13 | }; 14 | use table::TablePrinter; 15 | use util::table_style; 16 | 17 | #[cfg(all(feature = "pager", unix))] 18 | use pager::Pager; 19 | 20 | fn main() { 21 | if let Err(e) = try_main() { 22 | if let Some(ioerr) = e.root_cause().downcast_ref::() { 23 | if ioerr.kind() == io::ErrorKind::BrokenPipe { 24 | process::exit(exitcode::OK); 25 | } 26 | } 27 | 28 | if let Some(csverr) = e.root_cause().downcast_ref::() { 29 | match csverr.kind() { 30 | ErrorKind::Utf8 { .. } => { 31 | eprintln!("[error] input is not utf8 encoded"); 32 | process::exit(exitcode::DATAERR) 33 | } 34 | ErrorKind::UnequalLengths { pos, expected_len, len } => { 35 | let pos_info = pos 36 | .as_ref() 37 | .map(|p| format!(" at (byte: {}, line: {}, record: {})", p.byte(), p.line(), p.record())) 38 | .unwrap_or_else(|| "".to_string()); 39 | eprintln!( 40 | "[error] unequal lengths{}: expected length is {}, but got {}", 41 | pos_info, expected_len, len 42 | ); 43 | process::exit(exitcode::DATAERR) 44 | } 45 | ErrorKind::Io(e) => { 46 | eprintln!("[error] io error: {}", e); 47 | process::exit(exitcode::IOERR) 48 | } 49 | e => { 50 | eprintln!("[error] failed to process input: {:?}", e); 51 | process::exit(exitcode::DATAERR) 52 | } 53 | } 54 | } 55 | 56 | eprintln!("{}: {}", env!("CARGO_PKG_NAME"), e); 57 | std::process::exit(1) 58 | } 59 | } 60 | 61 | fn try_main() -> anyhow::Result<()> { 62 | let App { 63 | file, 64 | no_headers, 65 | number, 66 | tsv, 67 | delimiter, 68 | style, 69 | padding, 70 | indent, 71 | sniff, 72 | header_align, 73 | body_align, 74 | #[cfg(all(feature = "pager", unix))] 75 | disable_pager, 76 | } = App::parse(); 77 | 78 | #[cfg(all(feature = "pager", unix))] 79 | if !disable_pager && io::stdout().is_terminal() { 80 | match std::env::var("CSVIEW_PAGER") { 81 | Ok(pager) => Pager::with_pager(&pager).setup(), 82 | // XXX: the extra null byte can be removed once https://gitlab.com/imp/pager-rs/-/merge_requests/8 is merged 83 | Err(_) => Pager::with_pager("less").pager_envs(["LESS=-SF\0"]).setup(), 84 | } 85 | } 86 | 87 | let stdout = io::stdout(); 88 | let wtr = &mut BufWriter::new(stdout.lock()); 89 | let rdr = ReaderBuilder::new() 90 | .delimiter(if tsv { b'\t' } else { delimiter as u8 }) 91 | .has_headers(!no_headers) 92 | .from_reader(match file { 93 | Some(path) => Box::new(File::open(path)?) as Box, 94 | None if io::stdin().is_terminal() => bail!("no input file specified (use -h for help)"), 95 | None => Box::new(io::stdin()), 96 | }); 97 | 98 | let sniff = if sniff == 0 { usize::MAX } else { sniff }; 99 | let table = TablePrinter::new(rdr, sniff, number)?; 100 | table.writeln(wtr, &table_style(style, padding, indent, header_align, body_align))?; 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /completions/powershell/_csview.ps1: -------------------------------------------------------------------------------- 1 | 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Register-ArgumentCompleter -Native -CommandName 'csview' -ScriptBlock { 6 | param($wordToComplete, $commandAst, $cursorPosition) 7 | 8 | $commandElements = $commandAst.CommandElements 9 | $command = @( 10 | 'csview' 11 | for ($i = 1; $i -lt $commandElements.Count; $i++) { 12 | $element = $commandElements[$i] 13 | if ($element -isnot [StringConstantExpressionAst] -or 14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or 15 | $element.Value.StartsWith('-') -or 16 | $element.Value -eq $wordToComplete) { 17 | break 18 | } 19 | $element.Value 20 | }) -join ';' 21 | 22 | $completions = @(switch ($command) { 23 | 'csview' { 24 | [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Specify the field delimiter') 25 | [CompletionResult]::new('--delimiter', '--delimiter', [CompletionResultType]::ParameterName, 'Specify the field delimiter') 26 | [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Specify the border style') 27 | [CompletionResult]::new('--style', '--style', [CompletionResultType]::ParameterName, 'Specify the border style') 28 | [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Specify padding for table cell') 29 | [CompletionResult]::new('--padding', '--padding', [CompletionResultType]::ParameterName, 'Specify padding for table cell') 30 | [CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Specify global indent for table') 31 | [CompletionResult]::new('--indent', '--indent', [CompletionResultType]::ParameterName, 'Specify global indent for table') 32 | [CompletionResult]::new('--sniff', '--sniff', [CompletionResultType]::ParameterName, 'Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit') 33 | [CompletionResult]::new('--header-align', '--header-align', [CompletionResultType]::ParameterName, 'Specify the alignment of the table header') 34 | [CompletionResult]::new('--body-align', '--body-align', [CompletionResultType]::ParameterName, 'Specify the alignment of the table body') 35 | [CompletionResult]::new('-H', '-H ', [CompletionResultType]::ParameterName, 'Specify that the input has no header row') 36 | [CompletionResult]::new('--no-headers', '--no-headers', [CompletionResultType]::ParameterName, 'Specify that the input has no header row') 37 | [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Prepend a column of line numbers to the table') 38 | [CompletionResult]::new('--number', '--number', [CompletionResultType]::ParameterName, 'Prepend a column of line numbers to the table') 39 | [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'Use ''\t'' as delimiter for tsv') 40 | [CompletionResult]::new('--tsv', '--tsv', [CompletionResultType]::ParameterName, 'Use ''\t'' as delimiter for tsv') 41 | [CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'Disable pager') 42 | [CompletionResult]::new('--disable-pager', '--disable-pager', [CompletionResultType]::ParameterName, 'Disable pager') 43 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 44 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 45 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 46 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 47 | break 48 | } 49 | }) 50 | 51 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | 52 | Sort-Object -Property ListItemText 53 | } 54 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | """ 12 | # template for the changelog body 13 | # https://keats.github.io/tera/docs/#introduction 14 | body = """ 15 | {%- macro remote_url() -%} 16 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 17 | {%- endmacro -%} 18 | 19 | {% macro print_commit(commit) -%} 20 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 21 | {% if commit.breaking %}[**breaking**] {% endif %}\ 22 | {{ commit.message | upper_first }} - \ 23 | ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ 24 | {% endmacro -%} 25 | 26 | {% if version %}\ 27 | {% if previous.version %}\ 28 | ## [{{ version | trim_start_matches(pat="v") }}]\ 29 | ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }}) 30 | {% else %}\ 31 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 32 | {% endif %}\ 33 | {% else %}\ 34 | ## [unreleased] 35 | {% endif %}\ 36 | 37 | {% for group, commits in commits | group_by(attribute="group") %} 38 | ### {{ group | striptags | trim | upper_first }} 39 | {% for commit in commits 40 | | filter(attribute="scope") 41 | | sort(attribute="scope") %} 42 | {{ self::print_commit(commit=commit) }} 43 | {%- endfor -%} 44 | {% raw %}\n{% endraw %}\ 45 | {%- for commit in commits %} 46 | {%- if not commit.scope -%} 47 | {{ self::print_commit(commit=commit) }} 48 | {% endif -%} 49 | {% endfor -%} 50 | {% endfor %}\n 51 | """ 52 | # template for the changelog footer 53 | footer = """ 54 | 55 | """ 56 | # remove the leading and trailing whitespace from the templates 57 | trim = true 58 | # postprocessors 59 | postprocessors = [ 60 | { pattern = '', replace = "https://github.com/wfxr/csview" }, # replace repository URL 61 | ] 62 | 63 | [git] 64 | # parse the commits based on https://www.conventionalcommits.org 65 | conventional_commits = true 66 | # filter out the commits that are not conventional 67 | filter_unconventional = true 68 | # process each line of a commit as an individual commit 69 | split_commits = false 70 | # regex for preprocessing the commit messages 71 | commit_preprocessors = [ 72 | { pattern = ' (#[0-9]+)', replace = '(${1})' }, 73 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, 74 | ] 75 | # regex for parsing and grouping commits 76 | commit_parsers = [ 77 | { message = "^feat", group = "🚀 Features" }, 78 | { message = "^fix", group = "🐛 Bug Fixes" }, 79 | { message = "^doc", group = "📚 Documentation" }, 80 | { message = "^perf", group = "⚡ Performance" }, 81 | { message = "^refactor\\(clippy\\)", skip = true }, 82 | { message = "^refactor", group = "🚜 Refactor" }, 83 | { message = "^style", group = "🎨 Styling" }, 84 | { message = "^test", group = "🧪 Testing" }, 85 | { message = "^chore: [rR]elease", skip = true }, 86 | { message = "^\\(cargo-release\\)", skip = true }, 87 | { message = "^chore\\(deps.*\\)", skip = true }, 88 | { message = "^chore\\(pr\\)", skip = true }, 89 | { message = "^chore\\(pull\\)", skip = true }, 90 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 91 | { body = ".*security", group = "🛡️ Security" }, 92 | { message = "^revert", group = "◀️ Revert" }, 93 | ] 94 | # protect breaking changes from being skipped due to matching a skipping commit_parser 95 | protect_breaking_commits = false 96 | # filter out the commits that are not matched by commit parsers 97 | filter_commits = false 98 | # regex for matching git tags 99 | tag_pattern = "v[0-9].*" 100 | # regex for skipping tags 101 | skip_tags = "beta|alpha" 102 | # regex for ignoring tags 103 | ignore_tags = "rc|v2.1.0|v2.1.1" 104 | # sort the tags topologically 105 | topo_order = false 106 | # sort the commits inside sections by oldest/newest order 107 | sort_commits = "newest" 108 | -------------------------------------------------------------------------------- /src/table/style.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Result, Write}; 2 | 3 | use unicode_truncate::Alignment; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct RowSeps { 7 | /// Top separator row (top border) 8 | /// ``` 9 | /// >┌───┬───┐ 10 | /// │ a │ b │ 11 | /// ``` 12 | pub top: Option, 13 | /// Second separator row (between the header row and the first data row) 14 | /// ``` 15 | /// ┌───┬───┐ 16 | /// │ a │ b │ 17 | /// >├───┼───┤ 18 | /// ``` 19 | pub snd: Option, 20 | /// Middle separator row (between data rows) 21 | /// ``` 22 | /// >├───┼───┤ 23 | /// │ 2 │ 2 │ 24 | /// >├───┼───┤ 25 | /// ``` 26 | pub mid: Option, 27 | /// Bottom separator row (bottom border) 28 | /// ``` 29 | /// │ 3 │ 3 │ 30 | /// >└───┴───┘ 31 | /// ``` 32 | pub bot: Option, 33 | } 34 | 35 | /// The characters used for printing a row separator 36 | #[derive(Debug, Clone, Copy)] 37 | pub struct RowSep { 38 | /// Inner row separator 39 | /// ``` 40 | /// ┌───┬───┐ 41 | /// ^ 42 | /// ``` 43 | inner: char, 44 | /// Left junction separator 45 | /// ``` 46 | /// ┌───┬───┐ 47 | /// ^ 48 | /// ``` 49 | ljunc: char, 50 | /// Crossing junction separator 51 | /// ``` 52 | /// ┌───┬───┐ 53 | /// ^ 54 | /// ``` 55 | cjunc: char, 56 | /// Right junction separator 57 | /// ``` 58 | /// ┌───┬───┐ 59 | /// ^ 60 | /// ``` 61 | rjunc: char, 62 | } 63 | 64 | #[derive(Debug, Clone, Copy)] 65 | pub struct ColSeps { 66 | /// Left separator column (left border) 67 | /// ``` 68 | /// │ 1 │ 2 │ 69 | /// ^ 70 | /// ``` 71 | pub lhs: Option, 72 | /// Middle column separators 73 | /// ``` 74 | /// │ 1 │ 2 │ 75 | /// ^ 76 | /// ``` 77 | pub mid: Option, 78 | /// Right separator column (right border) 79 | /// ``` 80 | /// │ 1 │ 2 │ 81 | /// ^ 82 | /// ``` 83 | pub rhs: Option, 84 | } 85 | 86 | impl RowSep { 87 | pub fn new(sep: char, ljunc: char, cjunc: char, rjunc: char) -> RowSep { 88 | RowSep { inner: sep, ljunc, cjunc, rjunc } 89 | } 90 | } 91 | 92 | impl Default for RowSep { 93 | fn default() -> Self { 94 | RowSep::new('-', '+', '+', '+') 95 | } 96 | } 97 | 98 | impl Default for RowSeps { 99 | fn default() -> Self { 100 | Self { 101 | top: Some(RowSep::default()), 102 | snd: Some(RowSep::default()), 103 | mid: Some(RowSep::default()), 104 | bot: Some(RowSep::default()), 105 | } 106 | } 107 | } 108 | 109 | impl Default for ColSeps { 110 | fn default() -> Self { 111 | Self { lhs: Some('|'), mid: Some('|'), rhs: Some('|') } 112 | } 113 | } 114 | 115 | #[derive(Debug, Clone, Copy)] 116 | pub struct Style { 117 | /// Column style 118 | pub colseps: ColSeps, 119 | 120 | /// Row style 121 | pub rowseps: RowSeps, 122 | 123 | /// Left and right padding 124 | pub padding: usize, 125 | 126 | /// Global indentation 127 | pub indent: usize, 128 | 129 | /// Header alignment 130 | pub header_align: Alignment, 131 | 132 | /// Data alignment 133 | pub body_align: Alignment, 134 | } 135 | 136 | impl Default for Style { 137 | fn default() -> Self { 138 | Self { 139 | indent: 0, 140 | padding: 1, 141 | colseps: ColSeps::default(), 142 | rowseps: RowSeps::default(), 143 | header_align: Alignment::Center, 144 | body_align: Alignment::Left, 145 | } 146 | } 147 | } 148 | 149 | impl Style { 150 | pub fn write_row_sep(&self, wtr: &mut W, widths: &[usize], sep: &RowSep) -> Result<()> { 151 | write!(wtr, "{:indent$}", "", indent = self.indent)?; 152 | if self.colseps.lhs.is_some() { 153 | write!(wtr, "{}", sep.ljunc)?; 154 | } 155 | let mut iter = widths.iter().peekable(); 156 | while let Some(width) = iter.next() { 157 | for _ in 0..width + self.padding * 2 { 158 | write!(wtr, "{}", sep.inner)?; 159 | } 160 | if self.colseps.mid.is_some() && iter.peek().is_some() { 161 | write!(wtr, "{}", sep.cjunc)?; 162 | } 163 | } 164 | if self.colseps.rhs.is_some() { 165 | write!(wtr, "{}", sep.rjunc)?; 166 | } 167 | writeln!(wtr) 168 | } 169 | 170 | #[inline] 171 | pub fn write_col_sep(&self, wtr: &mut W, sep: char) -> Result<()> { 172 | write!(wtr, "{}", sep) 173 | } 174 | } 175 | 176 | #[derive(Default, Debug, Clone)] 177 | pub struct StyleBuilder { 178 | format: Box