linman 0.0.1__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.
- apphub/__init__.py +0 -0
- apphub/cli/__init__.py +0 -0
- apphub/cli/banner.py +74 -0
- apphub/cli/commands.py +276 -0
- apphub/cli/formatters.py +136 -0
- apphub/cli/serializers.py +14 -0
- apphub/core/__init__.py +0 -0
- apphub/core/exceptions.py +52 -0
- apphub/core/hub.py +163 -0
- apphub/core/logger.py +46 -0
- apphub/core/models.py +67 -0
- apphub/core/runtime.py +49 -0
- apphub/core/utils.py +57 -0
- apphub/main.py +53 -0
- apphub/plugins/__init__.py +1 -0
- apphub/plugins/appimage.py +442 -0
- apphub/plugins/apt.py +440 -0
- apphub/plugins/base.py +46 -0
- apphub/plugins/flatpak.py +313 -0
- apphub/plugins/snap.py +307 -0
- linman-0.0.1.dist-info/METADATA +163 -0
- linman-0.0.1.dist-info/RECORD +25 -0
- linman-0.0.1.dist-info/WHEEL +4 -0
- linman-0.0.1.dist-info/entry_points.txt +2 -0
- linman-0.0.1.dist-info/licenses/LICENSE +21 -0
apphub/__init__.py
ADDED
|
File without changes
|
apphub/cli/__init__.py
ADDED
|
File without changes
|
apphub/cli/banner.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from rich.align import Align
|
|
2
|
+
from rich.console import Console, Group
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
# Block-style wordmark — reads clearly at typical terminal widths.
|
|
7
|
+
_LOGO_LINES = [
|
|
8
|
+
r" ██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
|
|
9
|
+
r" ██║ ██║████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
|
|
10
|
+
r" ██║ ██║██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
|
|
11
|
+
r" ██║ ██║██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
|
|
12
|
+
r" ███████╗██║██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
|
|
13
|
+
r" ╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# Gradient stops (top → bottom) — cyan → blue → magenta.
|
|
17
|
+
_GRADIENT = [
|
|
18
|
+
"bright_cyan",
|
|
19
|
+
"cyan",
|
|
20
|
+
"deep_sky_blue1",
|
|
21
|
+
"dodger_blue1",
|
|
22
|
+
"medium_purple",
|
|
23
|
+
"medium_orchid",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_TAGLINE = "Linux App Manager · apt · snap · flatpak · appimage"
|
|
27
|
+
_SUB = "one CLI · every format · zero friction"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_logo() -> Text:
|
|
31
|
+
"""Return a colorized LINMAN wordmark."""
|
|
32
|
+
logo = Text()
|
|
33
|
+
for i, line in enumerate(_LOGO_LINES):
|
|
34
|
+
style = _GRADIENT[i % len(_GRADIENT)]
|
|
35
|
+
logo.append(line + "\n", style=f"bold {style}")
|
|
36
|
+
return logo
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_banner(
|
|
40
|
+
console: Console | None = None,
|
|
41
|
+
*,
|
|
42
|
+
version: str = "0.1.0",
|
|
43
|
+
show_panel: bool = True,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Print the linman logo banner to the terminal."""
|
|
46
|
+
console = console or Console()
|
|
47
|
+
|
|
48
|
+
logo = render_logo()
|
|
49
|
+
tagline = Text(_TAGLINE, style="bold white")
|
|
50
|
+
sub = Text(_SUB, style="dim italic")
|
|
51
|
+
ver = Text(f"v{version}", style="dim cyan")
|
|
52
|
+
|
|
53
|
+
body = Group(
|
|
54
|
+
Align.center(logo),
|
|
55
|
+
Align.center(tagline),
|
|
56
|
+
Align.center(sub),
|
|
57
|
+
Align.center(ver),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if show_panel:
|
|
61
|
+
console.print(
|
|
62
|
+
Panel(
|
|
63
|
+
body,
|
|
64
|
+
border_style="bright_cyan",
|
|
65
|
+
padding=(1, 2),
|
|
66
|
+
title="[bold bright_magenta]◆[/] [bold white]linman[/]",
|
|
67
|
+
title_align="left",
|
|
68
|
+
subtitle="[dim]unified package hub[/]",
|
|
69
|
+
subtitle_align="right",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
console.print(body)
|
|
74
|
+
console.print()
|
apphub/cli/commands.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from apphub.cli.formatters import (
|
|
8
|
+
format_app_panel,
|
|
9
|
+
format_app_table,
|
|
10
|
+
format_history_table,
|
|
11
|
+
format_storage_table,
|
|
12
|
+
)
|
|
13
|
+
from apphub.cli.serializers import to_json, to_json_single
|
|
14
|
+
from apphub.core.exceptions import AppNotFoundError
|
|
15
|
+
from apphub.core.hub import AppHubCore
|
|
16
|
+
from apphub.core.models import AppFormat, AppManifest, LifeCycleEvent
|
|
17
|
+
|
|
18
|
+
cli_app = typer.Typer(no_args_is_help=True)
|
|
19
|
+
hub = AppHubCore()
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _emit_app_list(
|
|
24
|
+
apps: list[AppManifest],
|
|
25
|
+
*,
|
|
26
|
+
title: str,
|
|
27
|
+
count: bool,
|
|
28
|
+
output_json: bool,
|
|
29
|
+
) -> None:
|
|
30
|
+
if count:
|
|
31
|
+
console.print(f"[bold cyan]{len(apps)}[/bold cyan] application(s) found.")
|
|
32
|
+
return
|
|
33
|
+
if output_json:
|
|
34
|
+
console.print(to_json(apps))
|
|
35
|
+
return
|
|
36
|
+
console.print(format_app_table(apps, title=f"{title} ({len(apps)})"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _prompt_select_app(
|
|
40
|
+
results: list[AppManifest], name: str, action: str
|
|
41
|
+
) -> AppManifest:
|
|
42
|
+
if len(results) == 1:
|
|
43
|
+
return results[0]
|
|
44
|
+
|
|
45
|
+
console.print(f"[yellow]Found multiple applications matching '{name}':[/yellow]")
|
|
46
|
+
for idx, app in enumerate(results, start=1):
|
|
47
|
+
desc = app.description or "No description"
|
|
48
|
+
console.print(
|
|
49
|
+
f"[cyan][{idx}] | {app.name} ({app.format.value}) | {desc}[/cyan]"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
choice: int = typer.prompt(f"Select an application to {action} (number)", type=int)
|
|
53
|
+
if choice < 1 or choice > len(results):
|
|
54
|
+
console.print("[red]Invalid selection.[/red]")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
return results[choice - 1]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cli_app.command(name="list")
|
|
60
|
+
def list_apps(
|
|
61
|
+
query: str | None = typer.Argument(None, help="Filter applications by name"),
|
|
62
|
+
formats: list[AppFormat] | None = typer.Option(
|
|
63
|
+
None, "--format", "-f", help="Filter by format"
|
|
64
|
+
),
|
|
65
|
+
exclude_defaults: bool = typer.Option(
|
|
66
|
+
False, "--exclude-defaults", "-e", help="Exclude system/default packages"
|
|
67
|
+
),
|
|
68
|
+
sort_by: str | None = typer.Option(
|
|
69
|
+
None, "--sort", "-s", help="Sort by field: name, version, format"
|
|
70
|
+
),
|
|
71
|
+
count: bool = typer.Option(
|
|
72
|
+
False, "--count", "-n", help="Print only the number of matching apps"
|
|
73
|
+
),
|
|
74
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
75
|
+
):
|
|
76
|
+
"""List installed applications across all package managers."""
|
|
77
|
+
apps = asyncio.run(
|
|
78
|
+
hub.list_apps(query=query, formats=formats, exclude_defaults=exclude_defaults)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if sort_by:
|
|
82
|
+
apps = sorted(apps, key=lambda a: getattr(a, sort_by, "") or "")
|
|
83
|
+
|
|
84
|
+
_emit_app_list(apps, title="Applications", count=count, output_json=output_json)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@cli_app.command(name="search")
|
|
88
|
+
def search(
|
|
89
|
+
query: str = typer.Argument(..., help="Search applications by name or description"),
|
|
90
|
+
formats: list[AppFormat] | None = typer.Option(
|
|
91
|
+
None, "--format", "-f", help="Filter by format"
|
|
92
|
+
),
|
|
93
|
+
count: bool = typer.Option(
|
|
94
|
+
False, "--count", "-n", help="Print only the number of matching apps"
|
|
95
|
+
),
|
|
96
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
97
|
+
):
|
|
98
|
+
"""Search available applications across supported registries."""
|
|
99
|
+
apps = asyncio.run(hub.search(query=query, formats=formats))
|
|
100
|
+
_emit_app_list(apps, title="Search Results", count=count, output_json=output_json)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@cli_app.command(name="inspect")
|
|
104
|
+
def inspect(
|
|
105
|
+
file_path: str = typer.Argument(
|
|
106
|
+
..., help="Path to the application file or Application Name to be searched"
|
|
107
|
+
),
|
|
108
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
109
|
+
):
|
|
110
|
+
"""Inspect Local Installable File."""
|
|
111
|
+
|
|
112
|
+
if not Path(file_path).exists():
|
|
113
|
+
console.print(f"[red] No Application Found: '{file_path}'.[/red]")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
app_info = asyncio.run(hub.inspect(path=file_path))
|
|
117
|
+
|
|
118
|
+
if output_json:
|
|
119
|
+
console.print(to_json_single(app_info))
|
|
120
|
+
else:
|
|
121
|
+
console.print(format_app_panel(app_info))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@cli_app.command(name="install")
|
|
125
|
+
def install(
|
|
126
|
+
path_or_name: str = typer.Argument(
|
|
127
|
+
..., help="Path to the application file or Application Name"
|
|
128
|
+
),
|
|
129
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto confirm installation"),
|
|
130
|
+
launch: bool = typer.Option(
|
|
131
|
+
False, "--launch", "-l", help="Launch after installation"
|
|
132
|
+
),
|
|
133
|
+
formats: list[AppFormat] | None = typer.Option(
|
|
134
|
+
None, "--format", "-f", help="Optional format lookup for application"
|
|
135
|
+
),
|
|
136
|
+
):
|
|
137
|
+
"""Install an application (from registery or from file)."""
|
|
138
|
+
|
|
139
|
+
if Path(path_or_name).exists():
|
|
140
|
+
app_info = asyncio.run(hub.inspect(path=path_or_name))
|
|
141
|
+
console.print(format_app_panel(app_info))
|
|
142
|
+
install_target = path_or_name
|
|
143
|
+
install_format = app_info.format
|
|
144
|
+
else:
|
|
145
|
+
results = asyncio.run(hub.search(query=path_or_name, formats=formats))
|
|
146
|
+
if not results:
|
|
147
|
+
console.print(f"[red]No application found matching '{path_or_name}'.[/red]")
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
|
|
150
|
+
app_info = _prompt_select_app(results, path_or_name, "install")
|
|
151
|
+
if len(results) == 1:
|
|
152
|
+
console.print(f"Found this on {app_info.format.value}")
|
|
153
|
+
console.print(format_app_panel(app_info))
|
|
154
|
+
install_target = app_info.name
|
|
155
|
+
install_format = app_info.format
|
|
156
|
+
|
|
157
|
+
if not yes:
|
|
158
|
+
confirm = typer.confirm("Install this application?")
|
|
159
|
+
if not confirm:
|
|
160
|
+
print("Installation cancelled.")
|
|
161
|
+
raise typer.Exit()
|
|
162
|
+
|
|
163
|
+
result = asyncio.run(
|
|
164
|
+
hub.install(
|
|
165
|
+
query_or_path=install_target, install_format=install_format, launch=launch
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if result:
|
|
170
|
+
console.print("[green]Installation successful[/green]")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@cli_app.command(name="uninstall")
|
|
174
|
+
def uninstall(
|
|
175
|
+
name: str = typer.Argument(..., help="Name of the application to uninstall"),
|
|
176
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto confirm installation"),
|
|
177
|
+
clean_uninstall: bool = typer.Option(
|
|
178
|
+
False,
|
|
179
|
+
"--clean",
|
|
180
|
+
"-c",
|
|
181
|
+
help="Clean Uninstall, remove data associated with application",
|
|
182
|
+
),
|
|
183
|
+
):
|
|
184
|
+
"""Uninstall an installed application."""
|
|
185
|
+
results = asyncio.run(hub.list_apps(query=name))
|
|
186
|
+
if not results:
|
|
187
|
+
console.print(f"[red]No application found matching '{name}'.[/red]")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
|
|
190
|
+
if len(results) == 1:
|
|
191
|
+
console.print(f"[bold yellow]Uninstalling {name}...[/bold yellow]")
|
|
192
|
+
app_info = _prompt_select_app(results, name, "uninstall")
|
|
193
|
+
console.print(format_app_panel(app_info))
|
|
194
|
+
|
|
195
|
+
if not yes:
|
|
196
|
+
confirm = typer.confirm("Uninstall this application?")
|
|
197
|
+
if not confirm:
|
|
198
|
+
print("Uninstallation cancelled.")
|
|
199
|
+
raise typer.Exit(1)
|
|
200
|
+
|
|
201
|
+
success = asyncio.run(hub.uninstall(app_info, clean_uninstall))
|
|
202
|
+
if success:
|
|
203
|
+
console.print(f"[green]Successfully uninstalled {name}.[/green]")
|
|
204
|
+
else:
|
|
205
|
+
console.print(
|
|
206
|
+
f"[red]Failed to uninstall {name} or application not found.[/red]"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@cli_app.command(name="info")
|
|
211
|
+
def info(
|
|
212
|
+
name: str = typer.Argument(..., help="Name of the application"),
|
|
213
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
214
|
+
):
|
|
215
|
+
"""Show detailed information about an application."""
|
|
216
|
+
try:
|
|
217
|
+
app_info = asyncio.run(hub.info(query=name))
|
|
218
|
+
except AppNotFoundError:
|
|
219
|
+
console.print(f"[red]No application found matching '{name}'.[/red]")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
if output_json:
|
|
223
|
+
console.print(to_json_single(app_info))
|
|
224
|
+
return
|
|
225
|
+
console.print(format_app_panel(app_info))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@cli_app.command(name="storage")
|
|
229
|
+
def storage(
|
|
230
|
+
formats: list[AppFormat] | None = typer.Option(
|
|
231
|
+
None, "--format", "-f", help="Filter by format"
|
|
232
|
+
),
|
|
233
|
+
top: int | None = typer.Option(None, "--top", "-t", help="Show top N apps by size"),
|
|
234
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
235
|
+
):
|
|
236
|
+
"""Show disk usage by installed applications."""
|
|
237
|
+
apps = asyncio.run(hub.storage(formats=formats, top=top))
|
|
238
|
+
|
|
239
|
+
if output_json:
|
|
240
|
+
console.print(to_json(apps))
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
console.print(format_storage_table(apps, top=None)) # already sliced
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@cli_app.command(name="history")
|
|
247
|
+
def history(
|
|
248
|
+
formats: list[AppFormat] | None = typer.Option(
|
|
249
|
+
None, "--format", "-f", help="Filter by format"
|
|
250
|
+
),
|
|
251
|
+
action_categories: list[LifeCycleEvent] | None = typer.Option(
|
|
252
|
+
None, "--action-categories", "-a", help="Filter by action categories"
|
|
253
|
+
),
|
|
254
|
+
output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
255
|
+
order_by: str = typer.Option(
|
|
256
|
+
None, "--sort", "-s", help="Sort by field: name, version, timestamp"
|
|
257
|
+
),
|
|
258
|
+
descending: bool = typer.Option(False, "--desc", "-d", help="Sort Descending"),
|
|
259
|
+
top: int = typer.Option(None, "--top", "-t", help="Show top N records"),
|
|
260
|
+
):
|
|
261
|
+
"""Show installation/uninstallation history."""
|
|
262
|
+
history_result = asyncio.run(
|
|
263
|
+
hub.history(formats=formats, action_categories=action_categories)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if order_by:
|
|
267
|
+
history_result.sort(key=lambda x: getattr(x, order_by, "") or "")
|
|
268
|
+
if descending:
|
|
269
|
+
history_result.reverse()
|
|
270
|
+
if top is not None:
|
|
271
|
+
history_result = history_result[:top]
|
|
272
|
+
if output_json:
|
|
273
|
+
console.print(to_json(history_result))
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
console.print(format_history_table(history_result))
|
apphub/cli/formatters.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from rich import box
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from apphub.core.models import AppManifest, AppRuntime, HistoryRecords, LifeCycleEvent
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _format_size(size_bytes: int | None) -> str:
|
|
12
|
+
if size_bytes is None:
|
|
13
|
+
return "—"
|
|
14
|
+
size = float(size_bytes)
|
|
15
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
16
|
+
if size < 1024:
|
|
17
|
+
return f"{size:.1f} {unit}"
|
|
18
|
+
size /= 1024
|
|
19
|
+
|
|
20
|
+
return f"{size:.1f} TB"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _styled_table(title: str) -> Table:
|
|
24
|
+
"""Shared Rich table chrome for list/history views."""
|
|
25
|
+
return Table(
|
|
26
|
+
title=title,
|
|
27
|
+
box=box.ROUNDED,
|
|
28
|
+
highlight=True,
|
|
29
|
+
show_lines=False,
|
|
30
|
+
header_style="bold cyan",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_app_table(apps: list[AppManifest], title: str = "Applications") -> Table:
|
|
35
|
+
"""Styled Rich table for a list of AppManifest objects."""
|
|
36
|
+
table = _styled_table(title)
|
|
37
|
+
table.add_column("Name", style="bold white", min_width=20)
|
|
38
|
+
table.add_column("Format", style="yellow", justify="center")
|
|
39
|
+
table.add_column("Version", style="magenta")
|
|
40
|
+
table.add_column("Publisher", style="green")
|
|
41
|
+
table.add_column("Category", style="dim cyan")
|
|
42
|
+
|
|
43
|
+
for app in apps:
|
|
44
|
+
table.add_row(
|
|
45
|
+
app.name,
|
|
46
|
+
app.format.value,
|
|
47
|
+
app.version,
|
|
48
|
+
app.publisher or "—",
|
|
49
|
+
app.category or "—",
|
|
50
|
+
)
|
|
51
|
+
return table
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def format_app_panel(app: AppManifest) -> Panel:
|
|
55
|
+
"""Rich Panel for `apphub info <name>`."""
|
|
56
|
+
lines = [
|
|
57
|
+
f"[bold]Name[/bold]: {app.name}",
|
|
58
|
+
f"[bold]ID[/bold]: {app.id}",
|
|
59
|
+
f"[bold]Format[/bold]: {app.format.value}",
|
|
60
|
+
f"[bold]Version[/bold]: {app.version}",
|
|
61
|
+
f"[bold]Publisher[/bold]: {app.publisher or '—'}",
|
|
62
|
+
f"[bold]Category[/bold]: {app.category or '—'}",
|
|
63
|
+
f"[bold]Installed[/bold]: {'✓' if app.installed else '✗'}",
|
|
64
|
+
f"[bold]Size[/bold]: {_format_size(app.size_bytes)}",
|
|
65
|
+
]
|
|
66
|
+
if app.description:
|
|
67
|
+
lines.append(f"\n[bold]Description[/bold]:\n {app.description}")
|
|
68
|
+
|
|
69
|
+
if app.icon:
|
|
70
|
+
lines.append(f"\n[bold]Icon[/bold]: {app.icon}")
|
|
71
|
+
|
|
72
|
+
if app.runtime != AppRuntime.NATIVE.value:
|
|
73
|
+
lines.append(f"\n[bold]Runtime[/bold]: {app.runtime.value}")
|
|
74
|
+
|
|
75
|
+
return Panel(
|
|
76
|
+
"\n".join(lines),
|
|
77
|
+
title=f"[bold cyan]{app.name}[/bold cyan]",
|
|
78
|
+
border_style="cyan",
|
|
79
|
+
padding=(1, 2),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_storage_table(apps: list[AppManifest], top: int | None = None) -> Table:
|
|
84
|
+
"""Storage breakdown table, sorted by size descending."""
|
|
85
|
+
sized = sorted(
|
|
86
|
+
[a for a in apps if a.size_bytes is not None],
|
|
87
|
+
key=lambda a: a.size_bytes or 0,
|
|
88
|
+
reverse=True,
|
|
89
|
+
)
|
|
90
|
+
unsized = [a for a in apps if a.size_bytes is None]
|
|
91
|
+
ordered = sized + unsized
|
|
92
|
+
|
|
93
|
+
if top is not None:
|
|
94
|
+
ordered = ordered[:top]
|
|
95
|
+
|
|
96
|
+
table = Table(
|
|
97
|
+
title="Storage Usage",
|
|
98
|
+
box=box.ROUNDED,
|
|
99
|
+
header_style="bold cyan",
|
|
100
|
+
show_lines=False,
|
|
101
|
+
)
|
|
102
|
+
table.add_column("Name", style="bold white", min_width=20)
|
|
103
|
+
table.add_column("Format", style="yellow", justify="center")
|
|
104
|
+
table.add_column("Size", style="magenta", justify="right")
|
|
105
|
+
|
|
106
|
+
for app in ordered:
|
|
107
|
+
table.add_row(app.name, app.format.value, _format_size(app.size_bytes))
|
|
108
|
+
|
|
109
|
+
return table
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def format_history_table(
|
|
113
|
+
history_records: list[HistoryRecords], title: str = "HistoryRecords"
|
|
114
|
+
) -> Table:
|
|
115
|
+
"""Styled Rich table for a list of HistoryRecords objects."""
|
|
116
|
+
|
|
117
|
+
table = _styled_table(title)
|
|
118
|
+
table.add_column("Name", style="bold white", min_width=20)
|
|
119
|
+
table.add_column("Format", style="yellow", justify="center")
|
|
120
|
+
table.add_column("Timestamp", style="magenta")
|
|
121
|
+
table.add_column("LifeCycle Event", style="red")
|
|
122
|
+
table.add_column("Version", style="green")
|
|
123
|
+
|
|
124
|
+
for record in history_records:
|
|
125
|
+
table.add_row(
|
|
126
|
+
record.app_name,
|
|
127
|
+
record.format.value,
|
|
128
|
+
record.timestamp.strftime("%B %d, %Y, %I:%M %p"),
|
|
129
|
+
record.lifecycle_event,
|
|
130
|
+
(
|
|
131
|
+
f"{record.old_version_id} -> {record.version_id}"
|
|
132
|
+
if record.lifecycle_event == LifeCycleEvent.UPGRADED
|
|
133
|
+
else record.version_id
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
return table
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from apphub.core.models import AppManifest, HistoryRecords
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def to_json(apps: list[AppManifest | HistoryRecords], indent: int = 2) -> str:
|
|
7
|
+
return json.dumps(
|
|
8
|
+
[app.model_dump(mode="json") for app in apps],
|
|
9
|
+
indent=indent,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_json_single(app: AppManifest, indent: int = 2) -> str:
|
|
14
|
+
return json.dumps(app.model_dump(mode="json"), indent=indent)
|
apphub/core/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class AppHubError(Exception):
|
|
2
|
+
"""Base error for AppHub."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PluginError(AppHubError):
|
|
8
|
+
"""Raised when Plugin need to raise exception"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, plugin_name: str, msg: str):
|
|
11
|
+
super().__init__(msg)
|
|
12
|
+
self.plugin_name = plugin_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PluginNotAvailableError(PluginError):
|
|
16
|
+
"""Raised when a required backend (e.g. snap, flatpak) is not installed."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, plugin_name: str):
|
|
19
|
+
super().__init__(
|
|
20
|
+
plugin_name=plugin_name,
|
|
21
|
+
msg=f"Plugin backend not available: '{plugin_name}'. Is it installed?",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AppNotFoundError(AppHubError):
|
|
26
|
+
"""Raised when a requested application cannot be found."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, app_name: str):
|
|
29
|
+
super().__init__(f"Application not found: '{app_name}'")
|
|
30
|
+
self.app_name = app_name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InstallError(AppHubError):
|
|
34
|
+
"""Raised when an installation fails."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, app_name: str, reason: str = ""):
|
|
37
|
+
msg = f"Failed to install '{app_name}'"
|
|
38
|
+
if reason:
|
|
39
|
+
msg += f": {reason}"
|
|
40
|
+
super().__init__(msg)
|
|
41
|
+
self.app_name = app_name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UninstallError(AppHubError):
|
|
45
|
+
"""Raised when an uninstallation fails."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, app_name: str, reason: str = ""):
|
|
48
|
+
msg = f"Failed to uninstall '{app_name}'"
|
|
49
|
+
if reason:
|
|
50
|
+
msg += f": {reason}"
|
|
51
|
+
super().__init__(msg)
|
|
52
|
+
self.app_name = app_name
|