pull/245/merge
Cannae E. W. A. Hyphen 2025-06-16 11:11:47 -07:00 committed by GitHub
commit 2d2fc311ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 495 additions and 405 deletions

489
README.md
View File

@ -1,417 +1,96 @@
# ![bashtop](Imgs/logo-t.png)
# Bash Data Viewer
![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux)
![OSX](https://img.shields.io/badge/-OSX-black?logo=apple)
![FreeBSD](https://img.shields.io/badge/-FreeBSD-red?logo=freebsd)
![Usage](https://img.shields.io/badge/Usage-System%20resource%20monitor-blue)
![Bash](https://img.shields.io/badge/Bash-v4.4%5E-green?logo=GNU%20bash)
![Python](https://img.shields.io/badge/Python-v3.6%5E-orange?logo=python)
![bashtop_version](https://img.shields.io/github/v/tag/aristocratos/bashtop?label=version)
[![Build Status](https://travis-ci.com/aristocratos/bashtop.svg?branch=master)](https://travis-ci.com/aristocratos/bashtop)
[![Donate](https://img.shields.io/badge/-Donate-yellow?logo=paypal)](https://paypal.me/aristocratos)
[![Sponsor](https://img.shields.io/badge/-Sponsor-red?logo=github)](https://github.com/sponsors/aristocratos)
[![Coffee](https://img.shields.io/badge/-Buy%20me%20a%20Coffee-grey?logo=Ko-fi)](https://ko-fi.com/aristocratos)
### C++ Version
##### 18 September 2021
![btop++](https://raw.githubusercontent.com/aristocratos/btop/main/Img/logo.png)
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.
![Screenshot 1](Imgs/main.png)
Main menu.
![Screenshot 2](Imgs/menu.png)
Options menu.
![Screenshot 3](Imgs/options.png)
## 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.

411
dataviewer.sh Executable file
View File

@ -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