lsdisk 0.1.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.
- lsdisk/__init__.py +3 -0
- lsdisk/analyzers/__init__.py +0 -0
- lsdisk/analyzers/capacity.py +64 -0
- lsdisk/analyzers/filesystems.py +116 -0
- lsdisk/collectors/__init__.py +0 -0
- lsdisk/collectors/block_devices.py +123 -0
- lsdisk/collectors/filesystems.py +89 -0
- lsdisk/inventory.py +79 -0
- lsdisk/main.py +128 -0
- lsdisk/models.py +78 -0
- lsdisk/serialization.py +101 -0
- lsdisk/ui/__init__.py +0 -0
- lsdisk/ui/terminal.py +643 -0
- lsdisk-0.1.1.dist-info/METADATA +173 -0
- lsdisk-0.1.1.dist-info/RECORD +19 -0
- lsdisk-0.1.1.dist-info/WHEEL +5 -0
- lsdisk-0.1.1.dist-info/entry_points.txt +2 -0
- lsdisk-0.1.1.dist-info/licenses/LICENSE +21 -0
- lsdisk-0.1.1.dist-info/top_level.txt +1 -0
lsdisk/ui/terminal.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"""Rich-based terminal interface for lsdisk."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.tree import Tree
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from lsdisk.analyzers.capacity import format_bytes
|
|
12
|
+
from lsdisk.analyzers.filesystems import classify_filesystem
|
|
13
|
+
from lsdisk.inventory import StorageInventory, walk_devices
|
|
14
|
+
from lsdisk.models import BlockDevice
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SUMMARY_CATEGORIES = {"persistent", "network"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_console(color: str = "auto") -> Console:
|
|
21
|
+
"""Create a terminal console with the requested color policy."""
|
|
22
|
+
if color == "always":
|
|
23
|
+
return Console(force_terminal=True)
|
|
24
|
+
|
|
25
|
+
if color == "never":
|
|
26
|
+
return Console(force_terminal=False, no_color=True)
|
|
27
|
+
|
|
28
|
+
return Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _device_type(device: BlockDevice) -> str:
|
|
32
|
+
"""Return a human-readable device classification."""
|
|
33
|
+
if device.device_type == "disk":
|
|
34
|
+
if device.physical is True:
|
|
35
|
+
return "Physical disk"
|
|
36
|
+
if device.physical is False:
|
|
37
|
+
return "Virtual or logical disk"
|
|
38
|
+
return "Unclassified disk"
|
|
39
|
+
|
|
40
|
+
if device.device_type == "part":
|
|
41
|
+
return "Partition"
|
|
42
|
+
|
|
43
|
+
if device.device_type == "rom":
|
|
44
|
+
return "Optical drive"
|
|
45
|
+
|
|
46
|
+
return device.device_type
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _device_table(*, detailed: bool = False) -> Table:
|
|
50
|
+
table = Table(title="Block Devices")
|
|
51
|
+
table.add_column("Device", style="cyan")
|
|
52
|
+
table.add_column("Type")
|
|
53
|
+
table.add_column("Capacity", justify="right")
|
|
54
|
+
table.add_column("Filesystem")
|
|
55
|
+
table.add_column("Mountpoints")
|
|
56
|
+
|
|
57
|
+
if detailed:
|
|
58
|
+
table.add_column("Model")
|
|
59
|
+
table.add_column("Transport")
|
|
60
|
+
table.add_column("Media")
|
|
61
|
+
|
|
62
|
+
return table
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _add_device_rows(
|
|
66
|
+
table: Table,
|
|
67
|
+
devices: list[BlockDevice],
|
|
68
|
+
*,
|
|
69
|
+
detailed: bool = False,
|
|
70
|
+
depth: int = 0,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Add a recursive device hierarchy to a table."""
|
|
73
|
+
for device in devices:
|
|
74
|
+
name = f"{' ' * depth}{device.name}"
|
|
75
|
+
mountpoints = ", ".join(device.mountpoints) or "Not mounted"
|
|
76
|
+
|
|
77
|
+
row = [
|
|
78
|
+
name,
|
|
79
|
+
_device_type(device),
|
|
80
|
+
format_bytes(device.size_bytes),
|
|
81
|
+
device.filesystem_type or "Unknown",
|
|
82
|
+
mountpoints,
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if detailed:
|
|
86
|
+
if device.rotational is True:
|
|
87
|
+
media = "Rotational"
|
|
88
|
+
elif device.rotational is False:
|
|
89
|
+
media = "Non-rotational"
|
|
90
|
+
else:
|
|
91
|
+
media = "Unknown"
|
|
92
|
+
|
|
93
|
+
row.extend([
|
|
94
|
+
device.model or "Unknown",
|
|
95
|
+
device.transport or "Unknown",
|
|
96
|
+
media,
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
table.add_row(*row)
|
|
100
|
+
|
|
101
|
+
_add_device_rows(
|
|
102
|
+
table,
|
|
103
|
+
device.children,
|
|
104
|
+
detailed=detailed,
|
|
105
|
+
depth=depth + 1,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _filesystem_group_table() -> Table:
|
|
110
|
+
table = Table(title="Filesystem Capacity Summary")
|
|
111
|
+
table.add_column("Filesystem", style="cyan")
|
|
112
|
+
table.add_column("Category")
|
|
113
|
+
table.add_column("Source")
|
|
114
|
+
table.add_column("Total", justify="right")
|
|
115
|
+
table.add_column("Used", justify="right")
|
|
116
|
+
table.add_column("Available", justify="right")
|
|
117
|
+
table.add_column("Used %", justify="right")
|
|
118
|
+
table.add_column("Mountpoints")
|
|
119
|
+
return table
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _mount_table() -> Table:
|
|
123
|
+
table = Table(title="Mounted Filesystems")
|
|
124
|
+
table.add_column("Mountpoint", style="cyan")
|
|
125
|
+
table.add_column("Filesystem")
|
|
126
|
+
table.add_column("Category")
|
|
127
|
+
table.add_column("Source")
|
|
128
|
+
table.add_column("Total", justify="right")
|
|
129
|
+
table.add_column("Used", justify="right")
|
|
130
|
+
table.add_column("Available", justify="right")
|
|
131
|
+
table.add_column("Used %", justify="right")
|
|
132
|
+
table.add_column("Device ID")
|
|
133
|
+
table.add_column("Filesystem Root")
|
|
134
|
+
return table
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _add_group_rows(
|
|
138
|
+
table: Table,
|
|
139
|
+
inventory: StorageInventory,
|
|
140
|
+
*,
|
|
141
|
+
include_all: bool,
|
|
142
|
+
) -> int:
|
|
143
|
+
"""Add unique filesystem groups without repeating capacity."""
|
|
144
|
+
count = 0
|
|
145
|
+
|
|
146
|
+
for group in inventory.filesystem_groups:
|
|
147
|
+
filesystem = group.representative
|
|
148
|
+
category = classify_filesystem(filesystem)
|
|
149
|
+
|
|
150
|
+
if not include_all and category not in SUMMARY_CATEGORIES:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
table.add_row(
|
|
154
|
+
filesystem.filesystem_type,
|
|
155
|
+
category.title(),
|
|
156
|
+
filesystem.source,
|
|
157
|
+
format_bytes(group.total_bytes),
|
|
158
|
+
format_bytes(group.used_bytes),
|
|
159
|
+
format_bytes(group.available_bytes),
|
|
160
|
+
f"{filesystem.used_percent:.1f}%",
|
|
161
|
+
", ".join(group.mountpoints),
|
|
162
|
+
)
|
|
163
|
+
count += 1
|
|
164
|
+
|
|
165
|
+
return count
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_simple(
|
|
169
|
+
inventory: StorageInventory,
|
|
170
|
+
console: Console | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Render the default storage overview."""
|
|
173
|
+
console = console or Console()
|
|
174
|
+
|
|
175
|
+
console.print(
|
|
176
|
+
Panel(
|
|
177
|
+
"[bold cyan]lsdisk[/bold cyan]\n"
|
|
178
|
+
"Cross-distro Linux Storage Explorer",
|
|
179
|
+
title="Storage Overview",
|
|
180
|
+
border_style="cyan",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
physical_disks = [
|
|
185
|
+
device for device in inventory.devices
|
|
186
|
+
if device.is_physical_disk
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
if physical_disks:
|
|
190
|
+
table = _device_table()
|
|
191
|
+
_add_device_rows(table, physical_disks)
|
|
192
|
+
console.print(table)
|
|
193
|
+
else:
|
|
194
|
+
console.print("[yellow]No physical disks detected.[/yellow]")
|
|
195
|
+
|
|
196
|
+
filesystems = _filesystem_group_table()
|
|
197
|
+
count = _add_group_rows(
|
|
198
|
+
filesystems,
|
|
199
|
+
inventory,
|
|
200
|
+
include_all=False,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if count:
|
|
204
|
+
console.print(filesystems)
|
|
205
|
+
else:
|
|
206
|
+
console.print(
|
|
207
|
+
"[yellow]No persistent or network filesystems detected.[/yellow]"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def render_all(
|
|
212
|
+
inventory: StorageInventory,
|
|
213
|
+
console: Console | None = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Render the complete storage inventory."""
|
|
216
|
+
console = console or Console()
|
|
217
|
+
|
|
218
|
+
console.print(
|
|
219
|
+
Panel(
|
|
220
|
+
"[bold cyan]lsdisk[/bold cyan]\n"
|
|
221
|
+
"Complete storage inventory",
|
|
222
|
+
title="Storage Overview",
|
|
223
|
+
border_style="cyan",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
devices = _device_table(detailed=True)
|
|
228
|
+
_add_device_rows(
|
|
229
|
+
devices,
|
|
230
|
+
inventory.devices,
|
|
231
|
+
detailed=True,
|
|
232
|
+
)
|
|
233
|
+
console.print(devices)
|
|
234
|
+
|
|
235
|
+
mounts = _mount_table()
|
|
236
|
+
for filesystem in inventory.filesystems:
|
|
237
|
+
mounts.add_row(
|
|
238
|
+
filesystem.mountpoint,
|
|
239
|
+
filesystem.filesystem_type,
|
|
240
|
+
classify_filesystem(filesystem).title(),
|
|
241
|
+
filesystem.source,
|
|
242
|
+
format_bytes(filesystem.total_bytes),
|
|
243
|
+
format_bytes(filesystem.used_bytes),
|
|
244
|
+
format_bytes(filesystem.available_bytes),
|
|
245
|
+
f"{filesystem.used_percent:.1f}%",
|
|
246
|
+
filesystem.device_id or "Unknown",
|
|
247
|
+
filesystem.filesystem_root,
|
|
248
|
+
)
|
|
249
|
+
console.print(mounts)
|
|
250
|
+
|
|
251
|
+
groups = _filesystem_group_table()
|
|
252
|
+
_add_group_rows(groups, inventory, include_all=True)
|
|
253
|
+
console.print(groups)
|
|
254
|
+
|
|
255
|
+
console.print(
|
|
256
|
+
f"\nDiscovered mountpoints: {len(inventory.filesystems)}"
|
|
257
|
+
f" | Unique filesystem groups: {len(inventory.filesystem_groups)}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _source_key(source: str) -> str:
|
|
262
|
+
"""Normalize a local device path for matching."""
|
|
263
|
+
if not source.startswith("/dev/"):
|
|
264
|
+
return source
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
return str(Path(source).resolve())
|
|
268
|
+
except (OSError, RuntimeError):
|
|
269
|
+
return source
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _percentage(used: int, total: int) -> str:
|
|
273
|
+
"""Format a percentage without hiding small nonzero values."""
|
|
274
|
+
if total <= 0:
|
|
275
|
+
return "N/A"
|
|
276
|
+
|
|
277
|
+
percent = used / total * 100
|
|
278
|
+
|
|
279
|
+
if 0 < percent < 0.1:
|
|
280
|
+
return "<0.1%"
|
|
281
|
+
|
|
282
|
+
return f"{percent:.1f}%"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _usage_color(percentage: float) -> str:
|
|
286
|
+
"""Select a color based on filesystem utilization."""
|
|
287
|
+
if percentage >= 90:
|
|
288
|
+
return "red"
|
|
289
|
+
if percentage >= 70:
|
|
290
|
+
return "yellow"
|
|
291
|
+
return "green"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _local_filesystem_groups(
|
|
295
|
+
inventory: StorageInventory,
|
|
296
|
+
):
|
|
297
|
+
"""Return mounted local persistent filesystems."""
|
|
298
|
+
return [
|
|
299
|
+
group
|
|
300
|
+
for group in inventory.filesystem_groups
|
|
301
|
+
if (
|
|
302
|
+
classify_filesystem(group.representative) == "persistent"
|
|
303
|
+
and group.representative.source.startswith("/dev/")
|
|
304
|
+
)
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _groups_by_source(inventory: StorageInventory):
|
|
309
|
+
"""Index local filesystem groups by their source device."""
|
|
310
|
+
result = {}
|
|
311
|
+
|
|
312
|
+
for group in _local_filesystem_groups(inventory):
|
|
313
|
+
key = _source_key(group.representative.source)
|
|
314
|
+
result.setdefault(key, []).append(group)
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _usage_bar(used: int, total: int, width: int = 24) -> Text:
|
|
320
|
+
"""Create a compact text-based utilization bar."""
|
|
321
|
+
bar = Text()
|
|
322
|
+
|
|
323
|
+
if total <= 0:
|
|
324
|
+
bar.append("Usage data unavailable", style="dim")
|
|
325
|
+
return bar
|
|
326
|
+
|
|
327
|
+
percentage = min(100.0, max(0.0, used / total * 100))
|
|
328
|
+
filled = round(width * percentage / 100)
|
|
329
|
+
|
|
330
|
+
bar.append("█" * filled, style=_usage_color(percentage))
|
|
331
|
+
bar.append("░" * (width - filled), style="dim")
|
|
332
|
+
bar.append(
|
|
333
|
+
f" {percentage:.1f}%",
|
|
334
|
+
style=_usage_color(percentage),
|
|
335
|
+
)
|
|
336
|
+
return bar
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _add_compact_device(
|
|
340
|
+
parent: Tree,
|
|
341
|
+
device: BlockDevice,
|
|
342
|
+
groups_by_source: dict,
|
|
343
|
+
*,
|
|
344
|
+
detailed: bool,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Add a device and its filesystem information to a compact tree."""
|
|
347
|
+
label = Text()
|
|
348
|
+
label.append(device.name, style="bold cyan")
|
|
349
|
+
label.append(f" {format_bytes(device.size_bytes)}")
|
|
350
|
+
|
|
351
|
+
if device.device_type == "disk":
|
|
352
|
+
if device.physical:
|
|
353
|
+
label.append(" Physical disk", style="green")
|
|
354
|
+
elif device.physical is False:
|
|
355
|
+
label.append(" Virtual/logical disk", style="dim")
|
|
356
|
+
else:
|
|
357
|
+
label.append(" Disk type unverified", style="yellow")
|
|
358
|
+
|
|
359
|
+
if device.rotational is True:
|
|
360
|
+
label.append(" HDD", style="yellow")
|
|
361
|
+
elif device.rotational is False and device.physical:
|
|
362
|
+
label.append(" SSD/Flash", style="green")
|
|
363
|
+
|
|
364
|
+
if device.model:
|
|
365
|
+
label.append(f" {device.model}", style="dim")
|
|
366
|
+
|
|
367
|
+
if detailed and device.transport:
|
|
368
|
+
label.append(f" [{device.transport}]", style="dim")
|
|
369
|
+
|
|
370
|
+
elif device.device_type == "part":
|
|
371
|
+
label.append(
|
|
372
|
+
f" {device.filesystem_type or 'Unknown filesystem'}",
|
|
373
|
+
style="cyan",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
elif device.device_type == "rom":
|
|
377
|
+
label.append(" Optical drive", style="dim")
|
|
378
|
+
|
|
379
|
+
node = parent.add(label)
|
|
380
|
+
|
|
381
|
+
if device.is_partition:
|
|
382
|
+
groups = groups_by_source.get(_source_key(device.path), [])
|
|
383
|
+
|
|
384
|
+
if groups:
|
|
385
|
+
for group in groups:
|
|
386
|
+
filesystem = group.representative
|
|
387
|
+
percentage = (
|
|
388
|
+
group.used_bytes / group.total_bytes * 100
|
|
389
|
+
if group.total_bytes
|
|
390
|
+
else 0.0
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
usage = Text("Usage: ")
|
|
394
|
+
usage.append(
|
|
395
|
+
f"{format_bytes(group.used_bytes)} used",
|
|
396
|
+
style=_usage_color(percentage),
|
|
397
|
+
)
|
|
398
|
+
#usage.append(
|
|
399
|
+
# f" ({_percentage(group.used_bytes, group.total_bytes)})"
|
|
400
|
+
#)
|
|
401
|
+
usage.append(
|
|
402
|
+
f" | {format_bytes(group.available_bytes)} available"
|
|
403
|
+
)
|
|
404
|
+
node.add(usage)
|
|
405
|
+
node.add(
|
|
406
|
+
_usage_bar(
|
|
407
|
+
group.used_bytes,
|
|
408
|
+
group.total_bytes,
|
|
409
|
+
width=24,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
mounts = ", ".join(group.mountpoints) or "Not mounted"
|
|
414
|
+
mount_text = Text("Mounts: ", style="dim")
|
|
415
|
+
mount_text.append(mounts)
|
|
416
|
+
node.add(mount_text)
|
|
417
|
+
|
|
418
|
+
else:
|
|
419
|
+
status = Text("Usage unavailable", style="dim")
|
|
420
|
+
mounts = ", ".join(device.mountpoints) or "Not mounted"
|
|
421
|
+
status.append(f" | {mounts}")
|
|
422
|
+
node.add(status)
|
|
423
|
+
|
|
424
|
+
for child in device.children:
|
|
425
|
+
_add_compact_device(
|
|
426
|
+
node,
|
|
427
|
+
child,
|
|
428
|
+
groups_by_source,
|
|
429
|
+
detailed=detailed,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _render_compact_summary(
|
|
434
|
+
inventory: StorageInventory,
|
|
435
|
+
console: Console,
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Display hardware counts and unique local filesystem capacity."""
|
|
438
|
+
devices = list(walk_devices(inventory.devices))
|
|
439
|
+
physical_disks = [
|
|
440
|
+
device for device in devices if device.is_physical_disk
|
|
441
|
+
]
|
|
442
|
+
partitions = [device for device in devices if device.is_partition]
|
|
443
|
+
|
|
444
|
+
physical_capacity = sum(
|
|
445
|
+
device.size_bytes for device in physical_disks
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
groups = _local_filesystem_groups(inventory)
|
|
449
|
+
total = sum(group.total_bytes for group in groups)
|
|
450
|
+
used = sum(group.used_bytes for group in groups)
|
|
451
|
+
available = sum(group.available_bytes for group in groups)
|
|
452
|
+
|
|
453
|
+
summary = Text()
|
|
454
|
+
summary.append(f"Physical disks: {len(physical_disks)}")
|
|
455
|
+
summary.append(f" Partitions: {len(partitions)}")
|
|
456
|
+
summary.append(
|
|
457
|
+
f" Physical capacity: {format_bytes(physical_capacity)}\n"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if total > 0:
|
|
461
|
+
summary.append(
|
|
462
|
+
f"Mounted local filesystems: {format_bytes(total)} total\n"
|
|
463
|
+
)
|
|
464
|
+
summary.append("Used: ", style="dim")
|
|
465
|
+
summary.append(
|
|
466
|
+
f"{format_bytes(used)}", # ({_percentage(used, total)})",
|
|
467
|
+
style=_usage_color(used / total * 100),
|
|
468
|
+
)
|
|
469
|
+
summary.append(
|
|
470
|
+
f" Available to users: {format_bytes(available)}\n"
|
|
471
|
+
)
|
|
472
|
+
summary.append_text(_usage_bar(used, total))
|
|
473
|
+
else:
|
|
474
|
+
summary.append(
|
|
475
|
+
"Mounted local filesystem usage is unavailable.",
|
|
476
|
+
style="dim",
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
console.print(
|
|
480
|
+
Panel(
|
|
481
|
+
summary,
|
|
482
|
+
title="Storage Summary",
|
|
483
|
+
border_style="cyan",
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _render_compact_mounts(
|
|
489
|
+
inventory: StorageInventory,
|
|
490
|
+
console: Console,
|
|
491
|
+
) -> None:
|
|
492
|
+
"""Display filesystem groups with their mountpoints underneath."""
|
|
493
|
+
root = Tree(
|
|
494
|
+
Text("Filesystems and Mountpoints", style="bold cyan"),
|
|
495
|
+
guide_style="dim",
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
category_order = {
|
|
499
|
+
"persistent": 0,
|
|
500
|
+
"network": 1,
|
|
501
|
+
"virtual": 2,
|
|
502
|
+
"unknown": 3,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
groups = sorted(
|
|
506
|
+
inventory.filesystem_groups,
|
|
507
|
+
key=lambda group: (
|
|
508
|
+
category_order.get(
|
|
509
|
+
classify_filesystem(group.representative), 4
|
|
510
|
+
),
|
|
511
|
+
group.representative.filesystem_type,
|
|
512
|
+
group.representative.source,
|
|
513
|
+
),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
for group in groups:
|
|
517
|
+
filesystem = group.representative
|
|
518
|
+
category = classify_filesystem(filesystem)
|
|
519
|
+
|
|
520
|
+
label = Text()
|
|
521
|
+
label.append(filesystem.filesystem_type, style="bold cyan")
|
|
522
|
+
label.append(" ")
|
|
523
|
+
label.append(filesystem.source, style="bold")
|
|
524
|
+
label.append(f" [{category}] ", style="dim")
|
|
525
|
+
label.append(f" {format_bytes(group.total_bytes)}")
|
|
526
|
+
|
|
527
|
+
node = root.add(label)
|
|
528
|
+
|
|
529
|
+
if group.total_bytes > 0:
|
|
530
|
+
percentage = group.used_bytes / group.total_bytes * 100
|
|
531
|
+
|
|
532
|
+
usage = Text()
|
|
533
|
+
usage.append("Used: ", style="dim")
|
|
534
|
+
usage.append(
|
|
535
|
+
format_bytes(group.used_bytes),
|
|
536
|
+
style=_usage_color(percentage),
|
|
537
|
+
)
|
|
538
|
+
usage.append(f" ({_percentage(group.used_bytes, group.total_bytes)})")
|
|
539
|
+
usage.append(" | Available: ")
|
|
540
|
+
usage.append(
|
|
541
|
+
format_bytes(group.available_bytes),
|
|
542
|
+
style="green",
|
|
543
|
+
)
|
|
544
|
+
node.add(usage)
|
|
545
|
+
node.add(_usage_bar(group.used_bytes, group.total_bytes, width=24))
|
|
546
|
+
else:
|
|
547
|
+
node.add(
|
|
548
|
+
Text(
|
|
549
|
+
"Capacity statistics unavailable or not applicable",
|
|
550
|
+
style="dim",
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
identity = (
|
|
555
|
+
filesystem.device_id or filesystem.source,
|
|
556
|
+
filesystem.filesystem_type,
|
|
557
|
+
filesystem.source,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
mounts = [
|
|
561
|
+
item
|
|
562
|
+
for item in inventory.filesystems
|
|
563
|
+
if (
|
|
564
|
+
(
|
|
565
|
+
item.device_id or item.source,
|
|
566
|
+
item.filesystem_type,
|
|
567
|
+
item.source,
|
|
568
|
+
) == identity
|
|
569
|
+
)
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
for item in sorted(mounts, key=lambda mount: mount.mountpoint):
|
|
573
|
+
mount_label = Text()
|
|
574
|
+
mount_label.append(item.mountpoint, style="cyan")
|
|
575
|
+
|
|
576
|
+
if item.filesystem_root != "/":
|
|
577
|
+
mount_label.append(
|
|
578
|
+
f" (subvolume root: {item.filesystem_root})",
|
|
579
|
+
style="dim",
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
node.add(mount_label)
|
|
583
|
+
|
|
584
|
+
console.print(
|
|
585
|
+
Panel(
|
|
586
|
+
root,
|
|
587
|
+
title="Filesystem Details",
|
|
588
|
+
subtitle="Capacity is shown once per filesystem group",
|
|
589
|
+
border_style="blue",
|
|
590
|
+
)
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
def render_compact(
|
|
594
|
+
inventory: StorageInventory,
|
|
595
|
+
console: Console | None = None,
|
|
596
|
+
*,
|
|
597
|
+
include_all: bool = False,
|
|
598
|
+
) -> None:
|
|
599
|
+
"""Render the default compact view or the complete compact inventory."""
|
|
600
|
+
console = console or Console()
|
|
601
|
+
|
|
602
|
+
console.print(
|
|
603
|
+
Panel(
|
|
604
|
+
Text.assemble(
|
|
605
|
+
("lsdisk", "bold cyan"),
|
|
606
|
+
(" Linux Storage Explorer", "white"),
|
|
607
|
+
),
|
|
608
|
+
border_style="cyan",
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
_render_compact_summary(inventory, console)
|
|
613
|
+
|
|
614
|
+
groups_by_source = _groups_by_source(inventory)
|
|
615
|
+
|
|
616
|
+
if include_all:
|
|
617
|
+
visible_devices = inventory.devices
|
|
618
|
+
title = "All Block Devices"
|
|
619
|
+
else:
|
|
620
|
+
visible_devices = [
|
|
621
|
+
device
|
|
622
|
+
for device in inventory.devices
|
|
623
|
+
if device.is_physical_disk
|
|
624
|
+
]
|
|
625
|
+
title = "Physical Storage"
|
|
626
|
+
|
|
627
|
+
root = Tree(Text(title, style="bold cyan"), guide_style="dim")
|
|
628
|
+
|
|
629
|
+
for device in visible_devices:
|
|
630
|
+
_add_compact_device(
|
|
631
|
+
root,
|
|
632
|
+
device,
|
|
633
|
+
groups_by_source,
|
|
634
|
+
detailed=include_all,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
if visible_devices:
|
|
638
|
+
console.print(Panel(root, border_style="blue"))
|
|
639
|
+
else:
|
|
640
|
+
console.print("[yellow]No matching block devices detected.[/yellow]")
|
|
641
|
+
|
|
642
|
+
if include_all:
|
|
643
|
+
_render_compact_mounts(inventory, console)
|