From 2b5dd3f44cd36fe64c1a513fe20553019ffd949d Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 14:16:29 +0800 Subject: [PATCH] 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)