import { useState, useEffect, useRef } from "react"; import { Box, Text } from "ink"; import { useStore } from "../state/store.tsx"; import { ProgressBar } from "./ProgressBar.tsx"; import { palette } from "../ui/theme.ts"; import type { QuickFilter } from "../types/index.ts"; // Animated spinner for refresh indicator function RefreshSpinner() { const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const [frameIndex, setFrameIndex] = useState(0); useEffect(() => { const timer = setInterval(() => { setFrameIndex((prev) => (prev + 1) % frames.length); }, 80); return () => clearInterval(timer); }, []); return {frames[frameIndex]} Syncing; } interface StatusBarProps { projectCount: number; dirtyCount: number; unpushedCount: number; localOnlyCount?: number; githubOnlyCount?: number; syncedCount?: number; } const QUICK_FILTER_LABELS: Record = { all: null, dirty: "dirty", unpushed: "unpushed", "no-remote": "no-remote", "github-only": "github-only", "local-only": "local-only", private: "private", public: "public", archived: "archived", forks: "forks", }; export function StatusBar({ projectCount, dirtyCount, unpushedCount, localOnlyCount = 0, githubOnlyCount = 0, syncedCount = 0, }: StatusBarProps) { const { state } = useStore(); const { actionInProgress, actionProgress, message, error, sortBy, sortDirection, quickFilter, viewMode, isLoadingGitHub, githubError, isRefreshing, } = state; // Debounce + hysteresis for spinner: avoid flicker but keep visible for real work const [showRefreshSpinner, setShowRefreshSpinner] = useState(false); const visibleSinceRef = useRef(null); const timersRef = useRef<{ show?: ReturnType; hide?: ReturnType }>({}); useEffect(() => { const clearTimers = () => { if (timersRef.current.show) clearTimeout(timersRef.current.show); if (timersRef.current.hide) clearTimeout(timersRef.current.hide); timersRef.current = {}; }; const spinnerActive = actionInProgress || isLoadingGitHub || isRefreshing; // Immediate show for explicit actions; delayed show for background loading if (spinnerActive) { clearTimers(); if (actionInProgress) { if (!showRefreshSpinner) visibleSinceRef.current = Date.now(); setShowRefreshSpinner(true); return; } timersRef.current.show = setTimeout(() => { if (!showRefreshSpinner) visibleSinceRef.current = Date.now(); setShowRefreshSpinner(true); }, 400); return () => clearTimers(); } // spinnerActive is false: hide after minimum visible time (500ms) to prevent flicker clearTimers(); const elapsed = visibleSinceRef.current ? Date.now() - visibleSinceRef.current : 0; const remaining = elapsed < 500 ? 500 - elapsed : 0; timersRef.current.hide = setTimeout(() => { setShowRefreshSpinner(false); visibleSinceRef.current = null; }, remaining); return () => clearTimers(); }, [actionInProgress, isLoadingGitHub, isRefreshing, showRefreshSpinner]); const quickFilterLabel = QUICK_FILTER_LABELS[quickFilter]; const showLocalStats = viewMode === "local" || viewMode === "combined"; const showRemoteStats = viewMode === "github" || viewMode === "combined"; // Build static segments with separators for stability const segments: { text: string; color?: string }[] = []; segments.push({ text: `${projectCount} repos`, color: palette.text.muted }); if (showLocalStats && syncedCount > 0) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `${syncedCount} synced`, color: palette.text.info }); } if (showLocalStats && localOnlyCount > 0) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `${localOnlyCount} local`, color: palette.text.success }); } if (showRemoteStats && githubOnlyCount > 0) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `${githubOnlyCount} remote`, color: palette.text.muted }); } if (showLocalStats && dirtyCount > 0) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `${dirtyCount} dirty`, color: palette.text.warning }); } if (showLocalStats && unpushedCount > 0) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `${unpushedCount} unpushed`, color: palette.badge.push.bg }); } if (quickFilterLabel) { segments.push({ text: "•", color: palette.text.muted }); segments.push({ text: `[${quickFilterLabel}]`, color: palette.text.warning }); } // Show action in progress with progress bar if (actionInProgress) { if (actionProgress) { return ( ); } return ( {actionInProgress}... ); } const sortArrow = sortDirection === "desc" ? "↓" : "↑"; return ( {error && ⚠ {error}} {message && !error && {message}} {isLoadingGitHub && ⟳ GitHub} {showRefreshSpinner && } {githubError && !isLoadingGitHub && ⚠ {githubError}} {segments.map((seg, idx) => ( {seg.text} ))} sort: {sortBy} {sortArrow} Tab: local · github · all D:clone ?:help ); }