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
);
}