From 0a8c24374a9cc467647921b711775aff0ae25dae Mon Sep 17 00:00:00 2001 From: fofolee Date: Mon, 13 Jan 2025 01:15:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Equickcommand.runCode=EF=BC=8C?= =?UTF-8?q?quickcommand.runInTerminal=E6=94=AF=E6=8C=81warp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/createTerminalCommand.js | 150 ++++++++++++++++++ plugin/lib/getCommandToLaunchTerminal.js | 44 ----- plugin/lib/quickcommand.js | 8 +- plugin/preload.js | 140 +++++++++------- src/components/CommandRunResult.vue | 2 +- .../quickcommandUI/QuickCommand.vue | 19 +++ src/js/composer/commands/index.js | 3 +- src/js/composer/commands/scriptCommands.js | 53 +++++++ .../monaco/types/quickcommand.api.d.ts | 52 +++++- 9 files changed, 366 insertions(+), 105 deletions(-) create mode 100644 plugin/lib/createTerminalCommand.js delete mode 100644 plugin/lib/getCommandToLaunchTerminal.js create mode 100644 src/js/composer/commands/scriptCommands.js diff --git a/plugin/lib/createTerminalCommand.js b/plugin/lib/createTerminalCommand.js new file mode 100644 index 0000000..fd480b6 --- /dev/null +++ b/plugin/lib/createTerminalCommand.js @@ -0,0 +1,150 @@ +const fs = require("fs"); +const path = require("path"); + +// 终端配置 +const DEFAULT_TERMINALS = { + windows: ["wt", "cmd"], + macos: ["warp", "iterm", "terminal"], +}; + +// Windows 终端命令生成器 +const getWindowsTerminalCommand = (cmdline, options = {}) => { + const { dir, terminal = "wt" } = options; + const appPath = path.join( + window.utools.getPath("home"), + "/AppData/Local/Microsoft/WindowsApps/" + ); + + const terminalCommands = { + wt: () => { + if ( + fs.existsSync(appPath) && + fs.readdirSync(appPath).includes("wt.exe") + ) { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : ""; + return `${appPath}wt.exe ${cd} cmd /k "${escapedCmd}"`; + } + return null; + }, + cmd: () => { + const escapedCmd = cmdline.replace(/"/g, `^"`); + const cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : ""; + return `${cd} start "" cmd /k "${escapedCmd}"`; + }, + }; + + // 按优先级尝试不同终端 + const terminalPriority = + terminal === "default" + ? DEFAULT_TERMINALS.windows + : [terminal, ...DEFAULT_TERMINALS.windows]; + + for (const term of terminalPriority) { + const command = terminalCommands[term]?.(); + if (command) return command; + } + + // 如果都失败了,返回默认的 cmd 命令 + return terminalCommands.cmd(); +}; + +// macOS 终端命令生成器 +const getMacTerminalCommand = (cmdline, options = {}) => { + const { dir, terminal = "warp" } = options; + + const terminalCommands = { + warp: () => { + if (fs.existsSync("/Applications/Warp.app")) { + const workingDir = dir || process.cwd(); + // 创建临时的 launch configuration + const configName = `temp_${Date.now()}`; + const configPath = path.join( + window.utools.getPath("home"), + ".warp/launch_configurations", + `${configName}.yml` + ); + + // 确保目录存在 + const configDir = path.dirname(configPath); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + // 创建配置文件,对于 Warp,命令不需要转义,因为是通过 YAML 配置传递 + const config = `--- +name: ${configName} +windows: + - tabs: + - layout: + cwd: "${workingDir}" + commands: + - exec: ${cmdline}`; + + fs.writeFileSync(configPath, config); + + // 使用配置文件启动 Warp + return `open "warp://launch/${configName}" && sleep 0.5 && rm "${configPath}"`; + } + return null; + }, + iterm: () => { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; + if (fs.existsSync("/Applications/iTerm.app")) { + return `osascript -e 'tell application "iTerm" + if application "iTerm" is running then + create window with default profile + end if + tell current session of first window to write text "clear && ${cd} ${escapedCmd}" + activate + end tell'`; + } + return null; + }, + terminal: () => { + const escapedCmd = cmdline.replace(/"/g, `\\"`); + const cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; + return `osascript -e 'tell application "Terminal" + if application "Terminal" is running then + do script "clear && ${cd} ${escapedCmd}" + else + do script "clear && ${cd} ${escapedCmd}" in window 1 + end if + activate + end tell'`; + }, + }; + + // 按优先级尝试不同终端 + const terminalPriority = + terminal === "default" + ? DEFAULT_TERMINALS.macos + : [terminal, ...DEFAULT_TERMINALS.macos]; + + for (const term of terminalPriority) { + const command = terminalCommands[term]?.(); + if (command) return command; + } + + // 如果都失败了,返回默认终端命令 + return terminalCommands.terminal(); +}; + +// 主函数 +const createTerminalCommand = (cmdline, options = {}) => { + const { windows = "default", macos = "default" } = options; + + if (window.utools.isWindows()) { + return getWindowsTerminalCommand(cmdline, { + ...options, + terminal: windows, + }); + } else if (window.utools.isMacOs()) { + return getMacTerminalCommand(cmdline, { ...options, terminal: macos }); + } + + throw new Error("Unsupported operating system"); +}; + +module.exports = createTerminalCommand; diff --git a/plugin/lib/getCommandToLaunchTerminal.js b/plugin/lib/getCommandToLaunchTerminal.js deleted file mode 100644 index 2aca186..0000000 --- a/plugin/lib/getCommandToLaunchTerminal.js +++ /dev/null @@ -1,44 +0,0 @@ -const fs = require("fs"); -const path = require("path"); - -const getCommandToLaunchTerminal = (cmdline, dir) => { - let cd, command; - if (window.utools.isWindows()) { - let appPath = path.join( - window.utools.getPath("home"), - "/AppData/Local/Microsoft/WindowsApps/" - ); - // 直接 existsSync wt.exe 无效 - if (fs.existsSync(appPath) && fs.readdirSync(appPath).includes("wt.exe")) { - cmdline = cmdline.replace(/"/g, `\\"`); - cd = dir ? `-d "${dir.replace(/\\/g, "/")}"` : ""; - command = `${appPath}wt.exe ${cd} cmd /k "${cmdline}"`; - } else { - cmdline = cmdline.replace(/"/g, `^"`); - cd = dir ? `cd /d "${dir.replace(/\\/g, "/")}" &&` : ""; - command = `${cd} start "" cmd /k "${cmdline}"`; - } - } else if (window.utools.isMacOs()) { - cmdline = cmdline.replace(/"/g, `\\"`); - cd = dir ? `cd ${dir.replace(/ /g, "\\\\ ")} &&` : ""; - command = fs.existsSync("/Applications/iTerm.app") - ? `osascript -e 'tell application "iTerm" - if application "iTerm" is running then - create window with default profile - end if - tell current session of first window to write text "clear && ${cd} ${cmdline}" - activate - end tell'` - : `osascript -e 'tell application "Terminal" - if application "Terminal" is running then - do script "clear && ${cd} ${cmdline}" - else - do script "clear && ${cd} ${cmdline}" in window 1 - end if - activate - end tell'`; - } - return command; -}; - -module.exports = getCommandToLaunchTerminal; diff --git a/plugin/lib/quickcommand.js b/plugin/lib/quickcommand.js index d7a4aeb..a73034a 100644 --- a/plugin/lib/quickcommand.js +++ b/plugin/lib/quickcommand.js @@ -10,7 +10,7 @@ const systemDialog = require("./systemDialog"); const { getQuickcommandTempFile } = require("./getQuickcommandFile"); -const getCommandToLaunchTerminal = require("./getCommandToLaunchTerminal"); +const createTerminalCommand = require("./createTerminalCommand"); const ctlKey = window.utools.isMacOs() ? "command" : "control"; @@ -302,8 +302,10 @@ window.runPythonCommand = (py) => { if (process.platform !== "linux") { // 在终端中执行 - quickcommand.runInTerminal = function (cmdline, dir) { - let command = getCommandToLaunchTerminal(cmdline, dir); + quickcommand.runInTerminal = function (cmdline, options) { + // 兼容老版本接口, 老版本第二个参数是dir + if (typeof options === "string") options = { dir: options }; + let command = createTerminalCommand(cmdline, options); child_process.exec(command); }; // 系统级弹窗 diff --git a/plugin/preload.js b/plugin/preload.js index 62309b3..775ddc1 100644 --- a/plugin/preload.js +++ b/plugin/preload.js @@ -18,7 +18,7 @@ const md5 = (input) => { window.lodashM = require("./lib/lodashMini"); -const getCommandToLaunchTerminal = require("./lib/getCommandToLaunchTerminal"); +const createTerminalCommand = require("./lib/createTerminalCommand"); const shortCodes = require("./lib/shortCodes"); const { pluginInfo, getUtoolsPlugins } = require("./lib/getUtoolsPlugins"); const { @@ -42,7 +42,8 @@ window.getuToolsLite = require("./lib/utoolsLite"); window.quickcommand = require("./lib/quickcommand"); window.quickcomposer = require("./lib/quickcomposer"); window.showUb = require("./lib/showDocs"); -window.getQuickcommandTempFile = require("./lib/getQuickcommandFile").getQuickcommandTempFile; +window.getQuickcommandTempFile = + require("./lib/getQuickcommandFile").getQuickcommandTempFile; window.getSharedQcById = async (id) => { const url = "https://qc.qaz.ink/home/quick/script/getScript"; @@ -211,66 +212,95 @@ window.runCodeInSandbox = (code, callback, addVars = {}) => { } }; -window.runCodeFile = (cmd, option, terminal, callback, realTime = true) => { - let { bin, argv, ext, charset, scptarg, envPath, alias } = option; - let script = getQuickcommandTempFile(ext, "quickcommandTempScript"); - // 批处理和 powershell 默认编码为 GBK, 解决批处理的换行问题 - if (charset.scriptCode) - cmd = iconv.encode(cmd.replace(/\n/g, "\r\n"), charset.scriptCode); - fs.writeFileSync(script, cmd); - // var argvs = [script] - // if (argv) { - // argvs = argv.split(' ') - // argvs.push(script); - // } - let child, cmdline; - if (bin.slice(-7) == "csc.exe") { - cmdline = `${bin} ${argv} /out:"${ - script.slice(0, -2) + "exe" - }" "${script}" && "${script.slice(0, -2) + "exe"}" ${scptarg}`; - } else if (bin == "gcc") { - var suffix = utools.isWindows() ? ".exe" : ""; - cmdline = `${bin} ${argv} "${script.slice(0, -2)}" "${script}" && "${ - script.slice(0, -2) + suffix - }" ${scptarg}`; - } else if (utools.isWindows() && bin == "bash") { - cmdline = `${bin} ${argv} "${script - .replace(/\\/g, "/") - .replace(/C:/i, "/mnt/c")}" ${scptarg}`; - } else { - cmdline = `${bin} ${argv} "${script}" ${scptarg}`; +// 构建命令行字符串的工具函数 +const buildCommandLine = (bin, argv, script, scptarg) => { + if (bin.slice(-7) === "csc.exe") { + const outFile = script.slice(0, -2) + "exe"; + return `${bin} ${argv} /out:"${outFile}" "${script}" && "${outFile}" ${scptarg}`; } - let processEnv = window.lodashM.cloneDeep(process.env); + + if (bin === "gcc") { + const suffix = utools.isWindows() ? ".exe" : ""; + const outFile = script.slice(0, -2) + suffix; + return `${bin} ${argv} "${script.slice( + 0, + -2 + )}" "${script}" && "${outFile}" ${scptarg}`; + } + + if (utools.isWindows() && bin === "bash") { + const wslPath = script.replace(/\\/g, "/").replace(/C:/i, "/mnt/c"); + return `${bin} ${argv} "${wslPath}" ${scptarg}`; + } + + return `${bin} ${argv} "${script}" ${scptarg}`; +}; + +// 处理进程输出的工具函数 +const handleProcessOutput = (child, charset, callback, realTime) => { + const chunks = []; + const errChunks = []; + + child.stdout.on("data", (chunk) => { + const decodedChunk = charset.outputCode + ? iconv.decode(chunk, charset.outputCode) + : chunk; + realTime + ? callback(decodedChunk.toString(), null) + : chunks.push(decodedChunk); + }); + + child.stderr.on("data", (errChunk) => { + const decodedChunk = charset.outputCode + ? iconv.decode(errChunk, charset.outputCode) + : errChunk; + realTime + ? callback(null, decodedChunk.toString()) + : errChunks.push(decodedChunk); + }); + + if (!realTime) { + child.on("close", () => { + callback(chunks.join(""), errChunks.join("")); + }); + } +}; + +window.runCodeFile = ( + cmd, + option, + terminalOptions, + callback, + realTime = true +) => { + const { bin, argv, ext, charset, scptarg, envPath, alias } = option; + const script = getQuickcommandTempFile(ext, "quickcommandTempScript"); + + // 处理编码和换行 + const processedCmd = charset.scriptCode + ? iconv.encode(cmd.replace(/\n/g, "\r\n"), charset.scriptCode) + : cmd; + fs.writeFileSync(script, processedCmd); + // 构建命令行 + let cmdline = buildCommandLine(bin, argv, script, scptarg); + + // 处理环境变量 + const processEnv = window.lodashM.cloneDeep(process.env); if (envPath) processEnv.PATH = envPath; - if (alias) cmdline = alias + "\n" + cmdline; - // 在终端中输出 - if (terminal) cmdline = getCommandToLaunchTerminal(cmdline); - child = child_process.spawn(cmdline, { + if (alias) cmdline = `${alias}\n${cmdline}`; + if (!!terminalOptions) { + cmdline = createTerminalCommand(cmdline, terminalOptions); + } + + // 创建子进程 + const child = child_process.spawn(cmdline, { encoding: "buffer", shell: true, env: processEnv, }); - let chunks = [], - err_chunks = []; + console.log("Running: " + cmdline); - child.stdout.on("data", (chunk) => { - if (charset.outputCode) chunk = iconv.decode(chunk, charset.outputCode); - realTime ? callback(chunk.toString(), null) : chunks.push(chunk); - }); - child.stderr.on("data", (err_chunk) => { - if (charset.outputCode) - err_chunk = iconv.decode(err_chunk, charset.outputCode); - realTime - ? callback(null, err_chunk.toString()) - : err_chunks.push(err_chunk); - }); - if (!realTime) { - child.on("close", (code) => { - let stdout = chunks.join(""); - let stderr = err_chunks.join(""); - callback(stdout, stderr); - }); - } + handleProcessOutput(child, charset, callback, realTime); return child; }; diff --git a/src/components/CommandRunResult.vue b/src/components/CommandRunResult.vue index 1d16c83..581d5d9 100644 --- a/src/components/CommandRunResult.vue +++ b/src/components/CommandRunResult.vue @@ -159,7 +159,7 @@ export default { this.childProcess = window.runCodeFile( currentCommand.cmd, this.getCommandOpt(currentCommand), - currentCommand.output === "terminal", + currentCommand.output === "terminal" ? {} : false, (stdout, stderr) => this.handleResult(stdout, stderr, resultOpts) ); this.listenStopSign(); diff --git a/src/components/quickcommandUI/QuickCommand.vue b/src/components/quickcommandUI/QuickCommand.vue index e505778..c575c5c 100644 --- a/src/components/quickcommandUI/QuickCommand.vue +++ b/src/components/quickcommandUI/QuickCommand.vue @@ -36,6 +36,7 @@ import ButtonBox from "components/quickcommandUI/ButtonBox"; import ConfirmBox from "components/quickcommandUI/ConfirmBox"; import TextArea from "components/quickcommandUI/TextArea"; import SelectList from "components/quickcommandUI/SelectList"; +import programs from "js/options/programs"; export default { components: { @@ -256,8 +257,26 @@ export default { }); }, }; + // 将quickcommandUI添加到quickcommand Object.assign(window.quickcommand, quickcommandUI); + + // 获取用户数据 window.quickcommand.userData = this.$root.utools.userData; + + // 添加runCode方法,不在preload中加是因为programs写在了src中-_- + quickcommand.runCode = (code, program, runInTerminal = false) => { + return new Promise((reslove, reject) => { + window.runCodeFile( + code, + { ...programs[program], charset: {}, scptarg: "" }, + runInTerminal, + (result, err) => (err ? reject(err) : reslove(result)) + ); + false; + }); + }; + + // 冻结quickcommand Object.freeze(quickcommand); }, methods: { diff --git a/src/js/composer/commands/index.js b/src/js/composer/commands/index.js index 3bf3d0a..05fe320 100644 --- a/src/js/composer/commands/index.js +++ b/src/js/composer/commands/index.js @@ -17,7 +17,7 @@ import { imageCommands } from "./imageCommands"; import { windowsCommands } from "./windowsCommands"; import { statusCommands } from "./statusCommands"; import { macosCommands } from "./macosCommands"; - +import { scriptCommands } from "./scriptCommands"; let commands = [ fileCommands, networkCommands, @@ -30,6 +30,7 @@ let commands = [ dataCommands, codingCommands, controlCommands, + scriptCommands, uiCommands, simulateCommands, mathCommands, diff --git a/src/js/composer/commands/scriptCommands.js b/src/js/composer/commands/scriptCommands.js new file mode 100644 index 0000000..1b5ebe9 --- /dev/null +++ b/src/js/composer/commands/scriptCommands.js @@ -0,0 +1,53 @@ +export const scriptCommands = { + label: "编程相关", + icon: "integration_instructions", + commands: [ + { + value: "", + label: "赋值", + icon: "script", + outputVariable: "value", + saveOutput: true, + config: [ + { + label: "值或表达式", + component: "VariableInput", + width: 12, + }, + ], + }, + { + value: "(function(code){new Function(code)()})", + label: "注入JS脚本", + icon: "script", + config: [ + { + label: "JS脚本", + component: "CodeEditor", + width: 12, + }, + ], + }, + { + value: "quickcommand.runAppleScript", + label: "执行 AppleScript", + icon: "script", + outputVariable: "result", + saveOutput: true, + config: [ + { + label: "脚本", + component: "CodeEditor", + width: 12, + }, + ], + }, + { + value: "quickcommand.runCsharp", + label: "执行C#脚本", + icon: "script", + outputVariable: "result", + saveOutput: true, + }, + ], +}; diff --git a/src/plugins/monaco/types/quickcommand.api.d.ts b/src/plugins/monaco/types/quickcommand.api.d.ts index a188d7d..97453fe 100644 --- a/src/plugins/monaco/types/quickcommand.api.d.ts +++ b/src/plugins/monaco/types/quickcommand.api.d.ts @@ -378,11 +378,23 @@ interface quickcommandApi { * 在终端运行,不支持 Linux * * @param command 要在终端运行的命令 + * @param options 终端运行参数 + * @param options.dir 运行目录 + * @param options.windows 终端类型,默认wt + * @param options.macos 终端类型,默认warp + * * ```js * quickcommand.runInTerminal(`whoami`) * ``` */ - runInTerminal(command: string); + runInTerminal( + command: string, + options?: { + dir?: string; + windows?: "wt" | "cmd"; + macos?: "warp" | "iterm" | "terminal"; + } + ); /** * 对应 utools.onPluginEnter 的 `code` `type` 和 `payload` @@ -597,6 +609,44 @@ interface quickcommandApi { defaultText?: string, title?: string ): Promise; + + /** + * 运行代码 + * @param code 代码 + * @param program 编程语言 + * @param runInTerminal 终端运行参数,不传则不在终端运行 + * @param runInTerminal.dir 运行目录 + * @param runInTerminal.windows windows使用的终端,默认wt + * @param runInTerminal.macos macos使用的终端,默认warp + * + * 支持的编程语言: + * shell, applescript, cmd, python, powershell, javascript, ruby, php, lua, perl, csharp, c + * + * ```js + * quickcommand.runCode("print('Hello, World!');", "python"); + * ``` + */ + runCode( + code: string, + program: + | "shell" + | "applescript" + | "cmd" + | "python" + | "powershell" + | "javascript" + | "ruby" + | "php" + | "lua" + | "perl" + | "csharp" + | "c", + runInTerminal?: { + dir?: string; + windows?: "wt" | "cmd"; + macos?: "warp" | "iterm" | "terminal"; + } + ): Promise; } declare var quickcommand: quickcommandApi;