将 AI CLI 响应与 Python PySide6 桌面应用程序进行并排比较
将 AI CLI 响应与 Python PySide6 桌面应用程序进行并排比较
在只能一次测试一个AI编码助手的情况下,选择合适的工具并非易事。安装了Qwen Code、GitHub Copilot CLI、OpenCode和Gemini CLI的开发者需要一种快速的方法,将相同的提示信息同时发送给它们,并并行读取结果。本项目使用PySide6构建了一个Python桌面应用程序,它能够实现这一目标——输入一个提示信息,即可同时输出四个实时响应面板。
你将构建:一个 PySide6 桌面 GUI,它可以动态检测已安装的 AI CLI 工具,同时向所有这些工具发送一个提示,并将每个响应流式传输到它自己的面板中,并提供 Markdown/渲染 HTML 切换选项以便于阅读。

先决条件
- Python 3.10+
- PySide6(
pip install PySide6) - markdown (
pip install markdown) - 至少安装并验证了以下一种 AI CLI 工具:
| 命令行界面 | 安装命令 | 文档 |
|---|---|---|
| Qwen Code | npm install -g @qwen-code/qwen-code |
github.com/QwenLM/qwen-code |
| GitHub Copilot CLI | npm install -g @github/copilot |
docs.github.com |
| OpenCode | npm install -g opencode-ai |
opencode.ai |
| Gemini CLI | npm install -g @google/gemini-cli |
github.com/google-gemini/gemini-cli |
安装依赖项
创建requirements.txt包含两个 Python 依赖项的文件并安装它们:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>PySide6>=6.6.0
markdown>=3.5
</code></span></span></span>
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>pip <span style="color:#336666">install</span> <span style="color:#330099"><strong>-r</strong></span> requirements.txt
</code></span></span></span>
定义 CLI 工具注册表
每个 AI CLI 都有自己的二进制名称、非交互式调用标志和品牌颜色。应用程序将这些信息存储为字典列表,以便在启动时动态生成面板。
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>CLI_DEFS <span style="color:#555555">=</span> [
{
<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">qwen</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">Qwen Code</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">qwen</span><span style="color:#cc3300">"</span>, p],
<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#4A9EEB</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://github.com/QwenLM/qwen-code</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g @qwen-code/qwen-code</span><span style="color:#cc3300">"</span>,
},
{
<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">GitHub Copilot CLI</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [_REAL_COPILOT, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p] <span style="color:#006699"><strong>if</strong></span> _REAL_COPILOT <span style="color:#006699"><strong>else</strong></span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p],
<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#9B6FE8</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://docs.github.com/en/copilot/github-copilot-in-the-cli</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">gh extension install github/gh-copilot</span><span style="color:#cc3300">"</span>,
},
{
<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">opencode</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">OpenCode</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">opencode</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">run</span><span style="color:#cc3300">"</span>, p],
<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#E8623A</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://opencode.ai</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g opencode-ai</span><span style="color:#cc3300">"</span>,
},
{
<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">gemini</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">Gemini CLI</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>: <span style="color:#006699"><strong>lambda</strong></span> p: [<span style="color:#cc3300">"</span><span style="color:#cc3300">gemini</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">-p</span><span style="color:#cc3300">"</span>, p],
<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">#34A853</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">https://github.com/google-gemini/gemini-cli</span><span style="color:#cc3300">"</span>,
<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>: <span style="color:#cc3300">"</span><span style="color:#cc3300">npm install -g @google/gemini-cli</span><span style="color:#cc3300">"</span>,
},
]
</code></span></span></span>
每个命令行界面的非交互式调用语法都不同:
| 命令行界面 | 命令模式 |
|---|---|
| Qwen Code | qwen "<prompt>" |
| GitHub Copilot CLI | copilot -p "<prompt>" |
| OpenCode | opencode run "<prompt>" |
| Gemini CLI | gemini -p "<prompt>" |
处理 Windows 子进程
在 Windows 系统中,通过 npm 安装的 CLI 工具是.CMD包装脚本,subprocess.Popen不能直接运行——它们需要通过 . 来执行cmd /c。该build_cmd辅助工具会透明地处理这一过程,同时跳过包装脚本,直接运行真正的.exe二进制文件:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_IS_WINDOWS <span style="color:#555555">=</span> platform.<span style="color:#cc00ff">system</span>() <span style="color:#555555">==</span> <span style="color:#cc3300">"</span><span style="color:#cc3300">Windows</span><span style="color:#cc3300">"</span>
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">build_cmd</span>(args: <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]) <span style="color:#555555">-></span> <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]:
<span style="color:#cc3300">"""</span><span style="color:#cc3300">On Windows .CMD/.BAT scripts cannot be spawned directly; wrap with cmd /c.
If the binary is already a .exe, no wrapping is needed.</span><span style="color:#cc3300">"""</span>
<span style="color:#006699"><strong>if</strong></span> _IS_WINDOWS <span style="color:#000000"><strong>and</strong></span> <span style="color:#000000"><strong>not</strong></span> args[<span style="color:#ff6600">0</span>].<span style="color:#cc00ff">lower</span>().<span style="color:#cc00ff">endswith</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">.exe</span><span style="color:#cc3300">"</span>):
<span style="color:#006699"><strong>return</strong></span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">cmd</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">/c</span><span style="color:#cc3300">"</span>] <span style="color:#555555">+</span> args
<span style="color:#006699"><strong>return</strong></span> args
</code></span></span></span>
GitHub Copilot CLI 还有个额外的问题:VS Code 扩展会安装一个 PowerShell 包装器(copilot.ps1),该包装器使用执行交互式版本检查Read-Host。当stdin版本为 0 时/dev/null,这会导致进程立即退出,而不会运行查询。该函数通过暂时从 PATH 中移除包装器的目录_find_real_copilot来解析实际的二进制文件:copilot.exe
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_find_real_copilot</span>() <span style="color:#555555">-></span> <span style="color:#336666">str</span> <span style="color:#555555">|</span> <span style="color:#336666">None</span>:
<span style="color:#cc3300">"""</span><span style="color:#cc3300">Find the real copilot binary, skipping the VS Code PS1/BAT wrapper.</span><span style="color:#cc3300">"""</span>
wrapper_path <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>if</strong></span> <span style="color:#000000"><strong>not</strong></span> wrapper_path:
<span style="color:#006699"><strong>return</strong></span> <span style="color:#336666">None</span>
<span style="color:#006699"><strong>if</strong></span> wrapper_path.<span style="color:#cc00ff">lower</span>().<span style="color:#cc00ff">endswith</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">.exe</span><span style="color:#cc3300">"</span>):
<span style="color:#006699"><strong>return</strong></span> wrapper_path
wrapper_dir <span style="color:#555555">=</span> os.path.<span style="color:#cc00ff">dirname</span>(os.path.<span style="color:#cc00ff">abspath</span>(wrapper_path))
filtered <span style="color:#555555">=</span> [p <span style="color:#006699"><strong>for</strong></span> p <span style="color:#000000"><strong>in</strong></span> os.environ.<span style="color:#cc00ff">get</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">""</span>).<span style="color:#cc00ff">split</span>(os.pathsep)
<span style="color:#006699"><strong>if</strong></span> os.path.<span style="color:#cc00ff">normcase</span>(os.path.<span style="color:#cc00ff">abspath</span>(p)) <span style="color:#555555">!=</span> os.path.<span style="color:#cc00ff">normcase</span>(wrapper_dir)]
old_path <span style="color:#555555">=</span> os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>]
os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>] <span style="color:#555555">=</span> os.pathsep.<span style="color:#cc00ff">join</span>(filtered)
<span style="color:#006699"><strong>try</strong></span>:
real <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">copilot</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>finally</strong></span>:
os.environ[<span style="color:#cc3300">"</span><span style="color:#cc3300">PATH</span><span style="color:#cc3300">"</span>] <span style="color:#555555">=</span> old_path
<span style="color:#006699"><strong>return</strong></span> real
</code></span></span></span>
使用后台 QThread 工作线程流式传输 CLI 输出
每个 CLI 都在各自的服务器上运行,QThread以保持用户界面的响应速度。该类CLIWorker会生成一个子进程,逐行读取标准输出,去除 ANSI 转义码,并将每个数据块作为 Qt 信号发出:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_ANSI_RE <span style="color:#555555">=</span> re.<span style="color:#cc00ff">compile</span>(r<span style="color:#cc3300">"</span><span style="color:#cc3300">\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">strip_ansi</span>(text: <span style="color:#336666">str</span>) <span style="color:#555555">-></span> <span style="color:#336666">str</span>:
<span style="color:#006699"><strong>return</strong></span> _ANSI_RE.<span style="color:#cc00ff">sub</span>(<span style="color:#cc3300">""</span>, text)
<span style="color:#006699"><strong>class</strong></span> <span style="color:#00aa88"><strong>CLIWorker</strong></span>(QThread):
output_chunk <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>Signal</strong></span>(<span style="color:#336666">str</span>)
finished <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>Signal</strong></span>(<span style="color:#336666">bool</span>, <span style="color:#336666">str</span>)
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">__init__</span>(self, cmd: <span style="color:#336666">list</span>[<span style="color:#336666">str</span>]):
<span style="color:#cc00ff">super</span>().<span style="color:#cc00ff">__init__</span>()
self._cmd <span style="color:#555555">=</span> cmd
self._cancelled <span style="color:#555555">=</span> <span style="color:#336666">False</span>
self._process: subprocess.Popen <span style="color:#555555">|</span> <span style="color:#336666">None</span> <span style="color:#555555">=</span> <span style="color:#336666">None</span>
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">cancel</span>(self):
self._cancelled <span style="color:#555555">=</span> <span style="color:#336666">True</span>
<span style="color:#006699"><strong>if</strong></span> self._process <span style="color:#000000"><strong>and</strong></span> self._process.<span style="color:#cc00ff">poll</span>() <span style="color:#000000"><strong>is</strong></span> <span style="color:#336666">None</span>:
self._process.<span style="color:#cc00ff">kill</span>()
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">run</span>(self):
<span style="color:#006699"><strong>try</strong></span>:
self._process <span style="color:#555555">=</span> subprocess.<span style="color:#00aa88"><strong>Popen</strong></span>(
self._cmd,
stdout<span style="color:#555555">=</span>subprocess.PIPE,
stderr<span style="color:#555555">=</span>subprocess.STDOUT,
stdin<span style="color:#555555">=</span>subprocess.DEVNULL,
text<span style="color:#555555">=</span><span style="color:#336666">True</span>,
encoding<span style="color:#555555">=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">utf-8</span><span style="color:#cc3300">"</span>,
errors<span style="color:#555555">=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">replace</span><span style="color:#cc3300">"</span>,
)
<span style="color:#006699"><strong>for</strong></span> line <span style="color:#000000"><strong>in</strong></span> <span style="color:#cc00ff">iter</span>(self._process.stdout.readline, <span style="color:#cc3300">""</span>):
<span style="color:#006699"><strong>if</strong></span> self._cancelled:
self._process.<span style="color:#cc00ff">kill</span>()
self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">Cancelled</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>return</strong></span>
self.output_chunk.<span style="color:#cc00ff">emit</span>(<span style="color:#cc00ff">strip_ansi</span>(line))
self._process.stdout.<span style="color:#cc00ff">close</span>()
self._process.<span style="color:#cc00ff">wait</span>()
rc <span style="color:#555555">=</span> self._process.returncode
<span style="color:#006699"><strong>if</strong></span> rc <span style="color:#555555">==</span> <span style="color:#ff6600">0</span>:
self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">True</span>, <span style="color:#cc3300">""</span>)
<span style="color:#006699"><strong>else</strong></span>:
self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, f<span style="color:#cc3300">"</span><span style="color:#cc3300">Process exited with code </span><span style="color:#aa0000">{</span>rc<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>except</strong></span> <span style="color:#336666">FileNotFoundError</span>:
self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, f<span style="color:#cc3300">"</span><span style="color:#cc3300">Command not found: </span><span style="color:#aa0000">{</span>self._cmd[<span style="color:#ff6600">0</span>]<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span>)
<span style="color:#006699"><strong>except</strong></span> <span style="color:#336666">Exception</span> <span style="color:#006699"><strong>as</strong></span> exc:
self.finished.<span style="color:#cc00ff">emit</span>(<span style="color:#336666">False</span>, <span style="color:#cc00ff">str</span>(exc))
</code></span></span></span>
关键设计决策:
stdin=subprocess.DEVNULL防止 CLI 工具在交互式提示符处阻塞。stderr=subprocess.STDOUT将错误输出合并到面板中,这样就不会悄无声息地丢失任何信息。- 逐行迭代(
readline)实现了实时流式传输,而无需等待进程完成。
使用 Markdown 渲染构建每个 CLI 的响应面板
每个元素CLIPanel都QFrame包含一个头部(命令行名称、切换按钮、状态指示器)和两个堆叠的内容区域——一个等宽纯文本视图和一个渲染后的 HTML 视图。切换按钮用于在两者之间切换:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_on_toggle</span>(self, checked: <span style="color:#336666">bool</span>):
self._rendered_mode <span style="color:#555555">=</span> checked
<span style="color:#006699"><strong>if</strong></span> checked:
self._toggle_btn.<span style="color:#cc00ff">setText</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">✎ Markdown</span><span style="color:#cc3300">"</span>)
self.render_area.<span style="color:#cc00ff">setHtml</span>(<span style="color:#cc00ff">markdown_to_html</span>(self._raw_text))
self.text_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">False</span>)
self.render_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">True</span>)
<span style="color:#006699"><strong>else</strong></span>:
self._toggle_btn.<span style="color:#cc00ff">setText</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">⟳ Render</span><span style="color:#cc3300">"</span>)
self.render_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">False</span>)
self.text_area.<span style="color:#cc00ff">setVisible</span>(<span style="color:#336666">True</span>)
</code></span></span></span>
Markdown 到 HTML 的转换使用了 Pythonmarkdown库,并扩展了代码块、表格和换行符的功能,同时还使用了深色主题的 CSS 样式表:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code>_MD_EXTENSIONS <span style="color:#555555">=</span> [<span style="color:#cc3300">"</span><span style="color:#cc3300">fenced_code</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">tables</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">nl2br</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">sane_lists</span><span style="color:#cc3300">"</span>]
<span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">markdown_to_html</span>(text: <span style="color:#336666">str</span>) <span style="color:#555555">-></span> <span style="color:#336666">str</span>:
body <span style="color:#555555">=</span> md_lib.<span style="color:#cc00ff">markdown</span>(text, extensions<span style="color:#555555">=</span>_MD_EXTENSIONS)
<span style="color:#006699"><strong>return</strong></span> f<span style="color:#cc3300">"""</span><span style="color:#cc3300"><!DOCTYPE html><html><head><meta charset=</span><span style="color:#cc3300">'</span><span style="color:#cc3300">utf-8</span><span style="color:#cc3300">'</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>_MD_CSS<span style="color:#aa0000">}</span><span style="color:#cc3300"></head>
<body></span><span style="color:#aa0000">{</span>body<span style="color:#aa0000">}</span><span style="color:#cc3300"></body></html></span><span style="color:#cc3300">"""</span>
</code></span></span></span>
如果未安装 CLI,面板将显示一个带有可点击下载 URL 和建议安装命令的样式信息卡,而不是空白文本区域:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_show_download_info</span>(self):
self.<span style="color:#cc00ff">_set_status</span>(<span style="color:#cc3300">"</span><span style="color:#cc3300">● Not installed</span><span style="color:#cc3300">"</span>, <span style="color:#cc3300">"</span><span style="color:#cc3300">#f44336</span><span style="color:#cc3300">"</span>)
url <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">download_url</span><span style="color:#cc3300">"</span>]
hint <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">install_hint</span><span style="color:#cc3300">"</span>]
name <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">name</span><span style="color:#cc3300">"</span>]
color <span style="color:#555555">=</span> self.cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">color</span><span style="color:#cc3300">"</span>]
html <span style="color:#555555">=</span> f<span style="color:#cc3300">"""</span><span style="color:#cc3300">
<div style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#e0e0e0; font-family:</span><span style="color:#cc3300">'</span><span style="color:#cc3300">Segoe UI</span><span style="color:#cc3300">'</span><span style="color:#cc3300">,sans-serif; padding:16px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>
<p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">font-size:16px; font-weight:bold; color:</span><span style="color:#aa0000">{</span>color<span style="color:#aa0000">}</span><span style="color:#cc3300">;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>name<span style="color:#aa0000">}</span><span style="color:#cc3300"></p>
<p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#f44336; font-size:13px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>⚠ Not installed on this system</p>
<p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">font-size:12px; color:#aaa; margin-top:16px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>Download / Documentation:</p>
<p style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">margin-top:4px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">>
<a href=</span><span style="color:#cc3300">"</span><span style="color:#aa0000">{</span>url<span style="color:#aa0000">}</span><span style="color:#cc3300">"</span><span style="color:#cc3300"> style=</span><span style="color:#cc3300">"</span><span style="color:#cc3300">color:#4A9EEB; font-size:13px;</span><span style="color:#cc3300">"</span><span style="color:#cc3300">></span><span style="color:#aa0000">{</span>url<span style="color:#aa0000">}</span><span style="color:#cc3300"></a>
</p>
</div>
</span><span style="color:#cc3300">"""</span>
self.text_area.<span style="color:#cc00ff">setHtml</span>(html)
</code></span></span></span>
组装主窗口和提示栏
它MainWindow会动态检查已安装的命令行界面 (CLI) shutil.which,为每个 CLI 创建一个CLIPanel列表,并将它们水平排列QSplitter。底部的提示输入区域同时支持“发送”按钮和Ctrl+Enter快捷键:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#0099ff"><em># ── CLI panels ───────────────────────────────────────────────────────
</em></span>splitter <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>QSplitter</strong></span>(Qt.Orientation.Horizontal)
available_count <span style="color:#555555">=</span> <span style="color:#ff6600">0</span>
<span style="color:#006699"><strong>for</strong></span> cli_def <span style="color:#000000"><strong>in</strong></span> CLI_DEFS:
avail <span style="color:#555555">=</span> shutil.<span style="color:#cc00ff">which</span>(cli_def[<span style="color:#cc3300">"</span><span style="color:#cc3300">id</span><span style="color:#cc3300">"</span>]) <span style="color:#000000"><strong>is</strong></span> <span style="color:#000000"><strong>not</strong></span> <span style="color:#336666">None</span>
panel <span style="color:#555555">=</span> <span style="color:#00aa88"><strong>CLIPanel</strong></span>(cli_def, avail)
splitter.<span style="color:#cc00ff">addWidget</span>(panel)
self._panels.<span style="color:#cc00ff">append</span>(panel)
<span style="color:#006699"><strong>if</strong></span> avail:
available_count <span style="color:#555555">+=</span> <span style="color:#ff6600">1</span>
</code></span></span></span>
当用户点击“发送”按钮时,提示文本会同时发送到所有已安装的面板:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">_send_prompt</span>(self):
prompt <span style="color:#555555">=</span> self._prompt_input.<span style="color:#cc00ff">toPlainText</span>().<span style="color:#cc00ff">strip</span>()
<span style="color:#006699"><strong>if</strong></span> <span style="color:#000000"><strong>not</strong></span> prompt:
<span style="color:#006699"><strong>return</strong></span>
self._prompt_input.<span style="color:#cc00ff">clear</span>()
<span style="color:#006699"><strong>for</strong></span> panel <span style="color:#000000"><strong>in</strong></span> self._panels:
panel.<span style="color:#cc00ff">start_query</span>(prompt)
</code></span></span></span>
Ctrl+Enter快捷键是通过QEvent对提示输入进行筛选来实现的:
<span style="color:#212529"><span style="color:#333333"><span style="background-color:#eaeaea"><code><span style="color:#006699"><strong>def</strong></span> <span style="color:#cc00ff">eventFilter</span>(self, obj, event):
<span style="color:#006699"><strong>if</strong></span> obj <span style="color:#000000"><strong>is</strong></span> self._prompt_input <span style="color:#000000"><strong>and</strong></span> event.<span style="color:#cc00ff">type</span>() <span style="color:#555555">==</span> QEvent.Type.KeyPress:
key_ev: QKeyEvent <span style="color:#555555">=</span> event
ctrl <span style="color:#555555">=</span> Qt.KeyboardModifier.ControlModifier
<span style="color:#cc00ff">if </span>(
key_ev.<span style="color:#cc00ff">key</span>() <span style="color:#555555">==</span> Qt.Key.Key_Return
<span style="color:#000000"><strong>and</strong></span> key_ev.<span style="color:#cc00ff">modifiers</span>() <span style="color:#555555">&</span> ctrl
):
self.<span style="color:#cc00ff">_send_prompt</span>()
<span style="color:#006699"><strong>return</strong></span> <span style="color:#336666">True</span>
<span style="color:#006699"><strong>return</strong></span> <span style="color:#cc00ff">super</span>().<span style="color:#cc00ff">eventFilter</span>(obj, event)
</code></span></span></span>
源代码
更多推荐

所有评论(0)