/ src / components / Spinner.tsx
Spinner.tsx
  1  import { Box, Text } from 'ink'
  2  import * as React from 'react'
  3  import { useEffect, useRef, useState } from 'react'
  4  import { getTheme } from '../utils/theme.js'
  5  import { sample } from 'lodash-es'
  6  
  7  // NB: The third character in this string is an emoji that
  8  // renders on Windows consoles with a green background
  9  const CHARACTERS =
 10    process.platform === 'darwin'
 11      ? ['·', '✢', '✳', '∗', '✻', '✽']
 12      : ['·', '✢', '*', '∗', '✻', '✽']
 13  
 14  const MESSAGES = [
 15    'Accomplishing',
 16    'Actioning',
 17    'Actualizing',
 18    'Baking',
 19    'Brewing',
 20    'Calculating',
 21    'Cerebrating',
 22    'Churning',
 23    'Clauding',
 24    'Coalescing',
 25    'Cogitating',
 26    'Computing',
 27    'Conjuring',
 28    'Considering',
 29    'Cooking',
 30    'Crafting',
 31    'Creating',
 32    'Crunching',
 33    'Deliberating',
 34    'Determining',
 35    'Doing',
 36    'Effecting',
 37    'Finagling',
 38    'Forging',
 39    'Forming',
 40    'Generating',
 41    'Hatching',
 42    'Herding',
 43    'Honking',
 44    'Hustling',
 45    'Ideating',
 46    'Inferring',
 47    'Manifesting',
 48    'Marinating',
 49    'Moseying',
 50    'Mulling',
 51    'Mustering',
 52    'Musing',
 53    'Noodling',
 54    'Percolating',
 55    'Pondering',
 56    'Processing',
 57    'Puttering',
 58    'Reticulating',
 59    'Ruminating',
 60    'Schlepping',
 61    'Shucking',
 62    'Simmering',
 63    'Smooshing',
 64    'Spinning',
 65    'Stewing',
 66    'Synthesizing',
 67    'Thinking',
 68    'Transmuting',
 69    'Vibing',
 70    'Working',
 71  ]
 72  
 73  export function Spinner(): React.ReactNode {
 74    const frames = [...CHARACTERS, ...[...CHARACTERS].reverse()]
 75    const [frame, setFrame] = useState(0)
 76    const [elapsedTime, setElapsedTime] = useState(0)
 77    const message = useRef(sample(MESSAGES))
 78    const startTime = useRef(Date.now())
 79  
 80    useEffect(() => {
 81      const timer = setInterval(() => {
 82        setFrame(f => (f + 1) % frames.length)
 83      }, 120)
 84  
 85      return () => clearInterval(timer)
 86    }, [frames.length])
 87  
 88    useEffect(() => {
 89      const timer = setInterval(() => {
 90        setElapsedTime(Math.floor((Date.now() - startTime.current) / 1000))
 91      }, 1000)
 92  
 93      return () => clearInterval(timer)
 94    }, [])
 95  
 96    return (
 97      <Box flexDirection="row" marginTop={1}>
 98        <Box flexWrap="nowrap" height={1} width={2}>
 99          <Text color={getTheme().claude}>{frames[frame]}</Text>
100        </Box>
101        <Text color={getTheme().claude}>{message.current}… </Text>
102        <Text color={getTheme().secondaryText}>
103          ({elapsedTime}s · <Text bold>esc</Text> to interrupt)
104        </Text>
105      </Box>
106    )
107  }
108  
109  export function SimpleSpinner(): React.ReactNode {
110    const frames = [...CHARACTERS, ...[...CHARACTERS].reverse()]
111    const [frame, setFrame] = useState(0)
112  
113    useEffect(() => {
114      const timer = setInterval(() => {
115        setFrame(f => (f + 1) % frames.length)
116      }, 120)
117  
118      return () => clearInterval(timer)
119    }, [frames.length])
120  
121    return (
122      <Box flexWrap="nowrap" height={1} width={2}>
123        <Text color={getTheme().claude}>{frames[frame]}</Text>
124      </Box>
125    )
126  }