import { GIT_SHA_MODE_VALUES, WORKTREE_SUMMARY_MODE_VALUES } from "./config-options.js"; import type { SegmentFeature } from "./segment-feature.js"; import type { GlanceConfig, SegmentData, SegmentRenderContext } from "./types.js"; const POLL_INTERVALS = [5000, 15000, 30000, 60000] as const; function nextIn(current: T, values: readonly T[]): T { const index = values.indexOf(current); return values[(index + 1) % values.length] ?? values[0]!; } function nextNumber(current: number, values: readonly T[]): T { const index = values.indexOf(current as T); return values[(index + 1) % values.length] ?? values[0]!; } function onOff(value: boolean): string { return value ? "on" : "off"; } function formatPolling(ms: number): string { if (ms % 1000 === 0) return `${ms / 1000}s`; return `${ms}ms`; } function worktreeSummaryLabel(mode: GlanceConfig["git"]["worktreeSummary"]): string { return mode === "border-right" ? "border right" : "status"; } function worktreeHasVisibleSummary(ctx: SegmentRenderContext): boolean { return ctx.state.git.status !== "clean" && ctx.state.git.worktree.files > 0; } function worktreeStatusParts(ctx: SegmentRenderContext): string[] { if (ctx.config.git.worktreeSummary !== "status" || !worktreeHasVisibleSummary(ctx)) return []; const worktree = ctx.state.git.worktree; const parts = [`Δ${worktree.files}`]; if (worktree.additions !== null && worktree.deletions !== null) { parts.push(`+${worktree.additions}`, `−${worktree.deletions}`); } return parts; } function gitBranchLabel(ctx: SegmentRenderContext): string { const git = ctx.state.git; if (git.branch) { if (ctx.config.git.shaMode === "always" && git.sha) return `${git.branch} ${git.sha}`; return git.branch; } if (git.detached && git.sha && ctx.config.git.shaMode !== "off") return git.sha; return "HEAD"; } function gitStatusMark(ctx: SegmentRenderContext): string { const status = ctx.state.git.status; if (status === "conflict") return ctx.config.icons === "nerd" ? "⚠" : "!"; if (status === "dirty") return ctx.config.icons === "nerd" ? "●" : "*"; return ""; } function gitBaseBehindLabel(behind: number): string | undefined { return behind > 0 ? `main↓${behind}` : undefined; } function gitShowsUpstreamBehind(ctx: SegmentRenderContext): boolean { return ctx.config.git.showAheadBehind && ctx.state.git.behind > 0; } function gitBaseBehindPart(ctx: SegmentRenderContext): string | undefined { if (!ctx.config.git.showBaseBehind) return undefined; if (gitShowsUpstreamBehind(ctx)) return undefined; return gitBaseBehindLabel(ctx.state.git.baseBehind); } function gitStatusPart(ctx: SegmentRenderContext): string { const status = gitStatusMark(ctx); if (!status) return ""; if (ctx.state.git.status === "conflict") return status; if (!ctx.config.git.showDirty) return ""; if (worktreeHasVisibleSummary(ctx)) return ""; return status; } function gitDetailParts(ctx: SegmentRenderContext): string[] { const git = ctx.state.git; const parts: string[] = []; const status = gitStatusPart(ctx); if (status) parts.push(status); if (ctx.config.git.showAheadBehind) { if (git.ahead > 0) parts.push(`↑${git.ahead}`); if (git.behind > 0) parts.push(`↓${git.behind}`); } const baseBehind = gitBaseBehindPart(ctx); if (baseBehind) parts.push(baseBehind); parts.push(...worktreeStatusParts(ctx)); return parts; } function collectGit(ctx: SegmentRenderContext): SegmentData | undefined { const git = ctx.state.git; if (!git.repo) return undefined; const branch = gitBranchLabel(ctx); const parts = gitDetailParts(ctx); const status = gitStatusPart(ctx); const baseBehind = gitBaseBehindPart(ctx); const minimalParts = status ? [status] : baseBehind ? [baseBehind] : []; return { primary: branch, secondary: parts.join(" ") || undefined, display: { minimal: [branch, ...minimalParts].join(" ").trim(), }, }; } export const gitSegmentFeature = { id: "git", label: "Git", defaultEnabled: true, settings: [ { id: "git.dirtyMarker", label: "Dirty marker", hint: "Off when file counts show. Conflicts stay.", kind: "toggle", value: (config: GlanceConfig) => onOff(config.git.showDirty), mutate: (config: GlanceConfig) => { config.git.showDirty = !config.git.showDirty; }, }, { id: "git.aheadBehind", label: "Ahead / behind", hint: "Show upstream counts.", kind: "toggle", value: (config: GlanceConfig) => onOff(config.git.showAheadBehind), mutate: (config: GlanceConfig) => { config.git.showAheadBehind = !config.git.showAheadBehind; }, }, { id: "git.baseBehind", label: "Behind main", hint: "Show main↓N when this branch is behind origin/main.", kind: "toggle", value: (config: GlanceConfig) => onOff(config.git.showBaseBehind), mutate: (config: GlanceConfig) => { config.git.showBaseBehind = !config.git.showBaseBehind; }, }, { id: "git.sha", label: "SHA", hint: "Keep branches quiet unless enabled.", kind: "cycle", value: (config: GlanceConfig) => config.git.shaMode, mutate: (config: GlanceConfig) => { config.git.shaMode = nextIn(config.git.shaMode, GIT_SHA_MODE_VALUES); }, }, { id: "git.worktreeSummary", label: "Working tree", hint: "Keep file and +/− counts in the Git status, or pin them to the bottom-right border.", kind: "cycle", value: (config: GlanceConfig) => worktreeSummaryLabel(config.git.worktreeSummary), mutate: (config: GlanceConfig) => { config.git.worktreeSummary = nextIn(config.git.worktreeSummary, WORKTREE_SUMMARY_MODE_VALUES); }, }, { id: "git.polling", label: "Polling", hint: "Check external Git changes.", kind: "cycle", value: (config: GlanceConfig) => formatPolling(config.git.pollIntervalMs), mutate: (config: GlanceConfig) => { config.git.pollIntervalMs = nextNumber(config.git.pollIntervalMs, POLL_INTERVALS); }, }, ], collect: collectGit, } as const satisfies SegmentFeature;