siftcode 0.3.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.
- siftcode/__init__.py +3 -0
- siftcode/__main__.py +11 -0
- siftcode/_version.py +59 -0
- siftcode/cli.py +531 -0
- siftcode/core/__init__.py +1 -0
- siftcode/core/analyzer.py +575 -0
- siftcode/core/cache.py +300 -0
- siftcode/core/config.py +258 -0
- siftcode/core/llm.py +295 -0
- siftcode/core/manual.py +208 -0
- siftcode/core/prompts.py +215 -0
- siftcode/data/__init__.py +0 -0
- siftcode/data/siftcode-logo.png +0 -0
- siftcode/gui/__init__.py +9 -0
- siftcode/gui/analyzer_view.py +560 -0
- siftcode/gui/app.py +252 -0
- siftcode/gui/guide_window.py +88 -0
- siftcode/gui/project_list.py +160 -0
- siftcode/gui/settings_view.py +430 -0
- siftcode/gui/widgets/__init__.py +1 -0
- siftcode/gui/widgets/exclusion_list.py +169 -0
- siftcode/gui/widgets/log_view.py +108 -0
- siftcode/gui/widgets/markdown_view.py +205 -0
- siftcode/gui/widgets/project_row.py +141 -0
- siftcode/gui/window.py +446 -0
- siftcode-0.3.0.dist-info/METADATA +413 -0
- siftcode-0.3.0.dist-info/RECORD +31 -0
- siftcode-0.3.0.dist-info/WHEEL +5 -0
- siftcode-0.3.0.dist-info/entry_points.txt +5 -0
- siftcode-0.3.0.dist-info/licenses/LICENSE +674 -0
- siftcode-0.3.0.dist-info/top_level.txt +1 -0
siftcode/gui/app.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gtk.Application entry point for Siftcode.
|
|
3
|
+
|
|
4
|
+
Creates the application, loads CSS, and opens the main window.
|
|
5
|
+
All metadata comes from siftcode._version.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import platform
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import gi
|
|
16
|
+
|
|
17
|
+
gi.require_version("Gtk", "4.0")
|
|
18
|
+
gi.require_version("Adw", "1")
|
|
19
|
+
|
|
20
|
+
from gi.repository import Adw, Gdk, Gio, Gtk # noqa: E402
|
|
21
|
+
|
|
22
|
+
from siftcode._version import ( # noqa: E402
|
|
23
|
+
APP_ID,
|
|
24
|
+
APP_NAME,
|
|
25
|
+
AUTHOR_NAME,
|
|
26
|
+
COPYRIGHT,
|
|
27
|
+
DESCRIPTION,
|
|
28
|
+
HOMEPAGE_URL,
|
|
29
|
+
ISSUES_URL,
|
|
30
|
+
__status__,
|
|
31
|
+
__version__,
|
|
32
|
+
)
|
|
33
|
+
from siftcode.gui.window import MainWindow # noqa: E402
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Paths
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
|
44
|
+
LOGO_PATH = DATA_DIR / "siftcode-logo.png"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# CSS
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
CSS = """
|
|
52
|
+
.welcome-title {
|
|
53
|
+
font-size: 24pt;
|
|
54
|
+
font-weight: 300;
|
|
55
|
+
margin-bottom: 12px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.welcome-subtitle {
|
|
59
|
+
font-size: 12pt;
|
|
60
|
+
color: alpha(currentColor, 0.6);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.welcome-icon {
|
|
64
|
+
color: alpha(currentColor, 0.4);
|
|
65
|
+
margin-bottom: 24px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stat-value {
|
|
69
|
+
font-size: 20pt;
|
|
70
|
+
font-weight: bold;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.stat-label {
|
|
74
|
+
font-size: 10pt;
|
|
75
|
+
color: alpha(currentColor, 0.6);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Log view colors */
|
|
79
|
+
.log-info { color: #5e5c64; }
|
|
80
|
+
.log-success { color: #26a269; }
|
|
81
|
+
.log-warning { color: #e5a50a; }
|
|
82
|
+
.log-error { color: #c01c28; }
|
|
83
|
+
|
|
84
|
+
/* Logo in About dialog */
|
|
85
|
+
.about-logo {
|
|
86
|
+
border-radius: 16px;
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Application
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
class SiftcodeApplication(Adw.Application):
|
|
96
|
+
"""The main Gtk.Application for Siftcode."""
|
|
97
|
+
|
|
98
|
+
def __init__(self) -> None:
|
|
99
|
+
super().__init__(
|
|
100
|
+
application_id=APP_ID,
|
|
101
|
+
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
|
|
102
|
+
)
|
|
103
|
+
self.window: MainWindow | None = None
|
|
104
|
+
self._about_dialog: Adw.AboutDialog | None = None
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
# Application lifecycle
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def do_startup(self) -> None:
|
|
111
|
+
Adw.Application.do_startup(self)
|
|
112
|
+
logger.info(f"{APP_NAME} {__version__} ({__status__}) starting")
|
|
113
|
+
|
|
114
|
+
self._load_css()
|
|
115
|
+
|
|
116
|
+
# Actions
|
|
117
|
+
about_action = Gio.SimpleAction.new("about", None)
|
|
118
|
+
about_action.connect("activate", self._on_about)
|
|
119
|
+
self.add_action(about_action)
|
|
120
|
+
|
|
121
|
+
quit_action = Gio.SimpleAction.new("quit", None)
|
|
122
|
+
quit_action.connect("activate", lambda *_: self.quit())
|
|
123
|
+
self.add_action(quit_action)
|
|
124
|
+
|
|
125
|
+
# Keyboard shortcuts
|
|
126
|
+
self.set_accels_for_action("app.quit", ["<Ctrl>q"])
|
|
127
|
+
self.set_accels_for_action("app.about", ["F1"])
|
|
128
|
+
|
|
129
|
+
def do_activate(self) -> None:
|
|
130
|
+
win = self.props.active_window
|
|
131
|
+
if not win:
|
|
132
|
+
win = MainWindow(application=self)
|
|
133
|
+
win.present()
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# CSS
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _load_css(self) -> None:
|
|
140
|
+
provider = Gtk.CssProvider()
|
|
141
|
+
provider.load_from_string(CSS)
|
|
142
|
+
Gtk.StyleContext.add_provider_for_display(
|
|
143
|
+
Gdk.Display.get_default(),
|
|
144
|
+
provider,
|
|
145
|
+
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Logo lookup
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def _find_logo(self) -> Path | None:
|
|
153
|
+
"""Find the Siftcode logo PNG (packaged or source tree)."""
|
|
154
|
+
try:
|
|
155
|
+
import siftcode.data as data_pkg
|
|
156
|
+
packaged = Path(data_pkg.__file__).parent / "siftcode-logo.png"
|
|
157
|
+
if packaged.exists():
|
|
158
|
+
return packaged
|
|
159
|
+
except ImportError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
source = Path(__file__).parent.parent / "data" / "siftcode-logo.png"
|
|
163
|
+
if source.exists():
|
|
164
|
+
return source
|
|
165
|
+
|
|
166
|
+
legacy = Path(__file__).parent / "assets" / "siftcode-logo.png"
|
|
167
|
+
if legacy.exists():
|
|
168
|
+
return legacy
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _register_logo_icon_theme(self) -> None:
|
|
173
|
+
"""Register the logo's directory as an icon theme search path."""
|
|
174
|
+
logo_path = self._find_logo()
|
|
175
|
+
if logo_path is None:
|
|
176
|
+
return
|
|
177
|
+
try:
|
|
178
|
+
display = Gdk.Display.get_default()
|
|
179
|
+
theme = Gtk.IconTheme.get_for_display(display)
|
|
180
|
+
theme.add_search_path(str(logo_path.parent))
|
|
181
|
+
logger.debug(f"Added icon search path: {logo_path.parent}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning(f"Could not register icon theme path: {e}")
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Actions
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def _on_about(self, _action, _param) -> None:
|
|
190
|
+
# Avoid opening multiple About dialogs
|
|
191
|
+
if self._about_dialog is not None:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Register the logo directory so "siftcode-logo" is available
|
|
195
|
+
self._register_logo_icon_theme()
|
|
196
|
+
|
|
197
|
+
about = Adw.AboutDialog(
|
|
198
|
+
application_name=APP_NAME,
|
|
199
|
+
application_icon="siftcode-logo",
|
|
200
|
+
version=__version__,
|
|
201
|
+
developer_name=AUTHOR_NAME,
|
|
202
|
+
license_type=Gtk.License.GPL_3_0,
|
|
203
|
+
comments=DESCRIPTION,
|
|
204
|
+
website=HOMEPAGE_URL,
|
|
205
|
+
issue_url=ISSUES_URL,
|
|
206
|
+
copyright=COPYRIGHT,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# "Built with" acknowledgement section
|
|
210
|
+
about.add_acknowledgement_section(
|
|
211
|
+
"Built with",
|
|
212
|
+
[
|
|
213
|
+
"GTK4 https://gtk.org",
|
|
214
|
+
"libadwaita https://gnome.pages.gitlab.gnome.org/libadwaita/",
|
|
215
|
+
"Ollama https://ollama.com",
|
|
216
|
+
"Python https://python.org",
|
|
217
|
+
],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Runtime info section
|
|
221
|
+
about.add_legal_section(
|
|
222
|
+
title="Runtime",
|
|
223
|
+
copyright=f"Python {platform.python_version()}",
|
|
224
|
+
license_type=Gtk.License.UNKNOWN,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self._about_dialog = about
|
|
228
|
+
about.connect("closed", self._on_about_closed)
|
|
229
|
+
about.present(self.props.active_window)
|
|
230
|
+
|
|
231
|
+
def _on_about_closed(self, _dialog) -> None:
|
|
232
|
+
"""Called when the About dialog is closed."""
|
|
233
|
+
self._about_dialog = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Entry point for `siftcode-gui`
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def main(argv: list[str] | None = None) -> int:
|
|
241
|
+
"""Launch the GTK application."""
|
|
242
|
+
logging.basicConfig(
|
|
243
|
+
level=logging.INFO,
|
|
244
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
245
|
+
datefmt="%H:%M:%S",
|
|
246
|
+
)
|
|
247
|
+
app = SiftcodeApplication()
|
|
248
|
+
return app.run(argv if argv is not None else sys.argv)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
sys.exit(main())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GuideWindow — a standalone window that renders a Markdown guide
|
|
3
|
+
using the MarkdownView widget.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import gi
|
|
12
|
+
|
|
13
|
+
gi.require_version("Gtk", "4.0")
|
|
14
|
+
gi.require_version("Adw", "1")
|
|
15
|
+
|
|
16
|
+
from gi.repository import Adw, Gtk # noqa: E402
|
|
17
|
+
|
|
18
|
+
from siftcode.gui.widgets.markdown_view import MarkdownView # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GuideWindow(Adw.Window):
|
|
22
|
+
"""A window that displays a Markdown file with a preview."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, parent, title: str, guide_path: Path) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.set_transient_for(parent)
|
|
27
|
+
self.set_modal(False)
|
|
28
|
+
self.set_title(title)
|
|
29
|
+
self.set_default_size(900, 700)
|
|
30
|
+
|
|
31
|
+
self._guide_path = guide_path
|
|
32
|
+
|
|
33
|
+
toolbar = Adw.ToolbarView()
|
|
34
|
+
header = Adw.HeaderBar()
|
|
35
|
+
|
|
36
|
+
subtitle = guide_path.parent.name if guide_path.parent else ""
|
|
37
|
+
header.set_title_widget(
|
|
38
|
+
Adw.WindowTitle(title=title, subtitle=subtitle)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# "Open in external editor" button
|
|
42
|
+
open_btn = Gtk.Button.new_from_icon_name("document-open-symbolic")
|
|
43
|
+
open_btn.set_tooltip_text("Open in external editor")
|
|
44
|
+
open_btn.connect("clicked", self._on_open_external)
|
|
45
|
+
header.pack_end(open_btn)
|
|
46
|
+
|
|
47
|
+
toolbar.add_top_bar(header)
|
|
48
|
+
|
|
49
|
+
# Content
|
|
50
|
+
self._view = MarkdownView()
|
|
51
|
+
try:
|
|
52
|
+
self._view.load_file(guide_path)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
error_label = Gtk.Label(label=f"Could not load guide:\n{e}")
|
|
55
|
+
error_label.set_margin_top(24)
|
|
56
|
+
error_label.set_margin_start(24)
|
|
57
|
+
error_label.set_margin_end(24)
|
|
58
|
+
toolbar.set_content(error_label)
|
|
59
|
+
self.set_content(toolbar)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
toolbar.set_content(self._view)
|
|
63
|
+
self.set_content(toolbar)
|
|
64
|
+
|
|
65
|
+
# Close on Escape
|
|
66
|
+
key_controller = Gtk.EventControllerKey()
|
|
67
|
+
key_controller.connect("key-pressed", self._on_key_pressed)
|
|
68
|
+
self.add_controller(key_controller)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _on_key_pressed(self, _ctrl, keyval, _keycode, _state) -> bool:
|
|
73
|
+
# Escape closes the window
|
|
74
|
+
if keyval == 0xFF1B: # GDK_KEY_Escape
|
|
75
|
+
self.close()
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def _on_open_external(self, _button) -> None:
|
|
80
|
+
"""Open the guide in the system's default Markdown viewer."""
|
|
81
|
+
try:
|
|
82
|
+
subprocess.Popen(
|
|
83
|
+
["xdg-open", str(self._guide_path)],
|
|
84
|
+
stdout=subprocess.DEVNULL,
|
|
85
|
+
stderr=subprocess.DEVNULL,
|
|
86
|
+
)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project list view — shows all saved projects and lets the user add new ones.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import gi
|
|
11
|
+
|
|
12
|
+
gi.require_version("Gtk", "4.0")
|
|
13
|
+
gi.require_version("Adw", "1")
|
|
14
|
+
|
|
15
|
+
from gi.repository import Adw, Gio, Gtk # noqa: E402
|
|
16
|
+
|
|
17
|
+
from siftcode.core.config import (
|
|
18
|
+
ProjectConfig,
|
|
19
|
+
delete_project,
|
|
20
|
+
list_projects,
|
|
21
|
+
save_project,
|
|
22
|
+
)
|
|
23
|
+
from siftcode.gui.widgets.project_row import ProjectRow # noqa: E402
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProjectListView(Adw.Bin):
|
|
30
|
+
"""A scrollable list of saved projects, plus a button to add new ones."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._on_project_activated = None
|
|
35
|
+
self._on_project_deleted = None
|
|
36
|
+
|
|
37
|
+
# Root layout: an Adw.StatusPage when empty, or a ListBox when full
|
|
38
|
+
self._stack = Gtk.Stack()
|
|
39
|
+
self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
|
40
|
+
|
|
41
|
+
# Empty state
|
|
42
|
+
self._empty_page = Adw.StatusPage(
|
|
43
|
+
title="No projects yet",
|
|
44
|
+
description="Click the + button in the header to add a project.",
|
|
45
|
+
icon_name="folder-open-symbolic",
|
|
46
|
+
)
|
|
47
|
+
self._stack.add_named(self._empty_page, "empty")
|
|
48
|
+
|
|
49
|
+
# List view
|
|
50
|
+
self._list_box = Gtk.ListBox()
|
|
51
|
+
self._list_box.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
|
52
|
+
self._list_box.add_css_class("boxed-list")
|
|
53
|
+
self._list_box.set_margin_top(12)
|
|
54
|
+
self._list_box.set_margin_bottom(12)
|
|
55
|
+
self._list_box.set_margin_start(12)
|
|
56
|
+
self._list_box.set_margin_end(12)
|
|
57
|
+
self._list_box.connect("row-activated", self._on_row_activated)
|
|
58
|
+
|
|
59
|
+
scrolled = Gtk.ScrolledWindow()
|
|
60
|
+
scrolled.set_vexpand(True)
|
|
61
|
+
scrolled.set_child(self._list_box)
|
|
62
|
+
self._stack.add_named(scrolled, "list")
|
|
63
|
+
|
|
64
|
+
self.set_child(self._stack)
|
|
65
|
+
|
|
66
|
+
# Load projects on construction
|
|
67
|
+
self.refresh()
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Public API
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def set_on_project_activated(self, callback) -> None:
|
|
74
|
+
"""Set a callback invoked when a project row is activated.
|
|
75
|
+
|
|
76
|
+
The callback receives the ProjectConfig of the activated row.
|
|
77
|
+
"""
|
|
78
|
+
self._on_project_activated = callback
|
|
79
|
+
|
|
80
|
+
def set_on_project_deleted(self, callback) -> None:
|
|
81
|
+
"""Set a callback invoked after a project is deleted.
|
|
82
|
+
|
|
83
|
+
Receives the deleted ProjectConfig.
|
|
84
|
+
"""
|
|
85
|
+
self._on_project_deleted = callback
|
|
86
|
+
|
|
87
|
+
def refresh(self) -> None:
|
|
88
|
+
"""Reload the project list from disk."""
|
|
89
|
+
# Clear existing rows
|
|
90
|
+
while (row := self._list_box.get_row_at_index(0)) is not None:
|
|
91
|
+
self._list_box.remove(row)
|
|
92
|
+
|
|
93
|
+
projects = list_projects()
|
|
94
|
+
|
|
95
|
+
if not projects:
|
|
96
|
+
self._stack.set_visible_child_name("empty")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
for config in projects:
|
|
100
|
+
row = ProjectRow(config)
|
|
101
|
+
self._list_box.append(row)
|
|
102
|
+
|
|
103
|
+
self._stack.set_visible_child_name("list")
|
|
104
|
+
|
|
105
|
+
def activate_row(self, row) -> None:
|
|
106
|
+
"""Programmatically activate a row (same as double-click)."""
|
|
107
|
+
if self._on_project_activated:
|
|
108
|
+
config = getattr(row, "config", None)
|
|
109
|
+
if config:
|
|
110
|
+
self._on_project_activated(config)
|
|
111
|
+
|
|
112
|
+
def request_delete_row(self, row) -> None:
|
|
113
|
+
"""Ask the user to confirm, then delete the row's project."""
|
|
114
|
+
config = getattr(row, "config", None)
|
|
115
|
+
if config is None:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
dialog = Adw.MessageDialog.new(
|
|
119
|
+
self.get_root(),
|
|
120
|
+
"Delete project?",
|
|
121
|
+
f"Are you sure you want to delete '{config.name}'?\n\n"
|
|
122
|
+
f"This removes the saved configuration, not the project files.",
|
|
123
|
+
)
|
|
124
|
+
dialog.add_response("cancel", "Cancel")
|
|
125
|
+
dialog.add_response("delete", "Delete")
|
|
126
|
+
dialog.set_response_appearance(
|
|
127
|
+
"delete", Adw.ResponseAppearance.DESTRUCTIVE
|
|
128
|
+
)
|
|
129
|
+
dialog.set_default_response("cancel")
|
|
130
|
+
dialog.set_close_response("cancel")
|
|
131
|
+
|
|
132
|
+
dialog.connect("response", self._on_delete_response, config)
|
|
133
|
+
dialog.present()
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Signals
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _on_row_activated(self, _list_box, row) -> None:
|
|
140
|
+
config = getattr(row, "config", None)
|
|
141
|
+
if config and self._on_project_activated:
|
|
142
|
+
self._on_project_activated(config)
|
|
143
|
+
|
|
144
|
+
def _on_delete_response(self, _dialog, response_id: str, config) -> None:
|
|
145
|
+
if response_id != "delete":
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if delete_project(config.name):
|
|
149
|
+
logger.info(f"Deleted project: {config.name}")
|
|
150
|
+
self.refresh()
|
|
151
|
+
if self._on_project_deleted:
|
|
152
|
+
self._on_project_deleted(config)
|
|
153
|
+
else:
|
|
154
|
+
error = Adw.MessageDialog.new(
|
|
155
|
+
self.get_root(),
|
|
156
|
+
"Could not delete",
|
|
157
|
+
f"Project '{config.name}' was not found on disk.",
|
|
158
|
+
)
|
|
159
|
+
error.add_response("ok", "OK")
|
|
160
|
+
error.present()
|