@silvery/ui 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/package.json +71 -0
  2. package/src/animation/easing.ts +38 -0
  3. package/src/animation/index.ts +18 -0
  4. package/src/animation/useAnimation.ts +143 -0
  5. package/src/animation/useInterval.ts +39 -0
  6. package/src/animation/useLatest.ts +35 -0
  7. package/src/animation/useTimeout.ts +65 -0
  8. package/src/animation/useTransition.ts +110 -0
  9. package/src/animation.ts +24 -0
  10. package/src/ansi/index.ts +43 -0
  11. package/src/canvas/index.ts +169 -0
  12. package/src/cli/ansi.ts +85 -0
  13. package/src/cli/index.ts +39 -0
  14. package/src/cli/multi-progress.ts +340 -0
  15. package/src/cli/progress-bar.ts +222 -0
  16. package/src/cli/spinner.ts +275 -0
  17. package/src/components/Badge.tsx +54 -0
  18. package/src/components/Breadcrumb.tsx +72 -0
  19. package/src/components/Button.tsx +73 -0
  20. package/src/components/CommandPalette.tsx +186 -0
  21. package/src/components/Console.tsx +79 -0
  22. package/src/components/CursorLine.tsx +71 -0
  23. package/src/components/Divider.tsx +67 -0
  24. package/src/components/EditContextDisplay.tsx +164 -0
  25. package/src/components/ErrorBoundary.tsx +179 -0
  26. package/src/components/Form.tsx +86 -0
  27. package/src/components/GridCell.tsx +42 -0
  28. package/src/components/HorizontalVirtualList.tsx +375 -0
  29. package/src/components/ModalDialog.tsx +179 -0
  30. package/src/components/PickerDialog.tsx +208 -0
  31. package/src/components/PickerList.tsx +93 -0
  32. package/src/components/ProgressBar.tsx +126 -0
  33. package/src/components/Screen.tsx +78 -0
  34. package/src/components/ScrollbackList.tsx +92 -0
  35. package/src/components/ScrollbackView.tsx +390 -0
  36. package/src/components/SelectList.tsx +176 -0
  37. package/src/components/Skeleton.tsx +87 -0
  38. package/src/components/Spinner.tsx +64 -0
  39. package/src/components/SplitView.tsx +199 -0
  40. package/src/components/Table.tsx +139 -0
  41. package/src/components/Tabs.tsx +203 -0
  42. package/src/components/TextArea.tsx +264 -0
  43. package/src/components/TextInput.tsx +240 -0
  44. package/src/components/Toast.tsx +216 -0
  45. package/src/components/Toggle.tsx +73 -0
  46. package/src/components/Tooltip.tsx +60 -0
  47. package/src/components/TreeView.tsx +212 -0
  48. package/src/components/Typography.tsx +233 -0
  49. package/src/components/VirtualList.tsx +318 -0
  50. package/src/components/VirtualView.tsx +221 -0
  51. package/src/components/useReadline.ts +213 -0
  52. package/src/components/useTextArea.ts +648 -0
  53. package/src/components.ts +133 -0
  54. package/src/display/Table.tsx +179 -0
  55. package/src/display/index.ts +13 -0
  56. package/src/hooks/useTea.ts +133 -0
  57. package/src/image/Image.tsx +187 -0
  58. package/src/image/index.ts +15 -0
  59. package/src/image/kitty-graphics.ts +161 -0
  60. package/src/image/sixel-encoder.ts +194 -0
  61. package/src/images.ts +22 -0
  62. package/src/index.ts +34 -0
  63. package/src/input/Select.tsx +155 -0
  64. package/src/input/TextInput.tsx +227 -0
  65. package/src/input/index.ts +25 -0
  66. package/src/progress/als-context.ts +160 -0
  67. package/src/progress/declarative.ts +519 -0
  68. package/src/progress/index.ts +54 -0
  69. package/src/progress/step-node.ts +152 -0
  70. package/src/progress/steps.ts +425 -0
  71. package/src/progress/task.ts +138 -0
  72. package/src/progress/tasks.ts +216 -0
  73. package/src/react/ProgressBar.tsx +146 -0
  74. package/src/react/Spinner.tsx +74 -0
  75. package/src/react/Tasks.tsx +144 -0
  76. package/src/react/context.tsx +145 -0
  77. package/src/react/index.ts +30 -0
  78. package/src/types.ts +252 -0
  79. package/src/utils/eta.ts +155 -0
  80. package/src/utils/index.ts +13 -0
  81. package/src/wrappers/index.ts +36 -0
  82. package/src/wrappers/with-progress.ts +250 -0
  83. package/src/wrappers/with-select.ts +194 -0
  84. package/src/wrappers/with-spinner.ts +108 -0
  85. package/src/wrappers/with-text-input.ts +388 -0
  86. package/src/wrappers/wrap-emitter.ts +158 -0
  87. package/src/wrappers/wrap-generator.ts +143 -0
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@silvery/ui",
3
+ "version": "0.3.0",
4
+ "description": "Component library for silvery — Box, Text, ScrollView, VirtualList, and more",
5
+ "license": "MIT",
6
+ "author": "Bjørn Stabell <bjorn@stabell.org>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/beorn/silvery.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "type": "module",
16
+ "main": "src/index.ts",
17
+ "types": "src/index.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.ts",
21
+ "import": "./src/index.ts"
22
+ },
23
+ "./cli": {
24
+ "types": "./src/cli/index.ts",
25
+ "import": "./src/cli/index.ts"
26
+ },
27
+ "./react": {
28
+ "types": "./src/react/index.ts",
29
+ "import": "./src/react/index.ts"
30
+ },
31
+ "./wrappers": {
32
+ "types": "./src/wrappers/index.ts",
33
+ "import": "./src/wrappers/index.ts"
34
+ },
35
+ "./ansi": {
36
+ "types": "./src/ansi/index.ts",
37
+ "import": "./src/ansi/index.ts"
38
+ },
39
+ "./utils": {
40
+ "types": "./src/utils/index.ts",
41
+ "import": "./src/utils/index.ts"
42
+ },
43
+ "./progress": {
44
+ "types": "./src/progress/index.ts",
45
+ "import": "./src/progress/index.ts"
46
+ },
47
+ "./display": {
48
+ "types": "./src/display/index.ts",
49
+ "import": "./src/display/index.ts"
50
+ },
51
+ "./input": {
52
+ "types": "./src/input/index.ts",
53
+ "import": "./src/input/index.ts"
54
+ },
55
+ "./animation": {
56
+ "types": "./src/animation/index.ts",
57
+ "import": "./src/animation/index.ts"
58
+ },
59
+ "./*": {
60
+ "types": "./src/*",
61
+ "import": "./src/*"
62
+ }
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "dependencies": {
68
+ "@silvery/react": "workspace:*",
69
+ "@silvery/theme": "workspace:*"
70
+ }
71
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Easing Functions
3
+ *
4
+ * Maps time progress (0-1) to value progress (0-1) for smooth animations.
5
+ * Includes common presets and a resolver for name-or-function usage.
6
+ */
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ /** Easing function: maps time progress (0-1) to value progress (0-1) */
13
+ export type EasingFn = (t: number) => number
14
+
15
+ // ============================================================================
16
+ // Presets
17
+ // ============================================================================
18
+
19
+ export const easings = {
20
+ linear: (t: number) => t,
21
+ ease: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
22
+ easeIn: (t: number) => t * t,
23
+ easeOut: (t: number) => t * (2 - t),
24
+ easeInOut: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
25
+ easeInCubic: (t: number) => t * t * t,
26
+ easeOutCubic: (t: number) => --t * t * t + 1,
27
+ } as const satisfies Record<string, EasingFn>
28
+
29
+ export type EasingName = keyof typeof easings
30
+
31
+ // ============================================================================
32
+ // Resolver
33
+ // ============================================================================
34
+
35
+ /** Resolve an easing — accepts a name string or a custom function. */
36
+ export function resolveEasing(easing: EasingName | EasingFn): EasingFn {
37
+ return typeof easing === "function" ? easing : easings[easing]
38
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Animation Utilities
3
+ *
4
+ * Hooks and helpers for smooth terminal UI animations at ~30fps.
5
+ */
6
+
7
+ // Easing
8
+ export { easings, resolveEasing } from "./easing"
9
+ export type { EasingFn, EasingName } from "./easing"
10
+
11
+ // Hooks
12
+ export { useAnimation } from "./useAnimation"
13
+ export type { UseAnimationOptions, UseAnimationResult } from "./useAnimation"
14
+ export { useTransition } from "./useTransition"
15
+ export type { UseTransitionOptions } from "./useTransition"
16
+ export { useInterval } from "./useInterval"
17
+ export { useTimeout } from "./useTimeout"
18
+ export { useLatest } from "./useLatest"
@@ -0,0 +1,143 @@
1
+ /**
2
+ * useAnimation - Animate a value from 0 to 1 over a duration.
3
+ *
4
+ * Drives a single animation cycle with configurable easing, delay,
5
+ * and completion callback. Targets ~30fps (33ms interval) since
6
+ * terminals don't benefit from higher refresh rates.
7
+ */
8
+
9
+ import { useState, useEffect, useRef, useCallback } from "react"
10
+ import { resolveEasing, type EasingName, type EasingFn } from "./easing"
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface UseAnimationOptions {
17
+ /** Duration in milliseconds */
18
+ duration: number
19
+ /** Easing function or preset name */
20
+ easing?: EasingName | EasingFn
21
+ /** Delay before starting (ms) */
22
+ delay?: number
23
+ /** Called when animation completes */
24
+ onComplete?: () => void
25
+ /** Whether to run the animation (default: true) */
26
+ enabled?: boolean
27
+ }
28
+
29
+ export interface UseAnimationResult {
30
+ /** Current progress value (0 to 1, eased) */
31
+ value: number
32
+ /** Whether the animation is still running */
33
+ isAnimating: boolean
34
+ /** Reset and replay the animation */
35
+ reset: () => void
36
+ }
37
+
38
+ // ============================================================================
39
+ // Constants
40
+ // ============================================================================
41
+
42
+ /** ~30fps tick interval for terminal animations */
43
+ const TICK_MS = 33
44
+
45
+ // ============================================================================
46
+ // Hook
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Animate a value from 0 to 1 over a duration with easing.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * function FadeIn({ children }) {
55
+ * const { value } = useAnimation({ duration: 300, easing: "easeOut" })
56
+ * return <Text dimColor={value < 1}>{children}</Text>
57
+ * }
58
+ * ```
59
+ */
60
+ export function useAnimation(options: UseAnimationOptions): UseAnimationResult {
61
+ const { duration, easing = "linear", delay = 0, onComplete, enabled = true } = options
62
+
63
+ const [value, setValue] = useState(0)
64
+ const [isAnimating, setIsAnimating] = useState(false)
65
+
66
+ const startTimeRef = useRef(0)
67
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
68
+ const onCompleteRef = useRef(onComplete)
69
+ onCompleteRef.current = onComplete
70
+
71
+ // Epoch bumps on each reset to invalidate stale intervals
72
+ const epochRef = useRef(0)
73
+
74
+ const easingFn = resolveEasing(easing)
75
+
76
+ const stopInterval = useCallback(() => {
77
+ if (intervalRef.current !== null) {
78
+ clearInterval(intervalRef.current)
79
+ intervalRef.current = null
80
+ }
81
+ }, [])
82
+
83
+ const startAnimation = useCallback(() => {
84
+ stopInterval()
85
+ epochRef.current++
86
+ const epoch = epochRef.current
87
+
88
+ setValue(0)
89
+ setIsAnimating(true)
90
+
91
+ const begin = () => {
92
+ // Guard against stale starts after a reset
93
+ if (epochRef.current !== epoch) return
94
+
95
+ startTimeRef.current = performance.now()
96
+
97
+ intervalRef.current = setInterval(() => {
98
+ // Guard against stale ticks after a reset
99
+ if (epochRef.current !== epoch) return
100
+
101
+ const elapsed = performance.now() - startTimeRef.current
102
+ const raw = Math.min(elapsed / duration, 1)
103
+ const eased = easingFn(raw)
104
+
105
+ setValue(eased)
106
+
107
+ if (raw >= 1) {
108
+ stopInterval()
109
+ setIsAnimating(false)
110
+ onCompleteRef.current?.()
111
+ }
112
+ }, TICK_MS)
113
+ }
114
+
115
+ if (delay > 0) {
116
+ setTimeout(() => begin(), delay)
117
+ } else {
118
+ begin()
119
+ }
120
+ }, [duration, delay, easingFn, stopInterval])
121
+
122
+ // Start on mount (if enabled)
123
+ useEffect(() => {
124
+ if (!enabled) {
125
+ stopInterval()
126
+ setValue(0)
127
+ setIsAnimating(false)
128
+ return
129
+ }
130
+
131
+ startAnimation()
132
+
133
+ return () => {
134
+ stopInterval()
135
+ }
136
+ }, [enabled, startAnimation, stopInterval])
137
+
138
+ const reset = useCallback(() => {
139
+ startAnimation()
140
+ }, [startAnimation])
141
+
142
+ return { value, isAnimating, reset }
143
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * useInterval - Run a callback on a fixed interval.
3
+ *
4
+ * Uses Dan Abramov's ref pattern to avoid stale closures.
5
+ * The callback is NOT called on mount — only on subsequent ticks.
6
+ */
7
+
8
+ import { useEffect, useRef } from "react"
9
+
10
+ // ============================================================================
11
+ // Hook
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Run a callback on a fixed interval.
16
+ *
17
+ * The callback is NOT called on mount — only on ticks after the interval
18
+ * elapses. Uses a ref for the callback to avoid stale closures.
19
+ *
20
+ * @param callback - Function to call on each tick
21
+ * @param ms - Interval in milliseconds
22
+ * @param enabled - Whether the interval is active (default: true)
23
+ */
24
+ export function useInterval(callback: () => void, ms: number, enabled = true): void {
25
+ const callbackRef = useRef(callback)
26
+ callbackRef.current = callback
27
+
28
+ useEffect(() => {
29
+ if (!enabled) return
30
+
31
+ const id = setInterval(() => {
32
+ callbackRef.current()
33
+ }, ms)
34
+
35
+ return () => {
36
+ clearInterval(id)
37
+ }
38
+ }, [ms, enabled])
39
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * useLatest - Always-current ref to a value.
3
+ *
4
+ * The classic React pattern for avoiding stale closures in callbacks,
5
+ * timers, and effects. Returns a ref whose `.current` is always the
6
+ * latest value — safe to read from any async context.
7
+ *
8
+ * ```tsx
9
+ * const countRef = useLatest(count)
10
+ * useInterval(() => {
11
+ * console.log(countRef.current) // always fresh
12
+ * }, 1000)
13
+ * ```
14
+ */
15
+
16
+ import { useRef } from "react"
17
+
18
+ // ============================================================================
19
+ // Hook
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Returns a ref that always holds the latest value.
24
+ *
25
+ * Useful when a callback needs access to current state/props without
26
+ * re-creating the callback (which would reset timers, event listeners, etc).
27
+ *
28
+ * @param value - The value to track
29
+ * @returns A ref whose `.current` is always `value`
30
+ */
31
+ export function useLatest<T>(value: T): { readonly current: T } {
32
+ const ref = useRef(value)
33
+ ref.current = value
34
+ return ref
35
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * useTimeout - Run a callback after a delay.
3
+ *
4
+ * Uses a ref for the callback to avoid stale closures (Dan Abramov pattern).
5
+ * The timer resets when `ms` or `enabled` changes. When `enabled` becomes false,
6
+ * the timer is cleared. Returns a `reset` function to restart the timer.
7
+ *
8
+ * Unlike useInterval, this fires exactly once per enable/reset cycle.
9
+ */
10
+
11
+ import { useCallback, useEffect, useRef } from "react"
12
+
13
+ // ============================================================================
14
+ // Hook
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Run a callback after a delay.
19
+ *
20
+ * The callback fires once after `ms` milliseconds. The timer resets when
21
+ * `ms` or `enabled` changes. Returns `{ reset, clear }` for manual control.
22
+ *
23
+ * @param callback - Function to call when the timer fires
24
+ * @param ms - Delay in milliseconds
25
+ * @param enabled - Whether the timer is active (default: true)
26
+ */
27
+ export function useTimeout(callback: () => void, ms: number, enabled = true): { reset: () => void; clear: () => void } {
28
+ const callbackRef = useRef(callback)
29
+ callbackRef.current = callback
30
+
31
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
32
+
33
+ const clear = useCallback(() => {
34
+ if (timerRef.current !== null) {
35
+ clearTimeout(timerRef.current)
36
+ timerRef.current = null
37
+ }
38
+ }, [])
39
+
40
+ const reset = useCallback(() => {
41
+ clear()
42
+ if (enabled) {
43
+ timerRef.current = setTimeout(() => {
44
+ timerRef.current = null
45
+ callbackRef.current()
46
+ }, ms)
47
+ }
48
+ }, [ms, enabled, clear])
49
+
50
+ useEffect(() => {
51
+ if (!enabled) {
52
+ clear()
53
+ return
54
+ }
55
+
56
+ timerRef.current = setTimeout(() => {
57
+ timerRef.current = null
58
+ callbackRef.current()
59
+ }, ms)
60
+
61
+ return clear
62
+ }, [ms, enabled, clear])
63
+
64
+ return { reset, clear }
65
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * useTransition - Smoothly interpolate between numeric values.
3
+ *
4
+ * When the target value changes, animates from the current value toward
5
+ * the new target. If the target changes mid-animation, restarts from
6
+ * the current interpolated position. Targets ~30fps.
7
+ */
8
+
9
+ import { useState, useEffect, useRef } from "react"
10
+ import { resolveEasing, type EasingName, type EasingFn } from "./easing"
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ export interface UseTransitionOptions {
17
+ /** Duration in milliseconds (default: 300) */
18
+ duration?: number
19
+ /** Easing function or preset name (default: "easeOut") */
20
+ easing?: EasingName | EasingFn
21
+ }
22
+
23
+ // ============================================================================
24
+ // Constants
25
+ // ============================================================================
26
+
27
+ /** ~30fps tick interval for terminal animations */
28
+ const TICK_MS = 33
29
+
30
+ // ============================================================================
31
+ // Hook
32
+ // ============================================================================
33
+
34
+ /**
35
+ * Smoothly interpolate when the target value changes.
36
+ *
37
+ * Returns the current interpolated value. On the first render, returns
38
+ * the target value immediately (no animation). Subsequent changes
39
+ * animate from the previous value to the new target.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * function ScrollOffset({ target }) {
44
+ * const smooth = useTransition(target, { duration: 200, easing: "easeOut" })
45
+ * return <Box marginTop={Math.round(smooth)}>...</Box>
46
+ * }
47
+ * ```
48
+ */
49
+ export function useTransition(targetValue: number, options?: UseTransitionOptions): number {
50
+ const { duration = 300, easing = "easeOut" } = options ?? {}
51
+
52
+ const [currentValue, setCurrentValue] = useState(targetValue)
53
+
54
+ const fromRef = useRef(targetValue)
55
+ const toRef = useRef(targetValue)
56
+ const startTimeRef = useRef(0)
57
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
58
+ const isFirstRef = useRef(true)
59
+
60
+ const easingFn = resolveEasing(easing)
61
+
62
+ useEffect(() => {
63
+ // On first render, snap to target without animation
64
+ if (isFirstRef.current) {
65
+ isFirstRef.current = false
66
+ return
67
+ }
68
+
69
+ // If target hasn't changed, nothing to do
70
+ if (targetValue === toRef.current) return
71
+
72
+ // Start from wherever we currently are
73
+ fromRef.current = currentValue
74
+ toRef.current = targetValue
75
+ startTimeRef.current = performance.now()
76
+
77
+ // Clear any existing interval
78
+ if (intervalRef.current !== null) {
79
+ clearInterval(intervalRef.current)
80
+ }
81
+
82
+ intervalRef.current = setInterval(() => {
83
+ const elapsed = performance.now() - startTimeRef.current
84
+ const raw = Math.min(elapsed / duration, 1)
85
+ const eased = easingFn(raw)
86
+ const interpolated = fromRef.current + (toRef.current - fromRef.current) * eased
87
+
88
+ setCurrentValue(interpolated)
89
+
90
+ if (raw >= 1) {
91
+ // Snap to exact target and stop
92
+ setCurrentValue(toRef.current)
93
+ if (intervalRef.current !== null) {
94
+ clearInterval(intervalRef.current)
95
+ intervalRef.current = null
96
+ }
97
+ }
98
+ }, TICK_MS)
99
+
100
+ return () => {
101
+ if (intervalRef.current !== null) {
102
+ clearInterval(intervalRef.current)
103
+ intervalRef.current = null
104
+ }
105
+ }
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [targetValue, duration, easingFn])
108
+
109
+ return currentValue
110
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * silvery/animation -- Smooth terminal UI animations at ~30fps.
3
+ *
4
+ * ```tsx
5
+ * import { useAnimation, easings } from '@silvery/ui/animation'
6
+ *
7
+ * function FadeIn() {
8
+ * const { value } = useAnimation({ duration: 300, easing: "easeOut" })
9
+ * return <Text dimColor={value < 1}>Hello</Text>
10
+ * }
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ export { easings, resolveEasing, useAnimation, useInterval, useTimeout, useLatest } from "./animation/index"
17
+ export { useTransition as useAnimatedTransition } from "./animation/index"
18
+ export type {
19
+ EasingFn,
20
+ EasingName,
21
+ UseAnimationOptions,
22
+ UseAnimationResult,
23
+ UseTransitionOptions,
24
+ } from "./animation/index"
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ANSI terminal control utilities
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import {
7
+ * CURSOR_HIDE,
8
+ * CURSOR_SHOW,
9
+ * CLEAR_LINE,
10
+ * write,
11
+ * isTTY,
12
+ * } from "@silvery/ui/ansi";
13
+ *
14
+ * if (isTTY()) {
15
+ * write(CURSOR_HIDE);
16
+ * // ... do work ...
17
+ * write(CURSOR_SHOW);
18
+ * }
19
+ * ```
20
+ */
21
+
22
+ // Re-export all from cli/ansi.ts
23
+ export {
24
+ // Cursor control
25
+ CURSOR_HIDE,
26
+ CURSOR_SHOW,
27
+ CURSOR_TO_START,
28
+ CURSOR_SAVE,
29
+ CURSOR_RESTORE,
30
+ cursorUp,
31
+ cursorDown,
32
+ // Line/screen clearing
33
+ CLEAR_LINE,
34
+ CLEAR_LINE_END,
35
+ CLEAR_SCREEN,
36
+ // Writing utilities
37
+ write,
38
+ writeLine,
39
+ withCursor,
40
+ // Terminal detection
41
+ isTTY,
42
+ getTerminalWidth,
43
+ } from "../cli/ansi"