linux-autoruns 2.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ def main():
2
+ from .main import main as launch
3
+ launch()
File without changes
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QFont
7
+ from PySide6.QtWidgets import (QApplication, QDialog, QGroupBox, QLabel,
8
+ QPlainTextEdit, QPushButton, QScrollArea,
9
+ QVBoxLayout, QWidget)
10
+
11
+ from ..models import AutostartEntry
12
+ from .theme import DARK_THEME_QSS
13
+
14
+
15
+ class DetailDialog(QDialog):
16
+ def __init__(self, entry: AutostartEntry, parent=None):
17
+ super().__init__(parent)
18
+ self.setWindowTitle(f"Entry Detail: {entry.name}")
19
+ self.setMinimumSize(550, 500)
20
+ self.setMaximumSize(700, 900)
21
+ self.setStyleSheet(DARK_THEME_QSS)
22
+ scroll = QScrollArea(self)
23
+ scroll.setWidgetResizable(True)
24
+ container = QWidget()
25
+ layout = QVBoxLayout(container)
26
+ layout.setSpacing(8)
27
+ layout.setContentsMargins(12, 12, 12, 12)
28
+ self._add_section(layout, "Genel Bilgiler", self._general_fields(entry))
29
+ if entry.command:
30
+ self._add_section(layout, "Komut", {"Command": entry.command})
31
+ if entry.exec_args:
32
+ self._add_section(layout, "Argümanlar", {"Args": " ".join(entry.exec_args)})
33
+ if entry.tags:
34
+ self._add_section(layout, "Etiketler", {"Tags": ", ".join(entry.tags)})
35
+ if entry.details:
36
+ detail_items = {}
37
+ for k, v in entry.details.items():
38
+ if isinstance(v, list):
39
+ detail_items[k] = ", ".join(str(i) for i in v)
40
+ else:
41
+ detail_items[k] = str(v)
42
+ self._add_section(layout, "Detaylar", detail_items)
43
+ if entry.file_path:
44
+ content = self._read_raw_content(entry.file_path)
45
+ if content:
46
+ group = QGroupBox("Ham İçerik")
47
+ vbox = QVBoxLayout()
48
+ text_edit = QPlainTextEdit()
49
+ text_edit.setPlainText(content)
50
+ text_edit.setReadOnly(True)
51
+ text_edit.setMaximumHeight(300)
52
+ mono = QFont("monospace")
53
+ mono.setStyleHint(QFont.Monospace)
54
+ text_edit.setFont(mono)
55
+ copy_btn = QPushButton("Kopyala")
56
+ copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(content))
57
+ btn_row = QVBoxLayout()
58
+ btn_row.addWidget(copy_btn)
59
+ btn_row.addStretch()
60
+ hbox = QVBoxLayout()
61
+ hbox.addWidget(text_edit)
62
+ hbox.addLayout(btn_row)
63
+ vbox.addLayout(hbox)
64
+ group.setLayout(vbox)
65
+ layout.addWidget(group)
66
+ layout.addStretch()
67
+ scroll.setWidget(container)
68
+ main_layout = QVBoxLayout(self)
69
+ main_layout.setContentsMargins(0, 0, 0, 0)
70
+ main_layout.addWidget(scroll)
71
+
72
+ def _general_fields(self, entry: AutostartEntry) -> dict:
73
+ fields = {}
74
+ fields["Kategori"] = entry.category
75
+ fields["Dosya"] = entry.file_path
76
+ fields["Ad"] = entry.name
77
+ fields["Durum"] = "+ Aktif" if entry.enabled else "- Pasif"
78
+ if entry.user:
79
+ fields["Kullanıcı"] = entry.user
80
+ fields["Scope"] = entry.scope
81
+ if entry.description:
82
+ fields["Tanım"] = entry.description
83
+ if entry.comment:
84
+ fields["Not"] = entry.comment
85
+ if entry.last_modified:
86
+ fields["Değişim"] = entry.last_modified
87
+ if entry.file_size is not None:
88
+ size = entry.file_size
89
+ if size > 1024:
90
+ fields["Boyut"] = f"{size / 1024:.1f} KB"
91
+ else:
92
+ fields["Boyut"] = f"{size} B"
93
+ if entry.file_permissions:
94
+ fields["İzinler"] = entry.file_permissions
95
+ if entry.owner:
96
+ fields["Sahip"] = entry.owner
97
+ return fields
98
+
99
+ def _add_section(self, layout: QVBoxLayout, title: str, items: dict):
100
+ group = QGroupBox(title)
101
+ vbox = QVBoxLayout()
102
+ for key, value in items.items():
103
+ lbl_key = QLabel(f"{key}:")
104
+ lbl_val = QLabel(str(value))
105
+ lbl_val.setWordWrap(True)
106
+ lbl_val.setTextInteractionFlags(Qt.TextSelectableByMouse)
107
+ btn = QPushButton("Kopyala")
108
+ btn.clicked.connect(lambda checked, v=str(value): QApplication.clipboard().setText(v))
109
+ top_row = QVBoxLayout()
110
+ top_row.addWidget(lbl_key)
111
+ top_row.addWidget(btn)
112
+ h = QVBoxLayout()
113
+ h.addLayout(top_row)
114
+ h.addWidget(lbl_val)
115
+ vbox.addLayout(h)
116
+ group.setLayout(vbox)
117
+ layout.addWidget(group)
118
+
119
+ def _read_raw_content(self, path: str) -> str | None:
120
+ try:
121
+ return Path(path).read_text(encoding="utf-8", errors="replace")
122
+ except (OSError, PermissionError):
123
+ return None
@@ -0,0 +1,342 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import io
5
+ import json
6
+ import os
7
+ import subprocess
8
+ from datetime import datetime, timezone
9
+
10
+ from PySide6.QtCore import QSettings, Qt, QTimer
11
+ from PySide6.QtGui import QColor, QKeySequence, QShortcut
12
+ from PySide6.QtWidgets import (QApplication, QCheckBox, QFileDialog,
13
+ QHBoxLayout, QHeaderView, QLabel, QLineEdit,
14
+ QMainWindow, QMenu, QMessageBox, QProgressBar,
15
+ QPushButton, QSplitter, QStatusBar, QTableView,
16
+ QTreeWidget, QTreeWidgetItem, QVBoxLayout,
17
+ QWidget)
18
+
19
+ from ..models import AutostartEntry
20
+ from ..scanners import SCANNERS
21
+ from .detail_dialog import DetailDialog
22
+ from .models import EntryFilterProxy, EntryTableModel
23
+ from .theme import CATPPUCCIN_MOCHA, DARK_THEME_QSS
24
+ from .worker import ScanWorker
25
+
26
+
27
+ class MainWindow(QMainWindow):
28
+ def __init__(self):
29
+ super().__init__()
30
+ self._settings = QSettings("linux-autoruns", "linux-autoruns")
31
+ self.setWindowTitle("Linux Autoruns")
32
+ self.setMinimumSize(1000, 600)
33
+ self._restore_geometry()
34
+ self._all_entries: list[AutostartEntry] = []
35
+ self._worker: ScanWorker | None = None
36
+ self._edit_mode = False
37
+ self._selected_category: str | None = None
38
+ self._error_count = 0
39
+ self._model = EntryTableModel(self)
40
+ self._proxy = EntryFilterProxy(self)
41
+ self._proxy.setSourceModel(self._model)
42
+ self._search_debounce = QTimer()
43
+ self._search_debounce.setSingleShot(True)
44
+ self._search_debounce.setInterval(150)
45
+ self._search_debounce.timeout.connect(self._apply_filters)
46
+ self._setup_ui()
47
+ self._apply_theme()
48
+ self._start_scan()
49
+
50
+ def _restore_geometry(self):
51
+ geom = self._settings.value("geometry")
52
+ if geom:
53
+ self.restoreGeometry(geom)
54
+ else:
55
+ self.resize(1200, 700)
56
+
57
+ def closeEvent(self, event):
58
+ self._settings.setValue("geometry", self.saveGeometry())
59
+ super().closeEvent(event)
60
+
61
+ def _setup_ui(self):
62
+ central = QWidget()
63
+ self.setCentralWidget(central)
64
+ main_layout = QVBoxLayout(central)
65
+ main_layout.setContentsMargins(0, 0, 0, 0)
66
+ main_layout.setSpacing(0)
67
+ toolbar = self._create_toolbar()
68
+ main_layout.addLayout(toolbar)
69
+ self._progress_bar = QProgressBar()
70
+ self._progress_bar.setMaximumHeight(4)
71
+ self._progress_bar.setTextVisible(False)
72
+ self._progress_bar.setValue(0)
73
+ main_layout.addWidget(self._progress_bar)
74
+ splitter = QSplitter(Qt.Horizontal)
75
+ self._tree = QTreeWidget()
76
+ self._tree.setHeaderLabel("Kategoriler")
77
+ self._tree.setMinimumWidth(200)
78
+ self._tree.setMaximumWidth(350)
79
+ self._tree.setSortingEnabled(True)
80
+ self._tree.sortByColumn(0, Qt.AscendingOrder)
81
+ self._tree.itemClicked.connect(self._on_category_clicked)
82
+ splitter.addWidget(self._tree)
83
+ self._table = QTableView()
84
+ self._table.setModel(self._proxy)
85
+ self._table.setSelectionBehavior(QTableView.SelectRows)
86
+ self._table.setSelectionMode(QTableView.SingleSelection)
87
+ self._table.setAlternatingRowColors(True)
88
+ self._table.setSortingEnabled(True)
89
+ self._table.verticalHeader().setVisible(False)
90
+ self._table.setContextMenuPolicy(Qt.CustomContextMenu)
91
+ self._table.customContextMenuRequested.connect(self._on_context_menu)
92
+ self._table.doubleClicked.connect(self._on_inspect)
93
+ header = self._table.horizontalHeader()
94
+ header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
95
+ header.setSectionResizeMode(1, QHeaderView.Stretch)
96
+ header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
97
+ header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
98
+ header.setSectionResizeMode(4, QHeaderView.Stretch)
99
+ header.setSectionResizeMode(5, QHeaderView.ResizeToContents)
100
+ splitter.addWidget(self._table)
101
+ splitter.setStretchFactor(0, 0)
102
+ splitter.setStretchFactor(1, 1)
103
+ main_layout.addWidget(splitter)
104
+ self._status_bar = QStatusBar()
105
+ self.setStatusBar(self._status_bar)
106
+ self._status_bar.showMessage("Tarama başlatılmadı")
107
+ quit_shortcut = QShortcut(QKeySequence("Ctrl+C"), self)
108
+ quit_shortcut.activated.connect(self.close)
109
+
110
+ def _create_toolbar(self) -> QHBoxLayout:
111
+ toolbar = QHBoxLayout()
112
+ toolbar.setContentsMargins(8, 8, 8, 8)
113
+ toolbar.setSpacing(8)
114
+ lbl = QLabel("🔍")
115
+ lbl.setStyleSheet(f"color: {CATPPUCCIN_MOCHA['blue']}; font-size: 16px;")
116
+ toolbar.addWidget(lbl)
117
+ self._search = QLineEdit()
118
+ self._search.setPlaceholderText("Ara... (isim, komut, dosya)")
119
+ self._search.setFixedWidth(250)
120
+ self._search.textChanged.connect(self._on_search)
121
+ toolbar.addWidget(self._search)
122
+ self._enabled_only = QCheckBox("Sadece aktif")
123
+ self._enabled_only.stateChanged.connect(self._on_filter_changed)
124
+ toolbar.addWidget(self._enabled_only)
125
+ self._edit_mode_cb = QCheckBox("Düzenleme Modu")
126
+ self._edit_mode_cb.stateChanged.connect(self._on_edit_mode_changed)
127
+ toolbar.addWidget(self._edit_mode_cb)
128
+ toolbar.addStretch()
129
+ self._scan_btn = QPushButton("Scan")
130
+ self._scan_btn.clicked.connect(self._start_scan)
131
+ toolbar.addWidget(self._scan_btn)
132
+ self._export_json_btn = QPushButton("Export JSON")
133
+ self._export_json_btn.clicked.connect(self._export_json)
134
+ self._export_json_btn.setEnabled(False)
135
+ toolbar.addWidget(self._export_json_btn)
136
+ self._export_csv_btn = QPushButton("Export CSV")
137
+ self._export_csv_btn.clicked.connect(self._export_csv)
138
+ self._export_csv_btn.setEnabled(False)
139
+ toolbar.addWidget(self._export_csv_btn)
140
+ return toolbar
141
+
142
+ def _apply_theme(self):
143
+ self.setStyleSheet(DARK_THEME_QSS)
144
+
145
+ def _on_edit_mode_changed(self, state):
146
+ self._edit_mode = state == Qt.Checked.value
147
+
148
+ def _start_scan(self):
149
+ if self._worker and self._worker.isRunning():
150
+ self._worker.cancel()
151
+ self._scan_btn.setText("Scan")
152
+ self._progress_bar.setValue(0)
153
+ return
154
+ self._all_entries.clear()
155
+ self._selected_category = None
156
+ self._error_count = 0
157
+ self._model.set_entries([])
158
+ self._tree.clear()
159
+ self._scan_btn.setText("Durdur")
160
+ self._export_json_btn.setEnabled(False)
161
+ self._export_csv_btn.setEnabled(False)
162
+ self._progress_bar.setValue(0)
163
+ self._worker = ScanWorker(SCANNERS, self)
164
+ self._worker.progress.connect(self._on_progress)
165
+ self._worker.entry_found.connect(self._on_entry_found)
166
+ self._worker.scan_complete.connect(self._on_scan_complete)
167
+ self._worker.error.connect(self._on_error)
168
+ self._worker.start()
169
+
170
+ def _on_progress(self, category: str, percent: int):
171
+ self._progress_bar.setValue(percent)
172
+ self._status_bar.showMessage(f"Taranıyor: {category} ({percent}%)")
173
+
174
+ def _on_entry_found(self, entry: AutostartEntry):
175
+ self._all_entries.append(entry)
176
+ self._model.add_entry(entry)
177
+
178
+ def _on_scan_complete(self, entries: list):
179
+ self._all_entries = entries
180
+ self._model.set_entries(entries)
181
+ self._scan_btn.setText("Scan")
182
+ self._export_json_btn.setEnabled(True)
183
+ self._export_csv_btn.setEnabled(True)
184
+ self._progress_bar.setValue(100)
185
+ total = len(entries)
186
+ enabled = sum(1 for e in entries if e.enabled)
187
+ cats = len(set(e.category for e in entries))
188
+ msg = f"Tamamlandı: {total} entry ({enabled} aktif) - {cats} kategori"
189
+ if self._error_count:
190
+ msg += f" ({self._error_count} hata)"
191
+ self._status_bar.showMessage(msg)
192
+ has_user = any(e.user for e in entries)
193
+ self._table.setColumnHidden(3, not has_user)
194
+ self._rebuild_tree()
195
+
196
+ def _on_error(self, msg: str):
197
+ self._error_count += 1
198
+ self._status_bar.showMessage(f"Hata ({self._error_count}): {msg[:80]}")
199
+
200
+ def _rebuild_tree(self):
201
+ self._tree.clear()
202
+ root = QTreeWidgetItem(self._tree)
203
+ root.setText(0, f"Tümü ({len(self._all_entries)})")
204
+ root.setData(0, Qt.UserRole, len(self._all_entries))
205
+ counts: dict[str, int] = {}
206
+ for entry in self._all_entries:
207
+ counts[entry.category] = counts.get(entry.category, 0) + 1
208
+ for cat in sorted(counts):
209
+ item = QTreeWidgetItem(self._tree)
210
+ item.setText(0, f"{cat} ({counts[cat]})")
211
+ item.setData(0, Qt.UserRole, counts[cat])
212
+
213
+ def _on_category_clicked(self, item: QTreeWidgetItem, column: int):
214
+ text = item.text(0)
215
+ if text.startswith("Tümü"):
216
+ self._selected_category = None
217
+ else:
218
+ category = text.rsplit(" (", 1)[0] if " (" in text else text
219
+ if self._selected_category == category:
220
+ self._selected_category = None
221
+ else:
222
+ self._selected_category = category
223
+ self._apply_filters()
224
+
225
+ def _apply_filters(self):
226
+ self._proxy.set_category_filter(self._selected_category)
227
+ self._proxy.set_enabled_only(self._enabled_only.isChecked())
228
+ self._proxy.set_search_text(self._search.text())
229
+ has_user = any(e.user for e in self._all_entries)
230
+ self._table.setColumnHidden(3, not has_user)
231
+
232
+ def _on_search(self, text: str):
233
+ self._search_debounce.start()
234
+
235
+ def _on_filter_changed(self):
236
+ self._apply_filters()
237
+
238
+ def _on_context_menu(self, pos):
239
+ index = self._table.indexAt(pos)
240
+ if not index.isValid():
241
+ return
242
+ source_index = self._proxy.mapToSource(index)
243
+ entry: AutostartEntry | None = self._model.entry_at(source_index.row())
244
+ if not entry:
245
+ return
246
+ menu = QMenu(self)
247
+ copy_name = menu.addAction("Copy Name")
248
+ copy_path = menu.addAction("Copy Path")
249
+ copy_command = menu.addAction("Copy Command")
250
+ copy_json = menu.addAction("Copy Full Details (JSON)")
251
+ menu.addSeparator()
252
+ inspect_action = menu.addAction("Inspect Details...")
253
+ menu.addSeparator()
254
+ open_location = menu.addAction("Open File Location")
255
+ if self._edit_mode:
256
+ menu.addSeparator()
257
+ toggle_action = menu.addAction(
258
+ "Disable" if entry.enabled else "Enable"
259
+ )
260
+ else:
261
+ toggle_action = None
262
+ action = menu.exec(self._table.mapToGlobal(pos))
263
+ if action == copy_name:
264
+ QApplication.clipboard().setText(entry.name)
265
+ elif action == copy_path:
266
+ QApplication.clipboard().setText(entry.file_path)
267
+ elif action == copy_command:
268
+ QApplication.clipboard().setText(entry.command or "")
269
+ elif action == copy_json:
270
+ QApplication.clipboard().setText(json.dumps(entry.to_dict(), indent=2, ensure_ascii=False))
271
+ elif action == inspect_action:
272
+ self._open_detail(entry)
273
+ elif action == open_location:
274
+ dirpath = os.path.dirname(entry.file_path)
275
+ if os.path.isdir(dirpath):
276
+ subprocess.Popen(["xdg-open", dirpath])
277
+ elif toggle_action and action == toggle_action:
278
+ entry.enabled = not entry.enabled
279
+ self._model.set_entries(list(self._model.get_all_entries()))
280
+
281
+ def _on_inspect(self, index):
282
+ source_index = self._proxy.mapToSource(index)
283
+ entry: AutostartEntry | None = self._model.entry_at(source_index.row())
284
+ if entry:
285
+ self._open_detail(entry)
286
+
287
+ def _open_detail(self, entry: AutostartEntry):
288
+ dialog = DetailDialog(entry, self)
289
+ dialog.exec()
290
+
291
+ def _export_json(self):
292
+ path, _ = QFileDialog.getSaveFileName(
293
+ self, "JSON olarak kaydet", "autoruns_export.json", "JSON (*.json)"
294
+ )
295
+ if not path:
296
+ return
297
+ data = {
298
+ "scan_time": datetime.now(tz=timezone.utc).isoformat(),
299
+ "hostname": os.uname().nodename,
300
+ "entries": [e.to_dict() for e in self._all_entries],
301
+ "summary": {
302
+ "total": len(self._all_entries),
303
+ "enabled": sum(1 for e in self._all_entries if e.enabled),
304
+ "by_category": {},
305
+ },
306
+ }
307
+ for entry in self._all_entries:
308
+ cat = entry.category
309
+ data["summary"]["by_category"][cat] = data["summary"]["by_category"].get(cat, 0) + 1
310
+ try:
311
+ with open(path, "w", encoding="utf-8") as f:
312
+ json.dump(data, f, indent=2, ensure_ascii=False)
313
+ self._status_bar.showMessage(f"Dışa aktarıldı: {path}")
314
+ except OSError as e:
315
+ QMessageBox.critical(self, "Hata", f"JSON kaydedilemedi: {e}")
316
+
317
+ def _export_csv(self):
318
+ path, _ = QFileDialog.getSaveFileName(
319
+ self, "CSV olarak kaydet", "autoruns_export.csv", "CSV (*.csv)"
320
+ )
321
+ if not path:
322
+ return
323
+ try:
324
+ with open(path, "w", encoding="utf-8", newline="") as f:
325
+ writer = csv.writer(f)
326
+ writer.writerow(["Enabled", "Name", "Category", "User", "Description",
327
+ "Command", "Path", "Modified", "Tags"])
328
+ for entry in self._all_entries:
329
+ writer.writerow([
330
+ "+" if entry.enabled else "-",
331
+ entry.name,
332
+ entry.category,
333
+ entry.user or "",
334
+ entry.description or "",
335
+ entry.command or "",
336
+ entry.file_path,
337
+ entry.last_modified or "",
338
+ ",".join(entry.tags),
339
+ ])
340
+ self._status_bar.showMessage(f"Dışa aktarıldı: {path}")
341
+ except OSError as e:
342
+ QMessageBox.critical(self, "Hata", f"CSV kaydedilemedi: {e}")
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtCore import (QAbstractTableModel, QModelIndex,
4
+ QRegularExpression, QSortFilterProxyModel, Qt)
5
+ from PySide6.QtGui import QColor
6
+
7
+ from ..models import AutostartEntry
8
+ from .theme import CATPPUCCIN_MOCHA
9
+
10
+
11
+ class EntryTableModel(QAbstractTableModel):
12
+ HEADERS = ["+", "Name", "Category", "User", "Description", "Modified"]
13
+ _FIELD_MAP = {
14
+ 1: "name",
15
+ 2: "category",
16
+ 3: "user",
17
+ 4: "description",
18
+ 5: "last_modified",
19
+ }
20
+
21
+ def __init__(self, parent=None):
22
+ super().__init__(parent)
23
+ self._entries: list[AutostartEntry] = []
24
+
25
+ def rowCount(self, parent=QModelIndex()) -> int:
26
+ return len(self._entries)
27
+
28
+ def columnCount(self, parent=QModelIndex()) -> int:
29
+ return len(self.HEADERS)
30
+
31
+ def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
32
+ if not index.isValid():
33
+ return None
34
+ entry = self._entries[index.row()]
35
+ col = index.column()
36
+ if role == Qt.DisplayRole:
37
+ if col == 0:
38
+ return "+" if entry.enabled else "-"
39
+ field = self._FIELD_MAP.get(col)
40
+ if field:
41
+ val = getattr(entry, field, None)
42
+ return str(val) if val is not None else ""
43
+ return ""
44
+ if role == Qt.TextAlignmentRole:
45
+ if col == 0:
46
+ return int(Qt.AlignCenter)
47
+ return int(Qt.AlignLeft | Qt.AlignVCenter)
48
+ if role == Qt.ForegroundRole:
49
+ if col == 0:
50
+ color = CATPPUCCIN_MOCHA["green"] if entry.enabled else CATPPUCCIN_MOCHA["red"]
51
+ return QColor(color)
52
+ if col == 1:
53
+ return QColor(CATPPUCCIN_MOCHA["blue"])
54
+ if role == Qt.UserRole:
55
+ return entry
56
+ return None
57
+
58
+ def headerData(self, section: int, orientation, role: int = Qt.DisplayRole):
59
+ if role == Qt.DisplayRole and orientation == Qt.Horizontal:
60
+ return self.HEADERS[section]
61
+ return None
62
+
63
+ def set_entries(self, entries: list[AutostartEntry]):
64
+ self.beginResetModel()
65
+ self._entries = entries
66
+ self.endResetModel()
67
+
68
+ def add_entry(self, entry: AutostartEntry):
69
+ row = len(self._entries)
70
+ self.beginInsertRows(QModelIndex(), row, row)
71
+ self._entries.append(entry)
72
+ self.endInsertRows()
73
+
74
+ def entry_at(self, row: int) -> AutostartEntry | None:
75
+ if 0 <= row < len(self._entries):
76
+ return self._entries[row]
77
+ return None
78
+
79
+ def get_all_entries(self) -> list[AutostartEntry]:
80
+ return list(self._entries)
81
+
82
+
83
+ class EntryFilterProxy(QSortFilterProxyModel):
84
+ def __init__(self, parent=None):
85
+ super().__init__(parent)
86
+ self._filter_category: str | None = None
87
+ self._filter_enabled_only: bool = False
88
+
89
+ def set_category_filter(self, category: str | None):
90
+ self._filter_category = category
91
+ self.invalidateFilter()
92
+
93
+ def set_enabled_only(self, enabled: bool):
94
+ self._filter_enabled_only = enabled
95
+ self.invalidateFilter()
96
+
97
+ def set_search_text(self, text: str):
98
+ self.setFilterRegularExpression(QRegularExpression(text, QRegularExpression.CaseInsensitiveOption))
99
+
100
+ def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
101
+ model = self.sourceModel()
102
+ if not model:
103
+ return True
104
+ index = model.index(source_row, 0, source_parent)
105
+ entry: AutostartEntry | None = model.data(index, Qt.UserRole)
106
+ if not entry:
107
+ return True
108
+ if self._filter_category and entry.category != self._filter_category:
109
+ return False
110
+ if self._filter_enabled_only and not entry.enabled:
111
+ return False
112
+ pattern = self.filterRegularExpression().pattern()
113
+ if pattern:
114
+ searchable = f"{entry.name} {entry.description or ''} {entry.command or ''} {entry.file_path}".lower()
115
+ if pattern.lower() not in searchable:
116
+ return False
117
+ return True