ghlang 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghlang/__init__.py +3 -0
- ghlang/cli/__init__.py +35 -0
- ghlang/cli/config.py +154 -0
- ghlang/cli/github.py +147 -0
- ghlang/cli/local.py +183 -0
- ghlang/cli/utils.py +82 -0
- ghlang/cloc_client.py +127 -0
- ghlang/config.py +162 -0
- ghlang/exceptions.py +28 -0
- ghlang/github_client.py +211 -0
- ghlang/logging.py +73 -0
- ghlang/static/__init__.py +1 -0
- ghlang/static/default_config.toml +29 -0
- ghlang/static/lang_mapping.py +195 -0
- ghlang/static/themes.py +25 -0
- ghlang/visualizers.py +321 -0
- ghlang-2.2.0.dist-info/METADATA +383 -0
- ghlang-2.2.0.dist-info/RECORD +22 -0
- ghlang-2.2.0.dist-info/WHEEL +5 -0
- ghlang-2.2.0.dist-info/entry_points.txt +2 -0
- ghlang-2.2.0.dist-info/licenses/LICENSE +21 -0
- ghlang-2.2.0.dist-info/top_level.txt +1 -0
ghlang/__init__.py
ADDED
ghlang/cli/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from ghlang import __version__
|
|
4
|
+
from ghlang.cli.config import config
|
|
5
|
+
from ghlang.cli.github import github
|
|
6
|
+
from ghlang.cli.local import local
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="See what languages you've been coding in", add_completion=True)
|
|
10
|
+
app.command()(config)
|
|
11
|
+
app.command()(github)
|
|
12
|
+
app.command()(local)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _version_callback(value: bool) -> None:
|
|
16
|
+
if value:
|
|
17
|
+
typer.echo(f"ghlang v{__version__}")
|
|
18
|
+
raise typer.Exit()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback()
|
|
22
|
+
def main(
|
|
23
|
+
version: bool = typer.Option(
|
|
24
|
+
False,
|
|
25
|
+
"--version",
|
|
26
|
+
help="Show version and exit",
|
|
27
|
+
callback=_version_callback,
|
|
28
|
+
is_eager=True,
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
app()
|
ghlang/cli/config.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ghlang.config import create_default_config
|
|
12
|
+
from ghlang.config import get_config_path
|
|
13
|
+
from ghlang.config import load_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _open_in_editor(path: Path) -> None:
|
|
17
|
+
"""Open file in default editor"""
|
|
18
|
+
editor = os.environ.get("EDITOR")
|
|
19
|
+
|
|
20
|
+
if editor:
|
|
21
|
+
subprocess.run([editor, str(path)], check=False)
|
|
22
|
+
elif platform.system() == "Darwin":
|
|
23
|
+
subprocess.run(["open", str(path)], check=False)
|
|
24
|
+
elif platform.system() == "Windows":
|
|
25
|
+
os.startfile(str(path)) # type: ignore[attr-defined]
|
|
26
|
+
else:
|
|
27
|
+
subprocess.run(["xdg-open", str(path)], check=False)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_value(value: object) -> str:
|
|
31
|
+
"""Format a config value for display"""
|
|
32
|
+
if isinstance(value, bool):
|
|
33
|
+
return "[green]true[/green]" if value else "[red]false[/red]"
|
|
34
|
+
|
|
35
|
+
if isinstance(value, list):
|
|
36
|
+
if not value:
|
|
37
|
+
return "[dim][][/dim]"
|
|
38
|
+
|
|
39
|
+
return ", ".join(str(v) for v in value)
|
|
40
|
+
|
|
41
|
+
if isinstance(value, Path):
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
return str(value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_config_table(config_path: Path) -> None:
|
|
48
|
+
"""Print config as a formatted table"""
|
|
49
|
+
console = Console()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
cfg = load_config(config_path=config_path, require_token=False)
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"[red]Error loading config:[/red] {e}")
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
|
|
58
|
+
console.print(f"\n[bold]Config:[/bold] {config_path}\n")
|
|
59
|
+
|
|
60
|
+
# GitHub section
|
|
61
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
62
|
+
table.add_column("Key", style="cyan")
|
|
63
|
+
table.add_column("Value")
|
|
64
|
+
|
|
65
|
+
console.print("[bold yellow]GitHub[/bold yellow]")
|
|
66
|
+
table.add_row("token", cfg.token if cfg.token else "[dim]not set[/dim]")
|
|
67
|
+
table.add_row("affiliation", cfg.affiliation)
|
|
68
|
+
table.add_row("visibility", cfg.visibility)
|
|
69
|
+
table.add_row("ignored_repos", _format_value(cfg.ignored_repos))
|
|
70
|
+
console.print(table)
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
73
|
+
# Cloc section
|
|
74
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
75
|
+
table.add_column("Key", style="cyan")
|
|
76
|
+
table.add_column("Value")
|
|
77
|
+
|
|
78
|
+
console.print("[bold yellow]Cloc[/bold yellow]")
|
|
79
|
+
table.add_row("ignored_dirs", _format_value(cfg.ignored_dirs))
|
|
80
|
+
console.print(table)
|
|
81
|
+
console.print()
|
|
82
|
+
|
|
83
|
+
# Output section
|
|
84
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
85
|
+
table.add_column("Key", style="cyan")
|
|
86
|
+
table.add_column("Value")
|
|
87
|
+
|
|
88
|
+
console.print("[bold yellow]Output[/bold yellow]")
|
|
89
|
+
table.add_row("directory", str(cfg.output_dir))
|
|
90
|
+
table.add_row("save_json", _format_value(cfg.save_json))
|
|
91
|
+
table.add_row("save_repos", _format_value(cfg.save_repos))
|
|
92
|
+
table.add_row("top_n_languages", str(cfg.top_n_languages))
|
|
93
|
+
console.print(table)
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
# Preferences section
|
|
97
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
98
|
+
table.add_column("Key", style="cyan")
|
|
99
|
+
table.add_column("Value")
|
|
100
|
+
|
|
101
|
+
console.print("[bold yellow]Preferences[/bold yellow]")
|
|
102
|
+
table.add_row("verbose", _format_value(cfg.verbose))
|
|
103
|
+
table.add_row("theme", cfg.theme)
|
|
104
|
+
console.print(table)
|
|
105
|
+
console.print()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def config(
|
|
109
|
+
show: bool = typer.Option(
|
|
110
|
+
False,
|
|
111
|
+
"--show",
|
|
112
|
+
help="Print config as formatted table",
|
|
113
|
+
),
|
|
114
|
+
path: bool = typer.Option(
|
|
115
|
+
False,
|
|
116
|
+
"--path",
|
|
117
|
+
help="Print config file path",
|
|
118
|
+
),
|
|
119
|
+
raw: bool = typer.Option(
|
|
120
|
+
False,
|
|
121
|
+
"--raw",
|
|
122
|
+
help="Print raw config file contents",
|
|
123
|
+
),
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Manage config file"""
|
|
126
|
+
config_path = get_config_path()
|
|
127
|
+
|
|
128
|
+
if path:
|
|
129
|
+
print(config_path)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if raw:
|
|
133
|
+
if not config_path.exists():
|
|
134
|
+
typer.echo(f"Config file doesn't exist yet: {config_path}")
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
|
|
137
|
+
console = Console()
|
|
138
|
+
syntax = Syntax(config_path.read_text(), "toml", theme="ansi_dark", line_numbers=True)
|
|
139
|
+
console.print(syntax)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if show:
|
|
143
|
+
if not config_path.exists():
|
|
144
|
+
typer.echo(f"Config file doesn't exist yet: {config_path}")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
_print_config_table(config_path)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if not config_path.exists():
|
|
151
|
+
create_default_config(config_path)
|
|
152
|
+
typer.echo(f"Created config at {config_path}")
|
|
153
|
+
|
|
154
|
+
_open_in_editor(config_path)
|
ghlang/cli/github.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ghlang.cli.utils import generate_charts
|
|
7
|
+
from ghlang.config import load_config
|
|
8
|
+
from ghlang.exceptions import ConfigError
|
|
9
|
+
from ghlang.github_client import GitHubClient
|
|
10
|
+
from ghlang.logging import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def github(
|
|
14
|
+
config_path: Path | None = typer.Option(
|
|
15
|
+
None,
|
|
16
|
+
"--config",
|
|
17
|
+
help="Use a different config file",
|
|
18
|
+
exists=True,
|
|
19
|
+
dir_okay=False,
|
|
20
|
+
file_okay=True,
|
|
21
|
+
readable=True,
|
|
22
|
+
path_type=Path,
|
|
23
|
+
),
|
|
24
|
+
output_dir: Path | None = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--output-dir",
|
|
27
|
+
help="Where to save the charts",
|
|
28
|
+
file_okay=False,
|
|
29
|
+
dir_okay=True,
|
|
30
|
+
writable=True,
|
|
31
|
+
path_type=Path,
|
|
32
|
+
),
|
|
33
|
+
output: Path | None = typer.Option(
|
|
34
|
+
None,
|
|
35
|
+
"--output",
|
|
36
|
+
"-o",
|
|
37
|
+
help="Custom output path/filename",
|
|
38
|
+
path_type=Path,
|
|
39
|
+
),
|
|
40
|
+
title: str | None = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--title",
|
|
43
|
+
"-t",
|
|
44
|
+
help="Custom chart title",
|
|
45
|
+
),
|
|
46
|
+
top_n: int | None = typer.Option(
|
|
47
|
+
None,
|
|
48
|
+
"--top-n",
|
|
49
|
+
help="How many languages to show in the bar chart",
|
|
50
|
+
),
|
|
51
|
+
json_only: bool = typer.Option(
|
|
52
|
+
False,
|
|
53
|
+
"--json-only",
|
|
54
|
+
help="Output JSON only, skip chart generation",
|
|
55
|
+
),
|
|
56
|
+
stdout: bool = typer.Option(
|
|
57
|
+
False,
|
|
58
|
+
"--stdout",
|
|
59
|
+
help="Output stats to stdout instead of files (implies --json-only --quiet)",
|
|
60
|
+
),
|
|
61
|
+
quiet: bool = typer.Option(
|
|
62
|
+
False,
|
|
63
|
+
"--quiet",
|
|
64
|
+
"-q",
|
|
65
|
+
help="Suppress log output (only show errors)",
|
|
66
|
+
),
|
|
67
|
+
verbose: bool = typer.Option(
|
|
68
|
+
False,
|
|
69
|
+
"--verbose",
|
|
70
|
+
"-v",
|
|
71
|
+
help="Show more details",
|
|
72
|
+
),
|
|
73
|
+
theme: str | None = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--theme",
|
|
76
|
+
help="Chart theme (default: light)",
|
|
77
|
+
),
|
|
78
|
+
fmt: str | None = typer.Option(
|
|
79
|
+
None,
|
|
80
|
+
"--format",
|
|
81
|
+
"-f",
|
|
82
|
+
help="Output format, overrides --output extension (png or svg)",
|
|
83
|
+
),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Analyze your GitHub repos"""
|
|
86
|
+
if stdout:
|
|
87
|
+
quiet = True
|
|
88
|
+
json_only = True
|
|
89
|
+
|
|
90
|
+
logger.configure(verbose, quiet=quiet)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
cli_overrides = {
|
|
94
|
+
"output_dir": output_dir,
|
|
95
|
+
"top_n_languages": top_n,
|
|
96
|
+
"verbose": verbose or None,
|
|
97
|
+
"theme": theme,
|
|
98
|
+
}
|
|
99
|
+
cfg = load_config(config_path=config_path, cli_overrides=cli_overrides, require_token=True)
|
|
100
|
+
|
|
101
|
+
except ConfigError as e:
|
|
102
|
+
logger.error(str(e))
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
if not stdout:
|
|
106
|
+
cfg.output_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
logger.info(f"Saving to {cfg.output_dir}")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
client = GitHubClient(
|
|
111
|
+
token=cfg.token,
|
|
112
|
+
affiliation=cfg.affiliation,
|
|
113
|
+
visibility=cfg.visibility,
|
|
114
|
+
ignored_repos=cfg.ignored_repos,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
language_stats = client.get_all_language_stats(
|
|
118
|
+
repos_output=(
|
|
119
|
+
cfg.output_dir / "repositories.json" if cfg.save_repos and not stdout else None
|
|
120
|
+
),
|
|
121
|
+
stats_output=(
|
|
122
|
+
cfg.output_dir / "language_stats.json" if cfg.save_json and not stdout else None
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not language_stats:
|
|
127
|
+
logger.error("No language statistics found, nothing to visualize")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
|
|
130
|
+
if stdout:
|
|
131
|
+
print(json.dumps(language_stats, indent=2))
|
|
132
|
+
elif json_only:
|
|
133
|
+
stats_file = cfg.output_dir / "language_stats.json"
|
|
134
|
+
|
|
135
|
+
with stats_file.open("w") as f:
|
|
136
|
+
json.dump(language_stats, f, indent=2)
|
|
137
|
+
|
|
138
|
+
logger.success(f"Saved stats to {stats_file}")
|
|
139
|
+
else:
|
|
140
|
+
chart_title = title if title else "GitHub Language Stats"
|
|
141
|
+
generate_charts(language_stats, cfg, title=chart_title, output=output, fmt=fmt)
|
|
142
|
+
|
|
143
|
+
except typer.Exit:
|
|
144
|
+
raise
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.exception(f"Something went wrong: {e}")
|
|
147
|
+
raise typer.Exit(1)
|
ghlang/cli/local.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ghlang.cli.utils import generate_charts
|
|
8
|
+
from ghlang.cloc_client import ClocClient
|
|
9
|
+
from ghlang.config import load_config
|
|
10
|
+
from ghlang.exceptions import ClocNotFoundError
|
|
11
|
+
from ghlang.exceptions import ConfigError
|
|
12
|
+
from ghlang.logging import logger
|
|
13
|
+
from ghlang.visualizers import normalize_language_stats
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def local(
|
|
17
|
+
path: Path = typer.Argument(
|
|
18
|
+
".",
|
|
19
|
+
exists=True,
|
|
20
|
+
file_okay=True,
|
|
21
|
+
dir_okay=True,
|
|
22
|
+
readable=True,
|
|
23
|
+
path_type=Path,
|
|
24
|
+
),
|
|
25
|
+
config_path: Path | None = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--config",
|
|
28
|
+
help="Use a different config file",
|
|
29
|
+
exists=True,
|
|
30
|
+
dir_okay=False,
|
|
31
|
+
file_okay=True,
|
|
32
|
+
readable=True,
|
|
33
|
+
path_type=Path,
|
|
34
|
+
),
|
|
35
|
+
output_dir: Path | None = typer.Option(
|
|
36
|
+
None,
|
|
37
|
+
"--output-dir",
|
|
38
|
+
help="Where to save the charts",
|
|
39
|
+
file_okay=False,
|
|
40
|
+
dir_okay=True,
|
|
41
|
+
writable=True,
|
|
42
|
+
path_type=Path,
|
|
43
|
+
),
|
|
44
|
+
output: Path | None = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
"--output",
|
|
47
|
+
"-o",
|
|
48
|
+
help="Custom output path/filename",
|
|
49
|
+
path_type=Path,
|
|
50
|
+
),
|
|
51
|
+
title: str | None = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--title",
|
|
54
|
+
"-t",
|
|
55
|
+
help="Custom chart title",
|
|
56
|
+
),
|
|
57
|
+
top_n: int | None = typer.Option(
|
|
58
|
+
None,
|
|
59
|
+
"--top-n",
|
|
60
|
+
help="How many languages to show in the bar chart",
|
|
61
|
+
),
|
|
62
|
+
json_only: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--json-only",
|
|
65
|
+
help="Output JSON only, skip chart generation",
|
|
66
|
+
),
|
|
67
|
+
stdout: bool = typer.Option(
|
|
68
|
+
False,
|
|
69
|
+
"--stdout",
|
|
70
|
+
help="Output stats to stdout instead of files (implies --json-only --quiet)",
|
|
71
|
+
),
|
|
72
|
+
quiet: bool = typer.Option(
|
|
73
|
+
False,
|
|
74
|
+
"--quiet",
|
|
75
|
+
"-q",
|
|
76
|
+
help="Suppress log output (only show errors)",
|
|
77
|
+
),
|
|
78
|
+
verbose: bool = typer.Option(
|
|
79
|
+
False,
|
|
80
|
+
"--verbose",
|
|
81
|
+
"-v",
|
|
82
|
+
help="Show more details",
|
|
83
|
+
),
|
|
84
|
+
follow_links: bool = typer.Option(
|
|
85
|
+
False,
|
|
86
|
+
"--follow-links",
|
|
87
|
+
help="Follow symlinks when analyzing (unix only)",
|
|
88
|
+
),
|
|
89
|
+
theme: str | None = typer.Option(
|
|
90
|
+
None,
|
|
91
|
+
"--theme",
|
|
92
|
+
help="Chart theme (default: light)",
|
|
93
|
+
),
|
|
94
|
+
fmt: str | None = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--format",
|
|
97
|
+
"-f",
|
|
98
|
+
help="Output format, overrides --output extension (png or svg)",
|
|
99
|
+
),
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Analyze local files with cloc"""
|
|
102
|
+
if stdout:
|
|
103
|
+
quiet = True
|
|
104
|
+
json_only = True
|
|
105
|
+
|
|
106
|
+
logger.configure(verbose, quiet=quiet)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
cli_overrides = {
|
|
110
|
+
"output_dir": output_dir,
|
|
111
|
+
"top_n_languages": top_n,
|
|
112
|
+
"verbose": verbose or None,
|
|
113
|
+
"theme": theme,
|
|
114
|
+
}
|
|
115
|
+
cfg = load_config(config_path=config_path, cli_overrides=cli_overrides, require_token=False)
|
|
116
|
+
|
|
117
|
+
except ConfigError as e:
|
|
118
|
+
logger.error(str(e))
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
if follow_links and sys.platform == "win32":
|
|
122
|
+
logger.warning("--follow-links is not supported on Windows, ignoring")
|
|
123
|
+
follow_links = False
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
cloc = ClocClient(ignored_dirs=cfg.ignored_dirs, follow_links=follow_links)
|
|
127
|
+
|
|
128
|
+
except ClocNotFoundError as e:
|
|
129
|
+
logger.error(str(e))
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
if not stdout:
|
|
133
|
+
cfg.output_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
logger.info(f"Saving to {cfg.output_dir}")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
detailed_stats = cloc.get_language_stats(
|
|
138
|
+
path,
|
|
139
|
+
stats_output=(
|
|
140
|
+
cfg.output_dir / "cloc_stats.json" if cfg.save_json and not stdout else None
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
raw_stats = {
|
|
144
|
+
lang: data["code"]
|
|
145
|
+
for lang, data in detailed_stats.items()
|
|
146
|
+
if lang != "_summary" and data["code"] > 0
|
|
147
|
+
}
|
|
148
|
+
language_stats = normalize_language_stats(raw_stats)
|
|
149
|
+
|
|
150
|
+
if not language_stats:
|
|
151
|
+
logger.error("No code found to analyze, nothing to visualize")
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
|
|
154
|
+
if stdout:
|
|
155
|
+
print(json.dumps(language_stats, indent=2))
|
|
156
|
+
elif json_only:
|
|
157
|
+
stats_file = cfg.output_dir / "language_stats.json"
|
|
158
|
+
|
|
159
|
+
with stats_file.open("w") as f:
|
|
160
|
+
json.dump(language_stats, f, indent=2)
|
|
161
|
+
|
|
162
|
+
logger.success(f"Saved stats to {stats_file}")
|
|
163
|
+
else:
|
|
164
|
+
if title:
|
|
165
|
+
chart_title = title
|
|
166
|
+
else:
|
|
167
|
+
resolved = path.expanduser().resolve()
|
|
168
|
+
chart_title = f"Local: {resolved.name}"
|
|
169
|
+
|
|
170
|
+
generate_charts(
|
|
171
|
+
language_stats,
|
|
172
|
+
cfg,
|
|
173
|
+
colors_required=False,
|
|
174
|
+
title=chart_title,
|
|
175
|
+
output=output,
|
|
176
|
+
fmt=fmt,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
except typer.Exit:
|
|
180
|
+
raise
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.exception(f"Something went wrong: {e}")
|
|
183
|
+
raise typer.Exit(1)
|
ghlang/cli/utils.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from ghlang.logging import logger
|
|
7
|
+
from ghlang.visualizers import generate_bar
|
|
8
|
+
from ghlang.visualizers import generate_pie
|
|
9
|
+
from ghlang.visualizers import load_github_colors
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ghlang.config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_charts(
|
|
17
|
+
language_stats: dict[str, int],
|
|
18
|
+
cfg: "Config",
|
|
19
|
+
colors_required: bool = True,
|
|
20
|
+
title: str | None = None,
|
|
21
|
+
output: Path | None = None,
|
|
22
|
+
fmt: str | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Load colors and generate pie/bar charts with progress"""
|
|
25
|
+
with logger.progress() as progress:
|
|
26
|
+
task = progress.add_task("Generating charts", total=3)
|
|
27
|
+
|
|
28
|
+
progress.update(task, description="Loading language colors...")
|
|
29
|
+
colors_file = cfg.output_dir / "github_colors.json" if cfg.save_json else None
|
|
30
|
+
colors = load_github_colors(output_file=colors_file)
|
|
31
|
+
progress.advance(task)
|
|
32
|
+
|
|
33
|
+
if not colors:
|
|
34
|
+
if colors_required:
|
|
35
|
+
logger.error("Couldn't load GitHub colors, can't continue without them")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
logger.warning("Couldn't load GitHub colors, charts will be gray")
|
|
39
|
+
colors = {}
|
|
40
|
+
|
|
41
|
+
# determine output format
|
|
42
|
+
# priority: --format > --output suffix > default png
|
|
43
|
+
if fmt:
|
|
44
|
+
if fmt not in ("png", "svg"):
|
|
45
|
+
logger.warning(f"We only support png and svg, not '{fmt}', using png instead")
|
|
46
|
+
suffix = ".png"
|
|
47
|
+
else:
|
|
48
|
+
suffix = f".{fmt}"
|
|
49
|
+
elif output and output.suffix:
|
|
50
|
+
suffix = output.suffix
|
|
51
|
+
else:
|
|
52
|
+
suffix = ".png"
|
|
53
|
+
|
|
54
|
+
if output:
|
|
55
|
+
if output.is_absolute():
|
|
56
|
+
parent = output.parent
|
|
57
|
+
elif str(output.parent) != ".":
|
|
58
|
+
parent = cfg.output_dir / output.parent
|
|
59
|
+
else:
|
|
60
|
+
parent = cfg.output_dir
|
|
61
|
+
|
|
62
|
+
stem = output.stem
|
|
63
|
+
pie_output = parent / f"{stem}_pie{suffix}"
|
|
64
|
+
bar_output = parent / f"{stem}_bar{suffix}"
|
|
65
|
+
else:
|
|
66
|
+
pie_output = cfg.output_dir / f"language_pie{suffix}"
|
|
67
|
+
bar_output = cfg.output_dir / f"language_bar{suffix}"
|
|
68
|
+
|
|
69
|
+
progress.update(task, description="Generating pie chart...")
|
|
70
|
+
generate_pie(language_stats, colors, pie_output, title=title, theme=cfg.theme)
|
|
71
|
+
progress.advance(task)
|
|
72
|
+
|
|
73
|
+
progress.update(task, description="Generating bar chart...")
|
|
74
|
+
generate_bar(
|
|
75
|
+
language_stats,
|
|
76
|
+
colors,
|
|
77
|
+
bar_output,
|
|
78
|
+
top_n=cfg.top_n_languages,
|
|
79
|
+
title=title,
|
|
80
|
+
theme=cfg.theme,
|
|
81
|
+
)
|
|
82
|
+
progress.advance(task)
|