@ecmaos/coreutils 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/commands/history.d.ts +4 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +157 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/ls.js +1 -1
- package/dist/commands/tty.d.ts +4 -0
- package/dist/commands/tty.d.ts.map +1 -0
- package/dist/commands/tty.js +60 -0
- package/dist/commands/tty.js.map +1 -0
- package/dist/commands/vim.d.ts +4 -0
- package/dist/commands/vim.d.ts.map +1 -0
- package/dist/commands/vim.js +272 -0
- package/dist/commands/vim.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/commands/history.ts +172 -0
- package/src/commands/ls.ts +1 -1
- package/src/commands/tty.ts +68 -0
- package/src/commands/vim.ts +307 -0
- package/src/index.ts +9 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
3
|
+
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
4
|
+
import { writelnStderr } from '../shared/helpers.js'
|
|
5
|
+
|
|
6
|
+
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
7
|
+
const usage = `Usage: vim [OPTION]... [FILE]...
|
|
8
|
+
Vi IMproved - a text editor.
|
|
9
|
+
|
|
10
|
+
FILE file(s) to edit
|
|
11
|
+
--help, -h display this help and exit
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
vim file.txt edit file.txt
|
|
15
|
+
vim file1.txt file2.txt edit multiple files`
|
|
16
|
+
writelnStderr(process, terminal, usage)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
|
20
|
+
return new TerminalCommand({
|
|
21
|
+
command: 'vim',
|
|
22
|
+
description: 'Vi IMproved - a text editor',
|
|
23
|
+
kernel,
|
|
24
|
+
shell,
|
|
25
|
+
terminal,
|
|
26
|
+
run: async (pid: number, argv: string[]) => {
|
|
27
|
+
const process = kernel.processes.get(pid) as Process | undefined
|
|
28
|
+
|
|
29
|
+
if (!process) return 1
|
|
30
|
+
|
|
31
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
32
|
+
printUsage(process, terminal)
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { VimWasm, checkBrowserCompatibility } = await import('vim-wasm/vimwasm.js')
|
|
38
|
+
|
|
39
|
+
const compatibilityError = checkBrowserCompatibility()
|
|
40
|
+
if (compatibilityError !== undefined) {
|
|
41
|
+
await writelnStderr(process, terminal, `vim: ${compatibilityError}`)
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const files: string[] = []
|
|
46
|
+
for (const arg of argv) {
|
|
47
|
+
if (arg && !arg.startsWith('-')) {
|
|
48
|
+
files.push(arg)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (files.length === 0) {
|
|
53
|
+
await writelnStderr(process, terminal, 'vim: no file specified')
|
|
54
|
+
await writelnStderr(process, terminal, "Try 'vim --help' for more information.")
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fileContents: Record<string, string> = {}
|
|
59
|
+
const dirs = new Set<string>()
|
|
60
|
+
const cmdArgs: string[] = []
|
|
61
|
+
|
|
62
|
+
const cwd = shell.cwd
|
|
63
|
+
let currentCwd = cwd
|
|
64
|
+
while (currentCwd !== '/' && currentCwd !== '') {
|
|
65
|
+
dirs.add(currentCwd)
|
|
66
|
+
currentCwd = path.dirname(currentCwd)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const homeDir = shell.expandTilde('~')
|
|
70
|
+
if (homeDir && homeDir !== '~') {
|
|
71
|
+
let currentDir = homeDir
|
|
72
|
+
while (currentDir !== '/' && currentDir !== '') {
|
|
73
|
+
dirs.add(currentDir)
|
|
74
|
+
currentDir = path.dirname(currentDir)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const vimrcPath = path.join(homeDir, '.vim', 'vimrc')
|
|
78
|
+
const vimrcExists = await shell.context.fs.promises.exists(vimrcPath)
|
|
79
|
+
if (vimrcExists) {
|
|
80
|
+
const vimrcContent = await shell.context.fs.promises.readFile(vimrcPath, 'utf-8')
|
|
81
|
+
const vimrcVirtualPath = '/home/web_user/.vim/vimrc'
|
|
82
|
+
fileContents[vimrcVirtualPath] = vimrcContent
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const expandedPath = shell.expandTilde(file)
|
|
88
|
+
const fullPath = path.resolve(shell.cwd, expandedPath)
|
|
89
|
+
|
|
90
|
+
const exists = await shell.context.fs.promises.exists(fullPath)
|
|
91
|
+
|
|
92
|
+
if (exists) {
|
|
93
|
+
const stats = await shell.context.fs.promises.stat(fullPath)
|
|
94
|
+
if (stats.isDirectory()) {
|
|
95
|
+
await writelnStderr(process, terminal, `vim: ${file}: Is a directory`)
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const content = await shell.context.fs.promises.readFile(fullPath, 'utf-8')
|
|
100
|
+
fileContents[fullPath] = content
|
|
101
|
+
} else {
|
|
102
|
+
fileContents[fullPath] = ''
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const dir = path.dirname(fullPath)
|
|
106
|
+
let currentDir = dir
|
|
107
|
+
while (currentDir !== '/' && currentDir !== '') {
|
|
108
|
+
dirs.add(currentDir)
|
|
109
|
+
currentDir = path.dirname(currentDir)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cmdArgs.push(fullPath)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const emscriptenDefaultDirs = new Set(['/', '/tmp', '/home', '/home/web_user', '/home/web_user/.vim', '/dev'])
|
|
116
|
+
const dirsArray = Array.from(dirs)
|
|
117
|
+
.filter(d => !emscriptenDefaultDirs.has(d))
|
|
118
|
+
.sort((a, b) => a.length - b.length)
|
|
119
|
+
|
|
120
|
+
const container = document.createElement('div')
|
|
121
|
+
container.style.width = '100%'
|
|
122
|
+
container.style.height = '100%'
|
|
123
|
+
container.style.display = 'flex'
|
|
124
|
+
container.style.flexDirection = 'column'
|
|
125
|
+
container.style.background = '#1e1e1e'
|
|
126
|
+
container.style.overflow = 'hidden'
|
|
127
|
+
|
|
128
|
+
const canvas = document.createElement('canvas')
|
|
129
|
+
canvas.id = 'vim-canvas'
|
|
130
|
+
canvas.style.width = '100%'
|
|
131
|
+
canvas.style.height = '100%'
|
|
132
|
+
canvas.style.flex = '1'
|
|
133
|
+
|
|
134
|
+
const input = document.createElement('input')
|
|
135
|
+
input.id = 'vim-input'
|
|
136
|
+
input.type = 'text'
|
|
137
|
+
input.autocomplete = 'off'
|
|
138
|
+
input.autofocus = true
|
|
139
|
+
input.style.position = 'absolute'
|
|
140
|
+
input.style.left = '-9999px'
|
|
141
|
+
input.style.width = '1px'
|
|
142
|
+
input.style.height = '1px'
|
|
143
|
+
input.style.opacity = '0'
|
|
144
|
+
|
|
145
|
+
container.appendChild(canvas)
|
|
146
|
+
container.appendChild(input)
|
|
147
|
+
|
|
148
|
+
const firstFile = files.length > 0 ? files[0] : undefined
|
|
149
|
+
const windowTitle = firstFile ? path.basename(firstFile) : 'vim'
|
|
150
|
+
const win = kernel.windows.create({
|
|
151
|
+
title: windowTitle,
|
|
152
|
+
width: 900,
|
|
153
|
+
height: 700,
|
|
154
|
+
max: false
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
win.mount(container)
|
|
158
|
+
|
|
159
|
+
let workerScriptPath: string
|
|
160
|
+
try {
|
|
161
|
+
workerScriptPath = new URL('vim-wasm/vim.js', import.meta.url).href
|
|
162
|
+
} catch {
|
|
163
|
+
await writelnStderr(process, terminal, 'vim: failed to resolve worker script path. Please ensure vim-wasm is properly installed.')
|
|
164
|
+
win.close()
|
|
165
|
+
return 1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const vim = new VimWasm({
|
|
169
|
+
canvas,
|
|
170
|
+
input,
|
|
171
|
+
workerScriptPath
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
let exitCode = 0
|
|
175
|
+
let vimExited = false
|
|
176
|
+
let resizeObserver: ResizeObserver | null = null
|
|
177
|
+
|
|
178
|
+
vim.onFileExport = async (fullpath: string, contents: ArrayBuffer) => {
|
|
179
|
+
try {
|
|
180
|
+
const text = new TextDecoder().decode(contents)
|
|
181
|
+
await shell.context.fs.promises.writeFile(fullpath, text, 'utf-8')
|
|
182
|
+
} catch (error) {
|
|
183
|
+
await writelnStderr(process, terminal, `vim: error writing file ${fullpath}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
vim.onVimExit = (status: number) => {
|
|
188
|
+
vimExited = true
|
|
189
|
+
exitCode = status === 0 ? 0 : 1
|
|
190
|
+
if (resizeObserver) {
|
|
191
|
+
resizeObserver.disconnect()
|
|
192
|
+
resizeObserver = null
|
|
193
|
+
}
|
|
194
|
+
win.close()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
vim.onError = async (err: Error) => {
|
|
198
|
+
console.error('[vim] Error callback triggered:', err)
|
|
199
|
+
console.error('[vim] Error message:', err.message)
|
|
200
|
+
console.error('[vim] Error stack:', err.stack)
|
|
201
|
+
await writelnStderr(process, terminal, `vim: error: ${err.message}`)
|
|
202
|
+
if (!vimExited) {
|
|
203
|
+
exitCode = 1
|
|
204
|
+
if (resizeObserver) {
|
|
205
|
+
resizeObserver.disconnect()
|
|
206
|
+
resizeObserver = null
|
|
207
|
+
}
|
|
208
|
+
win.close()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
vim.onTitleUpdate = (title: string) => {
|
|
213
|
+
win.setTitle(title || windowTitle)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
vim.onVimInit = async () => {
|
|
217
|
+
try {
|
|
218
|
+
await vim.cmdline('autocmd BufWritePost * :export')
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('[vim] Failed to set up auto-export:', error)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const handleResize = () => {
|
|
224
|
+
if (vimExited || !vim.isRunning()) return
|
|
225
|
+
|
|
226
|
+
const rect = container.getBoundingClientRect()
|
|
227
|
+
const width = Math.floor(rect.width)
|
|
228
|
+
const height = Math.floor(rect.height)
|
|
229
|
+
|
|
230
|
+
if (width > 0 && height > 0) {
|
|
231
|
+
const dpr = window.devicePixelRatio || 1
|
|
232
|
+
canvas.width = width * dpr
|
|
233
|
+
canvas.height = height * dpr
|
|
234
|
+
|
|
235
|
+
vim.resize(width, height)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resizeObserver = new ResizeObserver(handleResize)
|
|
240
|
+
resizeObserver.observe(container)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const filePath of Object.keys(fileContents)) {
|
|
244
|
+
const parentDir = path.dirname(filePath)
|
|
245
|
+
if (!dirsArray.includes(parentDir)) {
|
|
246
|
+
console.warn(`[vim] WARNING: Parent directory ${parentDir} of file ${filePath} is not in dirs array!`)
|
|
247
|
+
}
|
|
248
|
+
if (dirsArray.includes(filePath)) {
|
|
249
|
+
console.error(`[vim] ERROR: File path ${filePath} is also in dirs array! This will cause ENOTDIR error.`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const dirPath of dirsArray) {
|
|
254
|
+
if (fileContents[dirPath] !== undefined) {
|
|
255
|
+
console.error(`[vim] ERROR: Directory path ${dirPath} is also in files object! This will cause ENOTDIR error.`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (let i = 0; i < dirsArray.length; i++) {
|
|
260
|
+
const dir = dirsArray[i]
|
|
261
|
+
if (dir) {
|
|
262
|
+
const parent = path.dirname(dir)
|
|
263
|
+
if (parent !== dir && !dirsArray.slice(0, i).includes(parent)) {
|
|
264
|
+
console.warn(`[vim] WARNING: Directory ${dir} has parent ${parent} that comes after it in the array!`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const startOptions = {
|
|
271
|
+
files: fileContents,
|
|
272
|
+
dirs: dirsArray,
|
|
273
|
+
cmdArgs,
|
|
274
|
+
debug: false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
vim.start(startOptions)
|
|
278
|
+
} catch (startError) {
|
|
279
|
+
console.error('[vim] Error calling vim.start():', startError)
|
|
280
|
+
await writelnStderr(process, terminal, `vim: failed to start: ${startError instanceof Error ? startError.message : 'Unknown error'}`)
|
|
281
|
+
if (startError instanceof Error && startError.stack) {
|
|
282
|
+
console.error('[vim] Start error stack:', startError.stack)
|
|
283
|
+
}
|
|
284
|
+
win.close()
|
|
285
|
+
return 1
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return new Promise<number>((resolve) => {
|
|
289
|
+
const checkExit = () => {
|
|
290
|
+
if (vimExited) {
|
|
291
|
+
resolve(exitCode)
|
|
292
|
+
} else {
|
|
293
|
+
setTimeout(checkExit, 100)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
checkExit()
|
|
297
|
+
})
|
|
298
|
+
} catch (error) {
|
|
299
|
+
await writelnStderr(process, terminal, `vim: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
300
|
+
if (error instanceof Error && error.stack) {
|
|
301
|
+
console.error(error.stack)
|
|
302
|
+
}
|
|
303
|
+
return 1
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { createCommand as createGrep } from './commands/grep.js'
|
|
|
21
21
|
import { createCommand as createGroups } from './commands/groups.js'
|
|
22
22
|
import { createCommand as createHash } from './commands/hash.js'
|
|
23
23
|
import { createCommand as createHead } from './commands/head.js'
|
|
24
|
+
import { createCommand as createHistory } from './commands/history.js'
|
|
24
25
|
import { createCommand as createHostname } from './commands/hostname.js'
|
|
25
26
|
import { createCommand as createLn } from './commands/ln.js'
|
|
26
27
|
import { createCommand as createLs } from './commands/ls.js'
|
|
@@ -85,6 +86,7 @@ import { createCommand as createTest } from './commands/test.js'
|
|
|
85
86
|
import { createCommand as createTime } from './commands/time.js'
|
|
86
87
|
import { createCommand as createTr } from './commands/tr.js'
|
|
87
88
|
import { createCommand as createTrue } from './commands/true.js'
|
|
89
|
+
import { createCommand as createTty } from './commands/tty.js'
|
|
88
90
|
import { createCommand as createUname } from './commands/uname.js'
|
|
89
91
|
import { createCommand as createUmount } from './commands/umount.js'
|
|
90
92
|
import { createCommand as createUnexpand } from './commands/unexpand.js'
|
|
@@ -99,6 +101,7 @@ import { createCommand as createZip } from './commands/zip.js'
|
|
|
99
101
|
import { createCommand as createUnzip } from './commands/unzip.js'
|
|
100
102
|
import { createCommand as createVideo } from './commands/video.js'
|
|
101
103
|
import { createCommand as createView } from './commands/view.js'
|
|
104
|
+
import { createCommand as createVim } from './commands/vim.js'
|
|
102
105
|
import { createCommand as createGit } from './commands/git.js'
|
|
103
106
|
|
|
104
107
|
// Export individual command factories
|
|
@@ -114,6 +117,7 @@ export { createCommand as createFetch } from './commands/fetch.js'
|
|
|
114
117
|
export { createCommand as createGrep } from './commands/grep.js'
|
|
115
118
|
export { createCommand as createGroups } from './commands/groups.js'
|
|
116
119
|
export { createCommand as createHash } from './commands/hash.js'
|
|
120
|
+
export { createCommand as createHistory } from './commands/history.js'
|
|
117
121
|
export { createCommand as createLn } from './commands/ln.js'
|
|
118
122
|
export { createCommand as createLs } from './commands/ls.js'
|
|
119
123
|
export { createCommand as createMkdir } from './commands/mkdir.js'
|
|
@@ -162,6 +166,7 @@ export { createCommand as createSleep } from './commands/sleep.js'
|
|
|
162
166
|
export { createCommand as createSort } from './commands/sort.js'
|
|
163
167
|
export { createCommand as createTest } from './commands/test.js'
|
|
164
168
|
export { createCommand as createTr } from './commands/tr.js'
|
|
169
|
+
export { createCommand as createTty } from './commands/tty.js'
|
|
165
170
|
export { createCommand as createUname } from './commands/uname.js'
|
|
166
171
|
export { createCommand as createUmount } from './commands/umount.js'
|
|
167
172
|
export { createCommand as createUptime } from './commands/uptime.js'
|
|
@@ -175,6 +180,7 @@ export { createCommand as createZip } from './commands/zip.js'
|
|
|
175
180
|
export { createCommand as createUnzip } from './commands/unzip.js'
|
|
176
181
|
export { createCommand as createVideo } from './commands/video.js'
|
|
177
182
|
export { createCommand as createView } from './commands/view.js'
|
|
183
|
+
export { createCommand as createVim } from './commands/vim.js'
|
|
178
184
|
export { createCommand as createGit } from './commands/git.js'
|
|
179
185
|
|
|
180
186
|
/**
|
|
@@ -197,6 +203,7 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
|
|
|
197
203
|
groups: createGroups(kernel, shell, terminal),
|
|
198
204
|
hash: createHash(kernel, shell, terminal),
|
|
199
205
|
head: createHead(kernel, shell, terminal),
|
|
206
|
+
history: createHistory(kernel, shell, terminal),
|
|
200
207
|
hostname: createHostname(kernel, shell, terminal),
|
|
201
208
|
ln: createLn(kernel, shell, terminal),
|
|
202
209
|
ls: createLs(kernel, shell, terminal),
|
|
@@ -261,6 +268,7 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
|
|
|
261
268
|
time: createTime(kernel, shell, terminal),
|
|
262
269
|
tr: createTr(kernel, shell, terminal),
|
|
263
270
|
true: createTrue(kernel, shell, terminal),
|
|
271
|
+
tty: createTty(kernel, shell, terminal),
|
|
264
272
|
uname: createUname(kernel, shell, terminal),
|
|
265
273
|
umount: createUmount(kernel, shell, terminal),
|
|
266
274
|
unexpand: createUnexpand(kernel, shell, terminal),
|
|
@@ -275,6 +283,7 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
|
|
|
275
283
|
unzip: createUnzip(kernel, shell, terminal),
|
|
276
284
|
video: createVideo(kernel, shell, terminal),
|
|
277
285
|
view: createView(kernel, shell, terminal),
|
|
286
|
+
vim: createVim(kernel, shell, terminal),
|
|
278
287
|
git: createGit(kernel, shell, terminal)
|
|
279
288
|
}
|
|
280
289
|
}
|