pipu-cli 0.1.dev6__py3-none-any.whl → 0.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.
- pipu_cli/__init__.py +2 -2
- pipu_cli/cache.py +316 -0
- pipu_cli/cli.py +867 -812
- pipu_cli/config.py +7 -58
- pipu_cli/config_file.py +80 -0
- pipu_cli/output.py +99 -0
- pipu_cli/package_management.py +1145 -0
- pipu_cli/pretty.py +286 -0
- pipu_cli/requirements.py +100 -0
- pipu_cli/rollback.py +110 -0
- pipu_cli-0.2.0.dist-info/METADATA +422 -0
- pipu_cli-0.2.0.dist-info/RECORD +16 -0
- pipu_cli/common.py +0 -4
- pipu_cli/internals.py +0 -819
- pipu_cli/package_constraints.py +0 -2286
- pipu_cli/thread_safe.py +0 -243
- pipu_cli/ui/__init__.py +0 -51
- pipu_cli/ui/apps.py +0 -1460
- pipu_cli/ui/constants.py +0 -19
- pipu_cli/ui/modal_dialogs.py +0 -1375
- pipu_cli/ui/table_widgets.py +0 -345
- pipu_cli/utils.py +0 -169
- pipu_cli-0.1.dev6.dist-info/METADATA +0 -517
- pipu_cli-0.1.dev6.dist-info/RECORD +0 -19
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/WHEEL +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/top_level.txt +0 -0
pipu_cli/ui/apps.py
DELETED
|
@@ -1,1460 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main application classes for the TUI interface.
|
|
3
|
-
|
|
4
|
-
Contains the complete MainTUIApp and PackageSelectionApp implementations
|
|
5
|
-
with full feature sets and functionality.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import List, Dict, Any, Tuple, cast, Optional
|
|
9
|
-
import logging
|
|
10
|
-
import subprocess
|
|
11
|
-
import sys
|
|
12
|
-
import time
|
|
13
|
-
import signal
|
|
14
|
-
import atexit
|
|
15
|
-
from textual.app import App, ComposeResult
|
|
16
|
-
from textual.containers import Horizontal, Vertical, Grid, ScrollableContainer
|
|
17
|
-
from textual.widgets import Header, Footer, DataTable, Button, Static, Input, Label
|
|
18
|
-
from textual.binding import Binding
|
|
19
|
-
from textual.message import Message
|
|
20
|
-
from textual.screen import ModalScreen
|
|
21
|
-
from textual.worker import get_current_worker
|
|
22
|
-
from textual.errors import NoWidget
|
|
23
|
-
from textual.coordinate import Coordinate
|
|
24
|
-
from rich.text import Text
|
|
25
|
-
from ..internals import _check_constraint_satisfaction, list_outdated, get_constraint_color
|
|
26
|
-
from ..package_constraints import add_constraints_to_config, read_constraints, read_ignores
|
|
27
|
-
from pip._internal.metadata import get_default_environment
|
|
28
|
-
|
|
29
|
-
# Import modular components
|
|
30
|
-
from .constants import (
|
|
31
|
-
COLUMN_SELECTION, COLUMN_PACKAGE, COLUMN_CURRENT, COLUMN_LATEST,
|
|
32
|
-
COLUMN_TYPE, COLUMN_CONSTRAINT, COLUMN_INVALID_WHEN,
|
|
33
|
-
FORCE_EXIT_TIMEOUT, UNINSTALL_TIMEOUT
|
|
34
|
-
)
|
|
35
|
-
from .modal_dialogs import (
|
|
36
|
-
ConstraintInputScreen, HelpScreen,
|
|
37
|
-
DeleteConstraintConfirmScreen, RemoveAllConstraintsConfirmScreen,
|
|
38
|
-
UninstallConfirmScreen, UpdateConfirmScreen, PackageUpdateScreen,
|
|
39
|
-
NetworkErrorScreen
|
|
40
|
-
)
|
|
41
|
-
from .table_widgets import PackageSelectionTable
|
|
42
|
-
|
|
43
|
-
# Set up module logger
|
|
44
|
-
logger = logging.getLogger(__name__)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _restore_terminal() -> None:
|
|
48
|
-
"""
|
|
49
|
-
Restore terminal to normal mode in case of unclean exit.
|
|
50
|
-
|
|
51
|
-
This prevents the terminal from being left in raw mode or alternate screen mode
|
|
52
|
-
which can cause control characters to be displayed instead of being interpreted.
|
|
53
|
-
|
|
54
|
-
Uses the centralized safe_terminal_reset utility for cross-platform compatibility.
|
|
55
|
-
"""
|
|
56
|
-
from ..utils import safe_terminal_reset
|
|
57
|
-
safe_terminal_reset()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _setup_signal_handlers() -> None:
|
|
61
|
-
"""
|
|
62
|
-
Set up signal handlers to ensure clean terminal restoration on exit.
|
|
63
|
-
|
|
64
|
-
Handles SIGINT (Ctrl+C), SIGTERM, and other termination signals to ensure
|
|
65
|
-
the terminal is properly restored even if the application is forcibly terminated.
|
|
66
|
-
"""
|
|
67
|
-
def signal_handler(signum, frame):
|
|
68
|
-
"""Handle termination signals by restoring terminal and exiting."""
|
|
69
|
-
del signum, frame # Unused parameters
|
|
70
|
-
_restore_terminal()
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
|
|
73
|
-
# Register signal handlers for clean exit
|
|
74
|
-
try:
|
|
75
|
-
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
|
|
76
|
-
signal.signal(signal.SIGTERM, signal_handler) # Termination
|
|
77
|
-
if hasattr(signal, 'SIGHUP'):
|
|
78
|
-
signal.signal(signal.SIGHUP, signal_handler) # Hangup (Unix)
|
|
79
|
-
except Exception:
|
|
80
|
-
# Signal handling may not be available on all platforms
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
# Also register atexit handler as final fallback
|
|
84
|
-
atexit.register(_restore_terminal)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class PackageSelectionApp(App):
|
|
88
|
-
"""Textual app for interactive package selection."""
|
|
89
|
-
|
|
90
|
-
BINDINGS = [
|
|
91
|
-
Binding("h", "show_help", "Help", show=True),
|
|
92
|
-
Binding("escape,q", "quit_app", "Quit", show=True),
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
CSS = """
|
|
96
|
-
Screen {
|
|
97
|
-
layers: base overlay;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
Header {
|
|
101
|
-
dock: top;
|
|
102
|
-
height: 3;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
Footer {
|
|
106
|
-
dock: bottom;
|
|
107
|
-
height: 3;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#info-panel {
|
|
111
|
-
height: 3;
|
|
112
|
-
dock: top;
|
|
113
|
-
background: $panel;
|
|
114
|
-
color: $text;
|
|
115
|
-
text-align: center;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
#selection-info {
|
|
119
|
-
height: 2;
|
|
120
|
-
dock: bottom;
|
|
121
|
-
background: $panel;
|
|
122
|
-
color: $text;
|
|
123
|
-
text-align: center;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
PackageSelectionTable {
|
|
127
|
-
border: solid $primary;
|
|
128
|
-
margin: 1;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
PackageSelectionTable > .datatable--cursor {
|
|
132
|
-
background: $primary 20%;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
PackageSelectionTable > .datatable--header {
|
|
136
|
-
background: $surface;
|
|
137
|
-
color: $text;
|
|
138
|
-
text-style: bold;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
#button-container > Button {
|
|
142
|
-
margin: 1;
|
|
143
|
-
width: 20;
|
|
144
|
-
height: 3;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
#button-container {
|
|
148
|
-
height: 4;
|
|
149
|
-
align: center middle;
|
|
150
|
-
margin-bottom: 2;
|
|
151
|
-
}
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
def __init__(self, outdated_packages: List[Dict[str, Any]]):
|
|
155
|
-
"""Initialize the package selection app."""
|
|
156
|
-
super().__init__()
|
|
157
|
-
self.outdated_packages = outdated_packages
|
|
158
|
-
self.selected_packages = []
|
|
159
|
-
self.confirmed = False
|
|
160
|
-
|
|
161
|
-
# Set up terminal cleanup handlers
|
|
162
|
-
_setup_signal_handlers()
|
|
163
|
-
|
|
164
|
-
def compose(self) -> ComposeResult:
|
|
165
|
-
"""Create child widgets for the app."""
|
|
166
|
-
yield Static(
|
|
167
|
-
f"Found {len(self.outdated_packages)} outdated packages.\n"
|
|
168
|
-
f"Select packages to update using SPACE, then press ENTER to confirm.",
|
|
169
|
-
id="info-panel"
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
yield PackageSelectionTable(self.outdated_packages, id="package-table")
|
|
173
|
-
|
|
174
|
-
with Horizontal(id="button-container"):
|
|
175
|
-
yield Button("Update Selected", id="update-btn", variant="primary")
|
|
176
|
-
yield Button("Cancel", id="cancel-btn", variant="error")
|
|
177
|
-
|
|
178
|
-
yield Static("", id="selection-info")
|
|
179
|
-
|
|
180
|
-
def on_mount(self) -> None:
|
|
181
|
-
"""Set up the app when mounted."""
|
|
182
|
-
self._update_selection_info()
|
|
183
|
-
|
|
184
|
-
def on_package_selection_table_selection_changed(self, message: PackageSelectionTable.SelectionChanged) -> None:
|
|
185
|
-
"""Handle package selection change."""
|
|
186
|
-
self._update_selection_info(message.selected_count, message.total_count)
|
|
187
|
-
|
|
188
|
-
def on_package_selection_table_confirm_selection(self, message: PackageSelectionTable.ConfirmSelection) -> None:
|
|
189
|
-
"""Handle selection confirmation."""
|
|
190
|
-
self.selected_packages = message.selected_packages
|
|
191
|
-
self.confirmed = True
|
|
192
|
-
self.exit()
|
|
193
|
-
|
|
194
|
-
def _update_selection_info(self, selected_count: int = 0, total_count: int = 0) -> None:
|
|
195
|
-
"""Update the selection info display."""
|
|
196
|
-
info_widget = self.query_one("#selection-info", Static)
|
|
197
|
-
info_widget.update(f"Selected {selected_count} of {total_count} packages")
|
|
198
|
-
|
|
199
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
200
|
-
"""Handle button presses."""
|
|
201
|
-
if event.button.id == "update-btn":
|
|
202
|
-
# Get selected packages from table
|
|
203
|
-
table = self.query_one("#package-table", PackageSelectionTable)
|
|
204
|
-
self.selected_packages = [
|
|
205
|
-
pkg for pkg in self.outdated_packages
|
|
206
|
-
if table.selected_packages.get(pkg['name'], False)
|
|
207
|
-
]
|
|
208
|
-
self.confirmed = True
|
|
209
|
-
self.exit()
|
|
210
|
-
elif event.button.id == "cancel-btn":
|
|
211
|
-
self.confirmed = False
|
|
212
|
-
self.exit()
|
|
213
|
-
|
|
214
|
-
def action_show_help(self) -> None:
|
|
215
|
-
"""Show help screen."""
|
|
216
|
-
self.push_screen(HelpScreen())
|
|
217
|
-
|
|
218
|
-
def action_quit_app(self) -> None:
|
|
219
|
-
"""Quit the application."""
|
|
220
|
-
# Ensure terminal is restored to normal mode
|
|
221
|
-
_restore_terminal()
|
|
222
|
-
self.confirmed = False
|
|
223
|
-
self.exit()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
class MainTUIApp(App):
|
|
227
|
-
"""Main TUI application that shows all packages and async checks for updates."""
|
|
228
|
-
|
|
229
|
-
BINDINGS = [
|
|
230
|
-
Binding("c", "add_constraint", "Add Constraint", show=True),
|
|
231
|
-
Binding("f", "filter_outdated", "F: filter to outdated", show=True),
|
|
232
|
-
Binding("s", "show_all", "S: show all", show=True),
|
|
233
|
-
Binding("u", "update_selected", "U: update selected", show=True),
|
|
234
|
-
Binding("x", "uninstall_package", "X: uninstall", show=True),
|
|
235
|
-
Binding("d", "delete_constraint", "Delete Constraint", show=True),
|
|
236
|
-
Binding("r", "remove_all_constraints", "Remove All Constraints", show=True),
|
|
237
|
-
Binding("h", "show_help", "Help", show=True),
|
|
238
|
-
Binding("escape,q,ctrl+q,ctrl+c", "quit_app", "Quit", show=True),
|
|
239
|
-
Binding("enter", "handle_enter", "", show=False), # Hidden binding for conditional Enter behavior
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
CSS = """
|
|
243
|
-
Screen {
|
|
244
|
-
layers: base overlay;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
Header {
|
|
248
|
-
dock: top;
|
|
249
|
-
height: 3;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
Footer {
|
|
253
|
-
dock: bottom;
|
|
254
|
-
height: 3;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
#header-stack {
|
|
258
|
-
dock: top;
|
|
259
|
-
height: 3;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
#info-panel {
|
|
263
|
-
height: 2;
|
|
264
|
-
background: $panel;
|
|
265
|
-
color: $text;
|
|
266
|
-
text-align: center;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
#filter-mode-container {
|
|
270
|
-
height: 1;
|
|
271
|
-
background: $panel;
|
|
272
|
-
align: center middle;
|
|
273
|
-
padding: 0 1;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
#filter-label {
|
|
277
|
-
width: auto;
|
|
278
|
-
text-style: bold;
|
|
279
|
-
margin-right: 1;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
#filter-description {
|
|
283
|
-
width: auto;
|
|
284
|
-
text-style: italic;
|
|
285
|
-
color: $text-muted;
|
|
286
|
-
margin-left: 1;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
#custom-footer {
|
|
290
|
-
height: 1;
|
|
291
|
-
dock: bottom;
|
|
292
|
-
background: $panel;
|
|
293
|
-
color: $text;
|
|
294
|
-
text-align: center;
|
|
295
|
-
padding: 0 1;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
#main-table {
|
|
299
|
-
border: solid $primary;
|
|
300
|
-
margin: 1;
|
|
301
|
-
margin-bottom: 2;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
#main-table > .datatable--cursor {
|
|
305
|
-
background: $primary 20%;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
#main-table > .datatable--header {
|
|
309
|
-
background: $surface;
|
|
310
|
-
color: $text;
|
|
311
|
-
text-style: bold;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
#button-container > Button {
|
|
315
|
-
margin: 1;
|
|
316
|
-
width: 20;
|
|
317
|
-
height: 3;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#button-container {
|
|
321
|
-
height: 4;
|
|
322
|
-
align: center middle;
|
|
323
|
-
margin-bottom: 2;
|
|
324
|
-
}
|
|
325
|
-
"""
|
|
326
|
-
|
|
327
|
-
def __init__(self):
|
|
328
|
-
"""Initialize the main TUI app."""
|
|
329
|
-
super().__init__()
|
|
330
|
-
self.all_packages = []
|
|
331
|
-
self.outdated_packages = []
|
|
332
|
-
self.update_check_complete = True # Start as True, will be set to False when checking begins
|
|
333
|
-
self.update_check_successful = False # Track if the check completed successfully
|
|
334
|
-
self.constraints = {}
|
|
335
|
-
self.ignores = set()
|
|
336
|
-
self.invalidation_triggers = {}
|
|
337
|
-
self.filter_outdated_only = False # Default to showing all packages
|
|
338
|
-
self.package_row_mapping = {} # Maps package name to row index for efficient updates
|
|
339
|
-
|
|
340
|
-
# Set up terminal cleanup handlers
|
|
341
|
-
_setup_signal_handlers()
|
|
342
|
-
|
|
343
|
-
def compose(self) -> ComposeResult:
|
|
344
|
-
"""Create child widgets for the app."""
|
|
345
|
-
# Use a vertical container to properly stack the header elements
|
|
346
|
-
with Vertical(id="header-stack"):
|
|
347
|
-
yield Static(
|
|
348
|
-
"pipu - Package Management\n"
|
|
349
|
-
"Space: toggle, U: update, H: help, Q: quit",
|
|
350
|
-
id="info-panel"
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
# Filter mode indicator - third line
|
|
354
|
-
with Horizontal(id="filter-mode-container"):
|
|
355
|
-
yield Static("Filter Mode:", id="filter-label")
|
|
356
|
-
yield Static("Show all packages", id="filter-description")
|
|
357
|
-
|
|
358
|
-
yield DataTable(id="main-table", cursor_type="row")
|
|
359
|
-
|
|
360
|
-
with Horizontal(id="button-container"):
|
|
361
|
-
yield Button("Update Selected", id="update-btn", variant="primary")
|
|
362
|
-
yield Button("Quit", id="quit-btn", variant="error")
|
|
363
|
-
|
|
364
|
-
# Custom footer with status
|
|
365
|
-
yield Static(
|
|
366
|
-
"Ready to load packages",
|
|
367
|
-
id="custom-footer"
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
def on_mount(self) -> None:
|
|
371
|
-
"""Set up the app when mounted."""
|
|
372
|
-
# Load constraints and ignores
|
|
373
|
-
self.constraints = read_constraints()
|
|
374
|
-
self.ignores = read_ignores()
|
|
375
|
-
|
|
376
|
-
# Load invalidation triggers
|
|
377
|
-
self.invalidation_triggers = self._load_invalidation_triggers()
|
|
378
|
-
|
|
379
|
-
# Load all installed packages first
|
|
380
|
-
self._load_installed_packages()
|
|
381
|
-
|
|
382
|
-
# Start async outdated package checking using a timer
|
|
383
|
-
self.call_later(self._start_update_check, 0.5)
|
|
384
|
-
|
|
385
|
-
def _load_invalidation_triggers(self) -> Dict[str, List[str]]:
|
|
386
|
-
"""Load invalidation triggers from pip configuration and auto-discovered constraints."""
|
|
387
|
-
from ..package_constraints import read_invalidation_triggers
|
|
388
|
-
return read_invalidation_triggers()
|
|
389
|
-
|
|
390
|
-
def on_unmount(self) -> None:
|
|
391
|
-
"""Clean up when the app is unmounting."""
|
|
392
|
-
# Ensure terminal is restored to normal mode
|
|
393
|
-
_restore_terminal()
|
|
394
|
-
|
|
395
|
-
# Cancel all workers immediately
|
|
396
|
-
try:
|
|
397
|
-
for worker in self.workers:
|
|
398
|
-
worker.cancel()
|
|
399
|
-
except Exception:
|
|
400
|
-
pass
|
|
401
|
-
|
|
402
|
-
def _load_installed_packages(self) -> None:
|
|
403
|
-
"""Load all installed packages into the table."""
|
|
404
|
-
# Update status to show we're loading packages
|
|
405
|
-
self._update_status("Loading installed packages...", False)
|
|
406
|
-
|
|
407
|
-
try:
|
|
408
|
-
env = get_default_environment()
|
|
409
|
-
installed_dists = env.iter_all_distributions()
|
|
410
|
-
|
|
411
|
-
# Detect editable packages for display and preservation
|
|
412
|
-
from ..internals import get_editable_packages
|
|
413
|
-
editable_packages = get_editable_packages()
|
|
414
|
-
|
|
415
|
-
# Set up the data table (if app is mounted)
|
|
416
|
-
table = None
|
|
417
|
-
try:
|
|
418
|
-
table = self.query_one("#main-table", DataTable)
|
|
419
|
-
table.add_column("Sel", width=4)
|
|
420
|
-
table.add_column("Package", width=20)
|
|
421
|
-
table.add_column("Current", width=12)
|
|
422
|
-
table.add_column("Latest", width=12)
|
|
423
|
-
table.add_column("Type", width=8)
|
|
424
|
-
table.add_column("Constraint", width=20)
|
|
425
|
-
table.add_column("Invalid When", width=25)
|
|
426
|
-
except Exception:
|
|
427
|
-
# App not mounted or table not available (e.g., during testing)
|
|
428
|
-
pass
|
|
429
|
-
|
|
430
|
-
# Load all packages
|
|
431
|
-
self.all_packages = []
|
|
432
|
-
for dist in installed_dists:
|
|
433
|
-
try:
|
|
434
|
-
package_name = dist.metadata["name"]
|
|
435
|
-
current_version = str(dist.version)
|
|
436
|
-
|
|
437
|
-
# Normalize package name for constraint and trigger lookups
|
|
438
|
-
from packaging.utils import canonicalize_name
|
|
439
|
-
canonical_name = canonicalize_name(package_name)
|
|
440
|
-
|
|
441
|
-
# Get invalidation triggers for this package
|
|
442
|
-
package_triggers = self.invalidation_triggers.get(canonical_name, [])
|
|
443
|
-
invalid_when_display = ", ".join(package_triggers) if package_triggers else None
|
|
444
|
-
|
|
445
|
-
package_info = {
|
|
446
|
-
"name": package_name,
|
|
447
|
-
"version": current_version,
|
|
448
|
-
"latest_version": "Checking...",
|
|
449
|
-
"latest_filetype": "",
|
|
450
|
-
"constraint": self.constraints.get(canonical_name),
|
|
451
|
-
"invalid_when": invalid_when_display,
|
|
452
|
-
"selected": False,
|
|
453
|
-
"outdated": False,
|
|
454
|
-
"editable": canonical_name in editable_packages
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
self.all_packages.append(package_info)
|
|
458
|
-
|
|
459
|
-
except Exception:
|
|
460
|
-
continue
|
|
461
|
-
|
|
462
|
-
# Sort packages alphabetically
|
|
463
|
-
self.all_packages.sort(key=lambda x: x["name"].lower())
|
|
464
|
-
|
|
465
|
-
# Add sorted packages to table and build row mapping (if table exists)
|
|
466
|
-
self.package_row_mapping = {}
|
|
467
|
-
if table is not None:
|
|
468
|
-
# In filtered mode, start with an empty table that gets populated as outdated packages are discovered
|
|
469
|
-
# In show-all mode, populate with all packages initially
|
|
470
|
-
if not self.filter_outdated_only:
|
|
471
|
-
for i, package_info in enumerate(self.all_packages):
|
|
472
|
-
constraint_display = package_info.get('constraint', '')
|
|
473
|
-
if constraint_display:
|
|
474
|
-
constraint_text = Text.from_markup(f"[yellow]{constraint_display}[/yellow]")
|
|
475
|
-
else:
|
|
476
|
-
constraint_text = Text.from_markup("[dim]-[/dim]")
|
|
477
|
-
|
|
478
|
-
# Format invalid when display
|
|
479
|
-
invalid_when = package_info.get('invalid_when')
|
|
480
|
-
if invalid_when:
|
|
481
|
-
invalid_when_text = Text.from_markup(f"[yellow]{invalid_when}[/yellow]")
|
|
482
|
-
else:
|
|
483
|
-
invalid_when_text = Text.from_markup("[dim]-[/dim]")
|
|
484
|
-
|
|
485
|
-
# Format selection indicator (initially unselected)
|
|
486
|
-
selection_text = Text(" ", style="dim")
|
|
487
|
-
|
|
488
|
-
# Format package name with editable indicator
|
|
489
|
-
if package_info.get("editable", False):
|
|
490
|
-
package_display = Text.from_markup(f"[bold cyan]{package_info['name']}[/bold cyan] [dim]📝[/dim]")
|
|
491
|
-
else:
|
|
492
|
-
package_display = package_info["name"]
|
|
493
|
-
|
|
494
|
-
table.add_row(
|
|
495
|
-
selection_text,
|
|
496
|
-
package_display,
|
|
497
|
-
package_info["version"],
|
|
498
|
-
"Checking...",
|
|
499
|
-
"",
|
|
500
|
-
constraint_text,
|
|
501
|
-
invalid_when_text,
|
|
502
|
-
key=package_info["name"]
|
|
503
|
-
)
|
|
504
|
-
self.package_row_mapping[package_info["name"]] = i
|
|
505
|
-
# In filtered mode, table starts empty - packages will be added via _update_package_result as they're discovered to be outdated
|
|
506
|
-
else:
|
|
507
|
-
# If no table, just build the row mapping for all packages
|
|
508
|
-
for i, package_info in enumerate(self.all_packages):
|
|
509
|
-
self.package_row_mapping[package_info["name"]] = i
|
|
510
|
-
|
|
511
|
-
# Update status to show packages loaded
|
|
512
|
-
self._update_status("Packages loaded. Checking for updates...", False)
|
|
513
|
-
|
|
514
|
-
except Exception as e:
|
|
515
|
-
self.notify(f"Error loading packages: {e}")
|
|
516
|
-
|
|
517
|
-
def _start_update_check(self, *args) -> None:
|
|
518
|
-
"""Start the update check using a worker."""
|
|
519
|
-
import threading
|
|
520
|
-
del args # Unused parameter
|
|
521
|
-
self.update_check_complete = False
|
|
522
|
-
self._set_update_button_enabled(False)
|
|
523
|
-
# Create a cancellation event for this check
|
|
524
|
-
self.update_check_cancel_event = threading.Event()
|
|
525
|
-
self.run_worker(self._check_outdated_packages, thread=True, exclusive=True, name="outdated_check")
|
|
526
|
-
|
|
527
|
-
def _check_outdated_packages(self) -> None:
|
|
528
|
-
"""Worker function to check for outdated packages."""
|
|
529
|
-
import threading
|
|
530
|
-
|
|
531
|
-
try:
|
|
532
|
-
worker = get_current_worker()
|
|
533
|
-
if worker.is_cancelled:
|
|
534
|
-
return
|
|
535
|
-
except Exception:
|
|
536
|
-
# No active worker (e.g., during testing)
|
|
537
|
-
worker = None
|
|
538
|
-
|
|
539
|
-
try:
|
|
540
|
-
self.call_from_thread(self._update_status, "Checking for package updates...", True)
|
|
541
|
-
except Exception:
|
|
542
|
-
# Not in app context (e.g., during testing)
|
|
543
|
-
pass
|
|
544
|
-
|
|
545
|
-
try:
|
|
546
|
-
# Define progress callback to update status
|
|
547
|
-
def progress_callback(package_name: str):
|
|
548
|
-
if worker and worker.is_cancelled:
|
|
549
|
-
return
|
|
550
|
-
try:
|
|
551
|
-
self.call_from_thread(self._update_status, f"Checking {package_name}...", True)
|
|
552
|
-
except Exception:
|
|
553
|
-
pass
|
|
554
|
-
|
|
555
|
-
# Define result callback to update individual table rows
|
|
556
|
-
def result_callback(package_result: Dict[str, Any]):
|
|
557
|
-
if worker and worker.is_cancelled:
|
|
558
|
-
return
|
|
559
|
-
try:
|
|
560
|
-
self.call_from_thread(self._update_package_result, package_result)
|
|
561
|
-
except Exception:
|
|
562
|
-
pass
|
|
563
|
-
|
|
564
|
-
# Create a silent console to avoid interference with TUI
|
|
565
|
-
from rich.console import Console
|
|
566
|
-
import io
|
|
567
|
-
silent_console = Console(file=io.StringIO(), width=120)
|
|
568
|
-
|
|
569
|
-
# Use list_outdated with silent console to avoid interference with TUI
|
|
570
|
-
outdated_packages = list_outdated(
|
|
571
|
-
console=silent_console,
|
|
572
|
-
print_table=False,
|
|
573
|
-
constraints=self.constraints,
|
|
574
|
-
ignores=self.ignores,
|
|
575
|
-
pre=False,
|
|
576
|
-
progress_callback=progress_callback,
|
|
577
|
-
result_callback=result_callback,
|
|
578
|
-
cancel_event=self.update_check_cancel_event
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
if not worker or not worker.is_cancelled:
|
|
582
|
-
try:
|
|
583
|
-
self.call_from_thread(self._update_outdated_results, outdated_packages)
|
|
584
|
-
except Exception:
|
|
585
|
-
pass
|
|
586
|
-
|
|
587
|
-
except ConnectionError as e:
|
|
588
|
-
# Network connectivity issue - show error dialog and exit
|
|
589
|
-
if not worker or not worker.is_cancelled:
|
|
590
|
-
try:
|
|
591
|
-
error_msg = str(e)
|
|
592
|
-
self.call_from_thread(self._handle_update_check_error, error_msg, is_network_error=True)
|
|
593
|
-
except Exception:
|
|
594
|
-
pass
|
|
595
|
-
except Exception as e:
|
|
596
|
-
# Other errors during update check
|
|
597
|
-
if not worker or not worker.is_cancelled:
|
|
598
|
-
try:
|
|
599
|
-
error_msg = f"Error checking for updates: {str(e)}"
|
|
600
|
-
logger.error(error_msg, exc_info=True)
|
|
601
|
-
self.call_from_thread(self._handle_update_check_error, error_msg, is_network_error=False)
|
|
602
|
-
except Exception:
|
|
603
|
-
pass
|
|
604
|
-
|
|
605
|
-
def _update_status(self, message: str, show_spinner: bool = False) -> None:
|
|
606
|
-
"""Update the status message."""
|
|
607
|
-
try:
|
|
608
|
-
footer_widget = self.query_one("#custom-footer", Static)
|
|
609
|
-
spinner_text = "⟳ " if show_spinner else ""
|
|
610
|
-
footer_widget.update(f"{spinner_text}{message}")
|
|
611
|
-
except Exception:
|
|
612
|
-
# Fallback to notification if status widget fails
|
|
613
|
-
self.notify(f"{message}")
|
|
614
|
-
|
|
615
|
-
def _handle_update_check_error(self, error_message: str, is_network_error: bool = False) -> None:
|
|
616
|
-
"""Handle errors that occur during the update check process."""
|
|
617
|
-
# Mark check as complete but unsuccessful
|
|
618
|
-
self.update_check_complete = True
|
|
619
|
-
self.update_check_successful = False
|
|
620
|
-
self._set_update_button_enabled(False)
|
|
621
|
-
|
|
622
|
-
# Update status to show error
|
|
623
|
-
self._update_status(f"❌ {error_message} - Press Enter or Escape to exit", False)
|
|
624
|
-
|
|
625
|
-
# Show notification
|
|
626
|
-
self.notify(f"Update check failed: {error_message}. Press Enter or Escape to exit.", severity="error")
|
|
627
|
-
|
|
628
|
-
# For network errors, show the modal dialog and exit
|
|
629
|
-
if is_network_error:
|
|
630
|
-
self.push_screen(NetworkErrorScreen(error_message))
|
|
631
|
-
|
|
632
|
-
def _show_network_error_and_exit(self, error_message: str) -> None:
|
|
633
|
-
"""Show network error dialog and exit when dismissed."""
|
|
634
|
-
self.push_screen(NetworkErrorScreen(error_message))
|
|
635
|
-
|
|
636
|
-
def _update_package_result(self, package_result: Dict[str, Any]) -> None:
|
|
637
|
-
"""Update a single package row with its result."""
|
|
638
|
-
try:
|
|
639
|
-
package_name = package_result["name"]
|
|
640
|
-
latest_version = package_result["latest_version"]
|
|
641
|
-
filetype = package_result["latest_filetype"] or ""
|
|
642
|
-
current_version = package_result["version"]
|
|
643
|
-
|
|
644
|
-
# First, update the package data in all_packages (this is the source of truth)
|
|
645
|
-
pkg_data = None
|
|
646
|
-
for pkg in self.all_packages:
|
|
647
|
-
if pkg["name"] == package_name:
|
|
648
|
-
pkg["latest_version"] = latest_version
|
|
649
|
-
pkg["latest_filetype"] = filetype
|
|
650
|
-
pkg["outdated"] = (latest_version != current_version)
|
|
651
|
-
|
|
652
|
-
# Auto-select packages that can be updated without constraint conflicts
|
|
653
|
-
if pkg["outdated"]:
|
|
654
|
-
constraint = pkg.get('constraint')
|
|
655
|
-
if constraint:
|
|
656
|
-
# Check if the latest version satisfies the constraint
|
|
657
|
-
selected = _check_constraint_satisfaction(latest_version, constraint)
|
|
658
|
-
else:
|
|
659
|
-
# No constraint, can be updated freely
|
|
660
|
-
selected = True
|
|
661
|
-
pkg["selected"] = selected
|
|
662
|
-
else:
|
|
663
|
-
# Package is up to date, don't select it
|
|
664
|
-
pkg["selected"] = False
|
|
665
|
-
|
|
666
|
-
pkg_data = pkg
|
|
667
|
-
break
|
|
668
|
-
|
|
669
|
-
if not pkg_data:
|
|
670
|
-
# Package not found in all_packages, skip
|
|
671
|
-
return
|
|
672
|
-
|
|
673
|
-
# Check if this package should be visible in current filter mode
|
|
674
|
-
should_be_visible = True
|
|
675
|
-
if self.filter_outdated_only:
|
|
676
|
-
# In filtered mode, only show packages that have been confirmed as outdated
|
|
677
|
-
# Don't show packages that are still being checked or up-to-date
|
|
678
|
-
should_be_visible = pkg_data["outdated"]
|
|
679
|
-
|
|
680
|
-
# Check if package is currently visible in the table
|
|
681
|
-
row_index = self.package_row_mapping.get(package_name)
|
|
682
|
-
is_currently_visible = row_index is not None
|
|
683
|
-
|
|
684
|
-
# Determine if we need to refresh the entire table
|
|
685
|
-
needs_refresh = False
|
|
686
|
-
if self.filter_outdated_only:
|
|
687
|
-
# If package visibility changed, we need to refresh
|
|
688
|
-
if should_be_visible != is_currently_visible:
|
|
689
|
-
needs_refresh = True
|
|
690
|
-
|
|
691
|
-
if needs_refresh:
|
|
692
|
-
# Full table refresh needed due to filtering changes
|
|
693
|
-
# Preserve cursor position during real-time filtering
|
|
694
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
695
|
-
elif is_currently_visible and should_be_visible:
|
|
696
|
-
# Package is visible and should remain visible - update the row in place
|
|
697
|
-
table = self.query_one("#main-table", DataTable)
|
|
698
|
-
|
|
699
|
-
# Validate that the row index is still valid
|
|
700
|
-
if row_index >= len(table.rows) or row_index < 0:
|
|
701
|
-
# Row mapping is stale, do a full refresh
|
|
702
|
-
# Preserve cursor position when doing emergency refresh
|
|
703
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
# Color code the latest version based on update status
|
|
707
|
-
if latest_version == current_version:
|
|
708
|
-
# Package is up-to-date, show in default color
|
|
709
|
-
latest_display = latest_version
|
|
710
|
-
type_display = "" # Empty type column for current packages
|
|
711
|
-
else:
|
|
712
|
-
# Package is outdated - check if it can be updated
|
|
713
|
-
constraint = pkg_data.get('constraint')
|
|
714
|
-
# Use utility method for consistent formatting
|
|
715
|
-
latest_display = self._format_latest_version(latest_version, constraint)
|
|
716
|
-
type_display = filetype
|
|
717
|
-
|
|
718
|
-
# Update the table cells
|
|
719
|
-
try:
|
|
720
|
-
# Update selection indicator
|
|
721
|
-
if pkg_data.get("selected", False):
|
|
722
|
-
selection_text = Text("●", style="green bold")
|
|
723
|
-
else:
|
|
724
|
-
selection_text = Text(" ", style="dim")
|
|
725
|
-
|
|
726
|
-
table.update_cell_at(cast(Coordinate, (row_index, COLUMN_SELECTION)), selection_text)
|
|
727
|
-
table.update_cell_at(cast(Coordinate, (row_index, COLUMN_LATEST)), latest_display)
|
|
728
|
-
table.update_cell_at(cast(Coordinate, (row_index, COLUMN_TYPE)), type_display)
|
|
729
|
-
except Exception:
|
|
730
|
-
# Coordinates are invalid, do a full refresh
|
|
731
|
-
# Preserve cursor position when doing emergency refresh
|
|
732
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
733
|
-
|
|
734
|
-
except Exception:
|
|
735
|
-
# Log error for debugging but don't crash the app - make it less verbose
|
|
736
|
-
pass # Silent failure for coordinate errors
|
|
737
|
-
|
|
738
|
-
def _format_latest_version(self, latest_version: str, constraint: Optional[str]) -> Text:
|
|
739
|
-
"""
|
|
740
|
-
Format latest version with conditional coloring based on constraint satisfaction.
|
|
741
|
-
|
|
742
|
-
:param latest_version: The latest version string
|
|
743
|
-
:param constraint: Optional constraint specification
|
|
744
|
-
:returns: Text object with appropriate color markup
|
|
745
|
-
"""
|
|
746
|
-
color = get_constraint_color(latest_version, constraint)
|
|
747
|
-
return Text.from_markup(f"[{color}]{latest_version}[/{color}]")
|
|
748
|
-
|
|
749
|
-
def _get_selected_package(self) -> dict | None:
|
|
750
|
-
"""
|
|
751
|
-
Get the complete package data for the currently selected row in the table.
|
|
752
|
-
|
|
753
|
-
This method properly handles the table cursor and filtering to return
|
|
754
|
-
the correct package data, avoiding RowKey object issues.
|
|
755
|
-
|
|
756
|
-
:returns: Package dictionary or None if no valid selection
|
|
757
|
-
"""
|
|
758
|
-
table = self.query_one("#main-table", DataTable)
|
|
759
|
-
if table.cursor_row is None or table.cursor_row >= len(table.rows):
|
|
760
|
-
return None
|
|
761
|
-
|
|
762
|
-
# Filter packages based on current display settings
|
|
763
|
-
packages_to_show = []
|
|
764
|
-
for pkg in self.all_packages:
|
|
765
|
-
if self.filter_outdated_only:
|
|
766
|
-
if pkg.get("outdated", False):
|
|
767
|
-
packages_to_show.append(pkg)
|
|
768
|
-
else:
|
|
769
|
-
packages_to_show.append(pkg)
|
|
770
|
-
|
|
771
|
-
# Sort to match table order
|
|
772
|
-
packages_to_show.sort(key=lambda x: x["name"].lower())
|
|
773
|
-
|
|
774
|
-
# Get the package at the cursor position
|
|
775
|
-
if table.cursor_row < len(packages_to_show):
|
|
776
|
-
return packages_to_show[table.cursor_row]
|
|
777
|
-
|
|
778
|
-
return None
|
|
779
|
-
|
|
780
|
-
def _refresh_table_display(self, preserve_cursor: bool = False) -> None:
|
|
781
|
-
"""Refresh the table display based on current filter settings."""
|
|
782
|
-
try:
|
|
783
|
-
table = self.query_one("#main-table", DataTable)
|
|
784
|
-
|
|
785
|
-
# Save cursor position and scroll offset if requested
|
|
786
|
-
cursor_row = None
|
|
787
|
-
cursor_package_name = None
|
|
788
|
-
scroll_offset_y = None
|
|
789
|
-
if preserve_cursor:
|
|
790
|
-
if table.cursor_row is not None:
|
|
791
|
-
cursor_row = table.cursor_row
|
|
792
|
-
# Try to get the package name at the current cursor position
|
|
793
|
-
try:
|
|
794
|
-
if cursor_row < len(table.rows):
|
|
795
|
-
# Get the package name from the displayed packages list
|
|
796
|
-
packages_to_show = []
|
|
797
|
-
for pkg in self.all_packages:
|
|
798
|
-
if self.filter_outdated_only:
|
|
799
|
-
if pkg.get("outdated", False):
|
|
800
|
-
packages_to_show.append(pkg)
|
|
801
|
-
else:
|
|
802
|
-
packages_to_show.append(pkg)
|
|
803
|
-
packages_to_show.sort(key=lambda x: x["name"].lower())
|
|
804
|
-
|
|
805
|
-
if cursor_row < len(packages_to_show):
|
|
806
|
-
cursor_package_name = packages_to_show[cursor_row]['name']
|
|
807
|
-
except Exception:
|
|
808
|
-
pass
|
|
809
|
-
|
|
810
|
-
# Save the current scroll position
|
|
811
|
-
try:
|
|
812
|
-
scroll_offset_y = table.scroll_offset.y
|
|
813
|
-
except Exception:
|
|
814
|
-
pass
|
|
815
|
-
|
|
816
|
-
# Clear only the rows, not the columns
|
|
817
|
-
table.clear(columns=False)
|
|
818
|
-
|
|
819
|
-
# Filter packages based on current settings
|
|
820
|
-
packages_to_show = []
|
|
821
|
-
for pkg in self.all_packages:
|
|
822
|
-
if self.filter_outdated_only:
|
|
823
|
-
# In filtered mode, only show packages that have been confirmed as outdated
|
|
824
|
-
# Don't show packages that are still being checked ("Checking...") or up-to-date
|
|
825
|
-
if pkg.get("outdated", False):
|
|
826
|
-
packages_to_show.append(pkg)
|
|
827
|
-
# Skip packages with "Checking..." or up-to-date packages
|
|
828
|
-
else:
|
|
829
|
-
# Show all packages
|
|
830
|
-
packages_to_show.append(pkg)
|
|
831
|
-
|
|
832
|
-
# Ensure packages remain sorted alphabetically
|
|
833
|
-
packages_to_show.sort(key=lambda x: x["name"].lower())
|
|
834
|
-
|
|
835
|
-
# Rebuild row mapping for displayed packages
|
|
836
|
-
self.package_row_mapping = {}
|
|
837
|
-
|
|
838
|
-
# Add rows to table
|
|
839
|
-
for i, pkg in enumerate(packages_to_show):
|
|
840
|
-
constraint_display = pkg.get('constraint', '')
|
|
841
|
-
if constraint_display:
|
|
842
|
-
constraint_text = Text.from_markup(f"[yellow]{constraint_display}[/yellow]")
|
|
843
|
-
else:
|
|
844
|
-
constraint_text = Text.from_markup("[dim]-[/dim]")
|
|
845
|
-
|
|
846
|
-
# Format the latest version with color coding based on update status
|
|
847
|
-
latest_version = pkg.get("latest_version", "Checking...")
|
|
848
|
-
if latest_version == "Checking...":
|
|
849
|
-
# Still checking, show as-is
|
|
850
|
-
latest_display = latest_version
|
|
851
|
-
type_display = pkg.get("latest_filetype", "")
|
|
852
|
-
elif latest_version == pkg["version"]:
|
|
853
|
-
# Package is up-to-date, show in default color
|
|
854
|
-
latest_display = latest_version
|
|
855
|
-
type_display = "" # Empty type column for current packages
|
|
856
|
-
else:
|
|
857
|
-
# Package is outdated - check if it can be updated
|
|
858
|
-
constraint = pkg.get('constraint')
|
|
859
|
-
# Use utility method for consistent formatting
|
|
860
|
-
latest_display = self._format_latest_version(latest_version, constraint)
|
|
861
|
-
type_display = pkg.get("latest_filetype", "")
|
|
862
|
-
|
|
863
|
-
# Format invalid when display
|
|
864
|
-
invalid_when = pkg.get('invalid_when')
|
|
865
|
-
if invalid_when:
|
|
866
|
-
invalid_when_text = Text.from_markup(f"[yellow]{invalid_when}[/yellow]")
|
|
867
|
-
else:
|
|
868
|
-
invalid_when_text = Text.from_markup("[dim]-[/dim]")
|
|
869
|
-
|
|
870
|
-
# Format selection indicator
|
|
871
|
-
if pkg.get("selected", False):
|
|
872
|
-
selection_text = Text("●", style="green bold") # Selected indicator
|
|
873
|
-
else:
|
|
874
|
-
selection_text = Text(" ", style="dim") # Empty space
|
|
875
|
-
|
|
876
|
-
# Format package name with editable indicator
|
|
877
|
-
if pkg.get("editable", False):
|
|
878
|
-
package_display = Text.from_markup(f"[bold cyan]{pkg['name']}[/bold cyan] [dim]📝[/dim]")
|
|
879
|
-
else:
|
|
880
|
-
package_display = pkg["name"]
|
|
881
|
-
|
|
882
|
-
table.add_row(
|
|
883
|
-
selection_text,
|
|
884
|
-
package_display,
|
|
885
|
-
pkg["version"],
|
|
886
|
-
latest_display,
|
|
887
|
-
type_display,
|
|
888
|
-
constraint_text,
|
|
889
|
-
invalid_when_text,
|
|
890
|
-
key=pkg["name"]
|
|
891
|
-
)
|
|
892
|
-
# Update row mapping for this package
|
|
893
|
-
self.package_row_mapping[pkg["name"]] = i
|
|
894
|
-
|
|
895
|
-
# Restore cursor position and scroll offset if possible
|
|
896
|
-
if preserve_cursor:
|
|
897
|
-
# First restore cursor position
|
|
898
|
-
if cursor_package_name:
|
|
899
|
-
# Try to find the package in the new table and restore cursor
|
|
900
|
-
new_row_index = self.package_row_mapping.get(cursor_package_name)
|
|
901
|
-
if new_row_index is not None:
|
|
902
|
-
try:
|
|
903
|
-
table.move_cursor(row=new_row_index)
|
|
904
|
-
except Exception:
|
|
905
|
-
pass # If cursor restoration fails, just continue
|
|
906
|
-
elif cursor_row is not None:
|
|
907
|
-
# Package is no longer visible, try to restore to the same row index
|
|
908
|
-
try:
|
|
909
|
-
max_row = len(table.rows) - 1
|
|
910
|
-
if max_row >= 0:
|
|
911
|
-
restore_row = min(cursor_row, max_row)
|
|
912
|
-
table.move_cursor(row=restore_row)
|
|
913
|
-
except Exception:
|
|
914
|
-
pass
|
|
915
|
-
|
|
916
|
-
# Then restore scroll position
|
|
917
|
-
if scroll_offset_y is not None:
|
|
918
|
-
try:
|
|
919
|
-
# Schedule scroll restoration to happen after the table is rendered
|
|
920
|
-
self.set_timer(0.01, lambda: self._restore_scroll_position(table, scroll_offset_y))
|
|
921
|
-
except Exception:
|
|
922
|
-
pass
|
|
923
|
-
|
|
924
|
-
except Exception:
|
|
925
|
-
pass
|
|
926
|
-
|
|
927
|
-
def _restore_scroll_position(self, table, scroll_y: int) -> None:
|
|
928
|
-
"""Helper method to restore scroll position after table refresh."""
|
|
929
|
-
try:
|
|
930
|
-
table.scroll_to(y=scroll_y, animate=False)
|
|
931
|
-
except Exception:
|
|
932
|
-
pass # If scroll restoration fails, just continue
|
|
933
|
-
|
|
934
|
-
def _show_uninstall_confirmation(self, package_name: str) -> None:
|
|
935
|
-
"""Show confirmation dialog for uninstalling a package."""
|
|
936
|
-
def uninstall_confirmed(confirmed: bool | None) -> None:
|
|
937
|
-
if confirmed:
|
|
938
|
-
self._uninstall_package(package_name)
|
|
939
|
-
|
|
940
|
-
self.push_screen(UninstallConfirmScreen(package_name), uninstall_confirmed)
|
|
941
|
-
|
|
942
|
-
def _uninstall_package(self, package_name: str) -> None:
|
|
943
|
-
"""Actually uninstall the package."""
|
|
944
|
-
self._update_status(f"Uninstalling {package_name}...", True)
|
|
945
|
-
|
|
946
|
-
def run_uninstall():
|
|
947
|
-
"""Run pip uninstall in a worker thread."""
|
|
948
|
-
try:
|
|
949
|
-
# Use sys.executable to find the correct pip for the current Python environment
|
|
950
|
-
pip_cmd = [sys.executable, "-m", "pip", "uninstall", package_name, "-y"]
|
|
951
|
-
result = subprocess.run(
|
|
952
|
-
pip_cmd,
|
|
953
|
-
capture_output=True,
|
|
954
|
-
text=True,
|
|
955
|
-
timeout=UNINSTALL_TIMEOUT
|
|
956
|
-
)
|
|
957
|
-
|
|
958
|
-
if result.returncode == 0:
|
|
959
|
-
self.call_from_thread(self.notify, f"Successfully uninstalled {package_name}", "information")
|
|
960
|
-
self.call_from_thread(self._remove_package_from_table, package_name)
|
|
961
|
-
else:
|
|
962
|
-
self.call_from_thread(self.notify, f"Failed to uninstall {package_name}: {result.stderr}", "error")
|
|
963
|
-
except subprocess.TimeoutExpired:
|
|
964
|
-
self.call_from_thread(self.notify, f"Uninstall of {package_name} timed out", "error")
|
|
965
|
-
except Exception as e:
|
|
966
|
-
self.call_from_thread(self.notify, f"Error uninstalling {package_name}: {e}", "error")
|
|
967
|
-
finally:
|
|
968
|
-
self.call_from_thread(self._update_status, "Ready", False)
|
|
969
|
-
|
|
970
|
-
self.run_worker(run_uninstall, thread=True, exclusive=False)
|
|
971
|
-
|
|
972
|
-
def _remove_package_from_table(self, package_name: str) -> None:
|
|
973
|
-
"""Remove a package from the table after uninstall."""
|
|
974
|
-
try:
|
|
975
|
-
table = self.query_one("#main-table", DataTable)
|
|
976
|
-
if package_name in table.rows:
|
|
977
|
-
table.remove_row(package_name)
|
|
978
|
-
|
|
979
|
-
# Remove from our data structures
|
|
980
|
-
self.all_packages = [pkg for pkg in self.all_packages if pkg["name"] != package_name]
|
|
981
|
-
self.outdated_packages = [pkg for pkg in self.outdated_packages if pkg["name"] != package_name]
|
|
982
|
-
except Exception:
|
|
983
|
-
pass
|
|
984
|
-
|
|
985
|
-
def _update_outdated_results(self, outdated_packages: List[Dict[str, Any]]) -> None:
|
|
986
|
-
"""Update the table with outdated package results."""
|
|
987
|
-
self.outdated_packages = outdated_packages
|
|
988
|
-
self.update_check_complete = True
|
|
989
|
-
self.update_check_successful = True # Mark as successful
|
|
990
|
-
self._set_update_button_enabled(True)
|
|
991
|
-
|
|
992
|
-
# Create a mapping for quick lookup
|
|
993
|
-
outdated_map = {pkg['name'].lower(): pkg for pkg in outdated_packages}
|
|
994
|
-
|
|
995
|
-
# Update package data first (source of truth)
|
|
996
|
-
for package in self.all_packages:
|
|
997
|
-
package_name_lower = package['name'].lower()
|
|
998
|
-
|
|
999
|
-
if package_name_lower in outdated_map:
|
|
1000
|
-
# Package is outdated
|
|
1001
|
-
outdated_info = outdated_map[package_name_lower]
|
|
1002
|
-
package['outdated'] = True
|
|
1003
|
-
package['latest_version'] = outdated_info['latest_version']
|
|
1004
|
-
package['latest_filetype'] = outdated_info['latest_filetype']
|
|
1005
|
-
|
|
1006
|
-
# Auto-select if no constraint or constraint is satisfied
|
|
1007
|
-
constraint = package.get('constraint')
|
|
1008
|
-
if constraint:
|
|
1009
|
-
selected = _check_constraint_satisfaction(outdated_info['latest_version'], constraint)
|
|
1010
|
-
else:
|
|
1011
|
-
selected = True
|
|
1012
|
-
|
|
1013
|
-
package['selected'] = selected
|
|
1014
|
-
|
|
1015
|
-
else:
|
|
1016
|
-
# Package is up to date
|
|
1017
|
-
package['outdated'] = False
|
|
1018
|
-
package['latest_version'] = package['version']
|
|
1019
|
-
package['latest_filetype'] = ""
|
|
1020
|
-
package['selected'] = False
|
|
1021
|
-
|
|
1022
|
-
# Now update the visible table display based on current filter
|
|
1023
|
-
# This handles the row mapping correctly and ensures table consistency
|
|
1024
|
-
self._refresh_table_display()
|
|
1025
|
-
|
|
1026
|
-
# Update status
|
|
1027
|
-
outdated_count = len(outdated_packages)
|
|
1028
|
-
selected_count = sum(1 for pkg in self.all_packages if pkg.get('selected', False))
|
|
1029
|
-
|
|
1030
|
-
if outdated_count > 0:
|
|
1031
|
-
self._update_status(f"Found {outdated_count} outdated packages, {selected_count} selected for update", False)
|
|
1032
|
-
else:
|
|
1033
|
-
self._update_status("All packages are up to date!", False)
|
|
1034
|
-
|
|
1035
|
-
def action_quit_app(self) -> None:
|
|
1036
|
-
"""Quit the application."""
|
|
1037
|
-
# Signal cancellation to the update check if it's running
|
|
1038
|
-
if hasattr(self, 'update_check_cancel_event') and self.update_check_cancel_event:
|
|
1039
|
-
self.update_check_cancel_event.set()
|
|
1040
|
-
|
|
1041
|
-
# Cancel any running workers before exiting with proper tracking
|
|
1042
|
-
cancelled_workers = []
|
|
1043
|
-
failed_workers = []
|
|
1044
|
-
|
|
1045
|
-
for worker in self.workers:
|
|
1046
|
-
if not worker.is_finished:
|
|
1047
|
-
try:
|
|
1048
|
-
worker.cancel()
|
|
1049
|
-
cancelled_workers.append(worker.name or "unnamed")
|
|
1050
|
-
except Exception as e:
|
|
1051
|
-
logger.error(f"Failed to cancel worker {worker.name or 'unnamed'}: {e}")
|
|
1052
|
-
failed_workers.append(worker.name or "unnamed")
|
|
1053
|
-
|
|
1054
|
-
if cancelled_workers:
|
|
1055
|
-
logger.debug(f"Cancelled workers: {', '.join(cancelled_workers)}")
|
|
1056
|
-
if failed_workers:
|
|
1057
|
-
logger.warning(f"Failed to cancel workers: {', '.join(failed_workers)}")
|
|
1058
|
-
|
|
1059
|
-
# Exit the application
|
|
1060
|
-
self.exit()
|
|
1061
|
-
|
|
1062
|
-
def action_handle_enter(self) -> None:
|
|
1063
|
-
"""Handle Enter key - only quit if update check failed."""
|
|
1064
|
-
# Only allow Enter to quit if the update check failed
|
|
1065
|
-
if self.update_check_complete and not self.update_check_successful:
|
|
1066
|
-
self.exit()
|
|
1067
|
-
# Otherwise, do nothing (Enter doesn't quit during normal operation)
|
|
1068
|
-
|
|
1069
|
-
def action_filter_outdated(self) -> None:
|
|
1070
|
-
"""Set filter to show only outdated packages."""
|
|
1071
|
-
if not self.filter_outdated_only:
|
|
1072
|
-
self.filter_outdated_only = True
|
|
1073
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1074
|
-
|
|
1075
|
-
# Update the description
|
|
1076
|
-
try:
|
|
1077
|
-
description = self.query_one("#filter-description", Static)
|
|
1078
|
-
description.update("Show outdated only")
|
|
1079
|
-
except NoWidget:
|
|
1080
|
-
logger.debug("Filter description widget not found - context may not support it")
|
|
1081
|
-
except Exception as e:
|
|
1082
|
-
logger.warning(f"Could not update filter description: {e}")
|
|
1083
|
-
|
|
1084
|
-
self.notify("Filter: showing only outdated packages")
|
|
1085
|
-
else:
|
|
1086
|
-
# Already filtering, just acknowledge
|
|
1087
|
-
self.notify("Filter: already showing only outdated packages")
|
|
1088
|
-
|
|
1089
|
-
def action_show_all(self) -> None:
|
|
1090
|
-
"""Set filter to show all packages."""
|
|
1091
|
-
if self.filter_outdated_only:
|
|
1092
|
-
self.filter_outdated_only = False
|
|
1093
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1094
|
-
|
|
1095
|
-
# Update the description
|
|
1096
|
-
try:
|
|
1097
|
-
description = self.query_one("#filter-description", Static)
|
|
1098
|
-
description.update("Show all packages")
|
|
1099
|
-
except NoWidget:
|
|
1100
|
-
logger.debug("Filter description widget not found - context may not support it")
|
|
1101
|
-
except Exception as e:
|
|
1102
|
-
logger.warning(f"Could not update filter description: {e}")
|
|
1103
|
-
|
|
1104
|
-
self.notify("Filter: showing all packages")
|
|
1105
|
-
else:
|
|
1106
|
-
# Already showing all, just acknowledge
|
|
1107
|
-
self.notify("Filter: already showing all packages")
|
|
1108
|
-
|
|
1109
|
-
def action_uninstall_package(self) -> None:
|
|
1110
|
-
"""Uninstall the currently selected package."""
|
|
1111
|
-
selected_package = self._get_selected_package()
|
|
1112
|
-
if selected_package:
|
|
1113
|
-
self._show_uninstall_confirmation(selected_package['name'])
|
|
1114
|
-
|
|
1115
|
-
def action_show_help(self) -> None:
|
|
1116
|
-
"""Show the help modal with keyboard shortcuts and features."""
|
|
1117
|
-
self.push_screen(HelpScreen())
|
|
1118
|
-
|
|
1119
|
-
def action_add_constraint(self) -> None:
|
|
1120
|
-
"""Add constraint to the currently selected package."""
|
|
1121
|
-
selected_package = self._get_selected_package()
|
|
1122
|
-
if not selected_package:
|
|
1123
|
-
return
|
|
1124
|
-
|
|
1125
|
-
package_name = selected_package['name']
|
|
1126
|
-
current_constraint = selected_package.get('constraint', '')
|
|
1127
|
-
|
|
1128
|
-
def handle_constraint_result(result) -> None:
|
|
1129
|
-
"""Handle the result from constraint input dialog."""
|
|
1130
|
-
if result:
|
|
1131
|
-
constraint = "" # Initialize to prevent unbound variable
|
|
1132
|
-
try:
|
|
1133
|
-
# Handle both string (constraint only) and tuple (constraint, trigger) results
|
|
1134
|
-
if isinstance(result, tuple):
|
|
1135
|
-
constraint, invalidation_trigger = result
|
|
1136
|
-
else:
|
|
1137
|
-
constraint = result
|
|
1138
|
-
invalidation_trigger = ""
|
|
1139
|
-
|
|
1140
|
-
from ..package_constraints import add_constraints_to_config
|
|
1141
|
-
|
|
1142
|
-
# Add constraint to configuration
|
|
1143
|
-
constraint_spec = f"{package_name}{constraint}"
|
|
1144
|
-
config_path, changes = add_constraints_to_config([constraint_spec])
|
|
1145
|
-
|
|
1146
|
-
# Add invalidation trigger if provided
|
|
1147
|
-
if invalidation_trigger:
|
|
1148
|
-
from ..package_constraints import format_invalidation_triggers, _get_section_name, _load_config, _write_config_file
|
|
1149
|
-
|
|
1150
|
-
section_name = _get_section_name(None)
|
|
1151
|
-
config, _ = _load_config(create_if_missing=False)
|
|
1152
|
-
|
|
1153
|
-
# Format the trigger entry
|
|
1154
|
-
formatted_entry = format_invalidation_triggers(constraint_spec, [invalidation_trigger])
|
|
1155
|
-
if formatted_entry:
|
|
1156
|
-
# Get existing triggers
|
|
1157
|
-
existing_triggers = ""
|
|
1158
|
-
if config.has_option(section_name, 'constraint_invalid_when'):
|
|
1159
|
-
existing_triggers = config.get(section_name, 'constraint_invalid_when')
|
|
1160
|
-
|
|
1161
|
-
# Add the new trigger
|
|
1162
|
-
if existing_triggers.strip():
|
|
1163
|
-
triggers_value = f"{existing_triggers},{formatted_entry}"
|
|
1164
|
-
else:
|
|
1165
|
-
triggers_value = formatted_entry
|
|
1166
|
-
|
|
1167
|
-
config.set(section_name, 'constraint_invalid_when', triggers_value)
|
|
1168
|
-
_write_config_file(config, config_path)
|
|
1169
|
-
|
|
1170
|
-
# Update the package data in all_packages
|
|
1171
|
-
for pkg in self.all_packages:
|
|
1172
|
-
if pkg['name'] == package_name:
|
|
1173
|
-
pkg['constraint'] = constraint
|
|
1174
|
-
if invalidation_trigger:
|
|
1175
|
-
pkg['invalid_when'] = invalidation_trigger
|
|
1176
|
-
break
|
|
1177
|
-
|
|
1178
|
-
# Refresh the table display
|
|
1179
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1180
|
-
|
|
1181
|
-
# Show success message
|
|
1182
|
-
change_type, old_constraint = changes.get(package_name.lower(), ('added', None))
|
|
1183
|
-
if change_type == 'updated':
|
|
1184
|
-
message = f"Updated constraint for {package_name}: {old_constraint} → {constraint}"
|
|
1185
|
-
elif change_type == 'added':
|
|
1186
|
-
message = f"Added constraint {package_name}{constraint}"
|
|
1187
|
-
else:
|
|
1188
|
-
message = f"Constraint {package_name}{constraint} already exists"
|
|
1189
|
-
|
|
1190
|
-
if invalidation_trigger:
|
|
1191
|
-
message += f" with invalidation trigger: {invalidation_trigger}"
|
|
1192
|
-
self.notify(message)
|
|
1193
|
-
|
|
1194
|
-
except Exception as e:
|
|
1195
|
-
error_msg = str(e)
|
|
1196
|
-
if "Invalid constraint specification" in error_msg:
|
|
1197
|
-
self.notify(f"Invalid constraint '{constraint}' for {package_name}. Try formats like: >=1.0.0, <2.0, ==1.5.0, >1.0")
|
|
1198
|
-
else:
|
|
1199
|
-
self.notify(f"Error adding constraint: {e}")
|
|
1200
|
-
|
|
1201
|
-
# Show constraint input screen
|
|
1202
|
-
self.push_screen(
|
|
1203
|
-
ConstraintInputScreen(package_name, current_constraint),
|
|
1204
|
-
handle_constraint_result
|
|
1205
|
-
)
|
|
1206
|
-
|
|
1207
|
-
def action_delete_constraint(self) -> None:
|
|
1208
|
-
"""Delete constraint for the currently selected package."""
|
|
1209
|
-
selected_package = self._get_selected_package()
|
|
1210
|
-
if not selected_package:
|
|
1211
|
-
return
|
|
1212
|
-
|
|
1213
|
-
package_name = selected_package['name']
|
|
1214
|
-
current_constraint = selected_package.get('constraint')
|
|
1215
|
-
|
|
1216
|
-
if not current_constraint:
|
|
1217
|
-
self.notify(f"No constraint to delete for {package_name}")
|
|
1218
|
-
return
|
|
1219
|
-
|
|
1220
|
-
def handle_delete_confirmation(confirmed: bool | None) -> None:
|
|
1221
|
-
"""Handle the result from delete constraint confirmation."""
|
|
1222
|
-
if confirmed:
|
|
1223
|
-
try:
|
|
1224
|
-
from ..package_constraints import remove_constraints_from_config
|
|
1225
|
-
|
|
1226
|
-
# Remove constraint from configuration
|
|
1227
|
-
_, removed_constraints, removed_triggers = remove_constraints_from_config([package_name])
|
|
1228
|
-
|
|
1229
|
-
if package_name.lower() in removed_constraints:
|
|
1230
|
-
# Update the package data in all_packages
|
|
1231
|
-
for pkg in self.all_packages:
|
|
1232
|
-
if pkg['name'] == package_name:
|
|
1233
|
-
pkg.pop('constraint', None)
|
|
1234
|
-
pkg.pop('invalid_when', None)
|
|
1235
|
-
break
|
|
1236
|
-
|
|
1237
|
-
# Refresh the table display
|
|
1238
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1239
|
-
|
|
1240
|
-
# Show success message
|
|
1241
|
-
trigger_count = len(removed_triggers.get(package_name.lower(), []))
|
|
1242
|
-
if trigger_count > 0:
|
|
1243
|
-
self.notify(f"Deleted constraint and {trigger_count} invalidation triggers for {package_name}")
|
|
1244
|
-
else:
|
|
1245
|
-
self.notify(f"Deleted constraint for {package_name}")
|
|
1246
|
-
else:
|
|
1247
|
-
self.notify(f"No constraint found for {package_name} in configuration")
|
|
1248
|
-
|
|
1249
|
-
except Exception as e:
|
|
1250
|
-
self.notify(f"Error deleting constraint: {e}")
|
|
1251
|
-
|
|
1252
|
-
# Show confirmation dialog
|
|
1253
|
-
self.push_screen(
|
|
1254
|
-
DeleteConstraintConfirmScreen(package_name, current_constraint),
|
|
1255
|
-
handle_delete_confirmation
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
def action_remove_all_constraints(self) -> None:
|
|
1259
|
-
"""Remove all constraints from the pip configuration."""
|
|
1260
|
-
# Count current constraints
|
|
1261
|
-
constraint_count = len(self.constraints)
|
|
1262
|
-
|
|
1263
|
-
if constraint_count == 0:
|
|
1264
|
-
self.notify("No constraints to remove")
|
|
1265
|
-
return
|
|
1266
|
-
|
|
1267
|
-
def handle_remove_all_confirmation(confirmed: bool | None) -> None:
|
|
1268
|
-
"""Handle the result from remove all constraints confirmation."""
|
|
1269
|
-
if confirmed:
|
|
1270
|
-
try:
|
|
1271
|
-
from ..package_constraints import _get_section_name, _load_config, _write_config_file
|
|
1272
|
-
|
|
1273
|
-
# Get config section
|
|
1274
|
-
section_name = _get_section_name(None)
|
|
1275
|
-
config, config_path = _load_config(create_if_missing=False)
|
|
1276
|
-
|
|
1277
|
-
if not config.has_section(section_name):
|
|
1278
|
-
self.notify("No constraints configuration found")
|
|
1279
|
-
return
|
|
1280
|
-
|
|
1281
|
-
# Remove all constraint-related options
|
|
1282
|
-
options_removed = []
|
|
1283
|
-
if config.has_option(section_name, 'constraint'):
|
|
1284
|
-
config.remove_option(section_name, 'constraint')
|
|
1285
|
-
options_removed.append('constraints')
|
|
1286
|
-
|
|
1287
|
-
if config.has_option(section_name, 'constraint_invalid_when'):
|
|
1288
|
-
config.remove_option(section_name, 'constraint_invalid_when')
|
|
1289
|
-
options_removed.append('invalidation triggers')
|
|
1290
|
-
|
|
1291
|
-
# Remove the section if it's empty
|
|
1292
|
-
if not config.options(section_name):
|
|
1293
|
-
config.remove_section(section_name)
|
|
1294
|
-
|
|
1295
|
-
# Write the updated config
|
|
1296
|
-
_write_config_file(config, config_path)
|
|
1297
|
-
|
|
1298
|
-
# Update our internal state
|
|
1299
|
-
self.constraints = {}
|
|
1300
|
-
self.invalidation_triggers = {}
|
|
1301
|
-
|
|
1302
|
-
# Update all package data
|
|
1303
|
-
for pkg in self.all_packages:
|
|
1304
|
-
pkg.pop('constraint', None)
|
|
1305
|
-
pkg.pop('invalid_when', None)
|
|
1306
|
-
|
|
1307
|
-
# Refresh the table display
|
|
1308
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1309
|
-
|
|
1310
|
-
# Show success message
|
|
1311
|
-
if options_removed:
|
|
1312
|
-
self.notify(f"Removed all {constraint_count} constraints and {' and '.join(options_removed)}")
|
|
1313
|
-
else:
|
|
1314
|
-
self.notify("No constraints were found to remove")
|
|
1315
|
-
|
|
1316
|
-
except Exception as e:
|
|
1317
|
-
self.notify(f"Error removing constraints: {e}")
|
|
1318
|
-
|
|
1319
|
-
# Show confirmation dialog
|
|
1320
|
-
self.push_screen(
|
|
1321
|
-
RemoveAllConstraintsConfirmScreen(constraint_count),
|
|
1322
|
-
handle_remove_all_confirmation
|
|
1323
|
-
)
|
|
1324
|
-
|
|
1325
|
-
def _reload_constraints_in_ui(self) -> None:
|
|
1326
|
-
"""Reload constraints from configuration and update the UI display."""
|
|
1327
|
-
try:
|
|
1328
|
-
# Reload constraints and invalidation triggers from configuration
|
|
1329
|
-
self.constraints = read_constraints()
|
|
1330
|
-
self.invalidation_triggers = self._load_invalidation_triggers()
|
|
1331
|
-
|
|
1332
|
-
# Update all packages with new constraint and invalidation trigger information
|
|
1333
|
-
for pkg in self.all_packages:
|
|
1334
|
-
pkg['constraint'] = self.constraints.get(pkg['name'].lower())
|
|
1335
|
-
|
|
1336
|
-
# Update invalidation triggers
|
|
1337
|
-
package_triggers = self.invalidation_triggers.get(pkg['name'].lower(), [])
|
|
1338
|
-
pkg['invalid_when'] = ", ".join(package_triggers) if package_triggers else None
|
|
1339
|
-
|
|
1340
|
-
# Refresh table display to show updated constraints
|
|
1341
|
-
self._refresh_table_display(preserve_cursor=True)
|
|
1342
|
-
|
|
1343
|
-
except Exception as e:
|
|
1344
|
-
self.notify(f"Error reloading constraints in UI: {e}")
|
|
1345
|
-
|
|
1346
|
-
def _set_update_button_enabled(self, enabled: bool) -> None:
|
|
1347
|
-
"""Enable or disable the Update Selected button."""
|
|
1348
|
-
try:
|
|
1349
|
-
button = self.query_one("#update-btn", Button)
|
|
1350
|
-
button.disabled = not enabled
|
|
1351
|
-
if not enabled:
|
|
1352
|
-
button.label = "Checking Updates..."
|
|
1353
|
-
else:
|
|
1354
|
-
button.label = "Update Selected"
|
|
1355
|
-
except Exception:
|
|
1356
|
-
# Button might not exist or be accessible in all contexts
|
|
1357
|
-
pass
|
|
1358
|
-
|
|
1359
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
1360
|
-
"""Handle button presses."""
|
|
1361
|
-
if event.button.id == "update-btn":
|
|
1362
|
-
self.action_update_selected()
|
|
1363
|
-
elif event.button.id == "quit-btn":
|
|
1364
|
-
self.action_quit_app()
|
|
1365
|
-
|
|
1366
|
-
def action_update_selected(self) -> None:
|
|
1367
|
-
"""Update selected packages."""
|
|
1368
|
-
if not self.update_check_complete:
|
|
1369
|
-
self.notify("⏳ Still checking for updates... Press U again once checking is complete", severity="warning")
|
|
1370
|
-
return
|
|
1371
|
-
|
|
1372
|
-
# Check if the update check was successful
|
|
1373
|
-
if not self.update_check_successful:
|
|
1374
|
-
self.notify("❌ Cannot update packages - the update check failed. Please check your network connection and try restarting pipu.", severity="error")
|
|
1375
|
-
return
|
|
1376
|
-
|
|
1377
|
-
# Get selected packages (those marked with green dots)
|
|
1378
|
-
selected_packages = []
|
|
1379
|
-
|
|
1380
|
-
for pkg in self.all_packages:
|
|
1381
|
-
# Check if this package is selected for update
|
|
1382
|
-
if pkg.get('selected', False) and pkg.get('outdated', False):
|
|
1383
|
-
selected_packages.append(pkg)
|
|
1384
|
-
|
|
1385
|
-
if not selected_packages:
|
|
1386
|
-
self.notify("No packages selected for update", severity="warning")
|
|
1387
|
-
return
|
|
1388
|
-
|
|
1389
|
-
# Show confirmation dialog with update details
|
|
1390
|
-
def handle_update_confirmation(confirmed: bool | None) -> None:
|
|
1391
|
-
"""Handle the result from update confirmation dialog."""
|
|
1392
|
-
logger.info(f"Update confirmation result: {confirmed}")
|
|
1393
|
-
if confirmed:
|
|
1394
|
-
logger.info(f"Pushing PackageUpdateScreen with {len(selected_packages)} packages")
|
|
1395
|
-
# Push the PackageUpdateScreen which handles the full update process
|
|
1396
|
-
self.push_screen(PackageUpdateScreen(selected_packages))
|
|
1397
|
-
logger.info("PackageUpdateScreen pushed successfully")
|
|
1398
|
-
else:
|
|
1399
|
-
logger.info("Update cancelled by user")
|
|
1400
|
-
# If cancelled, just return without doing anything
|
|
1401
|
-
|
|
1402
|
-
logger.info(f"Showing UpdateConfirmScreen for {len(selected_packages)} packages")
|
|
1403
|
-
self.push_screen(UpdateConfirmScreen(selected_packages), handle_update_confirmation)
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
def main_tui_app() -> None:
|
|
1408
|
-
"""Launch the main TUI application."""
|
|
1409
|
-
# Set up terminal cleanup handlers before starting the app
|
|
1410
|
-
_setup_signal_handlers()
|
|
1411
|
-
|
|
1412
|
-
try:
|
|
1413
|
-
app = MainTUIApp()
|
|
1414
|
-
app.run()
|
|
1415
|
-
except KeyboardInterrupt:
|
|
1416
|
-
# Handle Ctrl+C gracefully
|
|
1417
|
-
_restore_terminal()
|
|
1418
|
-
sys.exit(0)
|
|
1419
|
-
except Exception as e:
|
|
1420
|
-
# Handle any other exceptions
|
|
1421
|
-
_restore_terminal()
|
|
1422
|
-
logger.error(f"TUI application error: {e}")
|
|
1423
|
-
raise
|
|
1424
|
-
finally:
|
|
1425
|
-
# Always restore terminal state
|
|
1426
|
-
_restore_terminal()
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
def interactive_package_selection(outdated_packages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1430
|
-
"""
|
|
1431
|
-
Run interactive package selection using Textual TUI.
|
|
1432
|
-
|
|
1433
|
-
:param outdated_packages: List of outdated package dictionaries
|
|
1434
|
-
:returns: List of selected package dictionaries
|
|
1435
|
-
"""
|
|
1436
|
-
if not outdated_packages:
|
|
1437
|
-
return []
|
|
1438
|
-
|
|
1439
|
-
# Set up terminal cleanup handlers before starting the app
|
|
1440
|
-
_setup_signal_handlers()
|
|
1441
|
-
|
|
1442
|
-
try:
|
|
1443
|
-
app = PackageSelectionApp(outdated_packages)
|
|
1444
|
-
app.run()
|
|
1445
|
-
|
|
1446
|
-
if app.confirmed:
|
|
1447
|
-
return app.selected_packages
|
|
1448
|
-
else:
|
|
1449
|
-
return []
|
|
1450
|
-
except KeyboardInterrupt:
|
|
1451
|
-
# Handle Ctrl+C gracefully
|
|
1452
|
-
_restore_terminal()
|
|
1453
|
-
return []
|
|
1454
|
-
except Exception:
|
|
1455
|
-
# Handle any other exceptions
|
|
1456
|
-
_restore_terminal()
|
|
1457
|
-
raise
|
|
1458
|
-
finally:
|
|
1459
|
-
# Always restore terminal state
|
|
1460
|
-
_restore_terminal()
|