@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.
@@ -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
  }