Skip to content

Commit 8900bee

Browse files
committed
init. create core of xsh
1 parent c10714e commit 8900bee

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

index.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
})

package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "xsh",
3+
"version": "1.0.0",
4+
"description": "xsh is a mordern windows shell",
5+
"keywords": [
6+
"terminal",
7+
"shell",
8+
"windows"
9+
],
10+
"homepage": "https://github.com/sanderhd/xsh#readme",
11+
"bugs": {
12+
"url": "https://github.com/sanderhd/xsh/issues"
13+
},
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/sanderhd/xsh.git"
17+
},
18+
"license": "MIT",
19+
"author": "sanderhd",
20+
"type": "module",
21+
"main": "index.js",
22+
"scripts": {
23+
"test": "xsh"
24+
},
25+
"bin": {
26+
"xsh": "./bin/index.js"
27+
},
28+
"dependencies": {
29+
"chalk": "^5.6.2"
30+
}
31+
}

0 commit comments

Comments
 (0)