ptdu 0.1.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.
ptdu/fonts.py ADDED
@@ -0,0 +1,237 @@
1
+ """Font configuration utility for PTDU.
2
+
3
+ Provides OS-aware font detection and configuration.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ import tkinter as tk
10
+ from tkinter import font as tkfont
11
+ from typing import Literal, Optional
12
+
13
+
14
+ class FontManager:
15
+ """Manages font detection and configuration for different operating systems."""
16
+
17
+ # Font size recommendations (increased for better readability)
18
+ BASE_SIZE: int = 13
19
+ HEADING_SIZE: int = 12
20
+ STATUS_SIZE: int = 12
21
+
22
+ # Font size adjustment
23
+ SIZE_STEP: int = 1
24
+ MIN_SIZE: int = 8
25
+ MAX_SIZE: int = 20
26
+
27
+ # OS-specific font preferences (prioritize cleaner, more modern fonts)
28
+ LINUX_FONTS: tuple[str, ...] = (
29
+ "Ubuntu", # Clean, modern, highly readable
30
+ "Noto Sans", # Google's unified font, very readable
31
+ "DejaVu Sans", # Widely available, clean
32
+ "Cantarell", # GNOME's default, modern
33
+ "Liberation Sans", # Metric-compatible with Arial
34
+ )
35
+ MACOS_FONTS: tuple[str, ...] = (".SF NS", "Helvetica Neue", "Helvetica", "Arial")
36
+ WINDOWS_FONTS: tuple[str, ...] = ("Segoe UI", "Tahoma", "Arial")
37
+
38
+ def __init__(self, root: Optional[tk.Tk] = None) -> None:
39
+ """Initialize the font manager.
40
+
41
+ Args:
42
+ root: Tk root window for font querying
43
+ """
44
+ self._root: Optional[tk.Tk] = root
45
+ self._family: Optional[str] = None
46
+ self._fonts: dict[str, tkfont.Font] = {}
47
+
48
+ def _get_available_fonts(self) -> set[str]:
49
+ """Get set of available font families on the system.
50
+
51
+ Returns:
52
+ Set of available font family names
53
+ """
54
+ if self._root is None:
55
+ # Create temporary root for font querying if needed
56
+ temp_root = tk.Tk()
57
+ temp_root.withdraw()
58
+ families = set(tkfont.families(temp_root))
59
+ temp_root.destroy()
60
+ else:
61
+ families = set(tkfont.families(self._root))
62
+
63
+ return families
64
+
65
+ def detect_font_family(self) -> str:
66
+ """Detect the best available font family for the current OS.
67
+
68
+ Returns:
69
+ Font family name
70
+ """
71
+ available = self._get_available_fonts()
72
+ system = sys.platform
73
+
74
+ if system == "darwin": # macOS
75
+ preferences = self.MACOS_FONTS
76
+ elif system == "win32": # Windows
77
+ preferences = self.WINDOWS_FONTS
78
+ else: # Linux and others
79
+ preferences = self.LINUX_FONTS
80
+
81
+ # Find first available preferred font
82
+ for family in preferences:
83
+ if family in available:
84
+ self._family = family
85
+ return family
86
+
87
+ # Fallback to TkDefaultFont
88
+ self._family = "TkDefaultFont"
89
+ return self._family
90
+
91
+ def get_family(self) -> str:
92
+ """Get the detected or default font family.
93
+
94
+ Returns:
95
+ Font family name
96
+ """
97
+ if self._family is None:
98
+ return self.detect_font_family()
99
+ return self._family
100
+
101
+ def get_font(
102
+ self,
103
+ name: str,
104
+ size: Optional[int] = None,
105
+ weight: Literal["normal", "bold"] = "normal",
106
+ slant: Literal["roman", "italic"] = "roman",
107
+ ) -> tkfont.Font:
108
+ """Get or create a named font configuration.
109
+
110
+ Args:
111
+ name: Font configuration name (e.g., 'treeview', 'heading', 'status')
112
+ size: Font size in points (uses default if None)
113
+ weight: Font weight ('normal', 'bold')
114
+ slant: Font slant ('roman', 'italic')
115
+
116
+ Returns:
117
+ Configured tkinter Font object
118
+ """
119
+ if name in self._fonts:
120
+ return self._fonts[name]
121
+
122
+ family = self.get_family()
123
+ actual_size = size if size is not None else self.BASE_SIZE
124
+
125
+ font = tkfont.Font(family=family, size=actual_size, weight=weight, slant=slant)
126
+ self._fonts[name] = font
127
+
128
+ return font
129
+
130
+ def get_treeview_font(self) -> tkfont.Font:
131
+ """Get font for treeview items.
132
+
133
+ Returns:
134
+ Font for treeview content
135
+ """
136
+ return self.get_font("treeview", size=self.BASE_SIZE)
137
+
138
+ def get_heading_font(self) -> tkfont.Font:
139
+ """Get font for treeview headings.
140
+
141
+ Returns:
142
+ Font for column headings
143
+ """
144
+ return self.get_font("heading", size=self.HEADING_SIZE, weight="bold")
145
+
146
+ def get_status_font(self) -> tkfont.Font:
147
+ """Get font for status bar.
148
+
149
+ Returns:
150
+ Font for status bar text
151
+ """
152
+ return self.get_font("status", size=self.STATUS_SIZE)
153
+
154
+ def get_directory_font(self) -> tkfont.Font:
155
+ """Get font for directory entries (bold).
156
+
157
+ Returns:
158
+ Font for directory names
159
+ """
160
+ return self.get_font("directory", size=self.BASE_SIZE, weight="bold")
161
+
162
+ def get_file_font(self) -> tkfont.Font:
163
+ """Get font for file entries.
164
+
165
+ Returns:
166
+ Font for file names
167
+ """
168
+ return self.get_font("file", size=self.BASE_SIZE)
169
+
170
+ def update_font_sizes(self, base_size: int) -> None:
171
+ """Update all font sizes relative to new base size.
172
+
173
+ Args:
174
+ base_size: New base font size
175
+ """
176
+ self.BASE_SIZE = base_size
177
+ self.HEADING_SIZE = base_size - 1
178
+ self.STATUS_SIZE = base_size - 1
179
+
180
+ # Recreate fonts with new sizes
181
+ for name, font in self._fonts.items():
182
+ if name == "heading":
183
+ font.configure(size=self.HEADING_SIZE)
184
+ elif name == "status":
185
+ font.configure(size=self.STATUS_SIZE)
186
+ else:
187
+ font.configure(size=self.BASE_SIZE)
188
+
189
+ def measure_text_width(self, text: str, font_name: str = "treeview") -> int:
190
+ """Measure the pixel width of text.
191
+
192
+ Args:
193
+ text: Text to measure
194
+ font_name: Name of font to use
195
+
196
+ Returns:
197
+ Width in pixels
198
+ """
199
+ font = self.get_font(font_name)
200
+ return font.measure(text)
201
+
202
+ def increase_font_size(self) -> int:
203
+ """Increase all font sizes by SIZE_STEP.
204
+
205
+ Returns:
206
+ New base font size
207
+ """
208
+ new_size = min(self.BASE_SIZE + self.SIZE_STEP, self.MAX_SIZE)
209
+ self.update_font_sizes(new_size)
210
+ return new_size
211
+
212
+ def decrease_font_size(self) -> int:
213
+ """Decrease all font sizes by SIZE_STEP.
214
+
215
+ Returns:
216
+ New base font size
217
+ """
218
+ new_size = max(self.BASE_SIZE - self.SIZE_STEP, self.MIN_SIZE)
219
+ self.update_font_sizes(new_size)
220
+ return new_size
221
+
222
+ def reset_font_size(self) -> int:
223
+ """Reset font sizes to default.
224
+
225
+ Returns:
226
+ New base font size
227
+ """
228
+ self.update_font_sizes(13)
229
+ return 13
230
+
231
+ def get_font_size(self) -> int:
232
+ """Get current base font size.
233
+
234
+ Returns:
235
+ Current base font size
236
+ """
237
+ return self.BASE_SIZE
ptdu/main.py ADDED
@@ -0,0 +1,118 @@
1
+ """Entry point for PTDU application."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from ptdu.cache import ScanCache
8
+ from ptdu.scanner import Scanner
9
+ from ptdu.ui import MainWindow
10
+
11
+
12
+ def create_argument_parser() -> argparse.ArgumentParser:
13
+ """Create and configure the argument parser."""
14
+ parser = argparse.ArgumentParser(
15
+ prog="ptdu",
16
+ description="Python Tkinter Disk Usage analyzer - A graphical disk usage tool",
17
+ epilog="Keyboard shortcuts: j/k=navigate, h/l=expand/collapse, q=quit, r=rescan, d=delete",
18
+ )
19
+ parser.add_argument(
20
+ "path",
21
+ nargs="?",
22
+ default=".",
23
+ type=Path,
24
+ help="Directory to analyze (default: current directory)",
25
+ )
26
+ parser.add_argument(
27
+ "--follow-symlinks",
28
+ action="store_true",
29
+ help="Follow symbolic links when scanning",
30
+ )
31
+ parser.add_argument(
32
+ "--exclude",
33
+ "-e",
34
+ action="append",
35
+ default=[],
36
+ help="Additional patterns to exclude (can be used multiple times)",
37
+ )
38
+ parser.add_argument(
39
+ "--max-depth",
40
+ "-d",
41
+ type=int,
42
+ default=None,
43
+ help="Maximum scan depth (default: unlimited)",
44
+ )
45
+ parser.add_argument(
46
+ "--no-cache",
47
+ action="store_true",
48
+ help="Disable SQLite caching",
49
+ )
50
+ parser.add_argument(
51
+ "--clear-cache",
52
+ action="store_true",
53
+ help="Clear cache before starting",
54
+ )
55
+ parser.add_argument(
56
+ "--version",
57
+ action="version",
58
+ version="%(prog)s 0.1.0",
59
+ )
60
+ return parser
61
+
62
+
63
+ def validate_path(path: Path) -> Path:
64
+ """Validate that the provided path exists and is a directory."""
65
+ if not path.exists():
66
+ raise SystemExit(f"Error: Path does not exist: {path}")
67
+ if not path.is_dir():
68
+ raise SystemExit(f"Error: Path is not a directory: {path}")
69
+ return path.resolve()
70
+
71
+
72
+ def main() -> int:
73
+ """Main entry point for the PTDU application."""
74
+ parser = create_argument_parser()
75
+ args = parser.parse_args()
76
+
77
+ try:
78
+ target_path = validate_path(args.path)
79
+ except SystemExit as e:
80
+ print(str(e), file=sys.stderr)
81
+ return 1
82
+
83
+ # Handle cache
84
+ cache: ScanCache | None = None
85
+ if not args.no_cache:
86
+ cache = ScanCache()
87
+ if args.clear_cache:
88
+ cache.clear()
89
+ print("Cache cleared.")
90
+
91
+ # Convert exclude list to tuple
92
+ exclude_patterns = tuple(args.exclude) if args.exclude else None
93
+
94
+ # Create scanner with all options
95
+ scanner = Scanner(
96
+ follow_symlinks=args.follow_symlinks,
97
+ exclude_patterns=exclude_patterns,
98
+ cache=cache,
99
+ )
100
+
101
+ # Create main window and set scanner
102
+ window = MainWindow()
103
+ window.set_scanner(scanner)
104
+
105
+ # Set max depth if specified
106
+ if args.max_depth is not None:
107
+ window.set_max_depth(args.max_depth)
108
+
109
+ # Start background scan of initial directory
110
+ window.start_background_scan(target_path)
111
+
112
+ # Run the application
113
+ window.run()
114
+ return 0
115
+
116
+
117
+ if __name__ == "__main__":
118
+ sys.exit(main())
ptdu/models.py ADDED
@@ -0,0 +1,130 @@
1
+ """Data models for PTDU directory structure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+
9
+ class DirNode:
10
+ """Represents a file or directory node in the tree structure."""
11
+
12
+ def __init__(
13
+ self,
14
+ path: Path,
15
+ name: str,
16
+ is_dir: bool,
17
+ parent: Optional[DirNode] = None,
18
+ ) -> None:
19
+ """Initialize a directory node.
20
+
21
+ Args:
22
+ path: Full filesystem path
23
+ name: Display name
24
+ is_dir: Whether this is a directory
25
+ parent: Reference to parent node (None for root)
26
+ """
27
+ self.path: Path = path
28
+ self.name: str = name
29
+ self.size: int = 0
30
+ self.children: dict[str, DirNode] = {}
31
+ self.parent: Optional[DirNode] = parent
32
+ self.is_dir: bool = is_dir
33
+ self.is_expanded: bool = False
34
+ self._is_loaded: bool = False
35
+
36
+ def get_size(self) -> int:
37
+ """Get the total size of this node.
38
+
39
+ For directories, returns the cached size.
40
+ For files, returns the file size.
41
+
42
+ Returns:
43
+ Size in bytes
44
+ """
45
+ return self.size
46
+
47
+ def add_child(self, node: DirNode) -> None:
48
+ """Add a child node to this directory.
49
+
50
+ Args:
51
+ node: The child node to add
52
+
53
+ Raises:
54
+ ValueError: If this node is not a directory
55
+ """
56
+ if not self.is_dir:
57
+ raise ValueError(f"Cannot add child to file: {self.name}")
58
+
59
+ self.children[node.name] = node
60
+ node.parent = self
61
+
62
+ def remove_child(self, name: str) -> Optional[DirNode]:
63
+ """Remove a child node by name.
64
+
65
+ Args:
66
+ name: Name of the child to remove
67
+
68
+ Returns:
69
+ The removed node or None if not found
70
+
71
+ Raises:
72
+ ValueError: If this node is not a directory
73
+ """
74
+ if not self.is_dir:
75
+ raise ValueError(f"Cannot remove child from file: {self.name}")
76
+
77
+ node = self.children.pop(name, None)
78
+ if node is not None:
79
+ node.parent = None
80
+ return node
81
+
82
+ def get_child(self, name: str) -> Optional[DirNode]:
83
+ """Get a child node by name.
84
+
85
+ Args:
86
+ name: Name of the child to get
87
+
88
+ Returns:
89
+ The child node or None if not found
90
+ """
91
+ return self.children.get(name)
92
+
93
+ def set_size(self, size: int) -> None:
94
+ """Set the size of this node and propagate to parents.
95
+
96
+ Args:
97
+ size: New size in bytes
98
+ """
99
+ old_size = self.size
100
+ self.size = size
101
+
102
+ # Propagate size change to parent
103
+ if self.parent is not None:
104
+ size_diff = size - old_size
105
+ self.parent.size += size_diff
106
+
107
+ def mark_loaded(self) -> None:
108
+ """Mark this directory as having been scanned."""
109
+ self._is_loaded = True
110
+
111
+ def is_loaded(self) -> bool:
112
+ """Check if this directory has been scanned.
113
+
114
+ Returns:
115
+ True if the directory has been loaded
116
+ """
117
+ return self._is_loaded
118
+
119
+ def get_item_count(self) -> int:
120
+ """Get the number of children.
121
+
122
+ Returns:
123
+ Number of children (0 for files)
124
+ """
125
+ return len(self.children)
126
+
127
+ def __repr__(self) -> str:
128
+ """Return string representation."""
129
+ node_type = "dir" if self.is_dir else "file"
130
+ return f"DirNode({node_type}: {self.name}, size={self.size})"