@@ -127,8 +127,8 @@ def order_messages(messages: list, message_order: str) -> list:
127127@click .argument ("path" , type = click .Path (exists = True , path_type = Path ), default = "." )
128128@click .option ("--raw" , is_flag = True , help = "Show raw JSON data instead of formatted view" )
129129@click .option ("--no-pager" , is_flag = True , help = "Disable pager and show all content at once" )
130- @click .option ("--format" , type = click .Choice (["terminal" , "html" ]), default = "terminal" , help = "Output format" )
131- @click .option ("--output" , type = click .Path (), help = "Output file for HTML format" )
130+ @click .option ("--format" , type = click .Choice (["terminal" , "html" , "animated" ]), default = "terminal" , help = "Output format" )
131+ @click .option ("--output" , type = click .Path (), help = "Output file ( HTML/GIF/MP4/cast format) " )
132132@click .option (
133133 "--session-order" ,
134134 type = click .Choice (["asc" , "desc" ]),
@@ -142,6 +142,20 @@ def order_messages(messages: list, message_order: str) -> list:
142142 help = "Order messages within sessions (asc=oldest first, desc=newest first)" ,
143143)
144144@click .option ("--style" , type = click .Path (exists = True ), help = "Custom CSS file to include with HTML format" )
145+ @click .option (
146+ "--typing-speed" , type = float , default = 0.05 , help = "Typing speed in seconds per character (animated format)"
147+ )
148+ @click .option (
149+ "--pause-duration" , type = float , default = 2.0 , help = "Pause duration between messages in seconds (animated format)"
150+ )
151+ @click .option ("--cols" , type = int , default = 120 , help = "Terminal columns (animated format)" )
152+ @click .option ("--rows" , type = int , default = 30 , help = "Terminal rows (animated format)" )
153+ @click .option ("--max-duration" , type = float , help = "Maximum animation duration in seconds (animated format)" )
154+ @click .option (
155+ "--emoji-fallbacks" ,
156+ is_flag = True ,
157+ help = "Replace emoji with text fallbacks for better GIF compatibility (animated format)" ,
158+ )
145159def show (
146160 path : Path ,
147161 raw : bool ,
@@ -151,6 +165,12 @@ def show(
151165 session_order : str ,
152166 message_order : str ,
153167 style : str | None ,
168+ typing_speed : float ,
169+ pause_duration : float ,
170+ cols : int ,
171+ rows : int ,
172+ max_duration : float | None ,
173+ emoji_fallbacks : bool ,
154174):
155175 """Show all conversations for a Claude project.
156176
@@ -276,6 +296,86 @@ def show(
276296 else :
277297 # Print to stdout
278298 print (html_output )
299+ elif format == "animated" :
300+ # Generate animated GIF
301+ from claude_notes .formatters .factory import FormatterFactory
302+
303+ # Create animated formatter with options
304+ formatter_kwargs = {
305+ "typing_speed" : typing_speed ,
306+ "pause_duration" : pause_duration ,
307+ "cols" : cols ,
308+ "rows" : rows ,
309+ "max_duration" : max_duration ,
310+ "use_emoji_fallbacks" : emoji_fallbacks ,
311+ }
312+
313+ try :
314+ formatter = FormatterFactory .create_formatter ("animated" , ** formatter_kwargs )
315+ except (ImportError , RuntimeError ) as e :
316+ console .print (f"[red]Error:[/red] { e } " )
317+ console .print ("[dim]Hint: Install animation dependencies with: uv add --optional-deps animation[/dim]" )
318+ return
319+
320+ # Collect all conversations into a single asciicast
321+ all_messages = []
322+ for conv in conversations :
323+ # Order the messages based on user preference
324+ ordered_messages = order_messages (conv ["messages" ], message_order )
325+ all_messages .extend (ordered_messages )
326+
327+ # Add separator between conversations if multiple
328+ if len (conversations ) > 1 :
329+ separator_msg = {
330+ "type" : "assistant" ,
331+ "message" : {
332+ "role" : "assistant" ,
333+ "content" : f"\n --- Conversation { conversations .index (conv ) + 1 } ---\n " ,
334+ },
335+ }
336+ all_messages .append (separator_msg )
337+
338+ # Generate asciicast
339+ try :
340+ cast_file = formatter .format_conversation (all_messages , conversation_info = {})
341+
342+ # Handle output options
343+ if output :
344+ output_path = Path (output )
345+ base_name = output_path .stem
346+ output_dir = output_path .parent
347+
348+ # Always save the cast file alongside the output
349+ cast_output = output_dir / f"{ base_name } .cast"
350+ import shutil
351+
352+ shutil .copy2 (cast_file , cast_output )
353+ console .print (f"[cyan]Asciicast file saved: { cast_output } [/cyan]" )
354+
355+ # Generate output based on file extension
356+ if output .endswith (".gif" ) or not output_path .suffix :
357+ gif_output = str (output_path .with_suffix (".gif" ))
358+ formatter .generate_gif (cast_file , gif_output )
359+ console .print (f"[green]Animated GIF generated: { gif_output } [/green]" )
360+ elif output .endswith (".mp4" ):
361+ mp4_output = str (output_path .with_suffix (".mp4" ))
362+ formatter .generate_mp4 (cast_file , mp4_output )
363+ console .print (f"[green]MP4 video generated: { mp4_output } [/green]" )
364+ elif output .endswith (".cast" ):
365+ # User specifically requested just the cast file
366+ console .print (f"[green]Asciicast file saved: { cast_output } [/green]" )
367+ else :
368+ # Unknown extension, assume they want GIF
369+ gif_output = str (output_path .with_suffix (".gif" ))
370+ formatter .generate_gif (cast_file , gif_output )
371+ console .print (f"[green]Animated GIF generated: { gif_output } [/green]" )
372+ else :
373+ console .print (f"[yellow]Asciicast file generated: { cast_file } [/yellow]" )
374+ console .print ("[dim]Use --output filename.cast/.gif/.mp4 to save in desired format[/dim]" )
375+
376+ except Exception as e :
377+ console .print (f"[red]Error generating animation: { e } [/red]" )
378+
279379 else :
280380 # Display formatted conversations in terminal
281381 from claude_notes .formatters .terminal import TerminalFormatter
0 commit comments