Simplify binder code by removing RICH availability checks

Since RICH is now a required dependency, removed all conditional logic:
- Removed try/except import block for rich modules
- Removed RICH_AVAILABLE variable and all associated checks
- Removed fallback print statements for when RICH is unavailable
- Simplified console initialization to always use Console()
- Removed ~70 conditional if statements throughout the code

Result: Cleaner, more maintainable code with the same beautiful output.
 Verified: All functionality working correctly after simplification
This commit is contained in:
Vijay Janapa Reddi
2025-07-28 16:09:20 -04:00
parent 3d65c70d38
commit 8b621b5845

473
binder
View File

@@ -13,22 +13,16 @@ import signal
from pathlib import Path
from contextlib import contextmanager
try:
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from rich.tree import Tree
from rich.text import Text
from rich.live import Live
from rich import print as rprint
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
def rprint(*args, **kwargs):
print(*args, **kwargs)
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
from rich.tree import Tree
from rich.text import Text
from rich.live import Live
from rich import print as rprint
console = Console() if RICH_AVAILABLE else None
console = Console()
class BookBinder:
def __init__(self):
@@ -41,10 +35,6 @@ class BookBinder:
def show_banner(self):
"""Display beautiful banner"""
if not RICH_AVAILABLE:
print("📚 Book Binder - Self-Contained MLSysBook CLI")
return
banner = Panel.fit(
"[bold blue]📚 Book Binder[/bold blue]\n"
"[dim]Self-contained lightning-fast MLSysBook development CLI[/dim]",
@@ -127,12 +117,6 @@ class BookBinder:
"""Display beautiful status information"""
status = self.get_status()
if not RICH_AVAILABLE:
print(f"📊 Status:")
print(f" 🔗 Active config: {status['active_config']}")
print(f" ✅ Clean: {status['is_clean']}")
return
table = Table(title="📊 Current Status", show_header=False, box=None)
table.add_column("", style="cyan", no_wrap=True)
table.add_column("", style="white")
@@ -150,14 +134,6 @@ class BookBinder:
"""Display available chapters in a beautiful format"""
chapters = self.find_chapters()
if not RICH_AVAILABLE:
print("📚 Available chapters:")
for chapter in chapters[:10]:
print(f" {chapter}")
if len(chapters) > 10:
print(f" ... and {len(chapters) - 10} more")
return
tree = Tree("📚 [bold blue]Available Chapters[/bold blue]")
# Group by category
@@ -185,11 +161,6 @@ class BookBinder:
@contextmanager
def progress_context(self, description):
"""Context manager for progress display"""
if not RICH_AVAILABLE:
print(f"Running: {description}")
yield None
return
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
@@ -236,10 +207,7 @@ class BookBinder:
)
return result.returncode == 0
except Exception as e:
if RICH_AVAILABLE:
console.print(f"[red]Error: {e}[/red]")
else:
print(f"Error: {e}")
console.print(f"[red]Error: {e}[/red]")
return False
def open_output_file(self, output_text):
@@ -263,28 +231,18 @@ class BookBinder:
full_path = Path(file_path)
if full_path.exists():
if RICH_AVAILABLE:
console.print(f"[green]🌐 Opening in browser: {full_path.name}[/green]")
else:
print(f"🌐 Opening in browser: {full_path.name}")
console.print(f"[green]🌐 Opening in browser: {full_path.name}[/green]")
try:
# Use 'open' command on macOS
subprocess.run(['open', str(full_path)], check=True)
return True
except subprocess.CalledProcessError:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Could not open file automatically: {full_path}[/yellow]")
else:
print(f"⚠️ Could not open file automatically: {full_path}")
console.print(f"[yellow]⚠️ Could not open file automatically: {full_path}[/yellow]")
return False
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Output file not found: {full_path}[/yellow]")
console.print(f"[dim] Tried: {full_path}[/dim]")
else:
print(f"⚠️ Output file not found: {full_path}")
print(f" Tried: {full_path}")
console.print(f"[yellow]⚠️ Output file not found: {full_path}[/yellow]")
console.print(f"[dim] Tried: {full_path}[/dim]")
return False
# No "Output created:" line found
@@ -299,26 +257,17 @@ class BookBinder:
full_path = Path(file_path)
if full_path.exists():
if RICH_AVAILABLE:
console.print(f"[green]🌐 Opening in browser: {full_path.name}[/green]")
else:
print(f"🌐 Opening in browser: {full_path.name}")
console.print(f"[green]🌐 Opening in browser: {full_path.name}[/green]")
try:
# Use 'open' command on macOS
subprocess.run(['open', str(full_path)], check=True)
return True
except subprocess.CalledProcessError:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Could not open file automatically: {full_path}[/yellow]")
else:
print(f"⚠️ Could not open file automatically: {full_path}")
console.print(f"[yellow]⚠️ Could not open file automatically: {full_path}[/yellow]")
return False
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Output file not found: {full_path}[/yellow]")
else:
print(f"⚠️ Output file not found: {full_path}")
console.print(f"[yellow]⚠️ Output file not found: {full_path}[/yellow]")
return False
def set_fast_build_mode(self, config_file, target_path):
@@ -353,10 +302,7 @@ class BookBinder:
f'navigate: true\n{render_config}'
)
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Could not find project navigation section in {config_file}[/yellow]")
else:
print(f"⚠️ Could not find project navigation section in {config_file}")
console.print(f"[yellow]⚠️ Could not find project navigation section in {config_file}[/yellow]")
return False
elif 'type: book' in content:
@@ -391,28 +337,19 @@ class BookBinder:
new_lines.append(line)
if not target_found:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Target chapter {target_relative_path} not found in chapters list[/yellow]")
else:
print(f"⚠️ Target chapter {target_relative_path} not found in chapters list")
console.print(f"[yellow]⚠️ Target chapter {target_relative_path} not found in chapters list[/yellow]")
return False
content = '\n'.join(new_lines)
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Unknown project type in {config_file}[/yellow]")
else:
print(f"⚠️ Unknown project type in {config_file}")
console.print(f"[yellow]⚠️ Unknown project type in {config_file}[/yellow]")
return False
# Write modified config
with open(config_file, 'w', encoding='utf-8') as f:
f.write(content)
if RICH_AVAILABLE:
console.print(f" 📝 Fast build mode: Only rendering {target_stem} + essential files")
else:
print(f" 📝 Fast build mode: Only rendering {target_stem} + essential files")
console.print(f" 📝 Fast build mode: Only rendering {target_stem} + essential files")
return True
@@ -456,10 +393,7 @@ class BookBinder:
f'navigate: true\n{render_config}'
)
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Could not find project navigation section in {config_file}[/yellow]")
else:
print(f"⚠️ Could not find project navigation section in {config_file}")
console.print(f"[yellow]⚠️ Could not find project navigation section in {config_file}[/yellow]")
return False
elif 'type: book' in content:
@@ -494,28 +428,19 @@ class BookBinder:
new_lines.append(line)
if targets_found != len(target_relative_paths):
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Only {targets_found}/{len(target_relative_paths)} target chapters found in chapters list[/yellow]")
else:
print(f"⚠️ Only {targets_found}/{len(target_relative_paths)} target chapters found in chapters list")
console.print(f"[yellow]⚠️ Only {targets_found}/{len(target_relative_paths)} target chapters found in chapters list[/yellow]")
return False
content = '\n'.join(new_lines)
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ Unknown project type in {config_file}[/yellow]")
else:
print(f"⚠️ Unknown project type in {config_file}")
console.print(f"[yellow]⚠️ Unknown project type in {config_file}[/yellow]")
return False
# Write modified config
with open(config_file, 'w', encoding='utf-8') as f:
f.write(content)
if RICH_AVAILABLE:
console.print(f" 📝 Fast build mode: Only rendering {', '.join(target_stems)} + essential files")
else:
print(f" 📝 Fast build mode: Only rendering {', '.join(target_stems)} + essential files")
console.print(f" 📝 Fast build mode: Only rendering {', '.join(target_stems)} + essential files")
return True
@@ -526,15 +451,9 @@ class BookBinder:
if backup_path.exists():
shutil.copy2(backup_path, config_file)
backup_path.unlink()
if RICH_AVAILABLE:
console.print(f" 🔄 Restored from backup: {backup_path.name}")
else:
print(f" 🔄 Restored from backup: {backup_path.name}")
console.print(f" 🔄 Restored from backup: {backup_path.name}")
else:
if RICH_AVAILABLE:
console.print(f"[yellow]⚠️ No backup found: {backup_path}[/yellow]")
else:
print(f"⚠️ No backup found: {backup_path}")
console.print(f"[yellow]⚠️ No backup found: {backup_path}[/yellow]")
def ensure_clean_config(self, config_file):
"""Ensure config is in clean state (remove any render restrictions)"""
@@ -544,10 +463,7 @@ class BookBinder:
# Restore from backup if it exists
shutil.copy2(backup_path, config_file)
backup_path.unlink()
if RICH_AVAILABLE:
console.print(f" ✅ {config_file.name} restored from backup")
else:
print(f" ✅ {config_file.name} restored from backup")
console.print(f" ✅ {config_file.name} restored from backup")
else:
# Check if file has render restrictions and remove them
with open(config_file, 'r', encoding='utf-8') as f:
@@ -575,15 +491,9 @@ class BookBinder:
if cleaned_content != content:
with open(config_file, 'w', encoding='utf-8') as f:
f.write(cleaned_content)
if RICH_AVAILABLE:
console.print(f" ✅ {config_file.name} cleaned (removed render restrictions)")
else:
print(f" ✅ {config_file.name} cleaned (removed render restrictions)")
console.print(f" ✅ {config_file.name} cleaned (removed render restrictions)")
else:
if RICH_AVAILABLE:
console.print(f" ✅ {config_file.name} already clean")
else:
print(f" ✅ {config_file.name} already clean")
console.print(f" ✅ {config_file.name} already clean")
def setup_symlink(self, format_type):
"""Setup _quarto.yml symlink"""
@@ -613,12 +523,8 @@ class BookBinder:
def build_multiple(self, chapter_list, format_type="html"):
"""Build multiple chapters together in a single unified build"""
if RICH_AVAILABLE:
console.print(f"[green]🚀 Building {len(chapter_list)} chapters together[/green] [dim]({format_type})[/dim]")
console.print(f"[dim] 📋 Chapters: {', '.join(chapter_list)}[/dim]")
else:
print(f"🚀 Building {len(chapter_list)} chapters together ({format_type})")
print(f" 📋 Chapters: {', '.join(chapter_list)}")
console.print(f"[green]🚀 Building {len(chapter_list)} chapters together[/green] [dim]({format_type})[/dim]")
console.print(f"[dim] 📋 Chapters: {', '.join(chapter_list)}[/dim]")
return self.build_multiple_unified(chapter_list, format_type)
@@ -629,10 +535,7 @@ class BookBinder:
for chapter in chapter_list:
chapter_file = self.find_chapter_file(chapter)
if not chapter_file:
if RICH_AVAILABLE:
console.print(f"[red]❌ Chapter not found: {chapter}[/red]")
else:
print(f"❌ Chapter not found: {chapter}")
console.print(f"[red]❌ Chapter not found: {chapter}[/red]")
return False
chapter_files.append(chapter_file)
@@ -651,10 +554,7 @@ class BookBinder:
# Ensure config is clean (remove any render restrictions)
self.ensure_clean_config(config_file)
if RICH_AVAILABLE:
console.print(f"[green]🚀 Building {len(chapter_list)} chapters together[/green] [dim]({format_type})[/dim]")
else:
print(f"🚀 Building {len(chapter_list)} chapters together ({format_type})")
console.print(f"[green]🚀 Building {len(chapter_list)} chapters together[/green] [dim]({format_type})[/dim]")
# Set up unified fast build mode for all chapters
success = self.set_fast_build_mode_multiple(config_file, chapter_files)
@@ -670,20 +570,14 @@ class BookBinder:
signal.signal(signal.SIGTERM, signal_handler)
# Build with unified project.render configuration
if RICH_AVAILABLE:
console.print("[yellow] 🔨 Building all chapters in single render...[/yellow]")
else:
print(" 🔨 Building all chapters in single render...")
console.print("[yellow] 🔨 Building all chapters in single render...[/yellow]")
# Render project with all target chapters
render_cmd = ["quarto", "render", "--to", format_arg]
# Show the raw command being executed
cmd_str = " ".join(render_cmd)
if RICH_AVAILABLE:
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
else:
print(f" 💻 Command: {cmd_str}")
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
# Capture output to find the created file and open it
result = self.run_command(
@@ -694,10 +588,7 @@ class BookBinder:
)
if result:
if RICH_AVAILABLE:
console.print(f" ✅ Unified build complete: build/{build_subdir}/")
else:
print(f" ✅ Unified build complete: build/{build_subdir}/")
console.print(f" ✅ Unified build complete: build/{build_subdir}/")
# Open browser for HTML builds
if format_type == "html" and result:
@@ -705,16 +596,12 @@ class BookBinder:
return True
else:
if RICH_AVAILABLE:
console.print(f"[red]❌ Build failed[/red]")
else:
print("❌ Build failed")
console.print(f"[red]❌ Build failed[/red]")
return False
finally:
# Always restore config, even if build fails
if RICH_AVAILABLE:
console.print(" 🛡️ Ensuring pristine config restoration...")
console.print(" 🛡️ Ensuring pristine config restoration...")
self.restore_config(config_file)
@@ -723,26 +610,17 @@ class BookBinder:
# Find the actual chapter file
chapter_file = self.find_chapter_file(chapter)
if not chapter_file:
if RICH_AVAILABLE:
console.print(f"[red]❌ No chapter found matching '{chapter}'[/red]")
console.print("[yellow]💡 Available chapters:[/yellow]")
self.show_chapters()
else:
print(f"❌ No chapter found matching '{chapter}'")
print("💡 Available chapters:")
self.show_chapters()
console.print(f"[red]❌ No chapter found matching '{chapter}'[/red]")
console.print("[yellow]💡 Available chapters:[/yellow]")
self.show_chapters()
return False
# Get relative path from book directory
target_path = str(chapter_file.relative_to(self.book_dir))
chapter_name = str(chapter_file.relative_to(self.book_dir / "contents")).replace(".qmd", "")
if RICH_AVAILABLE:
console.print(f"[green]🚀 Building[/green] [bold]{chapter_name}[/bold] [dim]({format_type})[/dim]")
console.print(f"[dim] ✅ Found: {chapter_file}[/dim]")
else:
print(f"🚀 Building {chapter_name} ({format_type})")
print(f" ✅ Found: {chapter_file}")
console.print(f"[green]🚀 Building[/green] [bold]{chapter_name}[/bold] [dim]({format_type})[/dim]")
console.print(f"[dim] ✅ Found: {chapter_file}[/dim]")
# Setup configuration
config_file = self.html_config if format_type == "html" else self.pdf_config
@@ -764,35 +642,23 @@ class BookBinder:
# Setup signal handler to restore config on Ctrl+C
def signal_handler(signum, frame):
if RICH_AVAILABLE:
console.print("\n[yellow]🛡️ Ctrl+C detected - restoring config and exiting...[/yellow]")
else:
print("\n🛡 Ctrl+C detected - restoring config and exiting...")
console.print("\n[yellow]🛡️ Ctrl+C detected - restoring config and exiting...[/yellow]")
self.restore_config(config_file)
if RICH_AVAILABLE:
console.print("[green]✅ Config restored to pristine state[/green]")
else:
print("✅ Config restored to pristine state")
console.print("[green]✅ Config restored to pristine state[/green]")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Build with project.render configuration (fast build)
if RICH_AVAILABLE:
console.print("[yellow] 🔨 Building with fast build configuration...[/yellow]")
else:
print(" 🔨 Building with fast build configuration...")
console.print("[yellow] 🔨 Building with fast build configuration...[/yellow]")
# Render project with limited file scope
render_cmd = ["quarto", "render", "--to", format_arg]
# Show the raw command being executed
cmd_str = " ".join(render_cmd)
if RICH_AVAILABLE:
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
else:
print(f" 💻 Command: {cmd_str}")
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
# Capture output to find the created file and open it
result = self.run_command(
@@ -809,28 +675,19 @@ class BookBinder:
success, output = result, ""
if success:
if RICH_AVAILABLE:
console.print(f"[green] ✅ Fast build complete: build/{build_subdir}/[/green]")
else:
print(f" ✅ Fast build complete: build/{build_subdir}/")
console.print(f"[green] ✅ Fast build complete: build/{build_subdir}/[/green]")
# Automatically open the output file if HTML and requested
if format_type == "html" and output and open_browser:
self.open_output_file(output)
else:
if RICH_AVAILABLE:
console.print("[red] ❌ Build failed[/red]")
else:
print(" ❌ Build failed")
console.print("[red] ❌ Build failed[/red]")
return success
finally:
# Always restore config (bulletproof cleanup)
if RICH_AVAILABLE:
console.print("[yellow] 🛡️ Ensuring pristine config restoration...[/yellow]")
else:
print(" 🛡️ Ensuring pristine config restoration...")
console.print("[yellow] 🛡️ Ensuring pristine config restoration...[/yellow]")
self.restore_config(config_file)
@@ -839,19 +696,13 @@ class BookBinder:
# Find the actual chapter file
chapter_file = self.find_chapter_file(chapter)
if not chapter_file:
if RICH_AVAILABLE:
console.print(f"[red]❌ No chapter found matching '{chapter}'[/red]")
else:
print(f"❌ No chapter found matching '{chapter}'")
console.print(f"[red]❌ No chapter found matching '{chapter}'[/red]")
return False
target_path = str(chapter_file.relative_to(self.book_dir))
chapter_name = str(chapter_file.relative_to(self.book_dir / "contents")).replace(".qmd", "")
if RICH_AVAILABLE:
console.print(f"[blue]🌐 Starting preview for[/blue] [bold]{chapter_name}[/bold]")
else:
print(f"🌐 Starting preview for {chapter_name}")
console.print(f"[blue]🌐 Starting preview for[/blue] [bold]{chapter_name}[/bold]")
# Setup for HTML preview
config_file = self.html_config
@@ -862,15 +713,9 @@ class BookBinder:
# Setup signal handler to restore config on exit
def signal_handler(signum, frame):
if RICH_AVAILABLE:
console.print("\n[yellow]🛡️ Ctrl+C detected - restoring config and exiting...[/yellow]")
else:
print("\n🛡 Ctrl+C detected - restoring config and exiting...")
console.print("\n[yellow]🛡️ Ctrl+C detected - restoring config and exiting...[/yellow]")
self.restore_config(config_file)
if RICH_AVAILABLE:
console.print("[green]✅ Config restored to pristine state[/green]")
else:
print("✅ Config restored to pristine state")
console.print("[green]✅ Config restored to pristine state[/green]")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
@@ -882,10 +727,7 @@ class BookBinder:
# Show the raw command being executed
cmd_str = " ".join(preview_cmd)
if RICH_AVAILABLE:
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
else:
print(f" 💻 Command: {cmd_str}")
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
subprocess.run(
preview_cmd,
@@ -900,20 +742,13 @@ class BookBinder:
except Exception as e:
# Always restore config on error
self.restore_config(config_file)
if RICH_AVAILABLE:
console.print(f"[red]❌ Preview failed: {e}[/red]")
else:
print(f"❌ Preview failed: {e}")
console.print(f"[red]❌ Preview failed: {e}[/red]")
return False
def clean(self):
"""Clean build artifacts and restore configs"""
if RICH_AVAILABLE:
console.print("[bold blue]🧹 Fast Build Cleanup[/bold blue]")
console.print("[dim]💡 Restoring master configs and basic cleanup[/dim]")
else:
print("🧹 Fast Build Cleanup")
print("💡 Restoring master configs and basic cleanup")
console.print("[bold blue]🧹 Fast Build Cleanup[/bold blue]")
console.print("[dim]💡 Restoring master configs and basic cleanup[/dim]")
# Restore HTML config
html_config = self.book_dir / "_quarto-html.yml"
@@ -927,15 +762,9 @@ class BookBinder:
symlink_path = self.book_dir / "_quarto.yml"
if symlink_path.exists() and symlink_path.is_symlink():
target = symlink_path.readlink()
if RICH_AVAILABLE:
console.print(f"[dim] 🔗 Current symlink: _quarto.yml → {target}[/dim]")
else:
print(f" 🔗 Current symlink: _quarto.yml → {target}")
console.print(f"[dim] 🔗 Current symlink: _quarto.yml → {target}[/dim]")
if RICH_AVAILABLE:
console.print("[green] ✅ All configs restored to clean state[/green]")
else:
print(" ✅ All configs restored to clean state")
console.print("[green] ✅ All configs restored to clean state[/green]")
# Clean build artifacts
artifacts_to_clean = [
@@ -952,29 +781,17 @@ class BookBinder:
else:
artifact_path.unlink()
if RICH_AVAILABLE:
console.print(f"[yellow] 🗑️ Removing: {artifact_path.name} ({description})[/yellow]")
else:
print(f" 🗑️ Removing: {artifact_path.name} ({description})")
console.print(f"[yellow] 🗑️ Removing: {artifact_path.name} ({description})[/yellow]")
cleaned_count += 1
if cleaned_count > 0:
if RICH_AVAILABLE:
console.print(f"[green] ✅ Cleaned {cleaned_count} items successfully[/green]")
else:
print(f" ✅ Cleaned {cleaned_count} items successfully")
console.print(f"[green] ✅ Cleaned {cleaned_count} items successfully[/green]")
else:
if RICH_AVAILABLE:
console.print("[green] ✅ No artifacts to clean[/green]")
else:
print(" ✅ No artifacts to clean")
console.print("[green] ✅ No artifacts to clean[/green]")
def check_artifacts(self):
"""Check for build artifacts that shouldn't be committed"""
if RICH_AVAILABLE:
console.print("[blue]🔍 Checking for build artifacts...[/blue]")
else:
print("🔍 Checking for build artifacts...")
console.print("[blue]🔍 Checking for build artifacts...[/blue]")
# Check for artifacts that shouldn't be committed
artifacts_found = []
@@ -992,37 +809,22 @@ class BookBinder:
artifacts_found.append((path, description))
if artifacts_found:
if RICH_AVAILABLE:
console.print("[yellow]⚠️ Build artifacts detected:[/yellow]")
for path, description in artifacts_found:
console.print(f"[yellow] 📄 {path} ({description})[/yellow]")
console.print("[blue]💡 Run './binder clean' to remove these artifacts[/blue]")
else:
print("⚠️ Build artifacts detected:")
for path, description in artifacts_found:
print(f" 📄 {path} ({description})")
print("💡 Run './binder clean' to remove these artifacts")
console.print("[yellow]⚠️ Build artifacts detected:[/yellow]")
for path, description in artifacts_found:
console.print(f"[yellow] 📄 {path} ({description})[/yellow]")
console.print("[blue]💡 Run './binder clean' to remove these artifacts[/blue]")
return False
else:
if RICH_AVAILABLE:
console.print("[green]✅ No build artifacts detected.[/green]")
else:
print("✅ No build artifacts detected.")
console.print("[green]✅ No build artifacts detected.[/green]")
return True
def switch(self, format_type):
"""Switch configuration format"""
if format_type not in ["html", "pdf"]:
if RICH_AVAILABLE:
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
else:
print("❌ Format must be 'html' or 'pdf'")
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
return False
if RICH_AVAILABLE:
console.print(f"[blue]🔗 Switching to {format_type} config...[/blue]")
else:
print(f"🔗 Switching to {format_type} config...")
console.print(f"[blue]🔗 Switching to {format_type} config...[/blue]")
# Clean up first
self.clean()
@@ -1030,19 +832,13 @@ class BookBinder:
# Setup new symlink
config_name = self.setup_symlink(format_type)
if RICH_AVAILABLE:
console.print(f"[green] ✅ _quarto.yml → {config_name}[/green]")
else:
print(f" ✅ _quarto.yml → {config_name}")
console.print(f"[green] ✅ _quarto.yml → {config_name}[/green]")
return True
def build_full(self, format_type="html"):
"""Build full book in specified format"""
if RICH_AVAILABLE:
console.print(f"[green]🔨 Building full {format_type.upper()} book...[/green]")
else:
print(f"🔨 Building full {format_type.upper()} book...")
console.print(f"[green]🔨 Building full {format_type.upper()} book...[/green]")
# Create build directory
build_subdir = format_type
@@ -1052,17 +848,11 @@ class BookBinder:
config_name = self.setup_symlink(format_type)
render_cmd = ["quarto", "render", "--to", "html" if format_type == "html" else "titlepage-pdf"]
if RICH_AVAILABLE:
console.print(f"[blue] 🔗 Using {config_name}[/blue]")
else:
print(f" 🔗 Using {config_name}")
console.print(f"[blue] 🔗 Using {config_name}[/blue]")
# Show the raw command being executed
cmd_str = " ".join(render_cmd)
if RICH_AVAILABLE:
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
else:
print(f" 💻 Command: {cmd_str}")
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
success = self.run_command(
render_cmd,
@@ -1071,90 +861,38 @@ class BookBinder:
)
if success:
if RICH_AVAILABLE:
console.print(f"[green] ✅ {format_type.upper()} build complete: build/{build_subdir}/[/green]")
else:
print(f" ✅ {format_type.upper()} build complete: build/{build_subdir}/")
console.print(f"[green] ✅ {format_type.upper()} build complete: build/{build_subdir}/[/green]")
return success
def preview_full(self, format_type="html"):
"""Start full preview server"""
if RICH_AVAILABLE:
console.print(f"[blue]🌐 Starting full {format_type.upper()} preview server...[/blue]")
else:
print(f"🌐 Starting full {format_type.upper()} preview server...")
console.print(f"[blue]🌐 Starting full {format_type.upper()} preview server...[/blue]")
# Setup config
config_name = self.setup_symlink(format_type)
if RICH_AVAILABLE:
console.print(f"[blue] 🔗 Using {config_name}[/blue]")
console.print("[dim] 🛑 Press Ctrl+C to stop the server[/dim]")
else:
print(f" 🔗 Using {config_name}")
print(" 🛑 Press Ctrl+C to stop the server")
console.print(f"[blue] 🔗 Using {config_name}[/blue]")
console.print("[dim] 🛑 Press Ctrl+C to stop the server[/dim]")
try:
preview_cmd = ["quarto", "preview"]
# Show the raw command being executed
cmd_str = " ".join(preview_cmd)
if RICH_AVAILABLE:
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
else:
print(f" 💻 Command: {cmd_str}")
console.print(f"[blue] 💻 Command: {cmd_str}[/blue]")
subprocess.run(preview_cmd, cwd=self.book_dir)
return True
except KeyboardInterrupt:
if RICH_AVAILABLE:
console.print("\n[yellow]🛑 Preview server stopped[/yellow]")
else:
print("\n🛑 Preview server stopped")
console.print("\n[yellow]🛑 Preview server stopped[/yellow]")
return True
except Exception as e:
if RICH_AVAILABLE:
console.print(f"[red]❌ Preview failed: {e}[/red]")
else:
print(f"❌ Preview failed: {e}")
console.print(f"[red]❌ Preview failed: {e}[/red]")
return False
def show_help(self):
"""Display beautiful help screen"""
if not RICH_AVAILABLE:
print("""
📚 Book Binder - Self-Contained MLSysBook CLI
Fast Chapter Commands:
build <chapter> [format] Build single chapter (html/pdf)
preview <chapter> Build and preview single chapter
Full Book Commands:
build-full [format] Build complete book (html/pdf)
preview-full [format] Preview complete book (html/pdf)
Management:
clean [deep] [dry] Clean up configurations and build artifacts
check Check for build artifacts
switch <format> Switch config (html/pdf)
status Show current status
list List available chapters
help Show this help
Examples:
./binder build intro # Build introduction chapter (HTML)
./binder b dl_primer pdf # Build deep learning primer (PDF)
./binder preview ops # Preview ops chapter
./binder build-full pdf # Build complete PDF book
./binder switch pdf # Switch to PDF config
./binder clean # Clean artifacts and configs
./binder check # Check for build artifacts
./binder status # Show current status
""")
return
# Create beautiful help panels
fast_table = Table(show_header=True, header_style="bold green", box=None)
fast_table.add_column("Command", style="green", width=22)
fast_table.add_column("Description", style="white")
@@ -1210,7 +948,7 @@ Examples:
console.print(Panel(shortcuts_table, title="🚀 Shortcuts", border_style="cyan"))
# Pro Tips
console.print(Padding(Panel("", title="💡 Pro Tips", border_style="blue"), (1, 0)))
console.print(Panel("", title="💡 Pro Tips", border_style="blue"))
examples = Text()
examples.append("🎯 Power User Examples:\n", style="bold magenta")
@@ -1259,10 +997,7 @@ def main():
try:
if command == "build":
if len(sys.argv) < 3:
if RICH_AVAILABLE:
console.print("[red]❌ Usage: ./binder build <chapter[,chapter2,...]> [format][/red]")
else:
print("❌ Usage: ./binder build <chapter[,chapter2,...]> [format]")
console.print("[red]❌ Usage: ./binder build <chapter[,chapter2,...]> [format][/red]")
return
chapter = sys.argv[2]
format_type = sys.argv[3] if len(sys.argv) > 3 else "html"
@@ -1270,10 +1005,7 @@ def main():
elif command == "preview":
if len(sys.argv) < 3:
if RICH_AVAILABLE:
console.print("[red]❌ Usage: ./binder preview <chapter>[/red]")
else:
print("❌ Usage: ./binder preview <chapter>")
console.print("[red]❌ Usage: ./binder preview <chapter>[/red]")
return
chapter = sys.argv[2]
binder.preview(chapter)
@@ -1281,20 +1013,14 @@ def main():
elif command == "build-full":
format_type = sys.argv[2] if len(sys.argv) > 2 else "html"
if format_type not in ["html", "pdf"]:
if RICH_AVAILABLE:
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
else:
print("❌ Format must be 'html' or 'pdf'")
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
return
binder.build_full(format_type)
elif command == "preview-full":
format_type = sys.argv[2] if len(sys.argv) > 2 else "html"
if format_type not in ["html", "pdf"]:
if RICH_AVAILABLE:
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
else:
print("❌ Format must be 'html' or 'pdf'")
console.print("[red]❌ Format must be 'html' or 'pdf'[/red]")
return
binder.preview_full(format_type)
@@ -1308,10 +1034,7 @@ def main():
elif command == "switch":
if len(sys.argv) < 3:
if RICH_AVAILABLE:
console.print("[red]❌ Usage: ./binder switch <html|pdf>[/red]")
else:
print("❌ Usage: ./binder switch <html|pdf>")
console.print("[red]❌ Usage: ./binder switch <html|pdf>[/red]")
return
format_type = sys.argv[2]
binder.switch(format_type)
@@ -1326,23 +1049,13 @@ def main():
binder.show_help()
else:
if RICH_AVAILABLE:
console.print(f"[red]❌ Unknown command: {command}[/red]")
console.print("[yellow]💡 Use './binder help' to see available commands[/yellow]")
else:
print(f"❌ Unknown command: {command}")
print("💡 Use './binder help' to see available commands")
console.print(f"[red]❌ Unknown command: {command}[/red]")
console.print("[yellow]💡 Use './binder help' to see available commands[/yellow]")
except KeyboardInterrupt:
if RICH_AVAILABLE:
console.print("\n[yellow]👋 Goodbye![/yellow]")
else:
print("\n👋 Goodbye!")
console.print("\n[yellow]👋 Goodbye![/yellow]")
except Exception as e:
if RICH_AVAILABLE:
console.print(f"[red]❌ Error: {e}[/red]")
else:
print(f"❌ Error: {e}")
console.print(f"[red]❌ Error: {e}[/red]")
if __name__ == "__main__":
main()