├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── build.rs ├── demo.tape ├── images ├── demo.gif ├── screenshot.png ├── theme-argonaut.png ├── theme-monokai.png └── theme-terminal.png ├── scripts └── release-version.sh ├── src ├── app.rs ├── args.rs ├── handler │ ├── command.rs │ ├── event.rs │ ├── keybind.rs │ └── mod.rs ├── lib.rs ├── main.rs ├── reader │ ├── fwf.rs │ └── mod.rs ├── sql.rs ├── tui │ ├── mod.rs │ ├── status_bar.rs │ ├── tabs.rs │ ├── tabular.rs │ ├── terminal.rs │ ├── theme.rs │ ├── utils.rs │ └── widget │ │ ├── data_frame_table.rs │ │ ├── mod.rs │ │ ├── prompt.rs │ │ └── sheet.rs ├── utils.rs └── writer │ └── mod.rs └── tutorial ├── company.csv ├── employment.csv ├── housing.csv ├── image.png ├── images ├── command_mode.png ├── help.png ├── main_page.png ├── new_tab.png ├── schema.png └── sheet_view.png ├── tutorial.md └── user.csv /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report" 3 | about: Create a bug report for tabler. 4 | title: "" 5 | labels: bug 6 | assignees: "trinhminhtriet" 7 | --- 8 | 9 | 14 | 15 | ### ISSUE TYPE: 16 | 17 | - Bug Report 18 | 19 | #### OS / ENVIRONMENT: 20 | 21 | 1. [ ] Operating system: 22 | 2. [ ] tabler version: 23 | 24 | #### STEPS TO REPRODUCE: 25 | 26 | 1. 27 | 2. 28 | 3. 29 | 30 | #### EXPECTED BEHAVIOUR: 31 | 32 | explanation 33 | 34 | #### ACTUAL BEHAVIOUR: 35 | 36 | explanation 37 | 38 | #### Additional information (optional): 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | publish: 13 | name: Publish to crates.io 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Rust toolchain 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: stable 25 | override: true 26 | 27 | - name: Publish tabler to crates.io 28 | run: cargo publish --manifest-path Cargo.toml --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | release: 13 | name: "Release" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | artifact_name: tabler 20 | asset_name: tabler-linux-gnu-amd64 21 | - os: windows-latest 22 | artifact_name: tabler.exe 23 | asset_name: tabler-windows-amd64.exe 24 | - os: macos-latest 25 | artifact_name: tabler 26 | asset_name: tabler-darwin-amd64 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | - name: Set up Rust toolchain 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | override: true 36 | - name: Build release 37 | run: cargo build --release --locked 38 | - name: Set prerelease flag (non-Windows) 39 | if: runner.os != 'Windows' 40 | run: | 41 | if [ $(echo ${{ github.ref }} | grep "rc") ]; then 42 | echo "PRERELEASE=true" >> $GITHUB_ENV 43 | echo "PRERELEASE=true" 44 | else 45 | echo "PRERELEASE=false" >> $GITHUB_ENV 46 | echo "PRERELEASE=false" 47 | fi 48 | echo $PRERELEASE 49 | VERSION=$(echo ${{ github.ref }} | sed 's/refs\/tags\///g') 50 | echo "VERSION=$VERSION" >> $GITHUB_ENV 51 | echo "VERSION=$VERSION" 52 | - name: Set prerelease flag (Windows) 53 | if: runner.os == 'Windows' 54 | shell: powershell 55 | run: | 56 | $full = "${{ github.ref }}" 57 | 58 | if ( $full -like '*rc*' ) { 59 | echo "PRERELEASE=true" >> $env:GITHUB_ENV 60 | echo "PRERELEASE=true" 61 | } else { 62 | echo "PRERELEASE=false" >> $env:GITHUB_ENV 63 | echo "PRERELEASE=false" 64 | } 65 | 66 | $trimmed = $full -replace 'refs/tags/','' 67 | echo "VERSION=$trimmed" >> $env:GITHUB_ENV 68 | echo "VERSION=$trimmed" 69 | - name: Upload release assets 70 | uses: svenstaro/upload-release-action@v2 71 | with: 72 | repo_token: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 73 | file: target/release/${{ matrix.artifact_name }} 74 | asset_name: ${{ matrix.asset_name }} 75 | tag: ${{ github.ref }} 76 | prerelease: ${{ env.PRERELEASE }} 77 | release_name: "tabler ${{ env.VERSION }}" 78 | body: "Please refer to **[CHANGELOG.md](https://github.com/trinhminhtriet/tabler/blob/master/CHANGELOG.md)** for information on this release." 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | target/ 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.0 (2024-11-08) 4 | 5 | - Initialize the project. 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tabler" 3 | version = "0.1.2" 4 | authors = ["Triet Trinh "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "📊 Tabler: A lightweight TUI tool to view, query, and navigate CSV, TSV, and Parquet data files." 8 | repository = "https://github.com/trinhminhtriet/tabler" 9 | documentation = "https://trinhminhtriet.com" 10 | homepage = "https://github.com/trinhminhtriet/tabler" 11 | readme = "README.md" 12 | categories = ["command-line-utilities"] 13 | keywords = ["csv", "tsv", "parquet", "data", "query"] 14 | 15 | [[bin]] 16 | name="tabler" 17 | path="src/main.rs" 18 | 19 | [dependencies] 20 | clap = { version = "4.5.35", features = ["derive"] } 21 | crossterm = "0.29.0" 22 | fwf-rs = "0.2.0" 23 | itertools = "0.14.0" 24 | polars = { version = "0.45.1", features = ["dtype-decimal", "lazy", "polars-sql", "polars-io", "parquet", "json", "ipc"] } 25 | polars-sql = "0.45.1" 26 | polars-arrow = "0.46.0" 27 | polars-lazy = "0.45.1" 28 | rand = "0.8.5" 29 | ratatui = "0.29.0" 30 | 31 | [build-dependencies] 32 | clap = { version = "4.5.35", features = ["derive"] } 33 | clap_mangen = { version = "0.2.26"} 34 | clap_complete = { version = "4.5.52"} 35 | 36 | [package.metadata.deb] 37 | license-file = [ "LICENSE", "4" ] 38 | depends = "$auto" 39 | extended-description = """ 40 | Tabler is a lightweight, terminal-based application to view and query delimiter separated value formatted documents, such as CSV and TSV files. 41 | """ 42 | section = "utils" 43 | priority = "optional" 44 | assets = [ 45 | [ "target/release/tabler", "/usr/bin/tabler", "0755" ], 46 | [ "target/manual/tabler.1", "/usr/share/man/man1/tabler.1", "0644" ], 47 | [ "target/manual/tabler.1", "/usr/share/man/man1/tabler.1", "0644" ], 48 | [ "target/completion/tabler.bash", "/usr/share/bash-completion/completions/tabler.bash", "0644" ], 49 | [ "target/completion/_tabler", "/usr/share/zsh/vendor-completions/_tabler", "0644" ], 50 | [ "target/completion/tabler.fish", "/usr/share/fish/completions/tabler.fish", "0644" ], 51 | ] 52 | 53 | [package.metadata.generate-rpm] 54 | assets = [ 55 | { source = "target/release/tabler", dest = "/usr/bin/tabler", mode = "755" }, 56 | { source = "target/manual/tabler.1", dest = "/usr/share/man/man1/tabler.1", mode = "0644" }, 57 | { source = "target/manual/tabler.1", dest = "/usr/share/man/man1/tabler.1", mode = "0644" }, 58 | { source = "target/completion/tabler.bash", dest = "/usr/share/bash-completion/completions/tabler.bash", mode = "0644" }, 59 | { source = "target/completion/_tabler", dest = "/usr/share/zsh/vendor-completions/_tabler", mode = "0644" }, 60 | { source = "target/completion/tabler.fish", dest = "/usr/share/fish/completions/tabler.fish", mode = "0644" }, 61 | ] 62 | 63 | [profile.release] 64 | lto = true 65 | strip = true 66 | opt-level = 3 67 | codegen-units = 1 68 | panic = 'abort' 69 | 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trinhminhtriet.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := tabler 2 | AUTHOR := trinhminhtriet 3 | DATE := $(shell date +%FT%T%Z) 4 | GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) 5 | VERSION := $(shell git describe --tags) 6 | 7 | default: build 8 | 9 | clean: 10 | @echo "Cleaning build dir" 11 | @$(RM) -r target 12 | @echo "Cleaning using cargo" 13 | @cargo clean 14 | 15 | check: 16 | @echo "Checking $(NAME)" 17 | @cargo check 18 | 19 | test: 20 | @echo "Running tests" 21 | @cargo test 22 | @echo "Running tests with coverage and open report in browser" 23 | @cargo tarpaulin --out Html --open 24 | 25 | build: 26 | @echo "Building release: $(VERSION)" 27 | @cargo build --release 28 | ln -sf $(PWD)/target/release/$(NAME) /usr/local/bin/$(NAME) 29 | which $(NAME) 30 | $(NAME) --version 31 | 32 | build_debug: 33 | @echo "Building debug" 34 | @cargo build 35 | 36 | run: 37 | @echo "Running debug" 38 | @cargo run 39 | 40 | release: 41 | ./scripts/release-version.sh 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📊 Tabler 2 | 3 | ```text 4 | 5 | _____ _ _ 6 | |_ _| __ _ | |__ | | ___ _ __ 7 | | | / _` || '_ \ | | / _ \| '__| 8 | | | | (_| || |_) || || __/| | 9 | |_| \__,_||_.__/ |_| \___||_| 10 | 11 | ``` 12 | 13 | 📊 Tabler: A lightweight TUI tool to view, query, and navigate CSV, TSV, and Parquet data files. 14 | 15 | ![Tabler Demo](images/demo.gif) 16 | 17 | ## ✨ Features 18 | 19 | ## Features 20 | 21 | - ⌨️ **Vim-style Keybindings** - Navigate efficiently with familiar, keyboard-driven controls. 22 | - 🛠️ **SQL Query Support** - Perform advanced queries directly within the interface. 23 | - 🗂️ **Multi-table Management** - Seamlessly load and switch between multiple data tables. 24 | - 📊 **Broad File Format Support** - Compatible with CSV, Parquet, JSON, JSONL, Arrow, and Fixed-Width Formats (FWF). 25 | 26 | ## 🚀 Installation 27 | 28 | To install **tabler**, simply clone the repository and follow the instructions below: 29 | 30 | ```bash 31 | git clone git@github.com:trinhminhtriet/tabler.git 32 | cd tabler 33 | 34 | cargo build --release 35 | cp ./target/release/tabler /usr/local/bin/ 36 | ``` 37 | 38 | Running the below command will globally install the `tabler` binary. 39 | 40 | ```bash 41 | cargo install tabler 42 | ``` 43 | 44 | Optionally, you can add `~/.cargo/bin` to your PATH if it's not already there 45 | 46 | ```bash 47 | echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc 48 | source ~/.bashrc 49 | ``` 50 | 51 | ## 💡 Usage 52 | 53 | Start Tabler with `tabler` 54 | 55 | ```bash 56 | tabler 57 | ``` 58 | 59 | Options: 60 | 61 | - `--no-header`: Use this option if the CSV file does not contain a header row. 62 | - `--ignore-errors`: Ignore parsing errors while loading the CSV file. 63 | - `--infer-schema`: Set the schema inference method. Options are no, fast, full, and safe. 64 | - `--quote-char`: Set the quote character. 65 | - `--separator`: Set the separator character. 66 | - `--theme`: Set the theme. 67 | 68 | To open TSV file(s), use: 69 | 70 | ```bash 71 | tabler --separator $'\t' --no-header 72 | ``` 73 | 74 | To open parquet file(s), use: 75 | 76 | ```bash 77 | tabler -f parquet 78 | ``` 79 | 80 | ## Tutorial 81 | 82 | For a guide on using Tabler, including instructions on opening files, navigating tables, performing queries, and using inline queries, kindly visit the [tutorial page](https://github.com/trinhminhtriet/tabler/blob/master/tutorial/tutorial.md). 83 | 84 | ## Keybindings️ 85 | 86 | | Key Combination | Functionality | 87 | | ----------------------- | --------------------------------------------------- | 88 | | `v` | Switch view | 89 | | `k` or `Arrow Up` | Move up in the table or scroll up in sheet view | 90 | | `j` or `Arrow Down` | Move down in the table or scroll down in sheet view | 91 | | `h` or `Arrow Left` | Move to the previous item in sheet view | 92 | | `l` or `Arrow Right` | Move to the next item in sheet view | 93 | | `Page Up` or `Ctrl+b` | Move one page up | 94 | | `Page Down` or `Ctrl+f` | Move one page down | 95 | | `H` | Select previous tab | 96 | | `L` | Select next tab | 97 | | `Ctrl+u` | Move up half a page | 98 | | `Ctrl+d` | Move down half a page | 99 | | `Home` or `g` | Move to the first row | 100 | | `End` or `G` | Move to the last row | 101 | | `R` | Select a random row | 102 | | `q` | Close current tab | 103 | | `:` | Command mode | 104 | 105 | ## Commands 106 | 107 | | Command | Example | Description | 108 | | ----------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------- | 109 | | `:Q` or `:query` | `:Q SELECT * FROM df` | Query the data in Structured Query Language(SQL). The table name is the file name without extension | 110 | | `:S` or `:select` | `:S price, area, bedrooms, parking` | Query current data frame for columns/functions | 111 | | `:F` or `:filter` | `:F price < 20000 AND bedrooms > 4` | Filter current data frame, keeping rows were the condition(s) match | 112 | | `:O` or `:order` | `:O area` | Sort current data frame by column(s) | 113 | | `:tabn` | `:tabn SELECT * FORM user WHERE balance > 1000` | Create a new tab with the given query | 114 | | `:q` or `:quit` | `:q` | Return to table from sheet view otherwise quit | 115 | | `:schema` | `:schema` | Show loaded data frame(s) alongside their path(s) | 116 | | `:reset` | `:reset` | Reset the table to the original data frame | 117 | | `:help` | `:help` | Show help menu | 118 | 119 | ## Themes 120 | 121 | ### Monokai (default): 122 | 123 | ![Image Alt text](/images/theme-monokai.png "Monokai") 124 | 125 | ### Argonaut: 126 | 127 | ![Image Alt text](/images/theme-argonaut.png "Argonaut") 128 | 129 | ### Terminal: 130 | 131 | ![Image Alt text](/images/theme-terminal.png "Terminal") 132 | 133 | ## 🗑️ Uninstallation 134 | 135 | Running the below command will globally uninstall the `tabler` binary. 136 | 137 | ```bash 138 | cargo uninstall tabler 139 | ``` 140 | 141 | Remove the project repo 142 | 143 | ```bash 144 | rm -rf /path/to/git/clone/tabler 145 | ``` 146 | 147 | ## 🤝 How to contribute 148 | 149 | We welcome contributions! 150 | 151 | - Fork this repository; 152 | - Create a branch with your feature: `git checkout -b my-feature`; 153 | - Commit your changes: `git commit -m "feat: my new feature"`; 154 | - Push to your branch: `git push origin my-feature`. 155 | 156 | Once your pull request has been merged, you can delete your branch. 157 | 158 | ## 📝 License 159 | 160 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 161 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs}; 2 | 3 | use clap::CommandFactory; 4 | 5 | include!("src/args.rs"); 6 | 7 | fn main() { 8 | // get target directory 9 | let target_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target"); 10 | 11 | // generate manual 12 | let manual_dir = target_dir.join("manual"); 13 | fs::create_dir_all(&manual_dir).unwrap(); 14 | println!( 15 | "man generated at {:?}", 16 | clap_mangen::generate_to(Args::command(), manual_dir).unwrap() 17 | ); 18 | 19 | // generate completions 20 | let completion_dir = target_dir.join("completion"); 21 | fs::create_dir_all(&completion_dir).unwrap(); 22 | for &shell in clap_complete::Shell::value_variants() { 23 | println!( 24 | "completions generated at {:?}", 25 | clap_complete::generate_to(shell, &mut Args::command(), "tabler", &completion_dir).unwrap() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | Output images/demo.gif 2 | 3 | Set FontSize 16 4 | Set Width 1440 5 | Set Height 768 6 | Set TypingSpeed 400ms 7 | 8 | Type@100ms "echo 'https://trinhminhtriet.com'" 9 | Sleep 50ms 10 | Enter 11 | 12 | Type@50ms "tabler --version" 13 | Sleep 50ms 14 | Enter 15 | Sleep 100ms 16 | 17 | Type@50ms "tabler --help" 18 | Sleep 50ms 19 | Enter 20 | Sleep 100ms 21 | 22 | Type@20ms "tabler tutorial/company.csv" 23 | Sleep 100ms 24 | Enter 25 | Sleep 20s 26 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/images/demo.gif -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/images/screenshot.png -------------------------------------------------------------------------------- /images/theme-argonaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/images/theme-argonaut.png -------------------------------------------------------------------------------- /images/theme-monokai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/images/theme-monokai.png -------------------------------------------------------------------------------- /images/theme-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/images/theme-terminal.png -------------------------------------------------------------------------------- /scripts/release-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | [ -z "$(git status --porcelain)" ] || (echo "dirty working directory" && exit 1) 5 | 6 | current_version="$(grep '^version = ' Cargo.toml | head -1 | cut -d '"' -f2)" 7 | IFS='.' read -r major minor patch <<<"$current_version" 8 | new_patch=$((patch + 1)) 9 | new_version="$major.$minor.$new_patch" 10 | tag_name="v$new_version" 11 | 12 | if [ -z "$new_version" ]; then 13 | echo "New version required as argument" 14 | exit 1 15 | fi 16 | 17 | echo ">>> Bumping version" 18 | sed -i.bak "s/version = \"$current_version\"/version = \"$new_version\"/" Cargo.toml 19 | rm Cargo.toml.bak 20 | 21 | sleep 10 22 | 23 | echo ">>> Commit" 24 | git add Cargo.toml Cargo.lock 25 | git commit -am "version $new_version" 26 | git tag $tag_name 27 | 28 | echo ">>> Publish" 29 | git push 30 | git push origin $tag_name 31 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{ops::Div, path::PathBuf}; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent}; 4 | use ratatui::{ 5 | layout::{Constraint, Layout}, 6 | Frame, 7 | }; 8 | 9 | use crate::{ 10 | handler::{ 11 | command::{commands_help_data_frame, parse_into_action}, 12 | keybind::{Action, Keybind}, 13 | }, 14 | sql::SqlBackend, 15 | tui, 16 | writer::{JsonFormat, WriteToArrow, WriteToCsv, WriteToFile, WriteToJson, WriteToParquet}, 17 | AppResult, 18 | }; 19 | 20 | use tui::status_bar::{StatusBar, StatusBarState}; 21 | use tui::tabs::Tabs; 22 | use tui::tabular::{self, Tabular, TabularType}; 23 | use tui::Styler; 24 | 25 | pub struct App { 26 | tabs: Tabs, 27 | status_bar: StatusBar, 28 | sql: SqlBackend, 29 | keybindings: Keybind, 30 | running: bool, 31 | } 32 | 33 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 34 | pub enum AppState { 35 | Empty, 36 | Table, 37 | Sheet, 38 | Command, 39 | Error, 40 | } 41 | 42 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 43 | pub enum AppAction { 44 | StatusBarStats, 45 | StatusBarCommand(String), 46 | StatausBarError(String), 47 | TabularTableView, 48 | TabularSheetView, 49 | TabularSwitchView, 50 | SqlQuery(String), 51 | SqlSchema, 52 | TabularGoto(usize), 53 | TabularGotoFirst, 54 | TabularGotoLast, 55 | TabularGotoRandom, 56 | TabularGoUp(usize), 57 | TabularGoUpHalfPage, 58 | TabularGoUpFullPage, 59 | TabularGoDown(usize), 60 | TabularGoDownHalfPage, 61 | TabularGoDownFullPage, 62 | SheetScrollUp, 63 | SheetScrollDown, 64 | TabularReset, 65 | TabularSelect(String), 66 | TabularOrder(String), 67 | TabularFilter(String), 68 | TabNew(String), 69 | TabSelect(usize), 70 | TabRemove(usize), 71 | TabRemoveSelected, 72 | TabSelectedPrev, 73 | TabSelectedNext, 74 | TabRemoveOrQuit, 75 | TabRename(usize, String), 76 | ExportDsv { 77 | path: PathBuf, 78 | separator: char, 79 | quote: char, 80 | header: bool, 81 | }, 82 | ExportParquet(PathBuf), 83 | ExportJson(PathBuf, JsonFormat), 84 | ExportArrow(PathBuf), 85 | Help, 86 | Quit, 87 | } 88 | 89 | impl App { 90 | pub fn new(tabs: Tabs, sql: SqlBackend, key_bind: Keybind) -> Self { 91 | Self { 92 | tabs, 93 | status_bar: StatusBar::::default(), 94 | sql, 95 | keybindings: key_bind, 96 | running: true, 97 | } 98 | } 99 | 100 | pub fn running(&self) -> bool { 101 | self.running 102 | } 103 | 104 | pub fn tick(&mut self) -> AppResult<()> { 105 | self.tabs.selected_mut().map(|tab| tab.tick()); 106 | self.status_bar.tick() 107 | } 108 | 109 | pub fn quit(&mut self) -> AppResult<()> { 110 | self.running = false; 111 | Ok(()) 112 | } 113 | 114 | pub fn infer_state(&self) -> AppState { 115 | match ( 116 | self.tabs.selected().map(Tabular::state), 117 | self.status_bar.state(), 118 | ) { 119 | (Some(tabular::TabularState::Table), StatusBarState::Info) => AppState::Table, 120 | (Some(tabular::TabularState::Table), StatusBarState::Error(_)) => AppState::Error, 121 | (Some(tabular::TabularState::Table), StatusBarState::Prompt(_)) => AppState::Command, 122 | (Some(tabular::TabularState::Sheet(_)), StatusBarState::Info) => AppState::Sheet, 123 | (Some(tabular::TabularState::Sheet(_)), StatusBarState::Error(_)) => AppState::Error, 124 | (Some(tabular::TabularState::Sheet(_)), StatusBarState::Prompt(_)) => AppState::Command, 125 | (None, StatusBarState::Info) => AppState::Empty, 126 | (None, StatusBarState::Error(_)) => AppState::Error, 127 | (None, StatusBarState::Prompt(_)) => AppState::Command, 128 | } 129 | } 130 | 131 | pub fn draw(&mut self, frame: &mut Frame) -> AppResult<()> { 132 | let layout = 133 | Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(frame.area()); 134 | 135 | // Draw table / item 136 | let state = self.infer_state(); 137 | if let Some(tab) = self.tabs.selected_mut() { 138 | tab.render(frame, layout[0], matches!(state, AppState::Table))?; 139 | } 140 | if let Some(tab) = self.tabs.selected() { 141 | self.status_bar.render( 142 | frame, 143 | layout[1], 144 | &[ 145 | ( 146 | match tab.tabular_type() { 147 | TabularType::Help => "Table", 148 | TabularType::Schema => "Table", 149 | TabularType::Name(_) => "Table", 150 | TabularType::Query(_) => "SQL", 151 | }, 152 | match tab.tabular_type() { 153 | TabularType::Help => "Help", 154 | TabularType::Schema => "Schema", 155 | TabularType::Name(name) => name, 156 | TabularType::Query(query) => query, 157 | }, 158 | ), 159 | ( 160 | "Tab", 161 | &format!( 162 | "{:>width$} / {}", 163 | self.tabs.idx() + 1, 164 | self.tabs.len(), 165 | width = self.tabs.len().to_string().len() 166 | ), 167 | ), 168 | ( 169 | "Row", 170 | &format!( 171 | "{:>width$}", 172 | tab.selected() + 1, 173 | width = tab.data_frame().height().to_string().len() 174 | ), 175 | ), 176 | ( 177 | "Shape", 178 | &format!( 179 | "{} x {}", 180 | tab.data_frame().height(), 181 | tab.data_frame().width() 182 | ), 183 | ), 184 | ], 185 | ) 186 | } else { 187 | self.status_bar.render(frame, layout[1], &[]) 188 | } 189 | } 190 | 191 | pub fn handle_key_event(&mut self, key_event: KeyEvent) -> AppResult<()> { 192 | let state = self.infer_state(); 193 | let key_code = key_event.code; 194 | match (state, key_code) { 195 | (AppState::Command | AppState::Error, KeyCode::Esc) => self.status_bar.show_info(), 196 | 197 | (AppState::Command, KeyCode::Enter) => { 198 | if let Some(cmd) = self.status_bar.commit_prompt() { 199 | let _ = parse_into_action(cmd) 200 | .and_then(|action| self.invoke(action)) 201 | .and_then(|_| self.status_bar.show_info()) 202 | .inspect_err(|err| { 203 | let _ = self.status_bar.show_error(err); 204 | }); 205 | Ok(()) 206 | } else { 207 | self.status_bar 208 | .show_error("Invalid state; consider restarting Tabler") 209 | } 210 | } 211 | 212 | (AppState::Command, _) => self.status_bar.input(key_event), 213 | 214 | (_, KeyCode::Char(':')) => self.status_bar.show_prompt(""), 215 | 216 | _ => { 217 | match self 218 | .keybindings 219 | .get_action(state, key_event) 220 | .cloned() 221 | .map(|action| self.invoke(action)) 222 | { 223 | Some(Err(error)) => self.status_bar.show_error(error), 224 | _ => Ok(()), 225 | } 226 | } 227 | } 228 | } 229 | 230 | pub fn invoke(&mut self, action: Action) -> AppResult<()> { 231 | match action { 232 | AppAction::StatusBarStats => self.status_bar.show_info(), 233 | 234 | AppAction::StatusBarCommand(prefix) => self.status_bar.show_prompt(prefix), 235 | 236 | AppAction::StatausBarError(msg) => self.status_bar.show_error(msg), 237 | 238 | AppAction::TabularTableView => { 239 | if let Some(tab) = self.tabs.selected_mut() { 240 | tab.show_table() 241 | } else { 242 | Ok(()) 243 | } 244 | } 245 | 246 | AppAction::TabularSheetView => { 247 | if let Some(tab) = self.tabs.selected_mut() { 248 | tab.show_sheet() 249 | } else { 250 | Ok(()) 251 | } 252 | } 253 | 254 | AppAction::TabularSwitchView => { 255 | if let Some(tab) = self.tabs.selected_mut() { 256 | tab.switch_view() 257 | } else { 258 | Ok(()) 259 | } 260 | } 261 | 262 | AppAction::SqlQuery(query) => { 263 | if let Some(tab) = self.tabs.selected_mut() { 264 | tab.set_data_frame(self.sql.execute(&query)?) 265 | } else { 266 | Ok(()) 267 | } 268 | } 269 | 270 | AppAction::SqlSchema => { 271 | let idx = self.tabs.iter().enumerate().find_map(|(idx, tab)| { 272 | matches!(tab.tabular_type(), TabularType::Schema).then_some(idx) 273 | }); 274 | if let Some(idx) = idx { 275 | self.tabs.select(idx) 276 | } else { 277 | self.tabs 278 | .add(Tabular::new(self.sql.schema(), TabularType::Schema))?; 279 | self.tabs.select_last() 280 | } 281 | } 282 | 283 | AppAction::TabularGoto(line) => { 284 | if let Some(tab) = self.tabs.selected_mut() { 285 | tab.select(line) 286 | } else { 287 | Ok(()) 288 | } 289 | } 290 | 291 | AppAction::TabularGotoFirst => { 292 | if let Some(tab) = self.tabs.selected_mut() { 293 | tab.select_first() 294 | } else { 295 | Ok(()) 296 | } 297 | } 298 | 299 | AppAction::TabularGotoLast => { 300 | if let Some(tab) = self.tabs.selected_mut() { 301 | tab.select_last() 302 | } else { 303 | Ok(()) 304 | } 305 | } 306 | 307 | AppAction::TabularGotoRandom => { 308 | if let Some(tab) = self.tabs.selected_mut() { 309 | tab.select_random() 310 | } else { 311 | Ok(()) 312 | } 313 | } 314 | 315 | AppAction::TabularGoUp(lines) => { 316 | if let Some(tab) = self.tabs.selected_mut() { 317 | tab.select_up(lines) 318 | } else { 319 | Ok(()) 320 | } 321 | } 322 | 323 | AppAction::TabularGoUpHalfPage => { 324 | if let Some(tab) = self.tabs.selected_mut() { 325 | tab.select_up(tab.page_len().div(2)) 326 | } else { 327 | Ok(()) 328 | } 329 | } 330 | 331 | AppAction::TabularGoUpFullPage => { 332 | if let Some(tab) = self.tabs.selected_mut() { 333 | tab.select_up(tab.page_len()) 334 | } else { 335 | Ok(()) 336 | } 337 | } 338 | 339 | AppAction::TabularGoDown(lines) => { 340 | if let Some(tab) = self.tabs.selected_mut() { 341 | tab.select_down(lines) 342 | } else { 343 | Ok(()) 344 | } 345 | } 346 | 347 | AppAction::TabularGoDownHalfPage => { 348 | if let Some(tab) = self.tabs.selected_mut() { 349 | tab.select_down(tab.page_len().div(2)) 350 | } else { 351 | Ok(()) 352 | } 353 | } 354 | 355 | AppAction::TabularGoDownFullPage => { 356 | if let Some(tab) = self.tabs.selected_mut() { 357 | tab.select_down(tab.page_len()) 358 | } else { 359 | Ok(()) 360 | } 361 | } 362 | 363 | AppAction::SheetScrollUp => { 364 | if let Some(tab) = self.tabs.selected_mut() { 365 | tab.scroll_up() 366 | } else { 367 | Ok(()) 368 | } 369 | } 370 | 371 | AppAction::SheetScrollDown => { 372 | if let Some(tab) = self.tabs.selected_mut() { 373 | tab.scroll_down() 374 | } else { 375 | Ok(()) 376 | } 377 | } 378 | 379 | AppAction::TabularReset => { 380 | if let Some(tab) = self.tabs.selected_mut() { 381 | tab.set_data_frame(match tab.tabular_type() { 382 | TabularType::Help => commands_help_data_frame(), 383 | TabularType::Schema => self.sql.schema(), 384 | TabularType::Name(name) => self 385 | .sql 386 | .execute(format!("SELECT * FROM {}", name).as_str())?, 387 | TabularType::Query(query) => self.sql.execute(query)?, 388 | }) 389 | } else { 390 | Ok(()) 391 | } 392 | } 393 | 394 | AppAction::TabularSelect(select) => { 395 | if let Some(tab) = self.tabs.selected_mut() { 396 | let mut sql = SqlBackend::new(); 397 | sql.register("df", tab.data_frame().clone(), "".into()); 398 | tab.set_data_frame(sql.execute(&format!("SELECT {} FROM df", select))?) 399 | } else { 400 | Ok(()) 401 | } 402 | } 403 | 404 | AppAction::TabularOrder(order) => { 405 | if let Some(tab) = self.tabs.selected_mut() { 406 | let mut sql = SqlBackend::new(); 407 | sql.register("df", tab.data_frame().clone(), "".into()); 408 | tab.set_data_frame( 409 | sql.execute(&format!("SELECT * FROM df ORDER BY {}", order))?, 410 | ) 411 | } else { 412 | Ok(()) 413 | } 414 | } 415 | 416 | AppAction::TabularFilter(filter) => { 417 | if let Some(tab) = self.tabs.selected_mut() { 418 | let mut sql = SqlBackend::new(); 419 | sql.register("df", tab.data_frame().clone(), "".into()); 420 | tab.set_data_frame(sql.execute(&format!("SELECT * FROM df where {}", filter))?) 421 | } else { 422 | Ok(()) 423 | } 424 | } 425 | 426 | AppAction::TabNew(query) => { 427 | if self.sql.contains_dataframe(&query) { 428 | let df = self.sql.execute(&format!("SELECT * FROM {}", query))?; 429 | self.tabs.add(Tabular::new(df, TabularType::Name(query)))?; 430 | } else { 431 | let df = self.sql.execute(&query)?; 432 | self.tabs.add(Tabular::new(df, TabularType::Query(query)))?; 433 | } 434 | self.tabs.select_last() 435 | } 436 | 437 | AppAction::TabSelect(idx) => { 438 | if idx == 0 { 439 | Err("zero is not a valid tab".into()) 440 | } else if idx <= self.tabs.len() { 441 | self.tabs.select(idx.saturating_sub(1)) 442 | } else { 443 | Err(format!( 444 | "index {} is out of bound, maximum is {}", 445 | idx, 446 | self.tabs.len() 447 | ) 448 | .into()) 449 | } 450 | } 451 | 452 | AppAction::TabRemove(idx) => self.tabs.remove(idx), 453 | 454 | AppAction::TabRemoveSelected => self.tabs.remove(self.tabs.idx()), 455 | 456 | AppAction::TabRename(_idx, _new_name) => { 457 | todo!() 458 | } 459 | 460 | AppAction::TabSelectedPrev => self.tabs.select_prev(), 461 | 462 | AppAction::TabSelectedNext => self.tabs.select_next(), 463 | 464 | AppAction::TabRemoveOrQuit => { 465 | if self.tabs.len() == 1 { 466 | self.quit() 467 | } else { 468 | self.tabs.remove(self.tabs.idx()) 469 | } 470 | } 471 | 472 | AppAction::ExportDsv { 473 | path, 474 | separator, 475 | quote, 476 | header, 477 | } => { 478 | if let Some(tab) = self.tabs.selected_mut() { 479 | WriteToCsv::default() 480 | .with_separator_char(separator) 481 | .with_quote_char(quote) 482 | .with_header(header) 483 | .write_to_file(path, tab.data_frame_mut()) 484 | } else { 485 | Err("Unable to export the data frame".into()) 486 | } 487 | } 488 | 489 | AppAction::ExportParquet(path) => { 490 | if let Some(tab) = self.tabs.selected_mut() { 491 | WriteToParquet.write_to_file(path, tab.data_frame_mut()) 492 | } else { 493 | Err("Unable to export the data frame".into()) 494 | } 495 | } 496 | AppAction::ExportJson(path, fmt) => { 497 | if let Some(tab) = self.tabs.selected_mut() { 498 | WriteToJson::default() 499 | .with_format(fmt) 500 | .write_to_file(path, tab.data_frame_mut()) 501 | } else { 502 | Err("Unable to export the data frame".into()) 503 | } 504 | } 505 | AppAction::ExportArrow(path) => { 506 | if let Some(tab) = self.tabs.selected_mut() { 507 | WriteToArrow.write_to_file(path, tab.data_frame_mut()) 508 | } else { 509 | Err("Unable to export the data frame".into()) 510 | } 511 | } 512 | 513 | AppAction::Help => { 514 | let idx = self.tabs.iter().enumerate().find_map(|(idx, tab)| { 515 | matches!(tab.tabular_type(), TabularType::Help).then_some(idx) 516 | }); 517 | if let Some(idx) = idx { 518 | self.tabs.select(idx) 519 | } else { 520 | self.tabs 521 | .add(Tabular::new(commands_help_data_frame(), TabularType::Help))?; 522 | self.tabs.select_last() 523 | } 524 | } 525 | 526 | AppAction::Quit => self.quit(), 527 | } 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueEnum}; 2 | use std::{num::NonZero, path::PathBuf}; 3 | 4 | #[derive(Parser, Debug)] 5 | #[command(version, about, long_about = None)] 6 | pub struct Args { 7 | #[arg(help = "Path(s) to the file(s) to be opened.", required = false)] 8 | pub files: Vec, 9 | 10 | #[arg(short, long, help = "Path to the startup script.", required = false)] 11 | pub script: Option, 12 | 13 | #[arg( 14 | short, 15 | long, 16 | help = "Specifies the input format. By default, the format is selected based on the file extension", 17 | value_enum 18 | )] 19 | pub format: Option, 20 | 21 | #[arg( 22 | long, 23 | help = "Specifies if the input does not contain a header row.", 24 | default_value_t = false 25 | )] 26 | pub no_header: bool, 27 | 28 | #[arg( 29 | long, 30 | help = "Ignores parsing errors while loading.", 31 | default_value_t = false 32 | )] 33 | pub ignore_errors: bool, 34 | 35 | #[arg( 36 | long, 37 | help = "Specifies the method to infer the schema.", 38 | required = false, 39 | value_enum, 40 | default_value_t = InferSchema::Safe, 41 | )] 42 | pub infer_schema: InferSchema, 43 | 44 | #[arg( 45 | long, 46 | help = "Character used as the field separator or delimiter while loading DSV files.", 47 | required = false, 48 | default_value_t = ',' 49 | )] 50 | pub separator: char, 51 | 52 | #[arg( 53 | long, 54 | help = "Character used to quote fields while loading DSV files.", 55 | required = false, 56 | default_value_t = '"' 57 | )] 58 | pub quote_char: char, 59 | 60 | #[arg( 61 | long, 62 | help = "A comma-separeted list of widths, which specifies the column widths for FWF files.", 63 | required = false, 64 | default_value_t = String::default(), 65 | )] 66 | pub widths: String, 67 | 68 | #[arg( 69 | long, 70 | help = "Specifies the separator length for FWF files.", 71 | required = false, 72 | default_value_t = 1_usize 73 | )] 74 | pub separator_length: usize, 75 | 76 | #[arg( 77 | long, 78 | help = "Sets strict column width restrictions for FWF files.", 79 | required = false, 80 | default_value_t = false 81 | )] 82 | pub no_flexible_width: bool, 83 | 84 | #[arg( 85 | long, 86 | help = "Tabler theme", 87 | required = false, 88 | value_enum, 89 | default_value_t = AppTheme::Monokai 90 | )] 91 | pub theme: AppTheme, 92 | } 93 | 94 | #[derive(Debug, Clone, ValueEnum)] 95 | pub enum Format { 96 | Dsv, 97 | Parquet, 98 | Jsonl, 99 | Json, 100 | Arrow, 101 | Fwf, 102 | } 103 | 104 | #[derive(Debug, Clone, Copy, ValueEnum)] 105 | pub enum InferSchema { 106 | No, 107 | Fast, 108 | Full, 109 | Safe, 110 | } 111 | 112 | #[derive(Debug, Clone, ValueEnum)] 113 | pub enum AppTheme { 114 | Monokai, 115 | Argonaut, 116 | Terminal, 117 | } 118 | 119 | impl InferSchema { 120 | pub fn to_csv_infer_schema_length(&self) -> Option { 121 | match self { 122 | InferSchema::No => Some(0), 123 | InferSchema::Fast => Some(128), 124 | InferSchema::Full => None, 125 | InferSchema::Safe => Some(0), 126 | } 127 | } 128 | 129 | pub fn to_json_infer_schema_length(&self) -> Option> { 130 | match self { 131 | InferSchema::No => None, 132 | InferSchema::Fast => Some(NonZero::new(128).unwrap()), 133 | InferSchema::Full => None, 134 | InferSchema::Safe => None, 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/handler/command.rs: -------------------------------------------------------------------------------- 1 | use polars::{df, frame::DataFrame}; 2 | use std::{collections::HashMap, sync::OnceLock}; 3 | 4 | use crate::{app::AppAction, writer::JsonFormat, AppResult}; 5 | 6 | pub fn parse_into_action(cmd: impl AsRef) -> AppResult { 7 | let (s1, s2) = cmd.as_ref().split_once(' ').unwrap_or((cmd.as_ref(), "")); 8 | if let Some(parse_fn) = registary().get(s1) { 9 | match parse_fn(s2) { 10 | Ok(action) => Ok(action), 11 | Err(error) => Err(error), 12 | } 13 | } else { 14 | Err(format!("Invalid command {}", cmd.as_ref()).into()) 15 | } 16 | } 17 | 18 | pub fn commands_help_data_frame() -> DataFrame { 19 | let len = ENTRIES.len(); 20 | let (short, long, usage, description) = ENTRIES.iter().fold( 21 | ( 22 | Vec::<&'static str>::with_capacity(len), 23 | Vec::<&'static str>::with_capacity(len), 24 | Vec::<&'static str>::with_capacity(len), 25 | Vec::<&'static str>::with_capacity(len), 26 | ), 27 | |(mut v1, mut v2, mut v3, mut v4), cmd| { 28 | v1.push(cmd.prefix.short().unwrap_or("-")); 29 | v2.push(cmd.prefix.long().unwrap_or("-")); 30 | v3.push(cmd.usage); 31 | v4.push(cmd.description); 32 | (v1, v2, v3, v4) 33 | }, 34 | ); 35 | df! { 36 | "Command" => long, 37 | "Short Form" => short, 38 | "Usage" => usage, 39 | "Description" => description, 40 | } 41 | .unwrap() 42 | } 43 | 44 | static REGISTERY: OnceLock = OnceLock::new(); 45 | 46 | fn registary() -> &'static Registery { 47 | REGISTERY.get_or_init(|| { 48 | ENTRIES 49 | .iter() 50 | .flat_map(|cmd| { 51 | match cmd.prefix { 52 | Prefix::Long(long) => vec![(long, cmd.parser)], 53 | Prefix::ShortAndLong(short, long) => { 54 | vec![(short, cmd.parser), (long, cmd.parser)] 55 | } 56 | } 57 | .into_iter() 58 | }) 59 | .collect() 60 | }) 61 | } 62 | 63 | type ParseFn = fn(&str) -> AppResult; 64 | type Registery = HashMap<&'static str, ParseFn>; 65 | 66 | enum Prefix { 67 | Long(&'static str), 68 | ShortAndLong(&'static str, &'static str), 69 | } 70 | 71 | impl Prefix { 72 | fn short(&self) -> Option<&'static str> { 73 | match self { 74 | Prefix::ShortAndLong(short, _) => Some(short), 75 | _ => None, 76 | } 77 | } 78 | 79 | fn long(&self) -> Option<&'static str> { 80 | match self { 81 | Prefix::Long(long) => Some(long), 82 | Prefix::ShortAndLong(_, long) => Some(long), 83 | } 84 | } 85 | } 86 | 87 | struct Entry { 88 | prefix: Prefix, 89 | usage: &'static str, 90 | description: &'static str, 91 | parser: ParseFn, 92 | } 93 | 94 | static ENTRIES: [Entry; 17] = [ 95 | Entry { 96 | prefix: Prefix::ShortAndLong(":Q", ":query"), 97 | usage: ":Q ", 98 | description: 99 | "Query the data in Structured Query Language(SQL). The table name is the file name without extension", 100 | parser: |query|{ 101 | Ok(AppAction::SqlQuery(query.to_owned())) 102 | }, 103 | }, 104 | Entry { 105 | prefix: Prefix::ShortAndLong(":q", ":quit"), 106 | usage: ":q", 107 | description: "Quit Tabler", 108 | parser: |_|{ 109 | Ok(AppAction::Quit) 110 | }, 111 | }, 112 | Entry { 113 | prefix: Prefix::Long(":goto"), 114 | usage: ":goto ", 115 | description: "Jumps to the line", 116 | parser: |line|{ 117 | Ok(AppAction::TabularGoto( 118 | line.parse::()?.saturating_sub(1), 119 | )) 120 | }, 121 | }, 122 | Entry { 123 | prefix: Prefix::Long(":goup"), 124 | usage: ":goup ", 125 | description: "Jump line(s) up", 126 | parser: |lines|{ 127 | Ok(match lines { 128 | "page" => AppAction::TabularGoUpFullPage, 129 | "half" => AppAction::TabularGoUpHalfPage, 130 | _ => AppAction::TabularGoUp(lines.parse()?), 131 | }) 132 | }, 133 | }, 134 | Entry { 135 | prefix: Prefix::Long(":godown"), 136 | usage: ":godown ", 137 | description: "Jump line(s) down", 138 | parser: |lines|{ 139 | Ok(match lines { 140 | "page" => AppAction::TabularGoDownFullPage, 141 | "half" => AppAction::TabularGoDownHalfPage, 142 | _ => AppAction::TabularGoDown(lines.parse()?), 143 | }) 144 | }, 145 | }, 146 | Entry { 147 | prefix: Prefix::Long(":reset"), 148 | usage: ":reset", 149 | description: "Reset the original data frame", 150 | parser: |_|{ 151 | Ok(AppAction::TabularReset) 152 | }, 153 | }, 154 | Entry { 155 | prefix: Prefix::Long(":help"), 156 | usage: ":help", 157 | description: "Show help menu", 158 | parser: |_|{ 159 | Ok(AppAction::Help) 160 | }, 161 | }, 162 | Entry { 163 | prefix: Prefix::ShortAndLong(":S", ":select"), 164 | usage: ":select ", 165 | description: "Query current data frame for columns/functions", 166 | parser: |query|{ 167 | Ok(AppAction::TabularSelect(query.to_owned())) 168 | }, 169 | }, 170 | Entry { 171 | prefix: Prefix::ShortAndLong(":F", ":filter"), 172 | usage: ":filter ", 173 | description: "Filter current data frame, keeping rows were the condition(s) match", 174 | parser: |query|{ 175 | Ok(AppAction::TabularFilter(query.to_owned())) 176 | }, 177 | }, 178 | Entry { 179 | prefix: Prefix::ShortAndLong(":O", ":order"), 180 | usage: ":order ", 181 | description: "Sort current data frame by column(s)", 182 | parser: |query|{ 183 | Ok(AppAction::TabularOrder(query.to_owned())) 184 | }, 185 | }, 186 | Entry { 187 | prefix: Prefix::Long(":schema"), 188 | usage: ":schema", 189 | description: "Show loaded data frame(s), their schmea(s), and their path(s)", 190 | parser: |_|{ 191 | Ok(AppAction::SqlSchema) 192 | }, 193 | }, 194 | Entry { 195 | prefix: Prefix::Long(":rand"), 196 | usage: ":rand", 197 | description: "Select a random row from current data frame", 198 | parser: |_|{ 199 | Ok(AppAction::TabularGotoRandom) 200 | }, 201 | }, 202 | Entry { 203 | prefix: Prefix::Long(":view"), 204 | usage: ":view (table | sheet | switch)", 205 | description: "Change tabular's view to table or sheet", 206 | parser: |query|{ 207 | Ok(match query { 208 | "table" => AppAction::TabularTableView, 209 | "sheet" => AppAction::TabularSheetView, 210 | "switch" => AppAction::TabularSwitchView, 211 | _ => Err("Invalid view")?, 212 | }) 213 | }, 214 | }, 215 | Entry { 216 | prefix: Prefix::Long(":tabn"), 217 | usage: ":tabn ", 218 | description: "Create a new tab with the query", 219 | parser: |query|{ 220 | Ok(AppAction::TabNew(query.to_owned())) 221 | }, 222 | }, 223 | Entry { 224 | prefix: Prefix::Long(":tabr"), 225 | usage: ":tabr ", 226 | description: "Remove the tab at the index", 227 | parser: |query|{ 228 | Ok(AppAction::TabRemove(query.parse()?)) 229 | }, 230 | }, 231 | Entry { 232 | prefix: Prefix::Long(":tab"), 233 | usage: ":tab ", 234 | description: "Select the tab at the index", 235 | parser: |query|{ 236 | Ok(AppAction::TabSelect(query.parse()?)) 237 | }, 238 | }, 239 | Entry { 240 | prefix: Prefix::Long(":export"), 241 | usage: ":export ", 242 | description: "Select the tab at the index", 243 | parser: |query| { 244 | let (fmt, path_str) = query.split_once(' ') 245 | .ok_or("Export argument should only contain format and path")?; 246 | match fmt { 247 | "csv" => { 248 | Ok( 249 | AppAction::ExportDsv{ 250 | path: path_str.into() , 251 | separator: ',', 252 | quote: '"', 253 | header: true } 254 | ) 255 | } 256 | 257 | "tsv" => { 258 | Ok( 259 | AppAction::ExportDsv{ 260 | path: path_str.into() , 261 | separator: '\t', 262 | quote: '"', 263 | header: true } 264 | ) 265 | } 266 | 267 | "parquet" => { 268 | Ok(AppAction::ExportParquet(path_str.into())) 269 | } 270 | 271 | "json" => { 272 | Ok(AppAction::ExportJson(path_str.into(), JsonFormat::Json)) 273 | } 274 | 275 | "jsonl" => { 276 | Ok(AppAction::ExportJson(path_str.into(), JsonFormat::JsonLine)) 277 | } 278 | 279 | "arrow" => { 280 | Ok(AppAction::ExportArrow(path_str.into())) 281 | } 282 | 283 | _ => { 284 | Err("Unsupported format. Supported ones: csv, tsv, parquet, json, jsonl, and arrow".into()) 285 | } 286 | } 287 | }, 288 | }, 289 | ]; 290 | -------------------------------------------------------------------------------- /src/handler/event.rs: -------------------------------------------------------------------------------- 1 | use crate::AppResult; 2 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time::{Duration, Instant}; 6 | 7 | /// Terminal events. 8 | #[derive(Clone, Copy, Debug)] 9 | pub enum Event { 10 | /// Terminal tick. 11 | Tick, 12 | /// Key press. 13 | Key(KeyEvent), 14 | /// Mouse click/scroll. 15 | Mouse(MouseEvent), 16 | /// Terminal resize. 17 | Resize(u16, u16), 18 | } 19 | 20 | /// Terminal event handler. 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | pub struct EventHandler { 24 | /// Event sender channel. 25 | sender: mpsc::Sender, 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | /// Event handler thread. 29 | handler: thread::JoinHandle<()>, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let tick_rate = Duration::from_millis(tick_rate); 36 | let (sender, receiver) = mpsc::channel(); 37 | let handler = { 38 | let sender = sender.clone(); 39 | thread::spawn(move || { 40 | let mut last_tick = Instant::now(); 41 | loop { 42 | let timeout = tick_rate 43 | .checked_sub(last_tick.elapsed()) 44 | .unwrap_or(tick_rate); 45 | 46 | if event::poll(timeout).expect("failed to poll new events") { 47 | match event::read().expect("unable to read event") { 48 | CrosstermEvent::Key(e) => sender.send(Event::Key(e)), 49 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 50 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 51 | CrosstermEvent::FocusGained => Ok(()), 52 | CrosstermEvent::FocusLost => Ok(()), 53 | CrosstermEvent::Paste(_) => unimplemented!(), 54 | } 55 | .expect("failed to send terminal event") 56 | } 57 | 58 | if last_tick.elapsed() >= tick_rate { 59 | sender.send(Event::Tick).expect("failed to send tick event"); 60 | last_tick = Instant::now(); 61 | } 62 | } 63 | }) 64 | }; 65 | Self { 66 | sender, 67 | receiver, 68 | handler, 69 | } 70 | } 71 | 72 | /// Receive the next event from the handler thread. 73 | /// 74 | /// This function will always block the current thread if 75 | /// there is no data available and it's possible for more data to be sent. 76 | pub fn next(&self) -> AppResult { 77 | Ok(self.receiver.recv()?) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/handler/keybind.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | use crate::{app::AppAction, app::AppState}; 6 | 7 | #[derive(Debug, PartialEq, Eq, Hash)] 8 | enum StateKey { 9 | Exact(AppState, KeyCode, KeyModifiers), 10 | KeyCode(KeyCode, KeyModifiers), 11 | State(AppState), 12 | } 13 | pub type Action = AppAction; 14 | pub struct Keybind { 15 | map: HashMap, 16 | } 17 | 18 | impl Default for Keybind { 19 | fn default() -> Self { 20 | Self { 21 | map: [ 22 | // Clear error 23 | (StateKey::State(AppState::Error), AppAction::StatusBarStats), 24 | // Close app/tab/sheet 25 | ( 26 | StateKey::Exact(AppState::Empty, KeyCode::Char('q'), KeyModifiers::empty()), 27 | AppAction::Quit, 28 | ), 29 | ( 30 | StateKey::Exact(AppState::Sheet, KeyCode::Char('q'), KeyModifiers::empty()), 31 | AppAction::TabularTableView, 32 | ), 33 | ( 34 | StateKey::Exact(AppState::Table, KeyCode::Char('q'), KeyModifiers::empty()), 35 | AppAction::TabRemoveOrQuit, 36 | ), 37 | // Switch tab/sheet 38 | ( 39 | StateKey::Exact(AppState::Table, KeyCode::Char('v'), KeyModifiers::empty()), 40 | AppAction::TabularSheetView, 41 | ), 42 | ( 43 | StateKey::Exact(AppState::Sheet, KeyCode::Char('v'), KeyModifiers::empty()), 44 | AppAction::TabularTableView, 45 | ), 46 | // Move half page 47 | ( 48 | StateKey::Exact(AppState::Table, KeyCode::Char('u'), KeyModifiers::CONTROL), 49 | AppAction::TabularGoUpHalfPage, 50 | ), 51 | ( 52 | StateKey::Exact(AppState::Table, KeyCode::Char('d'), KeyModifiers::CONTROL), 53 | AppAction::TabularGoDownHalfPage, 54 | ), 55 | // Move full page 56 | ( 57 | StateKey::Exact(AppState::Table, KeyCode::PageUp, KeyModifiers::empty()), 58 | AppAction::TabularGoUpFullPage, 59 | ), 60 | ( 61 | StateKey::Exact(AppState::Table, KeyCode::PageDown, KeyModifiers::empty()), 62 | AppAction::TabularGoDownFullPage, 63 | ), 64 | ( 65 | StateKey::Exact(AppState::Table, KeyCode::Char('b'), KeyModifiers::CONTROL), 66 | AppAction::TabularGoUpFullPage, 67 | ), 68 | ( 69 | StateKey::Exact(AppState::Table, KeyCode::Char('f'), KeyModifiers::CONTROL), 70 | AppAction::TabularGoDownFullPage, 71 | ), 72 | // Move to prev/next record 73 | ( 74 | StateKey::Exact(AppState::Table, KeyCode::Up, KeyModifiers::empty()), 75 | AppAction::TabularGoUp(1), 76 | ), 77 | ( 78 | StateKey::Exact(AppState::Table, KeyCode::Down, KeyModifiers::empty()), 79 | AppAction::TabularGoDown(1), 80 | ), 81 | ( 82 | StateKey::Exact(AppState::Table, KeyCode::Char('k'), KeyModifiers::empty()), 83 | AppAction::TabularGoUp(1), 84 | ), 85 | ( 86 | StateKey::Exact(AppState::Table, KeyCode::Char('j'), KeyModifiers::empty()), 87 | AppAction::TabularGoDown(1), 88 | ), 89 | ( 90 | StateKey::Exact(AppState::Sheet, KeyCode::Right, KeyModifiers::empty()), 91 | AppAction::TabularGoDown(1), 92 | ), 93 | ( 94 | StateKey::Exact(AppState::Sheet, KeyCode::Left, KeyModifiers::empty()), 95 | AppAction::TabularGoUp(1), 96 | ), 97 | ( 98 | StateKey::Exact(AppState::Sheet, KeyCode::Char('h'), KeyModifiers::empty()), 99 | AppAction::TabularGoUp(1), 100 | ), 101 | ( 102 | StateKey::Exact(AppState::Sheet, KeyCode::Char('l'), KeyModifiers::empty()), 103 | AppAction::TabularGoDown(1), 104 | ), 105 | // Move to first/last record 106 | ( 107 | StateKey::Exact(AppState::Sheet, KeyCode::Home, KeyModifiers::empty()), 108 | AppAction::TabularGotoFirst, 109 | ), 110 | ( 111 | StateKey::Exact(AppState::Sheet, KeyCode::End, KeyModifiers::empty()), 112 | AppAction::TabularGotoLast, 113 | ), 114 | ( 115 | StateKey::Exact(AppState::Table, KeyCode::Home, KeyModifiers::empty()), 116 | AppAction::TabularGotoFirst, 117 | ), 118 | ( 119 | StateKey::Exact(AppState::Table, KeyCode::End, KeyModifiers::empty()), 120 | AppAction::TabularGotoLast, 121 | ), 122 | ( 123 | StateKey::Exact(AppState::Sheet, KeyCode::Char('g'), KeyModifiers::empty()), 124 | AppAction::TabularGotoFirst, 125 | ), 126 | ( 127 | StateKey::Exact(AppState::Sheet, KeyCode::Char('G'), KeyModifiers::SHIFT), 128 | AppAction::TabularGotoLast, 129 | ), 130 | ( 131 | StateKey::Exact(AppState::Table, KeyCode::Char('g'), KeyModifiers::empty()), 132 | AppAction::TabularGotoFirst, 133 | ), 134 | ( 135 | StateKey::Exact(AppState::Table, KeyCode::Char('G'), KeyModifiers::SHIFT), 136 | AppAction::TabularGotoLast, 137 | ), 138 | // Scroll up/down in sheets 139 | ( 140 | StateKey::Exact(AppState::Sheet, KeyCode::Up, KeyModifiers::empty()), 141 | AppAction::SheetScrollUp, 142 | ), 143 | ( 144 | StateKey::Exact(AppState::Sheet, KeyCode::Down, KeyModifiers::empty()), 145 | AppAction::SheetScrollDown, 146 | ), 147 | ( 148 | StateKey::Exact(AppState::Sheet, KeyCode::Char('k'), KeyModifiers::empty()), 149 | AppAction::SheetScrollUp, 150 | ), 151 | ( 152 | StateKey::Exact(AppState::Sheet, KeyCode::Char('j'), KeyModifiers::empty()), 153 | AppAction::SheetScrollDown, 154 | ), 155 | // Move prev/next tab 156 | ( 157 | StateKey::Exact(AppState::Table, KeyCode::Char('H'), KeyModifiers::SHIFT), 158 | AppAction::TabSelectedPrev, 159 | ), 160 | ( 161 | StateKey::Exact(AppState::Table, KeyCode::Char('L'), KeyModifiers::SHIFT), 162 | AppAction::TabSelectedNext, 163 | ), 164 | ( 165 | StateKey::Exact(AppState::Sheet, KeyCode::Char('H'), KeyModifiers::SHIFT), 166 | AppAction::TabSelectedPrev, 167 | ), 168 | ( 169 | StateKey::Exact(AppState::Sheet, KeyCode::Char('L'), KeyModifiers::SHIFT), 170 | AppAction::TabSelectedNext, 171 | ), 172 | // Move to line by number 173 | ( 174 | StateKey::Exact(AppState::Table, KeyCode::Char('1'), KeyModifiers::empty()), 175 | AppAction::StatusBarCommand("goto 1".to_owned()), 176 | ), 177 | ( 178 | StateKey::Exact(AppState::Table, KeyCode::Char('2'), KeyModifiers::empty()), 179 | AppAction::StatusBarCommand("goto 2".to_owned()), 180 | ), 181 | ( 182 | StateKey::Exact(AppState::Table, KeyCode::Char('3'), KeyModifiers::empty()), 183 | AppAction::StatusBarCommand("goto 3".to_owned()), 184 | ), 185 | ( 186 | StateKey::Exact(AppState::Table, KeyCode::Char('4'), KeyModifiers::empty()), 187 | AppAction::StatusBarCommand("goto 4".to_owned()), 188 | ), 189 | ( 190 | StateKey::Exact(AppState::Table, KeyCode::Char('5'), KeyModifiers::empty()), 191 | AppAction::StatusBarCommand("goto 5".to_owned()), 192 | ), 193 | ( 194 | StateKey::Exact(AppState::Table, KeyCode::Char('6'), KeyModifiers::empty()), 195 | AppAction::StatusBarCommand("goto 6".to_owned()), 196 | ), 197 | ( 198 | StateKey::Exact(AppState::Table, KeyCode::Char('7'), KeyModifiers::empty()), 199 | AppAction::StatusBarCommand("goto 7".to_owned()), 200 | ), 201 | ( 202 | StateKey::Exact(AppState::Table, KeyCode::Char('8'), KeyModifiers::empty()), 203 | AppAction::StatusBarCommand("goto 8".to_owned()), 204 | ), 205 | ( 206 | StateKey::Exact(AppState::Table, KeyCode::Char('9'), KeyModifiers::empty()), 207 | AppAction::StatusBarCommand("goto 9".to_owned()), 208 | ), 209 | // Select Random 210 | ( 211 | StateKey::Exact(AppState::Table, KeyCode::Char('R'), KeyModifiers::SHIFT), 212 | AppAction::TabularGotoRandom, 213 | ), 214 | ] 215 | .into_iter() 216 | .collect(), 217 | } 218 | } 219 | } 220 | 221 | impl Keybind { 222 | pub fn get_action(&self, state: AppState, key_event: KeyEvent) -> Option<&Action> { 223 | self.map 224 | .get(&StateKey::Exact(state, key_event.code, key_event.modifiers)) 225 | .or(self 226 | .map 227 | .get(&StateKey::KeyCode(key_event.code, key_event.modifiers))) 228 | .or(self.map.get(&StateKey::State(state))) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod event; 3 | pub mod keybind; 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | 3 | /// User interface. 4 | pub mod tui; 5 | 6 | /// Utils 7 | pub mod utils; 8 | 9 | /// CLI arguments 10 | pub mod args; 11 | 12 | /// SQL 13 | pub mod sql; 14 | 15 | /// Event, keybind, and commands 16 | pub mod handler; 17 | 18 | /// App 19 | pub mod app; 20 | 21 | /// Readers 22 | pub mod reader; 23 | 24 | /// Writers 25 | pub mod writer; 26 | 27 | pub type AppResult = std::result::Result>; 28 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use itertools::Itertools; 3 | use polars::frame::DataFrame; 4 | use ratatui::backend::CrosstermBackend; 5 | use std::fs::{self, File}; 6 | use std::io::{self, Cursor, Read}; 7 | use std::path::PathBuf; 8 | use std::str::FromStr; 9 | use tabler::app::App; 10 | use tabler::args::{AppTheme, Args}; 11 | use tabler::handler::command::parse_into_action; 12 | use tabler::handler::event::{Event, EventHandler}; 13 | use tabler::handler::keybind::Keybind; 14 | use tabler::reader::BuildReader; 15 | use tabler::sql::SqlBackend; 16 | use tabler::tui::{themes, Styler}; 17 | use tabler::tui::{Tabular, TabularType, Terminal}; 18 | use tabler::AppResult; 19 | 20 | fn main() -> AppResult<()> { 21 | // Parse CLI 22 | let args = Args::parse(); 23 | 24 | // Create the sql backend. 25 | let mut sql_backend = SqlBackend::new(); 26 | 27 | // Loading files to data frames 28 | let mut tabs = args 29 | .files 30 | .iter() 31 | .map(|path| { 32 | let reader = args.build_reader(path); 33 | let name = path 34 | .file_stem() 35 | .expect("Invalid file name") 36 | .to_string_lossy() 37 | .into_owned(); 38 | 39 | let df = reader 40 | .read_to_data_frame(File::open(path).unwrap_or_else(|err| panic!("{}", err))) 41 | .unwrap_or_else(|err| panic!("{}", err)); 42 | let name = sql_backend.register(&name, df.clone(), path.clone()); 43 | (df, name) 44 | }) 45 | .collect_vec(); 46 | if tabs.is_empty() { 47 | let reader = args.build_reader("stdin"); 48 | let mut buf = Vec::new(); 49 | io::stdin().read_to_end(&mut buf)?; 50 | let df = reader.read_to_data_frame(Cursor::new(buf))?; 51 | let name = sql_backend.register("stdin", df.clone(), PathBuf::from_str("stdin")?); 52 | tabs.push((df, name)); 53 | } 54 | 55 | let script = if let Some(path) = args.script { 56 | fs::read_to_string(path).unwrap_or_else(|err| panic!("{}", err)) 57 | } else { 58 | Default::default() 59 | }; 60 | 61 | match args.theme { 62 | AppTheme::Monokai => start_tui::(tabs, sql_backend, script), 63 | AppTheme::Argonaut => start_tui::(tabs, sql_backend, script), 64 | AppTheme::Terminal => start_tui::(tabs, sql_backend, script), 65 | } 66 | } 67 | 68 | fn start_tui( 69 | tabs: Vec<(DataFrame, String)>, 70 | sql_backend: SqlBackend, 71 | script: String, 72 | ) -> AppResult<()> { 73 | let tabs = tabs 74 | .into_iter() 75 | .map(|(df, name)| Tabular::new(df, TabularType::Name(name))) 76 | .collect(); 77 | let keybind = Keybind::default(); 78 | let mut app = App::new(tabs, sql_backend, keybind); 79 | 80 | // Initialize the terminal user interface. 81 | let mut tui = Terminal::new( 82 | ratatui::Terminal::new(CrosstermBackend::new(io::stderr()))?, 83 | EventHandler::new(250), 84 | ); 85 | tui.init()?; 86 | 87 | tui.draw::(&mut app)?; 88 | for line in script.lines().filter(|line| !line.is_empty()) { 89 | let action = parse_into_action(line) 90 | .unwrap_or_else(|err| panic!("Error in startup script: {}", err)); 91 | app.invoke(action) 92 | .unwrap_or_else(|err| panic!("Error in startup script: {}", err)); 93 | } 94 | 95 | // Run the main loop 96 | while app.running() { 97 | tui.draw::(&mut app)?; 98 | 99 | match tui.events.next()? { 100 | Event::Tick => app.tick()?, 101 | Event::Key(key_event) => { 102 | #[cfg(target_os = "windows")] 103 | { 104 | use crossterm::event::KeyEventKind; 105 | if matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) { 106 | app.handle_key_event(key_event)? 107 | } 108 | } 109 | #[cfg(not(target_os = "windows"))] 110 | { 111 | app.handle_key_event(key_event)? 112 | } 113 | } 114 | Event::Mouse(_) => {} 115 | Event::Resize(_, _) => {} 116 | } 117 | } 118 | 119 | // Exit the user interface. 120 | tui.exit()?; 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /src/reader/fwf.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | io::{Cursor, Read}, 4 | iter::once, 5 | }; 6 | 7 | use fwf_rs::Reader; 8 | use itertools::Itertools; 9 | use polars::{frame::DataFrame, prelude::NamedFrom, series::Series}; 10 | 11 | use crate::{ 12 | args::{Args, InferSchema}, 13 | utils::{safe_infer_schema, ZipItersExt}, 14 | AppResult, 15 | }; 16 | 17 | use super::ReadToDataFrame; 18 | 19 | pub struct ReadFwfToDataFrame { 20 | width_str: String, 21 | has_header: bool, 22 | separator_length: usize, 23 | flexible_width: bool, 24 | infer_schema: InferSchema, 25 | } 26 | 27 | impl ReadFwfToDataFrame { 28 | pub fn from_args(args: &Args) -> Self { 29 | Self { 30 | width_str: args.widths.to_owned(), 31 | has_header: !args.no_header, 32 | separator_length: args.separator_length, 33 | flexible_width: !args.no_flexible_width, 34 | infer_schema: args.infer_schema, 35 | } 36 | } 37 | } 38 | 39 | impl ReadToDataFrame for ReadFwfToDataFrame { 40 | fn read_to_data_frame(&self, mut reader: R) -> AppResult { 41 | let file_content = { 42 | let mut buf = String::new(); 43 | reader.read_to_string(&mut buf)?; 44 | buf 45 | }; 46 | let widths = if self.width_str.is_empty() { 47 | let common_space_indices = file_content 48 | .lines() 49 | .map(|line| { 50 | let length = line.chars().count(); 51 | let spaces = line 52 | .chars() 53 | .enumerate() 54 | .filter_map(|(i, c)| c.is_whitespace().then_some(i)) 55 | .collect::>(); 56 | (length, spaces) 57 | }) 58 | .reduce(|(la, sa), (lb, sb)| (la.max(lb), sa.intersection(&sb).copied().collect())) 59 | .map(|(len, idx_set)| idx_set.into_iter().chain(once(len)).sorted().collect_vec()) 60 | .unwrap_or_default(); 61 | infer_widths(common_space_indices) 62 | } else { 63 | parse_width(&self.width_str)? 64 | }; 65 | 66 | let reader = Reader::new( 67 | Cursor::new(file_content), 68 | widths.clone(), 69 | self.separator_length, 70 | self.flexible_width, 71 | self.has_header, 72 | )?; 73 | let header = reader 74 | .header() 75 | .map(|rec| rec.iter().map(|slice| slice.trim().to_owned()).collect()) 76 | .unwrap_or_else(|| { 77 | (0..widths.len()) 78 | .map(|idx| format!("column_{}", idx + 1)) 79 | .collect_vec() 80 | }); 81 | 82 | let columns = reader 83 | .records() 84 | .filter_map(Result::ok) 85 | .map(|record| { 86 | record 87 | .iter() 88 | .map(str::trim) 89 | .map(ToOwned::to_owned) 90 | .collect_vec() 91 | .into_iter() 92 | }) 93 | .zip_iters() 94 | .collect_vec(); 95 | 96 | let mut df = DataFrame::new( 97 | header 98 | .into_iter() 99 | .zip(columns) 100 | .map(|(name, values)| Series::new(name.into(), values)) 101 | .collect_vec(), 102 | )?; 103 | 104 | if matches!( 105 | self.infer_schema, 106 | InferSchema::Fast | InferSchema::Full | InferSchema::Safe 107 | ) { 108 | safe_infer_schema(&mut df); 109 | } 110 | 111 | Ok(df) 112 | } 113 | } 114 | 115 | fn parse_width(widths: impl AsRef) -> AppResult> { 116 | Ok(widths 117 | .as_ref() 118 | .split(',') 119 | .map(|w| w.parse::()) 120 | .collect::, _>>()?) 121 | } 122 | 123 | fn infer_widths(space_indices: Vec) -> Vec { 124 | let mut indices = Vec::default(); 125 | let mut start = 0; 126 | // let chars = line.chars().collect_vec(); 127 | for (i, idx) in space_indices.iter().enumerate() { 128 | if let Some(nidx) = space_indices.get(i + 1) { 129 | if nidx - idx > 1 { 130 | indices.push(idx - start); 131 | start = idx + 1 132 | } 133 | } else { 134 | indices.push(idx - start); 135 | } 136 | } 137 | indices 138 | } 139 | -------------------------------------------------------------------------------- /src/reader/mod.rs: -------------------------------------------------------------------------------- 1 | mod fwf; 2 | 3 | use std::path::Path; 4 | 5 | use fwf::ReadFwfToDataFrame; 6 | use polars::{ 7 | frame::DataFrame, 8 | io::{mmap::MmapBytesReader, SerReader}, 9 | prelude::{ 10 | CsvParseOptions, CsvReadOptions, IpcReader, JsonLineReader, JsonReader, ParquetReader, 11 | }, 12 | }; 13 | 14 | use crate::{ 15 | args::{Args, Format, InferSchema}, 16 | utils::{as_ascii, safe_infer_schema}, 17 | AppResult, 18 | }; 19 | 20 | pub trait ReadToDataFrame { 21 | fn read_to_data_frame(&self, reader: R) -> AppResult; 22 | } 23 | 24 | pub trait BuildReader { 25 | fn build_reader>(&self, path: P) -> Box>; 26 | } 27 | 28 | impl BuildReader for Args { 29 | fn build_reader>(&self, path: P) -> Box> { 30 | match self.format { 31 | Some(Format::Dsv) => Box::new(CsvToDataFrame::from_args(self)), 32 | Some(Format::Parquet) => Box::new(ParquetToDataFrame), 33 | Some(Format::Json) => Box::new(JsonToDataFrame::from_args(self)), 34 | Some(Format::Jsonl) => Box::new(JsonLineToDataFrame::from_args(self)), 35 | Some(Format::Arrow) => Box::new(ArrowIpcToDataFrame), 36 | Some(Format::Fwf) => Box::new(ReadFwfToDataFrame::from_args(self)), 37 | None => match path.as_ref().extension().and_then(|ext| ext.to_str()) { 38 | Some("tsv") => { 39 | let mut reader = CsvToDataFrame::from_args(self); 40 | reader.separator_char = '\t'; 41 | Box::new(reader) 42 | } 43 | Some("parquet") => Box::new(ParquetToDataFrame), 44 | Some("json") => Box::new(JsonToDataFrame::from_args(self)), 45 | Some("jsonl") => Box::new(JsonLineToDataFrame::from_args(self)), 46 | Some("arrow") => Box::new(ArrowIpcToDataFrame), 47 | Some("fwf") => Box::new(ReadFwfToDataFrame::from_args(self)), 48 | _ => Box::new(CsvToDataFrame::from_args(self)), 49 | }, 50 | } 51 | } 52 | } 53 | 54 | pub struct CsvToDataFrame { 55 | infer_schema: InferSchema, 56 | quote_char: char, 57 | separator_char: char, 58 | no_header: bool, 59 | ignore_errors: bool, 60 | } 61 | 62 | impl CsvToDataFrame { 63 | pub fn from_args(args: &Args) -> Self { 64 | Self { 65 | infer_schema: args.infer_schema, 66 | quote_char: args.quote_char, 67 | separator_char: args.separator, 68 | no_header: args.no_header, 69 | ignore_errors: args.ignore_errors, 70 | } 71 | } 72 | } 73 | 74 | impl ReadToDataFrame for CsvToDataFrame { 75 | fn read_to_data_frame(&self, reader: R) -> AppResult { 76 | let mut df = CsvReadOptions::default() 77 | .with_ignore_errors(self.ignore_errors) 78 | .with_infer_schema_length(self.infer_schema.to_csv_infer_schema_length()) 79 | .with_has_header(!self.no_header) 80 | .with_parse_options( 81 | CsvParseOptions::default() 82 | .with_quote_char(as_ascii(self.quote_char)) 83 | .with_separator(as_ascii(self.separator_char).expect("Invalid separator")), 84 | ) 85 | .into_reader_with_file_handle(reader) 86 | .finish()?; 87 | if matches!(self.infer_schema, InferSchema::Safe) { 88 | safe_infer_schema(&mut df); 89 | } 90 | Ok(df) 91 | } 92 | } 93 | 94 | pub struct ParquetToDataFrame; 95 | 96 | impl ReadToDataFrame for ParquetToDataFrame { 97 | fn read_to_data_frame(&self, reader: R) -> AppResult { 98 | Ok(ParquetReader::new(reader).set_rechunk(true).finish()?) 99 | } 100 | } 101 | 102 | pub struct JsonLineToDataFrame { 103 | infer_schema: InferSchema, 104 | ignore_errors: bool, 105 | } 106 | 107 | impl JsonLineToDataFrame { 108 | pub fn from_args(args: &Args) -> Self { 109 | Self { 110 | infer_schema: args.infer_schema, 111 | ignore_errors: args.ignore_errors, 112 | } 113 | } 114 | } 115 | 116 | impl ReadToDataFrame for JsonLineToDataFrame { 117 | fn read_to_data_frame(&self, reader: R) -> AppResult { 118 | let mut df = JsonLineReader::new(reader) 119 | .with_rechunk(true) 120 | .infer_schema_len(None) 121 | .with_ignore_errors(self.ignore_errors) 122 | .finish()?; 123 | if matches!( 124 | self.infer_schema, 125 | InferSchema::Safe | InferSchema::Full | InferSchema::Fast 126 | ) { 127 | safe_infer_schema(&mut df); 128 | } 129 | Ok(df) 130 | } 131 | } 132 | 133 | pub struct JsonToDataFrame { 134 | infer_schema: InferSchema, 135 | ignore_errors: bool, 136 | } 137 | 138 | impl JsonToDataFrame { 139 | pub fn from_args(args: &Args) -> Self { 140 | Self { 141 | infer_schema: args.infer_schema, 142 | ignore_errors: args.ignore_errors, 143 | } 144 | } 145 | } 146 | 147 | impl ReadToDataFrame for JsonToDataFrame { 148 | fn read_to_data_frame(&self, reader: R) -> AppResult { 149 | let mut df = JsonReader::new(reader) 150 | .set_rechunk(true) 151 | .infer_schema_len(None) 152 | .with_ignore_errors(self.ignore_errors) 153 | .finish()?; 154 | if matches!( 155 | self.infer_schema, 156 | InferSchema::Safe | InferSchema::Full | InferSchema::Fast 157 | ) { 158 | safe_infer_schema(&mut df); 159 | } 160 | Ok(df) 161 | } 162 | } 163 | 164 | pub struct ArrowIpcToDataFrame; 165 | 166 | impl ReadToDataFrame for ArrowIpcToDataFrame { 167 | fn read_to_data_frame(&self, reader: R) -> AppResult { 168 | Ok(IpcReader::new(reader).set_rechunk(true).finish()?) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/sql.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, path::PathBuf}; 2 | 3 | use itertools::Itertools; 4 | use polars::{ 5 | error::PolarsResult, 6 | frame::DataFrame, 7 | prelude::{IntoLazy, LazyFrame, NamedFrom}, 8 | series::Series, 9 | }; 10 | use polars_sql::SQLContext; 11 | 12 | pub struct SqlBackend { 13 | sql: SQLContext, 14 | tables: BTreeMap, 15 | } 16 | 17 | impl SqlBackend { 18 | pub fn new() -> Self { 19 | Self { 20 | sql: SQLContext::new(), 21 | tables: Default::default(), 22 | } 23 | } 24 | 25 | pub fn schema(&self) -> DataFrame { 26 | let (tables, structures, paths) = self.tables.iter().fold( 27 | (Vec::new(), Vec::new(), Vec::new()), 28 | |(mut vt, mut vs, mut vp), (t, (s, p))| { 29 | vt.push(t.to_owned()); 30 | vs.push(s.to_owned()); 31 | vp.push(p.to_string_lossy().into_owned()); 32 | (vt, vs, vp) 33 | }, 34 | ); 35 | 36 | DataFrame::new( 37 | [ 38 | Series::new("Table".into(), tables), 39 | Series::new("Structure".into(), structures), 40 | Series::new("Path".into(), paths), 41 | ] 42 | .into(), 43 | ) 44 | .expect("Invalid SQL backed state") 45 | } 46 | 47 | pub fn contains_dataframe(&self, name: &str) -> bool { 48 | self.tables.contains_key(name) 49 | } 50 | 51 | pub fn register(&mut self, name: &str, data_frame: DataFrame, path: PathBuf) -> String { 52 | if let Some(name) = TableNameGen::with(name).find(|name| !self.tables.contains_key(name)) { 53 | self.tables 54 | .insert(name.clone(), (data_frame_structure(&data_frame), path)); 55 | self.sql.register(&name, data_frame.lazy()); 56 | name 57 | } else { 58 | panic!("Not implemented") 59 | } 60 | } 61 | 62 | pub fn execute(&mut self, query: &str) -> PolarsResult { 63 | self.sql.execute(query).and_then(LazyFrame::collect) 64 | } 65 | } 66 | 67 | impl Default for SqlBackend { 68 | fn default() -> Self { 69 | Self::new() 70 | } 71 | } 72 | 73 | pub struct TableNameGen<'a> { 74 | base: &'a str, 75 | stage: u32, 76 | } 77 | 78 | impl<'a> TableNameGen<'a> { 79 | pub fn with(base: &'a str) -> Self { 80 | Self { base, stage: 0 } 81 | } 82 | } 83 | 84 | impl<'a> Iterator for TableNameGen<'a> { 85 | type Item = String; 86 | 87 | fn next(&mut self) -> Option { 88 | self.stage += 1; 89 | match self.stage { 90 | 1 => self.base.to_owned().into(), 91 | 2.. => format!("{}_{}", self.base, self.stage).into(), 92 | _ => unimplemented!(), 93 | } 94 | } 95 | } 96 | 97 | fn data_frame_structure(df: &DataFrame) -> String { 98 | format!( 99 | "({})", 100 | df.iter() 101 | .map(|series| format!("{} {}", series.name().trim(), series.dtype())) 102 | .join(", ") 103 | ) 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use polars::df; 109 | 110 | use super::*; 111 | 112 | #[test] 113 | fn test_table_name_gen() { 114 | let mut name_gen = TableNameGen::with("student"); 115 | assert_eq!(name_gen.next().unwrap(), "student"); 116 | assert_eq!(name_gen.next().unwrap(), "student_2"); 117 | assert_eq!(name_gen.next().unwrap(), "student_3"); 118 | assert_eq!(name_gen.next().unwrap(), "student_4"); 119 | } 120 | 121 | #[test] 122 | fn test_data_frame_structure() { 123 | // Create a sample DataFrame 124 | let df = df![ 125 | "name" => ["Alice", "Bob", "Charlie"], 126 | "age" => [25, 30, 35], 127 | " space " => [1, 1, 1], 128 | "salary" => [50000.0, 60000.0, 70000.0], 129 | "married" => [true, false, false], 130 | ] 131 | .unwrap(); 132 | 133 | // Expected output 134 | assert_eq!( 135 | data_frame_structure(&df), 136 | "(name str, age i32, space i32, salary f64, married bool)" 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod status_bar; 2 | pub mod tabs; 3 | pub mod tabular; 4 | pub mod terminal; 5 | pub mod theme; 6 | mod utils; 7 | pub mod widget; 8 | 9 | pub use tabular::{Tabular, TabularType}; 10 | pub use terminal::Terminal; 11 | pub use theme::Styler; 12 | 13 | pub mod themes { 14 | pub use super::theme::{Argonaut, Monokai, Terminal}; 15 | } 16 | -------------------------------------------------------------------------------- /src/tui/status_bar.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent}; 4 | use ratatui::{ 5 | layout::{Alignment, Rect}, 6 | style::Style, 7 | text::{Line, Span}, 8 | Frame, 9 | }; 10 | 11 | use crate::tui::theme::Styler; 12 | 13 | use super::widget::prompt::{Prompt, PromptState}; 14 | use crate::AppResult; 15 | 16 | #[derive(Debug)] 17 | pub struct StatusBar { 18 | state: StatusBarState, 19 | prompt_history: Vec, 20 | _theme: PhantomData, 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | pub enum StatusBarState { 25 | #[default] 26 | Info, 27 | Error(String), 28 | Prompt(PromptState), 29 | } 30 | 31 | impl Default for StatusBar { 32 | fn default() -> Self { 33 | Self { 34 | state: Default::default(), 35 | prompt_history: Default::default(), 36 | _theme: PhantomData, 37 | } 38 | } 39 | } 40 | 41 | impl StatusBar { 42 | pub fn state(&self) -> &StatusBarState { 43 | &self.state 44 | } 45 | 46 | pub fn show_info(&mut self) -> AppResult<()> { 47 | self.state = StatusBarState::Info; 48 | Ok(()) 49 | } 50 | 51 | pub fn show_error(&mut self, msg: impl ToString) -> AppResult<()> { 52 | self.state = StatusBarState::Error(msg.to_string()); 53 | Ok(()) 54 | } 55 | 56 | pub fn show_prompt(&mut self, prefix: impl AsRef) -> AppResult<()> { 57 | let mut history = self.prompt_history.clone(); 58 | history.push(format!(":{}", prefix.as_ref())); 59 | self.state = StatusBarState::Prompt(history.into()); 60 | Ok(()) 61 | } 62 | 63 | pub fn commit_prompt(&mut self) -> Option { 64 | if let StatusBarState::Prompt(prompt) = &self.state { 65 | let command = prompt.command(); 66 | self.prompt_history.push(command.clone()); 67 | Some(command) 68 | } else { 69 | None 70 | } 71 | } 72 | 73 | pub fn tick(&mut self) -> AppResult<()> { 74 | Ok(()) 75 | } 76 | 77 | pub fn input(&mut self, input: KeyEvent) -> AppResult<()> { 78 | if let StatusBarState::Prompt(prompt) = &mut self.state { 79 | match input.code { 80 | KeyCode::Up => { 81 | prompt.move_up().move_eol(); 82 | } 83 | KeyCode::Down => { 84 | prompt.move_down().move_eol(); 85 | } 86 | KeyCode::Left => { 87 | if prompt.cursor().1 > 1 { 88 | prompt.move_left(); 89 | } 90 | } 91 | KeyCode::Right => { 92 | prompt.move_right(); 93 | } 94 | 95 | KeyCode::Backspace => { 96 | if prompt.command_len() == 1 { 97 | self.show_info()?; 98 | } else if prompt.cursor().1 > 1 { 99 | prompt.delete_backward(); 100 | } 101 | } 102 | 103 | KeyCode::Delete => { 104 | prompt.delete(); 105 | } 106 | 107 | KeyCode::Home => { 108 | prompt.move_bol().move_right(); 109 | } 110 | 111 | KeyCode::End => { 112 | prompt.move_eol(); 113 | } 114 | 115 | KeyCode::PageUp | KeyCode::PageDown => (), 116 | 117 | KeyCode::Char(c) => { 118 | prompt.input_char(c); 119 | } 120 | 121 | _ => (), 122 | } 123 | } 124 | Ok(()) 125 | } 126 | 127 | pub fn render( 128 | &mut self, 129 | frame: &mut Frame, 130 | layout: Rect, 131 | info: &[(&str, &str)], 132 | ) -> AppResult<()> { 133 | match &mut self.state { 134 | StatusBarState::Info => frame.render_widget( 135 | Line::default() 136 | .spans( 137 | info.iter() 138 | .enumerate() 139 | .flat_map(|(i, (k, v))| info_key_value::(i, k, v)), 140 | ) 141 | .alignment(Alignment::Right) 142 | .style(Theme::status_bar_info()), 143 | layout, 144 | ), 145 | 146 | StatusBarState::Error(msg) => frame.render_widget( 147 | Line::raw(msg.as_str()) 148 | .alignment(Alignment::Center) 149 | .style(Theme::status_bar_error()), 150 | layout, 151 | ), 152 | 153 | StatusBarState::Prompt(text) => { 154 | frame.render_stateful_widget( 155 | Prompt::new( 156 | Theme::status_bar_prompt(), 157 | invert_style(Theme::status_bar_prompt()), 158 | ), 159 | layout, 160 | text, 161 | ); 162 | } 163 | } 164 | Ok(()) 165 | } 166 | } 167 | 168 | fn info_key_value<'a, Theme: Styler>(idx: usize, key: &'a str, value: &'a str) -> [Span<'a>; 3] { 169 | [ 170 | Span::raw(format!(" {} ", key)).style(Theme::status_bar_info_key(idx)), 171 | Span::raw(format!(" {} ", value)).style(Theme::status_bar_info_val(idx)), 172 | Span::raw(" "), 173 | ] 174 | } 175 | fn invert_style(mut style: Style) -> Style { 176 | std::mem::swap(&mut style.bg, &mut style.fg); 177 | style 178 | } 179 | -------------------------------------------------------------------------------- /src/tui/tabs.rs: -------------------------------------------------------------------------------- 1 | use crate::AppResult; 2 | 3 | use super::{tabular::Tabular, Styler}; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct Tabs { 7 | tabulars: Vec>, 8 | idx: usize, 9 | } 10 | 11 | impl Tabs { 12 | pub fn add(&mut self, tabular: Tabular) -> AppResult<()> { 13 | self.tabulars.push(tabular); 14 | Ok(()) 15 | } 16 | 17 | pub fn len(&self) -> usize { 18 | self.tabulars.len() 19 | } 20 | 21 | pub fn is_empty(&self) -> bool { 22 | self.len() == 0 23 | } 24 | 25 | pub fn idx(&self) -> usize { 26 | self.idx 27 | } 28 | 29 | pub fn selected(&self) -> Option<&Tabular> { 30 | self.tabulars.get(self.idx) 31 | } 32 | 33 | pub fn selected_mut(&mut self) -> Option<&mut Tabular> { 34 | self.tabulars.get_mut(self.idx) 35 | } 36 | 37 | pub fn remove(&mut self, idx: usize) -> AppResult<()> { 38 | self.validate_index(idx)?; 39 | self.tabulars.remove(idx); 40 | self.saturating_select(self.idx.saturating_sub(1)) 41 | } 42 | 43 | pub fn remove_selected(&mut self) -> AppResult<()> { 44 | self.remove(self.idx) 45 | } 46 | 47 | pub fn saturating_select(&mut self, idx: usize) -> AppResult<()> { 48 | self.idx = idx.min(self.tabulars.len().saturating_sub(1)); 49 | Ok(()) 50 | } 51 | 52 | pub fn select(&mut self, idx: usize) -> AppResult<()> { 53 | self.validate_index(idx)?; 54 | self.idx = idx; 55 | Ok(()) 56 | } 57 | 58 | pub fn select_next(&mut self) -> AppResult<()> { 59 | self.saturating_select(self.idx.saturating_add(1)) 60 | } 61 | 62 | pub fn select_prev(&mut self) -> AppResult<()> { 63 | self.saturating_select(self.idx.saturating_sub(1)) 64 | } 65 | 66 | pub fn select_first(&mut self) -> AppResult<()> { 67 | self.saturating_select(0) 68 | } 69 | 70 | pub fn select_last(&mut self) -> AppResult<()> { 71 | self.saturating_select(usize::MAX) 72 | } 73 | 74 | fn validate_index(&self, idx: usize) -> AppResult<()> { 75 | if self.tabulars.is_empty() { 76 | Err("no tab is currently available".into()) 77 | } else if idx < self.tabulars.len() { 78 | Ok(()) 79 | } else { 80 | Err(format!( 81 | "invalid tab index, valid index range is between 0 and {}", 82 | self.tabulars.len() 83 | ) 84 | .into()) 85 | } 86 | } 87 | 88 | pub fn iter(&self) -> impl Iterator> { 89 | self.tabulars.iter() 90 | } 91 | } 92 | 93 | impl FromIterator> for Tabs { 94 | fn from_iter>>(iter: T) -> Self { 95 | Self { 96 | tabulars: iter.into_iter().collect(), 97 | idx: 0, 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/tui/tabular.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use itertools::{izip, Itertools}; 4 | use polars::frame::DataFrame; 5 | use rand::Rng; 6 | use ratatui::{ 7 | layout::{Alignment, Margin, Rect}, 8 | text::{Line, Span}, 9 | widgets::{Block, Borders, Paragraph, Wrap}, 10 | Frame, 11 | }; 12 | 13 | use super::{ 14 | utils::{line_count, Scroll}, 15 | widget::data_frame_table::{DataFrameTable, DataFrameTableState}, 16 | }; 17 | use crate::tui::{theme::Styler, utils::any_value_into_string}; 18 | 19 | use crate::AppResult; 20 | 21 | #[derive(Debug)] 22 | pub enum TabularState { 23 | Table, 24 | Sheet(Scroll), 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum TabularType { 29 | Help, 30 | Schema, 31 | Name(String), 32 | Query(String), 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct Tabular { 37 | table_state: DataFrameTableState, 38 | state: TabularState, 39 | tabular_type: TabularType, 40 | _theme: PhantomData, 41 | } 42 | 43 | impl Tabular { 44 | /// Constructs a new instance of [`App`]. 45 | pub fn new(data_frame: DataFrame, tabular_type: TabularType) -> Self { 46 | Self { 47 | table_state: DataFrameTableState::new(data_frame), 48 | state: TabularState::Table, 49 | tabular_type, 50 | _theme: PhantomData, 51 | } 52 | } 53 | 54 | /// Handles the tick event of the terminal. 55 | pub fn tick(&mut self) -> AppResult<()> { 56 | Ok(()) 57 | } 58 | 59 | pub fn select_up(&mut self, len: usize) -> AppResult<()> { 60 | self.table_state.select_up(len); 61 | Ok(()) 62 | } 63 | 64 | pub fn select_down(&mut self, len: usize) -> AppResult<()> { 65 | self.table_state.select_down(len); 66 | Ok(()) 67 | } 68 | 69 | pub fn select_first(&mut self) -> AppResult<()> { 70 | self.table_state.select_first(); 71 | Ok(()) 72 | } 73 | 74 | pub fn select_last(&mut self) -> AppResult<()> { 75 | self.table_state.select_last(); 76 | Ok(()) 77 | } 78 | 79 | pub fn select_random(&mut self) -> AppResult<()> { 80 | let mut rng = rand::thread_rng(); 81 | self.table_state 82 | .select(rng.gen_range(0..self.table_state.height())); 83 | Ok(()) 84 | } 85 | 86 | pub fn select(&mut self, select: usize) -> AppResult<()> { 87 | self.table_state.select(select); 88 | Ok(()) 89 | } 90 | 91 | pub fn scroll_up(&mut self) -> AppResult<()> { 92 | if let TabularState::Sheet(scroll) = &mut self.state { 93 | scroll.up(); 94 | Ok(()) 95 | } else { 96 | Err("Not in table view".into()) 97 | } 98 | } 99 | 100 | pub fn scroll_down(&mut self) -> AppResult<()> { 101 | if let TabularState::Sheet(scroll) = &mut self.state { 102 | scroll.down(); 103 | Ok(()) 104 | } else { 105 | Err("Not in table view".into()) 106 | } 107 | } 108 | 109 | pub fn page_len(&self) -> usize { 110 | self.table_state.rendered_rows().into() 111 | } 112 | 113 | pub fn switch_view(&mut self) -> AppResult<()> { 114 | match self.state { 115 | TabularState::Table => self.show_sheet(), 116 | TabularState::Sheet(_) => self.show_table(), 117 | } 118 | } 119 | 120 | pub fn show_sheet(&mut self) -> AppResult<()> { 121 | self.state = TabularState::Sheet(Scroll::default()); 122 | Ok(()) 123 | } 124 | 125 | pub fn show_table(&mut self) -> AppResult<()> { 126 | self.state = TabularState::Table; 127 | Ok(()) 128 | } 129 | 130 | pub fn set_data_frame(&mut self, data_frame: DataFrame) -> AppResult<()> { 131 | self.table_state.set_data_frame(data_frame); 132 | Ok(()) 133 | } 134 | 135 | pub fn data_frame(&self) -> &DataFrame { 136 | self.table_state.data_frame() 137 | } 138 | 139 | pub fn data_frame_mut(&mut self) -> &mut DataFrame { 140 | self.table_state.data_frame_mut() 141 | } 142 | 143 | pub fn state(&self) -> &TabularState { 144 | &self.state 145 | } 146 | 147 | pub fn selected(&self) -> usize { 148 | self.table_state.selected() 149 | } 150 | 151 | pub fn tabular_type(&self) -> &TabularType { 152 | &self.tabular_type 153 | } 154 | 155 | pub fn render(&mut self, frame: &mut Frame, layout: Rect, selection: bool) -> AppResult<()> { 156 | match &mut self.state { 157 | TabularState::Table => { 158 | frame.render_stateful_widget( 159 | DataFrameTable::::new() 160 | .with_selection(selection) 161 | .with_column_space(2), 162 | layout, 163 | &mut self.table_state, 164 | ); 165 | } 166 | TabularState::Sheet(scroll) => { 167 | let space = layout.inner(Margin::new(1, 1)); 168 | let title = format!(" {} ", self.table_state.selected() + 1); 169 | let values = self 170 | .table_state 171 | .data_frame() 172 | .get(self.table_state.selected()) 173 | .map(|row| row.into_iter().map(any_value_into_string).collect_vec()) 174 | .unwrap_or_default(); 175 | 176 | let (paragraph, line_count) = paragraph_from_headers_values::( 177 | &title, 178 | self.table_state.headers(), 179 | &values, 180 | space.width, 181 | ); 182 | 183 | scroll.adjust(line_count, space.height as usize); 184 | frame.render_widget(paragraph.scroll((scroll.to_u16(), 0)), layout); 185 | } 186 | } 187 | Ok(()) 188 | } 189 | } 190 | 191 | fn paragraph_from_headers_values<'a, Theme: Styler>( 192 | title: &'a str, 193 | headers: &'a [String], 194 | values: &'a [String], 195 | width: u16, 196 | ) -> (Paragraph<'a>, usize) { 197 | let lines = izip!(headers, values.iter()) 198 | .enumerate() 199 | .flat_map(|(idx, (header, value))| lines_from_header_value::(idx, header, value)) 200 | .collect_vec(); 201 | let lc = lines 202 | .iter() 203 | .map(|line| line_count(&line.to_string(), width as usize)) 204 | .sum(); 205 | let prgr = Paragraph::new(lines) 206 | .block(Block::new().title(title).borders(Borders::ALL)) 207 | .style(Theme::sheet_block()) 208 | .alignment(Alignment::Left) 209 | .wrap(Wrap { trim: true }); 210 | (prgr, lc) 211 | } 212 | 213 | fn lines_from_header_value<'a, Theme: Styler>( 214 | idx: usize, 215 | header: &'a str, 216 | value: &'a str, 217 | ) -> Vec> { 218 | let header_line = std::iter::once(Line::from(Span::styled( 219 | header, 220 | Theme::table_header_cell(idx), 221 | ))); 222 | let value_lines = value 223 | .lines() 224 | .map(|line| Line::from(Span::styled(line, Theme::sheet_value()))); 225 | header_line 226 | .chain(value_lines) 227 | .chain(std::iter::once(Line::default())) 228 | .collect_vec() 229 | } 230 | -------------------------------------------------------------------------------- /src/tui/terminal.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::handler::event::EventHandler; 3 | use crate::tui::Styler; 4 | use crate::AppResult; 5 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 6 | use ratatui::backend::Backend; 7 | use std::io; 8 | use std::panic; 9 | 10 | /// Representation of a terminal user interface. 11 | /// 12 | /// It is responsible for setting up the terminal, 13 | /// initializing the interface and handling the draw events. 14 | #[derive(Debug)] 15 | pub struct Terminal { 16 | /// Interface to the Terminal. 17 | terminal: ratatui::Terminal, 18 | /// Terminal event handler. 19 | pub events: EventHandler, 20 | } 21 | 22 | impl Terminal { 23 | /// Constructs a new instance of [`Tui`]. 24 | pub fn new(terminal: ratatui::Terminal, events: EventHandler) -> Self { 25 | Self { terminal, events } 26 | } 27 | 28 | /// Initializes the terminal interface. 29 | /// 30 | /// It enables the raw mode and sets terminal properties. 31 | pub fn init(&mut self) -> AppResult<()> { 32 | terminal::enable_raw_mode()?; 33 | crossterm::execute!(io::stderr(), EnterAlternateScreen)?; 34 | 35 | // Define a custom panic hook to reset the terminal properties. 36 | // This way, you won't have your terminal messed up if an unexpected error happens. 37 | let panic_hook = panic::take_hook(); 38 | panic::set_hook(Box::new(move |panic| { 39 | Self::reset().expect("failed to reset the terminal"); 40 | panic_hook(panic); 41 | })); 42 | 43 | self.terminal.hide_cursor()?; 44 | self.terminal.clear()?; 45 | Ok(()) 46 | } 47 | 48 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 49 | /// 50 | /// [`Draw`]: ratatui::Terminal::draw 51 | /// [`rendering`]: crate::ui::render 52 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 53 | self.terminal.draw(|frame| { 54 | let _ = app.draw(frame); 55 | })?; 56 | Ok(()) 57 | } 58 | 59 | /// Resets the terminal interface. 60 | /// 61 | /// This function is also used for the panic hook to revert 62 | /// the terminal properties if unexpected errors occur. 63 | fn reset() -> AppResult<()> { 64 | terminal::disable_raw_mode()?; 65 | crossterm::execute!(io::stderr(), LeaveAlternateScreen)?; 66 | Ok(()) 67 | } 68 | 69 | /// Exits the terminal interface. 70 | /// 71 | /// It disables the raw mode and reverts back the terminal properties. 72 | pub fn exit(&mut self) -> AppResult<()> { 73 | Self::reset()?; 74 | self.terminal.show_cursor()?; 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/tui/theme.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::{Color, Style, Stylize}; 2 | 3 | pub trait Styler { 4 | fn table_header() -> Style; 5 | fn table_header_cell(col: usize) -> Style; 6 | fn table_row(row: usize) -> Style; 7 | fn table_highlight() -> Style; 8 | fn sheet_value() -> Style; 9 | fn status_bar_error() -> Style; 10 | fn status_bar_prompt() -> Style; 11 | fn status_bar_info() -> Style; 12 | fn sheet_block() -> Style; 13 | fn status_bar_info_key(idx: usize) -> Style; 14 | fn status_bar_info_val(idx: usize) -> Style; 15 | } 16 | pub trait SixColorsTwoRowsStyler { 17 | const BACKGROUND: Color; 18 | const LIGHT_BACKGROUND: Color; 19 | const FOREGROUND: Color; 20 | 21 | const COLORS: [Color; 6]; 22 | const DARK_COLORS: [Color; 6]; 23 | 24 | const ROW_BACKGROUNDS: [Color; 2]; 25 | const HIGHTLIGHT_BACKGROUND: Color; 26 | const HIGHTLIGHT_FOREGROUND: Color; 27 | 28 | const STATUS_BAR_ERROR: Color; 29 | const STATUS_BAR_PROMPT: Color; 30 | const STATUS_BAR_INFO: Color; 31 | } 32 | 33 | impl Styler for T 34 | where 35 | T: SixColorsTwoRowsStyler, 36 | { 37 | fn table_header() -> Style { 38 | Style::default().bg(Self::BACKGROUND) 39 | } 40 | 41 | fn table_header_cell(col: usize) -> Style { 42 | Style::default() 43 | .fg(Self::COLORS[col % Self::COLORS.len()]) 44 | .bold() 45 | } 46 | 47 | fn table_row(row: usize) -> Style { 48 | Style::new() 49 | .bg(Self::ROW_BACKGROUNDS[row % Self::ROW_BACKGROUNDS.len()]) 50 | .fg(Self::FOREGROUND) 51 | } 52 | 53 | fn table_highlight() -> Style { 54 | Style::new() 55 | .bg(Self::HIGHTLIGHT_BACKGROUND) 56 | .fg(Self::HIGHTLIGHT_FOREGROUND) 57 | } 58 | 59 | fn sheet_value() -> Style { 60 | Style::default().fg(Self::FOREGROUND) 61 | } 62 | 63 | fn status_bar_error() -> Style { 64 | Style::default() 65 | .bg(Self::STATUS_BAR_ERROR) 66 | .fg(Self::FOREGROUND) 67 | } 68 | 69 | fn status_bar_prompt() -> Style { 70 | Style::default() 71 | .bg(Self::STATUS_BAR_PROMPT) 72 | .fg(Self::FOREGROUND) 73 | } 74 | 75 | fn status_bar_info() -> Style { 76 | Style::default() 77 | .bg(Self::STATUS_BAR_INFO) 78 | .fg(Self::FOREGROUND) 79 | } 80 | 81 | fn sheet_block() -> Style { 82 | Style::new() 83 | .bg(Self::BACKGROUND) 84 | .fg(Self::HIGHTLIGHT_BACKGROUND) 85 | } 86 | 87 | fn status_bar_info_key(idx: usize) -> Style { 88 | Style::default() 89 | .bg(Self::DARK_COLORS[idx % Self::DARK_COLORS.len()]) 90 | .fg(Self::LIGHT_BACKGROUND) 91 | } 92 | 93 | fn status_bar_info_val(idx: usize) -> Style { 94 | Style::default() 95 | .bg(Self::LIGHT_BACKGROUND) 96 | .fg(Self::COLORS[idx % Self::COLORS.len()]) 97 | } 98 | } 99 | 100 | pub struct Monokai; 101 | pub struct Argonaut; 102 | pub struct Terminal; 103 | 104 | impl SixColorsTwoRowsStyler for Monokai { 105 | const BACKGROUND: Color = Color::from_u32(0x001c191d); 106 | const LIGHT_BACKGROUND: Color = Color::from_u32(0x003e3b3f); 107 | const FOREGROUND: Color = Color::from_u32(0x00fffaf4); 108 | 109 | const COLORS: [Color; 6] = [ 110 | Color::from_u32(0x00ff6188), 111 | Color::from_u32(0x00fc9867), 112 | Color::from_u32(0x00ffd866), 113 | Color::from_u32(0x00a9dc76), 114 | Color::from_u32(0x0078dce8), 115 | Color::from_u32(0x00ab9df2), 116 | ]; 117 | const DARK_COLORS: [Color; 6] = [ 118 | Color::from_u32(0x00ee4066), 119 | Color::from_u32(0x00da7645), 120 | Color::from_u32(0x00ddb644), 121 | Color::from_u32(0x0087ba54), 122 | Color::from_u32(0x0056bac6), 123 | Color::from_u32(0x00897bd0), 124 | ]; 125 | 126 | const ROW_BACKGROUNDS: [Color; 2] = [Color::from_u32(0x00232024), Self::BACKGROUND]; 127 | const HIGHTLIGHT_BACKGROUND: Color = Color::from_u32(0x00c89f2d); 128 | const HIGHTLIGHT_FOREGROUND: Color = Self::FOREGROUND; 129 | 130 | const STATUS_BAR_ERROR: Color = Color::from_u32(0x00d02d00); 131 | const STATUS_BAR_PROMPT: Color = Color::from_u32(0x00008f1f); 132 | const STATUS_BAR_INFO: Color = Self::BACKGROUND; 133 | } 134 | 135 | impl SixColorsTwoRowsStyler for Argonaut { 136 | const BACKGROUND: Color = Color::from_u32(0x0001030b); 137 | const LIGHT_BACKGROUND: Color = Color::from_u32(0x0023252d); 138 | const FOREGROUND: Color = Color::from_u32(0x00fffaf4); 139 | 140 | const COLORS: [Color; 6] = [ 141 | Color::from_u32(0x00ff000f), 142 | Color::from_u32(0x00ffb900), 143 | Color::from_u32(0x00ffd866), 144 | Color::from_u32(0x008ce10b), 145 | Color::from_u32(0x006d43a6), 146 | Color::from_u32(0x0000d8eb), 147 | ]; 148 | const DARK_COLORS: [Color; 6] = [ 149 | Color::from_u32(0x00ff000f), 150 | Color::from_u32(0x00ffb900), 151 | Color::from_u32(0x00ffd866), 152 | Color::from_u32(0x008ce10b), 153 | Color::from_u32(0x006d43a6), 154 | Color::from_u32(0x0000d8eb), 155 | ]; 156 | 157 | const ROW_BACKGROUNDS: [Color; 2] = [Color::from_u32(0x0011131b), Color::from_u32(0x0001030b)]; 158 | const HIGHTLIGHT_BACKGROUND: Color = Color::from_u32(0x00002a3b); 159 | const HIGHTLIGHT_FOREGROUND: Color = Self::FOREGROUND; 160 | 161 | const STATUS_BAR_ERROR: Color = Color::from_u32(0x00dd0000); 162 | const STATUS_BAR_PROMPT: Color = Color::from_u32(0x006cc100); 163 | const STATUS_BAR_INFO: Color = Self::BACKGROUND; 164 | } 165 | 166 | impl Styler for Terminal { 167 | fn table_header() -> Style { 168 | Style::default().bg(Color::Cyan).fg(Color::Black) 169 | } 170 | 171 | fn table_header_cell(_col: usize) -> Style { 172 | Style::default() 173 | } 174 | 175 | fn table_row(_row: usize) -> Style { 176 | Default::default() 177 | } 178 | 179 | fn table_highlight() -> Style { 180 | Style::default().bg(Color::Yellow).fg(Color::Black) 181 | } 182 | 183 | fn sheet_value() -> Style { 184 | Style::default() 185 | } 186 | 187 | fn status_bar_error() -> Style { 188 | Style::default().bg(Color::Red).fg(Color::White) 189 | } 190 | 191 | fn status_bar_prompt() -> Style { 192 | Style::default().bg(Color::Green).fg(Color::White) 193 | } 194 | 195 | fn status_bar_info() -> Style { 196 | Style::default().bg(Color::Blue).fg(Color::White) 197 | } 198 | 199 | fn sheet_block() -> Style { 200 | Style::default() 201 | } 202 | 203 | fn status_bar_info_key(_idx: usize) -> Style { 204 | Style::default() 205 | } 206 | 207 | fn status_bar_info_val(_idx: usize) -> Style { 208 | Style::default() 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/tui/utils.rs: -------------------------------------------------------------------------------- 1 | use polars::{prelude::AnyValue, series::Series}; 2 | 3 | #[derive(Debug, Default, Clone)] 4 | pub struct Scroll(usize); 5 | 6 | impl From for usize { 7 | fn from(val: Scroll) -> Self { 8 | val.0 9 | } 10 | } 11 | 12 | impl From for u16 { 13 | fn from(val: Scroll) -> Self { 14 | val.0 as u16 15 | } 16 | } 17 | 18 | impl Scroll { 19 | pub fn up(&mut self) { 20 | self.0 = self.0.saturating_sub(1); 21 | } 22 | 23 | pub fn down(&mut self) { 24 | self.0 = self.0.saturating_add(1); 25 | } 26 | 27 | pub fn adjust(&mut self, lines: usize, space: usize) { 28 | self.0 = self.0.min(lines.saturating_sub(space)) 29 | } 30 | 31 | pub fn to_u16(&self) -> u16 { 32 | self.0 as u16 33 | } 34 | } 35 | 36 | pub fn line_count(text: &str, width: usize) -> usize { 37 | let mut line_count = 1; 38 | let mut used_space = 0; 39 | for word_len in text.split(' ').map(str::len) { 40 | if word_len <= width { 41 | if used_space + word_len <= width { 42 | used_space += word_len + 1; 43 | } else { 44 | used_space = word_len + 1; 45 | line_count += 1; 46 | } 47 | } else { 48 | line_count += (word_len - width + used_space).div_ceil(width) 49 | } 50 | } 51 | line_count 52 | } 53 | 54 | pub fn data_frame_widths(df: &polars::frame::DataFrame) -> Vec { 55 | df.get_column_names() 56 | .into_iter() 57 | .zip(df.get_columns()) 58 | .map(|(col, series)| col.len().max(series_width(series))) 59 | .collect::>() 60 | } 61 | 62 | pub fn series_width(series: &Series) -> usize { 63 | series 64 | .iter() 65 | .map(|any_value| { 66 | any_value_into_string(any_value) 67 | .lines() 68 | .next() 69 | .map(str::len) 70 | .unwrap_or(0) 71 | }) 72 | .max() 73 | .unwrap_or_default() 74 | } 75 | 76 | pub fn any_value_into_string(value: polars::datatypes::AnyValue) -> String { 77 | match value { 78 | AnyValue::Null => "".to_owned(), 79 | AnyValue::StringOwned(v) => v.to_string(), 80 | AnyValue::String(v) => v.to_string(), 81 | _ => value.to_string(), 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | 89 | #[test] 90 | fn test_line_count_single_line() { 91 | let text = "Hello world"; 92 | assert_eq!(line_count(text, 15), 1); 93 | assert_eq!(line_count(text, 11), 1); 94 | assert_eq!(line_count(text, 10), 2); 95 | } 96 | 97 | #[test] 98 | fn test_line_count_multiple_lines() { 99 | let text = "Hello world this is a test"; 100 | assert_eq!(line_count(text, 15), 2); 101 | assert_eq!(line_count(text, 10), 3); 102 | assert_eq!(line_count(text, 5), 5); 103 | } 104 | 105 | #[test] 106 | fn test_line_count_exact_width() { 107 | let text = "Hello world"; 108 | assert_eq!(line_count(text, 5), 2); 109 | assert_eq!(line_count(text, 6), 2); 110 | assert_eq!(line_count(text, 11), 1); 111 | } 112 | 113 | #[test] 114 | fn test_line_count_with_long_word() { 115 | let text = "supercalifragilisticexpialidocious"; 116 | assert_eq!(line_count(text, 10), 4); 117 | assert_eq!(line_count(text, 20), 2); 118 | assert_eq!(line_count(text, 30), 2); 119 | } 120 | 121 | #[test] 122 | fn test_line_count_with_mixed_length_words() { 123 | let text = "a bb ccc dddd eeeee ffffff ggggggg"; 124 | assert_eq!(line_count(text, 10), 4); 125 | assert_eq!(line_count(text, 5), 8); 126 | assert_eq!(line_count(text, 20), 2); 127 | } 128 | 129 | #[test] 130 | fn test_line_count_empty_string() { 131 | let text = ""; 132 | assert_eq!(line_count(text, 10), 1); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/tui/widget/data_frame_table.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use itertools::Itertools; 4 | use polars::{frame::DataFrame, prelude::PlSmallStr, series::Series}; 5 | use ratatui::{ 6 | layout::Constraint, 7 | widgets::{Cell, Row, StatefulWidget, Table, TableState, Widget}, 8 | }; 9 | 10 | use crate::{ 11 | tui::{ 12 | utils::{any_value_into_string, data_frame_widths}, 13 | Styler, 14 | }, 15 | utils::ZipItersExt, 16 | }; 17 | 18 | #[derive(Debug)] 19 | pub struct DataFrameTableState { 20 | offset: usize, 21 | select: usize, 22 | rendered_rows: u16, 23 | widths: Vec, 24 | headers: Vec, 25 | data_frame: DataFrame, 26 | } 27 | 28 | impl DataFrameTableState { 29 | pub fn new(data_frame: DataFrame) -> Self { 30 | Self { 31 | offset: 0, 32 | select: 0, 33 | rendered_rows: 0, 34 | widths: data_frame_widths(&data_frame), 35 | headers: data_frame 36 | .get_column_names() 37 | .into_iter() 38 | .map(PlSmallStr::to_string) 39 | .collect(), 40 | data_frame, 41 | } 42 | } 43 | 44 | pub fn data_frame(&self) -> &DataFrame { 45 | &self.data_frame 46 | } 47 | 48 | pub fn data_frame_mut(&mut self) -> &mut DataFrame { 49 | &mut self.data_frame 50 | } 51 | 52 | pub fn set_data_frame(&mut self, data_frame: DataFrame) { 53 | self.offset = 0; 54 | self.select = 0; 55 | self.widths = data_frame_widths(&data_frame); 56 | self.headers = data_frame 57 | .get_column_names() 58 | .into_iter() 59 | .map(PlSmallStr::to_string) 60 | .collect(); 61 | self.data_frame = data_frame; 62 | } 63 | 64 | pub fn headers(&self) -> &[String] { 65 | &self.headers 66 | } 67 | 68 | pub fn selected(&self) -> usize { 69 | self.select 70 | } 71 | 72 | pub fn select(&mut self, select: usize) { 73 | self.select = select.min(self.data_frame.height().saturating_sub(1)); 74 | } 75 | 76 | pub fn select_up(&mut self, len: usize) { 77 | self.select(self.select.saturating_sub(len)) 78 | } 79 | 80 | pub fn select_down(&mut self, len: usize) { 81 | self.select(self.select + len) 82 | } 83 | 84 | pub fn select_first(&mut self) { 85 | self.select(0) 86 | } 87 | 88 | pub fn select_last(&mut self) { 89 | self.select(self.height()); 90 | } 91 | 92 | pub fn height(&self) -> usize { 93 | self.data_frame.height() 94 | } 95 | pub fn rendered_rows(&self) -> u16 { 96 | self.rendered_rows 97 | } 98 | 99 | fn adjust(&mut self, rendered_rows: u16) { 100 | self.rendered_rows = rendered_rows; 101 | self.offset = self.offset.clamp( 102 | self.select 103 | .saturating_sub(rendered_rows.saturating_sub(1).into()), 104 | self.select, 105 | ); 106 | } 107 | } 108 | 109 | pub struct DataFrameTable { 110 | selection: bool, 111 | column_space: u16, 112 | _theme: PhantomData, 113 | } 114 | 115 | impl DataFrameTable { 116 | pub fn new() -> Self { 117 | Self { 118 | selection: false, 119 | column_space: 1, 120 | _theme: Default::default(), 121 | } 122 | } 123 | 124 | pub fn with_selection(mut self, selection: bool) -> Self { 125 | self.selection = selection; 126 | self 127 | } 128 | 129 | pub fn with_column_space(mut self, space: u16) -> Self { 130 | self.column_space = space; 131 | self 132 | } 133 | } 134 | 135 | impl Default for DataFrameTable { 136 | fn default() -> Self { 137 | Self::new() 138 | } 139 | } 140 | 141 | impl StatefulWidget for DataFrameTable { 142 | type State = DataFrameTableState; 143 | 144 | fn render( 145 | self, 146 | area: ratatui::prelude::Rect, 147 | buf: &mut ratatui::prelude::Buffer, 148 | state: &mut Self::State, 149 | ) { 150 | state.adjust(area.height.saturating_sub(1)); 151 | let df = state 152 | .data_frame 153 | .slice(state.offset as i64, state.rendered_rows as usize); 154 | 155 | let header = Row::new( 156 | state 157 | .headers 158 | .iter() 159 | .enumerate() 160 | .map(|(col_idx, name)| { 161 | Cell::new(name.as_str()).style(Theme::table_header_cell(col_idx)) 162 | }) 163 | .collect::>(), 164 | ) 165 | .style(Theme::table_header()); 166 | 167 | let table = Table::new( 168 | df.iter() 169 | .map(Series::iter) 170 | .zip_iters() 171 | .enumerate() 172 | .map(|(ridx, vals)| { 173 | Row::new(vals.into_iter().map(any_value_into_string).map(Cell::new)) 174 | .style(Theme::table_row(ridx + state.offset)) 175 | }) 176 | .collect_vec(), 177 | state 178 | .widths 179 | .iter() 180 | .copied() 181 | .map(|w| Constraint::Length(w as u16)) 182 | .collect::>(), 183 | ) 184 | .header(header) 185 | .highlight_style(Theme::table_highlight()) 186 | .column_spacing(2); 187 | 188 | if self.selection { 189 | StatefulWidget::render( 190 | table, 191 | area, 192 | buf, 193 | &mut TableState::new() 194 | .with_offset(0) 195 | .with_selected(state.select.saturating_sub(state.offset)), 196 | ); 197 | } else { 198 | Widget::render(table, area, buf); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/tui/widget/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data_frame_table; 2 | pub mod prompt; 3 | pub mod sheet; 4 | -------------------------------------------------------------------------------- /src/tui/widget/prompt.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{layout::Rect, style::Style, widgets::StatefulWidget}; 2 | 3 | #[derive(Debug)] 4 | pub struct PromptState { 5 | chars: Vec>, 6 | cursor: (usize, usize), 7 | offset: usize, 8 | } 9 | 10 | impl PromptState { 11 | pub fn input_char(&mut self, character: char) -> &mut Self { 12 | self.chars[self.cursor.0].insert(self.cursor.1, character); 13 | self.cursor.1 += 1; 14 | self 15 | } 16 | 17 | pub fn delete(&mut self) -> &mut Self { 18 | if self.cursor.1 < self.chars[self.cursor.0].len() { 19 | self.chars[self.cursor.0].remove(self.cursor.1); 20 | } 21 | self 22 | } 23 | 24 | pub fn delete_backward(&mut self) -> &mut Self { 25 | if self.cursor.1 > 0 { 26 | self.chars[self.cursor.0].remove(self.cursor.1 - 1); 27 | self.cursor.1 -= 1; 28 | } 29 | self 30 | } 31 | 32 | pub fn move_up(&mut self) -> &mut Self { 33 | self.move_cursor(self.cursor.0.saturating_sub(1), self.cursor.1); 34 | self 35 | } 36 | 37 | pub fn move_down(&mut self) -> &mut Self { 38 | self.move_cursor(self.cursor.0.saturating_add(1), self.cursor.1); 39 | self 40 | } 41 | 42 | pub fn move_left(&mut self) -> &mut Self { 43 | self.move_cursor(self.cursor.0, self.cursor.1.saturating_sub(1)); 44 | self 45 | } 46 | 47 | pub fn move_right(&mut self) -> &mut Self { 48 | self.move_cursor(self.cursor.0, self.cursor.1.saturating_add(1)); 49 | self 50 | } 51 | 52 | pub fn move_bol(&mut self) -> &mut Self { 53 | self.move_cursor(self.cursor.0, 0); 54 | self 55 | } 56 | 57 | pub fn move_eol(&mut self) -> &mut Self { 58 | self.move_cursor(self.cursor.0, usize::MAX); 59 | self 60 | } 61 | 62 | pub fn command(&self) -> String { 63 | self.chars[self.cursor.0].iter().collect() 64 | } 65 | 66 | pub fn command_len(&self) -> usize { 67 | self.chars[self.cursor.0].len() 68 | } 69 | 70 | pub fn cursor(&self) -> (usize, usize) { 71 | self.cursor 72 | } 73 | 74 | #[inline] 75 | fn move_cursor(&mut self, x0: usize, x1: usize) { 76 | let x0 = x0.min(self.chars.len().saturating_sub(1)); 77 | let x1 = x1.min(self.chars[x0].len()); 78 | self.cursor = (x0, x1); 79 | } 80 | } 81 | 82 | impl From> for PromptState { 83 | fn from(value: Vec) -> Self { 84 | Self { 85 | cursor: ( 86 | value.len().saturating_sub(1), 87 | value 88 | .last() 89 | .map(|str| str.chars().count()) 90 | .unwrap_or_default(), 91 | ), 92 | chars: value.into_iter().map(|str| str.chars().collect()).collect(), 93 | offset: 0, 94 | } 95 | } 96 | } 97 | 98 | pub struct Prompt { 99 | style: Style, 100 | cursor_style: Style, 101 | } 102 | 103 | impl Prompt { 104 | pub fn new(style: Style, cursor_style: Style) -> Self { 105 | Self { 106 | style, 107 | cursor_style, 108 | } 109 | } 110 | } 111 | 112 | impl StatefulWidget for Prompt { 113 | type State = PromptState; 114 | 115 | fn render( 116 | self, 117 | area: ratatui::prelude::Rect, 118 | buf: &mut ratatui::prelude::Buffer, 119 | state: &mut Self::State, 120 | ) { 121 | state.offset = state 122 | .offset 123 | .clamp( 124 | state 125 | .cursor 126 | .1 127 | .saturating_sub(area.width.saturating_sub(1).into()), 128 | state.cursor.1.min(state.chars[state.cursor.0].len()), 129 | ) 130 | .min( 131 | state.chars[state.cursor.0] 132 | .len() 133 | .saturating_sub(area.width.saturating_sub(1).into()), 134 | ); 135 | buf.set_string( 136 | area.x, 137 | area.y, 138 | state.chars[state.cursor.0] 139 | .iter() 140 | .skip(state.offset) 141 | .collect::(), 142 | self.style, 143 | ); 144 | buf.set_style(area, self.style); 145 | buf.set_style( 146 | Rect { 147 | x: area.x + state.cursor.1.saturating_sub(state.offset) as u16, 148 | y: area.y, 149 | width: 1, 150 | height: 1, 151 | }, 152 | self.cursor_style, 153 | ); 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | 161 | #[test] 162 | fn state_test() { 163 | let mut state = PromptState::from(vec!["".to_owned()]); 164 | println!("{}", state.command()); 165 | state.input_char('c'); 166 | state.input_char('h'); 167 | state.input_char('a'); 168 | state.input_char('r'); 169 | assert_eq!(state.command(), "char") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/tui/widget/sheet.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use polars::{ 4 | datatypes::DataType, 5 | frame::DataFrame, 6 | series::{ChunkCompare, Series}, 7 | }; 8 | 9 | #[derive(Debug)] 10 | pub struct RoundRobin { 11 | queue: VecDeque, 12 | } 13 | 14 | impl RoundRobin { 15 | fn new(iter: impl IntoIterator) -> Self { 16 | Self { 17 | queue: iter.into_iter().collect(), 18 | } 19 | } 20 | } 21 | 22 | impl Iterator for RoundRobin 23 | where 24 | T: Iterator, 25 | { 26 | type Item = I; 27 | 28 | fn next(&mut self) -> Option { 29 | self.queue.pop_front().and_then(|mut iter| { 30 | let next = iter.next(); 31 | self.queue.push_back(iter); 32 | next 33 | }) 34 | } 35 | } 36 | 37 | pub trait RoundRobinExt { 38 | type Item; 39 | 40 | fn round_robin(self) -> RoundRobin; 41 | } 42 | 43 | impl RoundRobinExt for T 44 | where 45 | T: IntoIterator, 46 | { 47 | type Item = I; 48 | 49 | fn round_robin(self) -> RoundRobin { 50 | RoundRobin::new(self) 51 | } 52 | } 53 | 54 | pub struct ZipIters { 55 | iterators: Vec, 56 | } 57 | 58 | impl Iterator for ZipIters 59 | where 60 | Iter: Iterator, 61 | T: Default, 62 | { 63 | type Item = Vec; 64 | 65 | fn next(&mut self) -> Option { 66 | let mut items = Vec::new(); 67 | let mut any_valid = false; 68 | 69 | for iter in self.iterators.iter_mut() { 70 | if let Some(item) = iter.next() { 71 | items.push(item); 72 | any_valid = true; 73 | } else { 74 | items.push(T::default()); // Using default to fill gaps 75 | } 76 | } 77 | 78 | if any_valid { 79 | Some(items) 80 | } else { 81 | None // If no valid items, all iterators are exhausted 82 | } 83 | } 84 | } 85 | 86 | pub trait ZipItersExt { 87 | fn zip_iters(self) -> ZipIters; 88 | } 89 | 90 | impl ZipItersExt for I1 91 | where 92 | I1: IntoIterator, 93 | I2: Iterator, 94 | T: Default, 95 | { 96 | fn zip_iters(self) -> ZipIters { 97 | ZipIters { 98 | iterators: self.into_iter().collect(), 99 | } 100 | } 101 | } 102 | 103 | pub struct SplitByLength<'a, WidthIter> { 104 | slice: &'a str, 105 | width_iter: WidthIter, 106 | start: usize, 107 | } 108 | 109 | impl<'a, WI> Iterator for SplitByLength<'a, WI> 110 | where 111 | WI: Iterator, 112 | { 113 | type Item = &'a str; 114 | 115 | fn next(&mut self) -> Option { 116 | self.width_iter.next().map(|width| { 117 | if let Some(end) = self.slice[self.start..] 118 | .char_indices() 119 | .nth(width) 120 | .map(|(offset, _)| self.start + offset) 121 | { 122 | let slice = &self.slice[self.start..end]; 123 | self.start = end; 124 | slice 125 | } else { 126 | let slice = &self.slice[self.start..]; 127 | self.start = self.slice.len(); 128 | slice 129 | } 130 | }) 131 | } 132 | } 133 | 134 | pub trait SplitByLengthExt<'a, WidthIter, IntoWidthIter> { 135 | fn split_by_length(&'a self, width: IntoWidthIter) -> SplitByLength<'a, WidthIter>; 136 | } 137 | 138 | impl<'a, WidthIter, IntoWidthIter> SplitByLengthExt<'a, WidthIter, IntoWidthIter> for str 139 | where 140 | WidthIter: Iterator, 141 | IntoWidthIter: IntoIterator, 142 | { 143 | fn split_by_length(&'a self, width: IntoWidthIter) -> SplitByLength<'a, WidthIter> { 144 | SplitByLength { 145 | slice: self, 146 | width_iter: width.into_iter(), 147 | start: 0, 148 | } 149 | } 150 | } 151 | 152 | pub fn safe_infer_schema(data_frame: &mut DataFrame) { 153 | for col_name in data_frame.get_column_names_owned() { 154 | let col_name = col_name.as_str(); 155 | if let Some(series) = type_infered_series(data_frame.column(col_name).unwrap()) { 156 | data_frame.replace(col_name, series).unwrap(); 157 | } 158 | } 159 | } 160 | 161 | fn type_infered_series(series: &Series) -> Option { 162 | [ 163 | DataType::Int64, 164 | DataType::Float64, 165 | DataType::Boolean, 166 | DataType::Date, 167 | DataType::Time, 168 | ] 169 | .iter() 170 | .filter_map(|dtype| series.cast(dtype).ok()) 171 | .find(|dtype_series| series.is_null().equal(&dtype_series.is_null()).all()) 172 | } 173 | 174 | #[inline] 175 | pub fn as_ascii(c: char) -> Option { 176 | c.is_ascii().then_some(c as u8) 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use super::*; 182 | use itertools::Itertools; 183 | use polars::prelude::*; 184 | 185 | #[test] 186 | fn test_round_robin() { 187 | let v1 = vec![1, 2, 3]; 188 | let v2 = vec![4, 5]; 189 | let v3 = vec![6, 7, 8, 9]; 190 | 191 | let iter1 = v1.into_iter(); 192 | let iter2 = v2.into_iter(); 193 | let iter3 = v3.into_iter(); 194 | 195 | let iterators = vec![iter1, iter2, iter3]; 196 | let round_robin = iterators.round_robin(); 197 | let result: Vec<_> = round_robin.collect(); 198 | 199 | assert_eq!(result, vec![1, 4, 6, 2, 5, 7, 3]); 200 | } 201 | 202 | #[test] 203 | fn test_round_robin_complete() { 204 | let v1 = vec![1, 4, 7]; 205 | let v2 = vec![2, 5, 8]; 206 | let v3 = vec![3, 6, 9]; 207 | 208 | let iter1 = v1.into_iter(); 209 | let iter2 = v2.into_iter(); 210 | let iter3 = v3.into_iter(); 211 | 212 | let iterators = vec![iter1, iter2, iter3]; 213 | let round_robin = iterators.round_robin(); 214 | let result: Vec<_> = round_robin.collect(); 215 | 216 | assert_eq!(result, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]); 217 | } 218 | 219 | #[test] 220 | fn test_zip_iters_all_same_length() { 221 | let iter1 = vec![1, 2, 3].into_iter(); 222 | let iter2 = vec![4, 5, 6].into_iter(); 223 | let iter3 = vec![7, 8, 9].into_iter(); 224 | 225 | let mut zipped = vec![iter1, iter2, iter3].zip_iters(); 226 | 227 | assert_eq!(zipped.next(), Some(vec![1, 4, 7])); 228 | assert_eq!(zipped.next(), Some(vec![2, 5, 8])); 229 | assert_eq!(zipped.next(), Some(vec![3, 6, 9])); 230 | assert_eq!(zipped.next(), None); 231 | } 232 | 233 | #[test] 234 | fn test_zip_iters_different_lengths() { 235 | let iter1 = vec![1, 2].into_iter(); 236 | let iter2 = vec![4, 5, 6].into_iter(); 237 | let iter3 = vec![7].into_iter(); 238 | 239 | let mut zipped = vec![iter1, iter2, iter3].zip_iters(); 240 | 241 | assert_eq!(zipped.next(), Some(vec![1, 4, 7])); 242 | assert_eq!(zipped.next(), Some(vec![2, 5, Default::default()])); 243 | assert_eq!( 244 | zipped.next(), 245 | Some(vec![Default::default(), 6, Default::default()]) 246 | ); 247 | assert_eq!(zipped.next(), None); 248 | } 249 | 250 | #[test] 251 | fn test_zip_iters_empty_iterator() { 252 | let iter1 = vec![].into_iter(); 253 | let iter2 = vec![4, 5, 6].into_iter(); 254 | let iter3 = vec![].into_iter(); 255 | 256 | let mut zipped = vec![iter1, iter2, iter3].zip_iters(); 257 | 258 | assert_eq!( 259 | zipped.next(), 260 | Some(vec![Default::default(), 4, Default::default()]) 261 | ); 262 | assert_eq!( 263 | zipped.next(), 264 | Some(vec![Default::default(), 5, Default::default()]) 265 | ); 266 | assert_eq!( 267 | zipped.next(), 268 | Some(vec![Default::default(), 6, Default::default()]) 269 | ); 270 | assert_eq!(zipped.next(), None); 271 | } 272 | 273 | #[test] 274 | fn test_zip_iters_single_iterator() { 275 | let iter1 = vec![1, 2, 3].into_iter(); 276 | 277 | let mut zipped = vec![iter1].zip_iters(); 278 | 279 | assert_eq!(zipped.next(), Some(vec![1])); 280 | assert_eq!(zipped.next(), Some(vec![2])); 281 | assert_eq!(zipped.next(), Some(vec![3])); 282 | assert_eq!(zipped.next(), None); 283 | } 284 | 285 | #[test] 286 | fn test_zip_iters_default_value() { 287 | #[derive(Clone, Default, PartialEq, Debug)] 288 | struct CustomType(i32); 289 | 290 | let iter1 = vec![CustomType(1), CustomType(2)].into_iter(); 291 | let iter2 = vec![CustomType(4), CustomType(5), CustomType(6)].into_iter(); 292 | let iter3 = vec![CustomType(7)].into_iter(); 293 | 294 | let mut zipped = vec![iter1, iter2, iter3].zip_iters(); 295 | 296 | assert_eq!( 297 | zipped.next(), 298 | Some(vec![CustomType(1), CustomType(4), CustomType(7)]) 299 | ); 300 | assert_eq!( 301 | zipped.next(), 302 | Some(vec![CustomType(2), CustomType(5), CustomType::default()]) 303 | ); 304 | assert_eq!( 305 | zipped.next(), 306 | Some(vec![ 307 | CustomType::default(), 308 | CustomType(6), 309 | CustomType::default() 310 | ]) 311 | ); 312 | assert_eq!(zipped.next(), None); 313 | } 314 | 315 | #[test] 316 | fn test_split_by_length() { 317 | let slice = "123ab9999"; 318 | let c = slice.split_by_length([3, 2, 4, 1, 2]).collect_vec(); 319 | assert_eq!(c[0], "123"); 320 | assert_eq!(c[1], "ab"); 321 | assert_eq!(c[2], "9999"); 322 | assert_eq!(c[3], ""); 323 | assert_eq!(c[4], ""); 324 | } 325 | 326 | #[test] 327 | fn test_infer_schema_safe_basic() { 328 | let mut df = df! { 329 | "integers"=> ["1", "2", "3", "4"], 330 | "floats"=> ["1.1", "2.2", "3.3", "4.4"], 331 | "dates"=> [ "2022-1-1", "2022-1-2", "2022-1-3", "2022-1-4" ], 332 | "strings"=> ["a", "b", "c", "d"], 333 | } 334 | .unwrap(); 335 | safe_infer_schema(&mut df); 336 | 337 | assert_eq!(df.column("integers").unwrap().dtype(), &DataType::Int64); 338 | assert_eq!(df.column("floats").unwrap().dtype(), &DataType::Float64); 339 | assert_eq!(df.column("dates").unwrap().dtype(), &DataType::Date); 340 | assert_eq!(df.column("strings").unwrap().dtype(), &DataType::String); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/writer/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, path::PathBuf}; 2 | 3 | use polars::{ 4 | frame::DataFrame, 5 | io::SerWriter, 6 | prelude::{CsvWriter, IpcWriter, JsonWriter, ParquetWriter}, 7 | }; 8 | 9 | use crate::AppResult; 10 | 11 | pub trait WriteToFile { 12 | fn write_to_file(&self, path: PathBuf, data_frame: &mut DataFrame) -> AppResult<()>; 13 | } 14 | pub struct WriteToCsv { 15 | separator: char, 16 | quote: char, 17 | header: bool, 18 | } 19 | 20 | impl Default for WriteToCsv { 21 | fn default() -> Self { 22 | Self { 23 | separator: ',', 24 | quote: '"', 25 | header: false, 26 | } 27 | } 28 | } 29 | 30 | impl WriteToCsv { 31 | pub fn with_separator_char(mut self, c: char) -> Self { 32 | self.separator = c; 33 | self 34 | } 35 | pub fn with_quote_char(mut self, c: char) -> Self { 36 | self.quote = c; 37 | self 38 | } 39 | pub fn with_header(mut self, no_header: bool) -> Self { 40 | self.header = no_header; 41 | self 42 | } 43 | } 44 | 45 | impl WriteToFile for WriteToCsv { 46 | fn write_to_file(&self, path: PathBuf, data_frame: &mut DataFrame) -> AppResult<()> { 47 | Ok(CsvWriter::new(File::create(path)?) 48 | .with_separator(self.separator.try_into()?) 49 | .with_quote_char(self.quote.try_into()?) 50 | .include_header(self.header) 51 | .finish(data_frame)?) 52 | } 53 | } 54 | 55 | #[derive(Debug, Default)] 56 | pub struct WriteToParquet; 57 | 58 | impl WriteToFile for WriteToParquet { 59 | fn write_to_file(&self, path: PathBuf, data_frame: &mut DataFrame) -> AppResult<()> { 60 | ParquetWriter::new(File::create(path)?).finish(data_frame)?; 61 | Ok(()) 62 | } 63 | } 64 | 65 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 66 | pub enum JsonFormat { 67 | #[default] 68 | Json, 69 | JsonLine, 70 | } 71 | 72 | impl From for polars::prelude::JsonFormat { 73 | fn from(value: JsonFormat) -> Self { 74 | match value { 75 | JsonFormat::Json => polars::prelude::JsonFormat::Json, 76 | JsonFormat::JsonLine => polars::prelude::JsonFormat::JsonLines, 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, Default)] 82 | pub struct WriteToJson { 83 | fmt: JsonFormat, 84 | } 85 | 86 | impl WriteToJson { 87 | pub fn with_format(mut self, fmt: JsonFormat) -> Self { 88 | self.fmt = fmt; 89 | self 90 | } 91 | } 92 | 93 | impl WriteToFile for WriteToJson { 94 | fn write_to_file(&self, path: PathBuf, data_frame: &mut DataFrame) -> AppResult<()> { 95 | Ok(JsonWriter::new(File::create(path)?) 96 | .with_json_format(self.fmt.into()) 97 | .finish(data_frame)?) 98 | } 99 | } 100 | 101 | #[derive(Debug, Default)] 102 | pub struct WriteToArrow; 103 | 104 | impl WriteToFile for WriteToArrow { 105 | fn write_to_file(&self, path: PathBuf, data_frame: &mut DataFrame) -> AppResult<()> { 106 | Ok(IpcWriter::new(File::create(path)?).finish(data_frame)?) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tutorial/company.csv: -------------------------------------------------------------------------------- 1 | id,name,sector,public,value,description 2 | 4a93dd9b-e6b5-4d60-beca-2a2adb220c4d,Gonzalez PLC,e-enable one-to-one niches,Yes,843559550.99,"Gonzalez PLC provides the following services: 3 | - Synergistic responsive Internet solution 4 | - Quality-focused logistical encryption 5 | - Vision-oriented client-server orchestration 6 | - User-centric optimizing product" 7 | 53e9fd6e-5335-4d3d-a97e-a723b9d7098c,"Erickson, Miller and Carroll",redefine compelling web services,Yes,52777102.84,"Erickson, Miller and Carroll provides the following services: 8 | - Public-key zero tolerance extranet 9 | - Horizontal heuristic product 10 | - Multi-channeled responsive product 11 | - Right-sized fault-tolerant complexity" 12 | 276431ab-82d2-4d8c-a675-2f8edc552e51,Madden-Morse,generate B2B action-items,Yes,120712777.78,"Madden-Morse provides the following services: 13 | - Extended multi-state frame 14 | - Multi-tiered mobile superstructure 15 | - Organic directional time-frame 16 | - Front-line motivating hardware" 17 | d11d9826-9556-4b3d-b479-b5cb9a0a2a63,Gillespie-Mueller,aggregate collaborative models,No,68508141.2,"Gillespie-Mueller provides the following services: 18 | - Vision-oriented responsive matrices 19 | - Diverse coherent moratorium 20 | - Decentralized leadingedge throughput 21 | - Realigned logistical intranet" 22 | 8f26cb78-e04d-4db4-9a09-8d3f606a6a8c,Schwartz-Blackwell,optimize clicks-and-mortar bandwidth,No,126233659.87,"Schwartz-Blackwell provides the following services: 23 | - Organic stable frame 24 | - Multi-tiered impactful array 25 | - Reduced regional contingency 26 | - Progressive tertiary standardization" 27 | f9301728-4d57-4c30-967e-ccebd5705711,"Jarvis, Rivera and Montoya",unleash e-business communities,No,630051959.2,"Jarvis, Rivera and Montoya provides the following services: 28 | - Ameliorated hybrid projection 29 | - Operative asynchronous groupware 30 | - Quality-focused systematic standardization 31 | - Progressive executive protocol" 32 | c0d3aa9a-3128-4ff1-9e90-ec66e14a017f,Smith-Owen,maximize B2B technologies,Yes,191621318.95,"Smith-Owen provides the following services: 33 | - Cross-platform tertiary algorithm 34 | - Phased secondary capacity 35 | - Reduced client-server frame 36 | - Re-engineered solution-oriented software" 37 | b13b9214-f77e-43fb-928c-157b8c8284ec,"Jacobs, Allison and Rodriguez",evolve cross-media e-services,Yes,619825086.42,"Jacobs, Allison and Rodriguez provides the following services: 38 | - Proactive optimizing focus group 39 | - Monitored intermediate function 40 | - De-engineered methodical hub 41 | - Down-sized national policy" 42 | 1df77618-b128-407b-af75-0ca2299a56f8,"Beasley, Gomez and Garrett",deliver vertical markets,Yes,983904510.19,"Beasley, Gomez and Garrett provides the following services: 43 | - Business-focused explicit frame 44 | - Decentralized cohesive instruction set 45 | - Focused dedicated alliance 46 | - Intuitive well-modulated groupware" 47 | 8d52d9db-9a3e-4864-9d23-77fe60df4e78,Franklin-Brennan,transform cross-media relationships,No,842908627.64,"Franklin-Brennan provides the following services: 48 | - Future-proofed responsive project 49 | - Configurable context-sensitive service-desk 50 | - Versatile analyzing interface 51 | - Public-key discrete software" 52 | 85f7fcf2-c010-49c9-9a23-109b270af8ff,Henderson-Williams,syndicate collaborative functionalities,Yes,530252537.75,"Henderson-Williams provides the following services: 53 | - Future-proofed hybrid flexibility 54 | - Programmable web-enabled model 55 | - Distributed regional intranet 56 | - Expanded actuating productivity" 57 | 2b8276ad-a35b-4ebb-8091-596e57e17256,Ibarra-Mcclure,embrace open-source synergies,Yes,491044563.43,"Ibarra-Mcclure provides the following services: 58 | - Open-source client-driven artificial intelligence 59 | - Compatible non-volatile archive 60 | - Seamless national intranet 61 | - Versatile transitional utilization" 62 | 11284cc3-b602-4bd0-86ff-eeed987ba32d,Newman LLC,empower transparent technologies,No,720863057.81,"Newman LLC provides the following services: 63 | - Stand-alone 3rdgeneration core 64 | - Triple-buffered 3rdgeneration functionalities 65 | - Open-source real-time service-desk 66 | - Triple-buffered content-based data-warehouse" 67 | 07820136-078e-46fe-b4ee-bc2f5011ffdc,Park PLC,innovate customized markets,Yes,723310925.32,"Park PLC provides the following services: 68 | - Adaptive content-based portal 69 | - Robust non-volatile algorithm 70 | - Persevering 24hour core 71 | - Quality-focused static array" 72 | 58166d0a-b036-4dbb-867a-d702417c437b,Torres Group,facilitate plug-and-play e-services,No,463093118.54,"Torres Group provides the following services: 73 | - Vision-oriented explicit service-desk 74 | - Enterprise-wide 24/7 secured line 75 | - Programmable clear-thinking throughput 76 | - Operative radical database" 77 | d71d233b-30e9-4458-b936-883cef82c84d,Watts and Sons,extend transparent web services,No,898754422.39,"Watts and Sons provides the following services: 78 | - Centralized needs-based productivity 79 | - Business-focused composite superstructure 80 | - Fully-configurable client-server initiative 81 | - Quality-focused hybrid functionalities" 82 | 634e4c94-1eff-4966-bc7b-f33476e68fc0,Chung PLC,unleash intuitive users,Yes,372234254.71,"Chung PLC provides the following services: 83 | - Virtual attitude-oriented paradigm 84 | - Persevering transitional system engine 85 | - Sharable real-time migration 86 | - Persistent scalable function" 87 | 94539dfc-e0d4-420f-b48e-2fe5170aecf3,"Parker, Gaines and Duran",embrace open-source applications,No,409723863.8,"Parker, Gaines and Duran provides the following services: 88 | - Proactive systematic emulation 89 | - Upgradable 24/7 function 90 | - Automated even-keeled architecture 91 | - Advanced global Local Area Network" 92 | 4ed0760e-ab52-416e-a3a1-f45983ad2de8,Schaefer-Thompson,embrace revolutionary infrastructures,No,868512262.22,"Schaefer-Thompson provides the following services: 93 | - Ergonomic 24hour time-frame 94 | - Ergonomic value-added definition 95 | - Switchable national groupware 96 | - Inverse demand-driven parallelism" 97 | 552b87e5-5a08-4c40-88f6-2936d5e9d6cd,Martinez PLC,grow next-generation bandwidth,No,49269774.6,"Martinez PLC provides the following services: 98 | - Reduced client-server definition 99 | - Cloned holistic analyzer 100 | - Digitized value-added standardization 101 | - Up-sized directional matrices" 102 | d8e35b50-7174-47f6-a057-4f4b643fe8d1,Harris LLC,embrace clicks-and-mortar communities,No,453890826.06,"Harris LLC provides the following services: 103 | - Total asymmetric intranet 104 | - Triple-buffered logistical conglomeration 105 | - Persistent human-resource methodology 106 | - Visionary composite model" 107 | 9de081d4-57d5-4e31-ad79-dcb228b76745,Parker-Cortez,evolve cutting-edge eyeballs,No,954584513.23,"Parker-Cortez provides the following services: 108 | - Synchronized full-range hub 109 | - Synchronized content-based challenge 110 | - Future-proofed responsive analyzer 111 | - Synergistic multi-state portal" 112 | 1876d9e7-2cb2-4034-aebd-b1ea9dd3eda6,Padilla-Hunt,cultivate real-time e-commerce,Yes,985836694.53,"Padilla-Hunt provides the following services: 113 | - Sharable hybrid leverage 114 | - Team-oriented disintermediate implementation 115 | - Pre-emptive tertiary interface 116 | - Cloned well-modulated adapter" 117 | 83bb2dde-f0e5-4694-bcdf-1337230112f6,Black LLC,reinvent real-time mindshare,No,213854246.94,"Black LLC provides the following services: 118 | - Programmable bi-directional middleware 119 | - Persistent neutral hierarchy 120 | - Future-proofed national product 121 | - Up-sized eco-centric function" 122 | 8407e268-55a1-406a-a1f3-ee800f2c8fcd,"Maldonado, Sanchez and Lee",whiteboard robust functionalities,No,622854283.05,"Maldonado, Sanchez and Lee provides the following services: 123 | - Pre-emptive full-range initiative 124 | - Right-sized holistic parallelism 125 | - Secured radical application 126 | - Fully-configurable client-driven extranet" 127 | d1aa9279-55af-4023-ba77-2676c32a28b9,Arellano-Moore,facilitate killer content,Yes,135967150.1,"Arellano-Moore provides the following services: 128 | - Compatible contextually-based Graphical User Interface 129 | - Intuitive executive core 130 | - Organic coherent open system 131 | - Profound user-facing leverage" 132 | 6cf0ba77-2ddc-410d-9240-ffe179831b0b,Jones-Pearson,evolve bricks-and-clicks mindshare,No,522054708.16,"Jones-Pearson provides the following services: 133 | - Down-sized contextually-based solution 134 | - Down-sized responsive solution 135 | - Multi-tiered logistical knowledge user 136 | - Face-to-face system-worthy hierarchy" 137 | 05690061-1949-4939-adef-ee23dfddb640,Dunn LLC,transition compelling interfaces,No,94345479.07,"Dunn LLC provides the following services: 138 | - Reduced object-oriented orchestration 139 | - Multi-tiered empowering knowledgebase 140 | - Configurable needs-based pricing structure 141 | - Compatible well-modulated application" 142 | 1a69bb26-1988-41f6-b574-38a9ede71ab2,"Mckay, Morris and Krause",facilitate seamless synergies,No,90121761.58,"Mckay, Morris and Krause provides the following services: 143 | - Integrated mobile architecture 144 | - Extended fault-tolerant flexibility 145 | - Distributed demand-driven info-mediaries 146 | - Customer-focused non-volatile circuit" 147 | e1da45ad-6184-4e50-be11-6d0074992b73,Smith PLC,incubate open-source eyeballs,No,328684402.38,"Smith PLC provides the following services: 148 | - Synergized motivating project 149 | - Up-sized directional analyzer 150 | - Customer-focused user-facing leverage 151 | - Assimilated exuding budgetary management" 152 | ccb857eb-69ee-4441-98b8-97166ccc36dc,Irwin Inc,enhance killer infrastructures,Yes,152026297.28,"Irwin Inc provides the following services: 153 | - Future-proofed zero administration success 154 | - Assimilated needs-based framework 155 | - Organized object-oriented conglomeration 156 | - Reverse-engineered human-resource superstructure" 157 | 67650b29-27d4-473e-b672-bfb1cbd81cf3,Liu Inc,benchmark efficient web-readiness,Yes,768385722.0,"Liu Inc provides the following services: 158 | - Fully-configurable national artificial intelligence 159 | - Persistent homogeneous installation 160 | - Synergized zero-defect protocol 161 | - Operative intangible hierarchy" 162 | 6ff87380-b43c-4589-90fb-9aee1e2a1848,"Elliott, Cole and Owens",scale 24/7 schemas,No,576748859.85,"Elliott, Cole and Owens provides the following services: 163 | - Configurable didactic process improvement 164 | - Public-key actuating open system 165 | - Business-focused regional approach 166 | - Versatile zero tolerance standardization" 167 | 007761b7-4196-429d-9934-ce55b211d051,Jennings and Sons,evolve transparent e-tailers,No,180267902.72,"Jennings and Sons provides the following services: 168 | - Ergonomic asynchronous structure 169 | - Operative directional capability 170 | - Down-sized mission-critical migration 171 | - Business-focused client-driven frame" 172 | 8bfcdc2c-bbfa-4bee-8a1c-de0f939ea02a,May-Rogers,integrate bricks-and-clicks supply-chains,No,771546812.4,"May-Rogers provides the following services: 173 | - Programmable logistical superstructure 174 | - Reduced systematic project 175 | - Reduced didactic extranet 176 | - Virtual next generation ability" 177 | e77f16b9-4208-4afd-8560-d307a8798185,"Coleman, Davis and Huff",morph next-generation bandwidth,Yes,396757434.49,"Coleman, Davis and Huff provides the following services: 178 | - Cross-group client-driven protocol 179 | - Open-architected modular system engine 180 | - Stand-alone zero-defect methodology 181 | - Ameliorated value-added product" 182 | b929d861-4536-4fed-8078-45b30a080eb8,"Wagner, Dorsey and Moore",re-contextualize interactive applications,No,923157326.69,"Wagner, Dorsey and Moore provides the following services: 183 | - Multi-tiered composite groupware 184 | - Fundamental coherent customer loyalty 185 | - Cross-platform uniform attitude 186 | - Persevering needs-based architecture" 187 | ab1c54de-1225-4586-8941-7d1ada6f83fb,Robinson LLC,re-intermediate out-of-the-box action-items,No,799883818.45,"Robinson LLC provides the following services: 188 | - Future-proofed grid-enabled open system 189 | - Realigned tangible projection 190 | - Monitored analyzing extranet 191 | - Multi-tiered zero-defect throughput" 192 | 9dafdfa9-cfbc-4f22-8992-d9ab8ae61d38,Davies Inc,matrix enterprise experiences,No,708589185.93,"Davies Inc provides the following services: 193 | - Customizable even-keeled ability 194 | - Reduced background utilization 195 | - Profound asymmetric implementation 196 | - Fundamental human-resource alliance" 197 | 07fef56e-6d6e-46da-b7a2-9ccd08d10f3c,Valentine PLC,deploy transparent channels,No,451389958.06,"Valentine PLC provides the following services: 198 | - Optimized human-resource Graphical User Interface 199 | - Intuitive uniform forecast 200 | - Stand-alone eco-centric projection 201 | - Persevering background initiative" 202 | e7ba15bf-30f3-4539-acaf-e7220cdcd5fd,"Harris, Anderson and Simmons",harness leading-edge relationships,Yes,829708846.24,"Harris, Anderson and Simmons provides the following services: 203 | - Stand-alone attitude-oriented throughput 204 | - Persistent clear-thinking migration 205 | - Fundamental modular collaboration 206 | - Cross-platform system-worthy hardware" 207 | d5664677-e39e-4c0c-a661-559ade0b3d29,Morse-Hunter,facilitate innovative synergies,Yes,565380402.04,"Morse-Hunter provides the following services: 208 | - Streamlined zero administration frame 209 | - Cloned 5thgeneration projection 210 | - Sharable mission-critical collaboration 211 | - Compatible zero-defect task-force" 212 | 7ba33664-4184-4fd7-9355-6ce61a26d15e,Haas-Harris,integrate wireless functionalities,Yes,538628358.85,"Haas-Harris provides the following services: 213 | - Decentralized even-keeled customer loyalty 214 | - Extended contextually-based knowledgebase 215 | - Robust solution-oriented capability 216 | - Reactive even-keeled strategy" 217 | 96bc310f-95ec-466f-9449-1b4a7bf33ef6,Richmond-Campbell,reinvent bleeding-edge networks,Yes,977627625.03,"Richmond-Campbell provides the following services: 218 | - Reverse-engineered secondary definition 219 | - Sharable 24/7 secured line 220 | - Synchronized well-modulated throughput 221 | - Devolved explicit benchmark" 222 | 55f0e5f6-7865-46b0-81e5-0096cde46c71,"Cameron, Graham and Weeks",leverage proactive convergence,Yes,262362960.71,"Cameron, Graham and Weeks provides the following services: 223 | - Future-proofed well-modulated encryption 224 | - Front-line intangible moratorium 225 | - Organized bi-directional intranet 226 | - Balanced contextually-based system engine" 227 | 395bee8e-ef58-477a-9e7d-255a287cf6eb,"Mcknight, Lyons and Madden",engineer interactive e-services,Yes,33115490.21,"Mcknight, Lyons and Madden provides the following services: 228 | - Cloned client-server customer loyalty 229 | - Pre-emptive disintermediate product 230 | - Horizontal didactic firmware 231 | - Persistent homogeneous ability" 232 | 7177317e-b0e8-4d92-bced-841d9f6541d8,"Ward, Simpson and Dunn",incubate turn-key models,Yes,469460871.47,"Ward, Simpson and Dunn provides the following services: 233 | - Streamlined zero-defect knowledgebase 234 | - Multi-tiered value-added groupware 235 | - Programmable multi-state encryption 236 | - Intuitive even-keeled access" 237 | 1af8689b-0adf-476a-94fe-677cc4e84c98,Henderson-Romero,incubate rich infrastructures,No,794072380.32,"Henderson-Romero provides the following services: 238 | - Polarized static migration 239 | - Total systemic ability 240 | - Exclusive stable complexity 241 | - Switchable demand-driven Graphical User Interface" 242 | 678e8841-f5b4-4ae7-b23a-c0b2fc1a554e,Miller PLC,drive impactful content,No,974340161.82,"Miller PLC provides the following services: 243 | - Configurable reciprocal productivity 244 | - Intuitive solution-oriented moderator 245 | - Grass-roots secondary framework 246 | - Compatible scalable moratorium" 247 | ca2666dc-2f87-462e-ab3c-5c05aecf5b0a,"Baker, Jensen and Bradshaw",repurpose holistic markets,No,559086806.22,"Baker, Jensen and Bradshaw provides the following services: 248 | - User-centric methodical archive 249 | - Enterprise-wide bifurcated alliance 250 | - Right-sized zero-defect success 251 | - Mandatory user-facing flexibility" 252 | -------------------------------------------------------------------------------- /tutorial/housing.csv: -------------------------------------------------------------------------------- 1 | price,area,bedrooms,bathrooms,stories,mainroad,guestroom,basement,hotwaterheating,airconditioning,parking,prefarea,furnishingstatus 2 | 13300000,7420,4,2,3,yes,no,no,no,yes,2,yes,furnished 3 | 12250000,8960,4,4,4,yes,no,no,no,yes,3,no,furnished 4 | 12250000,9960,3,2,2,yes,no,yes,no,no,2,yes,semi-furnished 5 | 12215000,7500,4,2,2,yes,no,yes,no,yes,3,yes,furnished 6 | 11410000,7420,4,1,2,yes,yes,yes,no,yes,2,no,furnished 7 | 10850000,7500,3,3,1,yes,no,yes,no,yes,2,yes,semi-furnished 8 | 10150000,8580,4,3,4,yes,no,no,no,yes,2,yes,semi-furnished 9 | 10150000,16200,5,3,2,yes,no,no,no,no,0,no,unfurnished 10 | 9870000,8100,4,1,2,yes,yes,yes,no,yes,2,yes,furnished 11 | 9800000,5750,3,2,4,yes,yes,no,no,yes,1,yes,unfurnished 12 | 9800000,13200,3,1,2,yes,no,yes,no,yes,2,yes,furnished 13 | 9681000,6000,4,3,2,yes,yes,yes,yes,no,2,no,semi-furnished 14 | 9310000,6550,4,2,2,yes,no,no,no,yes,1,yes,semi-furnished 15 | 9240000,3500,4,2,2,yes,no,no,yes,no,2,no,furnished 16 | 9240000,7800,3,2,2,yes,no,no,no,no,0,yes,semi-furnished 17 | 9100000,6000,4,1,2,yes,no,yes,no,no,2,no,semi-furnished 18 | 9100000,6600,4,2,2,yes,yes,yes,no,yes,1,yes,unfurnished 19 | 8960000,8500,3,2,4,yes,no,no,no,yes,2,no,furnished 20 | 8890000,4600,3,2,2,yes,yes,no,no,yes,2,no,furnished 21 | 8855000,6420,3,2,2,yes,no,no,no,yes,1,yes,semi-furnished 22 | 8750000,4320,3,1,2,yes,no,yes,yes,no,2,no,semi-furnished 23 | 8680000,7155,3,2,1,yes,yes,yes,no,yes,2,no,unfurnished 24 | 8645000,8050,3,1,1,yes,yes,yes,no,yes,1,no,furnished 25 | 8645000,4560,3,2,2,yes,yes,yes,no,yes,1,no,furnished 26 | 8575000,8800,3,2,2,yes,no,no,no,yes,2,no,furnished 27 | 8540000,6540,4,2,2,yes,yes,yes,no,yes,2,yes,furnished 28 | 8463000,6000,3,2,4,yes,yes,yes,no,yes,0,yes,semi-furnished 29 | 8400000,8875,3,1,1,yes,no,no,no,no,1,no,semi-furnished 30 | 8400000,7950,5,2,2,yes,no,yes,yes,no,2,no,unfurnished 31 | 8400000,5500,4,2,2,yes,no,yes,no,yes,1,yes,semi-furnished 32 | 8400000,7475,3,2,4,yes,no,no,no,yes,2,no,unfurnished 33 | 8400000,7000,3,1,4,yes,no,no,no,yes,2,no,semi-furnished 34 | 8295000,4880,4,2,2,yes,no,no,no,yes,1,yes,furnished 35 | 8190000,5960,3,3,2,yes,yes,yes,no,no,1,no,unfurnished 36 | 8120000,6840,5,1,2,yes,yes,yes,no,yes,1,no,furnished 37 | 8080940,7000,3,2,4,yes,no,no,no,yes,2,no,furnished 38 | 8043000,7482,3,2,3,yes,no,no,yes,no,1,yes,furnished 39 | 7980000,9000,4,2,4,yes,no,no,no,yes,2,no,furnished 40 | 7962500,6000,3,1,4,yes,yes,no,no,yes,2,no,unfurnished 41 | 7910000,6000,4,2,4,yes,no,no,no,yes,1,no,semi-furnished 42 | 7875000,6550,3,1,2,yes,no,yes,no,yes,0,yes,furnished 43 | 7840000,6360,3,2,4,yes,no,no,no,yes,0,yes,furnished 44 | 7700000,6480,3,2,4,yes,no,no,no,yes,2,no,unfurnished 45 | 7700000,6000,4,2,4,yes,no,no,no,no,2,no,semi-furnished 46 | 7560000,6000,4,2,4,yes,no,no,no,yes,1,no,furnished 47 | 7560000,6000,3,2,3,yes,no,no,no,yes,0,no,semi-furnished 48 | 7525000,6000,3,2,4,yes,no,no,no,yes,1,no,furnished 49 | 7490000,6600,3,1,4,yes,no,no,no,yes,3,yes,furnished 50 | 7455000,4300,3,2,2,yes,no,yes,no,no,1,no,unfurnished 51 | 7420000,7440,3,2,1,yes,yes,yes,no,yes,0,yes,semi-furnished 52 | 7420000,7440,3,2,4,yes,no,no,no,no,1,yes,unfurnished 53 | 7420000,6325,3,1,4,yes,no,no,no,yes,1,no,unfurnished 54 | 7350000,6000,4,2,4,yes,yes,no,no,yes,1,no,furnished 55 | 7350000,5150,3,2,4,yes,no,no,no,yes,2,no,semi-furnished 56 | 7350000,6000,3,2,2,yes,yes,no,no,yes,1,no,semi-furnished 57 | 7350000,6000,3,1,2,yes,no,no,no,yes,1,no,unfurnished 58 | 7343000,11440,4,1,2,yes,no,yes,no,no,1,yes,semi-furnished 59 | 7245000,9000,4,2,4,yes,yes,no,no,yes,1,yes,furnished 60 | 7210000,7680,4,2,4,yes,yes,no,no,yes,1,no,semi-furnished 61 | 7210000,6000,3,2,4,yes,yes,no,no,yes,1,no,furnished 62 | 7140000,6000,3,2,2,yes,yes,no,no,no,1,no,semi-furnished 63 | 7070000,8880,2,1,1,yes,no,no,no,yes,1,no,semi-furnished 64 | 7070000,6240,4,2,2,yes,no,no,no,yes,1,no,furnished 65 | 7035000,6360,4,2,3,yes,no,no,no,yes,2,yes,furnished 66 | 7000000,11175,3,1,1,yes,no,yes,no,yes,1,yes,furnished 67 | 6930000,8880,3,2,2,yes,no,yes,no,yes,1,no,furnished 68 | 6930000,13200,2,1,1,yes,no,yes,yes,no,1,no,furnished 69 | 6895000,7700,3,2,1,yes,no,no,no,no,2,no,unfurnished 70 | 6860000,6000,3,1,1,yes,no,no,no,yes,1,no,furnished 71 | 6790000,12090,4,2,2,yes,no,no,no,no,2,yes,furnished 72 | 6790000,4000,3,2,2,yes,no,yes,no,yes,0,yes,semi-furnished 73 | 6755000,6000,4,2,4,yes,no,no,no,yes,0,no,unfurnished 74 | 6720000,5020,3,1,4,yes,no,no,no,yes,0,yes,unfurnished 75 | 6685000,6600,2,2,4,yes,no,yes,no,no,0,yes,furnished 76 | 6650000,4040,3,1,2,yes,no,yes,yes,no,1,no,furnished 77 | 6650000,4260,4,2,2,yes,no,no,yes,no,0,no,semi-furnished 78 | 6650000,6420,3,2,3,yes,no,no,no,yes,0,yes,furnished 79 | 6650000,6500,3,2,3,yes,no,no,no,yes,0,yes,furnished 80 | 6650000,5700,3,1,1,yes,yes,yes,no,yes,2,yes,furnished 81 | 6650000,6000,3,2,3,yes,yes,no,no,yes,0,no,furnished 82 | 6629000,6000,3,1,2,yes,no,no,yes,no,1,yes,semi-furnished 83 | 6615000,4000,3,2,2,yes,no,yes,no,yes,1,no,semi-furnished 84 | 6615000,10500,3,2,1,yes,no,yes,no,yes,1,yes,furnished 85 | 6580000,6000,3,2,4,yes,no,no,no,yes,0,no,semi-furnished 86 | 6510000,3760,3,1,2,yes,no,no,yes,no,2,no,semi-furnished 87 | 6510000,8250,3,2,3,yes,no,no,no,yes,0,no,furnished 88 | 6510000,6670,3,1,3,yes,no,yes,no,no,0,yes,unfurnished 89 | 6475000,3960,3,1,1,yes,no,yes,no,no,2,no,semi-furnished 90 | 6475000,7410,3,1,1,yes,yes,yes,no,yes,2,yes,unfurnished 91 | 6440000,8580,5,3,2,yes,no,no,no,no,2,no,furnished 92 | 6440000,5000,3,1,2,yes,no,no,no,yes,0,no,semi-furnished 93 | 6419000,6750,2,1,1,yes,yes,yes,no,no,2,yes,furnished 94 | 6405000,4800,3,2,4,yes,yes,no,no,yes,0,no,furnished 95 | 6300000,7200,3,2,1,yes,no,yes,no,yes,3,no,semi-furnished 96 | 6300000,6000,4,2,4,yes,no,no,no,no,1,no,semi-furnished 97 | 6300000,4100,3,2,3,yes,no,no,no,yes,2,no,semi-furnished 98 | 6300000,9000,3,1,1,yes,no,yes,no,no,1,yes,furnished 99 | 6300000,6400,3,1,1,yes,yes,yes,no,yes,1,yes,semi-furnished 100 | 6293000,6600,3,2,3,yes,no,no,no,yes,0,yes,unfurnished 101 | 6265000,6000,4,1,3,yes,yes,yes,no,no,0,yes,unfurnished 102 | 6230000,6600,3,2,1,yes,no,yes,no,yes,0,yes,unfurnished 103 | 6230000,5500,3,1,3,yes,no,no,no,no,1,yes,unfurnished 104 | 6195000,5500,3,2,4,yes,yes,no,no,yes,1,no,semi-furnished 105 | 6195000,6350,3,2,3,yes,yes,no,no,yes,0,no,furnished 106 | 6195000,5500,3,2,1,yes,yes,yes,no,no,2,yes,furnished 107 | 6160000,4500,3,1,4,yes,no,no,no,yes,0,no,unfurnished 108 | 6160000,5450,4,2,1,yes,no,yes,no,yes,0,yes,semi-furnished 109 | 6125000,6420,3,1,3,yes,no,yes,no,no,0,yes,unfurnished 110 | 6107500,3240,4,1,3,yes,no,no,no,no,1,no,semi-furnished 111 | 6090000,6615,4,2,2,yes,yes,no,yes,no,1,no,semi-furnished 112 | 6090000,6600,3,1,1,yes,yes,yes,no,no,2,yes,semi-furnished 113 | 6090000,8372,3,1,3,yes,no,no,no,yes,2,no,unfurnished 114 | 6083000,4300,6,2,2,yes,no,no,no,no,0,no,furnished 115 | 6083000,9620,3,1,1,yes,no,yes,no,no,2,yes,furnished 116 | 6020000,6800,2,1,1,yes,yes,yes,no,no,2,no,furnished 117 | 6020000,8000,3,1,1,yes,yes,yes,no,yes,2,yes,semi-furnished 118 | 6020000,6900,3,2,1,yes,yes,yes,no,no,0,yes,unfurnished 119 | 5950000,3700,4,1,2,yes,yes,no,no,yes,0,no,furnished 120 | 5950000,6420,3,1,1,yes,no,yes,no,yes,0,yes,furnished 121 | 5950000,7020,3,1,1,yes,no,yes,no,yes,2,yes,semi-furnished 122 | 5950000,6540,3,1,1,yes,yes,yes,no,no,2,yes,furnished 123 | 5950000,7231,3,1,2,yes,yes,yes,no,yes,0,yes,semi-furnished 124 | 5950000,6254,4,2,1,yes,no,yes,no,no,1,yes,semi-furnished 125 | 5950000,7320,4,2,2,yes,no,no,no,no,0,no,furnished 126 | 5950000,6525,3,2,4,yes,no,no,no,no,1,no,furnished 127 | 5943000,15600,3,1,1,yes,no,no,no,yes,2,no,semi-furnished 128 | 5880000,7160,3,1,1,yes,no,yes,no,no,2,yes,unfurnished 129 | 5880000,6500,3,2,3,yes,no,no,no,yes,0,no,unfurnished 130 | 5873000,5500,3,1,3,yes,yes,no,no,yes,1,no,furnished 131 | 5873000,11460,3,1,3,yes,no,no,no,no,2,yes,semi-furnished 132 | 5866000,4800,3,1,1,yes,yes,yes,no,no,0,no,unfurnished 133 | 5810000,5828,4,1,4,yes,yes,no,no,no,0,no,semi-furnished 134 | 5810000,5200,3,1,3,yes,no,no,no,yes,0,no,semi-furnished 135 | 5810000,4800,3,1,3,yes,no,no,no,yes,0,no,unfurnished 136 | 5803000,7000,3,1,1,yes,no,yes,no,no,2,yes,semi-furnished 137 | 5775000,6000,3,2,4,yes,no,no,no,yes,0,no,unfurnished 138 | 5740000,5400,4,2,2,yes,no,no,no,yes,2,no,unfurnished 139 | 5740000,4640,4,1,2,yes,no,no,no,no,1,no,semi-furnished 140 | 5740000,5000,3,1,3,yes,no,no,no,yes,0,no,semi-furnished 141 | 5740000,6360,3,1,1,yes,yes,yes,no,yes,2,yes,furnished 142 | 5740000,5800,3,2,4,yes,no,no,no,yes,0,no,unfurnished 143 | 5652500,6660,4,2,2,yes,yes,yes,no,no,1,yes,semi-furnished 144 | 5600000,10500,4,2,2,yes,no,no,no,no,1,no,semi-furnished 145 | 5600000,4800,5,2,3,no,no,yes,yes,no,0,no,unfurnished 146 | 5600000,4700,4,1,2,yes,yes,yes,no,yes,1,no,furnished 147 | 5600000,5000,3,1,4,yes,no,no,no,no,0,no,furnished 148 | 5600000,10500,2,1,1,yes,no,no,no,no,1,no,semi-furnished 149 | 5600000,5500,3,2,2,yes,no,no,no,no,1,no,semi-furnished 150 | 5600000,6360,3,1,3,yes,no,no,no,no,0,yes,semi-furnished 151 | 5600000,6600,4,2,1,yes,no,yes,no,no,0,yes,semi-furnished 152 | 5600000,5136,3,1,2,yes,yes,yes,no,yes,0,yes,unfurnished 153 | 5565000,4400,4,1,2,yes,no,no,no,yes,2,yes,semi-furnished 154 | 5565000,5400,5,1,2,yes,yes,yes,no,yes,0,yes,furnished 155 | 5530000,3300,3,3,2,yes,no,yes,no,no,0,no,semi-furnished 156 | 5530000,3650,3,2,2,yes,no,no,no,no,2,no,semi-furnished 157 | 5530000,6100,3,2,1,yes,no,yes,no,no,2,yes,furnished 158 | 5523000,6900,3,1,1,yes,yes,yes,no,no,0,yes,semi-furnished 159 | 5495000,2817,4,2,2,no,yes,yes,no,no,1,no,furnished 160 | 5495000,7980,3,1,1,yes,no,no,no,no,2,no,semi-furnished 161 | 5460000,3150,3,2,1,yes,yes,yes,no,yes,0,no,furnished 162 | 5460000,6210,4,1,4,yes,yes,no,no,yes,0,no,furnished 163 | 5460000,6100,3,1,3,yes,yes,no,no,yes,0,yes,semi-furnished 164 | 5460000,6600,4,2,2,yes,yes,yes,no,no,0,yes,semi-furnished 165 | 5425000,6825,3,1,1,yes,yes,yes,no,yes,0,yes,semi-furnished 166 | 5390000,6710,3,2,2,yes,yes,yes,no,no,1,yes,furnished 167 | 5383000,6450,3,2,1,yes,yes,yes,yes,no,0,no,unfurnished 168 | 5320000,7800,3,1,1,yes,no,yes,no,yes,2,yes,unfurnished 169 | 5285000,4600,2,2,1,yes,no,no,no,yes,2,no,semi-furnished 170 | 5250000,4260,4,1,2,yes,no,yes,no,yes,0,no,furnished 171 | 5250000,6540,4,2,2,no,no,no,no,yes,0,no,semi-furnished 172 | 5250000,5500,3,2,1,yes,no,yes,no,no,0,no,semi-furnished 173 | 5250000,10269,3,1,1,yes,no,no,no,no,1,yes,semi-furnished 174 | 5250000,8400,3,1,2,yes,yes,yes,no,yes,2,yes,unfurnished 175 | 5250000,5300,4,2,1,yes,no,no,no,yes,0,yes,unfurnished 176 | 5250000,3800,3,1,2,yes,yes,yes,no,no,1,yes,unfurnished 177 | 5250000,9800,4,2,2,yes,yes,no,no,no,2,no,semi-furnished 178 | 5250000,8520,3,1,1,yes,no,no,no,yes,2,no,furnished 179 | 5243000,6050,3,1,1,yes,no,yes,no,no,0,yes,semi-furnished 180 | 5229000,7085,3,1,1,yes,yes,yes,no,no,2,yes,semi-furnished 181 | 5215000,3180,3,2,2,yes,no,no,no,no,2,no,semi-furnished 182 | 5215000,4500,4,2,1,no,no,yes,no,yes,2,no,semi-furnished 183 | 5215000,7200,3,1,2,yes,yes,yes,no,no,1,yes,furnished 184 | 5145000,3410,3,1,2,no,no,no,no,yes,0,no,semi-furnished 185 | 5145000,7980,3,1,1,yes,no,no,no,no,1,yes,semi-furnished 186 | 5110000,3000,3,2,2,yes,yes,yes,no,no,0,no,furnished 187 | 5110000,3000,3,1,2,yes,no,yes,no,no,0,no,unfurnished 188 | 5110000,11410,2,1,2,yes,no,no,no,no,0,yes,furnished 189 | 5110000,6100,3,1,1,yes,no,yes,no,yes,0,yes,semi-furnished 190 | 5075000,5720,2,1,2,yes,no,no,no,yes,0,yes,unfurnished 191 | 5040000,3540,2,1,1,no,yes,yes,no,no,0,no,semi-furnished 192 | 5040000,7600,4,1,2,yes,no,no,no,yes,2,no,furnished 193 | 5040000,10700,3,1,2,yes,yes,yes,no,no,0,no,semi-furnished 194 | 5040000,6600,3,1,1,yes,yes,yes,no,no,0,yes,furnished 195 | 5033000,4800,2,1,1,yes,yes,yes,no,no,0,no,semi-furnished 196 | 5005000,8150,3,2,1,yes,yes,yes,no,no,0,no,semi-furnished 197 | 4970000,4410,4,3,2,yes,no,yes,no,no,2,no,semi-furnished 198 | 4970000,7686,3,1,1,yes,yes,yes,yes,no,0,no,semi-furnished 199 | 4956000,2800,3,2,2,no,no,yes,no,yes,1,no,semi-furnished 200 | 4935000,5948,3,1,2,yes,no,no,no,yes,0,no,semi-furnished 201 | 4907000,4200,3,1,2,yes,no,no,no,no,1,no,furnished 202 | 4900000,4520,3,1,2,yes,no,yes,no,yes,0,no,semi-furnished 203 | 4900000,4095,3,1,2,no,yes,yes,no,yes,0,no,semi-furnished 204 | 4900000,4120,2,1,1,yes,no,yes,no,no,1,no,semi-furnished 205 | 4900000,5400,4,1,2,yes,no,no,no,no,0,no,semi-furnished 206 | 4900000,4770,3,1,1,yes,yes,yes,no,no,0,no,semi-furnished 207 | 4900000,6300,3,1,1,yes,no,no,no,yes,2,no,semi-furnished 208 | 4900000,5800,2,1,1,yes,yes,yes,no,yes,0,no,semi-furnished 209 | 4900000,3000,3,1,2,yes,no,yes,no,yes,0,no,semi-furnished 210 | 4900000,2970,3,1,3,yes,no,no,no,no,0,no,semi-furnished 211 | 4900000,6720,3,1,1,yes,no,no,no,no,0,no,unfurnished 212 | 4900000,4646,3,1,2,yes,yes,yes,no,no,2,no,semi-furnished 213 | 4900000,12900,3,1,1,yes,no,no,no,no,2,no,furnished 214 | 4893000,3420,4,2,2,yes,no,yes,no,yes,2,no,semi-furnished 215 | 4893000,4995,4,2,1,yes,no,yes,no,no,0,no,semi-furnished 216 | 4865000,4350,2,1,1,yes,no,yes,no,no,0,no,unfurnished 217 | 4830000,4160,3,1,3,yes,no,no,no,no,0,no,unfurnished 218 | 4830000,6040,3,1,1,yes,no,no,no,no,2,yes,semi-furnished 219 | 4830000,6862,3,1,2,yes,no,no,no,yes,2,yes,furnished 220 | 4830000,4815,2,1,1,yes,no,no,no,yes,0,yes,semi-furnished 221 | 4795000,7000,3,1,2,yes,no,yes,no,no,0,no,unfurnished 222 | 4795000,8100,4,1,4,yes,no,yes,no,yes,2,no,semi-furnished 223 | 4767000,3420,4,2,2,yes,no,no,no,no,0,no,semi-furnished 224 | 4760000,9166,2,1,1,yes,no,yes,no,yes,2,no,semi-furnished 225 | 4760000,6321,3,1,2,yes,no,yes,no,yes,1,no,furnished 226 | 4760000,10240,2,1,1,yes,no,no,no,yes,2,yes,unfurnished 227 | 4753000,6440,2,1,1,yes,no,no,no,yes,3,no,semi-furnished 228 | 4690000,5170,3,1,4,yes,no,no,no,yes,0,no,semi-furnished 229 | 4690000,6000,2,1,1,yes,no,yes,no,yes,1,no,furnished 230 | 4690000,3630,3,1,2,yes,no,no,no,no,2,no,semi-furnished 231 | 4690000,9667,4,2,2,yes,yes,yes,no,no,1,no,semi-furnished 232 | 4690000,5400,2,1,2,yes,no,no,no,no,0,yes,semi-furnished 233 | 4690000,4320,3,1,1,yes,no,no,no,no,0,yes,semi-furnished 234 | 4655000,3745,3,1,2,yes,no,yes,no,no,0,no,furnished 235 | 4620000,4160,3,1,1,yes,yes,yes,no,yes,0,no,unfurnished 236 | 4620000,3880,3,2,2,yes,no,yes,no,no,2,no,semi-furnished 237 | 4620000,5680,3,1,2,yes,yes,no,no,yes,1,no,semi-furnished 238 | 4620000,2870,2,1,2,yes,yes,yes,no,no,0,yes,semi-furnished 239 | 4620000,5010,3,1,2,yes,no,yes,no,no,0,no,semi-furnished 240 | 4613000,4510,4,2,2,yes,no,yes,no,no,0,no,semi-furnished 241 | 4585000,4000,3,1,2,yes,no,no,no,no,1,no,furnished 242 | 4585000,3840,3,1,2,yes,no,no,no,no,1,yes,semi-furnished 243 | 4550000,3760,3,1,1,yes,no,no,no,no,2,no,semi-furnished 244 | 4550000,3640,3,1,2,yes,no,no,no,yes,0,no,furnished 245 | 4550000,2550,3,1,2,yes,no,yes,no,no,0,no,furnished 246 | 4550000,5320,3,1,2,yes,yes,yes,no,no,0,yes,semi-furnished 247 | 4550000,5360,3,1,2,yes,no,no,no,no,2,yes,unfurnished 248 | 4550000,3520,3,1,1,yes,no,no,no,no,0,yes,semi-furnished 249 | 4550000,8400,4,1,4,yes,no,no,no,no,3,no,unfurnished 250 | 4543000,4100,2,2,1,yes,yes,yes,no,no,0,no,semi-furnished 251 | 4543000,4990,4,2,2,yes,yes,yes,no,no,0,yes,furnished 252 | 4515000,3510,3,1,3,yes,no,no,no,no,0,no,semi-furnished 253 | 4515000,3450,3,1,2,yes,no,yes,no,no,1,no,semi-furnished 254 | 4515000,9860,3,1,1,yes,no,no,no,no,0,no,semi-furnished 255 | 4515000,3520,2,1,2,yes,no,no,no,no,0,yes,furnished 256 | 4480000,4510,4,1,2,yes,no,no,no,yes,2,no,semi-furnished 257 | 4480000,5885,2,1,1,yes,no,no,no,yes,1,no,unfurnished 258 | 4480000,4000,3,1,2,yes,no,no,no,no,2,no,furnished 259 | 4480000,8250,3,1,1,yes,no,no,no,no,0,no,furnished 260 | 4480000,4040,3,1,2,yes,no,no,no,no,1,no,semi-furnished 261 | 4473000,6360,2,1,1,yes,no,yes,no,yes,1,no,furnished 262 | 4473000,3162,3,1,2,yes,no,no,no,yes,1,no,furnished 263 | 4473000,3510,3,1,2,yes,no,no,no,no,0,no,semi-furnished 264 | 4445000,3750,2,1,1,yes,yes,yes,no,no,0,no,semi-furnished 265 | 4410000,3968,3,1,2,no,no,no,no,no,0,no,semi-furnished 266 | 4410000,4900,2,1,2,yes,no,yes,no,no,0,no,semi-furnished 267 | 4403000,2880,3,1,2,yes,no,no,no,no,0,yes,semi-furnished 268 | 4403000,4880,3,1,1,yes,no,no,no,no,2,yes,unfurnished 269 | 4403000,4920,3,1,2,yes,no,no,no,no,1,no,semi-furnished 270 | 4382000,4950,4,1,2,yes,no,no,no,yes,0,no,semi-furnished 271 | 4375000,3900,3,1,2,yes,no,no,no,no,0,no,unfurnished 272 | 4340000,4500,3,2,3,yes,no,no,yes,no,1,no,furnished 273 | 4340000,1905,5,1,2,no,no,yes,no,no,0,no,semi-furnished 274 | 4340000,4075,3,1,1,yes,yes,yes,no,no,2,no,semi-furnished 275 | 4340000,3500,4,1,2,yes,no,no,no,no,2,no,furnished 276 | 4340000,6450,4,1,2,yes,no,no,no,no,0,no,semi-furnished 277 | 4319000,4032,2,1,1,yes,no,yes,no,no,0,no,furnished 278 | 4305000,4400,2,1,1,yes,no,no,no,no,1,no,semi-furnished 279 | 4305000,10360,2,1,1,yes,no,no,no,no,1,yes,semi-furnished 280 | 4277000,3400,3,1,2,yes,no,yes,no,no,2,yes,semi-furnished 281 | 4270000,6360,2,1,1,yes,no,no,no,no,0,no,furnished 282 | 4270000,6360,2,1,2,yes,no,no,no,no,0,no,unfurnished 283 | 4270000,4500,2,1,1,yes,no,no,no,yes,2,no,furnished 284 | 4270000,2175,3,1,2,no,yes,yes,no,yes,0,no,unfurnished 285 | 4270000,4360,4,1,2,yes,no,no,no,no,0,no,furnished 286 | 4270000,7770,2,1,1,yes,no,no,no,no,1,no,furnished 287 | 4235000,6650,3,1,2,yes,yes,no,no,no,0,no,semi-furnished 288 | 4235000,2787,3,1,1,yes,no,yes,no,no,0,yes,furnished 289 | 4200000,5500,3,1,2,yes,no,no,no,yes,0,no,unfurnished 290 | 4200000,5040,3,1,2,yes,no,yes,no,yes,0,no,unfurnished 291 | 4200000,5850,2,1,1,yes,yes,yes,no,no,2,no,semi-furnished 292 | 4200000,2610,4,3,2,no,no,no,no,no,0,no,semi-furnished 293 | 4200000,2953,3,1,2,yes,no,yes,no,yes,0,no,unfurnished 294 | 4200000,2747,4,2,2,no,no,no,no,no,0,no,semi-furnished 295 | 4200000,4410,2,1,1,no,no,no,no,no,1,no,unfurnished 296 | 4200000,4000,4,2,2,no,no,no,no,no,0,no,semi-furnished 297 | 4200000,2325,3,1,2,no,no,no,no,no,0,no,semi-furnished 298 | 4200000,4600,3,2,2,yes,no,no,no,yes,1,no,semi-furnished 299 | 4200000,3640,3,2,2,yes,no,yes,no,no,0,no,unfurnished 300 | 4200000,5800,3,1,1,yes,no,no,yes,no,2,no,semi-furnished 301 | 4200000,7000,3,1,1,yes,no,no,no,no,3,no,furnished 302 | 4200000,4079,3,1,3,yes,no,no,no,no,0,no,semi-furnished 303 | 4200000,3520,3,1,2,yes,no,no,no,no,0,yes,semi-furnished 304 | 4200000,2145,3,1,3,yes,no,no,no,no,1,yes,unfurnished 305 | 4200000,4500,3,1,1,yes,no,yes,no,no,0,no,furnished 306 | 4193000,8250,3,1,1,yes,no,yes,no,no,3,no,semi-furnished 307 | 4193000,3450,3,1,2,yes,no,no,no,no,1,no,semi-furnished 308 | 4165000,4840,3,1,2,yes,no,no,no,no,1,no,semi-furnished 309 | 4165000,4080,3,1,2,yes,no,no,no,no,2,no,semi-furnished 310 | 4165000,4046,3,1,2,yes,no,yes,no,no,1,no,semi-furnished 311 | 4130000,4632,4,1,2,yes,no,no,no,yes,0,no,semi-furnished 312 | 4130000,5985,3,1,1,yes,no,yes,no,no,0,no,semi-furnished 313 | 4123000,6060,2,1,1,yes,no,yes,no,no,1,no,semi-furnished 314 | 4098500,3600,3,1,1,yes,no,yes,no,yes,0,yes,furnished 315 | 4095000,3680,3,2,2,yes,no,no,no,no,0,no,semi-furnished 316 | 4095000,4040,2,1,2,yes,no,no,no,no,1,no,semi-furnished 317 | 4095000,5600,2,1,1,yes,no,no,no,yes,0,no,semi-furnished 318 | 4060000,5900,4,2,2,no,no,yes,no,no,1,no,unfurnished 319 | 4060000,4992,3,2,2,yes,no,no,no,no,2,no,unfurnished 320 | 4060000,4340,3,1,1,yes,no,no,no,no,0,no,semi-furnished 321 | 4060000,3000,4,1,3,yes,no,yes,no,yes,2,no,semi-furnished 322 | 4060000,4320,3,1,2,yes,no,no,no,no,2,yes,furnished 323 | 4025000,3630,3,2,2,yes,no,no,yes,no,2,no,semi-furnished 324 | 4025000,3460,3,2,1,yes,no,yes,no,yes,1,no,furnished 325 | 4025000,5400,3,1,1,yes,no,no,no,no,3,no,semi-furnished 326 | 4007500,4500,3,1,2,no,no,yes,no,yes,0,no,semi-furnished 327 | 4007500,3460,4,1,2,yes,no,no,no,yes,0,no,semi-furnished 328 | 3990000,4100,4,1,1,no,no,yes,no,no,0,no,unfurnished 329 | 3990000,6480,3,1,2,no,no,no,no,yes,1,no,semi-furnished 330 | 3990000,4500,3,2,2,no,no,yes,no,yes,0,no,semi-furnished 331 | 3990000,3960,3,1,2,yes,no,no,no,no,0,no,furnished 332 | 3990000,4050,2,1,2,yes,yes,yes,no,no,0,yes,unfurnished 333 | 3920000,7260,3,2,1,yes,yes,yes,no,no,3,no,furnished 334 | 3920000,5500,4,1,2,yes,yes,yes,no,no,0,no,semi-furnished 335 | 3920000,3000,3,1,2,yes,no,no,no,no,0,no,semi-furnished 336 | 3920000,3290,2,1,1,yes,no,no,yes,no,1,no,furnished 337 | 3920000,3816,2,1,1,yes,no,yes,no,yes,2,no,furnished 338 | 3920000,8080,3,1,1,yes,no,no,no,yes,2,no,semi-furnished 339 | 3920000,2145,4,2,1,yes,no,yes,no,no,0,yes,unfurnished 340 | 3885000,3780,2,1,2,yes,yes,yes,no,no,0,no,semi-furnished 341 | 3885000,3180,4,2,2,yes,no,no,no,no,0,no,furnished 342 | 3850000,5300,5,2,2,yes,no,no,no,no,0,no,semi-furnished 343 | 3850000,3180,2,2,1,yes,no,yes,no,no,2,no,semi-furnished 344 | 3850000,7152,3,1,2,yes,no,no,no,yes,0,no,furnished 345 | 3850000,4080,2,1,1,yes,no,no,no,no,0,no,semi-furnished 346 | 3850000,3850,2,1,1,yes,no,no,no,no,0,no,semi-furnished 347 | 3850000,2015,3,1,2,yes,no,yes,no,no,0,yes,semi-furnished 348 | 3850000,2176,2,1,2,yes,yes,no,no,no,0,yes,semi-furnished 349 | 3836000,3350,3,1,2,yes,no,no,no,no,0,no,unfurnished 350 | 3815000,3150,2,2,1,no,no,yes,no,no,0,no,semi-furnished 351 | 3780000,4820,3,1,2,yes,no,no,no,no,0,no,semi-furnished 352 | 3780000,3420,2,1,2,yes,no,no,yes,no,1,no,semi-furnished 353 | 3780000,3600,2,1,1,yes,no,no,no,no,0,no,semi-furnished 354 | 3780000,5830,2,1,1,yes,no,no,no,no,2,no,unfurnished 355 | 3780000,2856,3,1,3,yes,no,no,no,no,0,yes,furnished 356 | 3780000,8400,2,1,1,yes,no,no,no,no,1,no,furnished 357 | 3773000,8250,3,1,1,yes,no,no,no,no,2,no,furnished 358 | 3773000,2520,5,2,1,no,no,yes,no,yes,1,no,furnished 359 | 3773000,6930,4,1,2,no,no,no,no,no,1,no,furnished 360 | 3745000,3480,2,1,1,yes,no,no,no,no,0,yes,semi-furnished 361 | 3710000,3600,3,1,1,yes,no,no,no,no,1,no,unfurnished 362 | 3710000,4040,2,1,1,yes,no,no,no,no,0,no,semi-furnished 363 | 3710000,6020,3,1,1,yes,no,no,no,no,0,no,semi-furnished 364 | 3710000,4050,2,1,1,yes,no,no,no,no,0,no,furnished 365 | 3710000,3584,2,1,1,yes,no,no,yes,no,0,no,semi-furnished 366 | 3703000,3120,3,1,2,no,no,yes,yes,no,0,no,semi-furnished 367 | 3703000,5450,2,1,1,yes,no,no,no,no,0,no,furnished 368 | 3675000,3630,2,1,1,yes,no,yes,no,no,0,no,furnished 369 | 3675000,3630,2,1,1,yes,no,no,no,yes,0,no,unfurnished 370 | 3675000,5640,2,1,1,no,no,no,no,no,0,no,semi-furnished 371 | 3675000,3600,2,1,1,yes,no,no,no,no,0,no,furnished 372 | 3640000,4280,2,1,1,yes,no,no,no,yes,2,no,semi-furnished 373 | 3640000,3570,3,1,2,yes,no,yes,no,no,0,no,semi-furnished 374 | 3640000,3180,3,1,2,no,no,yes,no,no,0,no,semi-furnished 375 | 3640000,3000,2,1,2,yes,no,no,no,yes,0,no,furnished 376 | 3640000,3520,2,2,1,yes,no,yes,no,no,0,no,semi-furnished 377 | 3640000,5960,3,1,2,yes,yes,yes,no,no,0,no,unfurnished 378 | 3640000,4130,3,2,2,yes,no,no,no,no,2,no,semi-furnished 379 | 3640000,2850,3,2,2,no,no,yes,no,no,0,yes,unfurnished 380 | 3640000,2275,3,1,3,yes,no,no,yes,yes,0,yes,semi-furnished 381 | 3633000,3520,3,1,1,yes,no,no,no,no,2,yes,unfurnished 382 | 3605000,4500,2,1,1,yes,no,no,no,no,0,no,semi-furnished 383 | 3605000,4000,2,1,1,yes,no,no,no,no,0,yes,semi-furnished 384 | 3570000,3150,3,1,2,yes,no,yes,no,no,0,no,furnished 385 | 3570000,4500,4,2,2,yes,no,yes,no,no,2,no,furnished 386 | 3570000,4500,2,1,1,no,no,no,no,no,0,no,furnished 387 | 3570000,3640,2,1,1,yes,no,no,no,no,0,no,unfurnished 388 | 3535000,3850,3,1,1,yes,no,no,no,no,2,no,unfurnished 389 | 3500000,4240,3,1,2,yes,no,no,no,yes,0,no,semi-furnished 390 | 3500000,3650,3,1,2,yes,no,no,no,no,0,no,unfurnished 391 | 3500000,4600,4,1,2,yes,no,no,no,no,0,no,semi-furnished 392 | 3500000,2135,3,2,2,no,no,no,no,no,0,no,unfurnished 393 | 3500000,3036,3,1,2,yes,no,yes,no,no,0,no,semi-furnished 394 | 3500000,3990,3,1,2,yes,no,no,no,no,0,no,semi-furnished 395 | 3500000,7424,3,1,1,no,no,no,no,no,0,no,unfurnished 396 | 3500000,3480,3,1,1,no,no,no,no,yes,0,no,unfurnished 397 | 3500000,3600,6,1,2,yes,no,no,no,no,1,no,unfurnished 398 | 3500000,3640,2,1,1,yes,no,no,no,no,1,no,semi-furnished 399 | 3500000,5900,2,1,1,yes,no,no,no,no,1,no,furnished 400 | 3500000,3120,3,1,2,yes,no,no,no,no,1,no,unfurnished 401 | 3500000,7350,2,1,1,yes,no,no,no,no,1,no,semi-furnished 402 | 3500000,3512,2,1,1,yes,no,no,no,no,1,yes,unfurnished 403 | 3500000,9500,3,1,2,yes,no,no,no,no,3,yes,unfurnished 404 | 3500000,5880,2,1,1,yes,no,no,no,no,0,no,unfurnished 405 | 3500000,12944,3,1,1,yes,no,no,no,no,0,no,unfurnished 406 | 3493000,4900,3,1,2,no,no,no,no,no,0,no,unfurnished 407 | 3465000,3060,3,1,1,yes,no,no,no,no,0,no,unfurnished 408 | 3465000,5320,2,1,1,yes,no,no,no,no,1,yes,unfurnished 409 | 3465000,2145,3,1,3,yes,no,no,no,no,0,yes,furnished 410 | 3430000,4000,2,1,1,yes,no,no,no,no,0,no,unfurnished 411 | 3430000,3185,2,1,1,yes,no,no,no,no,2,no,unfurnished 412 | 3430000,3850,3,1,1,yes,no,no,no,no,0,no,unfurnished 413 | 3430000,2145,3,1,3,yes,no,no,no,no,0,yes,furnished 414 | 3430000,2610,3,1,2,yes,no,yes,no,no,0,yes,unfurnished 415 | 3430000,1950,3,2,2,yes,no,yes,no,no,0,yes,unfurnished 416 | 3423000,4040,2,1,1,yes,no,no,no,no,0,no,unfurnished 417 | 3395000,4785,3,1,2,yes,yes,yes,no,yes,1,no,furnished 418 | 3395000,3450,3,1,1,yes,no,yes,no,no,2,no,unfurnished 419 | 3395000,3640,2,1,1,yes,no,no,no,no,0,no,furnished 420 | 3360000,3500,4,1,2,yes,no,no,no,yes,2,no,unfurnished 421 | 3360000,4960,4,1,3,no,no,no,no,no,0,no,semi-furnished 422 | 3360000,4120,2,1,2,yes,no,no,no,no,0,no,unfurnished 423 | 3360000,4750,2,1,1,yes,no,no,no,no,0,no,unfurnished 424 | 3360000,3720,2,1,1,no,no,no,no,yes,0,no,unfurnished 425 | 3360000,3750,3,1,1,yes,no,no,no,no,0,no,unfurnished 426 | 3360000,3100,3,1,2,no,no,yes,no,no,0,no,semi-furnished 427 | 3360000,3185,2,1,1,yes,no,yes,no,no,2,no,furnished 428 | 3353000,2700,3,1,1,no,no,no,no,no,0,no,furnished 429 | 3332000,2145,3,1,2,yes,no,yes,no,no,0,yes,furnished 430 | 3325000,4040,2,1,1,yes,no,no,no,no,1,no,unfurnished 431 | 3325000,4775,4,1,2,yes,no,no,no,no,0,no,unfurnished 432 | 3290000,2500,2,1,1,no,no,no,no,yes,0,no,unfurnished 433 | 3290000,3180,4,1,2,yes,no,yes,no,yes,0,no,unfurnished 434 | 3290000,6060,3,1,1,yes,yes,yes,no,no,0,no,furnished 435 | 3290000,3480,4,1,2,no,no,no,no,no,1,no,semi-furnished 436 | 3290000,3792,4,1,2,yes,no,no,no,no,0,no,semi-furnished 437 | 3290000,4040,2,1,1,yes,no,no,no,no,0,no,unfurnished 438 | 3290000,2145,3,1,2,yes,no,yes,no,no,0,yes,furnished 439 | 3290000,5880,3,1,1,yes,no,no,no,no,1,no,unfurnished 440 | 3255000,4500,2,1,1,no,no,no,no,no,0,no,semi-furnished 441 | 3255000,3930,2,1,1,no,no,no,no,no,0,no,unfurnished 442 | 3234000,3640,4,1,2,yes,no,yes,no,no,0,no,unfurnished 443 | 3220000,4370,3,1,2,yes,no,no,no,no,0,no,unfurnished 444 | 3220000,2684,2,1,1,yes,no,no,no,yes,1,no,unfurnished 445 | 3220000,4320,3,1,1,no,no,no,no,no,1,no,unfurnished 446 | 3220000,3120,3,1,2,no,no,no,no,no,0,no,furnished 447 | 3150000,3450,1,1,1,yes,no,no,no,no,0,no,furnished 448 | 3150000,3986,2,2,1,no,yes,yes,no,no,1,no,unfurnished 449 | 3150000,3500,2,1,1,no,no,yes,no,no,0,no,semi-furnished 450 | 3150000,4095,2,1,1,yes,no,no,no,no,2,no,semi-furnished 451 | 3150000,1650,3,1,2,no,no,yes,no,no,0,no,unfurnished 452 | 3150000,3450,3,1,2,yes,no,yes,no,no,0,no,semi-furnished 453 | 3150000,6750,2,1,1,yes,no,no,no,no,0,no,semi-furnished 454 | 3150000,9000,3,1,2,yes,no,no,no,no,2,no,semi-furnished 455 | 3150000,3069,2,1,1,yes,no,no,no,no,1,no,unfurnished 456 | 3143000,4500,3,1,2,yes,no,no,no,yes,0,no,unfurnished 457 | 3129000,5495,3,1,1,yes,no,yes,no,no,0,no,unfurnished 458 | 3118850,2398,3,1,1,yes,no,no,no,no,0,yes,semi-furnished 459 | 3115000,3000,3,1,1,no,no,no,no,yes,0,no,unfurnished 460 | 3115000,3850,3,1,2,yes,no,no,no,no,0,no,unfurnished 461 | 3115000,3500,2,1,1,yes,no,no,no,no,0,no,unfurnished 462 | 3087000,8100,2,1,1,yes,no,no,no,no,1,no,unfurnished 463 | 3080000,4960,2,1,1,yes,no,yes,no,yes,0,no,unfurnished 464 | 3080000,2160,3,1,2,no,no,yes,no,no,0,no,semi-furnished 465 | 3080000,3090,2,1,1,yes,yes,yes,no,no,0,no,unfurnished 466 | 3080000,4500,2,1,2,yes,no,no,yes,no,1,no,semi-furnished 467 | 3045000,3800,2,1,1,yes,no,no,no,no,0,no,unfurnished 468 | 3010000,3090,3,1,2,no,no,no,no,no,0,no,semi-furnished 469 | 3010000,3240,3,1,2,yes,no,no,no,no,2,no,semi-furnished 470 | 3010000,2835,2,1,1,yes,no,no,no,no,0,no,semi-furnished 471 | 3010000,4600,2,1,1,yes,no,no,no,no,0,no,furnished 472 | 3010000,5076,3,1,1,no,no,no,no,no,0,no,unfurnished 473 | 3010000,3750,3,1,2,yes,no,no,no,no,0,no,unfurnished 474 | 3010000,3630,4,1,2,yes,no,no,no,no,3,no,semi-furnished 475 | 3003000,8050,2,1,1,yes,no,no,no,no,0,no,unfurnished 476 | 2975000,4352,4,1,2,no,no,no,no,no,1,no,unfurnished 477 | 2961000,3000,2,1,2,yes,no,no,no,no,0,no,semi-furnished 478 | 2940000,5850,3,1,2,yes,no,yes,no,no,1,no,unfurnished 479 | 2940000,4960,2,1,1,yes,no,no,no,no,0,no,unfurnished 480 | 2940000,3600,3,1,2,no,no,no,no,no,1,no,unfurnished 481 | 2940000,3660,4,1,2,no,no,no,no,no,0,no,unfurnished 482 | 2940000,3480,3,1,2,no,no,no,no,no,1,no,semi-furnished 483 | 2940000,2700,2,1,1,no,no,no,no,no,0,no,furnished 484 | 2940000,3150,3,1,2,no,no,no,no,no,0,no,unfurnished 485 | 2940000,6615,3,1,2,yes,no,no,no,no,0,no,semi-furnished 486 | 2870000,3040,2,1,1,no,no,no,no,no,0,no,unfurnished 487 | 2870000,3630,2,1,1,yes,no,no,no,no,0,no,unfurnished 488 | 2870000,6000,2,1,1,yes,no,no,no,no,0,no,semi-furnished 489 | 2870000,5400,4,1,2,yes,no,no,no,no,0,no,unfurnished 490 | 2852500,5200,4,1,3,yes,no,no,no,no,0,no,unfurnished 491 | 2835000,3300,3,1,2,no,no,no,no,no,1,no,semi-furnished 492 | 2835000,4350,3,1,2,no,no,no,yes,no,1,no,unfurnished 493 | 2835000,2640,2,1,1,no,no,no,no,no,1,no,furnished 494 | 2800000,2650,3,1,2,yes,no,yes,no,no,1,no,unfurnished 495 | 2800000,3960,3,1,1,yes,no,no,no,no,0,no,furnished 496 | 2730000,6800,2,1,1,yes,no,no,no,no,0,no,unfurnished 497 | 2730000,4000,3,1,2,yes,no,no,no,no,1,no,unfurnished 498 | 2695000,4000,2,1,1,yes,no,no,no,no,0,no,unfurnished 499 | 2660000,3934,2,1,1,yes,no,no,no,no,0,no,unfurnished 500 | 2660000,2000,2,1,2,yes,no,no,no,no,0,no,semi-furnished 501 | 2660000,3630,3,3,2,no,yes,no,no,no,0,no,unfurnished 502 | 2660000,2800,3,1,1,yes,no,no,no,no,0,no,unfurnished 503 | 2660000,2430,3,1,1,no,no,no,no,no,0,no,unfurnished 504 | 2660000,3480,2,1,1,yes,no,no,no,no,1,no,semi-furnished 505 | 2660000,4000,3,1,1,yes,no,no,no,no,0,no,semi-furnished 506 | 2653000,3185,2,1,1,yes,no,no,no,yes,0,no,unfurnished 507 | 2653000,4000,3,1,2,yes,no,no,no,yes,0,no,unfurnished 508 | 2604000,2910,2,1,1,no,no,no,no,no,0,no,unfurnished 509 | 2590000,3600,2,1,1,yes,no,no,no,no,0,no,unfurnished 510 | 2590000,4400,2,1,1,yes,no,no,no,no,0,no,unfurnished 511 | 2590000,3600,2,2,2,yes,no,yes,no,no,1,no,furnished 512 | 2520000,2880,3,1,1,no,no,no,no,no,0,no,unfurnished 513 | 2520000,3180,3,1,1,no,no,no,no,no,0,no,unfurnished 514 | 2520000,3000,2,1,2,yes,no,no,no,no,0,no,furnished 515 | 2485000,4400,3,1,2,yes,no,no,no,no,0,no,unfurnished 516 | 2485000,3000,3,1,2,no,no,no,no,no,0,no,semi-furnished 517 | 2450000,3210,3,1,2,yes,no,yes,no,no,0,no,unfurnished 518 | 2450000,3240,2,1,1,no,yes,no,no,no,1,no,unfurnished 519 | 2450000,3000,2,1,1,yes,no,no,no,no,1,no,unfurnished 520 | 2450000,3500,2,1,1,yes,yes,no,no,no,0,no,unfurnished 521 | 2450000,4840,2,1,2,yes,no,no,no,no,0,no,unfurnished 522 | 2450000,7700,2,1,1,yes,no,no,no,no,0,no,unfurnished 523 | 2408000,3635,2,1,1,no,no,no,no,no,0,no,unfurnished 524 | 2380000,2475,3,1,2,yes,no,no,no,no,0,no,furnished 525 | 2380000,2787,4,2,2,yes,no,no,no,no,0,no,furnished 526 | 2380000,3264,2,1,1,yes,no,no,no,no,0,no,unfurnished 527 | 2345000,3640,2,1,1,yes,no,no,no,no,0,no,unfurnished 528 | 2310000,3180,2,1,1,yes,no,no,no,no,0,no,unfurnished 529 | 2275000,1836,2,1,1,no,no,yes,no,no,0,no,semi-furnished 530 | 2275000,3970,1,1,1,no,no,no,no,no,0,no,unfurnished 531 | 2275000,3970,3,1,2,yes,no,yes,no,no,0,no,unfurnished 532 | 2240000,1950,3,1,1,no,no,no,yes,no,0,no,unfurnished 533 | 2233000,5300,3,1,1,no,no,no,no,yes,0,yes,unfurnished 534 | 2135000,3000,2,1,1,no,no,no,no,no,0,no,unfurnished 535 | 2100000,2400,3,1,2,yes,no,no,no,no,0,no,unfurnished 536 | 2100000,3000,4,1,2,yes,no,no,no,no,0,no,unfurnished 537 | 2100000,3360,2,1,1,yes,no,no,no,no,1,no,unfurnished 538 | 1960000,3420,5,1,2,no,no,no,no,no,0,no,unfurnished 539 | 1890000,1700,3,1,2,yes,no,no,no,no,0,no,unfurnished 540 | 1890000,3649,2,1,1,yes,no,no,no,no,0,no,unfurnished 541 | 1855000,2990,2,1,1,no,no,no,no,no,1,no,unfurnished 542 | 1820000,3000,2,1,1,yes,no,yes,no,no,2,no,unfurnished 543 | 1767150,2400,3,1,1,no,no,no,no,no,0,no,semi-furnished 544 | 1750000,3620,2,1,1,yes,no,no,no,no,0,no,unfurnished 545 | 1750000,2910,3,1,1,no,no,no,no,no,0,no,furnished 546 | 1750000,3850,3,1,2,yes,no,no,no,no,0,no,unfurnished 547 | -------------------------------------------------------------------------------- /tutorial/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/image.png -------------------------------------------------------------------------------- /tutorial/images/command_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/command_mode.png -------------------------------------------------------------------------------- /tutorial/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/help.png -------------------------------------------------------------------------------- /tutorial/images/main_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/main_page.png -------------------------------------------------------------------------------- /tutorial/images/new_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/new_tab.png -------------------------------------------------------------------------------- /tutorial/images/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/schema.png -------------------------------------------------------------------------------- /tutorial/images/sheet_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/tabler/46836dd5a481765fd856b1960e31d16fb0ab35bb/tutorial/images/sheet_view.png -------------------------------------------------------------------------------- /tutorial/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tabler Tutorial 2 | 3 | Welcome to the Tabler tutorial—a guide to using this terminal-based tool for viewing and querying tabular files directly from the command line. Tabler enables efficient data manipulation and analysis across multiple formats, making it a valuable asset for professionals and enthusiasts in data-centric roles. This guide covers the core features and functionality to help you get the most out of Tabler. 4 | 5 | This tutorial uses example CSV files located in the same directory as `tutorial.md`: 6 | 7 | 1. **housing.csv**: Housing price dataset. 8 | 2. **user.csv**: Dataset of users with properties generated by a Language Learning Model (LLM). 9 | 3. **company.csv**: Dataset of companies with properties generated by an LLM. 10 | 4. **employment.csv**: Relationship table between users and companies, showing employment history. 11 | 12 | It's helpful to review these files to understand their structure and content. 13 | 14 | ## Opening Files 15 | 16 | To open a file with Tabler, use: 17 | 18 | ```shell 19 | tabler 20 | ``` 21 | 22 | Replace `` with the name of your file. For instance, to load the housing data: 23 | 24 | ```shell 25 | tabler housing.csv 26 | ``` 27 | 28 | This command opens `housing.csv` and displays it in Tabler, with a status bar showing table properties such as name and dimensions. 29 | 30 | ### Supported Formats 31 | 32 | Tabler supports various file types, including CSV, Parquet, JSON, JSON-Line, Arrow, and Fixed-Width Format (FWF). For example, to open a Parquet file: 33 | 34 | ```shell 35 | tabler sample.parquet -f parquet 36 | ``` 37 | 38 | ## Command Mode 39 | 40 | Activate Command Mode by pressing `:`. Type a command and press `Enter` to execute or `Esc` to cancel. Commands appear at the bottom of the screen, replacing the status bar. 41 | 42 | ## Table Navigation 43 | 44 | Navigate through data with these key mappings: 45 | 46 | | Key | Action | 47 | | ---------------------- | --------------------- | 48 | | `k` / `Arrow Up` | Move up one row | 49 | | `j` / `Arrow Down` | Move down one row | 50 | | `Page Up` / `Ctrl+b` | Move up a page | 51 | | `Page Down` / `Ctrl+f` | Move down a page | 52 | | `Ctrl+u` | Move up half a page | 53 | | `Ctrl+d` | Move down half a page | 54 | | `Home` / `g` | Jump to first row | 55 | | `End` / `G` | Jump to last row | 56 | 57 | To jump to a specific row, use the `:goto` command. 58 | 59 | ## Sheet View 60 | 61 | In default table view, all rows display in a single line. For better readability, switch to **Sheet View** by pressing `v`. This mode shows each row individually and allows scrolling when rows exceed screen size. 62 | 63 | To return to table view, press `q` or `v`. 64 | 65 | ## Multi-File Opening 66 | 67 | To open multiple files at once, list them after the `tabler` command: 68 | 69 | ```shell 70 | tabler housing.csv user.csv 71 | ``` 72 | 73 | This command opens each file in a separate tab. You can switch between tabs with `H` (previous tab) and `L` (next tab). 74 | 75 | Use wildcards to open all files of a specific format in the directory: 76 | 77 | ```shell 78 | tabler *.csv 79 | ``` 80 | 81 | ## SQL Queries 82 | 83 | Tabler supports SQL for powerful data manipulation. To run a query, use `:Q` or `:query`: 84 | 85 | ```sql 86 | :Q SELECT * FROM housing WHERE price > 500000 AND mainroad = 'yes' AND bedrooms >= 4 87 | ``` 88 | 89 | To view results in a new tab, use `:tabn`: 90 | 91 | ```sql 92 | :tabn SELECT * FROM user WHERE marriage='Married' AND balance < 10000 93 | ``` 94 | 95 | The table name corresponds to the file name (e.g., `housing.csv` becomes `housing`). If duplicate file names exist, suffixes (`_2`, `_3`, etc.) are added. 96 | 97 | ## Inline Queries 98 | 99 | Inline queries enable quick manipulations on the active table. Commands include: 100 | 101 | | Command | Example | Description | 102 | | ---------------- | ----------------------------------- | ------------------------------- | 103 | | `:S` / `:select` | `:S price, area, bedrooms, parking` | Select specific columns | 104 | | `:F` / `:filter` | `:F price < 20000 AND bedrooms > 4` | Filter rows based on conditions | 105 | | `:O` / `:order` | `:O area` | Sort by a specific column | 106 | 107 | Chained commands on a table (e.g., `housing`) mirror a SQL equivalent. 108 | 109 | ## Exporting 110 | 111 | Export data in various formats (CSV, TSV, Parquet, JSON, JSONL, Arrow) with: 112 | 113 | ```sql 114 | :export 115 | ``` 116 | 117 | ## Help Command 118 | 119 | Use `:help` to view all commands and examples. 120 | 121 | ![image not found](images/help.png) 122 | --------------------------------------------------------------------------------