-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
clikt/src/wasmJsMain/kotlin/com/github/ajalt/clikt/mpp/MppImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package com.github.ajalt.clikt.mpp | ||
|
||
|
||
private external interface FsModule { | ||
fun readFileSync(path: String, encoding: String): String | ||
} | ||
|
||
@Suppress("ClassName") | ||
private external object process { | ||
val platform: String | ||
fun exit(status: Int) | ||
} | ||
|
||
|
||
@Suppress("RedundantNullableReturnType") // invalid diagnostic due to KTIJ-28239 | ||
private fun nodeReadEnvvar(@Suppress("UNUSED_PARAMETER") key: String): String? = | ||
js("process.env[key]") | ||
|
||
private interface JsMppImpls { | ||
fun readEnvvar(key: String): String? | ||
fun isWindowsMpp(): Boolean | ||
fun exitProcessMpp(status: Int) | ||
fun readFileIfExists(filename: String): String? | ||
} | ||
|
||
private object BrowserMppImpls : JsMppImpls { | ||
override fun readEnvvar(key: String): String? = null | ||
override fun isWindowsMpp(): Boolean = false | ||
override fun exitProcessMpp(status: Int) {} | ||
override fun readFileIfExists(filename: String): String? = null | ||
} | ||
|
||
private class NodeMppImpls(private val fs: FsModule) : JsMppImpls { | ||
override fun readEnvvar(key: String): String? = nodeReadEnvvar(key) | ||
override fun isWindowsMpp(): Boolean = process.platform == "win32" | ||
override fun exitProcessMpp(status: Int): Unit = process.exit(status) | ||
override fun readFileIfExists(filename: String): String? { | ||
return try { | ||
fs.readFileSync(filename, "utf-8") | ||
} catch (e: Throwable) { | ||
null | ||
} | ||
} | ||
} | ||
|
||
// See jsMain/MppImpl.kt for the details of node detection | ||
private fun runningOnNode(): Boolean = | ||
js("Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'") | ||
|
||
|
||
private fun importNodeFsModule(): FsModule = | ||
js("""require("fs")""") | ||
|
||
private val impls: JsMppImpls = when { | ||
runningOnNode() -> NodeMppImpls(importNodeFsModule()) | ||
else -> BrowserMppImpls | ||
} | ||
|
||
internal actual val String.graphemeLengthMpp: Int get() = replace(ANSI_CODE_RE, "").length | ||
|
||
internal actual fun readEnvvar(key: String): String? = impls.readEnvvar(key) | ||
internal actual fun isWindowsMpp(): Boolean = impls.isWindowsMpp() | ||
internal actual fun exitProcessMpp(status: Int): Unit = impls.exitProcessMpp(status) | ||
internal actual fun readFileIfExists(filename: String): String? = impls.readFileIfExists(filename) |
158 changes: 158 additions & 0 deletions
158
clikt/src/wasmJsMain/kotlin/com/github/ajalt/clikt/output/EditorWasm.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package com.github.ajalt.clikt.output | ||
|
||
import com.github.ajalt.clikt.core.CliktError | ||
import com.github.ajalt.clikt.mpp.readEnvvar | ||
|
||
@JsName("Object") | ||
external class JsObject : JsAny { | ||
operator fun get(key: JsString): JsAny? | ||
operator fun set(key: JsString, value: JsAny?) | ||
} | ||
|
||
// Can't use the wasm-interop definition because it only has get and set methods | ||
@JsName("Array") | ||
external class JsArray<T : JsAny?> : JsAny { | ||
fun push(value: T) | ||
} | ||
|
||
|
||
private external object Buffer { | ||
fun toString(encoding: String): String | ||
} | ||
|
||
private external interface FsStats { | ||
val mtimeMs: Double | ||
} | ||
|
||
private external interface FsModule { | ||
fun statSync(path: String): FsStats | ||
fun writeFileSync(path: String, data: String) | ||
fun readFileSync(path: String, encoding: String): String | ||
fun unlinkSync(path: String) | ||
} | ||
|
||
private external interface CryptoModule { | ||
fun randomBytes(size: Int): Buffer | ||
} | ||
|
||
private external interface ChildProcessModule { | ||
fun execSync(command: String, options: JsObject): Int | ||
fun spawnSync(command: String, args: JsArray<JsString>, options: JsAny?): JsObject | ||
} | ||
|
||
private fun importNodeFsModule(): FsModule = | ||
js("""require("fs")""") | ||
|
||
private fun importNodeCryptoModule(): CryptoModule = | ||
js("""require("crypto")""") | ||
|
||
private fun importNodeChildProcessModule(): ChildProcessModule = | ||
js("""require("child_process")""") | ||
|
||
internal actual fun createEditor( | ||
editorPath: String?, | ||
env: Map<String, String>, | ||
requireSave: Boolean, | ||
extension: String, | ||
): Editor { | ||
try { | ||
val fs = importNodeFsModule() | ||
val crypto = importNodeCryptoModule() | ||
val childProcess = importNodeChildProcessModule() | ||
return NodeJsEditor(fs, crypto, childProcess, editorPath, env, requireSave, extension) | ||
} catch (e: Exception) { | ||
throw IllegalStateException("Cannot edit files on this platform", e) | ||
} | ||
} | ||
|
||
private class NodeJsEditor( | ||
private val fs: FsModule, | ||
private val crypto: CryptoModule, | ||
private val childProcess: ChildProcessModule, | ||
private val editorPath: String?, | ||
private val env: Map<String, String>, | ||
private val requireSave: Boolean, | ||
private val extension: String, | ||
) : Editor { | ||
private fun getEditorPath(): String { | ||
return editorPath ?: inferEditorPath { editor -> | ||
val options = jsObject( | ||
"timeout" to 100.toJsNumber(), | ||
"windowsHide" to true.toJsBoolean(), | ||
"stdio" to "ignore".toJsString(), | ||
) | ||
childProcess.execSync("${getWhichCommand()} $editor", options) == 0 | ||
} | ||
} | ||
|
||
private fun getEditorCommand(): Array<String> { | ||
return getEditorPath().trim().split(" ").toTypedArray() | ||
} | ||
|
||
private fun editFileWithEditor(editorCmd: Array<String>, filename: String) { | ||
val cmd = editorCmd[0] | ||
val args = JsArray<JsString>() | ||
(editorCmd.drop(1) + filename).forEach { args.push(it.toJsString()) } | ||
val options = jsObject( | ||
"stdio" to "inherit".toJsString(), | ||
"env" to jsObject(*env.map { (k, v) -> k to v.toJsString() }.toTypedArray()) | ||
) | ||
try { | ||
val exitCode = childProcess.spawnSync(cmd, args, options) | ||
if (exitCode["status".toJsString()]?.unsafeCast<JsNumber>()?.toInt() != 0) { | ||
throw CliktError("$cmd: Editing failed!") | ||
} | ||
} catch (err: CliktError) { | ||
throw err | ||
} catch (err: Throwable) { | ||
throw CliktError("Error staring editor") | ||
} | ||
} | ||
|
||
override fun editFile(filename: String) { | ||
editFileWithEditor(getEditorCommand(), filename) | ||
} | ||
|
||
private fun getTmpFileName(extension: String): String { | ||
val rand = crypto.randomBytes(8).toString("hex") | ||
val dir = readEnvvar("TMP") ?: "." | ||
return "$dir/clikt_tmp_$rand.${extension.trimStart { it == '.' }}" | ||
} | ||
|
||
private fun getLastModified(path: String): Int { | ||
return (fs.statSync(path).mtimeMs as Number).toInt() | ||
} | ||
|
||
override fun edit(text: String): String? { | ||
val editorCmd = getEditorCommand() | ||
val textToEdit = normalizeEditorText(editorCmd[0], text) | ||
val tmpFilename = getTmpFileName(extension) | ||
try { | ||
fs.writeFileSync(tmpFilename, textToEdit) | ||
try { | ||
val lastModified = getLastModified(tmpFilename) | ||
editFileWithEditor(editorCmd, tmpFilename) | ||
if (requireSave && getLastModified(tmpFilename) == lastModified) { | ||
return null | ||
} | ||
val readFileSync = fs.readFileSync(tmpFilename, "utf8") | ||
return (readFileSync as String?)?.replace("\r\n", "\n") | ||
} finally { | ||
try { | ||
fs.unlinkSync(tmpFilename) | ||
} catch (ignored: Throwable) { | ||
} | ||
} | ||
} catch (e: Throwable) { | ||
throw CliktError("Error staring editing text: ${e.message}") | ||
} | ||
} | ||
} | ||
|
||
private fun jsObject(vararg pairs: Pair<String, JsAny?>): JsObject { | ||
val obj = JsObject() | ||
for ((k, v) in pairs) { | ||
obj[k.toJsString()] = v | ||
} | ||
return obj | ||
} |