Diskteki dosya değişikliği takibi

Windowsta yeni bir program kurduğumuzda kurmadan önce dosya adlarını taratıp

kurduktan sonra karşılaştırma yapabildiğimiz program vardı adını bulamadığımdan yazamadım.

Linux ve debian için benzer program var mı biliyor musunuz?

Hocam bahsettiğiniz türden bir yazılımı ilk defa duydum ve açıkçası tam anlamadım. YZ’ye Murat Taşkıran böyle soruyor ( :crazy_face:) nedir bu anladın mı? dedim, şunları sıraladı:

  • InstallWach Pro
  • Total Uninstall
  • Revo Uninstaller (her zaman bulundururum)
  • Regshot

Böyle birşey mi lazım linux’da size? Nereye ne kuruyor, kurduktan sonra sistemde ne değişiklik oluyor, öncesi-sonrası raporu yapacak bir program… Böyle mi?

Anlamadıysam tarif edin lütfen. Belki biraz çabayla bir alternatif üretebiliriz, kim bilir?..

Adımı söylemesen iyiymiş

Şaka bi yana doğru anlamış

ingilizce olarak bakıyorum ben de

yapmam gereken bi taşıma işlemi var hangi dosyaların oluştuğunu ve değiştiğini görüp kopyalayıp bi deneme yapayım diyorum

kapsamlı kaldırma için iyi oluyor winde bazı atık dosya bırakıyor programlar etiketlemek için

deneme versiyonlarını hep denemek için de kullanılabilir

Hocam sorularımı arttırdım ve sohbetin devamı şurada:

(https://gemini.google.com/share/223bcd4830f7)

İleride (muhtemelen yarın) bu kaynağı google hesabımdan silerim, kırık link oluşursa forumun SEO’suna zarar vermesin diye kod etiketi ile veriyorum (kırılırsa google farkedemez).

Ama google hesabınız varsa bu linki alıp sohbete devam et derseniz, sanki siz başlatmışsınız gibi sizin YZ sohbet geçmişinize geçer ve oradan devam edebilirsiniz.

Google hesabınız yoksa, kayda geçmeden sohbeti sürdürebilirs… öyle oluyor muydu?.. Onu tam bilmiyorum.

Yani bu linkte daha fazla programdan bahsetti bilginiz olsun.

Revo uninstaller’in portable versiyonu full gibi çalışıyor ve işini iyi yapıyor bilginiz olsun. Bereket versin linux tarafında çok fazla çöp oluşmuyor ve Pardus Paket Kurucu, kaldırma sonrasında tüm klasör ve dosyaları siliyor (denendi). Üstelik postuninst betiği oluşturmasam bile…

1 Beğeni

Gemini Snapper var dedi depodan kurdum gui olmadan arayüz açılmadı onu da kurdum deneyeyim bakayım

Timeshift de de varmış karşılaştırma o kadar kullandım görmedim

Snapper-gui nin bağımlılıkları varmış sayfasındaki ubuntu için olan bağımlılıkları da kurdum (synaptic kurmuyor) sonra snapper i kurdum hata vermedi ama istediğim gibi değil

timeshift te de karşılaştırma yok

sorunu gemini ye anlattım çözüm yollarını yazdı not aldım tahmin ettiğim dosyalar çıktı sistemi taşımam gerekirse işe yarar gibi.

Olası tüm sistem dizinlerinin içindekilerin listesini yapıp isteğe bağlı olarak bir önceki ile farklılıkları gösteren basit bir betik bu işi görmez mi?

Konuyu en başından ele almak istiyorum. Çünkü şu cümleyle aklıma birşey getirdiniz:

Yeni bir sistem kurarsam, şu anda kurulu programları tek komutla nasıl bir seferde kurabilirim gibi birşey sormuştum. Bana dedi ki aha şu komutu gir, çıktı dosyası oluşacak, yeni kurulum yaptığında da şu komutla o çıktı txt dosyasını göstert, tüm programların tek seferde kurulacak. Komutu verdim, şöyle bir çıktı dosyası oluştu:

accountsservice					install
acl						install
adduser						install
adwaita-icon-theme				install
alsa-topology-conf				install
alsa-ucm-conf					install
alsa-utils					install
amd64-microcode					install
ant						install
ant-optional					install
anydesk						install
apparmor					install
apt						install
apt-file					install
apt-listchanges					install
apt-utils					install
aspell						install
aspell-en					install
at-spi2-common					install
at-spi2-core					install
atmel-firmware					install
audacious					install
audacious-plugins:amd64				install
audacious-plugins-data				install
audacity					install
audacity-data					install
avahi-daemon					install
avahi-utils					install
b43-fwcutter					install
baobab						install
base-files					install
base-passwd					install
bash						install
bash-completion					install
bc						install
bind9-dnsutils					install
bind9-host					install
bind9-libs:amd64				install
binutils					install
binutils-common:amd64				install
binutils-x86-64-linux-gnu			install
blackbird-gtk-theme				install
blt						install
bluebird-gtk-theme				install
blueman						install
bluez						install
bluez-firmware					install
bluez-obexd					install
bogofilter					deinstall
bogofilter-bdb					deinstall
bogofilter-common				deinstall
brasero-common					install
breeze						install
breeze-cursor-theme				install
breeze-gtk-theme				install
breeze-icon-theme				install
bsdextrautils					install
bsdutils					install
Devamı var ama sığmadı...

Devamı var ama sığmadı…

İkinci komutla da bu liste otomatik kurulacakmış öyle demişti. O konuşmadan sonra yüzlerce sohbet başlığı oluştuğundan şimdi nereye gitti o sohbet bilmiyorum.

Amacınız kurulum sonrası eski düzene dönmekse bu işe yarayabilir.

Yok, sadece yazılımların ne yaptığını incelemek istiyorum ise,

En mantıklı çözüm.

Yine de biraz uğraşarak bir install.deb yapılabilir.

Şu anda hala düşünüyorum… :thinking:

  • PyQt5 tabanlı bir GUI çatısı oluştururum.
  • Bu çatı iki panellidir: sol ve sağ panel.
  • Sol panel öncesi, sağ panel sonrası.
  • Kullanıcı “şu anın snapshot’unu al” butonuna basar.
  • Sol panel dolar. Program metin tabanlı bir tür sistem durumu snapshotu alır; tüm klasör-dosya ağacını rapor olarak bir json dosyasına kaydeder.
  • Daha sonra kullanıcı bir program kurar.
  • Ardından “Karşılaştır (Compare)” düğmesine basar.
  • Program, önceki json dosyasını bulur ve şu anın durumu ile karşılaştırıp, sadece farklılıkları koyulaştırarak sana gösterir.

Hangi yoldaki hangi dizine ne olmuş? Hangi dizinler oluşmuş? Hangi config dosyasının boyutu artmış? Rahatça görürsün.

Basit aslında. 1-2 gün uğraşmayla hallederim gibi geliyor ama elimde 2-3 proje var ve buraya ara vermek için giriyorum. Söz veremem.

1 Beğeni

Yine ben :grimacing:

Şöyle birşey yaptım @Murat_Taskiran :

Sağda gördüğün YENİ, SİLİNEN ve DEĞİŞEN işaretlilerden silinenleri ve yenileri google chrome yapıyor. Değişenler de, yine google chrome ve ferdium tarafından oluşturuluyor. Hiçbirşey yapmasam bile birkaç saniyede bir değişiklikler oluyor, yani bunlar açıkken ha bire birşey yazıp siliyorlar.

Bu program ne yapıyor: Kendisine belirtilmiş sistem dizinlerini tarayarak değişiklikleri kaydediyor. Yeni klasör var mı? Yeni dosya var mı? Var olan dosyalarda boyut değişikliği olmuş mu? Silinen var mı?

Soldaki panelde taranan dizinler var. Bunları koda müdahale ederek arttırabilirsin.

İlk tarama işleminde “/home/kullanıcı_adı/” dizininde compare.json dosyası oluşur. Böylece program kapatılıp açıldığında son tarama durumunu gösterir ve 2. butonla karşılaştırma yapabilirsin.

  1. buton ile de durumu rapor halinde txt dosyasına yazdırabiliyorsun.

The source code:

import sys
import os
import json
import hashlib
from pathlib import Path
from datetime import datetime
import time # time modülü mtime'ı işlemek için gerekli

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
    QPushButton, QTextEdit, QTreeWidget, QTreeWidgetItem, QSplitter, 
    QLabel, QSizePolicy, QMessageBox, QHeaderView, QProgressBar, QFileDialog
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal

# --- 1. Sabitler ve Yardımcı Fonksiyonlar ---

# Kontrol edilecek yolların listesi
DIRS_TO_CHECK = [
    "/etc", 
    "/usr/bin", 
    "/usr/share", 
    "/usr/local/bin", 
    "~/.config", 
    "~/.local", 
    "/opt"
]

# Snapshot dosyasının kaydedileceği yol: /home/kullanıcı_adı/compare.json
SNAPSHOT_FILE = str(Path.home() / "compare.json")

# Belirtilen config uzantıları
CONFIG_EXTENSIONS = ['.conf', '.ini', '.json', '.yaml', '.xml', '.cfg']

def get_file_hash(filepath, block_size=65536):
    """Verilen dosyanın SHA256 özetini (hash) hesaplar."""
    try:
        hasher = hashlib.sha256()
        with open(filepath, 'rb') as f:
            while True:
                buf = f.read(block_size)
                if not buf:
                    break
                hasher.update(buf)
        return hasher.hexdigest()
    except Exception:
        return None 

def take_snapshot(root_dirs):
    """
    Belirtilen kök dizinleri özyinelemeli olarak tarar ve meta verileri toplar.
    """
    snapshot_data = {}
    for root_dir_str in root_dirs:
        root_dir = Path(root_dir_str).expanduser() 

        if not root_dir.exists():
            continue

        for entry in root_dir.rglob('*'):
            try:
                if entry.is_symlink():
                    continue

                stats = entry.stat()
                data = {
                    "type": "dir" if entry.is_dir() else "file",
                    "size": stats.st_size,
                    "mtime": stats.st_mtime,
                }
                
                if entry.is_file() and any(entry.name.endswith(ext) for ext in CONFIG_EXTENSIONS):
                    data["hash"] = get_file_hash(entry)

                snapshot_data[str(entry)] = data
            except Exception:
                continue
                
    return snapshot_data

def compare_snapshots(snap1, snap2):
    """İki snapshot verisini karşılaştırır ve farkları döndürür."""
    differences = {
        "added": {},
        "deleted": {},
        "modified": {}
    }

    # 1. Silinenleri ve Değiştirilenleri bul (snap1'i temel alarak)
    for path, data1 in snap1.items():
        if path not in snap2:
            differences["deleted"][path] = data1
        else:
            data2 = snap2[path]
            is_modified = False
            
            if data1["type"] == "file" and data2["type"] == "file":
                if data1["size"] != data2["size"]:
                    is_modified = True
                elif "hash" in data1 and data1.get("hash") != data2.get("hash"):
                    is_modified = True
            
            if is_modified:
                differences["modified"][path] = {"before": data1, "after": data2}

    # 2. Eklenenleri bul (snap2'yi temel alarak)
    for path, data2 in snap2.items():
        if path not in snap1:
            differences["added"][path] = data2
            
    return differences

# --- 2. Arka Plan Çalışanı ---

class WorkerThread(QThread):
    snapshot_finished = pyqtSignal(dict)
    error_occurred = pyqtSignal(str)

    def __init__(self, dirs_to_scan):
        super().__init__()
        self.dirs_to_scan = dirs_to_scan

    def run(self):
        try:
            snapshot = take_snapshot(self.dirs_to_scan)
            self.snapshot_finished.emit(snapshot)
        except Exception as e:
            self.error_occurred.emit(f"Snapshot sırasında hata oluştu: {e}")

# --- 3. PyQt5 Ana Pencere ---

class SystemMonitor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Sistem Durumu İzleyici")
        # Kullanıcının güncellediği pencere boyutları korunmuştur.
        self.setGeometry(100, 100, 800, 500) 
        
        self.snapshot_before = None
        self.snapshot_time = None
        self.differences = None 
        
        self._setup_ui()
        self._setup_worker()
        
        # Program başlangıcında daha önce kaydedilmiş snapshot'ı yükle
        self.load_initial_snapshot() 

    def _setup_ui(self):
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(5) 
        main_layout.setContentsMargins(10, 10, 10, 10) 

        # 1. Butonlar için Yatay Düzen (En üst satır)
        button_layout = QHBoxLayout()
        
        self.snapshot_button = QPushButton("1. Başlangıç Snapshot'ı Al")
        self.snapshot_button.clicked.connect(self.take_snapshot_before)
        button_layout.addWidget(self.snapshot_button)
        
        self.compare_button = QPushButton("2. Durum Karşılaştır")
        self.compare_button.clicked.connect(self.compare_snapshots_after)
        self.compare_button.setEnabled(False) 
        button_layout.addWidget(self.compare_button)
        
        # 3. TXT Dosyasına Yazdır Butonu
        self.save_button = QPushButton("3. TXT Dosyasına Yazdır") 
        self.save_button.clicked.connect(self.save_to_txt)
        self.save_button.setEnabled(False) 
        button_layout.addWidget(self.save_button)
        
        main_layout.addLayout(button_layout)

        # 2. Durum Etiketi (Ayrı Satır)
        self.status_label = QLabel("Durum: Başlangıç")
        self.status_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        main_layout.addWidget(self.status_label) 

        # 3. İlerleme Çubuğu (Ayrı Satır, Tam Genişlik)
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 0) 
        self.progress_bar.setVisible(False) 
        self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        main_layout.addWidget(self.progress_bar)
        
        # Splitter (Sol ve Sağ Paneller) - Yatay Bölücü
        splitter = QSplitter(Qt.Horizontal)
        splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 

        # Sol Panel (Önceki Durum Bilgisi)
        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)
        left_layout.setContentsMargins(0, 0, 0, 0) 
        left_layout.setSpacing(5) 

        left_layout.addWidget(QLabel("Başlangıç Durumu"))
        self.before_info = QTextEdit()
        self.before_info.setReadOnly(True)
        self.before_info.setPlainText("Başlangıç Snapshot'ı Alınmadı.")
        left_layout.addWidget(self.before_info)
        
        # Sağ Panel (Karşılaştırma Sonucu)
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        right_layout.setContentsMargins(0, 0, 0, 0)
        right_layout.setSpacing(5) 

        right_layout.addWidget(QLabel("Karşılaştırma Sonuçları (Farklar)"))
        self.comparison_tree = QTreeWidget()
        self.comparison_tree.setColumnCount(3)
        self.comparison_tree.setHeaderLabels(["Tür", "Yol", "Açıklama"])
        self.comparison_tree.header().setStretchLastSection(True) 
        
        # Başlıkları elle ayarlanabilir yapma
        self.comparison_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
        self.comparison_tree.header().setSectionResizeMode(1, QHeaderView.Interactive)
        self.comparison_tree.header().setSectionResizeMode(2, QHeaderView.Interactive) 
        
        self.comparison_tree.setColumnWidth(0, 150) 
        
        right_layout.addWidget(self.comparison_tree)
        
        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        # Kullanıcının güncellediği splitter boyutları korunmuştur.
        splitter.setSizes([400, 750]) 
        
        main_layout.addWidget(splitter)
        self.setCentralWidget(central_widget)

    def _setup_worker(self):
        self.worker_thread = None

    # --- YENİ: Başlangıçta Kayıtlı Snapshot Yükleme İşlevi ---
    def load_initial_snapshot(self):
        snapshot_path = Path(SNAPSHOT_FILE)
        
        if snapshot_path.exists():
            try:
                with open(snapshot_path, 'r') as f:
                    self.snapshot_before = json.load(f)
                
                # Dosyanın son değiştirilme zamanını (modification time) kullan
                mtime = snapshot_path.stat().st_mtime
                self.snapshot_time = datetime.fromtimestamp(mtime)

                info_text = (
                    f"***Önceki Snapshot Dosyadan Yüklendi!***\n"
                    f"Yükleme Tarihi: {self.snapshot_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
                    f"Toplam Öğe Sayısı: {len(self.snapshot_before)}\n"
                    f"JSON Dosyası: {SNAPSHOT_FILE}\n\n"
                    f"Taranan Dizinler:\n"
                    + "\n".join(DIRS_TO_CHECK)
                )
                self.before_info.setText(info_text)
                self.compare_button.setEnabled(True)
                self.status_label.setText(f"Durum: Eski snapshot ({snapshot_path.name}) başarıyla yüklendi. 'Durum Karşılaştır'a basılabilir.")
                
            except Exception as e:
                self.snapshot_before = None 
                self.status_label.setText(f"Durum: HATA - Önceki snapshot yüklenemedi: {e}")
                QMessageBox.warning(self, "Yükleme Hatası", f"Kaydedilmiş snapshot dosyası yüklenemedi:\n{e}")


    # --- Güncellendi: Snapshot Alma İşlevi (Uyarı Eklendi) ---
    def take_snapshot_before(self):
        # Eğer önceden bir snapshot varsa (dosyadan yüklense de, bu oturumda alınsa da) uyarı ver.
        if self.snapshot_before is not None:
            reply = QMessageBox.question(self, 
                                         'Onay Gerekli', 
                                         "Daha önce alınmış bir snapshot kaydı mevcut. Yeni bir snapshot almak, eski kaydı silip (üzerine yazıp) güncelleyecek.\n\nDevam etmek istiyor musunuz?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            
            if reply == QMessageBox.No:
                self.status_label.setText("Durum: Yeni snapshot alma işlemi kullanıcı tarafından iptal edildi.")
                return

        self.status_label.setText("Durum: Başlangıç snapshot'ı alınıyor... Lütfen bekleyin.") 
        self._set_ui_busy(True)
        
        if self.worker_thread:
            self.worker_thread.quit()
            self.worker_thread.wait()

        self.worker_thread = WorkerThread(DIRS_TO_CHECK)
        self.worker_thread.snapshot_finished.connect(self._handle_before_snapshot_result)
        self.worker_thread.error_occurred.connect(self._handle_error)
        self.worker_thread.start()

    def _handle_before_snapshot_result(self, snapshot_data):
        self._set_ui_busy(False)
        self.snapshot_before = snapshot_data
        self.snapshot_time = datetime.now()
        
        try:
            # Snapshot dosyası, SNAPSHOT_FILE sabitinde tanımlanan yeni yola kaydediliyor.
            with open(SNAPSHOT_FILE, 'w') as f:
                json.dump(self.snapshot_before, f, indent=4)
            
            info_text = (
                f"***Başlangıç Snapshot'ı Alındı!***\n"
                f"Tarih/Saat: {self.snapshot_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
                f"Toplam Öğe Sayısı: {len(self.snapshot_before)}\n"
                f"JSON Dosyası: {SNAPSHOT_FILE}\n\n" # Kayıt yolunu da gösteriyoruz
                f"Taranan Dizinler:\n"
                + "\n".join(DIRS_TO_CHECK)
            )
            
            self.before_info.setText(info_text)

            self.status_label.setText("Durum: Snapshot başarılı. Şimdi bir program kurup 'Durum Karşılaştır'a basın.") 
            self.compare_button.setEnabled(True)
            
        except Exception as e:
            self._set_ui_busy(False)
            self._handle_error(f"Snapshot kaydedilirken hata oluştu: {e}")

    def compare_snapshots_after(self):
        if not self.snapshot_before:
            QMessageBox.warning(self, "Uyarı", "Önce 'Başlangıç Snapshot'ı Al' düğmesine basmalısınız.") 
            return

        self.status_label.setText("Durum: Yeni snapshot alınıyor ve karşılaştırma yapılıyor... Lütfen bekleyin.") 
        self._set_ui_busy(True)

        self.worker_thread = WorkerThread(DIRS_TO_CHECK)
        self.worker_thread.snapshot_finished.connect(self._handle_compare_snapshot_result)
        self.worker_thread.error_occurred.connect(self._handle_error)
        self.worker_thread.start()

    def _handle_compare_snapshot_result(self, snapshot_after):
        self._set_ui_busy(False)
        try:
            self.differences = compare_snapshots(self.snapshot_before, snapshot_after)
            self.display_differences(self.differences)
            
            total_diff_count = len(self.differences['added']) + len(self.differences['deleted']) + len(self.differences['modified'])
            self.status_label.setText(f"Durum: Karşılaştırma tamamlandı. Fark sayısı: {total_diff_count}. Sonuçlar sağ panelde.") 
            
            self.save_button.setEnabled(True)
            
        except Exception as e:
            self._set_ui_busy(False)
            self._handle_error(f"Karşılaştırma sırasında beklenmeyen hata oluştu: {e}")

    # --- TXT Kaydetme İşlevi ---
    
    def _format_differences_for_txt(self):
        """Karşılaştırma sonuçlarını TXT dosyası için formatlar."""
        
        # Başlık ve Tarih Bilgisi
        content = f"### Sistem Değişiklik Raporu ###\n" 
        content += f"Rapor Tarihi: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
        content += f"Başlangıç Snapshot Tarihi: {self.snapshot_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
        content += "\n"
        
        # Taranan Dizinler (En üstte)
        content += "--- TARANAN DİZİNLER ---\n"
        for d in DIRS_TO_CHECK:
            content += f"- {d}\n"
        content += "\n"
        
        # Değişiklikler
        
        # 1. Eklenenler
        added = self.differences.get('added', {})
        content += f"--- 1. YENİ EKLENEN ÖĞELER ({len(added)}) ---\n"
        if added:
            for path, data in added.items():
                content += f"[YENİ] Yol: {path}, Tip: {data['type'].capitalize()}, Boyut: {data['size']} bayt\n"
        else:
            content += "Yeni öğe bulunamadı.\n"
        content += "\n"
        
        # 2. Silinenler
        deleted = self.differences.get('deleted', {})
        content += f"--- 2. SİLİNEN ÖĞELER ({len(deleted)}) ---\n"
        if deleted:
            for path, data in deleted.items():
                content += f"[SİLİNDİ] Yol: {path}, Tip: {data['type'].capitalize()}, Boyut: {data['size']} bayt\n"
        else:
            content += "Silinen öğe bulunamadı.\n"
        content += "\n"

        # 3. Değiştirilenler
        modified = self.differences.get('modified', {})
        content += f"--- 3. DEĞİŞTİRİLEN ÖĞELER ({len(modified)}) ---\n"
        if modified:
            for path, data_pair in modified.items():
                data1 = data_pair["before"]
                data2 = data_pair["after"]
                
                detail = []
                if data1["size"] != data2["size"]:
                    size_diff = data2["size"] - data1["size"]
                    detail.append(f"Boyut Farkı: {size_diff} bayt ({data1['size']} -> {data2['size']})")
                
                if "hash" in data1 and data1.get("hash") != data2.get("hash"):
                    detail.append("İçerik (Hash) Değişti")

                content += f"[DEĞİŞTİ] Yol: {path}, Detay: {', '.join(detail)}\n"
        else:
            content += "Değiştirilen öğe bulunamadı.\n"
            
        return content


    def save_to_txt(self):
        """Sonuçları compare.txt dosyasına kaydeder."""
        if self.differences is None:
            QMessageBox.warning(self, "Uyarı", "Lütfen önce bir karşılaştırma yapın.")
            return

        # Kullanıcıdan kaydetme yolu al
        filename, _ = QFileDialog.getSaveFileName(self, 
                                                  "Karşılaştırma Sonuçlarını Kaydet", 
                                                  "compare.txt", 
                                                  "Metin Dosyaları (*.txt);;Tüm Dosyalar (*)")
        
        if not filename:
            return

        # Dosya içeriğini oluştur
        content = self._format_differences_for_txt()
        
        # Windows satır sonu standardı ('\r\n') ile kaydet
        try:
            with open(filename, 'w', encoding='utf-8', newline='\r\n') as f:
                f.write(content)
            
            self.status_label.setText(f"Durum: Sonuçlar başarıyla kaydedildi: {filename}")
            QMessageBox.information(self, "Başarılı", f"Karşılaştırma sonuçları başarıyla kaydedildi:\n{filename}")
        except Exception as e:
            self._handle_error(f"Dosya kaydedilirken hata oluştu: {e}")
            
    # -----------------------------------


    def _set_ui_busy(self, is_busy):
        """UI meşgul durumunu yönetir."""
        self.snapshot_button.setEnabled(not is_busy)
        self.compare_button.setEnabled(not is_busy and self.snapshot_before is not None) 
        self.save_button.setEnabled(not is_busy and self.differences is not None) 
        self.progress_bar.setVisible(is_busy)


    def _handle_error(self, message):
        """Hata mesajlarını kullanıcıya gösterir ve GUI durumunu düzeltir."""
        self._set_ui_busy(False)
        self.status_label.setText(f"Durum: HATA - {message}")
        QMessageBox.critical(self, "Hata", message)

    # --- Farkları Görüntüleme ---

    def display_differences(self, differences):
        """Farkları QTreeWidget içinde görüntüler."""
        self.comparison_tree.clear()

        # 1. Eklenenler (Added)
        added_root = QTreeWidgetItem(self.comparison_tree, [f"YENİ ({len(differences['added'])})", "", ""]) 
        added_root.setBackground(0, Qt.darkGreen)
        added_root.setForeground(0, Qt.white)
        for path, data in differences['added'].items():
            detail = f"{data['type'].capitalize()}, Boyut: {data['size']} bayt"
            QTreeWidgetItem(added_root, ["Yeni", path, detail])

        # 2. Silinenler (Deleted)
        deleted_root = QTreeWidgetItem(self.comparison_tree, [f"SİLİNEN ({len(differences['deleted'])})", "", ""]) 
        deleted_root.setBackground(0, Qt.darkRed)
        deleted_root.setForeground(0, Qt.white)
        for path, data in differences['deleted'].items():
            detail = f"{data['type'].capitalize()}, Boyut: {data['size']} bayt"
            QTreeWidgetItem(deleted_root, ["Kaldırıldı", path, detail])

        # 3. Değiştirilenler (Modified)
        modified_root = QTreeWidgetItem(self.comparison_tree, [f"DEĞİŞEN ({len(differences['modified'])})", "", ""]) 
        modified_root.setBackground(0, Qt.darkYellow)
        modified_root.setForeground(0, Qt.black)
        for path, data_pair in differences['modified'].items():
            data1 = data_pair["before"]
            data2 = data_pair["after"]
            
            detail = ""
            if data1["size"] != data2["size"]:
                size_diff = data2["size"] - data1["size"]
                detail += f"Boyut Farkı: {size_diff} bayt ({data1['size']} -> {data2['size']})"
            
            if "hash" in data1 and data1.get("hash") != data2.get("hash"):
                if detail: detail += " | "
                detail += "İçerik (Hash) Değişti"

            QTreeWidgetItem(modified_root, ["Değişti", path, detail])
            
        self.comparison_tree.expandAll() 


# --- 4. Programı Çalıştırma ---

if __name__ == '__main__':
    final_dirs = [str(Path(d).expanduser()) for d in DIRS_TO_CHECK]
    
    app = QApplication(sys.argv)
    window = SystemMonitor()
    window.show()
    sys.exit(app.exec_())

Metni alıp örneğin compare.py adıyla bir yere kaydet.
Ardından o dizinde sağtık–>burada terminal aç de, şunu verip entırla:

python3 compare.py

Bu kadar.

Duruma göre ilerde geliştirebilirim de, belli olmaz. Şu haliyle çöp yazılım ama iyi bir raporlayıcı olabilir.

3 Beğeni

Harika!
Belki başka bazı klasörler de ekleyip ve ~/.config klasöründeki google-chrome vb. bazı yerleri tarama dışında tutmak gibi küçük bazı kolay değişiklikler yaparak kullanabilirim bunu şahsen.
Teşekkürler! :blue_heart:

1 Beğeni

Aaaa doğru ya! :thinking:

Zaman bulabilirsem onunla da biraz uğaşayım.

Yalnız bir sıkıntısı var; json dosyası çok büyük. Şu an elimdeki 35,6MB olmuş ve binlerce satırdan oluşuyor.

EDİT:

Özellik eklemesi yapıldı ve test edildi. Düzgün çalışıyor.

import sys
import os
import json
import hashlib
from pathlib import Path
from datetime import datetime
import time 

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
    QPushButton, QTextEdit, QTreeWidget, QTreeWidgetItem, QSplitter, 
    QLabel, QSizePolicy, QMessageBox, QHeaderView, QProgressBar, QFileDialog,
    QDialog, QLineEdit, QListWidget, QListWidgetItem
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QDir # QDir eklendi

# --- 1. Sabitler ve Yardımcı Fonksiyonlar ---
# ... (Diğer sabitler ve fonksiyonlar aynı kalmıştır)
DIRS_TO_CHECK = [
    "/etc", 
    "/usr/bin", 
    "/usr/share", 
    "/usr/local/bin", 
    "~/.config", 
    "~/.local", 
    "/opt"
]

SNAPSHOT_FILE = str(Path.home() / "compare.json")
EXCLUSION_FILE = Path.home() / ".config" / "system_monitor_exclude.json"
EXCLUSION_FILE.parent.mkdir(parents=True, exist_ok=True) 
CONFIG_EXTENSIONS = ['.conf', '.ini', '.json', '.yaml', '.xml', '.cfg']

def get_file_hash(filepath, block_size=65536):
    """Verilen dosyanın SHA256 özetini (hash) hesaplar."""
    try:
        hasher = hashlib.sha256()
        with open(filepath, 'rb') as f:
            while True:
                buf = f.read(block_size)
                if not buf:
                    break
                hasher.update(buf)
        return hasher.hexdigest()
    except Exception:
        return None 

def is_excluded(path_str, excluded_paths):
    """
    Verilen yolun hariç tutulan yollardan herhangi birinin altında olup olmadığını kontrol eder.
    """
    path = Path(path_str)
    for excluded in excluded_paths:
        if path == excluded or excluded in path.parents:
            return True
    return False

def take_snapshot(root_dirs, excluded_paths_str):
    """
    Belirtilen kök dizinleri özyinelemeli olarak tarar ve meta verileri toplar.
    Hariç tutulan yolları atlar.
    """
    snapshot_data = {}
    excluded_paths = [Path(d).expanduser() for d in excluded_paths_str]

    for root_dir_str in root_dirs:
        root_dir = Path(root_dir_str).expanduser() 

        if not root_dir.exists():
            continue

        for entry in root_dir.rglob('*'):
            try:
                if is_excluded(str(entry), excluded_paths):
                    continue

                if entry.is_symlink():
                    continue

                stats = entry.stat()
                data = {
                    "type": "dir" if entry.is_dir() else "file",
                    "size": stats.st_size,
                    "mtime": stats.st_mtime,
                }
                
                if entry.is_file() and any(entry.name.endswith(ext) for ext in CONFIG_EXTENSIONS):
                    data["hash"] = get_file_hash(entry)

                snapshot_data[str(entry)] = data
            except Exception:
                continue
                
    return snapshot_data

def compare_snapshots(snap1, snap2):
    """İki snapshot verisini karşılaştırır ve farkları döndürür."""
    differences = {
        "added": {},
        "deleted": {},
        "modified": {}
    }

    for path, data1 in snap1.items():
        if path not in snap2:
            differences["deleted"][path] = data1
        else:
            data2 = snap2[path]
            is_modified = False
            
            if data1["type"] == "file" and data2["type"] == "file":
                if data1["size"] != data2["size"]:
                    is_modified = True
                elif "hash" in data1 and data1.get("hash") != data2.get("hash"):
                    is_modified = True
            
            if is_modified:
                differences["modified"][path] = {"before": data1, "after": data2}

    for path, data2 in snap2.items():
        if path not in snap1:
            differences["added"][path] = data2
            
    return differences

# --- Hariç Tutma Penceresi (Exclusion Dialog) ---
class ExclusionDialog(QDialog):
    def __init__(self, parent=None, initial_exclusions=None):
        super().__init__(parent)
        self.setWindowTitle("Hariç Tutulacak Yolları Yönet")
        self.setGeometry(200, 200, 500, 400)
        self.exclusions = initial_exclusions if initial_exclusions is not None else []
        self._setup_ui()
        
    def _setup_ui(self):
        main_layout = QVBoxLayout(self)
        
        main_layout.addWidget(QLabel("Hariç Tutulan Yollar:"))
        self.list_widget = QListWidget()
        self.list_widget.addItems(self.exclusions)
        main_layout.addWidget(self.list_widget)
        
        button_layout = QHBoxLayout()
        self.add_button = QPushButton("Yol Ekle...")
        self.add_button.clicked.connect(self._add_path)
        self.remove_button = QPushButton("Seçileni Kaldır")
        self.remove_button.clicked.connect(self._remove_path)
        
        button_layout.addWidget(self.add_button)
        button_layout.addWidget(self.remove_button)
        main_layout.addLayout(button_layout)
        
        dialog_buttons = QHBoxLayout()
        self.save_button = QPushButton("Kaydet ve Kapat")
        self.save_button.clicked.connect(self.accept)
        self.cancel_button = QPushButton("İptal")
        self.cancel_button.clicked.connect(self.reject)
        
        dialog_buttons.addWidget(self.save_button)
        dialog_buttons.addWidget(self.cancel_button)
        main_layout.addLayout(dialog_buttons)

    def _add_path(self):
        # YENİ YAKLAŞIM: QFileDialog.getExistingDirectory yerine QFileDialog nesnesi kullanılıyor.
        # Bu, gizli klasörleri göstermede daha güvenilir ve adres satırı girişini iyileştirir.
        
        dialog = QFileDialog(self, "Hariç Tutulacak Dizin Seç", str(Path.home()))
        
        # 1. Sadece Dizinleri Göster modunu ayarla
        dialog.setFileMode(QFileDialog.DirectoryOnly)
        
        # 2. Gizli dizinleri de dahil etmek için QDir filtre bayrağını ayarla
        # QDir.Dirs: Yalnızca dizinleri listele
        # QDir.Hidden: Gizli dosya ve dizinleri dahil et
        # QDir.NoDotAndDotDot: . ve .. dizinlerini listeden kaldır (tercih edilebilir)
        dialog.setFilter(QDir.Dirs | QDir.Hidden | QDir.NoDotAndDotDot)
        
        # 3. İstenen bayrakları (ShowDirsOnly ve DontResolveSymlinks) setOption ile ayarla
        # QFileDialog.ShowDirsOnly, QFileDialog.DirectoryOnly ile zaten ayarlanmış olsa da, 
        # adres çubuğundaki kısıtlamaları kaldırmak için DirectoryOnly kullanıldı.
        # DontResolveSymlinks sembolik bağları çözmemeyi sağlar.
        dialog.setOption(QFileDialog.DontResolveSymlinks, True)
        
        if dialog.exec_() == QDialog.Accepted:
            # Seçilen yolu al
            path = dialog.selectedFiles()[0]
            
            if path and path not in self.exclusions:
                try:
                    # Yol '~' ile başlayan bir göreli yola çevrilir (Daha temiz kayıt için)
                    relative_path = Path(path).relative_to(Path.home())
                    path_to_save = str(Path('~') / relative_path)
                except ValueError:
                    # Eğer yol Home dizininin dışında ise, tam yolu kaydet
                    path_to_save = path

                if path_to_save not in self.exclusions:
                    self.exclusions.append(path_to_save)
                    self.exclusions.sort()
                    self.list_widget.clear()
                    self.list_widget.addItems(self.exclusions)

    def _remove_path(self):
        current_row = self.list_widget.currentRow()
        if current_row >= 0:
            item_text = self.list_widget.item(current_row).text()
            self.list_widget.takeItem(current_row)
            if item_text in self.exclusions:
                self.exclusions.remove(item_text)

    def get_exclusions(self):
        """Kaydedilecek nihai listeyi döndürür."""
        return self.exclusions

# --- 2. Arka Plan Çalışanı ---
# ... (WorkerThread aynı kaldı)
class WorkerThread(QThread):
    snapshot_finished = pyqtSignal(dict)
    error_occurred = pyqtSignal(str)

    def __init__(self, dirs_to_scan, excluded_paths):
        super().__init__()
        self.dirs_to_scan = dirs_to_scan
        self.excluded_paths = excluded_paths

    def run(self):
        try:
            snapshot = take_snapshot(self.dirs_to_scan, self.excluded_paths)
            self.snapshot_finished.emit(snapshot)
        except Exception as e:
            self.error_occurred.emit(f"Snapshot sırasında hata oluştu: {e}")

# --- 3. PyQt5 Ana Pencere ---
# ... (SystemMonitor sınıfının geri kalanı aynı kalmıştır)

class SystemMonitor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Sistem Durumu İzleyici")
        self.setGeometry(100, 100, 900, 600) 
        
        self.snapshot_before = None
        self.snapshot_time = None
        self.differences = None 
        self.excluded_paths = self._load_exclusions() 
        
        self._setup_ui()
        self._setup_worker()
        self.update_before_info_panel() 
        
        self.load_initial_snapshot() 

    def _load_exclusions(self):
        if EXCLUSION_FILE.exists():
            try:
                with open(EXCLUSION_FILE, 'r') as f:
                    return json.load(f)
            except Exception:
                return []
        return []

    def _save_exclusions(self):
        try:
            with open(EXCLUSION_FILE, 'w') as f:
                json.dump(self.excluded_paths, f, indent=4)
            return True
        except Exception as e:
            QMessageBox.critical(self, "Kayıt Hatası", f"Hariç tutma listesi kaydedilemedi: {e}")
            return False

    def open_exclusion_dialog(self):
        dialog = ExclusionDialog(self, initial_exclusions=list(self.excluded_paths))
        if dialog.exec_() == QDialog.Accepted:
            self.excluded_paths = dialog.get_exclusions()
            if self._save_exclusions():
                QMessageBox.information(self, "Başarılı", "Hariç tutma listesi başarıyla güncellendi ve kaydedildi.")
            self.update_before_info_panel() 
            
            if self.snapshot_before is not None:
                 QMessageBox.warning(self, "Uyarı", "Hariç tutma listesi değişti. Yeni ayarlarla doğru karşılaştırma yapmak için lütfen **yeniden 'Başlangıç Snapshot'ı Al'** düğmesine basın.")

    def _setup_ui(self):
        central_widget = QWidget()
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(5) 
        main_layout.setContentsMargins(10, 10, 10, 10) 

        button_layout = QHBoxLayout()
        
        self.snapshot_button = QPushButton("1. Başlangıç Snapshot'ı Al")
        self.snapshot_button.clicked.connect(self.take_snapshot_before)
        button_layout.addWidget(self.snapshot_button)
        
        self.compare_button = QPushButton("2. Durum Karşılaştır")
        self.compare_button.clicked.connect(self.compare_snapshots_after)
        self.compare_button.setEnabled(False) 
        button_layout.addWidget(self.compare_button)
        
        self.save_button = QPushButton("3. TXT Dosyasına Yazdır") 
        self.save_button.clicked.connect(self.save_to_txt)
        self.save_button.setEnabled(False) 
        button_layout.addWidget(self.save_button)
        
        self.exclude_button = QPushButton("4. Hariç Tutulacak Yolları Yönet")
        self.exclude_button.clicked.connect(self.open_exclusion_dialog)
        button_layout.addWidget(self.exclude_button)
        
        main_layout.addLayout(button_layout)

        self.status_label = QLabel("Durum: Başlangıç")
        self.status_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        main_layout.addWidget(self.status_label) 

        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 0) 
        self.progress_bar.setVisible(False) 
        self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        main_layout.addWidget(self.progress_bar)
        
        splitter = QSplitter(Qt.Horizontal)
        splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 

        left_panel = QWidget()
        left_layout = QVBoxLayout(left_panel)
        left_layout.setContentsMargins(0, 0, 0, 0) 
        left_layout.setSpacing(5) 

        left_layout.addWidget(QLabel("Başlangıç Durumu"))
        self.before_info = QTextEdit()
        self.before_info.setReadOnly(True)
        self.before_info.setPlainText("Başlangıç Snapshot'ı Alınmadı.")
        left_layout.addWidget(self.before_info)
        
        right_panel = QWidget()
        right_layout = QVBoxLayout(right_panel)
        right_layout.setContentsMargins(0, 0, 0, 0)
        right_layout.setSpacing(5) 

        right_layout.addWidget(QLabel("Karşılaştırma Sonuçları (Farklar)"))
        self.comparison_tree = QTreeWidget()
        self.comparison_tree.setColumnCount(3)
        self.comparison_tree.setHeaderLabels(["Tür", "Yol", "Açıklama"])
        self.comparison_tree.header().setStretchLastSection(True) 
        
        self.comparison_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
        self.comparison_tree.header().setSectionResizeMode(1, QHeaderView.Interactive)
        self.comparison_tree.header().setSectionResizeMode(2, QHeaderView.Interactive) 
        
        self.comparison_tree.setColumnWidth(0, 150) 
        
        right_layout.addWidget(self.comparison_tree)
        
        splitter.addWidget(left_panel)
        splitter.addWidget(right_panel)
        splitter.setSizes([400, 750]) 
        
        main_layout.addWidget(splitter)
        self.setCentralWidget(central_widget)

    def _setup_worker(self):
        self.worker_thread = None

    def update_before_info_panel(self, is_loaded=False):
        info_parts = []
        
        if self.snapshot_before is not None and self.snapshot_time is not None:
            status = "***Önceki Snapshot Dosyadan Yüklendi!***" if is_loaded else "***Başlangıç Snapshot'ı Alındı!***"
            info_parts.append(status)
            info_parts.append(f"Tarih/Saat: {self.snapshot_time.strftime('%Y-%m-%d %H:%M:%S')}")
            info_parts.append(f"Toplam Öğe Sayısı: {len(self.snapshot_before)}")
            info_parts.append(f"JSON Dosyası: {SNAPSHOT_FILE}")
        else:
            info_parts.append("Başlangıç Snapshot'ı Alınmadı.")
        
        info_parts.append("\n" + "-"*30 + "\n")
        
        info_parts.append("Taranan Kök Dizinler:")
        info_parts.extend([f"  - {d}" for d in DIRS_TO_CHECK])
        
        info_parts.append("\n")
        
        info_parts.append(f"Hariç Tutulan Dizinler ({len(self.excluded_paths)}):")
        if self.excluded_paths:
            info_parts.extend([f"  - {d}" for d in self.excluded_paths])
        else:
            info_parts.append("  - (Yok)")
            
        self.before_info.setText('\n'.join(info_parts))
        
        self.compare_button.setEnabled(self.snapshot_before is not None and not self.progress_bar.isVisible())

    def load_initial_snapshot(self):
        snapshot_path = Path(SNAPSHOT_FILE)
        
        if snapshot_path.exists():
            try:
                with open(snapshot_path, 'r') as f:
                    self.snapshot_before = json.load(f)
                
                mtime = snapshot_path.stat().st_mtime
                self.snapshot_time = datetime.fromtimestamp(mtime)

                self.update_before_info_panel(is_loaded=True)

                self.compare_button.setEnabled(True)
                self.status_label.setText(f"Durum: Eski snapshot ({snapshot_path.name}) başarıyla yüklendi. 'Durum Karşılaştır'a basılabilir.")
                
            except Exception as e:
                self.snapshot_before = None 
                self.status_label.setText(f"Durum: HATA - Önceki snapshot yüklenemedi: {e}")
                QMessageBox.warning(self, "Yükleme Hatası", f"Kaydedilmiş snapshot dosyası yüklenemedi:\n{e}")

    def take_snapshot_before(self):
        if self.snapshot_before is not None:
            reply = QMessageBox.question(self, 
                                         'Onay Gerekli', 
                                         "Daha önce alınmış bir snapshot kaydı mevcut. Yeni bir snapshot almak, eski kaydı silip (üzerine yazıp) güncelleyecek.\n\nDevam etmek istiyor musunuz?",
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            
            if reply == QMessageBox.No:
                self.status_label.setText("Durum: Yeni snapshot alma işlemi kullanıcı tarafından iptal edildi.")
                return

        self.status_label.setText("Durum: Başlangıç snapshot'ı alınıyor... Lütfen bekleyin. (Hariç tutulan yollar atlanacak)") 
        self._set_ui_busy(True)
        
        if self.worker_thread:
            self.worker_thread.quit()
            self.worker_thread.wait()

        self.worker_thread = WorkerThread(DIRS_TO_CHECK, self.excluded_paths)
        self.worker_thread.snapshot_finished.connect(self._handle_before_snapshot_result)
        self.worker_thread.error_occurred.connect(self._handle_error)
        self.worker_thread.start()

    def _handle_before_snapshot_result(self, snapshot_data):
        self._set_ui_busy(False)
        self.snapshot_before = snapshot_data
        self.snapshot_time = datetime.now()
        
        try:
            with open(SNAPSHOT_FILE, 'w') as f:
                json.dump(self.snapshot_before, f, indent=4)
            
            self.update_before_info_panel() 

            self.status_label.setText("Durum: Snapshot başarılı. Şimdi bir program kurup 'Durum Karşılaştır'a basın.") 
            self.compare_button.setEnabled(True)
            
        except Exception as e:
            self._set_ui_busy(False)
            self._handle_error(f"Snapshot kaydedilirken hata oluştu: {e}")

    def compare_snapshots_after(self):
        if not self.snapshot_before:
            QMessageBox.warning(self, "Uyarı", "Önce 'Başlangıç Snapshot'ı Al' düğmesine basmalısınız.") 
            return

        self.status_label.setText("Durum: Yeni snapshot alınıyor ve karşılaştırma yapılıyor... Lütfen bekleyin. (Hariç tutulan yollar atlanacak)") 
        self._set_ui_busy(True)

        self.worker_thread = WorkerThread(DIRS_TO_CHECK, self.excluded_paths)
        self.worker_thread.snapshot_finished.connect(self._handle_compare_snapshot_result)
        self.worker_thread.error_occurred.connect(self._handle_error)
        self.worker_thread.start()

    def _handle_compare_snapshot_result(self, snapshot_after):
        self._set_ui_busy(False)
        try:
            self.differences = compare_snapshots(self.snapshot_before, snapshot_after)
            self.display_differences(self.differences)
            
            total_diff_count = len(self.differences['added']) + len(self.differences['deleted']) + len(self.differences['modified'])
            self.status_label.setText(f"Durum: Karşılaştırma tamamlandı. Fark sayısı: {total_diff_count}. Sonuçlar sağ panelde.") 
            
            self.save_button.setEnabled(True)
            
        except Exception as e:
            self._set_ui_busy(False)
            self._handle_error(f"Karşılaştırma sırasında beklenmeyen hata oluştu: {e}")

    def _format_differences_for_txt(self):
        content = f"### Sistem Değişiklik Raporu ###\n" 
        content += f"Rapor Tarihi: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
        content += f"Başlangıç Snapshot Tarihi: {self.snapshot_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
        content += "\n"
        
        content += "--- TARANAN KÖK DİZİNLER ---\n"
        for d in DIRS_TO_CHECK:
            content += f"- {d}\n"
        content += "\n"
        
        content += "--- HARİÇ TUTULAN DİZİNLER ---\n"
        if self.excluded_paths:
            for d in self.excluded_paths:
                content += f"- {d}\n"
        else:
            content += "(Yok)\n"
        content += "\n"
        
        added = self.differences.get('added', {})
        content += f"--- 1. YENİ EKLENEN ÖĞELER ({len(added)}) ---\n"
        if added:
            for path, data in added.items():
                content += f"[YENİ] Yol: {path}, Tip: {data['type'].capitalize()}, Boyut: {data['size']} bayt\n"
        else:
            content += "Yeni öğe bulunamadı.\n"
        content += "\n"
        
        deleted = self.differences.get('deleted', {})
        content += f"--- 2. SİLİNEN ÖĞELER ({len(deleted)}) ---\n"
        if deleted:
            for path, data in deleted.items():
                content += f"[SİLİNDİ] Yol: {path}, Tip: {data['type'].capitalize()}, Boyut: {data['size']} bayt\n"
        else:
            content += "Silinen öğe bulunamadı.\n"
        content += "\n"

        modified = self.differences.get('modified', {})
        content += f"--- 3. DEĞİŞTİRİLEN ÖĞELER ({len(modified)}) ---\n"
        if modified:
            for path, data_pair in modified.items():
                data1 = data_pair["before"]
                data2 = data_pair["after"]
                
                detail = []
                if data1["size"] != data2["size"]:
                    size_diff = data2["size"] - data1["size"]
                    detail.append(f"Boyut Farkı: {size_diff} bayt ({data1['size']} -> {data2['size']})")
                
                if "hash" in data1 and data1.get("hash") != data2.get("hash"):
                    if detail: detail += " | "
                    detail.append("İçerik (Hash) Değişti")

                content += f"[DEĞİŞTİ] Yol: {path}, Detay: {', '.join(detail)}\n"
        else:
            content += "Değiştirilen öğe bulunamadı.\n"
            
        return content


    def save_to_txt(self):
        if self.differences is None:
            QMessageBox.warning(self, "Uyarı", "Lütfen önce bir karşılaştırma yapın.")
            return

        filename, _ = QFileDialog.getSaveFileName(self, 
                                                  "Karşılaştırma Sonuçlarını Kaydet", 
                                                  "compare.txt", 
                                                  "Metin Dosyaları (*.txt);;Tüm Dosyalar (*)")
        
        if not filename:
            return

        content = self._format_differences_for_txt()
        
        try:
            with open(filename, 'w', encoding='utf-8', newline='\r\n') as f:
                f.write(content)
            
            self.status_label.setText(f"Durum: Sonuçlar başarıyla kaydedildi: {filename}")
            QMessageBox.information(self, "Başarılı", f"Karşılaştırma sonuçları başarıyla kaydedildi:\n{filename}")
        except Exception as e:
            self._handle_error(f"Dosya kaydedilirken hata oluştu: {e}")
            
    def _set_ui_busy(self, is_busy):
        self.snapshot_button.setEnabled(not is_busy)
        self.exclude_button.setEnabled(not is_busy) 
        self.compare_button.setEnabled(not is_busy and self.snapshot_before is not None) 
        self.save_button.setEnabled(not is_busy and self.differences is not None) 
        self.progress_bar.setVisible(is_busy)

    def _handle_error(self, message):
        self._set_ui_busy(False)
        self.status_label.setText(f"Durum: HATA - {message}")
        QMessageBox.critical(self, "Hata", message)

    def display_differences(self, differences):
        self.comparison_tree.clear()

        added_root = QTreeWidgetItem(self.comparison_tree, [f"YENİ ({len(differences['added'])})", "", ""]) 
        added_root.setBackground(0, Qt.darkGreen)
        added_root.setForeground(0, Qt.white)
        for path, data in differences['added'].items():
            detail = f"{data['type'].capitalize()}, Boyut: {data['size']} bayt"
            QTreeWidgetItem(added_root, ["Yeni", path, detail])

        deleted_root = QTreeWidgetItem(self.comparison_tree, [f"SİLİNEN ({len(differences['deleted'])})", "", ""]) 
        deleted_root.setBackground(0, Qt.darkRed)
        deleted_root.setForeground(0, Qt.white)
        for path, data in differences['deleted'].items():
            detail = f"{data['type'].capitalize()}, Boyut: {data['size']} bayt"
            QTreeWidgetItem(deleted_root, ["Kaldırıldı", path, detail])

        modified_root = QTreeWidgetItem(self.comparison_tree, [f"DEĞİŞEN ({len(differences['modified'])})", "", ""]) 
        modified_root.setBackground(0, Qt.darkYellow)
        modified_root.setForeground(0, Qt.black)
        for path, data_pair in differences['modified'].items():
            data1 = data_pair["before"]
            data2 = data_pair["after"]
            
            detail = ""
            if data1["size"] != data2["size"]:
                size_diff = data2["size"] - data1["size"]
                detail += f"Boyut Farkı: {size_diff} bayt ({data1['size']} -> {data2['size']})"
            
            if "hash" in data1 and data1.get("hash") != data2.get("hash"):
                if detail: detail += " | "
                detail += "İçerik (Hash) Değişti"

            QTreeWidgetItem(modified_root, ["Değişti", path, detail])
            
        self.comparison_tree.expandAll() 


# --- 4. Programı Çalıştırma ---

if __name__ == '__main__':
    
    app = QApplication(sys.argv)
    window = SystemMonitor()
    window.show()
    sys.exit(app.exec_())

Bir test edin.

manifest json bende 1.3 kb

home klasörünü de ekledim koda oldu

teşekkürler