2026年6月27日土曜日

dot_cleanしてくれる常駐アプリを作りました

 最近MacbookNeoをつかいまくってて、

SDカードとか書き込んでて気がついた

そうだった。.ds_storeとか_ファイル名とか、Macを使ってると隠しファイルができてしまうのだ。


しかし!

それらを消すコマンドがある!


ターミナルを開いて、以下のコマンドを入れる

dot_clean -m /Volumes/NO\ NAME 

マウントされている名前はNO NAMEなんだけど、スペースが入ってるからバックスラッシュ・スペースでつながっている

これを毎回やるのは嫌なので、

取り出しをするときにフックして、dot_cleanを実行するアプリを作ってみた。

名前はdcleanにした

xcodeを立ち上げて、アカウント入れたりなんだりしてから、contentViewやItemを消して、dcleanAppだけ残して以下のように書き換え

import SwiftUI

import AppKit

import Combine // 🌟 これを追加!


// MARK: - 1. ログのデータ構造と管理クラス


// 重複したログでも区別できるように Identifiable にする

struct LogItem: Identifiable {

    let id = UUID()

    let text: String

}


// ログを管理し、UI(メニューバー)を更新する専用のクラス

class LogStore: ObservableObject {

    static let shared = LogStore()

    

    @Published var logs: [LogItem] = []

    

    // UIの更新は必ずメインスレッドで行う

    func addLog(_ message: String) {

        DispatchQueue.main.async {

            let formatter = DateFormatter()

            formatter.dateFormat = "HH:mm:ss" // 実行時間も表示

            let timeString = formatter.string(from: Date())

            

            let newLog = LogItem(text: "[\(timeString)] \(message)")

            

            // 最新のログが一番上に来るように追加

            self.logs.insert(newLog, at: 0)

            

            // メニューが長くなりすぎないよう最新10件に制限

            if self.logs.count > 10 {

                self.logs.removeLast()

            }

        }

    }

    

    func clear() {

        DispatchQueue.main.async {

            self.logs.removeAll()

        }

    }

}


// MARK: - 2. メインのApp定義


@main

struct dcleanApp: App {

    // LogStoreの変更を監視する

    @StateObject private var logStore = LogStore.shared

    

    init() {

        // アプリ起動時にディスク監視を開始

        DiskObserver.shared.start()

    }

    

    var body: some Scene {

        MenuBarExtra("dclean", systemImage: "wand.and.stars") {

            Text("外付けドライブの取り外しを監視中...")

                .font(.caption)

                .disabled(true)

            

            // ログが存在する場合のみ履歴メニューを表示

            if !logStore.logs.isEmpty {

                Divider()

                Text("最近の動作履歴")

                    .font(.caption)

                    .disabled(true)

                

                ForEach(logStore.logs) { log in

                    Text(log.text)

                        .font(.system(size: 11)) // ログは少し小さめの文字で表示

                }

                

                Divider()

                Button("履歴をクリア") {

                    logStore.clear()

                }

            }

            

            Divider()

            Button("アプリを終了") {

                NSApplication.shared.terminate(nil)

            }

        }

        .menuBarExtraStyle(.menu)

    }

}


// MARK: - 3. ディスク監視クラス


class DiskObserver: NSObject {

    static let shared = DiskObserver()

    

    private override init() {

        super.init()

    }

    

    func start() {

        NSWorkspace.shared.notificationCenter.addObserver(

            self,

            selector: #selector(handleVolumeWillUnmount(_:)),

            name: NSWorkspace.willUnmountNotification,

            object: nil

        )

    }

    

    @objc func handleVolumeWillUnmount(_ notification: Notification) {

        guard let volumeURL = notification.userInfo?[NSWorkspace.volumeURLUserInfoKey] as? URL else { return }

        let path = volumeURL.path

        

        guard path.hasPrefix("/Volumes/") else { return }

        

        // "/Volumes/USB_Drive" から "USB_Drive" の部分だけを抽出して見やすくする

        let driveName = volumeURL.lastPathComponent

        

        print("取り外しを検知: \(path) - dot_cleanを開始します。")

        LogStore.shared.addLog("⏳ \(driveName) をクリーン中...")

        

        runDotClean(for: path, driveName: driveName)

    }

    

    func runDotClean(for path: String, driveName: String) {

        let task = Process()

        task.launchPath = "/usr/sbin/dot_clean"

        task.arguments = ["-m", path]

        

        do {

            try task.run()

            task.waitUntilExit()

            

            print("dot_clean完了: \(path) - 安全に取り外します。")

            LogStore.shared.addLog("✅ \(driveName) のクリーン完了")

        } catch {

            print("dot_clean実行エラー: \(error.localizedDescription)")

            LogStore.shared.addLog("❌ \(driveName) でエラー発生。\(error.localizedDescription)")

        }

    }

}

三角を押して動作確認したら、ビルド対象をAny Mac(arm64, x86_64)にして、メニューのプロダクトからアーカイブを選んでディストリビュートし、CopyAppを選んで適当なところに配置、XCODEでを終わらせて、できたAppをダブルクリックしたら動くはず

これで、いちいち、dot_cleanする必要はなくなった

欲しい方はこちらからどうぞ

https://drive.google.com/file/d/1SG18pzazkZzb-8_BR4v4Jonl2tqZUpFc/view?usp=drive_link

起動するとこのようなアイコンが出ます。

終了させるときは「アプリを終了」です。





0 件のコメント: