mirror of https://github.com/aristocratos/bashtop
feat: Add Bash TUI Data Viewer
Implements a Text-based User Interface (TUI) script in Bash to display CSV, TSV, and JSON data. Features: - Parses and displays CSV, TSV (tab-delimited), and common JSON (array of objects, single object) structures. - Configurable refresh interval via command-line argument. - Handles basic errors such as file not found, unknown file type, and missing 'jq' dependency for JSON. - Simple TUI with a box drawn around the data. - Basic input: 'q' to quit, 'r' to re-parse and refresh data. - Includes a README.md with usage instructions and details. The TUI is styled after tools like bashtop, using ANSI escape codes for screen manipulation and display.pull/245/head
parent
60f95a1a74
commit
172e9a4d5c
489
README.md
489
README.md
|
@ -1,417 +1,96 @@
|
|||
# 
|
||||
# Bash Data Viewer
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://travis-ci.com/aristocratos/bashtop)
|
||||
[](https://paypal.me/aristocratos)
|
||||
[](https://github.com/sponsors/aristocratos)
|
||||
[](https://ko-fi.com/aristocratos)
|
||||
|
||||
### C++ Version
|
||||
|
||||
##### 18 September 2021
|
||||
|
||||

|
||||
|
||||
The C++ version of bashtop - btop++ is available.
|
||||
|
||||
Get it at https://github.com/aristocratos/btop
|
||||
|
||||
#
|
||||
|
||||
## Index
|
||||
|
||||
* [Documents](#documents)
|
||||
* [Description](#description)
|
||||
* [Features](#features)
|
||||
* [Themes](#themes)
|
||||
* [Support and funding](#support-and-funding)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Screenshots](#screenshots)
|
||||
* [Installation](#installation)
|
||||
* [Configurability](#configurability)
|
||||
* [TODO](#todo)
|
||||
* [License](#license)
|
||||
|
||||
|
||||
## Documents
|
||||
|
||||
#### [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
#### [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
#### [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
||||
|
||||
## Description
|
||||
|
||||
Resource monitor that shows usage and stats for processor, memory, disks, network and processes.
|
||||
A TUI (Text-based User Interface) tool, written purely in Bash, for displaying CSV, TSV, or JSON data. The display refreshes at a configurable interval, inspired by tools like bashtop.
|
||||
|
||||
## Features
|
||||
|
||||
* Easy to use, with a game inspired menu system.
|
||||
* Fast and "mostly" responsive UI with UP, DOWN keys process selection.
|
||||
* Function for showing detailed stats for selected process.
|
||||
* Ability to filter processes.
|
||||
* Easy switching between sorting options.
|
||||
* Send SIGTERM, SIGKILL, SIGINT to selected process.
|
||||
* UI menu for changing all config file options.
|
||||
* Auto scaling graph for network usage.
|
||||
* Shows message in menu if new version is available
|
||||
* Shows current read and write speeds for disks
|
||||
* Multiple data collection methods which can be switched if running on Linux
|
||||
|
||||
## Themes
|
||||
|
||||
Bashtop now has theme support and a function to download missing local themes from repository.
|
||||
|
||||
See [themes](themes) folder for available themes.
|
||||
|
||||
The builtin theme downloader places the default themes in `$HOME/.config/bashtop/themes`.
|
||||
User created themes should be placed in `$HOME/.config/bashtop/user_themes` to be safe from overwrites.
|
||||
|
||||
Let me know if you want to contribute with new themes.
|
||||
|
||||
## Support and funding
|
||||
|
||||
Bug fixes and updates might be slow during normal workdays since I work full time as an industrial worker and don't have much time or energy left during the week.
|
||||
I'm looking into ways of funding this project that would allow me to take off time from my day job to work on this.
|
||||
|
||||
Any advice on how to get funding for open source projects is very welcome!
|
||||
|
||||
#### Update
|
||||
|
||||
You can now sponsor this project through github, see [my sponsors page](https://github.com/sponsors/aristocratos) for options.
|
||||
|
||||
Also added donation links for [paypal](https://paypal.me/aristocratos) and [ko-fi](https://ko-fi.com/aristocratos).
|
||||
|
||||
Any support is greatly appreciated!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
#### Mac Os X
|
||||
|
||||
Will not display correctly in the standard terminal!
|
||||
Recommended alternative [iTerm2](https://www.iterm2.com/)
|
||||
|
||||
Will also need to be run as superuser to display stats for processes not owned by user.
|
||||
|
||||
#### Linux, Mac Os X and FreeBSD
|
||||
|
||||
For correct display, a terminal with support for:
|
||||
|
||||
* 24-bit truecolor ([See list of terminals with truecolor support](https://gist.github.com/XVilka/8346728))
|
||||
* Wide characters (Are sometimes problematic in web-based terminals)
|
||||
|
||||
Also needs a UTF8 locale and a font that covers:
|
||||
|
||||
* Unicode Block “Braille Patterns” U+2800 - U+28FF
|
||||
* Unicode Block “Geometric Shapes” U+25A0 - U+25FF
|
||||
* Unicode Block "Box Drawing" and "Block Elements" U+2500 - U+259F
|
||||
|
||||
#### Notice
|
||||
|
||||
Dropbear seems to not be able to set correct locale. So if accessing bashtop over ssh, OpenSSH is recommended.
|
||||
* Displays CSV, TSV, and JSON data in a tabular format.
|
||||
* Configurable refresh interval for the data display.
|
||||
* Basic TUI for easy viewing in the terminal.
|
||||
* Error handling for file issues and unsupported formats.
|
||||
|
||||
## Dependencies
|
||||
|
||||
## Linux, OSX and FreeBSD
|
||||
* **Bash**: Version 4.4 or later.
|
||||
* **jq**: Required for parsing JSON files. You can usually install it via your system's package manager (e.g., `sudo apt install jq`, `sudo yum install jq`, `brew install jq`).
|
||||
|
||||
**[bash](https://www.gnu.org/software/bash/)** (v4.4 or later) Script functionality will most probably break with earlier versions.
|
||||
Bash version 5 is highly recommended to make use of $EPOCHREALTIME variable instead of a lot of external date command calls.
|
||||
## Usage
|
||||
|
||||
**[GNU coreutils](https://www.gnu.org/software/coreutils/)**
|
||||
To use the Bash Data Viewer, run the script from your terminal:
|
||||
|
||||
**[GNU sed](https://www.gnu.org/software/sed/)**
|
||||
|
||||
## Linux using /proc for data collection
|
||||
|
||||
**[GNU grep](https://www.gnu.org/software/grep/)**
|
||||
|
||||
**[ps from procps-ng](https://gitlab.com/procps-ng/procps)** (v3.1.15 or later)
|
||||
|
||||
**[GNU awk](https://www.gnu.org/software/gawk/)**
|
||||
|
||||
## OSX and FreeBSD or Linux using psutil for data collection
|
||||
|
||||
**[Python3](https://www.python.org/downloads/)** (v3.6 or later)
|
||||
|
||||
**[psutil python module](https://github.com/giampaolo/psutil)** (v5.7.0 or later)
|
||||
|
||||
## Optionals for additional stats
|
||||
|
||||
(Optional OSX) **[osx-cpu-temp](https://github.com/lavoiesl/osx-cpu-temp)** Needed to show CPU temperatures.
|
||||
|
||||
(Optional Linux) **[lm-sensors](https://github.com/lm-sensors/lm-sensors)** Needed to show CPU temperatures.
|
||||
|
||||
(Optional Linux) **[iostat (part of sysstat)](https://github.com/sysstat/sysstat)** Needed if you want disk read/write stats and are not using psutil data collection.
|
||||
|
||||
(Optional OSX/Linux/FreeBSD) **[curl](https://curl.haxx.se/download.html)** (v7.16.2 or later) Needed if you want messages about updates and the ability to download themes.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Main UI showing details for a selected process.
|
||||

|
||||
|
||||
Main menu.
|
||||

|
||||
|
||||
Options menu.
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
#### Dependencies installation OSX
|
||||
|
||||
>Install homebrew if not already installed
|
||||
|
||||
``` bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
```shell
|
||||
./dataviewer.sh [path_to_data_file] [refresh_interval_seconds]
|
||||
```
|
||||
|
||||
|
||||
|
||||
>If you got python 3.6 or later installed outside of brew:
|
||||
|
||||
``` bash
|
||||
sudo python3 -m ensurepip
|
||||
sudo python3 -m pip install psutil
|
||||
```
|
||||
|
||||
>If you haven't got python3 installed:
|
||||
|
||||
```
|
||||
brew install python3
|
||||
python3 -m pip install psutil
|
||||
```
|
||||
|
||||
>Install dependencies
|
||||
|
||||
|
||||
``` bash
|
||||
brew install bash coreutils gnu-sed git
|
||||
```
|
||||
|
||||
>Install optional dependency osx-cpu-temp
|
||||
|
||||
``` bash
|
||||
brew install osx-cpu-temp
|
||||
```
|
||||
|
||||
#### Dependencies installation FreeBSD
|
||||
|
||||
>Install with pkg and pip
|
||||
|
||||
``` bash
|
||||
sudo pkg install coreutils gsed git py37-psutil
|
||||
```
|
||||
|
||||
#### Manual installation Linux, OSX and FreeBSD
|
||||
|
||||
>Clone and install
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/aristocratos/bashtop.git
|
||||
cd bashtop
|
||||
sudo make install
|
||||
```
|
||||
|
||||
>to uninstall it
|
||||
|
||||
``` bash
|
||||
sudo make uninstall
|
||||
```
|
||||
|
||||
#### FreeBSD package
|
||||
|
||||
Available in [FreeBSD ports](https://www.freshports.org/sysutils/bashtop/)
|
||||
|
||||
Install pre-built pacakge
|
||||
|
||||
``` bash
|
||||
sudo pkg install bashtop
|
||||
```
|
||||
|
||||
#### Arch based
|
||||
|
||||
Available in the AUR as [bashtop-git](https://aur.archlinux.org/packages/bashtop-git/)
|
||||
|
||||
Available in the Arch Linux repository as [bashtop](https://www.archlinux.org/packages/community/any/bashtop/)
|
||||
|
||||
#### Debian based
|
||||
|
||||
Available in [official Debian repository](https://tracker.debian.org/pkg/bashtop) since Debian 11
|
||||
|
||||
Available for debian/ubuntu from [Azlux's repository](http://packages.azlux.fr/)
|
||||
|
||||
Or use quick installation:
|
||||
|
||||
>Quick install go to DEB folder and type
|
||||
|
||||
``` bash
|
||||
sudo ./build
|
||||
```
|
||||
|
||||
>to uninstall it go to DEB folder and type
|
||||
|
||||
``` bash
|
||||
sudo ./build --remove
|
||||
```
|
||||
|
||||
#### Guix based
|
||||
|
||||
Available in [official Guix repository](https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages/admin.scm) since 6bbd0fd2
|
||||
|
||||
>Installation
|
||||
|
||||
``` bash
|
||||
guix install bashtop
|
||||
```
|
||||
|
||||
#### Ubuntu based
|
||||
|
||||
Available in [official Ubuntu repository](https://launchpad.net/ubuntu/+source/bashtop) since Ubuntu 20.10
|
||||
|
||||
Available for Ubuntu from [PPA repository](https://code.launchpad.net/~bashtop-monitor/+archive/ubuntu/bashtop)
|
||||
|
||||
>Add PPA repository and install bashtop
|
||||
|
||||
``` bash
|
||||
sudo add-apt-repository ppa:bashtop-monitor/bashtop
|
||||
sudo apt update
|
||||
sudo apt install bashtop
|
||||
```
|
||||
|
||||
#### Fedora
|
||||
|
||||
Available in the Fedora repository.
|
||||
|
||||
>Installation
|
||||
|
||||
``` bash
|
||||
sudo dnf install bashtop
|
||||
```
|
||||
|
||||
#### CentOS 8
|
||||
|
||||
>Installation
|
||||
|
||||
``` bash
|
||||
dnf config-manager --set-enabled PowerTools
|
||||
dnf install epel-release
|
||||
dnf install bashtop
|
||||
```
|
||||
|
||||
#### RHEL 8
|
||||
|
||||
>Installation
|
||||
|
||||
``` bash
|
||||
ARCH=$( /bin/arch )
|
||||
subscription-manager repos --enable
|
||||
"codeready-builder-for-rhel-8-${ARCH}-rpms"
|
||||
dnf install epel-release
|
||||
dnf install bashtop
|
||||
```
|
||||
|
||||
## Configurability
|
||||
|
||||
All options changeable from within UI.
|
||||
Config files stored in "$HOME/.config/bashtop" folder
|
||||
|
||||
#### bashtop.cfg: (auto generated if not found)
|
||||
|
||||
```bash
|
||||
#? Config file for bashtop v. 0.9.21
|
||||
|
||||
#* Color theme, looks for a .theme file in "$HOME/.config/bashtop/themes" and "$HOME/.config/bashtop/user_themes"
|
||||
#* Should be prefixed with either "themes/" or "user_themes/" depending on location, "Default" for builtin default theme
|
||||
color_theme="Default"
|
||||
|
||||
#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs
|
||||
update_ms="2500"
|
||||
|
||||
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive"
|
||||
#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly
|
||||
proc_sorting="cpu lazy"
|
||||
|
||||
#* Reverse sorting order, "true" or "false"
|
||||
proc_reversed="false"
|
||||
|
||||
#* Show processes as a tree
|
||||
proc_tree="false"
|
||||
|
||||
#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available
|
||||
check_temp="true"
|
||||
|
||||
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable
|
||||
draw_clock="%X"
|
||||
|
||||
#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort
|
||||
background_update="true"
|
||||
|
||||
#* Custom cpu model name, empty string to disable
|
||||
custom_cpu_name=""
|
||||
|
||||
#* Enable error logging to "$HOME/.config/bashtop/error.log", "true" or "false"
|
||||
error_logging="true"
|
||||
|
||||
#* Show color gradient in process list, "true" or "false"
|
||||
proc_gradient="true"
|
||||
|
||||
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power
|
||||
proc_per_core="false"
|
||||
|
||||
#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space
|
||||
disks_filter=""
|
||||
|
||||
#* Enable check for new version from github.com/aristocratos/bashtop at start
|
||||
update_check="true"
|
||||
|
||||
#* Enable graphs with double the horizontal resolution, increases cpu usage
|
||||
hires_graphs="false"
|
||||
|
||||
#* Enable the use of psutil python3 module for data collection, default on OSX
|
||||
use_psutil="true"
|
||||
```
|
||||
|
||||
#### Command line options: (not yet implemented)
|
||||
|
||||
``` bash
|
||||
USAGE: bashtop
|
||||
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
Might finish off items out of order since I usually work on multiple at a time.
|
||||
|
||||
- [x] Add options to change colors for text, graphs and meters.
|
||||
- [x] Fix cross platform compatibility for Mac OSX and *BSD: Working on OSX, and FreeBSD.
|
||||
- [x] Add support for showing AMD cpu temperatures.
|
||||
- [x] Add option to show tree view of processes.
|
||||
- [x] Add option to reset network download/upload totals.
|
||||
- [x] Add option to turn of gradient in processes list.
|
||||
- [ ] Add gpu temp and usage. (If feasible)
|
||||
- [x] Add io stats for disks.
|
||||
- [ ] Add cpu and mem stats for docker containers. (If feasible)
|
||||
- [x] Change process list to line scroll instead of page change.
|
||||
- [ ] Add optional window for tailing log files.
|
||||
- [ ] Add options for resizing all boxes.
|
||||
- [ ] Add command line argument parsing.
|
||||
- [ ] Builtin updater. Relevant PR #96 by Jukoo
|
||||
- [ ] Add support for zram in memory box. Relevant PR #122 by perkinslr
|
||||
|
||||
- [ ] Miscellaneous optimizations and code cleanup.
|
||||
- [ ] Add more commenting where it's sparse.
|
||||
|
||||
- [ ] Python port. (Porting started)
|
||||
|
||||
## LICENSE
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
**Arguments:**
|
||||
|
||||
* `path_to_data_file` (optional): The path to the CSV, TSV, or JSON file you want to display.
|
||||
* If not provided, the TUI will launch and show "No input file specified."
|
||||
* `refresh_interval_seconds` (optional): The time in seconds between display refreshes.
|
||||
* Defaults to 5 seconds if not provided.
|
||||
* Must be a positive integer.
|
||||
|
||||
**Examples:**
|
||||
|
||||
* Display a CSV file with a 2-second refresh interval:
|
||||
```shell
|
||||
./dataviewer.sh data.csv 2
|
||||
```
|
||||
* Display a JSON file with the default 5-second refresh interval:
|
||||
```shell
|
||||
./dataviewer.sh logs.json
|
||||
```
|
||||
* Launch the TUI without an initial file (it will show an appropriate message):
|
||||
```shell
|
||||
./dataviewer.sh
|
||||
```
|
||||
|
||||
## Supported Data Formats
|
||||
|
||||
### CSV (Comma-Separated Values)
|
||||
* File extension must be `.csv`.
|
||||
* The first line is treated as the header row.
|
||||
* Uses comma (`,`) as the delimiter.
|
||||
|
||||
### TSV (Tab-Separated Values)
|
||||
* File extension must be `.tsv`.
|
||||
* The first line is treated as the header row.
|
||||
* Uses tab (`\t`) as the delimiter.
|
||||
|
||||
### JSON (JavaScript Object Notation)
|
||||
* File extension must be `.json`.
|
||||
* Requires `jq` to be installed.
|
||||
* **Supported Structures:**
|
||||
1. **Array of Objects**:
|
||||
* Example: `[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 24}]`
|
||||
* The keys of the first object in the array are used as headers.
|
||||
* Each object in the array is displayed as a row.
|
||||
2. **Single Object**:
|
||||
* Example: `{"hostname": "server1", "ip": "192.168.1.10", "status": "running"}`
|
||||
* The keys of the object are used as headers.
|
||||
* The object's values are displayed as a single data row.
|
||||
3. **Empty Array**:
|
||||
* Example: `[]`
|
||||
* Displays an info message: "Info: JSON file is an empty array []".
|
||||
* Other JSON structures (e.g., simple arrays of strings/numbers, nested complex objects without a clear tabular mapping) might show an "Unsupported JSON structure" error or may not display as intended.
|
||||
|
||||
## Controls
|
||||
|
||||
* **`q`**: Quit the application.
|
||||
* **`r`**: Manually re-parse the input file and refresh the display. This is useful if the data file has been updated.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The TUI will display messages for common errors, such as:
|
||||
* File not found.
|
||||
* Unknown file type.
|
||||
* `jq` not installed (for JSON files).
|
||||
* Empty or invalid JSON files.
|
||||
* Unsupported JSON structures.
|
||||
|
||||
## TODO / Potential Future Enhancements
|
||||
* More sophisticated column width calculation (e.g., based on content).
|
||||
* Scrolling (up/down, left/right) for larger datasets.
|
||||
* Configuration file for persistent settings.
|
||||
* Theming support.
|
||||
* Search/filter data within the TUI.
|
||||
|
|
|
@ -0,0 +1,411 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Ensure Bash 4.4 or later
|
||||
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]] || [[ "${BASH_VERSINFO[0]}" == 4 && "${BASH_VERSINFO[1]}" -lt 4 ]]; then
|
||||
echo "ERROR: Bash 4.4 or later is required (you are using Bash ${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set script to exit on error
|
||||
set -e
|
||||
|
||||
# --- Drawing Functions ---
|
||||
# Usage: draw_box <line> <col> <height> <width> [title]
|
||||
draw_box() {
|
||||
local line="$1" col="$2" height="$3" width="$4" title="$5"
|
||||
local i
|
||||
|
||||
# Draw top border
|
||||
echo -en "\033[${line};${col}H┌"
|
||||
for ((i=0; i<width-2; i++)); do echo -n "─"; done
|
||||
echo -n "┐"
|
||||
|
||||
# Draw side borders
|
||||
for ((i=1; i<height-1; i++)); do
|
||||
echo -en "\033[$((line+i));${col}H│"
|
||||
echo -en "\033[$((line+i));$((col+width-1))H│"
|
||||
done
|
||||
|
||||
# Draw bottom border
|
||||
echo -en "\033[$((line+height-1));${col}H└"
|
||||
for ((i=0; i<width-2; i++)); do echo -n "─"; done
|
||||
echo -n "┘"
|
||||
|
||||
# Draw title
|
||||
if [[ -n "$title" ]]; then
|
||||
echo -en "\033[${line};$((col + 2))H┤ ${title} ├"
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage: display_data <start_line> <start_col> <max_width> <max_height>
|
||||
display_data() {
|
||||
local start_line="$1" start_col="$2" max_width="$3" max_height="$4"
|
||||
local i row_idx col_idx
|
||||
|
||||
# Clear previous content within the data area of the box
|
||||
for ((i=0; i<max_height-2; i++)); do
|
||||
echo -en "\033[$((start_line+1+i));$((start_col+1))H"
|
||||
printf "%*s" "$((max_width-2))" ""
|
||||
done
|
||||
|
||||
# Check if header_row itself contains an error message or info
|
||||
if [[ "${header_row[0]}" == "Error:"* || "${header_row[0]}" == "Info:"* || "${header_row[0]}" == "No input file specified."* ]]; then
|
||||
echo -en "\033[$((start_line + 1));$((start_col + 1))H${header_row[0]:0:$((max_width-2))}"
|
||||
# If there's a second line to the error/info message in header_row
|
||||
if [[ -n "${header_row[1]}" ]]; then
|
||||
echo -en "\033[$((start_line + 2));$((start_col + 1))H${header_row[1]:0:$((max_width-2))}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
# Display Header
|
||||
if ((${#header_row[@]} > 0)); then
|
||||
echo -en "\033[$((start_line + 1));$((start_col + 1))H"
|
||||
local header_line_to_print=""
|
||||
for cell in "${header_row[@]}"; do
|
||||
local col_width=$(( (max_width - 2) / ${#header_row[@]} ))
|
||||
if ((col_width < 1)); then col_width=1; fi
|
||||
local truncated_cell
|
||||
if ((${#cell} > col_width -1 )); then
|
||||
truncated_cell="${cell:0:$((col_width-2))}…"
|
||||
else
|
||||
truncated_cell="$cell"
|
||||
fi
|
||||
header_line_to_print+=$(printf "\033[1m%-*s\033[0m" "$col_width" "$truncated_cell")
|
||||
done
|
||||
echo -n "$header_line_to_print"
|
||||
row_idx=1
|
||||
else
|
||||
row_idx=0
|
||||
fi
|
||||
|
||||
# Display Data Rows
|
||||
local data_display_count=0
|
||||
for row_nameref in "${parsed_data_rows[@]}"; do
|
||||
if ((row_idx >= max_height - 2)); then break; fi
|
||||
|
||||
declare -n current_row="$row_nameref"
|
||||
|
||||
echo -en "\033[$((start_line + 1 + row_idx));$((start_col + 1))H"
|
||||
|
||||
local line_to_print=""
|
||||
col_idx=0
|
||||
if ((${#current_row[@]} > 0)); then
|
||||
for cell in "${current_row[@]}"; do
|
||||
# Use header_row for column count if available, otherwise current_row
|
||||
local num_cols_for_width=${#header_row[@]}
|
||||
if ((num_cols_for_width == 0)); then num_cols_for_width=${#current_row[@]}; fi
|
||||
if ((num_cols_for_width == 0)); then num_cols_for_width=1; fi # Avoid division by zero
|
||||
|
||||
local col_width=$(( (max_width - 2) / num_cols_for_width ))
|
||||
if ((col_width < 1)); then col_width=1; fi
|
||||
local truncated_cell
|
||||
|
||||
if ((${#cell} > col_width -1 )); then
|
||||
truncated_cell="${cell:0:$((col_width-2))}…"
|
||||
else
|
||||
truncated_cell="$cell"
|
||||
fi
|
||||
line_to_print+=$(printf "%-*s" "$col_width" "$truncated_cell")
|
||||
((col_idx++))
|
||||
done
|
||||
echo -n "$line_to_print"
|
||||
fi
|
||||
|
||||
((row_idx++))
|
||||
((data_display_count++))
|
||||
done
|
||||
|
||||
if [[ ${#header_row[@]} -eq 0 && $data_display_count -eq 0 ]]; then
|
||||
# This case should be largely covered by the initial check of header_row for errors
|
||||
# but kept as a fallback.
|
||||
local error_msg="No data to display."
|
||||
if [[ -n "$INPUT_FILE_PATH" && ! -f "$INPUT_FILE_PATH" ]]; then # Check again, in case it was deleted
|
||||
error_msg="Error: File not found ($INPUT_FILE_PATH)"
|
||||
elif [[ -n "$INPUT_FILE_PATH" ]]; then
|
||||
error_msg="Error: Could not parse or file empty ($INPUT_FILE_PATH)"
|
||||
fi
|
||||
echo -en "\033[$((start_line + 1));$((start_col + 1))H${error_msg:0:$((max_width-2))}"
|
||||
elif [[ ${#header_row[@]} -gt 0 && $data_display_count -eq 0 ]]; then
|
||||
local no_data_msg="Info: Header found, but no data rows."
|
||||
echo -en "\033[$((start_line + 2));$((start_col + 1))H${no_data_msg:0:$((max_width-2))}"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Terminal Control Functions ---
|
||||
hide_cursor() {
|
||||
echo -en "\033[?25l"
|
||||
}
|
||||
|
||||
show_cursor() {
|
||||
echo -en "\033[?25h"
|
||||
}
|
||||
|
||||
alt_screen() {
|
||||
echo -en "\033[?1049h"
|
||||
}
|
||||
|
||||
normal_screen() {
|
||||
echo -en "\033[?1049l"
|
||||
}
|
||||
|
||||
clear_screen() {
|
||||
echo -en "\033[2J"
|
||||
}
|
||||
|
||||
# --- Global Variables ---
|
||||
SCRIPT_NAME="DataViewer"
|
||||
VERSION="0.1.0"
|
||||
CONFIG_REFRESH_INTERVAL_SECONDS=5 # Default refresh interval in seconds
|
||||
INPUT_FILE_PATH=""
|
||||
declare -a header_row # Will store the header fields
|
||||
declare -a parsed_data_rows # Will store namerefs to actual data row arrays
|
||||
|
||||
# --- Data Parsing Functions ---
|
||||
# Usage: parse_csv_tsv <file_path> <delimiter>
|
||||
parse_csv_tsv() {
|
||||
local file_path="$1"
|
||||
local delimiter="$2"
|
||||
local line_num=0
|
||||
|
||||
# Reset global data arrays
|
||||
header_row=()
|
||||
parsed_data_rows=()
|
||||
|
||||
if [[ ! -f "$file_path" ]]; then
|
||||
# This message will be shown by display_data
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Read the header line
|
||||
IFS="$delimiter" read -r -a header_row < <(head -n 1 "$file_path")
|
||||
|
||||
local temp_row_idx=0
|
||||
# Use process substitution and tail correctly
|
||||
# Ensure to handle files with only a header or files where data lines might not end with a newline
|
||||
while IFS="$delimiter" read -r -a line_fields || { [[ -n "${line_fields[*]}" ]] && (( ${#line_fields[@]} > 0 )); }; do
|
||||
# Skip empty lines that might be read by `read` if not careful
|
||||
[[ -z "${line_fields[*]}" ]] && continue
|
||||
|
||||
local row_var_name="data_row_${temp_row_idx}"
|
||||
# Declare the array dynamically and assign fields to it
|
||||
declare -g "$row_var_name=()"
|
||||
for field in "${line_fields[@]}"; do
|
||||
# Ensure fields containing only spaces are preserved if necessary, by quoting.
|
||||
# Using printf to escape quotes within the field itself if necessary for the declare statement.
|
||||
printf -v safe_field "%q" "$field"
|
||||
declare -g -a "$row_var_name+=($safe_field)"
|
||||
done
|
||||
|
||||
parsed_data_rows+=("$row_var_name")
|
||||
((temp_row_idx++))
|
||||
done < <(tail -n +2 "$file_path") # Start from the second line for data
|
||||
|
||||
if [[ $(wc -l < "$file_path") -eq 1 && ${#header_row[@]} -gt 0 && ${#parsed_data_rows[@]} -eq 0 ]]; then
|
||||
: # Current behavior: single line is header.
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: parse_json <file_path>
|
||||
parse_json() {
|
||||
local file_path="$1"
|
||||
|
||||
# Reset global data arrays
|
||||
header_row=()
|
||||
parsed_data_rows=()
|
||||
|
||||
if [[ ! -f "$file_path" ]]; then
|
||||
# This message will be displayed by display_data if header_row remains empty
|
||||
header_row=("Error: File not found ($file_path)")
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
header_row=("Error: jq is required for JSON.")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for completely empty file or just whitespace
|
||||
if [[ ! -s "$file_path" ]] || [[ -z "$(jq . "$file_path" 2>/dev/null)" ]]; then
|
||||
header_row=("Error: JSON file is empty or invalid.")
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if it's an empty array specifically
|
||||
if [[ "$(jq 'if type == "array" and length == 0 then "empty_array" else "not_empty" end' -r "$file_path")" == "empty_array" ]]; then
|
||||
header_row=("Info: JSON file is an empty array [].")
|
||||
return 0 # Successfully parsed an empty array
|
||||
fi
|
||||
|
||||
# Attempt to parse JSON as an array of objects
|
||||
# Extract headers from the keys of the first object
|
||||
mapfile -t header_row < <(jq -r 'if type == "array" and length > 0 and .[0] != null and .[0] != "null" then .[0] | keys_unsorted[] else empty end' "$file_path")
|
||||
|
||||
if ((${#header_row[@]} == 0)); then
|
||||
# Attempt to parse as a simple object (key-value pairs)
|
||||
mapfile -t header_row < <(jq -r 'if type == "object" then keys_unsorted[] else empty end' "$file_path")
|
||||
if ((${#header_row[@]} > 0)); then
|
||||
local temp_row_idx=0
|
||||
local row_var_name="data_row_${temp_row_idx}"
|
||||
|
||||
# Correctly initialize the array before appending
|
||||
declare -g "$row_var_name=()"
|
||||
|
||||
for key in "${header_row[@]}"; do
|
||||
local value
|
||||
# Use printf for safer value assignment, then read into var
|
||||
value_str=$(jq -r --arg k "$key" '.[$k]' "$file_path")
|
||||
declare -g -a "$row_var_name+=($(printf %q "$value_str"))"
|
||||
done
|
||||
parsed_data_rows+=("$row_var_name")
|
||||
else
|
||||
header_row=("Error: Unsupported JSON structure or empty object.")
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Process as an array of objects
|
||||
local temp_row_idx=0
|
||||
local num_objects
|
||||
num_objects=$(jq '. | length' "$file_path")
|
||||
|
||||
for ((i=0; i<num_objects; i++)); do
|
||||
local row_var_name="data_row_${temp_row_idx}"
|
||||
declare -g "$row_var_name=()" # Initialize row array
|
||||
for key in "${header_row[@]}"; do
|
||||
local value
|
||||
value_str=$(jq -r --arg k "$key" ".[$i] | .[\$k]" "$file_path")
|
||||
declare -g -a "$row_var_name+=($(printf %q "$value_str"))"
|
||||
done
|
||||
parsed_data_rows+=("$row_var_name")
|
||||
((temp_row_idx++))
|
||||
done
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Main Function Placeholder ---
|
||||
main() {
|
||||
alt_screen
|
||||
clear_screen
|
||||
hide_cursor
|
||||
|
||||
trap 'cleanup' EXIT SIGINT SIGTERM
|
||||
|
||||
local box_line=1 box_col=1
|
||||
# These will be dynamically set based on terminal size anyway
|
||||
# local box_height=15 box_width=60
|
||||
|
||||
# Determine file type and parse data
|
||||
if [[ -n "$INPUT_FILE_PATH" ]]; then
|
||||
local file_ext="${INPUT_FILE_PATH##*.}"
|
||||
if [[ ! -f "$INPUT_FILE_PATH" ]]; then
|
||||
# File not found at the start, set header_row to an error message
|
||||
# This allows display_data to show it.
|
||||
header_row=("Error: File not found ($INPUT_FILE_PATH)")
|
||||
parsed_data_rows=()
|
||||
elif [[ "$file_ext" == "csv" ]]; then
|
||||
parse_csv_tsv "$INPUT_FILE_PATH" ","
|
||||
elif [[ "$file_ext" == "tsv" ]]; then
|
||||
parse_csv_tsv "$INPUT_FILE_PATH" $'\t' # Tab delimiter
|
||||
elif [[ "$file_ext" == "json" ]]; then
|
||||
parse_json "$INPUT_FILE_PATH"
|
||||
else
|
||||
header_row=("Error: Unknown file type ($file_ext)")
|
||||
parsed_data_rows=()
|
||||
fi
|
||||
else
|
||||
# No input file path given
|
||||
header_row=("No input file specified.")
|
||||
parsed_data_rows=()
|
||||
fi
|
||||
|
||||
# Main loop
|
||||
while true; do
|
||||
local term_height term_width
|
||||
if ! read -r term_height term_width < <(stty size); then
|
||||
term_height=24
|
||||
term_width=80
|
||||
# echo "Failed to get terminal size, defaulting to ${term_height}x${term_width}" >&2 # Debug
|
||||
fi
|
||||
|
||||
|
||||
local current_box_height=$((term_height - 1)) # Leave 1 line for status/quit message
|
||||
local current_box_width=$((term_width))
|
||||
if ((current_box_height < 5)); then current_box_height=5; fi
|
||||
if ((current_box_width < 20)); then current_box_width=20; fi
|
||||
|
||||
clear_screen
|
||||
local current_title="${INPUT_FILE_PATH:-Data Viewer}"
|
||||
if [[ "${header_row[0]}" == "Error:"* || "${header_row[0]}" == "No input file specified." ]]; then
|
||||
current_title="${header_row[0]}" # Show error as title if parsing failed early
|
||||
elif [[ "${header_row[0]}" == "Info:"* ]]; then # Also show Info messages in title
|
||||
current_title="${header_row[0]}"
|
||||
fi
|
||||
draw_box "$box_line" "$box_col" "$current_box_height" "$current_box_width" "$current_title"
|
||||
|
||||
display_data "$box_line" "$box_col" "$current_box_width" "$current_box_height"
|
||||
|
||||
# Status/Quit message at the bottom of the terminal
|
||||
echo -en "\033[$((term_height));1H" # Move to last line, first column
|
||||
printf "%-${term_width}s" "Press 'q' to quit. (r)efresh. Interval: ${CONFIG_REFRESH_INTERVAL_SECONDS}s" # Clear line and print
|
||||
|
||||
read -rsn1 -t "${CONFIG_REFRESH_INTERVAL_SECONDS}" key
|
||||
key=${key,,} # to lower case
|
||||
if [[ "$key" == "q" ]]; then
|
||||
break
|
||||
elif [[ "$key" == "r" ]]; then
|
||||
# Re-evaluate what to parse or show on refresh
|
||||
if [[ -n "$INPUT_FILE_PATH" ]]; then
|
||||
local file_ext="${INPUT_FILE_PATH##*.}"
|
||||
if [[ ! -f "$INPUT_FILE_PATH" ]]; then
|
||||
header_row=("Error: File not found ($INPUT_FILE_PATH)")
|
||||
parsed_data_rows=()
|
||||
elif [[ "$file_ext" == "csv" ]]; then
|
||||
parse_csv_tsv "$INPUT_FILE_PATH" ","
|
||||
elif [[ "$file_ext" == "tsv" ]]; then
|
||||
parse_csv_tsv "$INPUT_FILE_PATH" $'\t'
|
||||
elif [[ "$file_ext" == "json" ]]; then
|
||||
parse_json "$INPUT_FILE_PATH"
|
||||
else
|
||||
header_row=("Error: Unknown file type ($file_ext)")
|
||||
parsed_data_rows=()
|
||||
fi
|
||||
else
|
||||
header_row=("No input file specified.")
|
||||
parsed_data_rows=()
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- Cleanup Function ---
|
||||
cleanup() {
|
||||
show_cursor
|
||||
normal_screen
|
||||
echo "Data Viewer Exiting."
|
||||
}
|
||||
|
||||
# --- Script Entry Point ---
|
||||
if [[ -n "$1" ]]; then
|
||||
INPUT_FILE_PATH="$1"
|
||||
# Sanitize INPUT_FILE_PATH? Expand ~? Resolve relative paths?
|
||||
# For now, assume it's usable as is.
|
||||
fi
|
||||
|
||||
if [[ -n "$2" ]]; then
|
||||
if [[ "$2" =~ ^[0-9]+$ && "$2" -gt 0 ]]; then
|
||||
CONFIG_REFRESH_INTERVAL_SECONDS="$2"
|
||||
else
|
||||
# Silently use default if invalid, or echo to stderr if not in alt screen mode yet.
|
||||
# Since we go to alt screen immediately, user won't see this.
|
||||
# echo "Warning: Invalid refresh interval '$2'. Using default ${CONFIG_REFRESH_INTERVAL_SECONDS}s." >&2
|
||||
: # Use default
|
||||
fi
|
||||
fi
|
||||
# Removed echo statements that would interfere with TUI.
|
||||
# Any initial messages should be handled after alt_screen if really needed,
|
||||
# or passed to main/display_data.
|
||||
|
||||
main
|
Loading…
Reference in New Issue