procclean 1.2.0__tar.gz

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.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: procclean
3
+ Version: 1.2.0
4
+ Summary: Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them
5
+ Keywords: cleanup,kill,memory,orphan,process,terminal,tui
6
+ Author: Kaj Kowalski
7
+ Author-email: Kaj Kowalski <info@kajkowalski.nl>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Classifier: Topic :: Utilities
20
+ Requires-Dist: psutil>=7.2.1
21
+ Requires-Dist: tabulate>=0.9.0
22
+ Requires-Dist: textual>=7.0.0
23
+ Requires-Python: >=3.14
24
+ Project-URL: Homepage, https://procclean.kjanat.com
25
+ Project-URL: Repository, https://github.com/kjanat/procclean
26
+ Description-Content-Type: text/markdown
27
+
28
+ <p align="center">
29
+ <img src="logo/procclean-transparent.svg" alt="procclean" width="500">
30
+ </p>
31
+
32
+ <p align="center">
33
+ <em>Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them.</em>
34
+ </p>
35
+
36
+ <p align="center">
37
+ <a href="https://github.com/kjanat/procclean/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/procclean" alt="License"></a>
38
+ <a href="https://github.com/kjanat/procclean/releases"><img src="https://img.shields.io/github/v/release/kjanat/procclean" alt="Release"></a>
39
+ <img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
40
+ <img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
41
+ </p>
42
+
43
+ ## Features
44
+
45
+ - **Memory overview** - Real-time total/used/free/swap display
46
+ - **Multiple views** - All processes, Orphaned, Process Groups, High Memory
47
+ (>500MB)
48
+ - **Orphan detection** - Finds processes whose parent died (PPID=1)
49
+ - **Tmux awareness** - Won't flag tmux processes as orphan candidates
50
+ - **Batch operations** - Select multiple processes and kill them at once
51
+ - **Process grouping** - Find duplicate/similar processes consuming resources
52
+ - **CLI mode** - Scriptable commands with JSON/CSV/Markdown output
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ uv tool install git+https://github.com/kjanat/procclean
58
+ ```
59
+
60
+ Or run directly:
61
+
62
+ ```bash
63
+ uvx git+https://github.com/kjanat/procclean
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ### TUI Mode (default)
69
+
70
+ ```bash
71
+ procclean
72
+ ```
73
+
74
+ ### CLI Commands
75
+
76
+ ```bash
77
+ procclean list # List processes (table)
78
+ procclean list -f json|csv|md # Different output formats
79
+ procclean list -s mem|cpu|pid|name|cwd # Sort by field
80
+ procclean list -o # Orphans only
81
+ procclean list -m # High memory only
82
+ procclean list -k # Killable orphans only
83
+ procclean list --cwd # Filter by current directory
84
+ procclean list --cwd /path/to/dir # Filter by specific cwd
85
+
86
+ procclean groups # Show process groups
87
+
88
+ procclean kill <PID> [PID...] # Kill process(es)
89
+ procclean kill -f <PID> # Force kill (SIGKILL)
90
+ procclean kill --cwd /path -y # Kill all in cwd (skip confirm)
91
+ procclean kill -k -y # Kill all killable orphans
92
+ procclean kill -k --preview # Preview what would be killed
93
+
94
+ procclean mem # Show memory summary
95
+ ```
96
+
97
+ ## TUI Keybindings
98
+
99
+ | Key | Action |
100
+ | ------- | ----------------------- |
101
+ | `q` | Quit |
102
+ | `r` | Refresh |
103
+ | `k` | Kill selected (SIGTERM) |
104
+ | `K` | Force kill (SIGKILL) |
105
+ | `o` | Show orphans |
106
+ | `a` | Show all |
107
+ | `g` | Show groups |
108
+ | `w` | Filter by selected cwd |
109
+ | `W` | Clear cwd filter |
110
+ | `Space` | Toggle selection |
111
+ | `s` | Select all visible |
112
+ | `c` | Clear selection |
113
+ | `1` | Sort by memory |
114
+ | `2` | Sort by CPU |
115
+ | `3` | Sort by PID |
116
+ | `4` | Sort by name |
117
+ | `5` | Sort by cwd |
118
+ | `!` | Reverse sort order |
119
+
120
+ ## Views
121
+
122
+ - **All Processes** - All user processes sorted by memory usage
123
+ - **Orphaned** - Processes with PPID=1 (parent died)
124
+ - **Process Groups** - Similar processes grouped together
125
+ - **High Memory** - Processes using >500MB RAM
126
+
127
+ ## Output Formats
128
+
129
+ CLI supports multiple output formats via `-f`:
130
+
131
+ - `table` - Human-readable table (default)
132
+ - `json` - JSON array for scripting
133
+ - `csv` - CSV for spreadsheets
134
+ - `md` - Markdown table
135
+
136
+ ## Requirements
137
+
138
+ - Python 3.14+
139
+ - Linux (uses `/proc` filesystem)
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ git clone https://github.com/kjanat/procclean
145
+ cd procclean
146
+ uv sync
147
+ uv run pre-commit install
148
+ ```
149
+
150
+ Run tests:
151
+
152
+ ```bash
153
+ uv run pytest
154
+ ```
155
+
156
+ ## License
157
+
158
+ [MIT][license]
159
+
160
+ <!--link definitions-->
161
+
162
+ [license]: https://github.com/kjanat/procclean/blob/master/LICENSE "MIT License"
163
+
164
+ <!--markdownlint-disable-file MD033 MD041-->
@@ -0,0 +1,137 @@
1
+ <p align="center">
2
+ <img src="logo/procclean-transparent.svg" alt="procclean" width="500">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <em>Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them.</em>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/kjanat/procclean/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/procclean" alt="License"></a>
11
+ <a href="https://github.com/kjanat/procclean/releases"><img src="https://img.shields.io/github/v/release/kjanat/procclean" alt="Release"></a>
12
+ <img src="https://img.shields.io/badge/python-3.14%2B-blue" alt="Python 3.14+">
13
+ <img src="https://img.shields.io/badge/platform-linux-lightgrey" alt="Linux">
14
+ </p>
15
+
16
+ ## Features
17
+
18
+ - **Memory overview** - Real-time total/used/free/swap display
19
+ - **Multiple views** - All processes, Orphaned, Process Groups, High Memory
20
+ (>500MB)
21
+ - **Orphan detection** - Finds processes whose parent died (PPID=1)
22
+ - **Tmux awareness** - Won't flag tmux processes as orphan candidates
23
+ - **Batch operations** - Select multiple processes and kill them at once
24
+ - **Process grouping** - Find duplicate/similar processes consuming resources
25
+ - **CLI mode** - Scriptable commands with JSON/CSV/Markdown output
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ uv tool install git+https://github.com/kjanat/procclean
31
+ ```
32
+
33
+ Or run directly:
34
+
35
+ ```bash
36
+ uvx git+https://github.com/kjanat/procclean
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### TUI Mode (default)
42
+
43
+ ```bash
44
+ procclean
45
+ ```
46
+
47
+ ### CLI Commands
48
+
49
+ ```bash
50
+ procclean list # List processes (table)
51
+ procclean list -f json|csv|md # Different output formats
52
+ procclean list -s mem|cpu|pid|name|cwd # Sort by field
53
+ procclean list -o # Orphans only
54
+ procclean list -m # High memory only
55
+ procclean list -k # Killable orphans only
56
+ procclean list --cwd # Filter by current directory
57
+ procclean list --cwd /path/to/dir # Filter by specific cwd
58
+
59
+ procclean groups # Show process groups
60
+
61
+ procclean kill <PID> [PID...] # Kill process(es)
62
+ procclean kill -f <PID> # Force kill (SIGKILL)
63
+ procclean kill --cwd /path -y # Kill all in cwd (skip confirm)
64
+ procclean kill -k -y # Kill all killable orphans
65
+ procclean kill -k --preview # Preview what would be killed
66
+
67
+ procclean mem # Show memory summary
68
+ ```
69
+
70
+ ## TUI Keybindings
71
+
72
+ | Key | Action |
73
+ | ------- | ----------------------- |
74
+ | `q` | Quit |
75
+ | `r` | Refresh |
76
+ | `k` | Kill selected (SIGTERM) |
77
+ | `K` | Force kill (SIGKILL) |
78
+ | `o` | Show orphans |
79
+ | `a` | Show all |
80
+ | `g` | Show groups |
81
+ | `w` | Filter by selected cwd |
82
+ | `W` | Clear cwd filter |
83
+ | `Space` | Toggle selection |
84
+ | `s` | Select all visible |
85
+ | `c` | Clear selection |
86
+ | `1` | Sort by memory |
87
+ | `2` | Sort by CPU |
88
+ | `3` | Sort by PID |
89
+ | `4` | Sort by name |
90
+ | `5` | Sort by cwd |
91
+ | `!` | Reverse sort order |
92
+
93
+ ## Views
94
+
95
+ - **All Processes** - All user processes sorted by memory usage
96
+ - **Orphaned** - Processes with PPID=1 (parent died)
97
+ - **Process Groups** - Similar processes grouped together
98
+ - **High Memory** - Processes using >500MB RAM
99
+
100
+ ## Output Formats
101
+
102
+ CLI supports multiple output formats via `-f`:
103
+
104
+ - `table` - Human-readable table (default)
105
+ - `json` - JSON array for scripting
106
+ - `csv` - CSV for spreadsheets
107
+ - `md` - Markdown table
108
+
109
+ ## Requirements
110
+
111
+ - Python 3.14+
112
+ - Linux (uses `/proc` filesystem)
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ git clone https://github.com/kjanat/procclean
118
+ cd procclean
119
+ uv sync
120
+ uv run pre-commit install
121
+ ```
122
+
123
+ Run tests:
124
+
125
+ ```bash
126
+ uv run pytest
127
+ ```
128
+
129
+ ## License
130
+
131
+ [MIT][license]
132
+
133
+ <!--link definitions-->
134
+
135
+ [license]: https://github.com/kjanat/procclean/blob/master/LICENSE "MIT License"
136
+
137
+ <!--markdownlint-disable-file MD033 MD041-->
@@ -0,0 +1,89 @@
1
+ #:schema https://json.schemastore.org/pyproject.json
2
+ #:tombi schema.strict = true
3
+
4
+ [project]
5
+ name = "procclean"
6
+ version = "1.2.0"
7
+ description = "Interactive TUI for exploring and cleaning up processes - find orphans, memory hogs, and kill them"
8
+ readme = "README.md"
9
+ requires-python = ">=3.14"
10
+ license = "MIT"
11
+ authors = [{ name = "Kaj Kowalski", email = "info@kajkowalski.nl" }]
12
+ keywords = ["cleanup", "kill", "memory", "orphan", "process", "terminal", "tui"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: System Administrators",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Topic :: System :: Monitoring",
23
+ "Topic :: System :: Systems Administration",
24
+ "Topic :: Utilities",
25
+ ]
26
+ dependencies = [
27
+ "psutil>=7.2.1",
28
+ "tabulate>=0.9.0",
29
+ "textual>=7.0.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://procclean.kjanat.com"
34
+ Repository = "https://github.com/kjanat/procclean"
35
+
36
+ [project.scripts]
37
+ procclean = "procclean:main"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pre-commit>=4.5.1",
42
+ ]
43
+ lint = [
44
+ "pylint>=4.0.4",
45
+ "ruff>=0.14.10",
46
+ "tombi>=0.7.14",
47
+ "ty>=0.0.8",
48
+ ]
49
+ mypy = [
50
+ "mypy>=1.19.1",
51
+ "types-psutil>=7.2.1.20251231",
52
+ "types-tabulate>=0.9.0.20241207",
53
+ ]
54
+ site = [
55
+ "markdown-it-py[plugins]>=4.0.0",
56
+ "mkdocs>=1.6.1",
57
+ "mkdocs-material[imaging]>=9.7.1",
58
+ "mkdocs-rich-argparse",
59
+ ]
60
+ test = [
61
+ "pytest>=9.0.2",
62
+ "pytest-asyncio>=1.3.0",
63
+ "pytest-cov>=7.0.0",
64
+ "pytest-xdist[psutil]>=3.8.0",
65
+ ]
66
+ tui = [
67
+ "textual-dev>=1.8.0",
68
+ ]
69
+
70
+ [build-system]
71
+ requires = ["uv_build>=0.9.21,<0.10.0"]
72
+ build-backend = "uv_build"
73
+
74
+ [tool.pytest]
75
+ addopts = ["-n", "logical"]
76
+ asyncio_mode = "strict"
77
+
78
+ [tool.uv]
79
+ environments = ["sys_platform == 'linux'"]
80
+ default-groups = "all"
81
+
82
+ [tool.uv.sources]
83
+ mkdocs-rich-argparse = { git = "https://github.com/kjanat/mkdocs_rich_argparse.git", branch = "prog-style" }
84
+
85
+ [[tool.uv.index]]
86
+ name = "testpypi"
87
+ url = "https://test.pypi.org/simple/"
88
+ publish-url = "https://test.pypi.org/legacy/"
89
+ explicit = true
@@ -0,0 +1,11 @@
1
+ """Process cleanup TUI application."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("procclean")
6
+
7
+ # Re-export main entry point and core types
8
+ from procclean.__main__ import main
9
+ from procclean.core import ProcessInfo
10
+
11
+ __all__ = ["ProcessInfo", "__version__", "main"]
@@ -0,0 +1,22 @@
1
+ """Entry point for procclean - runs as python -m procclean or via console script."""
2
+
3
+ from .cli import run_cli
4
+ from .tui import ProcessCleanerApp
5
+
6
+
7
+ def main() -> None:
8
+ """Dispatch to CLI or run TUI.
9
+
10
+ Raises:
11
+ SystemExit: When CLI command returns non-zero exit code.
12
+ """
13
+ result = run_cli()
14
+ if result == -1:
15
+ # No subcommand - run TUI
16
+ ProcessCleanerApp().run()
17
+ else:
18
+ raise SystemExit(result)
19
+
20
+
21
+ if __name__ == "__main__":
22
+ main()
@@ -0,0 +1,27 @@
1
+ """CLI interface for procclean."""
2
+
3
+ # Internal helpers - exported for testing
4
+ from .commands import (
5
+ _confirm_kill,
6
+ _do_preview,
7
+ _get_kill_targets,
8
+ cmd_groups,
9
+ cmd_kill,
10
+ cmd_list,
11
+ cmd_memory,
12
+ get_filtered_processes,
13
+ )
14
+ from .parser import create_parser, run_cli
15
+
16
+ __all__ = [
17
+ "_confirm_kill",
18
+ "_do_preview",
19
+ "_get_kill_targets",
20
+ "cmd_groups",
21
+ "cmd_kill",
22
+ "cmd_list",
23
+ "cmd_memory",
24
+ "create_parser",
25
+ "get_filtered_processes",
26
+ "run_cli",
27
+ ]
@@ -0,0 +1,213 @@
1
+ """CLI command handlers."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from rich import print # pylint: disable=redefined-builtin
9
+
10
+ from procclean.core import (
11
+ PREVIEW_LIMIT,
12
+ filter_by_cwd,
13
+ filter_high_memory,
14
+ filter_killable,
15
+ filter_orphans,
16
+ find_similar_processes,
17
+ get_memory_summary,
18
+ get_process_list,
19
+ kill_processes,
20
+ sort_processes,
21
+ )
22
+ from procclean.formatters import format_output
23
+
24
+
25
+ def cmd_list(args: argparse.Namespace) -> int:
26
+ """List processes command.
27
+
28
+ Returns:
29
+ int: Exit code (0 on success).
30
+ """
31
+ procs = get_filtered_processes(args)
32
+
33
+ # Apply sorting
34
+ reverse = not args.ascending
35
+ procs = sort_processes(procs, sort_by=args.sort, reverse=reverse)
36
+
37
+ # Limit output
38
+ if args.limit:
39
+ procs = procs[: args.limit]
40
+
41
+ # Parse columns
42
+ columns = args.columns.split(",") if args.columns else None
43
+
44
+ print(format_output(procs, args.format, columns=columns))
45
+ return 0
46
+
47
+
48
+ def cmd_groups(args: argparse.Namespace) -> int:
49
+ """Show grouped processes command.
50
+
51
+ Returns:
52
+ int: Exit code (0 on success).
53
+ """
54
+ procs = get_process_list(min_memory_mb=args.min_memory)
55
+ groups = find_similar_processes(procs)
56
+
57
+ if not groups:
58
+ print("No process groups found.")
59
+ return 0
60
+
61
+ if args.format == "json":
62
+ data = {
63
+ cmd: [
64
+ {"pid": p.pid, "name": p.name, "rss_mb": round(p.rss_mb, 2)}
65
+ for p in group_procs
66
+ ]
67
+ for cmd, group_procs in groups.items()
68
+ }
69
+ print(json.dumps(data, indent=2))
70
+ else:
71
+ for cmd, group_procs in sorted(
72
+ groups.items(), key=lambda x: sum(p.rss_mb for p in x[1]), reverse=True
73
+ ):
74
+ total_mb = sum(p.rss_mb for p in group_procs)
75
+ print(f"\n{cmd} ({len(group_procs)} processes, {total_mb:.1f} MB total)")
76
+ for p in sorted(group_procs, key=lambda x: x.rss_mb, reverse=True):
77
+ print(f" PID {p.pid}: {p.rss_mb:.1f} MB")
78
+
79
+ return 0
80
+
81
+
82
+ def get_filtered_processes(args: argparse.Namespace) -> list:
83
+ """Get processes with all filters from args applied.
84
+
85
+ Returns:
86
+ list: Filtered list of processes.
87
+ """
88
+ procs = get_process_list(min_memory_mb=getattr(args, "min_memory", 5.0))
89
+
90
+ # Apply cwd filter
91
+ if getattr(args, "cwd", None) is not None:
92
+ cwd_path = args.cwd or str(Path.cwd())
93
+ procs = filter_by_cwd(procs, cwd_path)
94
+
95
+ # Apply preset filters
96
+ filt = getattr(args, "filter", None)
97
+ threshold = getattr(args, "high_memory_threshold", 500.0)
98
+ if filt == "killable" or getattr(args, "killable", False):
99
+ procs = filter_killable(procs)
100
+ elif filt == "orphans" or getattr(args, "orphans", False):
101
+ procs = filter_orphans(procs)
102
+ elif filt == "high-memory" or getattr(args, "high_memory", False):
103
+ procs = filter_high_memory(procs, threshold_mb=threshold)
104
+
105
+ return procs
106
+
107
+
108
+ def _get_kill_targets(args: argparse.Namespace) -> list:
109
+ """Get target processes for kill command from PIDs or filters.
110
+
111
+ Returns:
112
+ list: Target processes to kill.
113
+ """
114
+ if args.pids:
115
+ all_procs = get_process_list(min_memory_mb=0)
116
+ pid_set = set(args.pids)
117
+ procs = [p for p in all_procs if p.pid in pid_set]
118
+ found_pids = {p.pid for p in procs}
119
+ for pid in args.pids:
120
+ if pid not in found_pids:
121
+ print(f"Warning: PID {pid} not found")
122
+ return procs
123
+ return get_filtered_processes(args)
124
+
125
+
126
+ def _do_preview(args: argparse.Namespace, procs: list) -> int:
127
+ """Show preview of what would be killed.
128
+
129
+ Returns:
130
+ int: Exit code (0 on success).
131
+ """
132
+ if hasattr(args, "sort") and args.sort:
133
+ procs = sort_processes(procs, sort_by=args.sort, reverse=True)
134
+ if hasattr(args, "limit") and args.limit:
135
+ procs = procs[: args.limit]
136
+ columns = args.columns.split(",") if getattr(args, "columns", None) else None
137
+ fmt = getattr(args, "out_format", "table")
138
+ print(format_output(procs, fmt, columns=columns))
139
+ print(f"\n{len(procs)} process(es) would be killed.")
140
+ return 0
141
+
142
+
143
+ def _confirm_kill(args: argparse.Namespace, procs: list) -> bool:
144
+ """Prompt for kill confirmation.
145
+
146
+ Args:
147
+ args: Parsed CLI arguments.
148
+ procs: Processes that would be killed.
149
+
150
+ Returns:
151
+ True if the kill action is confirmed (or confirmation is skipped), otherwise
152
+ False.
153
+ """
154
+ if args.yes or not sys.stdin.isatty():
155
+ return True
156
+ action = "FORCE KILL" if args.force else "terminate"
157
+ print(f"About to {action} {len(procs)} process(es):")
158
+ for p in procs[:PREVIEW_LIMIT]:
159
+ print(f" {p.pid}: {p.name} ({p.rss_mb:.1f} MB)")
160
+ if len(procs) > PREVIEW_LIMIT:
161
+ print(f" ... and {len(procs) - PREVIEW_LIMIT} more")
162
+ try:
163
+ response = input("Continue? [y/N] ")
164
+ return response.lower() in {"y", "yes"}
165
+ except EOFError:
166
+ return True # Non-interactive
167
+
168
+
169
+ def cmd_kill(args: argparse.Namespace) -> int:
170
+ """Kill processes command.
171
+
172
+ Returns:
173
+ int: Exit code (0 on success).
174
+ """
175
+ procs = _get_kill_targets(args)
176
+ if not procs:
177
+ print("No processes match the filters.")
178
+ return 0
179
+
180
+ if getattr(args, "preview", False):
181
+ return _do_preview(args, procs)
182
+
183
+ if not _confirm_kill(args, procs):
184
+ print("Aborted.")
185
+ return 1
186
+
187
+ results = kill_processes([p.pid for p in procs], force=args.force)
188
+ exit_code = 0
189
+ for _, success, msg in results:
190
+ status = "OK" if success else "FAILED"
191
+ print(f"[{status}] {msg}")
192
+ if not success:
193
+ exit_code = 1
194
+ return exit_code
195
+
196
+
197
+ def cmd_memory(args: argparse.Namespace) -> int:
198
+ """Show memory summary command.
199
+
200
+ Returns:
201
+ int: Exit code (0 on success).
202
+ """
203
+ mem = get_memory_summary()
204
+
205
+ if args.format == "json":
206
+ print(json.dumps(mem, indent=2))
207
+ else:
208
+ print(f"Total: {mem['total_gb']:.2f} GB")
209
+ print(f"Used: {mem['used_gb']:.2f} GB ({mem['percent']:.1f}%)")
210
+ print(f"Free: {mem['free_gb']:.2f} GB")
211
+ print(f"Swap: {mem['swap_used_gb']:.2f} / {mem['swap_total_gb']:.2f} GB")
212
+
213
+ return 0