From 2394c5d768f282f85d297cae21e41e6ddc8fc273 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 11:02:10 +0800 Subject: [PATCH 1/7] feat(touchid): add sudo_local support with silent migration --- bin/touchid.sh | 133 +++++++++++++++++++++++++++++++++++++++++------ lib/core/sudo.sh | 7 +++ 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/bin/touchid.sh b/bin/touchid.sh index c1a626a..4f2ad54 100755 --- a/bin/touchid.sh +++ b/bin/touchid.sh @@ -14,10 +14,17 @@ LIB_DIR="$(cd "$SCRIPT_DIR/../lib" && pwd)" source "$LIB_DIR/core/common.sh" readonly PAM_SUDO_FILE="${MOLE_PAM_SUDO_FILE:-/etc/pam.d/sudo}" +readonly PAM_SUDO_LOCAL_FILE="${MOLE_PAM_SUDO_LOCAL_FILE:-/etc/pam.d/sudo_local}" readonly PAM_TID_LINE="auth sufficient pam_tid.so" # Check if Touch ID is already configured is_touchid_configured() { + # Check sudo_local first + if [[ -f "$PAM_SUDO_LOCAL_FILE" ]]; then + grep -q "pam_tid.so" "$PAM_SUDO_LOCAL_FILE" 2> /dev/null && return 0 + fi + + # Fallback to standard sudo file if [[ ! -f "$PAM_SUDO_FILE" ]]; then return 1 fi @@ -74,7 +81,74 @@ enable_touchid() { echo "" fi - # Check if already configured + # Check if we should use sudo_local (Sonoma+) + if grep -q "sudo_local" "$PAM_SUDO_FILE"; then + # Check if already correctly configured in sudo_local + if [[ -f "$PAM_SUDO_LOCAL_FILE" ]] && grep -q "pam_tid.so" "$PAM_SUDO_LOCAL_FILE"; then + # It is in sudo_local, but let's check if it's ALSO in sudo (incomplete migration) + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + # Clean up legacy config + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS} Cleanup legacy configuration${NC}" + fi + fi + echo -e "${GREEN}${ICON_SUCCESS} Touch ID is already enabled${NC}" + return 0 + fi + + # Not configured in sudo_local yet. + # Check if configured in sudo (Legacy) + local is_legacy_configured=false + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + is_legacy_configured=true + fi + + # Function to write to sudo_local + local write_success=false + if [[ ! -f "$PAM_SUDO_LOCAL_FILE" ]]; then + # Create the file + echo "# sudo_local: local customizations for sudo" | sudo tee "$PAM_SUDO_LOCAL_FILE" > /dev/null + echo "$PAM_TID_LINE" | sudo tee -a "$PAM_SUDO_LOCAL_FILE" > /dev/null + sudo chmod 444 "$PAM_SUDO_LOCAL_FILE" + sudo chown root:wheel "$PAM_SUDO_LOCAL_FILE" + write_success=true + else + # Append if not present + if ! grep -q "pam_tid.so" "$PAM_SUDO_LOCAL_FILE"; then + temp_file=$(mktemp) + cp "$PAM_SUDO_LOCAL_FILE" "$temp_file" + echo "$PAM_TID_LINE" >> "$temp_file" + sudo mv "$temp_file" "$PAM_SUDO_LOCAL_FILE" + sudo chmod 444 "$PAM_SUDO_LOCAL_FILE" + sudo chown root:wheel "$PAM_SUDO_LOCAL_FILE" + write_success=true + else + write_success=true # Already there (should be caught by first check, but safe fallback) + fi + fi + + if $write_success; then + # If we migrated from legacy, clean it up now + if $is_legacy_configured; then + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + sudo mv "$temp_file" "$PAM_SUDO_FILE" + log_success "Touch ID migrated to sudo_local" + else + log_success "Touch ID enabled (via sudo_local) - try: sudo ls" + fi + return 0 + else + log_error "Failed to write to sudo_local" + return 1 + fi + fi + + # Legacy method: Modify sudo file directly + + # Check if already configured (Legacy) if is_touchid_configured; then echo -e "${GREEN}${ICON_SUCCESS} Touch ID is already enabled${NC}" return 0 @@ -129,26 +203,55 @@ disable_touchid() { return 0 fi - # Create backup only if it doesn't exist - if [[ ! -f "${PAM_SUDO_FILE}.mole-backup" ]]; then - if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then - log_error "Failed to create backup" + # Check sudo_local first + if [[ -f "$PAM_SUDO_LOCAL_FILE" ]] && grep -q "pam_tid.so" "$PAM_SUDO_LOCAL_FILE"; then + # Remove from sudo_local + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_LOCAL_FILE" > "$temp_file" + + if sudo mv "$temp_file" "$PAM_SUDO_LOCAL_FILE" 2> /dev/null; then + # Since we modified sudo_local, we should also check if it's in sudo file (legacy cleanup) + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + sudo mv "$temp_file" "$PAM_SUDO_FILE" + fi + echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled (removed from sudo_local)${NC}" + echo "" + return 0 + else + log_error "Failed to disable Touch ID from sudo_local" return 1 fi fi - # Remove pam_tid.so line - temp_file=$(mktemp) - grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + # Fallback to sudo file (legacy) + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + # Create backup only if it doesn't exist + if [[ ! -f "${PAM_SUDO_FILE}.mole-backup" ]]; then + if ! sudo cp "$PAM_SUDO_FILE" "${PAM_SUDO_FILE}.mole-backup" 2> /dev/null; then + log_error "Failed to create backup" + return 1 + fi + fi - if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then - echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled${NC}" - echo "" - return 0 - else - log_error "Failed to disable Touch ID" - return 1 + # Remove pam_tid.so line + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + + if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled${NC}" + echo "" + return 0 + else + log_error "Failed to disable Touch ID" + return 1 + fi fi + + # Should not reach here if is_touchid_configured was true + log_error "Could not find Touch ID configuration to disable" + return 1 } # Interactive menu diff --git a/lib/core/sudo.sh b/lib/core/sudo.sh index 9527d14..57b80b4 100644 --- a/lib/core/sudo.sh +++ b/lib/core/sudo.sh @@ -9,6 +9,13 @@ set -euo pipefail # ============================================================================ check_touchid_support() { + # Check sudo_local first (Sonoma+) + if [[ -f /etc/pam.d/sudo_local ]]; then + grep -q "pam_tid.so" /etc/pam.d/sudo_local 2> /dev/null + return $? + fi + + # Fallback to checking sudo directly if [[ -f /etc/pam.d/sudo ]]; then grep -q "pam_tid.so" /etc/pam.d/sudo 2> /dev/null return $? From bb4561e6826e5023fec98186e89bf52eed931097 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 03:02:38 +0000 Subject: [PATCH 2/7] chore: update contributors [skip ci] --- CONTRIBUTORS.svg | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTORS.svg b/CONTRIBUTORS.svg index a51f7f9..02f0fa1 100644 --- a/CONTRIBUTORS.svg +++ b/CONTRIBUTORS.svg @@ -1,4 +1,4 @@ - + @@ -35,6 +35,17 @@ + + + + + + + + alexandear + + + @@ -45,7 +56,7 @@ rubnogueira - + @@ -56,17 +67,6 @@ bsisduck - - - - - - - - - alexandear - - From 2b5dd3f44cd36fe64c1a513fe20553019ffd949d Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 14:16:29 +0800 Subject: [PATCH 3/7] feat: show scanning progress as percentage in disk analyzer - Implemented progress percentage display (e.g., `(45%)`) in `cmd/analyze` to show scanning status based on cached total files. - Kept the UI clean by avoiding a full progress bar. - fix: formatting improvements in `bin/touchid.sh`. --- bin/touchid.sh | 50 +++++++++++++++++++++--------------------- cmd/analyze/cache.go | 25 +++++++++++++++++++++ cmd/analyze/main.go | 22 +++++++++++++++++++ cmd/analyze/scanner.go | 1 + cmd/analyze/view.go | 17 +++++++++++++- 5 files changed, 89 insertions(+), 26 deletions(-) diff --git a/bin/touchid.sh b/bin/touchid.sh index 4f2ad54..1f45914 100755 --- a/bin/touchid.sh +++ b/bin/touchid.sh @@ -85,17 +85,17 @@ enable_touchid() { if grep -q "sudo_local" "$PAM_SUDO_FILE"; then # Check if already correctly configured in sudo_local if [[ -f "$PAM_SUDO_LOCAL_FILE" ]] && grep -q "pam_tid.so" "$PAM_SUDO_LOCAL_FILE"; then - # It is in sudo_local, but let's check if it's ALSO in sudo (incomplete migration) - if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then - # Clean up legacy config - temp_file=$(mktemp) - grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" - if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then - echo -e "${GREEN}${ICON_SUCCESS} Cleanup legacy configuration${NC}" - fi - fi - echo -e "${GREEN}${ICON_SUCCESS} Touch ID is already enabled${NC}" - return 0 + # It is in sudo_local, but let's check if it's ALSO in sudo (incomplete migration) + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + # Clean up legacy config + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + if sudo mv "$temp_file" "$PAM_SUDO_FILE" 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS} Cleanup legacy configuration${NC}" + fi + fi + echo -e "${GREEN}${ICON_SUCCESS} Touch ID is already enabled${NC}" + return 0 fi # Not configured in sudo_local yet. @@ -132,12 +132,12 @@ enable_touchid() { if $write_success; then # If we migrated from legacy, clean it up now if $is_legacy_configured; then - temp_file=$(mktemp) - grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" - sudo mv "$temp_file" "$PAM_SUDO_FILE" - log_success "Touch ID migrated to sudo_local" + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + sudo mv "$temp_file" "$PAM_SUDO_FILE" + log_success "Touch ID migrated to sudo_local" else - log_success "Touch ID enabled (via sudo_local) - try: sudo ls" + log_success "Touch ID enabled (via sudo_local) - try: sudo ls" fi return 0 else @@ -210,15 +210,15 @@ disable_touchid() { grep -v "pam_tid.so" "$PAM_SUDO_LOCAL_FILE" > "$temp_file" if sudo mv "$temp_file" "$PAM_SUDO_LOCAL_FILE" 2> /dev/null; then - # Since we modified sudo_local, we should also check if it's in sudo file (legacy cleanup) - if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then - temp_file=$(mktemp) - grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" - sudo mv "$temp_file" "$PAM_SUDO_FILE" - fi - echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled (removed from sudo_local)${NC}" - echo "" - return 0 + # Since we modified sudo_local, we should also check if it's in sudo file (legacy cleanup) + if grep -q "pam_tid.so" "$PAM_SUDO_FILE"; then + temp_file=$(mktemp) + grep -v "pam_tid.so" "$PAM_SUDO_FILE" > "$temp_file" + sudo mv "$temp_file" "$PAM_SUDO_FILE" + fi + echo -e "${GREEN}${ICON_SUCCESS} Touch ID disabled (removed from sudo_local)${NC}" + echo "" + return 0 else log_error "Failed to disable Touch ID from sudo_local" return 1 diff --git a/cmd/analyze/cache.go b/cmd/analyze/cache.go index a0dcb7a..93fb391 100644 --- a/cmd/analyze/cache.go +++ b/cmd/analyze/cache.go @@ -30,6 +30,7 @@ func snapshotFromModel(m model) historyEntry { Entries: cloneDirEntries(m.entries), LargeFiles: cloneFileEntries(m.largeFiles), TotalSize: m.totalSize, + TotalFiles: m.totalFiles, Selected: m.selected, EntryOffset: m.offset, LargeSelected: m.largeSelected, @@ -250,6 +251,7 @@ func saveCacheToDisk(path string, result scanResult) error { Entries: result.Entries, LargeFiles: result.LargeFiles, TotalSize: result.TotalSize, + TotalFiles: result.TotalFiles, ModTime: info.ModTime(), ScanTime: time.Now(), } @@ -264,6 +266,29 @@ func saveCacheToDisk(path string, result scanResult) error { return encoder.Encode(entry) } +// peekCacheTotalFiles attempts to read the total file count from cache, +// ignoring expiration. Used for initial scan progress estimates. +func peekCacheTotalFiles(path string) (int64, error) { + cachePath, err := getCachePath(path) + if err != nil { + return 0, err + } + + file, err := os.Open(cachePath) + if err != nil { + return 0, err + } + defer file.Close() //nolint:errcheck + + var entry cacheEntry + decoder := gob.NewDecoder(file) + if err := decoder.Decode(&entry); err != nil { + return 0, err + } + + return entry.TotalFiles, nil +} + func invalidateCache(path string) { cachePath, err := getCachePath(path) if err == nil { diff --git a/cmd/analyze/main.go b/cmd/analyze/main.go index e97500e..bba4647 100644 --- a/cmd/analyze/main.go +++ b/cmd/analyze/main.go @@ -35,12 +35,14 @@ type scanResult struct { Entries []dirEntry LargeFiles []fileEntry TotalSize int64 + TotalFiles int64 } type cacheEntry struct { Entries []dirEntry LargeFiles []fileEntry TotalSize int64 + TotalFiles int64 ModTime time.Time ScanTime time.Time } @@ -50,6 +52,7 @@ type historyEntry struct { Entries []dirEntry LargeFiles []fileEntry TotalSize int64 + TotalFiles int64 Selected int EntryOffset int LargeSelected int @@ -114,6 +117,8 @@ type model struct { height int // Terminal height multiSelected map[string]bool // Track multi-selected items by path (safer than index) largeMultiSelected map[string]bool // Track multi-selected large files by path (safer than index) + totalFiles int64 // Total files found in current/last scan + lastTotalFiles int64 // Total files from previous scan (for progress bar) } func (m model) inOverviewMode() bool { @@ -195,6 +200,13 @@ func newModel(path string, isOverview bool) model { } } + // Try to peek last total files for progress bar, even if cache is stale + if !isOverview { + if total, err := peekCacheTotalFiles(path); err == nil && total > 0 { + m.lastTotalFiles = total + } + } + return m } @@ -355,6 +367,7 @@ func (m model) scanCmd(path string) tea.Cmd { Entries: cached.Entries, LargeFiles: cached.LargeFiles, TotalSize: cached.TotalSize, + TotalFiles: 0, // Cache doesn't store file count currently, minor UI limitation } return scanResultMsg{result: result, err: nil} } @@ -441,6 +454,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.entries = filteredEntries m.largeFiles = msg.result.LargeFiles m.totalSize = msg.result.TotalSize + m.totalFiles = msg.result.TotalFiles m.status = fmt.Sprintf("Scanned %s", humanizeBytes(m.totalSize)) m.clampEntrySelection() m.clampLargeSelection() @@ -685,6 +699,9 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { invalidateCache(m.path) m.status = "Refreshing..." m.scanning = true + if m.totalFiles > 0 { + m.lastTotalFiles = m.totalFiles + } atomic.StoreInt64(m.filesScanned, 0) atomic.StoreInt64(m.dirsScanned, 0) atomic.StoreInt64(m.bytesScanned, 0) @@ -968,6 +985,7 @@ func (m model) enterSelectedDir() (tea.Model, tea.Cmd) { m.entries = cloneDirEntries(cached.Entries) m.largeFiles = cloneFileEntries(cached.LargeFiles) m.totalSize = cached.TotalSize + m.totalFiles = cached.TotalFiles m.selected = cached.Selected m.offset = cached.EntryOffset m.largeSelected = cached.LargeSelected @@ -978,6 +996,10 @@ func (m model) enterSelectedDir() (tea.Model, tea.Cmd) { m.scanning = false return m, nil } + m.lastTotalFiles = 0 + if total, err := peekCacheTotalFiles(m.path); err == nil && total > 0 { + m.lastTotalFiles = total + } return m, tea.Batch(m.scanCmd(m.path), tickCmd()) } m.status = fmt.Sprintf("File: %s (%s)", selected.Name, humanizeBytes(selected.Size)) diff --git a/cmd/analyze/scanner.go b/cmd/analyze/scanner.go index b6ab09b..0d7ddca 100644 --- a/cmd/analyze/scanner.go +++ b/cmd/analyze/scanner.go @@ -251,6 +251,7 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in Entries: entries, LargeFiles: largeFiles, TotalSize: total, + TotalFiles: atomic.LoadInt64(filesScanned), }, nil } diff --git a/cmd/analyze/view.go b/cmd/analyze/view.go index 6cdf400..67ae52b 100644 --- a/cmd/analyze/view.go +++ b/cmd/analyze/view.go @@ -75,10 +75,25 @@ func (m model) View() string { if m.scanning { filesScanned, dirsScanned, bytesScanned := m.getScanProgress() - fmt.Fprintf(&b, "%s%s%s%s Scanning: %s%s files%s, %s%s dirs%s, %s%s%s\n", + progressPrefix := "" + if m.lastTotalFiles > 0 { + percent := float64(filesScanned) / float64(m.lastTotalFiles) * 100 + // Cap at 100% generally + if percent > 100 { + percent = 100 + } + // While strictly scanning, cap at 99% to avoid "100% but still working" confusion + if m.scanning && percent >= 100 { + percent = 99 + } + progressPrefix = fmt.Sprintf(" %s(%.0f%%)%s", colorCyan, percent, colorReset) + } + + fmt.Fprintf(&b, "%s%s%s%s Scanning%s: %s%s files%s, %s%s dirs%s, %s%s%s\n", colorCyan, colorBold, spinnerFrames[m.spinner], colorReset, + progressPrefix, colorYellow, formatNumber(filesScanned), colorReset, colorYellow, formatNumber(dirsScanned), colorReset, colorGreen, humanizeBytes(bytesScanned), colorReset) From 07e1ee0f3ed0e3257ea16600777d54b6859051c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:16:55 +0000 Subject: [PATCH 4/7] chore: update contributors [skip ci] --- CONTRIBUTORS.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.svg b/CONTRIBUTORS.svg index 02f0fa1..3ab37f2 100644 --- a/CONTRIBUTORS.svg +++ b/CONTRIBUTORS.svg @@ -1,4 +1,4 @@ - + From 24fa0f8f21fcd075cc293de01e183af64b64c2f0 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 14:38:59 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20Add=20cleanup=20rule=20for=20Quark?= =?UTF-8?q?=20(=E5=A4=B8=E5=85=8B)=20video=20cache=20(#279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/clean/app_caches.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/clean/app_caches.sh b/lib/clean/app_caches.sh index 3418aa3..c4b62c6 100644 --- a/lib/clean/app_caches.sh +++ b/lib/clean/app_caches.sh @@ -88,6 +88,7 @@ clean_productivity_apps() { safe_clean ~/Library/Caches/com.orabrowser.app/* "Ora browser cache" safe_clean ~/Library/Caches/com.filo.client/* "Filo cache" safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache" + safe_clean ~/Library/Application\ Support/Quark/Cache/videoCache/* "Quark video cache" } # Music/media players (protect Spotify offline music). clean_media_players() { From e33e4285695de3c1a34026761028fa15323bc10d Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 14:43:50 +0800 Subject: [PATCH 6/7] fix: add KeePassXC new bundle id to protection list (#285) --- lib/core/app_protection.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index 2c617a4..8f68be5 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -85,8 +85,9 @@ readonly DATA_PROTECTED_BUNDLES=( "com.lastpass.*" # LastPass "com.dashlane.*" # Dashlane "com.bitwarden.*" # Bitwarden - "com.keepassx.*" # KeePassXC - "org.keepassx.*" # KeePassX + "com.keepassx.*" # KeePassXC (Legacy) + "org.keepassx.*" # KeePassX + "org.keepassxc.*" # KeePassXC "com.authy.*" # Authy "com.yubico.*" # YubiKey Manager From ce8989d3e9b815dca55000dc4393e55d2e165957 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 06:44:51 +0000 Subject: [PATCH 7/7] chore: auto format code --- lib/core/app_protection.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index 8f68be5..5822c6a 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -85,9 +85,9 @@ readonly DATA_PROTECTED_BUNDLES=( "com.lastpass.*" # LastPass "com.dashlane.*" # Dashlane "com.bitwarden.*" # Bitwarden - "com.keepassx.*" # KeePassXC (Legacy) - "org.keepassx.*" # KeePassX - "org.keepassxc.*" # KeePassXC + "com.keepassx.*" # KeePassXC (Legacy) + "org.keepassx.*" # KeePassX + "org.keepassxc.*" # KeePassXC "com.authy.*" # Authy "com.yubico.*" # YubiKey Manager