viewinline 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: viewinline
3
+ Version: 0.3.0
4
+ Summary: Quick look geospatial viewer for the terminal, with inline image previews
5
+ Project-URL: Homepage, https://github.com/nkeikon/viewinline
6
+ Project-URL: Repository, https://github.com/nkeikon/viewinline
7
+ Project-URL: Issues, https://github.com/nkeikon/viewinline/issues
8
+ Author: Keiko Nomura
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: geopandas
13
+ Requires-Dist: matplotlib
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pandas
16
+ Requires-Dist: pillow
17
+ Requires-Dist: pyogrio
18
+ Requires-Dist: rasterio
19
+ Provides-Extra: all
20
+ Requires-Dist: duckdb; extra == 'all'
21
+ Requires-Dist: h5py; extra == 'all'
22
+ Requires-Dist: netcdf4; extra == 'all'
23
+ Requires-Dist: pyarrow; extra == 'all'
24
+ Provides-Extra: hdf5
25
+ Requires-Dist: h5py; extra == 'hdf5'
26
+ Provides-Extra: netcdf
27
+ Requires-Dist: netcdf4; extra == 'netcdf'
28
+ Provides-Extra: parquet
29
+ Requires-Dist: pyarrow; extra == 'parquet'
30
+ Provides-Extra: sql
31
+ Requires-Dist: duckdb; extra == 'sql'
32
+ Description-Content-Type: text/markdown
33
+
1
34
  # viewinline
2
35
  [![Downloads](https://static.pepy.tech/badge/viewinline)](https://pepy.tech/project/viewinline)
3
36
  [![PyPI version](https://img.shields.io/pypi/v/viewinline)](https://pypi.org/project/viewinline/)
4
37
  [![Python version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://pypi.org/project/viewinline/)
5
38
 
6
39
  **Quick-look geospatial viewer for compatible terminals.**
7
- Displays rasters, vectors, and tabular data directly in the terminal with no GUI and no temporary files.
40
+ Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
8
41
 
9
42
  <p align="center">
10
43
  <a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
@@ -17,7 +50,7 @@ Particularly useful on HPC systems and remote servers accessed via SSH. Images r
17
50
 
18
51
  This tool combines the core display logic of `viewtif` and `viewgeom`, but is **non-interactive**: you can't zoom, pan, or switch colormaps on the fly. Instead, you control everything through command-line options (e.g. --display, --color-by, --colormap).
19
52
 
20
- It uses the iTerm2 inline image protocol (OSC 1337) to render previews. In incompatible terminals, the escape codes are silently ignored with no errors or crashes.
53
+ It uses the iTerm2 inline image protocol (OSC 1337) in supported terminals, and falls back to `chafa` in others, which displays real high-res images in terminals like kitty and Ghostty, and colored block-art (ASCII art) previews in Terminal.app, VS Code, and most Linux terminals. Without `chafa` installed, non-iTerm2-family terminals show an info message instead.
21
54
 
22
55
  ## Installation
23
56
  Requires Python 3.9 or later.
@@ -31,6 +64,7 @@ pip install viewinline
31
64
  # Rasters
32
65
  viewinline path/to/file.tif
33
66
  viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
67
+ viewinline hyperspectral.nc --band 50
34
68
  viewinline path/to/multiband.tif --rgb 3 2 1
35
69
  viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
36
70
 
@@ -39,6 +73,7 @@ viewinline file.nc # list variables
39
73
  viewinline file.nc --subset 2 # display variable 2
40
74
  viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
41
75
  viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
76
+ viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
42
77
 
43
78
  # Vectors
44
79
  viewinline path/to/vector.geojson
@@ -63,18 +98,31 @@ viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
63
98
 
64
99
  ## Compatible terminals
65
100
 
66
- The iTerm2 inline image protocol (OSC 1337) is supported by:
101
+ Native (no extra install required): images render via the iTerm2 inline image protocol on:
67
102
 
68
103
  - **iTerm2** (macOS)
69
104
  - **WezTerm** (cross-platform)
70
105
  - **Konsole** (Linux/KDE)
71
106
  - **Rio**, **Contour** (cross-platform)
72
107
 
73
- **Not compatible:** Mac Terminal, GNOME Terminal, Kitty (uses different protocol), Ghostty, Alacritty
108
+ Via `chafa` (recommended for everyone else): install chafa and viewinline works in nearly every terminal:
109
+ - kitty, Ghostty, foot — real high-resolution images via the kitty graphics protocol or sixel
110
+ - Terminal.app, VS Code, GNOME Terminal, Alacritty, Warp, Hyper — colored block-art previews with 24-bit color
111
+
112
+ Install chafa once (it's a system binary, available across all conda/virtualenv environments):
113
+
114
+ ```
115
+ brew install chafa # macOS
116
+ sudo apt install chafa # Debian/Ubuntu
117
+ sudo dnf install chafa # Fedora
118
+ scoop install chafa # Windows
119
+ ```
120
+ Without chafa, terminals outside the native list above show an info message instead of an image.
121
+ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
74
122
 
75
123
  **SSH/HPC usage:** Works over SSH when connecting from a compatible terminal. Images render on your local machine, not the remote server. No X11 forwarding or VNC required.
76
124
 
77
- **tmux/screen:** Inline images don't work inside tmux or screen sessions, even with `allow-passthrough on`. Use a plain terminal tab.
125
+ **tmux/screen:** Inline images work inside tmux only when the outer terminal is iTerm2 (or WezTerm/Konsole/Rio/Contour). In tmux with other outer terminals (kitty, Terminal.app, etc.), viewinline displays ASCII art previews instead of full-quality images.
78
126
 
79
127
  **Fallback:** In terminals that do not support inline images, you can fallback to ASCII art by installing [`chafa`](https://hpjansson.org/chafa/) command-line tool. Install `chafa` with your package manager (e.g. `brew install chafa` or `sudo apt install chafa`). You can also force the use of `chafa` by setting the environment variable `INLINE_VIEWER_ENGINE=chafa`.
80
128
 
@@ -124,7 +172,9 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
124
172
 
125
173
  **NetCDF/HDF notes:**
126
174
  - viewinline lists only variables that can be displayed as 2D or 3D arrays
127
- - Variables with additional dimensions (e.g., vertical levels) may be listed but will fail to display with a clear error message
175
+ - 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
176
+ - For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
177
+ - Variables with 4+ dimensions are not supported
128
178
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
129
179
 
130
180
  ## Dependencies
@@ -137,6 +187,7 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
137
187
  - `numpy`, `pandas` — data handling
138
188
 
139
189
  **Optional dependencies:**
190
+ - `chafa` — strongly recommended for terminal coverage beyond iTerm2/WezTerm/Konsole/Rio/Contour. System binary, not a Python package. See "Terminal support" above for install instructions.
140
191
  - `duckdb` — required for `--where`, `--sort`, `--sql`, `--limit` with filtering
141
192
  ```bash
142
193
  pip install duckdb
@@ -152,7 +203,7 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
152
203
 
153
204
  **Note on HDF support:**
154
205
  - **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
155
- - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (common in MODIS data)
206
+ - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
156
207
  - **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
157
208
 
158
209
  ## Available options
@@ -164,6 +215,7 @@ Raster:
164
215
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
165
216
  --timestep INTEGER Alias for --band when working with NetCDF files.
166
217
  --subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
218
+ --reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
167
219
  --colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
168
220
  --rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
169
221
  --rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
@@ -1,40 +1,10 @@
1
- Metadata-Version: 2.4
2
- Name: viewinline
3
- Version: 0.2.2
4
- Summary: Quick look geospatial viewer for iTerm2 compatible terminals
5
- Project-URL: Homepage, https://github.com/nkeikon/viewinline
6
- Project-URL: Repository, https://github.com/nkeikon/viewinline
7
- Project-URL: Issues, https://github.com/nkeikon/viewinline/issues
8
- Author: Keiko Nomura
9
- License: Apache-2.0
10
- License-File: LICENSE
11
- Requires-Python: >=3.9
12
- Requires-Dist: geopandas
13
- Requires-Dist: matplotlib
14
- Requires-Dist: numpy
15
- Requires-Dist: pandas
16
- Requires-Dist: pillow
17
- Requires-Dist: pyogrio
18
- Requires-Dist: rasterio
19
- Provides-Extra: all
20
- Requires-Dist: duckdb; extra == 'all'
21
- Requires-Dist: h5py; extra == 'all'
22
- Requires-Dist: pyarrow; extra == 'all'
23
- Provides-Extra: hdf5
24
- Requires-Dist: h5py; extra == 'hdf5'
25
- Provides-Extra: parquet
26
- Requires-Dist: pyarrow; extra == 'parquet'
27
- Provides-Extra: sql
28
- Requires-Dist: duckdb; extra == 'sql'
29
- Description-Content-Type: text/markdown
30
-
31
1
  # viewinline
32
2
  [![Downloads](https://static.pepy.tech/badge/viewinline)](https://pepy.tech/project/viewinline)
33
3
  [![PyPI version](https://img.shields.io/pypi/v/viewinline)](https://pypi.org/project/viewinline/)
34
4
  [![Python version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://pypi.org/project/viewinline/)
35
5
 
36
6
  **Quick-look geospatial viewer for compatible terminals.**
37
- Displays rasters, vectors, and tabular data directly in the terminal with no GUI and no temporary files.
7
+ Displays rasters, vectors, and tabular data directly in the terminal with no GUI.
38
8
 
39
9
  <p align="center">
40
10
  <a href="viewinline_gif1.gif"><img src="viewinline_gif1.gif" width="49%"></a>
@@ -47,7 +17,7 @@ Particularly useful on HPC systems and remote servers accessed via SSH. Images r
47
17
 
48
18
  This tool combines the core display logic of `viewtif` and `viewgeom`, but is **non-interactive**: you can't zoom, pan, or switch colormaps on the fly. Instead, you control everything through command-line options (e.g. --display, --color-by, --colormap).
49
19
 
50
- It uses the iTerm2 inline image protocol (OSC 1337) to render previews. In incompatible terminals, the escape codes are silently ignored with no errors or crashes.
20
+ It uses the iTerm2 inline image protocol (OSC 1337) in supported terminals, and falls back to `chafa` in others, which displays real high-res images in terminals like kitty and Ghostty, and colored block-art (ASCII art) previews in Terminal.app, VS Code, and most Linux terminals. Without `chafa` installed, non-iTerm2-family terminals show an info message instead.
51
21
 
52
22
  ## Installation
53
23
  Requires Python 3.9 or later.
@@ -61,6 +31,7 @@ pip install viewinline
61
31
  # Rasters
62
32
  viewinline path/to/file.tif
63
33
  viewinline R.tif G.tif B.tif # RGB composite (also works with --rgbfiles)
34
+ viewinline hyperspectral.nc --band 50
64
35
  viewinline path/to/multiband.tif --rgb 3 2 1
65
36
  viewinline path/to/folder --gallery 4x3 # show image gallery (e.g. 4x3 grid)
66
37
 
@@ -69,6 +40,7 @@ viewinline file.nc # list variables
69
40
  viewinline file.nc --subset 2 # display variable 2
70
41
  viewinline file.nc --subset 1 --band 10 # variable 1, timestep 10 --band or --timestep
71
42
  viewinline temp.nc --subset 1 --colormap plasma --vmin 273 --vmax 310
43
+ viewinline hyperspectral.nc --subset 1 --reduce NumberOfScanlines # override auto-detected axis
72
44
 
73
45
  # Vectors
74
46
  viewinline path/to/vector.geojson
@@ -93,18 +65,31 @@ viewinline data.geoparquet --table --where "POP > 100000" --sort POP --desc
93
65
 
94
66
  ## Compatible terminals
95
67
 
96
- The iTerm2 inline image protocol (OSC 1337) is supported by:
68
+ Native (no extra install required): images render via the iTerm2 inline image protocol on:
97
69
 
98
70
  - **iTerm2** (macOS)
99
71
  - **WezTerm** (cross-platform)
100
72
  - **Konsole** (Linux/KDE)
101
73
  - **Rio**, **Contour** (cross-platform)
102
74
 
103
- **Not compatible:** Mac Terminal, GNOME Terminal, Kitty (uses different protocol), Ghostty, Alacritty
75
+ Via `chafa` (recommended for everyone else): install chafa and viewinline works in nearly every terminal:
76
+ - kitty, Ghostty, foot — real high-resolution images via the kitty graphics protocol or sixel
77
+ - Terminal.app, VS Code, GNOME Terminal, Alacritty, Warp, Hyper — colored block-art previews with 24-bit color
78
+
79
+ Install chafa once (it's a system binary, available across all conda/virtualenv environments):
80
+
81
+ ```
82
+ brew install chafa # macOS
83
+ sudo apt install chafa # Debian/Ubuntu
84
+ sudo dnf install chafa # Fedora
85
+ scoop install chafa # Windows
86
+ ```
87
+ Without chafa, terminals outside the native list above show an info message instead of an image.
88
+ You can also force the chafa path on any terminal by setting `INLINE_VIEWER_ENGINE=chafa`.
104
89
 
105
90
  **SSH/HPC usage:** Works over SSH when connecting from a compatible terminal. Images render on your local machine, not the remote server. No X11 forwarding or VNC required.
106
91
 
107
- **tmux/screen:** Inline images don't work inside tmux or screen sessions, even with `allow-passthrough on`. Use a plain terminal tab.
92
+ **tmux/screen:** Inline images work inside tmux only when the outer terminal is iTerm2 (or WezTerm/Konsole/Rio/Contour). In tmux with other outer terminals (kitty, Terminal.app, etc.), viewinline displays ASCII art previews instead of full-quality images.
108
93
 
109
94
  **Fallback:** In terminals that do not support inline images, you can fallback to ASCII art by installing [`chafa`](https://hpjansson.org/chafa/) command-line tool. Install `chafa` with your package manager (e.g. `brew install chafa` or `sudo apt install chafa`). You can also force the use of `chafa` by setting the environment variable `INLINE_VIEWER_ENGINE=chafa`.
110
95
 
@@ -154,7 +139,9 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
154
139
 
155
140
  **NetCDF/HDF notes:**
156
141
  - viewinline lists only variables that can be displayed as 2D or 3D arrays
157
- - Variables with additional dimensions (e.g., vertical levels) may be listed but will fail to display with a clear error message
142
+ - 3D variables with time or known spatial dimensions are auto-handled (slices along the non-spatial axis)
143
+ - For 3D variables with non-standard dimensions (e.g., hyperspectral cubes like PICARD), viewinline auto-detects the band axis by smallest dimension. Use `--reduce DIM_NAME` to override.
144
+ - Variables with 4+ dimensions are not supported
158
145
  - For a complete variable list, use `ncdump -h file.nc` or `viewtif`
159
146
 
160
147
  ## Dependencies
@@ -167,6 +154,7 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
167
154
  - `numpy`, `pandas` — data handling
168
155
 
169
156
  **Optional dependencies:**
157
+ - `chafa` — strongly recommended for terminal coverage beyond iTerm2/WezTerm/Konsole/Rio/Contour. System binary, not a Python package. See "Terminal support" above for install instructions.
170
158
  - `duckdb` — required for `--where`, `--sort`, `--sql`, `--limit` with filtering
171
159
  ```bash
172
160
  pip install duckdb
@@ -182,7 +170,7 @@ The iTerm2 inline image protocol (OSC 1337) is supported by:
182
170
 
183
171
  **Note on HDF support:**
184
172
  - **HDF5** (.h5, .hdf5): Supported via rasterio if GDAL has HDF5 support (most installations)
185
- - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (common in MODIS data)
173
+ - **HDF4** (.hdf): Requires GDAL compiled with HDF4 support (the legacy format used by MODIS and older NASA products)
186
174
  - **NetCDF** (.nc): Supported via rasterio (uses GDAL's NetCDF driver)
187
175
 
188
176
  ## Available options
@@ -194,6 +182,7 @@ Raster:
194
182
  --band BAND Band number to display (single raster), or slice number for NetCDF. (default: 1)
195
183
  --timestep INTEGER Alias for --band when working with NetCDF files.
196
184
  --subset INTEGER Variable index for NetCDF/HDF files (e.g., --subset 1).
185
+ --reduce DIM_NAME For 3D NetCDF variables, specify which dimension to use as the band/slider axis. Auto-detected if omitted.
197
186
  --colormap Apply colormap to single-band rasters. Flag without the color scheme → 'terrain'.
198
187
  --rgb R G B Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3.
199
188
  --rgbfiles R G B Three single-band rasters for RGB composite. Can also provide as positional arguments.
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "viewinline"
7
- version = "0.2.2"
8
- description = "Quick look geospatial viewer for iTerm2 compatible terminals"
7
+ version = "0.3.0"
8
+ description = "Quick look geospatial viewer for the terminal, with inline image previews"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
11
11
  authors = [
@@ -22,12 +22,12 @@ dependencies = [
22
22
  "pyogrio",
23
23
  "pandas"
24
24
  ]
25
-
26
25
  [project.optional-dependencies]
27
26
  sql = ["duckdb"]
28
27
  parquet = ["pyarrow"]
29
28
  hdf5 = ["h5py"]
30
- all = ["duckdb", "pyarrow", "h5py"]
29
+ netcdf = ["netCDF4"]
30
+ all = ["duckdb", "pyarrow", "h5py", "netCDF4"]
31
31
 
32
32
  [project.scripts]
33
33
  viewinline = "viewinline.viewinline:main"
@@ -28,12 +28,19 @@ from matplotlib import colormaps
28
28
  import matplotlib as mpl
29
29
  import subprocess
30
30
 
31
+ try:
32
+ import netCDF4
33
+ HAS_NETCDF4 = True
34
+ except ImportError:
35
+ HAS_NETCDF4 = False
36
+
31
37
  import warnings
32
38
 
33
39
  warnings.filterwarnings("ignore", message="More than one layer found", category=UserWarning)
34
40
  warnings.filterwarnings("ignore", message="Dataset has no geotransform", category=UserWarning)
41
+ warnings.filterwarnings("ignore", message="invalid scale_factor or add_offset attribute", category=UserWarning)
35
42
 
36
- __version__ = "0.2.2"
43
+ __version__ = "0.3.0"
37
44
 
38
45
  AVAILABLE_COLORMAPS = [
39
46
  "viridis", "inferno", "magma", "plasma",
@@ -41,12 +48,61 @@ AVAILABLE_COLORMAPS = [
41
48
  "Spectral", "cubehelix", "tab10", "turbo"
42
49
  ]
43
50
 
51
+ # Terminals that don't natively support the iTerm2 OSC 1337 inline image
52
+ # protocol. Output for these is routed through chafa instead.
53
+ #
54
+ # Note: presence in this list does NOT mean "no images." Chafa auto-detects
55
+ # the terminal and picks the best output:
56
+ # - kitty (xterm-kitty) → real images via kitty graphics protocol
57
+ # - some others (e.g. foot, Ghostty) → may render real images via sixel
58
+ # or kitty protocol depending on chafa's detection
59
+ # - most others → Unicode block-art preview with 24-bit color
60
+ # (Terminal.app, VS Code, GNOME Terminal, Alacritty, Warp, etc.)
61
+ # Only terminals without chafa installed see no rendering at all.
62
+
44
63
  _TERMINALS_WITHOUT_IMAGES = [
45
- 'unknown', 'cygwin', 'tmux', 'screen',
46
- 'vscode', 'xterm', 'rxvt', 'alacritty', 'foot', 'st', 'kitty',
47
- 'gnome-terminal', 'xfce4-terminal', 'lxterminal', 'terminator',
48
- 'tilix', 'hyper', 'windows terminal', 'putty', 'sakura',
49
- 'terminology', 'eterm', 'guake', 'tilda', 'deepin-terminal',
64
+ # macOS
65
+ 'Apple_Terminal', # $TERM_PROGRAM for Terminal.app
66
+
67
+ # kitty (renders real images via chafa → kitty graphics protocol)
68
+ 'xterm-kitty', # $TERM in kitty
69
+
70
+ # tmux / screen (TERM strings; $TMUX env var also signals tmux)
71
+ 'screen', 'screen-256color',
72
+ 'tmux', 'tmux-256color',
73
+
74
+ # Editors / IDE terminals
75
+ 'vscode', # $TERM_PROGRAM in VS Code integrated terminal
76
+
77
+ # Cross-platform terminals known not to support OSC 1337
78
+ 'alacritty',
79
+ 'foot', # supports sixel → chafa renders real images
80
+ 'ghostty', 'xterm-ghostty',
81
+ 'WarpTerminal', # $TERM_PROGRAM in Warp
82
+ 'Hyper', # $TERM_PROGRAM in Hyper
83
+
84
+ # Generic / legacy
85
+ 'unknown',
86
+ 'cygwin',
87
+ 'rxvt', 'rxvt-unicode', 'rxvt-unicode-256color',
88
+ 'st-256color', # suckless st
89
+
90
+ # Linux desktop terminals (most are VTE-based, no OSC 1337)
91
+ 'gnome-terminal',
92
+ 'xfce4-terminal',
93
+ 'lxterminal',
94
+ 'terminator',
95
+ 'tilix',
96
+ 'sakura',
97
+ 'terminology',
98
+ 'guake',
99
+ 'tilda',
100
+ 'deepin-terminal',
101
+ 'eterm',
102
+
103
+ # Windows
104
+ 'putty',
105
+ 'Windows Terminal',
50
106
  ]
51
107
 
52
108
  def detect_terminal() -> dict[str, str]:
@@ -135,41 +191,51 @@ def show_inline_image(image_array: np.ndarray, display_scale = None, is_vector:
135
191
 
136
192
  if _TERMINAL_SUPPORTS_IMAGES:
137
193
  sys.stdout.write(f"\033]1337;File=inline=1;width={width_pct}%:{encoded}\a\n")
138
- else:
194
+ else:
139
195
  if is_chafa_available():
196
+ # Inside tmux, force chafa to use block-art symbols instead of
197
+ # graphics protocols. Tmux mangles kitty graphics and sixel
198
+ # protocols, producing dot-character garbage on screen. Block-art
199
+ # passes through tmux reliably on every outer terminal.
200
+ chafa_args = ["chafa", "-"]
201
+ if os.environ.get("TMUX"):
202
+ chafa_args = ["chafa", "-f", "symbols", "-"]
140
203
  chafa_output = subprocess.check_output(
141
- ["chafa", "-"],
204
+ chafa_args,
142
205
  input=image_bytes
143
206
  ).decode()
144
207
 
145
208
  sys.stdout.write(f"\n{chafa_output}\a\n")
209
+
146
210
  else:
147
211
  sys.stdout.write(f"[INFO] Use supported terminal or install 'chafa' for ascii art fallback. Detected: {_TERMINAL_INFO}\n")
148
212
 
149
213
  sys.stdout.flush()
150
214
 
151
-
152
- def show_image_auto(img: np.ndarray, display_scale = None, is_vector: bool = False) -> None:
153
- """Attempt inline image display. No fallbacks, no detection.
215
+ def show_image_auto(img: np.ndarray, display_scale=None, is_vector: bool = False) -> None:
216
+ """Render an image inline, with chafa fallback for non-iTerm2 terminals.
154
217
 
155
- Just sends the iTerm2 inline image escape sequence. If the terminal supports it,
156
- great. If not, the escape codes are ignored and nothing happens.
218
+ Cascade:
219
+ 1. If terminal supports OSC 1337 emit iTerm2 inline image sequence.
220
+ 2. Else if chafa is installed → pipe through chafa (which auto-detects
221
+ and emits the terminal's native graphics protocol or block-art).
222
+ 3. Else → print an info message suggesting chafa installation.
157
223
 
224
+ The branching happens inside show_inline_image(); this wrapper handles
225
+ status messaging and exception safety.
158
226
  """
159
- if os.environ.get("TMUX"):
160
- print("[WARN] Inside tmux — inline images won't display even with allow-passthrough on (known iTerm2/tmux issue). Use a plain terminal tab.")
161
- return
162
-
163
227
  try:
164
228
  show_inline_image(img, display_scale, is_vector)
165
- print("[VIEW] Image sent — visible in compatible terminals")
229
+ if _TERMINAL_SUPPORTS_IMAGES:
230
+ print("[VIEW] Inline render complete")
231
+ elif is_chafa_available():
232
+ print("[VIEW] Inline render complete via chafa")
233
+ # If neither path applies, show_inline_image already printed the info message
166
234
  except Exception as e:
167
- # If image encoding fails, print error but don't crash
168
- print(f"[ERROR] Failed to encode image: {e}")
235
+ print(f"[ERROR] Failed to render image: {e}")
169
236
  import traceback
170
237
  traceback.print_exc()
171
238
 
172
-
173
239
  def resize_to_terminal(img: np.ndarray) -> tuple[np.ndarray, float]:
174
240
  """Resize image to fit terminal window (approx 8x16 pixel cells)."""
175
241
  cols, rows = shutil.get_terminal_size((100, 40))
@@ -196,7 +262,6 @@ def load_csv_to_df(path: str) -> pd.DataFrame:
196
262
  print(f"[ERROR] Failed to read CSV: {e}")
197
263
  return pd.DataFrame()
198
264
 
199
-
200
265
  # =============================================================
201
266
  # Preview
202
267
  # =============================================================
@@ -577,6 +642,177 @@ def render_simple_image(filepath: str, args) -> None:
577
642
  except Exception as e:
578
643
  print(f"[ERROR] Failed to load image: {e}")
579
644
 
645
+ def render_netcdf_via_netcdf4(path, args):
646
+ """Read a NetCDF file via netCDF4 (bypassing GDAL). Handles hierarchical
647
+ groups and hyperspectral cubes where GDAL aborts or interprets axes wrong.
648
+ """
649
+ if not HAS_NETCDF4:
650
+ print("[ERROR] netCDF4 not installed. Install with:")
651
+ print(" pip install netCDF4")
652
+ print(" or: pip install viewinline[netcdf]")
653
+ return
654
+
655
+ try:
656
+ nc = netCDF4.Dataset(path)
657
+ except Exception as e:
658
+ print(f"[ERROR] Could not open NetCDF file: {e}")
659
+ return
660
+
661
+ # Recursively collect (path, variable) pairs across all groups
662
+ def collect_vars(group, prefix=""):
663
+ out = []
664
+ for name, var in group.variables.items():
665
+ full_name = f"{prefix}{name}"
666
+ out.append((full_name, var))
667
+ for sub_name, sub in group.groups.items():
668
+ out.extend(collect_vars(sub, f"{prefix}{sub_name}/"))
669
+ return out
670
+
671
+ all_vars = collect_vars(nc)
672
+
673
+ if not all_vars:
674
+ print("[ERROR] No variables found in file.")
675
+ nc.close()
676
+ return
677
+
678
+ # If no --subset, list all variables and exit
679
+ if not args.subset:
680
+ print(f"Found {len(all_vars)} variables in {os.path.basename(path)}:")
681
+ for i, (name, var) in enumerate(all_vars, 1):
682
+ shape_str = "x".join(str(s) for s in var.shape)
683
+ print(f" [{i}] {name} ({shape_str}, {var.dtype})")
684
+ print(f"\nUse --subset <N> to display a specific variable.")
685
+ nc.close()
686
+ return
687
+
688
+ # Validate --subset
689
+ if args.subset < 1 or args.subset > len(all_vars):
690
+ print(f"[ERROR] --subset must be between 1 and {len(all_vars)}")
691
+ nc.close()
692
+ return
693
+
694
+ var_name, var = all_vars[args.subset - 1]
695
+ print(f"[INFO] Displaying variable {args.subset}: {var_name}")
696
+ print(f"[DATA] Shape: {var.shape} dtype: {var.dtype} dims: {var.dimensions}")
697
+
698
+ # Detect dimensionality and read the right slice
699
+ if var.ndim == 2:
700
+ data = np.asarray(var[:, :], dtype=np.float64)
701
+ slice_info = "2D variable"
702
+
703
+ elif var.ndim == 3:
704
+ spatial_dims = {'lat', 'lon', 'latitude', 'longitude', 'y', 'x'}
705
+
706
+ spectral_axis = None
707
+
708
+ # 1. User override via --reduce
709
+ if args.reduce_dim is not None:
710
+ if args.reduce_dim in var.dimensions:
711
+ spectral_axis = list(var.dimensions).index(args.reduce_dim)
712
+ print(f"[INFO] Using user-specified --reduce '{args.reduce_dim}'")
713
+ else:
714
+ print(f"[ERROR] --reduce '{args.reduce_dim}' is not a dimension of this variable.")
715
+ print(f"[INFO] Available dimensions: {list(var.dimensions)}")
716
+ nc.close()
717
+ return
718
+
719
+ # 2. Standard convention: reduce along the non-spatial dim
720
+ if spectral_axis is None:
721
+ has_standard_spatial = any(d in spatial_dims for d in var.dimensions)
722
+ if has_standard_spatial:
723
+ for i, dim_name in enumerate(var.dimensions):
724
+ if dim_name not in spatial_dims:
725
+ spectral_axis = i
726
+ break
727
+
728
+ # 3. Fallback heuristic: smallest dim is typically the band axis
729
+ if spectral_axis is None:
730
+ sizes = [(i, var.shape[i]) for i in range(3)]
731
+ spectral_axis = min(sizes, key=lambda x: x[1])[0]
732
+ print(f"[INFO] Non-standard dimensions detected: {list(var.dimensions)}")
733
+ print(f"[INFO] Reducing along '{var.dimensions[spectral_axis]}' (size {var.shape[spectral_axis]}, assumed band/spectral axis)")
734
+ print(f"[INFO] If this is not correct, use --reduce DIM_NAME to override.")
735
+
736
+ # Slice along chosen axis
737
+ band_count = var.shape[spectral_axis]
738
+ band_num = args.band if args.band is not None else 1
739
+ band_idx = max(0, min(band_num - 1, band_count - 1))
740
+ slicer = [slice(None)] * 3
741
+ slicer[spectral_axis] = band_idx
742
+ data = np.asarray(var[tuple(slicer)], dtype=np.float64)
743
+ slice_info = f"slice along axis {spectral_axis} ({var.dimensions[spectral_axis]}), band {band_idx + 1} of {band_count}"
744
+
745
+ else:
746
+ print(f"[ERROR] viewinline only supports 2D or 3D variables. This one is {var.ndim}D.")
747
+ nc.close()
748
+ return
749
+
750
+ print(f"[DATA] {slice_info}")
751
+ # Apply fill value
752
+ fill = getattr(var, '_FillValue', None)
753
+ if fill is not None:
754
+ data = np.where(data == fill, np.nan, data)
755
+ # Flip vertically if data is stored south-to-north so north appears at top.
756
+ # Determine which dims remain after slicing — for the 2D result, figure out
757
+ # which axis (0 or 1) corresponds to latitude, and check that dim's coord values.
758
+ if var.ndim == 2:
759
+ remaining_dims = list(var.dimensions)
760
+ elif var.ndim == 3:
761
+ if spectral_axis is not None:
762
+ remaining_dims = [d for i, d in enumerate(var.dimensions) if i != spectral_axis]
763
+ else:
764
+ remaining_dims = list(var.dimensions[1:]) # axis 0 was reduced
765
+ else:
766
+ remaining_dims = []
767
+ lat_names = {'lat', 'latitude', 'y'}
768
+ for axis_in_2d, dim_name in enumerate(remaining_dims):
769
+ if dim_name in lat_names and dim_name in nc.variables:
770
+ lat_vals = nc[dim_name][:]
771
+ if len(lat_vals) > 1 and lat_vals[0] < lat_vals[-1]:
772
+ data = np.flip(data, axis=axis_in_2d)
773
+ print(f"[INFO] Flipped along '{dim_name}' for display (data stored south-to-north).")
774
+ break
775
+ nc.close()
776
+
777
+ # Normalize and display
778
+ band_u8 = normalize_to_uint8(data, vmin=args.vmin, vmax=args.vmax,
779
+ nodata=args.nodata)
780
+
781
+ if args.colormap:
782
+ cmap = colormaps[args.colormap]
783
+ colored = cmap(band_u8 / 255.0)
784
+ img = (colored[:, :, :3] * 255).astype(np.uint8)
785
+ print(f"[INFO] Applying colormap: {args.colormap}")
786
+ else:
787
+ img = np.stack([band_u8] * 3, axis=-1)
788
+ print("[INFO] Displaying grayscale")
789
+
790
+ # Resize to terminal
791
+ H, W = img.shape[:2]
792
+ if args.display:
793
+ new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
794
+ img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
795
+ print(f"[VIEW] Manual resize ×{args.display:.2f} → {new_w}×{new_h}px")
796
+ # else:
797
+ # img, scale = resize_to_terminal(img)
798
+ # print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
799
+ else:
800
+ max_dim = 2000
801
+ if max(img.shape[:2]) > max_dim:
802
+ scale = max_dim / max(img.shape[:2])
803
+ new_w = int(img.shape[1] * scale)
804
+ new_h = int(img.shape[0] * scale)
805
+ img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
806
+ print(f"[VIEW] Downsampled from {W}×{H}px to {new_w}×{new_h}px (scale={scale:.2f})")
807
+ print(f"[INFO] Use --display 1 for full resolution.")
808
+ else:
809
+ # (matches the width_pct logic in show_inline_image)
810
+ display_pct = args.display if args.display is not None else 0.33
811
+
812
+ print(f"[VIEW] Rendered image size → {img.shape[1]}×{img.shape[0]}px (size={display_pct:.2f})")
813
+
814
+ show_image_auto(img, getattr(args, "display", None), is_vector=False)
815
+
580
816
  def render_raster(paths: list[str], args) -> None:
581
817
  try:
582
818
  import rasterio
@@ -589,10 +825,16 @@ def render_raster(paths: list[str], args) -> None:
589
825
 
590
826
  if len(paths) == 1:
591
827
  path = paths[0]
828
+
829
+ if path.lower().endswith('.nc'):
830
+ render_netcdf_via_netcdf4(path, args)
831
+ return
592
832
 
593
833
  # Handle NetCDF/HDF with subdatasets
834
+
594
835
  if path.lower().endswith(('.nc', '.hdf', '.hdf5', '.h5')):
595
836
  try:
837
+
596
838
  with rasterio.open(path) as src:
597
839
  subdatasets = src.subdatasets
598
840
 
@@ -665,7 +907,7 @@ def render_raster(paths: list[str], args) -> None:
665
907
  resampling=rasterio.enums.Resampling.bilinear
666
908
  )
667
909
 
668
- print(f"[PROC] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
910
+ print(f"[VIEW] Downsampled for preview → {out_w}×{out_h}px (scale={scale:.3f})")
669
911
  else:
670
912
  data = ds.read()
671
913
 
@@ -677,8 +919,11 @@ def render_raster(paths: list[str], args) -> None:
677
919
  print(f"[INFO] Multi-band raster detected ({band_count} bands)")
678
920
 
679
921
  # MULTI BAND RGB (skip for NetCDF - treat as slices/timesteps, not RGB)
680
- if band_count >= 3 and not paths[0].lower().endswith('.nc'):
681
-
922
+ # if band_count >= 3 and not paths[0].lower().endswith('.nc'):
923
+ # Auto-composite to RGB only when user didn't explicitly ask for a single band
924
+ # user_specified_band = args.band is not None and args.band != 1
925
+ user_specified_band = args.band is not None
926
+ if band_count >= 3 and not paths[0].lower().endswith('.nc') and not user_specified_band:
682
927
 
683
928
  if getattr(args, "rgb", None):
684
929
  try:
@@ -707,7 +952,8 @@ def render_raster(paths: list[str], args) -> None:
707
952
  # SINGLE BAND
708
953
  else:
709
954
 
710
- band_idx = max(0, min(args.band - 1, band_count - 1))
955
+ band_num = args.band if args.band is not None else 1
956
+ band_idx = max(0, min(band_num - 1, band_count - 1))
711
957
  # print(f"[INFO] Displaying band {band_idx + 1} of {band_count}")
712
958
  raw_band = data[band_idx].astype(float)
713
959
 
@@ -784,7 +1030,7 @@ def render_raster(paths: list[str], args) -> None:
784
1030
  print(f"[ERROR] Cannot display this variable.")
785
1031
  print("[INFO] viewinline only supports 2D or 3D NetCDF variables")
786
1032
  else:
787
- print(f"[ERROR] Raster rendering failed: {e}")
1033
+ print(f"[ERROR] Inline render failed: {e}")
788
1034
 
789
1035
 
790
1036
  def render_gallery(folder: str, grid: str = "4x4", display_scale=None, is_vector=False) -> None:
@@ -889,13 +1135,6 @@ def render_vector(path, args):
889
1135
  except Exception as e:
890
1136
  print(f"[WARN] Could not list layers: {e}")
891
1137
 
892
- # try:
893
- # gdf = gpd.read_file(path, layer=getattr(args, "layer", None))
894
- # print(f"[DATA] Vector loaded: {os.path.basename(path)} ({len(gdf)} features)")
895
- # except Exception as e:
896
- # print(f"[ERROR] Failed to read vector: {e}")
897
- # return
898
-
899
1138
  try:
900
1139
  # Use read_parquet for parquet/geoparquet files
901
1140
  if path.lower().endswith(('.parquet', '.geoparquet')):
@@ -1237,7 +1476,7 @@ def main() -> None:
1237
1476
 
1238
1477
  # Raster options
1239
1478
  parser.add_argument(
1240
- "--band", type=int, default=1,
1479
+ "--band", type=int, default=None,
1241
1480
  help="Band number to display (single raster case), or slice number for NetCDF."
1242
1481
  )
1243
1482
  parser.add_argument(
@@ -1253,6 +1492,10 @@ def main() -> None:
1253
1492
  "--rgb", nargs=3, type=int, metavar=('R', 'G', 'B'), default=None,
1254
1493
  help="Three band numbers for RGB display (e.g., --rgb 4 3 2). Overrides default 1 2 3."
1255
1494
  )
1495
+ parser.add_argument(
1496
+ "--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
1497
+ help="Three single-band rasters for RGB composite (e.g., --rgbfiles R.tif G.tif B.tif). Can also provide as positional arguments without the flag."
1498
+ )
1256
1499
  parser.add_argument(
1257
1500
  "--vmin", type=float, default=None,
1258
1501
  help="Minimum pixel value for raster display scaling."
@@ -1274,8 +1517,9 @@ def main() -> None:
1274
1517
  help="Variable index for NetCDF files (e.g. --subset 1)."
1275
1518
  )
1276
1519
  parser.add_argument(
1277
- "--rgbfiles", nargs=3, type=str, metavar=('R', 'G', 'B'),
1278
- help="Three single-band rasters for RGB composite (e.g., --rgbfiles R.tif G.tif B.tif). Can also provide as positional arguments without the flag."
1520
+ "--reduce", dest="reduce_dim", type=str, default=None,
1521
+ metavar="DIM_NAME",
1522
+ help="For 3D NetCDF variables, specify which dimension to use as the band axis (auto-detected if omitted)."
1279
1523
  )
1280
1524
 
1281
1525
  # CSV options
@@ -1359,7 +1603,7 @@ def main() -> None:
1359
1603
  parser.add_argument(
1360
1604
  "--table", action="store_true",
1361
1605
  help="Display vector/parquet file as tabular data instead of rendering geometry."
1362
- )
1606
+ )
1363
1607
 
1364
1608
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
1365
1609
 
File without changes
File without changes