1+ import chalk from "chalk" ;
2+ import readline from "node:readline"
3+ import { exec } from "node:child_process"
4+ import os from "node:os"
5+ import path from "node:path"
6+ import process , { stderr , stdout } from "node:process" ;
7+
8+ const icons = {
9+ sep : "" ,
10+ branch : "" ,
11+ ok : "✔" ,
12+ fail : "✖" ,
13+ folder : "" ,
14+ github : "" ,
15+ } ;
16+
17+ let lastExitCode = 0 ;
18+
19+ function bg ( hex ) { return chalk . bgHex ( hex ) ; }
20+ function fg ( hex ) { return chalk . hex ( hex ) ; }
21+
22+ function segment ( text , { fgColor = "#000000" , bgColor = "#ffffff" } = { } ) {
23+ return bg ( bgColor ) ( fg ( fgColor ) ( ` ${ text } ` ) ) ;
24+ }
25+
26+ function joinSegments ( segments ) {
27+ let out = "" ;
28+ for ( let i = 0 ; i < segments . length ; i ++ ) {
29+ const cur = segments [ i ] ;
30+ out += cur . render ;
31+
32+ const next = segments [ i + 1 ] ;
33+ if ( next ) {
34+ out += chalk . hex ( cur . bgColor ) . bgHex ( next . bgColor ) ( icons . sep ) ;
35+ } else {
36+ out += chalk . hex ( cur . bgColor ) ( icons . sep ) + chalk . reset ( "" ) ;
37+ }
38+ }
39+ return out ;
40+ }
41+
42+ function getCwdLabel ( ) {
43+ const cwd = process . cwd ( ) ;
44+ const home = os . homedir ( ) ;
45+ if ( cwd === home ) return "~" ;
46+ if ( cwd . startsWith ( home ) ) return "~" + cwd . slice ( home . length ) ;
47+ return cwd ;
48+ }
49+
50+ function getGitBranch ( ) {
51+ return new Promise ( ( resolve ) => {
52+ exec ( "git rev-parse --abbrev-ref HEAD" , { cwd : process . cwd ( ) } , ( err , stdout ) => {
53+ if ( err ) return resolve ( null ) ;
54+ const branch = stdout . trim ( ) ;
55+ resolve ( branch || null ) ;
56+ } ) ;
57+ } ) ;
58+ }
59+
60+ async function renderPrompt ( ) {
61+ const cwd = getCwdLabel ( ) ;
62+ const branch = await getGitBranch ( ) ;
63+
64+ const status = lastExitCode === 0
65+ ? `${ icons . ok } 0`
66+ : `${ icons . fail } ${ lastExitCode } ` ;
67+
68+ const segs = [ ] ;
69+
70+ segs . push ( {
71+ bgColor : lastExitCode === 0 ? "#6e40c9" : "#a371f7" ,
72+ render : segment ( status , { fgColor : "#ffffff" , bgColor : lastExitCode === 0 ? "#6e40c9" : "#a371f7" } ) ,
73+ } ) ;
74+
75+ segs . push ( {
76+ bgColor : "#7d4fd4" ,
77+ render : segment ( `${ icons . folder } ${ cwd } ` , { fgColor : "#ffffff" , bgColor : "#7d4fd4" } ) ,
78+ } )
79+
80+ if ( branch ) {
81+ segs . push ( {
82+ bgColor : "#a371f7" ,
83+ render : segment ( `${ icons . branch } ${ branch } ` , { fgColor : "#0d1117" , bgColor : "#a371f7" } ) ,
84+ } ) ;
85+ }
86+
87+ const line1 = joinSegments ( segs ) ;
88+ const line2 = chalk . hex ( "#a371f7" ) ( "❯ " ) ;
89+
90+ return `${ line1 } \n${ line2 } `
91+ }
92+
93+ readline . emitKeypressEvents ( process . stdin ) ;
94+ process . stdin . setRawMode ( true ) ;
95+
96+ let buffer = "" ;
97+ let firstDraw = true ;
98+
99+ function redraw ( promptStr ) {
100+ if ( firstDraw ) {
101+ firstDraw = false ;
102+ process . stdout . write ( promptStr + buffer ) ;
103+ } else {
104+ process . stdout . write ( "\x1b[2K\x1b[G" ) ;
105+ process . stdout . write ( "\x1b[1A\x1b[2K\x1b[G" ) ;
106+ process . stdout . write ( promptStr + buffer ) ;
107+ }
108+ }
109+
110+ async function runCommand ( cmd ) {
111+ return new Promise ( ( resolve ) => {
112+ exec ( cmd , { shell : "cmd.exe" , cwd : process . cwd ( ) } , ( err , stdout , stderr ) => {
113+ if ( stdout ) process . stdout . write ( stdout ) ;
114+ if ( stderr ) process . stderr . write ( stderr ) ;
115+ if ( err ) return resolve ( err . code ?? 1 ) ;
116+ resolve ( 0 ) ;
117+ } ) ;
118+ } ) ;
119+ }
120+
121+ async function loop ( ) {
122+ while ( true ) {
123+ buffer = "" ;
124+ firstDraw = true ;
125+ const promptStr = await renderPrompt ( ) ;
126+ redraw ( promptStr ) ;
127+
128+ const exitCode = await new Promise ( ( resolve ) => {
129+ function onKey ( str , key ) {
130+ if ( key ?. ctrl && key . name === "c" ) {
131+ process . stdout . write ( chalk . reset ( "\n" ) ) ;
132+ process . exit ( 0 ) ;
133+ }
134+
135+ if ( key ?. name === "return" ) {
136+ process . stdin . off ( "keypress" , onKey ) ;
137+ process . stdout . write ( "\n" ) ;
138+ return resolve ( "ENTER" ) ;
139+ }
140+
141+ if ( key ?. name === "backspace" ) {
142+ buffer = buffer . slice ( 0 , - 1 ) ;
143+ redraw ( promptStr ) ;
144+ return ;
145+ }
146+
147+ if ( key ?. name === "tab" ) {
148+ return ;
149+ }
150+
151+ if ( key ?. sequence && key . sequence . length === 1 ) {
152+ buffer += key . sequence ;
153+ redraw ( promptStr ) ;
154+ }
155+ }
156+
157+ process . stdin . on ( "keypress" , onKey ) ;
158+ } ) ;
159+
160+ const cmd = buffer . trim ( ) ;
161+
162+ if ( ! cmd ) {
163+ lastExitCode = 0 ;
164+ continue ;
165+ }
166+
167+ if ( cmd === "exit" ) process . exit ( 0 ) ;
168+
169+ if ( cmd === "xsh" || cmd === "xsh -x" ) {
170+ console . log ( chalk . hex ( "#a371f7" ) ( `\n\nwelcome to xsh! ` ) ) ;
171+ console . log ( chalk . gray ( `a simple custom shell written in node.js\n\n` ) ) ;
172+
173+ lastExitCode = 0 ;
174+ continue ;
175+ }
176+
177+ if ( cmd === "xsh --help" || cmd === "xsh -h" ) {
178+ console . log ( chalk . hex ( "#a371f7" ) ( `
179+ xsh - a simple custom shell written in node.js
180+
181+ usage:
182+ xsh [-x] show a welcome message
183+ xsh --help [-h] show this message
184+ xsh --config [-c] show config file path
185+ xsh --version [-v] show version info
186+ ` ) ) ;
187+
188+ lastExitCode = 0 ;
189+ continue ;
190+ }
191+
192+ if ( cmd === "xsh --version" || cmd === "xsh -v" ) {
193+ const pkg = JSON . parse ( await import ( "fs" ) . then ( m => m . promises . readFile ( "package.json" , "utf-8" ) ) ) ;
194+ console . log ( chalk . hex ( "#a371f7" ) ( `xsh version ${ pkg . version } ` ) ) ;
195+
196+ lastExitCode = 0 ;
197+ continue ;
198+ }
199+
200+ if ( cmd === "xsh --config" || cmd === "xsh -c" ) {
201+ const configPath = path . join ( os . homedir ( ) , ".xshrc" ) ;
202+ await import ( "fs" ) . then ( m => m . promises . writeFile ( configPath , "# xsh config file\n" , { flag : "a" } ) ) ;
203+ console . log ( chalk . hex ( "#a371f7" ) ( `xsh config file path: ${ configPath } ` ) ) ;
204+
205+ lastExitCode = 0 ;
206+ continue ;
207+ }
208+
209+ if ( cmd === "clear" ) {
210+ process . stdout . write ( "\x1b[2J\x1b[H" ) ;
211+ lastExitCode = 0 ;
212+ continue ;
213+ }
214+
215+ if ( cmd . startsWith ( "cd " ) ) {
216+ const target = cmd . slice ( 3 ) . trim ( ) . replace ( / ^ " ( .* ) " $ / , "$1" ) ;
217+ const next = path . isAbsolute ( target ) ? target : path . join ( process . cwd ( ) , target ) ;
218+
219+ try {
220+ process . chdir ( next ) ;
221+ lastExitCode = 0 ;
222+ } catch {
223+ console . error ( chalk . red ( `cd: no such dir ${ next } ` ) ) ;
224+ lastExitCode = 1 ;
225+ }
226+ continue ;
227+ }
228+
229+ lastExitCode = await runCommand ( cmd ) ;
230+ }
231+ }
232+
233+ loop ( ) . catch ( ( e ) => {
234+ console . error ( e ) ;
235+ process . exit ( 1 ) ;
236+ } )
0 commit comments