11/** @jsxImportSource @opentui /solid */
2- import { createSignal , onCleanup } from "solid-js"
2+ import type { TextRenderable } from "@opentui/core"
3+ import { onCleanup } from "solid-js"
34import type { Accessor } from "solid-js"
45import type { PromptContext } from "../../prompt/types"
56import type { VimLog } from "./log"
6- import type { VimMode } from "./state"
7+ import type { VimMode , VimStateListener } from "./state"
78
89const STATUS_SYNC_MS = 50
910
1011type VimStatusProps = {
1112 mode : Accessor < VimMode >
1213 pending : Accessor < string | undefined >
14+ subscribe : ( listener : VimStateListener ) => ( ) => void
1315 enabled : Accessor < boolean >
1416 theme : PromptContext [ "api" ] [ "theme" ] [ "current" ]
1517 pendingDisplayDelay ?: number
@@ -19,45 +21,63 @@ type VimStatusProps = {
1921}
2022
2123export function VimStatus ( props : VimStatusProps ) {
22- const [ mode , setMode ] = createSignal ( props . mode ( ) )
23- const [ pending , setPending ] = createSignal < string | undefined > ( )
24- const [ enabled , setEnabled ] = createSignal ( props . enabled ( ) )
24+ let modeText : TextRenderable | undefined
25+ let pendingText : TextRenderable | undefined
26+ let displayedMode = props . mode ( )
27+ let displayedPending : string | undefined
28+ let displayedEnabled = props . enabled ( )
2529 let pendingTimer : ReturnType < typeof setTimeout > | undefined
2630 let scheduledPending : string | undefined
2731
2832 const sync = ( ) => {
2933 const nextMode = props . mode ( )
3034 const nextPending = props . pending ( )
35+ let changed = false
3136
32- if ( mode ( ) !== nextMode ) {
33- props . log ?.( "status.sync" , { fromMode : mode ( ) , toMode : nextMode , fromPending : pending ( ) , toPending : pending ( ) } )
34- setMode ( nextMode )
35- props . requestRender ?. ( )
37+ if ( displayedMode !== nextMode ) {
38+ props . log ?.( "status.sync" , { fromMode : displayedMode , toMode : nextMode , fromPending : displayedPending , toPending : displayedPending } )
39+ displayedMode = nextMode
40+ changed = true
3641 }
3742
3843 const nextEnabled = props . enabled ( )
39- if ( enabled ( ) !== nextEnabled ) {
40- props . log ?.( "status.enabled" , { from : enabled ( ) , to : nextEnabled } )
41- setEnabled ( nextEnabled )
42- props . requestRender ?. ( )
44+ if ( displayedEnabled !== nextEnabled ) {
45+ props . log ?.( "status.enabled" , { from : displayedEnabled , to : nextEnabled } )
46+ displayedEnabled = nextEnabled
47+ changed = true
4348 }
4449
50+ if ( changed ) updateModeText ( )
4551 syncPending ( nextPending )
52+ if ( changed ) props . requestRender ?.( )
4653 }
4754
55+ const unsubscribe = props . subscribe ( sync )
4856 const timer = setInterval ( sync , STATUS_SYNC_MS )
4957 onCleanup ( ( ) => {
58+ unsubscribe ( )
5059 clearInterval ( timer )
5160 if ( pendingTimer ) clearTimeout ( pendingTimer )
5261 } )
5362
5463 return (
5564 < box paddingLeft = { 1 } paddingRight = { 1 } flexDirection = "row" >
56- { enabled ( ) && pending ( ) ? < text fg = { props . theme . info } > { pending ( ) } </ text > : undefined }
57- { enabled ( ) ? < text fg = { props . disabled ? props . theme . textMuted : mode ( ) === "insert" ? props . theme . success : props . theme . warning } > { modeLabel ( mode ( ) ) } </ text > : undefined }
65+ < text ref = { ( ref : TextRenderable ) => { pendingText = ref ; updatePendingText ( ) } } fg = { props . theme . info } > { displayedPending ? ` ${ displayedPending } ` : "" } </ text >
66+ < text ref = { ( ref : TextRenderable ) => { modeText = ref ; updateModeText ( ) } } fg = { modeColor ( displayedMode , displayedEnabled , props . theme , props . disabled ) } > { displayedEnabled ? modeLabel ( displayedMode ) : "" } </ text >
5867 </ box >
5968 )
6069
70+ function updateModeText ( ) {
71+ if ( ! modeText ) return
72+ modeText . content = displayedEnabled ? modeLabel ( displayedMode ) : ""
73+ modeText . fg = modeColor ( displayedMode , displayedEnabled , props . theme , props . disabled )
74+ }
75+
76+ function updatePendingText ( ) {
77+ if ( ! pendingText ) return
78+ pendingText . content = displayedEnabled && displayedPending ? `${ displayedPending } ` : ""
79+ }
80+
6181 function syncPending ( nextPending : string | undefined ) {
6282 if ( ! nextPending ) {
6383 if ( pendingTimer ) clearTimeout ( pendingTimer )
@@ -67,7 +87,7 @@ export function VimStatus(props: VimStatusProps) {
6787 return
6888 }
6989
70- if ( pending ( ) === nextPending ) return
90+ if ( displayedPending === nextPending ) return
7191 if ( scheduledPending === nextPending ) return
7292
7393 if ( pendingTimer ) clearTimeout ( pendingTimer )
@@ -87,13 +107,20 @@ export function VimStatus(props: VimStatusProps) {
87107 }
88108
89109 function setDisplayedPending ( nextPending : string | undefined ) {
90- if ( pending ( ) === nextPending ) return
91- props . log ?.( "status.sync" , { fromMode : mode ( ) , toMode : mode ( ) , fromPending : pending ( ) , toPending : nextPending } )
92- setPending ( nextPending )
110+ if ( displayedPending === nextPending ) return
111+ props . log ?.( "status.sync" , { fromMode : displayedMode , toMode : displayedMode , fromPending : displayedPending , toPending : nextPending } )
112+ displayedPending = nextPending
113+ updatePendingText ( )
93114 props . requestRender ?.( )
94115 }
95116}
96117
118+ function modeColor ( mode : VimMode , enabled : boolean , theme : PromptContext [ "api" ] [ "theme" ] [ "current" ] , disabled ?: boolean ) {
119+ if ( ! enabled ) return undefined
120+ if ( disabled ) return theme . textMuted
121+ return mode === "insert" ? theme . success : theme . warning
122+ }
123+
97124function modeLabel ( mode : VimMode ) {
98125 if ( mode === "visual" ) return "VISUAL"
99126 if ( mode === "visual-line" ) return "VISUAL LINE"
0 commit comments