Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/gitfetch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ def parse_args() -> argparse.Namespace:
help="GitHub personal access token (optional, increases rate limits)"
)

parser.add_argument(
"--spaced",
action="store_true",
help="Enable spaced layout"
)

parser.add_argument(
"--not-spaced",
action="store_true",
help="Disable spaced layout"
)

parser.add_argument(
"--version",
action="store_true",
Expand Down Expand Up @@ -92,7 +104,13 @@ def main() -> int:
cache_expiry = config_manager.get_cache_expiry_hours()
cache_manager = CacheManager(cache_expiry_hours=cache_expiry)
fetcher = GitHubFetcher() # Uses gh CLI, no token needed
formatter = DisplayFormatter()
formatter = DisplayFormatter(config_manager)
if args.spaced:
spaced = True
elif args.not_spaced:
spaced = False
else:
spaced = True

# Handle cache clearing
if args.clear_cache:
Expand Down Expand Up @@ -126,8 +144,12 @@ def main() -> int:
username)
stale_stats = cache_manager.get_stale_cached_stats(username)
if stale_user_data is not None and stale_stats is not None:
formatter.display(username, stale_user_data, stale_stats)
print("\n🔄 Refreshing data in background...", file=sys.stderr)
# Display stale cache immediately
formatter.display(username, stale_user_data, stale_stats, spaced=spaced)
print("\n🔄 Refreshing data in background...",
file=sys.stderr)

# Refresh cache in background (don't wait for it)
import threading

def refresh_cache():
Expand All @@ -148,7 +170,11 @@ def refresh_cache():
user_data = fetcher.fetch_user_data(username)
stats = fetcher.fetch_user_stats(username, user_data)
cache_manager.cache_user_data(username, user_data, stats)
formatter.display(username, user_data, stats)
# else: fresh cache available, proceed to display

# Display the results
formatter.display(username, user_data, stats, spaced=spaced)

return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
Expand Down
37 changes: 37 additions & 0 deletions src/gitfetch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,42 @@ def _ensure_config_dir(self) -> None:

def _load_config(self) -> None:
"""Load configuration from file."""
default_colors = {
'reset': '\\033[0m',
'bold': '\\033[1m',
'dim': '\\033[2m',
'red': '\\033[91m',
'green': '\\033[92m',
'yellow': '\\033[93m',
'blue': '\\033[94m',
'magenta': '\\033[95m',
'cyan': '\\033[96m',
'white': '\\033[97m',
'orange': '\\033[38;2;255;165;0m',
'accent': '\\033[1m',
'header': '\\033[38;2;118;215;161m',
'muted': '\\033[2m',
'0': '\\033[48;5;238m',
'1': '\\033[48;5;28m',
'2': '\\033[48;5;34m',
'3': '\\033[48;5;40m',
'4': '\\033[48;5;82m'
}
if self.CONFIG_FILE.exists():
self.config.read(self.CONFIG_FILE)
if "COLORS" in self.config:
self.config._sections['COLORS'] = {**default_colors, **self.config._sections['COLORS']}
else:
self.config._sections['COLORS'] = default_colors
else:
# Create default config
self.config['DEFAULT'] = {
'username': '',
'cache_expiry_hours': '24'
}
self.config._sections['COLORS'] = default_colors
for k,v in self.config._sections['COLORS'].items():
self.config._sections['COLORS'][k] = v.encode('utf-8').decode('unicode_escape')

def get_default_username(self) -> Optional[str]:
"""
Expand All @@ -44,6 +72,15 @@ def get_default_username(self) -> Optional[str]:
username = self.config.get('DEFAULT', 'username', fallback='')
return username if username else None

def get_colors(self) -> dict:
"""
Get colors

Returns:
User defined colors or default colors if not set
"""
return self.config._sections["COLORS"]

def set_default_username(self, username: str) -> None:
"""
Set the default GitHub username in config.
Expand Down
64 changes: 29 additions & 35 deletions src/gitfetch/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,40 @@
import re
import unicodedata
from datetime import datetime

from .config import ConfigManager

class DisplayFormatter:
"""Formats and displays GitHub stats in a neofetch-style layout."""

def __init__(self):
def __init__(self,config_manager: ConfigManager):
"""Initialize the display formatter."""
self.terminal_width = shutil.get_terminal_size().columns
self.enable_color = sys.stdout.isatty()
self.colors = config_manager.get_colors()

def display(self, username: str, user_data: Dict[str, Any],
stats: Dict[str, Any]) -> None:
stats: Dict[str, Any],spaced=True) -> None:
"""
Display GitHub statistics in neofetch style.

Args:
username: GitHub username
user_data: User profile data
stats: User statistics data
spaced: Spaced layout
"""
# Determine layout based on terminal width
layout = self._determine_layout()

if layout == 'minimal':
# Only show contribution graph
self._display_minimal(username, stats)
self._display_minimal(username, stats,spaced)
elif layout == 'compact':
# Show graph and key info
self._display_compact(username, user_data, stats)
self._display_compact(username, user_data, stats,spaced)
else:
# Full layout with all sections
self._display_full(username, user_data, stats)
self._display_full(username, user_data, stats,spaced)

print() # Empty line at the end

Expand All @@ -52,20 +54,21 @@ def _determine_layout(self) -> str:
else:
return 'full'

def _display_minimal(self, username: str, stats: Dict[str, Any]) -> None:
def _display_minimal(self, username: str, stats: Dict[str, Any], spaced=True) -> None:
"""Display only contribution graph for narrow terminals."""
contrib_graph = stats.get('contribution_graph', [])
graph_lines = self._get_contribution_graph_lines(
contrib_graph,
username,
width_constraint=self.terminal_width - 4,
include_sections=False
include_sections=False,
spaced=spaced,
)
for line in graph_lines:
print(line)

def _display_compact(self, username: str, user_data: Dict[str, Any],
stats: Dict[str, Any]) -> None:
stats: Dict[str, Any], spaced= True) -> None:
"""Display graph and minimal info side-by-side."""
contrib_graph = stats.get('contribution_graph', [])
recent_weeks = self._get_recent_weeks(contrib_graph)
Expand All @@ -74,7 +77,8 @@ def _display_compact(self, username: str, user_data: Dict[str, Any],
contrib_graph,
username,
width_constraint=graph_width,
include_sections=False
include_sections=False,
spaced=spaced,
)

info_lines = self._format_user_info_compact(user_data, stats)
Expand All @@ -97,15 +101,16 @@ def _display_compact(self, username: str, user_data: Dict[str, Any],
print(f"{graph_part}{padding} {info_part}")

def _display_full(self, username: str, user_data: Dict[str, Any],
stats: Dict[str, Any]) -> None:
stats: Dict[str, Any], spaced=True) -> None:
"""Display full layout with graph and all info sections."""
contrib_graph = stats.get('contribution_graph', [])
graph_width = max(50, (self.terminal_width - 10) // 2)
left_side = self._get_contribution_graph_lines(
contrib_graph,
username,
width_constraint=graph_width,
include_sections=False
include_sections=False,
spaced=spaced,
)

pull_request_lines = self._format_pull_requests(stats)
Expand Down Expand Up @@ -152,7 +157,8 @@ def _display_full(self, username: str, user_data: Dict[str, Any],
def _get_contribution_graph_lines(self, weeks_data: list,
username: str,
width_constraint: int = None,
include_sections: bool = True) -> list:
include_sections: bool = True,
spaced: bool = True) -> list:
"""
Get contribution graph as lines for display.

Expand Down Expand Up @@ -187,7 +193,10 @@ def _get_contribution_graph_lines(self, weeks_data: list,
for idx in range(7):
day = days[idx] if idx < len(days) else {}
count = day.get('contributionCount', 0)
block = self._get_contribution_block_spaced(count)
if spaced:
block = self._get_contribution_block_spaced(count)
else:
block = self._get_contribution_block(count)
day_rows[idx].append(block)

lines = [*header_lines]
Expand Down Expand Up @@ -825,22 +834,7 @@ def _colorize(self, text: str, color: str) -> str:
if not text:
return text

colors = {
'reset': '\033[0m',
'bold': '\033[1m',
'dim': '\033[2m',
'red': '\033[91m',
'green': '\033[92m',
'yellow': '\033[93m',
'blue': '\033[94m',
'magenta': '\033[95m',
'cyan': '\033[96m',
'white': '\033[97m',
'orange': '\033[38;2;255;165;0m',
'accent': '\033[1m',
'header': '\033[38;2;118;215;161m',
'muted': '\033[2m'
}
colors = self.colors

color_code = colors.get(color.lower())
reset = colors['reset']
Expand Down Expand Up @@ -882,15 +876,15 @@ def _get_contribution_block(self, count: int) -> str:

reset = '\033[0m'
if count == 0:
color = '\033[48;5;238m'
color = self.colors['0']
elif count < 3:
color = '\033[48;5;28m'
color = self.colors['1']
elif count < 7:
color = '\033[48;5;34m'
color = self.colors['2']
elif count < 13:
color = '\033[48;5;40m'
color = self.colors['3']
else:
color = '\033[48;5;82m'
color = self.colors['4']

return f"{color} {reset}"

Expand Down