viewinline 0.1.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.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: viewinline
3
+ Version: 0.1.0
4
+ Summary: Quick look geospatial viewer for iTerm2 and ANSI 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: MIT
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: geopandas
12
+ Requires-Dist: matplotlib
13
+ Requires-Dist: numpy
14
+ Requires-Dist: pillow
15
+ Requires-Dist: pyogrio
16
+ Requires-Dist: rasterio
17
+ Description-Content-Type: text/markdown
18
+
19
+ # viewinline
20
+ **Quick-look geospatial viewer for iTerm2.**
21
+ Displays rasters and vectors directly in the terminal — no GUI, no temporary files.
22
+
23
+ This tool combines the core display logic of `viewtif` and `viewgeom`, but is **non-interactive**:
24
+ 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).
25
+
26
+ ```bash
27
+ viewinline path/to/file.tif
28
+ viewinline path/to/vector.geojson
29
+ viewinline R.tif G.tif B.tif # RGB composite
30
+ ```
31
+ It’s designed for iTerm2 on macOS, using its inline image protocol to render a preview.
32
+
33
+ ### Dependencies
34
+ Requires Python 3.9 or later and the following libraries:
35
+ ```bash
36
+ pip install numpy pillow rasterio geopandas matplotlib pyogrio
37
+ ```
38
+
39
+ ### Available options
40
+ ```bash
41
+ --display DISPLAY # resize the displayed image (0.5=smaller, 2=bigger). default: auto fit to terminal
42
+ --ansi-size ANSI_SIZE # set resolution if you are viewing the ANSI preview (try 180x90 or 200x100)
43
+ --band BAND # band number to display (single raster case). default: 1
44
+ --colormap [{viridis,inferno,magma,plasma,cividis,terrain,RdYlGn,coolwarm,Spectral,cubehelix,tab10,turbo}]
45
+ # apply a colormap to single band rasters or vector coloring
46
+ # flag without value uses 'terrain' by default
47
+ --color-by COLOR_BY # select a numeric column to color vector features
48
+ --edgecolor EDGECOLOR # edge color for vector outlines (hex or named color). default: #F6FF00
49
+ --layer LAYER # layer name for GeoPackage or multi layer files
50
+ ```
51
+
52
+ ### ANSI/ASCII color preview
53
+ If iTerm2 isn’t available, viewinline will automatically switch an
54
+ ANSI/ASCII color preview or save a quick PNG under /tmp/viewinline_preview.png.
55
+
56
+ This mode works on terminals with **ANSI color support** and may not display correctly on others.
57
+ For compatible terminals, `viewinline` renders images in a very coarse resolution. This feature is experimental.
58
+
59
+ ## License
60
+ This project is released under the MIT License © 2025 Keiko Nomura
61
+ If you find this tool useful, please consider supporting or acknowledging the author.
@@ -0,0 +1,43 @@
1
+ # viewinline
2
+ **Quick-look geospatial viewer for iTerm2.**
3
+ Displays rasters and vectors directly in the terminal — no GUI, no temporary files.
4
+
5
+ This tool combines the core display logic of `viewtif` and `viewgeom`, but is **non-interactive**:
6
+ 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).
7
+
8
+ ```bash
9
+ viewinline path/to/file.tif
10
+ viewinline path/to/vector.geojson
11
+ viewinline R.tif G.tif B.tif # RGB composite
12
+ ```
13
+ It’s designed for iTerm2 on macOS, using its inline image protocol to render a preview.
14
+
15
+ ### Dependencies
16
+ Requires Python 3.9 or later and the following libraries:
17
+ ```bash
18
+ pip install numpy pillow rasterio geopandas matplotlib pyogrio
19
+ ```
20
+
21
+ ### Available options
22
+ ```bash
23
+ --display DISPLAY # resize the displayed image (0.5=smaller, 2=bigger). default: auto fit to terminal
24
+ --ansi-size ANSI_SIZE # set resolution if you are viewing the ANSI preview (try 180x90 or 200x100)
25
+ --band BAND # band number to display (single raster case). default: 1
26
+ --colormap [{viridis,inferno,magma,plasma,cividis,terrain,RdYlGn,coolwarm,Spectral,cubehelix,tab10,turbo}]
27
+ # apply a colormap to single band rasters or vector coloring
28
+ # flag without value uses 'terrain' by default
29
+ --color-by COLOR_BY # select a numeric column to color vector features
30
+ --edgecolor EDGECOLOR # edge color for vector outlines (hex or named color). default: #F6FF00
31
+ --layer LAYER # layer name for GeoPackage or multi layer files
32
+ ```
33
+
34
+ ### ANSI/ASCII color preview
35
+ If iTerm2 isn’t available, viewinline will automatically switch an
36
+ ANSI/ASCII color preview or save a quick PNG under /tmp/viewinline_preview.png.
37
+
38
+ This mode works on terminals with **ANSI color support** and may not display correctly on others.
39
+ For compatible terminals, `viewinline` renders images in a very coarse resolution. This feature is experimental.
40
+
41
+ ## License
42
+ This project is released under the MIT License © 2025 Keiko Nomura
43
+ If you find this tool useful, please consider supporting or acknowledging the author.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "viewinline"
7
+ version = "0.1.0"
8
+ description = "Quick look geospatial viewer for iTerm2 and ANSI compatible terminals"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "Keiko Nomura" }
13
+ ]
14
+ requires-python = ">=3.9"
15
+ dependencies = [
16
+ "numpy",
17
+ "pillow",
18
+ "rasterio",
19
+ "geopandas",
20
+ "matplotlib",
21
+ "pyogrio"
22
+ ]
23
+
24
+ [project.scripts]
25
+ viewinline = "viewinline.viewinline:main"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/viewinline"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/nkeikon/viewinline"
32
+ Repository = "https://github.com/nkeikon/viewinline"
33
+ Issues = "https://github.com/nkeikon/viewinline/issues"
34
+
File without changes
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ viewinline — quick-look geospatial viewer for iTerm2 / ANSI/ASCII preview.
4
+
5
+ Supports:
6
+ • Rasters (.tif, .tiff)
7
+ • Vectors (.shp, .geojson, .gpkg)
8
+ • ANSI color preview in text-only terminals (half-block resolution)
9
+
10
+ Notes:
11
+ - iTerm2 inline images require ITERM_SESSION_ID.
12
+ - In HPC/text-only shells, switches to ANSI color preview.
13
+ """
14
+
15
+ import sys, os, base64, shutil, argparse
16
+ from io import BytesIO
17
+ import numpy as np
18
+ from PIL import Image, ImageOps
19
+ from matplotlib import colormaps
20
+ import warnings
21
+
22
+ warnings.filterwarnings("ignore", message="More than one layer found", category=UserWarning)
23
+
24
+ AVAILABLE_COLORMAPS = [
25
+ "viridis", "inferno", "magma", "plasma",
26
+ "cividis", "terrain", "RdYlGn", "coolwarm",
27
+ "Spectral", "cubehelix", "tab10", "turbo"
28
+ ]
29
+
30
+ # ---------------------------------------------------------------------
31
+ # Display utilities
32
+ # ---------------------------------------------------------------------
33
+ def show_inline_image(image_array: np.ndarray) -> None:
34
+ """Display a numpy RGB image inline in iTerm2."""
35
+ try:
36
+ buffer = BytesIO()
37
+ Image.fromarray(image_array).save(buffer, format="PNG")
38
+ encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
39
+ # sys.stdout.write(f"\033]1337;File=inline=1:{encoded}\a\n")
40
+ sys.stdout.write(f"\033]1337;File=inline=1;width=33%:{encoded}\a\n") #1/3 of window size
41
+ sys.stdout.flush()
42
+ except Exception as e:
43
+ print(f"[WARN] Inline display failed ({e})")
44
+
45
+
46
+ def show_ansi_preview(image_array: np.ndarray, width: int = 120, height: int = 60) -> None:
47
+ """ANSI preview using half-block characters (▀)."""
48
+ try:
49
+ img = Image.fromarray(image_array).resize((width, height * 2), Image.BILINEAR)
50
+ arr = np.array(img)
51
+ # for y in range(0, arr.shape[0] - 1, 2):
52
+ # top = arr[y]
53
+ # bottom = arr[y + 1]
54
+ # line = []
55
+ # for (r1, g1, b1), (r2, g2, b2) in zip(top, bottom):
56
+ # line.append(f"\033[38;2;{r1};{g1};{b1}m\033[48;2;{r2};{g2};{b2}m▀")
57
+ # print("".join(line) + "\033[0m")
58
+ for y in range(0, arr.shape[0] - 1, 2):
59
+ top, bottom = arr[y], arr[y + 1]
60
+ line = "".join(
61
+ f"\033[38;2;{r1};{g1};{b1}m\033[48;2;{r2};{g2};{b2}m▀"
62
+ for (r1, g1, b1), (r2, g2, b2) in zip(top, bottom)
63
+ )
64
+ print(f"{line}\033[0m")
65
+
66
+ # sys.stdout.flush()
67
+ print("[OK] ANSI preview displayed.")
68
+ except Exception as e:
69
+ print(f"[WARN] ANSI preview failed ({e}); saving file...")
70
+ save_image_to_tmp(image_array)
71
+
72
+
73
+ def save_image_to_tmp(image_array: np.ndarray) -> str:
74
+ """Save to /tmp and print file path."""
75
+ outfile = "/tmp/viewinline_preview.png"
76
+ Image.fromarray(image_array).save(outfile)
77
+ print(f"[WARN] Inline not supported — saved preview to {outfile}")
78
+ return outfile
79
+
80
+
81
+ def resize_to_terminal(img: np.ndarray) -> tuple[np.ndarray, float]:
82
+ """Resize image to fit terminal window (approx 8x16 pixel cells)."""
83
+ cols, rows = shutil.get_terminal_size((100, 40))
84
+ max_w = cols * 8
85
+ max_h = rows * 16
86
+ h, w = img.shape[:2]
87
+ scale = min(max_w / w, max_h / h, 1.0)
88
+ new_w, new_h = max(1, int(w * scale)), max(1, int(h * scale))
89
+ pil_img = Image.fromarray(img)
90
+ pil_img = ImageOps.contain(pil_img, (new_w, new_h))
91
+ return np.array(pil_img), scale
92
+
93
+
94
+ # ---------------------------------------------------------------------
95
+ # Raster handling
96
+ # ---------------------------------------------------------------------
97
+ def normalize_to_uint8(band: np.ndarray) -> np.ndarray:
98
+ band = band.astype(float)
99
+ valid = np.isfinite(band)
100
+ if not np.any(valid):
101
+ return np.zeros_like(band, dtype=np.uint8)
102
+ mn, mx = np.percentile(
103
+ band[valid] if band[valid].size < 1_000_000 else np.random.choice(band[valid], 1_000_000, replace=False),
104
+ (2, 98)
105
+ )
106
+ if mx <= mn:
107
+ return np.zeros_like(band, dtype=np.uint8)
108
+ band = np.clip((band - mn) / (mx - mn), 0, 1)
109
+ band[~valid] = 0
110
+ return (band * 255).astype(np.uint8)
111
+
112
+
113
+ def render_raster(paths: list[str], args) -> None:
114
+ try:
115
+ import rasterio
116
+ import rasterio.enums
117
+ except ImportError:
118
+ print("[ERROR] rasterio not installed. Please install with `pip install rasterio`.")
119
+ return
120
+
121
+ try:
122
+ if len(paths) == 1:
123
+ with rasterio.open(paths[0]) as ds:
124
+ H, W = ds.height, ds.width
125
+ print(f"[DATA] Raster loaded: {os.path.basename(paths[0])} ({W}×{H})")
126
+
127
+ max_dim = 2000
128
+ if max(H, W) > max_dim:
129
+ scale = max_dim / max(H, W)
130
+ out_h, out_w = int(H * scale), int(W * scale)
131
+ data = ds.read(
132
+ out_shape=(ds.count, out_h, out_w),
133
+ resampling=rasterio.enums.Resampling.bilinear
134
+ )
135
+ print(f"[PROC] Downsampled → {out_w}×{out_h}px (scale={scale:.3f})")
136
+ else:
137
+ data = ds.read()
138
+
139
+ band_idx = max(0, min(args.band - 1, data.shape[0] - 1))
140
+ band = normalize_to_uint8(data[band_idx])
141
+ print(f"[INFO] Displaying band {band_idx + 1} of {data.shape[0]}")
142
+
143
+ # Grayscale default
144
+ if args.colormap:
145
+ cmap_name = args.colormap or "terrain"
146
+ cmap = colormaps[cmap_name]
147
+ colored = cmap(band / 255.0)
148
+ img = (colored[:, :, :3] * 255).astype(np.uint8)
149
+ print(f"[INFO] Applying colormap: {cmap_name}")
150
+ else:
151
+ img = np.stack([band] * 3, axis=-1)
152
+ print("[INFO] Displaying in grayscale (no colormap applied)")
153
+
154
+ elif len(paths) == 3:
155
+ bands = []
156
+ for p in paths:
157
+ with rasterio.open(p) as ds:
158
+ bands.append(ds.read(1))
159
+ shapes = {b.shape for b in bands}
160
+ if len(shapes) != 1:
161
+ print("[ERROR] Raster sizes do not match.")
162
+ return
163
+ data = np.stack(bands, axis=0)
164
+ H, W = data.shape[1:]
165
+ print(f"[DATA] RGB raster stack loaded: {W}×{H}")
166
+ img = np.stack([normalize_to_uint8(b) for b in data], axis=-1)
167
+ print("[INFO] Displaying 3-band RGB composite")
168
+
169
+ else:
170
+ print("[ERROR] Provide one raster or exactly three rasters for RGB.")
171
+ return
172
+
173
+ # Resize for terminal
174
+ H, W = img.shape[:2]
175
+ if args.display:
176
+ new_w, new_h = max(1, int(W * args.display)), max(1, int(H * args.display))
177
+ img = np.array(Image.fromarray(img).resize((new_w, new_h), Image.BILINEAR))
178
+ print(f"[VIEW] Manual resize ×{args.display:.2f} → {new_w}×{new_h}px")
179
+ else:
180
+ img, scale = resize_to_terminal(img)
181
+ print(f"[VIEW] Auto-fit display → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
182
+
183
+ show_image_auto(img, args)
184
+
185
+ except Exception as e:
186
+ print(f"[ERROR] Raster rendering failed: {e}")
187
+
188
+
189
+ # ---------------------------------------------------------------------
190
+ # Vector handling
191
+ # ---------------------------------------------------------------------
192
+ def render_vector(path, args):
193
+ try:
194
+ import geopandas as gpd
195
+ import matplotlib.pyplot as plt
196
+ from pyogrio import list_layers
197
+ except ImportError as e:
198
+ print("[ERROR] Missing dependency. Install with:")
199
+ print(" pip install geopandas matplotlib pyogrio")
200
+ return
201
+
202
+ try:
203
+ # layers = list_layers(path)
204
+ if path.lower().endswith((".shp", ".geojson", ".json", ".parquet", ".geoparquet")):
205
+ # Common single-layer formats — skip list_layers() call
206
+ layers = [(os.path.splitext(os.path.basename(path))[0], None)]
207
+ else:
208
+ layers = list_layers(path)
209
+ if len(layers) > 1 and not getattr(args, "layer", None):
210
+ print(f"[INFO] Multiple layers found in '{os.path.basename(path)}':")
211
+ for i, lyr in enumerate(layers, 1):
212
+ name = lyr[0]
213
+ geom = lyr[1] if len(lyr) > 1 and lyr[1] else "Unknown"
214
+ print(f" {i}. {name} ({geom})")
215
+ first = layers[0][0]
216
+ print(f"[INFO] Defaulting to first layer: '{first}' (use --layer <name> to select another).")
217
+ args.layer = first
218
+ except Exception as e:
219
+ print(f"[WARN] Could not list layers: {e}")
220
+
221
+ try:
222
+ gdf = gpd.read_file(path, layer=getattr(args, "layer", None))
223
+ print(f"[DATA] Vector loaded: {os.path.basename(path)} ({len(gdf)} features)")
224
+ except Exception as e:
225
+ print(f"[ERROR] Failed to read vector: {e}")
226
+ return
227
+
228
+ # Detect numeric columns
229
+ num_cols = []
230
+ for c in gdf.columns:
231
+ if c == gdf.geometry.name:
232
+ continue
233
+ try:
234
+ if np.issubdtype(gdf[c].dtype, np.number):
235
+ num_cols.append(c)
236
+ except TypeError:
237
+ continue
238
+
239
+ if num_cols:
240
+ print("[INFO] Numeric columns detected:", ", ".join(num_cols))
241
+ if not args.color_by:
242
+ print("[INFO] Showing border-only view (use --color-by <column> to color by numeric values).")
243
+ else:
244
+ print("[INFO] Displaying boundaries only - no numeric columns detected")
245
+
246
+ # Figure setup (black background)
247
+ fig, ax = plt.subplots(figsize=(6, 6), dpi=150, facecolor="gray")
248
+ ax.set_facecolor("gray")
249
+ ax.set_axis_off()
250
+
251
+ # Determine colormap
252
+ column = args.color_by if args.color_by in gdf.columns else None
253
+
254
+ # Warn if user provided an invalid column
255
+ if args.color_by and args.color_by not in gdf.columns:
256
+ print(f"[WARN] Column '{args.color_by}' not found. Showing border-only view.")
257
+ column = None
258
+
259
+ if column and args.colormap is None:
260
+ args.colormap = "terrain"
261
+ print("[INFO] Applying default colormap: terrain")
262
+
263
+ cmap = colormaps.get(args.colormap) if args.colormap else None
264
+
265
+ # Plot
266
+ try:
267
+ if column and np.issubdtype(gdf[column].dtype, np.number):
268
+ vmin, vmax = np.percentile(gdf[column].dropna(), (2, 98))
269
+ print(f"[INFO] Coloring by '{column}' (range: {vmin:.2f}–{vmax:.2f})")
270
+ gdf.plot(ax=ax, column=column, cmap=cmap, vmin=vmin, vmax=vmax,
271
+ linewidth=0.3, edgecolor="black", zorder=1)
272
+ else:
273
+ gdf.plot(ax=ax, facecolor="none", edgecolor=args.edgecolor,
274
+ linewidth=0.7, zorder=1)
275
+ except Exception as e:
276
+ print(f"[WARN] Plotting failed ({e}) — fallback to border-only.")
277
+ gdf.plot(ax=ax, facecolor="none", edgecolor="gray", linewidth=0.5)
278
+
279
+ # Save to buffer (adaptive DPI)
280
+ render_dpi = 200 if "ITERM_SESSION_ID" in os.environ else 400
281
+ buf = BytesIO()
282
+ fig.savefig(buf, format="png", dpi=render_dpi,
283
+ bbox_inches="tight", pad_inches=0.05,
284
+ facecolor=fig.get_facecolor())
285
+ plt.close(fig)
286
+ buf.seek(0)
287
+ img = np.array(Image.open(buf).convert("RGB"))
288
+ # print(f"[PROC] Rendered vector") # (DPI={render_dpi})
289
+
290
+ img, scale = resize_to_terminal(img)
291
+ print(f"[VIEW] Auto-fit display → {img.shape[1]}×{img.shape[0]}px (size={scale:.2f})")
292
+
293
+ show_image_auto(img, args)
294
+
295
+
296
+ # ---------------------------------------------------------------------
297
+ # Smart display selector
298
+ # ---------------------------------------------------------------------
299
+ def show_image_auto(img: np.ndarray, args) -> None:
300
+ """Automatically pick best display method."""
301
+ if "ITERM_SESSION_ID" in os.environ:
302
+ try:
303
+ show_inline_image(img)
304
+ print("[OK] Inline render complete.")
305
+ return
306
+ except Exception:
307
+ print("[WARN] Inline display failed; trying ANSI fallback...")
308
+
309
+ if sys.stdout.isatty(): #and os.getenv("TERM"):
310
+ try:
311
+ w, h = (120, 60)
312
+ mode = "auto"
313
+ if getattr(args, "ansi_size", None):
314
+ try:
315
+ w, h = map(int, args.ansi_size.lower().split("x"))
316
+ mode = f"ansi-size {w}x{h}"
317
+ except Exception:
318
+ pass
319
+ elif getattr(args, "display", None):
320
+ w = max(1, int(w * args.display))
321
+ h = max(1, int(h * args.display))
322
+ mode = f"display size {args.display:.2f}"
323
+ print(f"[VIEW] ANSI display → {w}×{h} grid ({mode})")
324
+ show_ansi_preview(img, width=w, height=h)
325
+ return
326
+ except Exception as e:
327
+ print(f"[WARN] ANSI preview failed ({e}); saving file...")
328
+
329
+ save_image_to_tmp(img)
330
+
331
+ # ---------------------------------------------------------------------
332
+ # Smart help formatter to hide None defaults
333
+ # ---------------------------------------------------------------------
334
+ import argparse
335
+
336
+ class SmartDefaults(argparse.ArgumentDefaultsHelpFormatter):
337
+ """Show defaults only when meaningful (not None or SUPPRESS)."""
338
+ def _get_help_string(self, action):
339
+ if action.help and "%(default)" in action.help:
340
+ return action.help
341
+ if action.default is None or action.default is argparse.SUPPRESS:
342
+ return action.help
343
+ return super()._get_help_string(action)
344
+
345
+ # ---------------------------------------------------------------------
346
+ # Dispatcher
347
+ # ---------------------------------------------------------------------
348
+ def main() -> None:
349
+ parser = argparse.ArgumentParser(
350
+ prog="viewinline",
351
+ description=(
352
+ "Quick-look geospatial viewer for iTerm2 and headless environments.\n\n"
353
+ "Supports rasters (.tif, .tiff) and vectors (.shp, .geojson, .gpkg).\n"
354
+ "Displays inline in iTerm2 if available, otherwise as ANSI color preview."
355
+ ),
356
+ formatter_class=SmartDefaults
357
+ )
358
+
359
+ # File input
360
+ parser.add_argument("paths", nargs="+",
361
+ help="Path to raster(s) or vector file. Provide 1 file or exactly 3 rasters for RGB (R G B).")
362
+
363
+ # Display options
364
+ parser.add_argument("--display", type=float, default=None,
365
+ help="Resize only the displayed image (0.5=smaller, 2=bigger). Default: auto-fit to terminal.")
366
+ parser.add_argument("--ansi-size", type=str, default=None,
367
+ help="ANSI fallback resolution. Try 180x90 or 200x100.")
368
+
369
+ # Raster options
370
+ parser.add_argument("--band", type=int, default=1,
371
+ help="Band number to display (single raster case).")
372
+ parser.add_argument("--colormap", nargs="?", const="terrain",
373
+ choices=AVAILABLE_COLORMAPS, default=None,
374
+ help="Apply colormap to single-band rasters or vector coloring. Flag without value → 'terrain'.")
375
+
376
+ # Vector options
377
+ parser.add_argument("--color-by", type=str, default=None,
378
+ help="Numeric column to color vector features by (optional).")
379
+ parser.add_argument("--edgecolor", type=str, default="#F6FF00",
380
+ help="Edge color for vector outlines (hex or named color).")
381
+ parser.add_argument("--layer", type=str, default=None,
382
+ help="Layer name for GeoPackage or multi-layer files.")
383
+
384
+ args = parser.parse_args()
385
+
386
+ # --- Basic argument sanity check ---
387
+ for bad in ("color-by", "edgecolor", "colormap", "band", "display"):
388
+ for a in args.paths:
389
+ if a == bad:
390
+ print(f"[ERROR] Missing '--' before '{bad}'.")
391
+ print(" Example: --color-by column_name")
392
+ sys.exit(1)
393
+
394
+ paths = args.paths
395
+
396
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
397
+
398
+ if "iterm" not in term_program:
399
+ print("[WARN] iTerm2 not detected. For better inline image display, use iTerm2 (mac).")
400
+ print("[INFO] Switching to ANSI/ASCII preview mode. This may not display correctly on all terminals.")
401
+
402
+ try:
403
+ ans = input("Continue with ANSI/ASCII preview? [y/N]: ").strip().lower()
404
+ except EOFError:
405
+ ans = "n"
406
+
407
+ if ans not in ("y", "yes"):
408
+ print("Cancelled by user.")
409
+ sys.exit(0)
410
+
411
+ raster_exts = (".tif", ".tiff")
412
+ vector_exts = (".shp", ".geojson", ".json", ".gpkg")
413
+
414
+ if len(paths) == 1:
415
+ p = paths[0].lower()
416
+ if p.endswith(raster_exts):
417
+ render_raster(paths, args)
418
+ elif p.endswith(vector_exts):
419
+ render_vector(paths[0], args)
420
+ else:
421
+ print("[ERROR] Unsupported file type.")
422
+ sys.exit(1)
423
+ elif len(paths) == 3 and all(p.lower().endswith(raster_exts) for p in paths):
424
+ render_raster(paths, args)
425
+ else:
426
+ print("[ERROR] Provide one raster/vector file or three rasters for RGB.")
427
+ sys.exit(1)
428
+
429
+
430
+ if __name__ == "__main__":
431
+ main()