diff --git a/README.md b/README.md index 280fdcc..6fa6f84 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/dataviewer.sh b/dataviewer.sh new file mode 100755 index 0000000..ade8430 --- /dev/null +++ b/dataviewer.sh @@ -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 [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 +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 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 +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 +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&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