@@ -27,7 +27,8 @@ def __init__(self, config_manager: ConfigManager,
2727 show_grid : bool = True ,
2828 custom_width : Optional [int ] = None ,
2929 custom_height : Optional [int ] = None ,
30- graph_timeline : bool = False ):
30+ graph_timeline : bool = False ,
31+ local_mode : bool = False ):
3132 """Initialize the display formatter."""
3233 terminal_size = shutil .get_terminal_size ()
3334 self .terminal_width = terminal_size .columns
@@ -49,6 +50,7 @@ def __init__(self, config_manager: ConfigManager,
4950 self .custom_width = custom_width
5051 self .custom_height = custom_height
5152 self .graph_timeline = graph_timeline
53+ self .local_mode = local_mode
5254
5355 def display (self , username : str , user_data : Dict [str , Any ],
5456 stats : Dict [str , Any ], spaced = True ) -> None :
@@ -316,14 +318,44 @@ def _display_compact(self, username: str, user_data: Dict[str, Any],
316318 info_part = (right_side [i ] if i < len (right_side ) else "" )
317319 print (f"{ graph_part } { padding } { info_part } " )
318320
321+ def _reverse_truncate (self , line : str , max_width : int ):
322+ pattern = r"(\x1b\[[0-9;]*m)(.*?)(?=(\x1b\[[0-9;]*m)|$)"
323+ matches = re .findall (pattern , line , flags = re .DOTALL )
324+ segments = [(m [0 ], m [1 ]) for m in matches ]
325+
326+ width = sum (len (text ) for _ , text in segments )
327+
328+ if width <= max_width :
329+ return line .rjust (max_width )
330+
331+ result = []
332+ remaining = max_width
333+
334+ for color , text in reversed (segments ):
335+ if remaining <= 0 :
336+ break
337+ if len (text ) <= remaining :
338+ result .append ((color , text ))
339+ remaining -= len (text )
340+ else :
341+ result .append ((color , text [- remaining :]))
342+ remaining = 0
343+
344+ final = '' .join (color + text for color , text in reversed (result ))
345+ final += self .colors .get ('reset' , '\x1b [0m' )
346+ return final
347+
319348 def _display_full (self , username : str , user_data : Dict [str , Any ],
320349 stats : Dict [str , Any ], spaced = True ) -> None :
321350 """Display full layout with graph and all info sections."""
322- if self .graph_timeline and not self .show_grid :
323- # Show git timeline graph (only when no grid is shown)
351+ left_side = []
352+ ANSI_PATTERN = re .compile (r'\033\[[0-9;]*m' )
353+ if self .graph_timeline :
354+ # Show git timeline graph instead of contribution graph
324355 try :
325- timeline_text = self ._get_graph_text ()
326- print (timeline_text )
356+ timeline_text = self ._get_graph_text (False )
357+ left_side = timeline_text .split ("\n " )
358+
327359 # Still show right side info
328360 contrib_graph = stats .get ('contribution_graph' , [])
329361 info_lines = (self ._format_user_info (user_data , stats )
@@ -342,10 +374,18 @@ def _display_full(self, username: str, user_data: Dict[str, Any],
342374 right_side .append ("" )
343375 right_side .extend (achievements )
344376
345- if right_side :
377+ right_width = max (
378+ self ._display_width (line ) for line in right_side
379+ )
380+
381+ offset = 80
382+ max_width = self .terminal_width - right_width - offset
383+
384+ if right_side and left_side :
346385 print () # Add spacing
347- for line in right_side :
348- print (line )
386+ for l , r in zip (left_side , right_side ):
387+ print (self ._reverse_truncate (l , max_width ),
388+ self .colors ['reset' ], r )
349389 except Exception as e :
350390 print (f"Error displaying timeline: { e } " )
351391 return
@@ -354,7 +394,6 @@ def _display_full(self, username: str, user_data: Dict[str, Any],
354394 graph_width = (self .custom_width if self .custom_width is not None
355395 else max (50 , (self .terminal_width - 10 ) * 3 // 4 ))
356396
357- left_side = []
358397 if self .show_grid :
359398 left_side = self ._get_contribution_graph_lines (
360399 contrib_graph ,
@@ -453,6 +492,14 @@ def _get_contribution_graph_lines(self, weeks_data: list,
453492 Returns:
454493 List of strings representing graph lines
455494 """
495+ if self .local_mode :
496+ try :
497+ weeks_data = self ._get_local_contribution_weeks ()
498+ except Exception as e :
499+ return [f"Error getting local contributions: { e } " ]
500+ else :
501+ weeks_data = weeks_data
502+
456503 recent_weeks = self ._get_recent_weeks (weeks_data )
457504 total_contribs = self ._calculate_total_contributions (recent_weeks )
458505
@@ -509,22 +556,66 @@ def _get_contribution_graph_lines(self, weeks_data: list,
509556
510557 return lines
511558
512- def _get_graph_text (vertical = False ):
559+ def _get_local_contribution_weeks (self ):
560+ """Get contribution weeks from local git repository."""
561+ from .fetcher import BaseFetcher
562+ return BaseFetcher ._build_contribution_graph_from_git ()
563+
564+ def _get_graph_text (self , vertical = False ):
513565 text = subprocess .check_output (
514- ['git' , '--no-pager' , 'log' , '--graph' , '--all' , '--pretty=format:""' ]).decode ().replace ('"' , '' )
566+ ['git' , '--no-pager' , 'log' , '--color=always' ,
567+ '--graph' , '--all' , '--pretty=format:""' ]
568+ ).decode ().replace ('"' , '' )
569+
515570 if vertical :
516571 return text
572+
517573 text = text .translate (str .maketrans (
518574 r"\/" , r"\/" [::- 1 ])).replace ("|" , "-" )
575+
576+ ANSI_PATTERN = re .compile (r'\033\[[0-9;]*m' )
577+
519578 lines = text .splitlines ()
520- max_len = max (len (line ) for line in lines )
521- padded = [line .ljust (max_len ) for line in lines ]
579+ parsed_lines = []
580+ for line in lines :
581+ parts = ANSI_PATTERN .split (line )
582+ codes = ANSI_PATTERN .findall (line )
583+ current_color = ''
584+ parsed = []
585+ for i , seg in enumerate (parts ):
586+ for ch in seg :
587+ parsed .append ((ch , current_color ))
588+ if i < len (codes ):
589+ code = codes [i ]
590+ current_color = '' if code == '\033 [0m' else code
591+ parsed_lines .append (parsed )
592+
593+ if not parsed_lines :
594+ return ''
595+
596+ max_len = max (len (line ) for line in parsed_lines )
597+ padded = [line + [(' ' , '' )] * (max_len - len (line ))
598+ for line in parsed_lines ]
522599
523600 rotated = []
524601 for col in reversed (range (max_len )):
525- new_line = '' .join (padded [row ][col ] for row in range (len (padded )))
526- rotated .append (new_line )
527- return '\n ' .join (rotated )
602+ new_row = [padded [row ][col ] for row in range (len (padded ))]
603+ rotated .append (new_row )
604+
605+ out_lines = []
606+ for row in rotated :
607+ cur_color = ''
608+ out_line = ''
609+ for ch , color in row :
610+ if color != cur_color :
611+ out_line += color
612+ cur_color = color
613+ out_line += ch
614+ if cur_color :
615+ out_line += '\033 [0m'
616+ out_lines .append (out_line )
617+
618+ return '\n ' .join (out_lines )
528619
529620 def _format_user_info_compact (self , user_data : Dict [str , Any ],
530621 stats : Dict [str , Any ]) -> list :
0 commit comments