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+
610import typer
711from rich .console import Console
12+ from rich .panel import Panel
813from 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__
1116from 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
1320from 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
5457app_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+
56166app = 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 ()
82192def 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
112222def _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 ---
230340def _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 ()
285404def 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 ()
326445def 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