@@ -51,6 +51,14 @@ export function getImportPreviewSettings(
5151 return applyImport ( current , validation . data , mode , validation . presentKeys ) ;
5252}
5353
54+ /**
55+ * Control characters as `\uXXXX`: an imported file's text is shown, never sent
56+ * to the terminal as escape sequences, and hidden characters stay visible.
57+ */
58+ export function escapeControlCharacters ( text : string ) : string {
59+ return text . replace ( / [ \x00 - \x1f \x7f - \x9f ] / g, char => `\\u${ char . charCodeAt ( 0 ) . toString ( 16 ) . padStart ( 4 , '0' ) } ` ) ;
60+ }
61+
5462function formatScalar ( value : unknown ) : string {
5563 if ( value === null || value === undefined ) {
5664 return 'none' ;
@@ -59,9 +67,25 @@ function formatScalar(value: unknown): string {
5967 return String ( value ) ;
6068 }
6169 if ( typeof value === 'string' ) {
62- return value || '(empty)' ;
70+ return escapeControlCharacters ( value ) || '(empty)' ;
6371 }
64- return JSON . stringify ( value ) ;
72+ return escapeControlCharacters ( JSON . stringify ( value ) ) ;
73+ }
74+
75+ function getCustomCommands ( settings : Settings ) : string [ ] {
76+ return settings . lines
77+ . flat ( )
78+ . filter ( widget => widget . type === 'custom-command' && widget . commandPath )
79+ . map ( widget => widget . commandPath ?? '' ) ;
80+ }
81+
82+ /**
83+ * Shell commands the imported settings would run that the current settings
84+ * don't: a Custom Command runs its command on every status line render.
85+ */
86+ export function getImportedCommands ( current : Settings , preview : Settings ) : string [ ] {
87+ const existing = new Set ( getCustomCommands ( current ) ) ;
88+ return [ ...new Set ( getCustomCommands ( preview ) . filter ( command => ! existing . has ( command ) ) ) ] ;
6589}
6690
6791function diffObject ( current : Record < string , unknown > , imported : Record < string , unknown > , prefix : string ) : DiffEntry [ ] {
@@ -139,8 +163,11 @@ export function ImportPreviewDialog({
139163 onCancel
140164} : ImportPreviewDialogProps ) : React . JSX . Element {
141165 const [ previewMode , setPreviewMode ] = useState < Exclude < ImportMode , 'cancel' > > ( 'replace' ) ;
166+ // The mode waiting for the user to confirm the shell commands it adds
167+ const [ pendingMode , setPendingMode ] = useState < Exclude < ImportMode , 'cancel' > | null > ( null ) ;
142168 const previewSettings = getImportPreviewSettings ( currentSettings , validation , previewMode ) ;
143169 const topLevelKeys = getImportPreviewKeys ( currentSettings , previewSettings ) ;
170+ const importedCommands = getImportedCommands ( currentSettings , previewSettings ) ;
144171
145172 const items : ListEntry < ImportMode > [ ] = [
146173 { label : 'Replace All' , value : 'replace' , description : 'Overwrite all settings with the imported config' } ,
@@ -152,9 +179,47 @@ export function ImportPreviewDialog({
152179 function handleSelect ( value : ImportMode | 'back' ) : void {
153180 if ( value === 'cancel' || value === 'back' ) {
154181 onCancel ( ) ;
155- } else {
156- onApply ( value ) ;
182+ return ;
183+ }
184+
185+ const commands = getImportedCommands ( currentSettings , getImportPreviewSettings ( currentSettings , validation , value ) ) ;
186+ if ( commands . length > 0 ) {
187+ setPendingMode ( value ) ;
188+ return ;
157189 }
190+ onApply ( value ) ;
191+ }
192+
193+ if ( pendingMode ) {
194+ const commands = getImportedCommands ( currentSettings , getImportPreviewSettings ( currentSettings , validation , pendingMode ) ) ;
195+ const confirmItems : ListEntry < 'cancel' | 'confirm' > [ ] = [
196+ { label : 'Cancel' , value : 'cancel' , description : 'Go back to the preview without importing' } ,
197+ { label : 'Apply and run these commands' , value : 'confirm' , description : 'Import the settings, including the commands above' }
198+ ] ;
199+
200+ return (
201+ < Box flexDirection = 'column' >
202+ < Text bold > Import Preview</ Text >
203+ < Text color = 'yellow' >
204+ { `Apply this import? It adds ${ commands . length === 1 ? 'a shell command' : `${ commands . length } shell commands` } that will run on every status line render:` }
205+ </ Text >
206+ < Box flexDirection = 'column' marginLeft = { 4 } >
207+ { commands . map ( ( command , index ) => (
208+ < Text key = { index } > { `$ ${ escapeControlCharacters ( command ) } ` } </ Text >
209+ ) ) }
210+ </ Box >
211+ < List
212+ items = { confirmItems }
213+ onSelect = { ( choice ) => {
214+ if ( choice === 'confirm' ) {
215+ onApply ( pendingMode ) ;
216+ return ;
217+ }
218+ setPendingMode ( null ) ;
219+ } }
220+ />
221+ </ Box >
222+ ) ;
158223 }
159224
160225 function handleSelectionChange ( value : ImportMode | 'back' ) : void {
@@ -186,7 +251,7 @@ export function ImportPreviewDialog({
186251 < Text > { ` ${ key } :` } </ Text >
187252 { entries . map ( ( e , i ) => (
188253 < Box key = { i } marginLeft = { 4 } >
189- < Text > { `${ e . path } : ` } </ Text >
254+ < Text > { `${ escapeControlCharacters ( e . path ) } : ` } </ Text >
190255 < Text color = 'red' > { formatScalar ( e . current ) } </ Text >
191256 < Text > { ' → ' } </ Text >
192257 < Text color = 'green' > { formatScalar ( e . imported ) } </ Text >
@@ -208,7 +273,7 @@ export function ImportPreviewDialog({
208273 < Text > { ` ${ key } :` } </ Text >
209274 { entries . map ( ( e , i ) => (
210275 < Box key = { i } marginLeft = { 4 } >
211- < Text > { `${ e . path } : ` } </ Text >
276+ < Text > { `${ escapeControlCharacters ( e . path ) } : ` } </ Text >
212277 < Text color = 'red' > { formatScalar ( e . current ) } </ Text >
213278 < Text > { ' → ' } </ Text >
214279 < Text color = 'green' > { formatScalar ( e . imported ) } </ Text >
@@ -221,7 +286,7 @@ export function ImportPreviewDialog({
221286
222287 diffRows . push (
223288 < Box key = { key } >
224- < Text > { ` ${ key } : ` } </ Text >
289+ < Text > { ` ${ escapeControlCharacters ( key ) } : ` } </ Text >
225290 < Text color = 'red' > { formatScalar ( current ) } </ Text >
226291 < Text > { ' → ' } </ Text >
227292 < Text color = 'green' > { formatScalar ( imported ) } </ Text >
@@ -236,6 +301,16 @@ export function ImportPreviewDialog({
236301 < Box flexDirection = 'column' >
237302 { diffRows }
238303 </ Box >
304+ { importedCommands . length > 0 && (
305+ < Box flexDirection = 'column' marginTop = { 1 } >
306+ < Text color = 'yellow' > This import adds shell commands that run on every status line render:</ Text >
307+ < Box flexDirection = 'column' marginLeft = { 4 } >
308+ { importedCommands . map ( ( command , index ) => (
309+ < Text key = { index } > { `$ ${ escapeControlCharacters ( command ) } ` } </ Text >
310+ ) ) }
311+ </ Box >
312+ </ Box >
313+ ) }
239314 < List
240315 items = { items }
241316 onSelect = { handleSelect }
0 commit comments