Skip to content

Commit ec504d1

Browse files
committed
add updates to gdal, qgis, sld, titiler command logic
1 parent 1858ada commit ec504d1

File tree

5 files changed

+206
-311
lines changed

5 files changed

+206
-311
lines changed

src/palettize/cli.py

Lines changed: 154 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@
22

33
"""Command-Line Interface for Palettize, built with Typer."""
44

5-
from typing import Optional, List, Tuple, Dict, Any
5+
import random
6+
import traceback
7+
from pathlib import Path
8+
from typing import Any
9+
610
import typer
711
from rich.console import Console
12+
from rich.panel import Panel
813
from rich.table import Table
9-
from palettize.exporters import list_available_exporters, get_exporter
10-
from palettize.presets import list_available_presets
14+
15+
from palettize import __version__
1116
from palettize.core import Colormap
12-
from palettize.exceptions import PresetNotFoundError, InvalidColorError
17+
from palettize.exceptions import InvalidColorError, PresetNotFoundError
18+
from palettize.exporters import get_exporter, list_available_exporters
19+
from palettize.presets import list_available_presets
1320
from palettize.scaling import get_scaler_by_name
14-
from palettize import __version__
15-
import traceback
16-
from rich.panel import Panel
17-
from pathlib import Path
1821

1922

2023
# --- Helper function to parse comma-separated min,max string ---
21-
def parse_min_max_str(value_str: str, param_name: str) -> Tuple[float, float]:
24+
def parse_min_max_str(value_str: str, param_name: str) -> tuple[float, float]:
2225
"""Parses a string like 'min,max' into a tuple of two floats."""
2326
try:
2427
min_val_str, max_val_str = value_str.split(",")
@@ -53,6 +56,113 @@ def __init__(self):
5356

5457
app_state = AppState()
5558

59+
# --- ASCII Art and Color Quotes for 'info' command ---
60+
61+
# Full ASCII art using block characters for wide terminals (>= 60 chars)
62+
ASCII_ART_FULL = """
63+
███████╗ █████╗ ██╗ ███████╗████████╗████████╗██╗███████╗███████╗
64+
██╔══██╗██╔══██╗██║ ██╔════╝╚══██╔══╝╚══██╔══╝██║╚══███╔╝██╔════╝
65+
██████╔╝███████║██║ █████╗ ██║ ██║ ██║ ███╔╝ █████╗
66+
██╔═══╝ ██╔══██║██║ ██╔══╝ ██║ ██║ ██║ ███╔╝ ██╔══╝
67+
██║ ██║ ██║███████╗███████╗ ██║ ██║ ██║███████╗███████╗
68+
╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝
69+
"""
70+
71+
# Compact ASCII art for narrow terminals (< 60 chars)
72+
ASCII_ART_COMPACT = """
73+
▄▀▀▄ ▄▀▀▄ █ ▄▀▀ ▀▀█▀▀ ▀▀█▀▀ █ ▀▀█ ▄▀▀
74+
█▀▀ █▀▀█ █ █▀▀ █ █ █ ▄▀ █▀▀
75+
█ █ █ █▄▄ █▄▄ █ █ █ █▄▄ █▄▄
76+
"""
77+
78+
COLOR_QUOTES = [
79+
'"Color is a power which directly influences the soul." — Wassily Kandinsky',
80+
'"The purest and most thoughtful minds are those which love color the most." — John Ruskin',
81+
'"Color is the keyboard, the eyes are the harmonies, the soul is the piano." — Wassily Kandinsky',
82+
'"I found I could say things with color and shapes that I couldn\'t say any other way." — Georgia O\'Keeffe',
83+
'"Color does not add a pleasant quality to design — it reinforces it." — Pierre Bonnard',
84+
'"Mere color, unspoiled by meaning, can speak to the soul in a thousand ways." — Oscar Wilde',
85+
'"Colors are the smiles of nature." — Leigh Hunt',
86+
'"The whole world, as we experience it visually, comes to us through the mystic realm of color." — Hans Hofmann',
87+
'"Color is my daylong obsession, joy, and torment." — Claude Monet',
88+
'"Why do two colors, put one next to the other, sing?" — Pablo Picasso',
89+
'"There is no blue without yellow and without orange." — Vincent van Gogh',
90+
'"In nature, light creates the color. In the picture, color creates the light." — Hans Hofmann',
91+
'"What we see is filtered sensory information; perception is shaped by our expectations." — Norwood Russell Hanson',
92+
'"By convention there is color... but in reality there are atoms and the void." — Edward Robert Harrison',
93+
'"Life is about using the whole box of crayons." — RuPaul',
94+
'"Color! What a deep and mysterious language, the language of dreams." — Paul Gauguin'
95+
]
96+
97+
# Colormaps to randomly select from for the banner
98+
BANNER_COLORMAPS = ["turbo", "viridis", "magma", "inferno", "plasma", "cividis", "mako", "rocket"]
99+
100+
101+
def _apply_colormap_gradient_by_column(text: str, colormap: Colormap) -> str:
102+
"""Apply a colormap gradient to text by column position for consistent vertical coloring."""
103+
lines = text.split("\n")
104+
if not lines:
105+
return text
106+
107+
# Find the maximum line length to determine gradient spread
108+
max_len = max(len(line) for line in lines)
109+
if max_len == 0:
110+
return text
111+
112+
result_lines = []
113+
for line in lines:
114+
result_chars = []
115+
for col, char in enumerate(line):
116+
if char == " ":
117+
result_chars.append(" ")
118+
else:
119+
# Get color from colormap based on column position (0-1)
120+
position = col / max_len
121+
hex_color = colormap.get_color(position)
122+
result_chars.append(f"[{hex_color}]{char}[/]")
123+
result_lines.append("".join(result_chars))
124+
125+
return "\n".join(result_lines)
126+
127+
128+
def _render_info_banner(console: Console) -> None:
129+
"""Render the palettize info banner with gradient and information."""
130+
width = console.width or 80
131+
132+
# Select ASCII art based on terminal width
133+
ascii_art = ASCII_ART_FULL if width >= 72 else ASCII_ART_COMPACT
134+
135+
# Select a random colormap for the banner
136+
colormap_name = random.choice(BANNER_COLORMAPS)
137+
colormap = Colormap.from_preset(colormap_name)
138+
139+
# Apply colormap gradient to the ASCII art (by column for vertical consistency)
140+
gradient_art = _apply_colormap_gradient_by_column(ascii_art.strip(), colormap)
141+
console.print(gradient_art)
142+
console.print()
143+
144+
# Display information
145+
console.print(f"[bold]Version:[/bold] {__version__}")
146+
console.print(
147+
"[bold]Description:[/bold] A Python CLI tool for creating and exporting "
148+
"colormaps for data visualization and mapping applications."
149+
)
150+
console.print()
151+
console.print(
152+
"[bold]GitHub:[/bold] [link=https://github.com/kovaca/palettize]"
153+
"https://github.com/kovaca/palettize[/link]"
154+
)
155+
console.print(
156+
"[bold]Docs:[/bold] [link=https://kovaca.github.io/palettize]"
157+
"https://kovaca.github.io/palettize[/link]"
158+
)
159+
console.print()
160+
161+
# Display random quote
162+
quote = random.choice(COLOR_QUOTES)
163+
console.print(f"[dim italic]{quote}[/dim italic]")
164+
165+
56166
app = typer.Typer(
57167
name="palettize",
58168
help="🎨 A Python utility and CLI tool for generating, previewing, and exporting color maps.",
@@ -75,21 +185,21 @@ def verbosity_callback(ctx: typer.Context, param: typer.CallbackParam, value: in
75185
return value
76186

77187

78-
188+
79189

80190

81191
@app.callback()
82192
def global_options(
83193
ctx: typer.Context,
84-
version: Optional[bool] = typer.Option(
194+
version: bool | None = typer.Option(
85195
None,
86196
"--version",
87197
"-V",
88198
callback=version_callback,
89199
is_eager=True,
90200
help="Show the application's version and exit.",
91201
),
92-
verbose: Optional[int] = typer.Option(
202+
verbose: int | None = typer.Option(
93203
0,
94204
"--verbose",
95205
"-v",
@@ -110,11 +220,11 @@ def global_options(
110220

111221

112222
def _create_colormap_from_options(
113-
preset: Optional[str],
114-
colors: Optional[List[str]],
223+
preset: str | None,
224+
colors: list[str] | None,
115225
cut: str,
116226
interpolation_space: str,
117-
name: Optional[str],
227+
name: str | None,
118228
) -> Colormap:
119229
"""Helper to create a Colormap object from common CLI options."""
120230
if colors and preset:
@@ -139,7 +249,7 @@ def _create_colormap_from_options(
139249
console.print(f"[bold red]Error:[/bold red] {e.message}", style="bold red")
140250
raise typer.Exit(code=ExitCodes.USAGE_ERROR)
141251

142-
actual_colors_list: Optional[List[str]] = None
252+
actual_colors_list: list[str] | None = None
143253
if colors:
144254
actual_colors_list = []
145255
for color_item in colors:
@@ -202,9 +312,9 @@ def _create_colormap_from_options(
202312
raise typer.Exit(code=ExitCodes.UNEXPECTED_ERROR)
203313

204314

205-
def _parse_exporter_options(options: List[str]) -> Dict[str, Any]:
315+
def _parse_exporter_options(options: list[str]) -> dict[str, Any]:
206316
"""Parses repeatable -O/--option flags into a dictionary."""
207-
parsed_options: Dict[str, Any] = {"_global": {}}
317+
parsed_options: dict[str, Any] = {"_global": {}}
208318
for option_str in options:
209319
if "=" not in option_str:
210320
console.print(
@@ -228,7 +338,7 @@ def _parse_exporter_options(options: List[str]) -> Dict[str, Any]:
228338

229339
# --- Helper function for terminal rendering ---
230340
def _render_colormap_to_terminal(
231-
console: Console, colormap_obj: Colormap, width: Optional[int], height: int
341+
console: Console, colormap_obj: Colormap, width: int | None, height: int
232342
):
233343
"""Renders the given Colormap object to the terminal."""
234344
console_width = console.width if console.width is not None else 80
@@ -278,21 +388,30 @@ def list_presets_command():
278388
console.print(table)
279389

280390

391+
# --- 'info' command ---
392+
393+
394+
@app.command()
395+
def info():
396+
"""Display information about Palettize with a colorful banner."""
397+
_render_info_banner(console)
398+
399+
281400
# --- Top-level commands ---
282401

283402

284403
@app.command()
285404
def show(
286-
preset_name: Optional[str] = typer.Argument(
405+
preset_name: str | None = typer.Argument(
287406
None, help="Name of a preset palette (e.g., 'viridis')."
288407
),
289-
colors: Optional[List[str]] = typer.Option(
408+
colors: list[str] | None = typer.Option(
290409
None,
291410
"--colors",
292411
"-c",
293412
help="List of input colors (e.g., 'red,blue', '#ff0000'). Use multiple times or comma-separate.",
294413
),
295-
width: Optional[int] = typer.Option(
414+
width: int | None = typer.Option(
296415
None,
297416
"--width",
298417
"-w",
@@ -307,7 +426,7 @@ def show(
307426
cut: str = typer.Option(
308427
"0,1", "--cut", help="Sub-segment of the colormap to use, e.g., '0.2,0.8'."
309428
),
310-
name: Optional[str] = typer.Option(
429+
name: str | None = typer.Option(
311430
None, "--name", help="Set a display name for the colormap."
312431
),
313432
):
@@ -324,19 +443,19 @@ def show(
324443

325444
@app.command()
326445
def create(
327-
preset_name: Optional[str] = typer.Argument(
446+
preset_name: str | None = typer.Argument(
328447
None, help="Name of a preset palette to export (e.g., 'viridis')."
329448
),
330-
colors: Optional[List[str]] = typer.Option(
449+
colors: list[str] | None = typer.Option(
331450
None, "--colors", "-c", help="List of input colors to create a colormap from."
332451
),
333-
formats: List[str] = typer.Option(
452+
formats: list[str] = typer.Option(
334453
...,
335454
"--format",
336455
"-f",
337456
help="One or more export format identifiers (e.g., 'gdal,qgis').",
338457
),
339-
output: Optional[str] = typer.Option(
458+
output: str | None = typer.Option(
340459
None,
341460
"--output",
342461
"-o",
@@ -348,23 +467,23 @@ def create(
348467
scale: str = typer.Option(
349468
"linear", "--scale", help="Scaling type: linear, power, sqrt, log, symlog."
350469
),
351-
scale_exponent: Optional[float] = typer.Option(
470+
scale_exponent: float | None = typer.Option(
352471
None, "--scale-exponent", help="Exponent for 'power' scale."
353472
),
354-
scale_log_base: Optional[float] = typer.Option(
473+
scale_log_base: float | None = typer.Option(
355474
None, "--scale-log-base", help="Log base for 'log'/'symlog' scales."
356475
),
357-
scale_symlog_linthresh: Optional[float] = typer.Option(
476+
scale_symlog_linthresh: float | None = typer.Option(
358477
None, "--scale-symlog-linthresh", help="Linear threshold for 'symlog' scale."
359478
),
360-
steps: Optional[int] = typer.Option(
479+
steps: int | None = typer.Option(
361480
7,
362481
"--steps",
363482
"-n",
364483
min=2,
365484
help="Number of discrete color steps for ramp outputs.",
366485
),
367-
precision: Optional[int] = typer.Option(
486+
precision: int | None = typer.Option(
368487
None, "--precision", min=0, help="Decimal places for numeric values in output."
369488
),
370489
interpolation_space: str = typer.Option(
@@ -373,10 +492,10 @@ def create(
373492
cut: str = typer.Option(
374493
"0,1", "--cut", help="Sub-segment of the colormap to use, e.g., '0.2,0.8'."
375494
),
376-
name: Optional[str] = typer.Option(
495+
name: str | None = typer.Option(
377496
None, "--name", help="Name for the colormap, used in file naming and output."
378497
),
379-
option: Optional[List[str]] = typer.Option(
498+
option: list[str] | None = typer.Option(
380499
None,
381500
"--option",
382501
"-O",
@@ -482,6 +601,7 @@ def create(
482601
final_options["precision"] = precision
483602
final_options["name"] = base_colormap_name
484603
final_options["scale_type"] = scale
604+
final_options["verbose"] = app_state.verbose_level > 0
485605

486606
try:
487607
output_str = exporter.export(

0 commit comments

Comments
 (0)