tr
This commit is contained in:
@@ -8,7 +8,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, Dict, List, Optional, Sequence
|
from typing import Any, Dict, List, Optional, Sequence
|
||||||
|
|
||||||
from rich.console import Console
|
from rich import box
|
||||||
|
from rich.console import Console, Group
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
@@ -92,32 +93,63 @@ class Renderer:
|
|||||||
|
|
||||||
# Internal renderers -------------------------------------------------
|
# Internal renderers -------------------------------------------------
|
||||||
def _render_card(self, card: Card) -> None:
|
def _render_card(self, card: Card) -> None:
|
||||||
suit = getattr(card, "suit", None)
|
"""Render a card in a rectangular layout with top/middle/bottom bars."""
|
||||||
lines: List[str] = [f"Arcana: {card.arcana}"]
|
|
||||||
if suit is not None and getattr(suit, "name", None):
|
name = getattr(card, "name", "")
|
||||||
lines.append(f"Suit: {suit.name}")
|
arcana = getattr(card, "arcana", "")
|
||||||
if getattr(card, "pip", 0):
|
suit_obj = getattr(card, "suit", None)
|
||||||
lines.append(f"Pip: {card.pip}")
|
suit = getattr(suit_obj, "name", "") if suit_obj is not None else ""
|
||||||
if getattr(card, "court_rank", ""):
|
pip = getattr(card, "pip", None)
|
||||||
lines.append(f"Court Rank: {card.court_rank}")
|
number = getattr(card, "number", "")
|
||||||
|
keywords = getattr(card, "keywords", None) or []
|
||||||
meaning = getattr(card, "meaning", None)
|
meaning = getattr(card, "meaning", None)
|
||||||
if meaning is not None:
|
|
||||||
if getattr(meaning, "upright", None):
|
top = Table.grid(expand=True)
|
||||||
lines.append(f"Upright: {meaning.upright}")
|
for align in ("left", "center", "right"):
|
||||||
if getattr(meaning, "reversed", None):
|
top.add_column(justify=align)
|
||||||
lines.append(f"Reversed: {meaning.reversed}")
|
top_left = suit or arcana
|
||||||
keywords = getattr(card, "keywords", None)
|
top_center = name
|
||||||
|
top_right = f"#{number}" if number not in (None, "") else ""
|
||||||
|
top.add_row(top_left, top_center, top_right)
|
||||||
|
|
||||||
|
body_lines = [f"Arcana: {arcana or '-'}"]
|
||||||
|
if suit:
|
||||||
|
body_lines.append(f"Suit: {suit}")
|
||||||
|
if pip:
|
||||||
|
body_lines.append(f"Pip: {pip}")
|
||||||
|
if getattr(card, "court_rank", ""):
|
||||||
|
body_lines.append(f"Court Rank: {card.court_rank}")
|
||||||
if keywords:
|
if keywords:
|
||||||
lines.append(f"Keywords: {', '.join(keywords)}")
|
body_lines.append(f"Keywords: {', '.join(keywords)}")
|
||||||
reversed_keywords = getattr(card, "reversed_keywords", None)
|
|
||||||
if reversed_keywords:
|
|
||||||
lines.append(f"Reversed Keywords: {', '.join(reversed_keywords)}")
|
|
||||||
guidance = getattr(card, "guidance", None)
|
guidance = getattr(card, "guidance", None)
|
||||||
if guidance:
|
if guidance:
|
||||||
lines.append(f"Guidance: {guidance}")
|
body_lines.append(f"Guidance: {guidance}")
|
||||||
self.console.print(
|
body = Panel("\n".join(body_lines), box=box.MINIMAL, padding=(1, 1))
|
||||||
Panel("\n".join(lines), title=f"{card.number}: {card.name}", expand=False)
|
|
||||||
|
bottom = Table(
|
||||||
|
show_header=False,
|
||||||
|
expand=True,
|
||||||
|
box=box.SIMPLE_HEAVY,
|
||||||
|
show_edge=True,
|
||||||
|
pad_edge=False,
|
||||||
)
|
)
|
||||||
|
bottom.add_column(justify="left")
|
||||||
|
bottom.add_column(justify="center")
|
||||||
|
bottom.add_column(justify="right")
|
||||||
|
bottom.add_row(
|
||||||
|
self._label_block("Upright", getattr(meaning, "upright", None)),
|
||||||
|
self._label_block("Reversed", getattr(meaning, "reversed", None)),
|
||||||
|
self._label_block("Keywords", ", ".join(keywords) if keywords else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
card_panel = Panel(
|
||||||
|
Group(top, body, bottom),
|
||||||
|
box=box.SQUARE,
|
||||||
|
padding=0,
|
||||||
|
title=name,
|
||||||
|
expand=False,
|
||||||
|
)
|
||||||
|
self.console.print(card_panel)
|
||||||
|
|
||||||
def _render_object(self, obj: Any) -> None:
|
def _render_object(self, obj: Any) -> None:
|
||||||
attrs = {k: v for k, v in vars(obj).items() if not k.startswith("_")}
|
attrs = {k: v for k, v in vars(obj).items() if not k.startswith("_")}
|
||||||
@@ -255,6 +287,11 @@ class Renderer:
|
|||||||
def _humanize_key(key: str) -> str:
|
def _humanize_key(key: str) -> str:
|
||||||
return key.replace("_", " ")
|
return key.replace("_", " ")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _label_block(label: str, value: Optional[str]) -> str:
|
||||||
|
safe_val = value if value not in (None, "") else "—"
|
||||||
|
return f"{label}:\n{safe_val}"
|
||||||
|
|
||||||
|
|
||||||
class PlanetRenderer:
|
class PlanetRenderer:
|
||||||
"""Type-specific table rendering for planets."""
|
"""Type-specific table rendering for planets."""
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ deck = Deck()
|
|||||||
cards = Tarot.deck.card.filter(suit="cups",type="ace")
|
cards = Tarot.deck.card.filter(suit="cups",type="ace")
|
||||||
|
|
||||||
print(cards)
|
print(cards)
|
||||||
# Display using default deck
|
# Display using default deck
|
||||||
|
display_cards(cards)
|
||||||
|
)
|
||||||
@@ -242,12 +242,17 @@ def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
|||||||
"""
|
"""
|
||||||
Draw cards for all positions in a spread.
|
Draw cards for all positions in a spread.
|
||||||
|
|
||||||
|
Ensures all drawn cards are unique (no duplicates in a single spread).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spread: The Spread object with positions defined
|
spread: The Spread object with positions defined
|
||||||
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
|
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of DrawnCard objects (one per position) with random cards and reversals
|
List of DrawnCard objects (one per position) with random cards and reversals
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If spread has more positions than cards in the deck
|
||||||
"""
|
"""
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -257,10 +262,18 @@ def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
|||||||
deck_instance = Deck()
|
deck_instance = Deck()
|
||||||
deck = deck_instance.cards
|
deck = deck_instance.cards
|
||||||
|
|
||||||
|
# Validate that we have enough cards to draw from without duplicates
|
||||||
|
num_positions = len(spread.positions)
|
||||||
|
if num_positions > len(deck):
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw unique cards using random.sample (no replacements)
|
||||||
|
drawn_deck = random.sample(deck, num_positions)
|
||||||
|
|
||||||
drawn_cards = []
|
drawn_cards = []
|
||||||
for position in spread.positions:
|
for position, card in zip(spread.positions, drawn_deck):
|
||||||
# Draw random card
|
|
||||||
card = random.choice(deck)
|
|
||||||
# Random reversal (50% chance)
|
# Random reversal (50% chance)
|
||||||
is_reversed = random.choice([True, False])
|
is_reversed = random.choice([True, False])
|
||||||
drawn_cards.append(DrawnCard(position, card, is_reversed))
|
drawn_cards.append(DrawnCard(position, card, is_reversed))
|
||||||
|
|||||||
165
src/tarot/ui.py
165
src/tarot/ui.py
@@ -6,13 +6,14 @@ supporting multiple decks and automatic image resolution.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk, filedialog
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk, ImageGrab, ImageDraw, ImageFont
|
||||||
HAS_PILLOW = True
|
HAS_PILLOW = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_PILLOW = False
|
HAS_PILLOW = False
|
||||||
@@ -596,6 +597,7 @@ class SpreadDisplay:
|
|||||||
ttk.Button(toolbar, text="Zoom Out (-)", command=lambda: self._zoom(0.8)).pack(side=tk.LEFT, padx=2)
|
ttk.Button(toolbar, text="Zoom Out (-)", command=lambda: self._zoom(0.8)).pack(side=tk.LEFT, padx=2)
|
||||||
ttk.Button(toolbar, text="Reset View", command=self._reset_view).pack(side=tk.LEFT, padx=2)
|
ttk.Button(toolbar, text="Reset View", command=self._reset_view).pack(side=tk.LEFT, padx=2)
|
||||||
ttk.Button(toolbar, text="Toggle Text", command=self._toggle_text).pack(side=tk.LEFT, padx=2)
|
ttk.Button(toolbar, text="Toggle Text", command=self._toggle_text).pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Button(toolbar, text="Export PNG", command=self._export_image).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
# Only show toggle top card if relevant
|
# Only show toggle top card if relevant
|
||||||
if self.reading.spread.name == 'Celtic Cross':
|
if self.reading.spread.name == 'Celtic Cross':
|
||||||
@@ -826,6 +828,40 @@ class SpreadDisplay:
|
|||||||
self._draw_spread()
|
self._draw_spread()
|
||||||
self._center_view()
|
self._center_view()
|
||||||
|
|
||||||
|
def _export_image(self):
|
||||||
|
if not HAS_PILLOW:
|
||||||
|
print("Pillow library is not installed. Cannot export images.")
|
||||||
|
return
|
||||||
|
|
||||||
|
region = self.canvas.bbox("all")
|
||||||
|
if not region:
|
||||||
|
print("Nothing to export.")
|
||||||
|
return
|
||||||
|
|
||||||
|
x1, y1, x2, y2 = region
|
||||||
|
padding = 10
|
||||||
|
x1 -= padding
|
||||||
|
y1 -= padding
|
||||||
|
x2 += padding
|
||||||
|
y2 += padding
|
||||||
|
|
||||||
|
try:
|
||||||
|
default_name = f"{self.reading.spread.name.replace(' ', '_').lower()}_spread.png"
|
||||||
|
file_path = filedialog.asksaveasfilename(
|
||||||
|
defaultextension=".png",
|
||||||
|
initialfile=default_name,
|
||||||
|
filetypes=[("PNG Image", "*.png"), ("All Files", "*.*")],
|
||||||
|
title="Save spread as...",
|
||||||
|
)
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
img = self._render_spread_to_image(padding=padding)
|
||||||
|
img.save(file_path, "PNG")
|
||||||
|
print(f"Exported spread to {file_path}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Failed to export spread: {exc}")
|
||||||
|
|
||||||
def _center_view(self):
|
def _center_view(self):
|
||||||
# Center the scroll region in the window
|
# Center the scroll region in the window
|
||||||
# This is a bit tricky in Tkinter without knowing window size,
|
# This is a bit tricky in Tkinter without knowing window size,
|
||||||
@@ -851,6 +887,131 @@ class SpreadDisplay:
|
|||||||
else:
|
else:
|
||||||
self._zoom(0.9)
|
self._zoom(0.9)
|
||||||
|
|
||||||
|
def _render_spread_to_image(self, padding: int = 40) -> Image.Image:
|
||||||
|
"""Render the current spread to a PIL Image (no screen capture)."""
|
||||||
|
|
||||||
|
layout = self.LAYOUTS.get(self.reading.spread.name)
|
||||||
|
if not layout:
|
||||||
|
layout = {i + 1: {"pos": (i * 1.2, 0)} for i in range(len(self.reading.drawn_cards))}
|
||||||
|
|
||||||
|
# Use 4x resolution multiplier for high-quality export
|
||||||
|
card_width = int(400 * self.zoom_level)
|
||||||
|
card_height = int(600 * self.zoom_level)
|
||||||
|
|
||||||
|
unit_x = card_width
|
||||||
|
unit_y = card_height
|
||||||
|
|
||||||
|
positions = []
|
||||||
|
min_x, min_y = float("inf"), float("inf")
|
||||||
|
max_x, max_y = float("-inf"), float("-inf")
|
||||||
|
|
||||||
|
cards_to_draw = []
|
||||||
|
for drawn in self.reading.drawn_cards:
|
||||||
|
pos_data = layout.get(drawn.position.number)
|
||||||
|
if not pos_data:
|
||||||
|
continue
|
||||||
|
z_index = pos_data.get("z", 0)
|
||||||
|
if z_index > 0 and not self.show_top_card:
|
||||||
|
continue
|
||||||
|
cards_to_draw.append((drawn, pos_data, z_index))
|
||||||
|
|
||||||
|
cards_to_draw.sort(key=lambda x: x[2])
|
||||||
|
|
||||||
|
for drawn, pos_data, _ in cards_to_draw:
|
||||||
|
rel_x, rel_y = pos_data["pos"]
|
||||||
|
rotation = pos_data.get("rotate", 0)
|
||||||
|
x = rel_x * unit_x
|
||||||
|
y = rel_y * unit_y
|
||||||
|
|
||||||
|
half_w = card_width / 2
|
||||||
|
half_h = card_height / 2
|
||||||
|
if abs(rotation % 180) == 90:
|
||||||
|
half_w, half_h = half_h, half_w
|
||||||
|
|
||||||
|
min_x = min(min_x, x - half_w)
|
||||||
|
max_x = max(max_x, x + half_w)
|
||||||
|
min_y = min(min_y, y - half_h)
|
||||||
|
max_y = max(max_y, y + half_h)
|
||||||
|
positions.append((drawn, x, y, rotation))
|
||||||
|
|
||||||
|
if min_x == float("inf"):
|
||||||
|
min_x = min_y = 0
|
||||||
|
max_x = max_y = 0
|
||||||
|
|
||||||
|
width = int((max_x - min_x) + padding * 2)
|
||||||
|
height = int((max_y - min_y) + padding * 2)
|
||||||
|
|
||||||
|
bg_color = (44, 62, 80)
|
||||||
|
img = Image.new("RGB", (width, height), bg_color)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
cx = -min_x + padding
|
||||||
|
cy = -min_y + padding
|
||||||
|
|
||||||
|
for drawn, rel_x, rel_y, rotation in positions:
|
||||||
|
x = cx + rel_x
|
||||||
|
y = cy + rel_y
|
||||||
|
pil_image = None
|
||||||
|
if self.image_loader:
|
||||||
|
img_path = self.image_loader.get_image_path(drawn.card)
|
||||||
|
if img_path and os.path.exists(img_path):
|
||||||
|
try:
|
||||||
|
pil_image = Image.open(img_path)
|
||||||
|
# Convert to RGB immediately to avoid transparency issues
|
||||||
|
if pil_image.mode in ("RGBA", "PA"):
|
||||||
|
# Create white background for transparency
|
||||||
|
background = Image.new("RGB", pil_image.size, "white")
|
||||||
|
background.paste(pil_image, mask=pil_image.split()[-1] if pil_image.mode == "RGBA" else None)
|
||||||
|
pil_image = background
|
||||||
|
elif pil_image.mode != "RGB":
|
||||||
|
pil_image = pil_image.convert("RGB")
|
||||||
|
except Exception:
|
||||||
|
pil_image = None
|
||||||
|
if pil_image is None:
|
||||||
|
pil_image = Image.new("RGB", (card_width, card_height), "white")
|
||||||
|
ph_draw = ImageDraw.Draw(pil_image)
|
||||||
|
ph_draw.rectangle([0, 0, card_width - 1, card_height - 1], outline="black", width=2)
|
||||||
|
ph_draw.text((4, 4), drawn.card.name, fill="black", font=font)
|
||||||
|
else:
|
||||||
|
pil_image = pil_image.resize((card_width, card_height), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
if drawn.is_reversed:
|
||||||
|
rotation += 180
|
||||||
|
if rotation % 360 != 0:
|
||||||
|
# Always use RGB mode before rotation to avoid mask issues
|
||||||
|
if pil_image.mode != "RGB":
|
||||||
|
pil_image = pil_image.convert("RGB")
|
||||||
|
pil_image = pil_image.rotate(rotation, expand=True, fillcolor="white")
|
||||||
|
|
||||||
|
# Final RGB conversion before paste
|
||||||
|
if pil_image.mode != "RGB":
|
||||||
|
pil_image = pil_image.convert("RGB")
|
||||||
|
|
||||||
|
pw, ph = pil_image.size
|
||||||
|
try:
|
||||||
|
img.paste(pil_image, (int(x - pw / 2), int(y - ph / 2)))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Debug: Paste failed for {drawn.card.name} at ({x}, {y}): {e}")
|
||||||
|
print(f" Image mode: {pil_image.mode}, size: {pil_image.size}")
|
||||||
|
print(f" Canvas size: {img.size}, position: ({int(x - pw / 2)}, {int(y - ph / 2)})")
|
||||||
|
|
||||||
|
if self.show_text:
|
||||||
|
text_font = font
|
||||||
|
label = f"{drawn.position.number}. {drawn.position.name}"
|
||||||
|
meaning = drawn.position.meaning
|
||||||
|
text_h = text_font.getbbox("Ag")[3] - text_font.getbbox("Ag")[1]
|
||||||
|
block_h = int(text_h * 3.2)
|
||||||
|
bx1 = int(x - card_width / 2)
|
||||||
|
by1 = int(y + card_height / 2 - block_h)
|
||||||
|
bx2 = int(x + card_width / 2)
|
||||||
|
by2 = int(y + card_height / 2)
|
||||||
|
draw.rectangle([bx1, by1, bx2, by2], fill=(0, 0, 0, 180))
|
||||||
|
draw.text((bx1 + 4, by1 + 4), label, fill="white", font=text_font)
|
||||||
|
draw.text((bx1 + 4, by1 + 4 + text_h * 1.4), meaning, fill="#ecf0f1", font=text_font)
|
||||||
|
|
||||||
|
return img.convert("RGB")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.root.mainloop()
|
self.root.mainloop()
|
||||||
|
|
||||||
|
|||||||
38
test_parse.py
Normal file
38
test_parse.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'src')
|
||||||
|
|
||||||
|
# Test parsing logic
|
||||||
|
test_cases = [
|
||||||
|
('2,11,20', [2, 11, 20]),
|
||||||
|
('1 5 10', [1, 5, 10]),
|
||||||
|
('3, 7, 14', [3, 7, 14]),
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_input, expected in test_cases:
|
||||||
|
# Simulate the parsing logic
|
||||||
|
parts = ['display-cards'] + test_input.split()
|
||||||
|
nums = []
|
||||||
|
tokens = []
|
||||||
|
for tok in parts[1:]:
|
||||||
|
if ',' in tok:
|
||||||
|
tokens.extend(tok.split(','))
|
||||||
|
else:
|
||||||
|
tokens.append(tok)
|
||||||
|
|
||||||
|
for tok in tokens:
|
||||||
|
tok = tok.strip()
|
||||||
|
if tok:
|
||||||
|
try:
|
||||||
|
nums.append(int(tok))
|
||||||
|
except ValueError:
|
||||||
|
nums.append(-1)
|
||||||
|
|
||||||
|
status = '✓' if nums == expected else '✗'
|
||||||
|
print(f'{status} Input: "{test_input}" -> {nums} (expected {expected})')
|
||||||
|
|
||||||
|
# Test with actual cards
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
print("\n✓ Parsing logic works! Now test in REPL with:")
|
||||||
|
print(" display-cards 2,11,20")
|
||||||
|
print(" display-cards 1 5 10")
|
||||||
@@ -37,6 +37,8 @@ if str(SRC_DIR) not in sys.path:
|
|||||||
|
|
||||||
from cli_renderers import Renderer # noqa: E402
|
from cli_renderers import Renderer # noqa: E402
|
||||||
from tarot import Card, Tarot, Tree, Cube # noqa: E402
|
from tarot import Card, Tarot, Tree, Cube # noqa: E402
|
||||||
|
import tarot.ui as ui_mod # noqa: E402
|
||||||
|
from tarot.ui import SpreadDisplay, display_cards as ui_display_cards # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
app = typer.Typer(add_completion=True, help="CLI playground for the Tarot API.")
|
app = typer.Typer(add_completion=True, help="CLI playground for the Tarot API.")
|
||||||
@@ -56,6 +58,7 @@ SAFE_ROOTS: Dict[str, Any] = {
|
|||||||
"tree": Tree,
|
"tree": Tree,
|
||||||
"cube": Cube,
|
"cube": Cube,
|
||||||
"letter": letter_ns,
|
"letter": letter_ns,
|
||||||
|
"ui": ui_mod,
|
||||||
}
|
}
|
||||||
|
|
||||||
_MISSING_DOCS: Set[str] = set()
|
_MISSING_DOCS: Set[str] = set()
|
||||||
@@ -69,6 +72,8 @@ ROOT_COMMANDS = [
|
|||||||
"tree",
|
"tree",
|
||||||
"cube",
|
"cube",
|
||||||
"letter",
|
"letter",
|
||||||
|
"display-cards",
|
||||||
|
"display-spread",
|
||||||
"help",
|
"help",
|
||||||
"quit",
|
"quit",
|
||||||
"exit",
|
"exit",
|
||||||
@@ -186,6 +191,10 @@ def _periodic_symbols() -> List[str]:
|
|||||||
return list(periodic.keys()) if isinstance(periodic, dict) else []
|
return list(periodic.keys()) if isinstance(periodic, dict) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _spread_names() -> List[str]:
|
||||||
|
return list(SpreadDisplay.LAYOUTS.keys())
|
||||||
|
|
||||||
|
|
||||||
def _current_fragment(text: str) -> str:
|
def _current_fragment(text: str) -> str:
|
||||||
stripped = text.rstrip()
|
stripped = text.rstrip()
|
||||||
if not stripped:
|
if not stripped:
|
||||||
@@ -269,6 +278,9 @@ def _completion_candidates(text: str) -> List[str]:
|
|||||||
if len(tokens) == 3 and tokens[1].lower() == "periodic":
|
if len(tokens) == 3 and tokens[1].lower() == "periodic":
|
||||||
return [s for s in _periodic_symbols() if s.lower().startswith(fragment)]
|
return [s for s in _periodic_symbols() if s.lower().startswith(fragment)]
|
||||||
|
|
||||||
|
if cmd == "display-spread":
|
||||||
|
return [name for name in _spread_names() if name.lower().startswith(fragment)]
|
||||||
|
|
||||||
if cmd == "search":
|
if cmd == "search":
|
||||||
# Simple heuristics: suggest option flags or option values based on prior flag
|
# Simple heuristics: suggest option flags or option values based on prior flag
|
||||||
if last_token.startswith("-"):
|
if last_token.startswith("-"):
|
||||||
@@ -399,6 +411,9 @@ def _print_structure_table() -> None:
|
|||||||
"Draw a spread with random cards",
|
"Draw a spread with random cards",
|
||||||
"tarot.deck.card.spread('Celtic Cross')",
|
"tarot.deck.card.spread('Celtic Cross')",
|
||||||
),
|
),
|
||||||
|
("ui.display_cards([card1, card2])", "Open GUI to view cards", "ui.display_cards([tarot.deck.card(1)])"),
|
||||||
|
("display-spread 'Celtic Cross'", "CLI: draw + show spread", "python typer-test.py display-spread 'Celtic Cross'"),
|
||||||
|
("display-cards 1 2 3", "CLI: show specific cards", "python typer-test.py display-cards 1 2 3"),
|
||||||
("tarot.planet()", "Planet correspondences as dict", "tarot.planet()['Venus']"),
|
("tarot.planet()", "Planet correspondences as dict", "tarot.planet()['Venus']"),
|
||||||
("tarot.hexagram()", "I Ching hexagrams as dict", "tarot.hexagram()[1]"),
|
("tarot.hexagram()", "I Ching hexagrams as dict", "tarot.hexagram()[1]"),
|
||||||
("tree.sephera(n)", "Kabbalistic sephiroth", "tree.sephera(1)"),
|
("tree.sephera(n)", "Kabbalistic sephiroth", "tree.sephera(1)"),
|
||||||
@@ -784,6 +799,39 @@ def _handle_repl_command(parts: List[str], raw_line: str = "") -> bool:
|
|||||||
console.print("No cards matched that filter.")
|
console.print("No cards matched that filter.")
|
||||||
else:
|
else:
|
||||||
_print_cards_table(cards)
|
_print_cards_table(cards)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("display-cards")
|
||||||
|
def display_cards_cmd(
|
||||||
|
numbers: List[int] = typer.Argument(..., help="Card numbers to display"),
|
||||||
|
deck_name: str = typer.Option("default", help="Deck name for UI display"),
|
||||||
|
) -> None:
|
||||||
|
"""Open a GUI window showing the given card numbers."""
|
||||||
|
|
||||||
|
_ensure_deck()
|
||||||
|
cards: List[Card] = []
|
||||||
|
for num in numbers:
|
||||||
|
card = Tarot.deck.card(num) # type: ignore[attr-defined]
|
||||||
|
if card is None:
|
||||||
|
typer.echo(f"Card {num} not found.")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
cards.append(card)
|
||||||
|
ui_display_cards(cards, deck_name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("display-spread")
|
||||||
|
def display_spread_cmd(
|
||||||
|
spread_name: str = typer.Argument("Celtic Cross", help="Spread name (e.g., 'Celtic Cross')"),
|
||||||
|
deck_name: str = typer.Option("default", help="Deck name for UI display"),
|
||||||
|
) -> None:
|
||||||
|
"""Draw a spread and open the GUI display for it."""
|
||||||
|
|
||||||
|
_ensure_deck()
|
||||||
|
reading = Tarot.deck.card.spread(spread_name) # type: ignore[attr-defined]
|
||||||
|
if reading is None:
|
||||||
|
typer.echo(f"Spread '{spread_name}' not found.")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
ui_mod.display_spread(reading, deck_name)
|
||||||
return True
|
return True
|
||||||
if cmd == "planet":
|
if cmd == "planet":
|
||||||
planet_name = parts[1] if len(parts) > 1 else None
|
planet_name = parts[1] if len(parts) > 1 else None
|
||||||
@@ -800,6 +848,52 @@ def _handle_repl_command(parts: List[str], raw_line: str = "") -> bool:
|
|||||||
return True
|
return True
|
||||||
_render_value(planet_obj)
|
_render_value(planet_obj)
|
||||||
return True
|
return True
|
||||||
|
if cmd == "display-cards":
|
||||||
|
if len(parts) < 2:
|
||||||
|
console.print("Usage: display-cards <card numbers...>")
|
||||||
|
console.print("Examples: display-cards 1 2 3")
|
||||||
|
console.print(" display-cards 2,11,20")
|
||||||
|
return True
|
||||||
|
_ensure_deck()
|
||||||
|
nums: List[int] = []
|
||||||
|
|
||||||
|
# Handle both space-separated and comma-separated numbers
|
||||||
|
tokens = []
|
||||||
|
for tok in parts[1:]:
|
||||||
|
# Split by comma if present
|
||||||
|
if "," in tok:
|
||||||
|
tokens.extend(tok.split(","))
|
||||||
|
else:
|
||||||
|
tokens.append(tok)
|
||||||
|
|
||||||
|
for tok in tokens:
|
||||||
|
tok = tok.strip()
|
||||||
|
if not tok:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
nums.append(int(tok))
|
||||||
|
except ValueError:
|
||||||
|
console.print(f"Invalid card number: {tok}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
cards: List[Card] = []
|
||||||
|
for num in nums:
|
||||||
|
card = Tarot.deck.card(num) # type: ignore[attr-defined]
|
||||||
|
if card is None:
|
||||||
|
console.print(f"Card {num} not found.")
|
||||||
|
return True
|
||||||
|
cards.append(card)
|
||||||
|
ui_display_cards(cards)
|
||||||
|
return True
|
||||||
|
if cmd == "display-spread":
|
||||||
|
spread_name = " ".join(parts[1:]) if len(parts) > 1 else "Celtic Cross"
|
||||||
|
_ensure_deck()
|
||||||
|
reading = Tarot.deck.card.spread(spread_name) # type: ignore[attr-defined]
|
||||||
|
if reading is None:
|
||||||
|
console.print(f"Spread '{spread_name}' not found.")
|
||||||
|
return True
|
||||||
|
ui_mod.display_spread(reading)
|
||||||
|
return True
|
||||||
if cmd == "hexagram":
|
if cmd == "hexagram":
|
||||||
hex_num = None
|
hex_num = None
|
||||||
if len(parts) > 1 and parts[1].isdigit():
|
if len(parts) > 1 and parts[1].isdigit():
|
||||||
|
|||||||
Reference in New Issue
Block a user