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.
- linux_autoruns/__init__.py +3 -0
- linux_autoruns/gui/__init__.py +0 -0
- linux_autoruns/gui/detail_dialog.py +123 -0
- linux_autoruns/gui/main_window.py +342 -0
- linux_autoruns/gui/models.py +117 -0
- linux_autoruns/gui/theme.py +228 -0
- linux_autoruns/gui/worker.py +43 -0
- linux_autoruns/main.py +77 -0
- linux_autoruns/models.py +41 -0
- linux_autoruns/scanner.py +110 -0
- linux_autoruns/scanners/__init__.py +35 -0
- linux_autoruns/scanners/cron.py +202 -0
- linux_autoruns/scanners/dbus.py +102 -0
- linux_autoruns/scanners/desktop_env.py +322 -0
- linux_autoruns/scanners/display_manager.py +135 -0
- linux_autoruns/scanners/grub.py +88 -0
- linux_autoruns/scanners/inetd.py +128 -0
- linux_autoruns/scanners/init.py +127 -0
- linux_autoruns/scanners/kernel.py +90 -0
- linux_autoruns/scanners/network_services.py +139 -0
- linux_autoruns/scanners/pam.py +66 -0
- linux_autoruns/scanners/shell.py +95 -0
- linux_autoruns/scanners/systemd.py +160 -0
- linux_autoruns/scanners/tmpfiles.py +69 -0
- linux_autoruns/scanners/udev.py +85 -0
- linux_autoruns/scanners/x11.py +77 -0
- linux_autoruns/scanners/xdg.py +96 -0
- linux_autoruns-2.9.1.dist-info/METADATA +85 -0
- linux_autoruns-2.9.1.dist-info/RECORD +32 -0
- linux_autoruns-2.9.1.dist-info/WHEEL +4 -0
- linux_autoruns-2.9.1.dist-info/entry_points.txt +2 -0
- linux_autoruns-2.9.1.dist-info/licenses/LICENSE +21 -0
|
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
|