@@ -17,6 +17,11 @@ function runRootTerminal(term) {
1717 if ( term . _initialized && ! term . locked ) {
1818 switch ( e ) {
1919 case '\r' : // Enter
20+ // Reset tab state
21+ term . tabIndex = 0 ;
22+ term . tabOptions = [ ] ;
23+ term . tabBase = "" ;
24+
2025 var exitStatus ;
2126 term . currentLine = term . currentLine . trim ( ) ;
2227 const tokens = term . currentLine . split ( " " ) ;
@@ -51,25 +56,45 @@ function runRootTerminal(term) {
5156 }
5257 break ;
5358 case '\u0003' : // Ctrl+C
59+ // Reset tab state
60+ term . tabIndex = 0 ;
61+ term . tabOptions = [ ] ;
62+ term . tabBase = "" ;
63+
5464 term . prompt ( ) ;
5565 term . clearCurrentLine ( true ) ;
5666 break ;
5767 case '\u0008' : // Ctrl+H
5868 case '\u007F' : // Backspace (DEL)
69+ // Reset tab state
70+ term . tabIndex = 0 ;
71+ term . tabOptions = [ ] ;
72+ term . tabBase = "" ;
73+
5974 // Do not delete the prompt
6075 if ( term . pos ( ) > 0 ) {
6176 const newLine = term . currentLine . slice ( 0 , term . pos ( ) - 1 ) + term . currentLine . slice ( term . pos ( ) ) ;
6277 term . setCurrentLine ( newLine , true )
6378 }
6479 break ;
6580 case '\033[A' : // up
81+ // Reset tab state
82+ term . tabIndex = 0 ;
83+ term . tabOptions = [ ] ;
84+ term . tabBase = "" ;
85+
6686 var h = [ ...term . history ] . reverse ( ) ;
6787 if ( term . historyCursor < h . length - 1 ) {
6888 term . historyCursor += 1 ;
6989 term . setCurrentLine ( h [ term . historyCursor ] , false ) ;
7090 }
7191 break ;
7292 case '\033[B' : // down
93+ // Reset tab state
94+ term . tabIndex = 0 ;
95+ term . tabOptions = [ ] ;
96+ term . tabBase = "" ;
97+
7398 var h = [ ...term . history ] . reverse ( ) ;
7499 if ( term . historyCursor > 0 ) {
75100 term . historyCursor -= 1 ;
@@ -89,42 +114,78 @@ function runRootTerminal(term) {
89114 }
90115 break ;
91116 case '\t' : // tab
92- cmd = term . currentLine . split ( " " ) [ 0 ] ;
93- const rest = term . currentLine . slice ( cmd . length ) . trim ( ) ;
94- const autocompleteCmds = Object . keys ( commands ) . filter ( ( c ) => c . startsWith ( cmd ) ) ;
95- var autocompleteArgs ;
96-
97- // detect what to autocomplete
98- if ( autocompleteCmds && autocompleteCmds . length > 1 ) {
99- const oldLine = term . currentLine ;
100- term . stylePrint ( `\r\n${ autocompleteCmds . sort ( ) . join ( " " ) } ` ) ;
101- term . prompt ( ) ;
102- term . setCurrentLine ( oldLine ) ;
103- } else if ( [ "cat" , "tail" , "less" , "head" , "open" , "mv" , "cp" , "chown" , "chmod" ] . includes ( cmd ) ) {
104- autocompleteArgs = _filesHere ( ) . filter ( ( f ) => f . startsWith ( rest ) ) ;
105- } else if ( [ "whois" , "finger" , "groups" ] . includes ( cmd ) ) {
106- autocompleteArgs = Object . keys ( team ) . filter ( ( f ) => f . startsWith ( rest ) ) ;
107- } else if ( [ "man" , "woman" , "tldr" ] . includes ( cmd ) ) {
108- autocompleteArgs = Object . keys ( portfolio ) . filter ( ( f ) => f . startsWith ( rest ) ) ;
109- } else if ( [ "cd" ] . includes ( cmd ) ) {
110- autocompleteArgs = _filesHere ( ) . filter ( ( dir ) => dir . startsWith ( rest ) && ! _DIRS [ term . cwd ] . includes ( dir ) ) ;
117+ const tabParts = term . currentLine . split ( " " ) ;
118+ const tabCmd = tabParts [ 0 ] ;
119+ const tabRest = tabParts . slice ( 1 ) . join ( " " ) ;
120+
121+ // Check if we need to reset tab state (input changed)
122+ if ( term . tabBase !== term . currentLine ) {
123+ term . tabIndex = 0 ;
124+ term . tabOptions = [ ] ;
125+ term . tabBase = term . currentLine ;
126+
127+ // Get completions based on context
128+ if ( tabParts . length === 1 ) {
129+ // Completing command
130+ term . tabOptions = Object . keys ( commands ) . filter ( c => c . startsWith ( tabCmd ) ) . sort ( ) ;
131+ } else if ( [ "cat" , "tail" , "less" , "head" , "open" , "mv" , "cp" , "chown" , "chmod" , "ls" ] . includes ( tabCmd ) ) {
132+ term . tabOptions = _filesHere ( ) . filter ( f => f . startsWith ( tabRest ) ) . sort ( ) ;
133+ } else if ( [ "whois" , "finger" , "groups" ] . includes ( tabCmd ) ) {
134+ term . tabOptions = Object . keys ( team ) . filter ( f => f . startsWith ( tabRest ) ) . sort ( ) ;
135+ } else if ( [ "man" , "woman" , "tldr" ] . includes ( tabCmd ) ) {
136+ term . tabOptions = Object . keys ( portfolio ) . filter ( f => f . startsWith ( tabRest ) ) . sort ( ) ;
137+ } else if ( [ "cd" ] . includes ( tabCmd ) ) {
138+ term . tabOptions = _filesHere ( ) . filter ( dir => dir . startsWith ( tabRest ) && ! _DIRS [ term . cwd ] . includes ( dir ) ) . sort ( ) ;
139+ }
111140 }
112-
113- // do the autocompleting
114- if ( autocompleteArgs && autocompleteArgs . length > 1 ) {
115- const oldLine = term . currentLine ;
116- term . writeln ( `\r\n${ autocompleteArgs . join ( " " ) } ` ) ;
117- term . prompt ( ) ;
118- term . setCurrentLine ( oldLine ) ;
119- } else if ( commands [ cmd ] && autocompleteArgs && autocompleteArgs . length > 0 ) {
120- term . setCurrentLine ( `${ cmd } ${ autocompleteArgs [ 0 ] } ` ) ;
121- } else if ( commands [ cmd ] && autocompleteArgs && autocompleteArgs . length == 0 ) {
122- term . setCurrentLine ( `${ cmd } ${ rest } ` ) ;
123- } else if ( autocompleteCmds && autocompleteCmds . length == 1 ) {
124- term . setCurrentLine ( `${ autocompleteCmds [ 0 ] } ` ) ;
141+
142+ // Handle tab completion
143+ if ( term . tabOptions . length === 0 ) {
144+ // No completions
145+ } else if ( term . tabOptions . length === 1 ) {
146+ // Single match - complete it
147+ if ( tabParts . length === 1 ) {
148+ // Check if it's already an exact match (like typing "ls" completely)
149+ if ( tabCmd === term . tabOptions [ 0 ] ) {
150+ // Exact match - just add a space
151+ term . setCurrentLine ( `${ term . tabOptions [ 0 ] } ` ) ;
152+ } else {
153+ // Partial match - complete it
154+ term . setCurrentLine ( `${ term . tabOptions [ 0 ] } ` ) ;
155+ }
156+ } else {
157+ term . setCurrentLine ( `${ tabCmd } ${ term . tabOptions [ 0 ] } ` ) ;
158+ }
159+ term . tabBase = "" ;
160+ term . tabIndex = 0 ;
161+ term . tabOptions = [ ] ;
162+ } else {
163+ // Multiple matches
164+ if ( term . tabIndex === 0 ) {
165+ // First tab - show options
166+ term . writeln ( `\r\n${ term . tabOptions . join ( " " ) } ` ) ;
167+ term . prompt ( ) ;
168+ term . setCurrentLine ( term . currentLine ) ;
169+ term . tabIndex = 1 ;
170+ } else {
171+ // Cycling through options
172+ const option = term . tabOptions [ ( term . tabIndex - 1 ) % term . tabOptions . length ] ;
173+ if ( tabParts . length === 1 ) {
174+ term . setCurrentLine ( option ) ;
175+ } else {
176+ term . setCurrentLine ( `${ tabCmd } ${ option } ` ) ;
177+ }
178+ term . tabIndex ++ ;
179+ term . tabBase = term . currentLine ; // Update base to current selection
180+ }
125181 }
126182 break ;
127183 default : // Print all other characters
184+ // Reset tab state on any other key
185+ term . tabIndex = 0 ;
186+ term . tabOptions = [ ] ;
187+ term . tabBase = "" ;
188+
128189 const newLine = `${ term . currentLine . slice ( 0 , term . pos ( ) ) } ${ e } ${ term . currentLine . slice ( term . pos ( ) ) } ` ;
129190 term . setCurrentLine ( newLine , true ) ;
130191 break ;
0 commit comments