├── .github
└── ISSUE_TEMPLATE
│ ├── Bug.md
│ └── Feature or Suggestion.md
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── README_ZH.md
├── design
├── logo.html
└── vue.png
├── index.html
├── package.js
├── package.json
├── publish.py
├── shortcut
├── dragging.gif
├── ele-info.png
├── logo.png
└── vue-web-terminal.gif
├── src
├── Terminal.vue
├── ansi
│ ├── ansi-256-colors.json
│ └── index.ts
├── common
│ ├── api
│ │ └── index.ts
│ ├── configuration.ts
│ ├── store
│ │ └── index.ts
│ └── util.ts
├── components
│ ├── TEditor.vue
│ ├── THeader.vue
│ ├── THelpBox.vue
│ ├── TViewerCode.vue
│ ├── TViewerJson.vue
│ ├── TViewerNormal.vue
│ └── TViewerTable.vue
├── css
│ ├── ansi.css
│ ├── scrollbar.css
│ ├── style.css
│ └── theme
│ │ ├── dark.css
│ │ └── light.css
├── env.d.ts
├── index.ts
└── types
│ └── index.ts
├── test
├── App.vue
└── main.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vue-web-terminal.iml
/.github/ISSUE_TEMPLATE/Bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: tzfun
7 |
8 | ---
9 |
10 | **Bug description**
11 | A clear and concise description of what the bug is.
12 |
13 | **Steps to reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Screenshot**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Reproduce environment:**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 2.0.4]
27 |
28 | **Detailed Description**
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature or Suggestion.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature or Suggestion
3 | about: Provide some better suggestions or optimizations
4 | title: ''
5 | labels: enhancement
6 | assignees: tzfun
7 |
8 | ---
9 |
10 | **Feature or optimization description**
11 |
12 | **Applicable scene**
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # local env files
11 | .env.local
12 | .env.*.local
13 |
14 | node_modules
15 | dist
16 | dist-ssr
17 | *.local
18 | /lib
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | .vscode
25 | .DS_Store
26 | *.suo
27 | *.ntvs*
28 | *.njsproj
29 | *.sln
30 | *.sw?
31 |
32 | /vue-web-terminal.css
33 | /vue-web-terminal.js
34 | stats.html
35 | /docs/
36 | /docs/.vuepress/
37 | pnpm-lock.yaml
38 | /yarn.lock
39 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | *.md
3 | *.yml
4 | *.iml
5 | *.lock
6 | *.html
7 | tsconfig.json
8 | tsconfig.node.json
9 | vite.config.ts
10 | build/
11 | node_modules/
12 | src/
13 | test/
14 | dist/
15 | public/
16 | gulpfile.js
17 | babel.config.js
18 | webpack.config.js
19 | vue.config.js
20 | package.js
21 | lib/*.html
22 | lib/*.chunk.*
23 | lib/*.txt
24 | lib/*.png
25 | lib/*.gif
26 | lib/*.jpg
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [中文版](./README_ZH.md) | English
2 |
3 |
4 |
5 |
6 |
7 | # vue-web-terminal
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | A web-side command line plugin built by `Vue`, supports multiple message formats such as tables, json, and codes, supports custom message styles, command line libraries, typing search prompts, etc., and simulates native terminal support ← → cursor toggle and ↑ ↓ history command toggle.
16 |
17 | > :tada: The new document is now available. It's more detailed and more friendly, welcome to experience it: [https://tzfun.github.io/vue-web-terminal/](https://tzfun.github.io/vue-web-terminal/)
18 |
19 | ## Feature Support
20 |
21 | * Supported message formats: plain text, table, json, code/multi-line text, html, ansi
22 | * Supports real-time content display and appending, and can create simple animation effects
23 | * Support user question and answer input
24 | * Support online text editing
25 | * Support window dragging and fixing
26 | * Support ← → cursor key switching and ↑ ↓ key history command switching
27 | * Support fullscreen
28 | * Support command input prompts
29 | * Support logging group folding
30 | * Supports multiple styles of slots, customizable styles
31 | * Supports themes, with built-in dark and light themes by default, and you can also customize themes
32 | * Provides a rich set of JS APIs, almost all functions can be simulated by APIs to simulate non-human operations
33 | * Supports Vue2/Vue3
34 |
35 | 
36 |
37 | > Short description:
38 | >
39 | > It does not have the ability to execute a specific command. This ability needs to be implemented by the developer.
40 | > What it is responsible for is to get the command to be executed from the user in the form of an interface, and then
41 | > hand it over to the developer to implement and execute. After that, hand it over to show it to the user
42 |
43 | # Online Experience
44 |
45 | You can learn about some of the features of this plugin through the [Online Experience](https://tzfun.github.io/vue-web-terminal/demo.html), or try editing the code and previewing it on [](https://codesandbox.io/s/silly-scooby-l8wk9b).
46 |
47 | # Document
48 |
49 | Please go to [Document](https://tzfun.github.io/vue-web-terminal/)
50 |
51 | # Quick Start
52 |
53 | > The **Vue2** version will be officially archived from **December 24, 2024** and will no longer provide maintenance updates.
54 | > For the source code, see [vue2 branch](https://github.com/tzfun/vue-web-terminal/tree/vue2).
55 |
56 | Install vue-web-terminal by npm. The `2.x.x` version corresponds to vue2, and the `3.x.x` version corresponds to vue3.
57 | It is recommended to download the latest version corresponding to the main version.
58 |
59 | ```shell
60 | # install for vue2
61 | npm install vue-web-terminal@2.xx --save
62 |
63 | # install for vue3
64 | npm install vue-web-terminal@3.xx --save
65 | ```
66 |
67 | Use Terminal plugin in `main`
68 |
69 | **Vue2**
70 | ```js
71 | import Terminal from 'vue-web-terminal'
72 |
73 | Vue.use(Terminal)
74 | ```
75 |
76 | **Vue3**
77 | ```ts
78 | import { createTerminal } from 'vue-web-terminal'
79 |
80 | const app = createApp(App)
81 |
82 | app.use(createTerminal())
83 |
84 | app.mount('#app')
85 | ```
86 |
87 | Example:
88 |
89 | ```vue
90 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
123 | ```
124 |
125 | # Contact Author
126 |
127 | I am a backend coder, and I know a little bit about frontend. This plugin was created out of my interest.
128 |
129 | If you have good ideas for code optimization or functions and are willing to contribute code, please submit [PR](https://github.com/tzfun/vue-web-terminal/pulls),
130 | If you have any questions about the use of the plugin or find bugs, please submit[issue](https://github.com/tzfun/vue-web-terminal/issues).
131 |
132 | > Contact me (Add please note vue-web-terminal)
133 | >
134 | > 📮 Email: *beifengtz@qq.com*
135 | >
136 | >  WeChat: *beifeng-tz*
137 |
138 | # License
139 |
140 | [Apache License 2.0](LICENSE)
141 |
--------------------------------------------------------------------------------
/README_ZH.md:
--------------------------------------------------------------------------------
1 | 中文版 | [English](./README.md)
2 |
3 |
4 |
5 |
6 |
7 | # vue-web-terminal
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 一个由 Vue 构建的支持多内容格式显示的网页端命令行窗口插件,支持表格、json、代码等多种消息格式,支持自定义消息样式、命令行库、键入搜索提示等,模拟原生终端窗口支持 ← →
16 | 光标切换和 ↑ ↓ 历史命令切换。
17 |
18 | > :tada: 新版文档已开放访问,文档更详细界面更友好,欢迎体验:[https://tzfun.github.io/vue-web-terminal/](https://tzfun.github.io/vue-web-terminal/)
19 |
20 | ## 功能支持
21 |
22 | * 支持消息格式:普通文本、表格、json、代码/多行文本、html、ansi
23 | * 支持内容实时回显、追加,可制作简单的动画效果
24 | * 支持用户问答输入
25 | * 支持在线文本编辑
26 | * 支持窗口拖拽、固定
27 | * 支持 ← → 光标键切换和 ↑ ↓ 键历史命令切换
28 | * 支持一键全屏
29 | * 支持命令输入提示
30 | * 支持日志记录分组折叠
31 | * 支持多种样式 Slot 插槽,可自定义样式
32 | * 支持主题,默认内置暗色和亮色主题,也可自定义主题
33 | * 提供丰富的JS API,几乎所有功能均可由API来模拟非人为操作
34 | * 支持Vue2/Vue3
35 |
36 | 
37 |
38 | > 一句话描述:
39 | >
40 | > 它并不具备执行某个具体命令的能力,这个能力需要开发者自己去实现,它负责的事情是在网页上以终端界面的形式从用户那拿到想要执行的命令,然后交给开发者去实现,执行完之后再交给它展示给用户。
41 |
42 | # 在线体验
43 |
44 | 你可以通过 [在线体验](https://tzfun.github.io/vue-web-terminal/demo.html) 了解本插件的一些功能,也可以在 [](https://codesandbox.io/s/silly-scooby-l8wk9b) 上尝试编辑代码并预览。
45 |
46 | # 文档
47 |
48 | 请前往 [Document](https://tzfun.github.io/vue-web-terminal/) 阅读使用文档
49 |
50 | # 快速上手
51 |
52 | > **Vue2**版本从 **2024年12月24日** 开始正式归档,不再提供维护更新,
53 | > 源码见 [vue2分支](https://github.com/tzfun/vue-web-terminal/tree/vue2)。
54 |
55 | npm安装vue-web-terminal,`2.x.x`版本对应vue2,`3.x.x`版本对应vue3,建议下载对应大版本的最新版。
56 |
57 | ```shell
58 | # install for vue2
59 | npm install vue-web-terminal@2.xx --save
60 |
61 | # install for vue3
62 | npm install vue-web-terminal@3.xx --save
63 | ```
64 |
65 | `main`中载入 Terminal 插件
66 |
67 | **Vue2**
68 | ```js
69 | import Terminal from 'vue-web-terminal'
70 |
71 | Vue.use(Terminal)
72 | ```
73 |
74 | **Vue3**
75 | ```ts
76 | import { createTerminal } from 'vue-web-terminal'
77 |
78 | const app = createApp(App)
79 |
80 | app.use(createTerminal())
81 |
82 | app.mount('#app')
83 | ```
84 |
85 | 使用示例
86 |
87 | ```vue
88 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
121 | ```
122 |
123 | # 联系作者
124 |
125 | 我是一名后端Coder,恰巧对前端也会一点,个人兴趣开发了此插件。
126 |
127 | 如果对代码优化或功能有好的想法并乐意贡献代码欢迎提交[PR](https://github.com/tzfun/vue-web-terminal/pulls)
128 | ,对插件使用存在疑问或发现bug请提交[issue](https://github.com/tzfun/vue-web-terminal/issues)。
129 |
130 | > 联系我(添加请备注vue-web-terminal)
131 | >
132 | > 📮 Email: *beifengtz@qq.com*
133 | >
134 | >  微信: *beifeng-tz*
135 |
136 | # License
137 |
138 | [Apache License 2.0](LICENSE)
139 |
--------------------------------------------------------------------------------
/design/logo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vue-web-terminal design
6 |
47 |
48 |
49 | 生成图片
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
91 |
--------------------------------------------------------------------------------
/design/vue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tzfun/vue-web-terminal/64e018f2107e8bf6aae2f095f7d6e735baf534da/design/vue.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vue-web-terminal demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import fs from 'fs'
3 |
4 | function copyDir(src, dest) {
5 | fs.mkdirSync(dest)
6 | for (let f of fs.readdirSync(src)) {
7 | fs.copyFileSync(path.join(src, f), path.join(dest, f));
8 | }
9 | console.log('copied', `'${src}'`, '-->', `'${dest}'`)
10 | }
11 |
12 | function deleteFile(fileOrDirName) {
13 | let stat = fs.statSync(fileOrDirName)
14 | if (stat.isDirectory()) {
15 | for (let f of fs.readdirSync(fileOrDirName)) {
16 | let filePath = path.join(fileOrDirName, f)
17 | deleteFile(filePath)
18 | }
19 | fs.rmdirSync(fileOrDirName)
20 | } else {
21 | fs.unlinkSync(fileOrDirName)
22 | console.log(`deleted ==> ${fileOrDirName}`)
23 | }
24 | }
25 |
26 | function deleteFileWithPattern(dir, regexp) {
27 | for (let f of fs.readdirSync(dir)) {
28 | if (f.match(regexp)) {
29 | fs.unlinkSync(path.join(dir, f))
30 | console.log(`deleted ==> ${dir}/${f}`)
31 | }
32 | }
33 | }
34 |
35 | function copyTypes() {
36 | fs.copyFileSync('./lib/types/index.d.ts', './lib/types.d.ts')
37 | deleteFile('./lib/types')
38 | console.log(`copied types.d.ts`)
39 | }
40 |
41 | // copyDir('./src/css/theme', './lib/theme');
42 | deleteFile('./lib/ansi')
43 | deleteFile('./lib/common')
44 | deleteFile('./lib/components')
45 | deleteFileWithPattern('./lib', /\.(png|gif|jpg)/)
46 | // copyTypes()
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-web-terminal",
3 | "version": "3.4.1",
4 | "description": "A beautiful web-side command line window plugin (native simulation). 一个漂亮的网页命令行插件(原生模拟)",
5 | "license": "Apache-2.0",
6 | "private": false,
7 | "type": "module",
8 | "main": "lib/vue-web-terminal.umd.cjs",
9 | "module": "lib/vue-web-terminal.js",
10 | "types": "./lib/index.d.ts",
11 | "exports": {
12 | ".": {
13 | "import": "./lib/vue-web-terminal.js",
14 | "require": "./lib/vue-web-terminal.umd.cjs",
15 | "types": [
16 | "./lib/index.d.ts",
17 | "./lib/Terminal.vue.d.ts",
18 | "./lib/types/index.d.ts"
19 | ]
20 | }
21 | },
22 | "files": [
23 | "package.json",
24 | "README.md",
25 | "LICENSE",
26 | "lib"
27 | ],
28 | "publishConfig": {
29 | "registry": "https://registry.npmjs.org",
30 | "access": "public"
31 | },
32 | "scripts": {
33 | "test": "vite --host",
34 | "build": "vite build&& vue-tsc",
35 | "preview": "vite preview"
36 | },
37 | "dependencies": {
38 | "vue": "^3.3.4",
39 | "vue-json-viewer": "^3.0.4"
40 | },
41 | "devDependencies": {
42 | "@types/node": "20.16.13",
43 | "@vitejs/plugin-vue": "4.6.2",
44 | "rollup-plugin-visualizer": "5.9.3",
45 | "sass": "1.62.1",
46 | "terser": "5.37.0",
47 | "typescript": "5.4.5",
48 | "vite": "5.4.6",
49 | "vue-tsc": "1.8.26",
50 | "vite-plugin-css-injected-by-js": "3.3.1",
51 | "vite-plugin-dts": "3.6.3"
52 | },
53 | "browserslist": [
54 | "> 1%",
55 | "last 2 versions",
56 | "not dead",
57 | "not ie 11"
58 | ],
59 | "repository": {
60 | "type": "git",
61 | "url": "https://github.com/tzfun/vue-web-terminal.git"
62 | },
63 | "bugs": {
64 | "url": "https://github.com/tzfun/vue-web-terminal/issues"
65 | },
66 | "homepage": "https://tzfun.github.io/vue-web-terminal/",
67 | "keywords": [
68 | "terminal",
69 | "vue",
70 | "vue3",
71 | "web-terminal",
72 | "vue-terminal",
73 | "console",
74 | "web-console",
75 | "vue-console"
76 | ],
77 | "author": "beifengtz"
78 | }
79 |
--------------------------------------------------------------------------------
/publish.py:
--------------------------------------------------------------------------------
1 | # encoding=utf8
2 |
3 | import subprocess
4 | import platform
5 |
6 | NPM_REPOSITORY="https://registry.npmjs.org"
7 | NPM_REPOSITORY_MIRROR="https://registry.npmmirror.com"
8 | NODE_VERSION = 18
9 | ENCODING = "gbk" if platform.system().lower() == 'windows' else 'utf-8'
10 |
11 | def execute(command, with_result = False,encoding = ENCODING):
12 | print(f"[calling]: {command}")
13 | if with_result:
14 | p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding=encoding)
15 | stdout,stderr = p.communicate()
16 | if p.returncode == 0:
17 | return stdout
18 | else:
19 | raise Exception(f"exit with {p.returncode}, stderr: {stderr}")
20 | else:
21 | returncode = subprocess.call(command, shell=True, encoding=encoding)
22 | if returncode != 0:
23 | raise Exception(f"exit with {returncode}")
24 |
25 | if __name__ == '__main__':
26 | registry = execute(f'npm config get registry', with_result=True)
27 | node_version = execute(f'node -v', with_result=True)
28 | if not node_version.startswith(f'v{NODE_VERSION}'):
29 | raise Exception(f'Node version must be {NODE_VERSION}')
30 | try:
31 | execute(f'pnpm install')
32 | execute(f'pnpm run build')
33 | execute(f'npm config set registry {NPM_REPOSITORY}')
34 | execute(f'npm publish')
35 | finally:
36 | execute(f'npm config set registry {registry}')
37 |
--------------------------------------------------------------------------------
/shortcut/dragging.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tzfun/vue-web-terminal/64e018f2107e8bf6aae2f095f7d6e735baf534da/shortcut/dragging.gif
--------------------------------------------------------------------------------
/shortcut/ele-info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tzfun/vue-web-terminal/64e018f2107e8bf6aae2f095f7d6e735baf534da/shortcut/ele-info.png
--------------------------------------------------------------------------------
/shortcut/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tzfun/vue-web-terminal/64e018f2107e8bf6aae2f095f7d6e735baf534da/shortcut/logo.png
--------------------------------------------------------------------------------
/shortcut/vue-web-terminal.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tzfun/vue-web-terminal/64e018f2107e8bf6aae2f095f7d6e735baf534da/shortcut/vue-web-terminal.gif
--------------------------------------------------------------------------------
/src/Terminal.vue:
--------------------------------------------------------------------------------
1 |
2076 |
2077 |
2078 |
2082 |
2083 |
2084 |
2085 |
2086 |
2087 |
2088 |
2089 |
2090 |
2103 |
2112 |
2121 |
2122 |
2123 | +
2126 | -
2127 |
2128 |
2129 |
2130 |
2134 |
2135 |
2136 |
2137 |
2138 |
2139 |
2140 |
2141 |
2142 |
2143 |
2144 |
2145 |
2146 |
2147 |
2148 |
2149 |
2150 |
2151 |
2152 |
2153 |
2154 |
2155 |
2156 |
2157 |
2158 |
2159 |
2160 |
2161 |
2162 |
2163 |
2164 |
2165 |
2166 |
2167 |
2168 |
2169 |
2170 |
2171 |
2172 |
2173 |
2184 |
2188 |
2189 | {{ context }}
2190 | {{ contextSuffix }}
2191 |
2196 |
2200 |
2201 |
2206 |
2207 |
2208 |
2209 |
2210 |
2213 |
2214 |
2229 |
2230 |
2231 |
2232 |
2233 |
2234 |
2238 |
2239 |
2240 |
2241 |
2243 |
2244 |
2248 |
2249 |
2250 |
2251 |
2252 | aaaaaaaaaa
2253 | 你你你你你你你你你你
2254 |
2255 |
2256 |
2257 |
2258 |
2261 |
--------------------------------------------------------------------------------
/src/ansi/ansi-256-colors.json:
--------------------------------------------------------------------------------
1 | {
2 | "c0": "rgb(0,0,0)",
3 | "c1": "rgb(128,0,0)",
4 | "c2": "rgb(0,128,0)",
5 | "c3": "rgb(128,128,0)",
6 | "c4": "rgb(0,0,128)",
7 | "c5": "rgb(128,0,128)",
8 | "c6": "rgb(0,128,128)",
9 | "c7": "rgb(192,192,192)",
10 | "c8": "rgb(128,128,128)",
11 | "c9": "rgb(255,0,0)",
12 | "c10": "rgb(0,255,0)",
13 | "c11": "rgb(255,255,0)",
14 | "c12": "rgb(0,0,255)",
15 | "c13": "rgb(255,0,255)",
16 | "c14": "rgb(0,255,255)",
17 | "c15": "rgb(255,255,255)",
18 | "c16": "rgb(0,0,0)",
19 | "c17": "rgb(0,0,95)",
20 | "c18": "rgb(0,0,135)",
21 | "c19": "rgb(0,0,175)",
22 | "c20": "rgb(0,0,215)",
23 | "c21": "rgb(0,0,255)",
24 | "c22": "rgb(0,95,0)",
25 | "c23": "rgb(0,95,95)",
26 | "c24": "rgb(0,95,135)",
27 | "c25": "rgb(0,95,175)",
28 | "c26": "rgb(0,95,215)",
29 | "c27": "rgb(0,95,255)",
30 | "c28": "rgb(0,135,0)",
31 | "c29": "rgb(0,135,95)",
32 | "c30": "rgb(0,135,135)",
33 | "c31": "rgb(0,135,175)",
34 | "c32": "rgb(0,135,215)",
35 | "c33": "rgb(0,135,255)",
36 | "c34": "rgb(0,175,0)",
37 | "c35": "rgb(0,175,95)",
38 | "c36": "rgb(0,175,135)",
39 | "c37": "rgb(0,175,175)",
40 | "c38": "rgb(0,175,215)",
41 | "c39": "rgb(0,175,255)",
42 | "c40": "rgb(0,215,0)",
43 | "c41": "rgb(0,215,95)",
44 | "c42": "rgb(0,215,135)",
45 | "c43": "rgb(0,215,175)",
46 | "c44": "rgb(0,215,215)",
47 | "c45": "rgb(0,215,255)",
48 | "c46": "rgb(0,255,0)",
49 | "c47": "rgb(0,255,95)",
50 | "c48": "rgb(0,255,135)",
51 | "c49": "rgb(0,255,175)",
52 | "c50": "rgb(0,255,215)",
53 | "c51": "rgb(0,255,255)",
54 | "c52": "rgb(95,0,0)",
55 | "c53": "rgb(95,0,95)",
56 | "c54": "rgb(95,0,135)",
57 | "c55": "rgb(95,0,175)",
58 | "c56": "rgb(95,0,215)",
59 | "c57": "rgb(95,0,255)",
60 | "c58": "rgb(95,95,0)",
61 | "c59": "rgb(95,95,95)",
62 | "c60": "rgb(95,95,135)",
63 | "c61": "rgb(95,95,175)",
64 | "c62": "rgb(95,95,215)",
65 | "c63": "rgb(95,95,255)",
66 | "c64": "rgb(95,135,0)",
67 | "c65": "rgb(95,135,95)",
68 | "c66": "rgb(95,135,135)",
69 | "c67": "rgb(95,135,175)",
70 | "c68": "rgb(95,135,215)",
71 | "c69": "rgb(95,135,255)",
72 | "c70": "rgb(95,175,0)",
73 | "c71": "rgb(95,175,95)",
74 | "c72": "rgb(95,175,135)",
75 | "c73": "rgb(95,175,175)",
76 | "c74": "rgb(95,175,215)",
77 | "c75": "rgb(95,175,255)",
78 | "c76": "rgb(95,215,0)",
79 | "c77": "rgb(95,215,95)",
80 | "c78": "rgb(95,215,135)",
81 | "c79": "rgb(95,215,175)",
82 | "c80": "rgb(95,215,215)",
83 | "c81": "rgb(95,215,255)",
84 | "c82": "rgb(95,255,0)",
85 | "c83": "rgb(95,255,95)",
86 | "c84": "rgb(95,255,135)",
87 | "c85": "rgb(95,255,175)",
88 | "c86": "rgb(95,255,215)",
89 | "c87": "rgb(95,255,255)",
90 | "c88": "rgb(135,0,0)",
91 | "c89": "rgb(135,0,95)",
92 | "c90": "rgb(135,0,135)",
93 | "c91": "rgb(135,0,175)",
94 | "c92": "rgb(135,0,215)",
95 | "c93": "rgb(135,0,255)",
96 | "c94": "rgb(135,95,0)",
97 | "c95": "rgb(135,95,95)",
98 | "c96": "rgb(135,95,135)",
99 | "c97": "rgb(135,95,175)",
100 | "c98": "rgb(135,95,215)",
101 | "c99": "rgb(135,95,255)",
102 | "c100": "rgb(135,135,0)",
103 | "c101": "rgb(135,135,95)",
104 | "c102": "rgb(135,135,135)",
105 | "c103": "rgb(135,135,175)",
106 | "c104": "rgb(135,135,215)",
107 | "c105": "rgb(135,135,255)",
108 | "c106": "rgb(135,175,0)",
109 | "c107": "rgb(135,175,95)",
110 | "c108": "rgb(135,175,135)",
111 | "c109": "rgb(135,175,175)",
112 | "c110": "rgb(135,175,215)",
113 | "c111": "rgb(135,175,255)",
114 | "c112": "rgb(135,215,0)",
115 | "c113": "rgb(135,215,95)",
116 | "c114": "rgb(135,215,135)",
117 | "c115": "rgb(135,215,175)",
118 | "c116": "rgb(135,215,215)",
119 | "c117": "rgb(135,215,255)",
120 | "c118": "rgb(135,255,0)",
121 | "c119": "rgb(135,255,95)",
122 | "c120": "rgb(135,255,135)",
123 | "c121": "rgb(135,255,175)",
124 | "c122": "rgb(135,255,215)",
125 | "c123": "rgb(135,255,255)",
126 | "c124": "rgb(175,0,0)",
127 | "c125": "rgb(175,0,95)",
128 | "c126": "rgb(175,0,135)",
129 | "c127": "rgb(175,0,175)",
130 | "c128": "rgb(175,0,215)",
131 | "c129": "rgb(175,0,255)",
132 | "c130": "rgb(175,95,0)",
133 | "c131": "rgb(175,95,95)",
134 | "c132": "rgb(175,95,135)",
135 | "c133": "rgb(175,95,175)",
136 | "c134": "rgb(175,95,215)",
137 | "c135": "rgb(175,95,255)",
138 | "c136": "rgb(175,135,0)",
139 | "c137": "rgb(175,135,95)",
140 | "c138": "rgb(175,135,135)",
141 | "c139": "rgb(175,135,175)",
142 | "c140": "rgb(175,135,215)",
143 | "c141": "rgb(175,135,255)",
144 | "c142": "rgb(175,175,0)",
145 | "c143": "rgb(175,175,95)",
146 | "c144": "rgb(175,175,135)",
147 | "c145": "rgb(175,175,175)",
148 | "c146": "rgb(175,175,215)",
149 | "c147": "rgb(175,175,255)",
150 | "c148": "rgb(175,215,0)",
151 | "c149": "rgb(175,215,95)",
152 | "c150": "rgb(175,215,135)",
153 | "c151": "rgb(175,215,175)",
154 | "c152": "rgb(175,215,215)",
155 | "c153": "rgb(175,215,255)",
156 | "c154": "rgb(175,255,0)",
157 | "c155": "rgb(175,255,95)",
158 | "c156": "rgb(175,255,135)",
159 | "c157": "rgb(175,255,175)",
160 | "c158": "rgb(175,255,215)",
161 | "c159": "rgb(175,255,255)",
162 | "c160": "rgb(215,0,0)",
163 | "c161": "rgb(215,0,95)",
164 | "c162": "rgb(215,0,135)",
165 | "c163": "rgb(215,0,175)",
166 | "c164": "rgb(215,0,215)",
167 | "c165": "rgb(215,0,255)",
168 | "c166": "rgb(215,95,0)",
169 | "c167": "rgb(215,95,95)",
170 | "c168": "rgb(215,95,135)",
171 | "c169": "rgb(215,95,175)",
172 | "c170": "rgb(215,95,215)",
173 | "c171": "rgb(215,95,255)",
174 | "c172": "rgb(215,135,0)",
175 | "c173": "rgb(215,135,95)",
176 | "c174": "rgb(215,135,135)",
177 | "c175": "rgb(215,135,175)",
178 | "c176": "rgb(215,135,215)",
179 | "c177": "rgb(215,135,255)",
180 | "c178": "rgb(215,175,0)",
181 | "c179": "rgb(215,175,95)",
182 | "c180": "rgb(215,175,135)",
183 | "c181": "rgb(215,175,175)",
184 | "c182": "rgb(215,175,215)",
185 | "c183": "rgb(215,175,255)",
186 | "c184": "rgb(215,215,0)",
187 | "c185": "rgb(215,215,95)",
188 | "c186": "rgb(215,215,135)",
189 | "c187": "rgb(215,215,175)",
190 | "c188": "rgb(215,215,215)",
191 | "c189": "rgb(215,215,255)",
192 | "c190": "rgb(215,255,0)",
193 | "c191": "rgb(215,255,95)",
194 | "c192": "rgb(215,255,135)",
195 | "c193": "rgb(215,255,175)",
196 | "c194": "rgb(215,255,215)",
197 | "c195": "rgb(215,255,255)",
198 | "c196": "rgb(255,0,0)",
199 | "c197": "rgb(255,0,95)",
200 | "c198": "rgb(255,0,135)",
201 | "c199": "rgb(255,0,175)",
202 | "c200": "rgb(255,0,215)",
203 | "c201": "rgb(255,0,255)",
204 | "c202": "rgb(255,95,0)",
205 | "c203": "rgb(255,95,95)",
206 | "c204": "rgb(255,95,135)",
207 | "c205": "rgb(255,95,175)",
208 | "c206": "rgb(255,95,215)",
209 | "c207": "rgb(255,95,255)",
210 | "c208": "rgb(255,135,0)",
211 | "c209": "rgb(255,135,95)",
212 | "c210": "rgb(255,135,135)",
213 | "c211": "rgb(255,135,175)",
214 | "c212": "rgb(255,135,215)",
215 | "c213": "rgb(255,135,255)",
216 | "c214": "rgb(255,175,0)",
217 | "c215": "rgb(255,175,95)",
218 | "c216": "rgb(255,175,135)",
219 | "c217": "rgb(255,175,175)",
220 | "c218": "rgb(255,175,215)",
221 | "c219": "rgb(255,175,255)",
222 | "c220": "rgb(255,215,0)",
223 | "c221": "rgb(255,215,95)",
224 | "c222": "rgb(255,215,135)",
225 | "c223": "rgb(255,215,175)",
226 | "c224": "rgb(255,215,215)",
227 | "c225": "rgb(255,215,255)",
228 | "c226": "rgb(255,255,0)",
229 | "c227": "rgb(255,255,95)",
230 | "c228": "rgb(255,255,135)",
231 | "c229": "rgb(255,255,175)",
232 | "c230": "rgb(255,255,215)",
233 | "c231": "rgb(255,255,255)",
234 | "c232": "rgb(8,8,8)",
235 | "c233": "rgb(18,18,18)",
236 | "c234": "rgb(28,28,28)",
237 | "c235": "rgb(38,38,38)",
238 | "c236": "rgb(48,48,48)",
239 | "c237": "rgb(58,58,58)",
240 | "c238": "rgb(68,68,68)",
241 | "c239": "rgb(78,78,78)",
242 | "c240": "rgb(88,88,88)",
243 | "c241": "rgb(98,98,98)",
244 | "c242": "rgb(108,108,108)",
245 | "c243": "rgb(118,118,118)",
246 | "c244": "rgb(128,128,128)",
247 | "c245": "rgb(138,138,138)",
248 | "c246": "rgb(148,148,148)",
249 | "c247": "rgb(158,158,158)",
250 | "c248": "rgb(168,168,168)",
251 | "c249": "rgb(178,178,178)",
252 | "c250": "rgb(188,188,188)",
253 | "c251": "rgb(198,198,198)",
254 | "c252": "rgb(208,208,208)",
255 | "c253": "rgb(218,218,218)",
256 | "c254": "rgb(228,228,228)",
257 | "c255": "rgb(238,238,238)"
258 | }
259 |
--------------------------------------------------------------------------------
/src/ansi/index.ts:
--------------------------------------------------------------------------------
1 | import ansi256colors from "./ansi-256-colors.json";
2 |
3 | export const ANSI_NUL = '\x00'
4 | export const ANSI_BEL = '\x07'
5 | export const ANSI_BS = '\x08'
6 | export const ANSI_CR = '\x0D'
7 | export const ANSI_ENQ = '\x05'
8 | export const ANSI_FF = '\x0C'
9 | export const ANSI_LF = '\x0A'
10 | export const ANSI_SO = '\x0E'
11 | export const ANSI_SP = '\x20'
12 | export const ANSI_TAB = '\x09'
13 | export const ANSI_VT = '\x0B'
14 | export const ANSI_SI = '\x0F'
15 | export const ANSI_ESC = '\x1B'
16 |
17 | export const ANSI_IND = ANSI_ESC + 'D' + '\u0000'
18 | export const ANSI_NEL = ANSI_ESC + 'E'
19 | export const ANSI_HTS = ANSI_ESC + 'H'
20 | export const ANSI_RI = ANSI_ESC + 'M'
21 | export const ANSI_SS2 = ANSI_ESC + 'N'
22 | export const ANSI_SS3 = ANSI_ESC + 'O'
23 | export const ANSI_DCS = ANSI_ESC + 'P'
24 | export const ANSI_SPA = ANSI_ESC + 'V'
25 | export const ANSI_EPA = ANSI_ESC + 'W'
26 | export const ANSI_SOS = ANSI_ESC + 'X'
27 | export const ANSI_DECID = ANSI_ESC + 'Z'
28 | export const ANSI_CSI = ANSI_ESC + '['
29 | export const ANSI_ST = ANSI_ESC + '\\'
30 | export const ANSI_OSC = ANSI_ESC + ']'
31 | export const ANSI_PM = ANSI_ESC + '^'
32 | export const ANSI_APC = ANSI_ESC + '_'
33 | export const ANSI_DECPAM = ANSI_ESC + '='
34 | export const ANSI_DECPNM = ANSI_ESC + '>'
35 | export const ANSI_DESIGNATE_CHARSET_0 = ANSI_ESC + '('
36 | export const ANSI_DESIGNATE_CHARSET_1 = ANSI_ESC + ')'
37 | export const ANSI_DESIGNATE_CHARSET_2 = ANSI_ESC + '*'
38 | export const ANSI_DESIGNATE_CHARSET_3 = ANSI_ESC + '+'
39 | export const ANSI_CHARSET = ANSI_ESC + '%'
40 | export const ANSI_DECD = ANSI_ESC + '#'
41 |
42 | /**
43 | * 解析并过滤ANSI Code,目前仅对着色码翻译,其余码过滤
44 | *
45 | * @param str
46 | * @param os windows | mac | linux | unix
47 | * @returns string
48 | * @private
49 | */
50 | export function _parseANSI(str: string, os: 'windows' | 'mac' | 'linux' | 'unix' = 'windows'): string {
51 | let lines = ['']
52 | let data = {
53 | attachStyle: '',
54 | styleFlag: >[]
55 | }
56 |
57 | let lastChar = {
58 | dom: null,
59 | attachStyle: ''
60 | };
61 |
62 | function getLastCharDomStr(): string {
63 | if (!lastChar.dom) {
64 | return ''
65 | }
66 | let container = document.createElement("div");
67 | container.appendChild(lastChar.dom)
68 | let domStr = container.innerHTML
69 | container = null
70 | return domStr
71 | }
72 |
73 | function updateLastChar(clazz: string | null, style: string, innerText: string): void {
74 | let dom = document.createElement('span')
75 | dom.className = clazz
76 | dom.setAttribute('style', style)
77 | dom.innerText = innerText
78 |
79 | lastChar.dom = dom
80 | lastChar.attachStyle = style
81 | }
82 |
83 | function checkDirtyChar(): void {
84 | if (lastChar.dom) {
85 | lines[lines.length - 1] = lines[lines.length - 1] + getLastCharDomStr()
86 | lastChar.dom = null
87 | lastChar.attachStyle = ''
88 | }
89 | }
90 |
91 | function newLine():void {
92 | checkDirtyChar()
93 | lines[lines.length - 1] = '' + lines[lines.length - 1] + '
'
94 | lines.push('')
95 | }
96 |
97 | function fillChar(char: string) {
98 | try {
99 | let arr = char.split('')
100 | for (let c of arr) {
101 | let clazz = "t-ansi-char"
102 | if (data.styleFlag.length > 0) {
103 | data.styleFlag.forEach(o => clazz += (' t-ansi-' + parseInt(String(o))))
104 | }
105 |
106 | let charStr = null
107 | if (lastChar.dom) {
108 | // 相同样式进行标签合并
109 | if (clazz === lastChar.dom.className && data.attachStyle === lastChar.attachStyle) {
110 | lastChar.dom.innerText += c
111 | } else {
112 | // 保存上一个dom并替换
113 | charStr = getLastCharDomStr()
114 | updateLastChar(clazz, data.attachStyle, c)
115 | }
116 | } else {
117 | updateLastChar(clazz, data.attachStyle, c)
118 | }
119 | if (charStr) {
120 | lines[lines.length - 1] = lines[lines.length - 1] + charStr
121 | }
122 | }
123 | } catch (e) {
124 | console.error('Can not fill char: ' + char.toString(), e)
125 | }
126 | }
127 |
128 | let arr = Array.from(str)
129 |
130 | for (let i = 0; i < arr.length; i++) {
131 | let c = arr[i]
132 | if (c === ANSI_NUL) {
133 | continue
134 | }
135 | // Control Sequence
136 | if (c === ANSI_ESC) {
137 | let flag = str.substring(i, i + 2)
138 | let y = i
139 | if (flag === ANSI_CSI) {
140 | const endFlagReg = /[@ABCDEFGHIJKLMPSTXZ`"bcdfghilmnpqrstwxz]/
141 | let controlType
142 | y = i + 1
143 | while (y < arr.length - 1) {
144 | let char = arr[++y]
145 | if (endFlagReg.test(char.toString())) {
146 | if (char === '`' && y + 1 < arr.length) {
147 | let next = arr[y + 1]
148 | if (/[wz{|]/.test(next.toString())) {
149 | controlType = char + next
150 | y++
151 | break
152 | }
153 | } else if (char === '"' && y + 1 < arr.length) {
154 | let next = arr[y + 1]
155 | if (/[pq]/.test(next.toString())) {
156 | controlType = char + next
157 | y++
158 | break
159 | }
160 | } else if (char === '&' && y + 1 < arr.length) {
161 | let next = arr[y + 1]
162 | if (next === 'w') {
163 | controlType = char + next
164 | y++
165 | break
166 | }
167 | }
168 | controlType = char
169 | break
170 | }
171 | }
172 |
173 | let cs = str.substring(i, y + 1)
174 |
175 | if (controlType === 'm') {
176 | let value = cs.substring(2, cs.length - 1)
177 | if (value.length === 0) {
178 | value = '0'
179 | }
180 | data.styleFlag = []
181 | for (let ps of value.split(";")) {
182 | let m = parseInt(ps)
183 | if (m === 0) {
184 | data.attachStyle = ''
185 | data.styleFlag = []
186 | } else {
187 | data.styleFlag.push(m)
188 | }
189 | }
190 |
191 | if (data.styleFlag.length === 3) {
192 | // 256前景色
193 | if (data.styleFlag[0] === 38 && data.styleFlag[1] === 5) {
194 | // @ts-ignore
195 | data.attachStyle += `color:${ansi256colors['c' + data.styleFlag[2]]};`
196 | data.styleFlag = []
197 | }
198 | // 256背景色
199 | else if (data.styleFlag[0] === 48 && data.styleFlag[1] === 5) {
200 | // @ts-ignore
201 | data.attachStyle += `background-color:${ansi256colors['c' + data.styleFlag[2]]};`
202 | data.styleFlag = []
203 | } else {
204 | data.attachStyle = ''
205 | }
206 | }
207 | }
208 | }
209 | // 窗口信息同步
210 | else if (flag === ANSI_OSC) {
211 | let p = i + 1
212 | while (p <= arr.length) {
213 | p++
214 | if (arr[p] === ANSI_BEL) {
215 | y = p
216 | break
217 | } else if (arr[p] === ANSI_ESC && arr[p] === '\\') {
218 | y = p + 1
219 | break
220 | }
221 | }
222 | } else if (flag === ANSI_PM) {
223 | // for xterm
224 | let p = i + 1
225 | while (p < arr.length) {
226 | ++p
227 | if (arr[p] === '\\') {
228 | break
229 | }
230 | }
231 | y = p
232 | }
233 | // 切换至Application Keypad
234 | else if (flag === ANSI_DECPAM) {
235 | y = i + 1
236 | }
237 | // 切换至常规键入模式
238 | else if (flag === ANSI_DECPNM) {
239 | y = i + 1
240 | }
241 | // 设置字符集
242 | else if (flag === ANSI_DESIGNATE_CHARSET_0 || flag === ANSI_DESIGNATE_CHARSET_1
243 | || ANSI_DESIGNATE_CHARSET_2 || ANSI_DESIGNATE_CHARSET_3) {
244 | // 忽略
245 | y = i + 2
246 | }
247 | // 设置字符编码
248 | else if (flag === ANSI_CHARSET) {
249 | // 忽略
250 | y = i + 2
251 | }
252 | // 设置字符和行 的 宽度、高度
253 | else if (flag === ANSI_DECD) {
254 | y = i + 2
255 | }
256 | i = y
257 | continue
258 | } else if (c === '\r') {
259 | if (os === 'windows') {
260 | if (i + 1 < arr.length && arr[i + 1] === '\n') { // \r\n换行
261 | newLine()
262 | i++
263 | } else { // \r回车
264 | newLine()
265 | }
266 | } else if (os === 'mac') {
267 | newLine()
268 | }
269 | continue
270 | } else if (c === '\n') {
271 | newLine()
272 | continue
273 | } else if (c === '\b') { // 退格
274 | continue
275 | } else if (c === '\t') { // 水平制表
276 | fillChar(' '.repeat(4))
277 | continue
278 | } else if (c >= '\x00' && c <= '\x1F') {
279 | // 特殊ascii,暂不做处理
280 | continue
281 | }
282 |
283 | fillChar(c)
284 | }
285 |
286 | checkDirtyChar()
287 |
288 | return lines.join('');
289 | }
--------------------------------------------------------------------------------
/src/common/api/index.ts:
--------------------------------------------------------------------------------
1 | import {TerminalApi, TerminalApiData, TerminalApiListenerFunc, TerminalConfiguration} from "~/types";
2 | import {_isEmpty} from "~/common/util.ts";
3 |
4 | const data: TerminalApiData = {
5 | pool: {},
6 | configuration: {
7 | maxStoredCommandCountPerInstance: 100,
8 | storeName: 'terminal',
9 | themes: {}
10 | }
11 | }
12 |
13 | export function register(name: string, listener: TerminalApiListenerFunc) {
14 | if (data.pool[name]) {
15 | throw Error(`Unable to register an existing terminal: ${name}`)
16 | }
17 | data.pool[name] = listener
18 | }
19 |
20 | export function unregister(name: string) {
21 | delete data.pool[name]
22 | }
23 |
24 | export function rename(newName: string, oldName: string, listener: TerminalApiListenerFunc) {
25 | unregister(oldName)
26 | register(newName, listener);
27 | }
28 |
29 | export function configTheme(theme: string, css: string) {
30 | let res = css.match(/^.*\{(.*)}\s*$/s)
31 | if (!res || res.length != 2) {
32 | throw new Error(`Incorrect theme style format, correct format example:
33 | :root {
34 | --t-main-background-color: #191b24;
35 | --t-main-font-color: #fff;
36 | ...
37 | }
38 | `)
39 | }
40 | let themes = data.configuration.themes
41 | if (!themes) {
42 | data.configuration.themes = themes = {}
43 | }
44 | themes[theme] = css
45 | }
46 |
47 | export function configStoreName(name: string) {
48 | if (_isEmpty(name)) {
49 | throw new Error("The terminal storage name is invalid: " + name)
50 | }
51 | data.configuration.storeName = name
52 | console.debug("Configured storeName", name)
53 | }
54 |
55 | export function configMaxStoredCommandCountPerInstance(count: number) {
56 | if (count <= 1) {
57 | throw new Error("The value of 'maxStoredLogCountPerInstance' must be a valid positive number: " + count)
58 | }
59 | data.configuration.maxStoredCommandCountPerInstance = count
60 | console.debug("Configured maxStoredCommandCountPerInstance", count)
61 | }
62 |
63 | export function getConfiguration(): TerminalConfiguration {
64 | return data.configuration
65 | }
66 |
67 | export default new TerminalApi(data)
68 |
--------------------------------------------------------------------------------
/src/common/configuration.ts:
--------------------------------------------------------------------------------
1 | import {Command} from "~/types";
2 |
3 | export const DEFAULT_COMMANDS:Command[] = [
4 | {
5 | key: 'help',
6 | title: 'Help',
7 | group: 'local',
8 | usage: 'help [pattern]',
9 | description: 'Show command document.',
10 | example: [
11 | {
12 | des: "Get all commands.",
13 | cmd: 'help'
14 | }, {
15 | des: "Get help documentation for exact match commands.",
16 | cmd: 'help refresh'
17 | }, {
18 | des: "Get help documentation for fuzzy matching commands.",
19 | cmd: 'help *e*'
20 | }, {
21 | des: "Get help documentation for specified group, match key must start with ':'.",
22 | cmd: 'help :groupA'
23 | }
24 | ]
25 | }, {
26 | key: 'clear',
27 | title: 'Clear screen or history logs',
28 | group: 'local',
29 | usage: 'clear [history]',
30 | description: 'Clear screen or history.',
31 | example: [
32 | {
33 | cmd: 'clear',
34 | des: 'Clear all records on the current screen.'
35 | }, {
36 | cmd: 'clear history',
37 | des: 'Clear command history'
38 | }
39 | ]
40 | }, {
41 | key: 'open',
42 | title: 'Open page',
43 | group: 'local',
44 | usage: 'open ',
45 | description: 'Open a specified page.',
46 | example: [
47 | {
48 | cmd: 'open blog.beifengtz.com'
49 | }
50 | ]
51 | }
52 | ]
53 |
54 | export const WINDOW_STYLE = {
55 | PADDING_LEFT: 10,
56 | PADDING_LEFT_FOLD: 20,
57 | PADDING_RIGHT: 10,
58 | PADDING_TOP: 0,
59 | PADDING_BOTTOM: 0
60 | }
--------------------------------------------------------------------------------
/src/common/store/index.ts:
--------------------------------------------------------------------------------
1 | import {CmdHistory} from "~/types";
2 | import {getConfiguration} from "~/common/api";
3 | import {ref} from "vue";
4 |
5 | const store = ref()
6 |
7 | export class TerminalStore {
8 | storageKey: string
9 | maxStoredCommandCountPerInstance: number
10 | dataMap: Record
11 |
12 | constructor(key: string, maxStoredCommandCountPerInstance: number) {
13 | this.storageKey = key
14 | this.maxStoredCommandCountPerInstance = maxStoredCommandCountPerInstance
15 | let dataMapStr = window.localStorage.getItem(this.storageKey)
16 | if (dataMapStr) {
17 | this.dataMap = JSON.parse(dataMapStr)
18 | } else {
19 | this.dataMap = {}
20 | }
21 | }
22 |
23 | push(name: string, cmd: string) {
24 | let data = this.getData(name)
25 | if (data.cmdLog == null) {
26 | data.cmdLog = []
27 | }
28 | if (data.cmdLog.length === 0 || data.cmdLog[data.cmdLog.length - 1] !== cmd) {
29 | data.cmdLog.push(cmd)
30 |
31 | console.log(data.cmdLog.length, this.maxStoredCommandCountPerInstance)
32 |
33 | if (data.cmdLog.length > this.maxStoredCommandCountPerInstance) {
34 | data.cmdLog.splice(0, data.cmdLog.length - this.maxStoredCommandCountPerInstance)
35 | }
36 | }
37 |
38 | data.cmdIdx = data.cmdLog.length
39 | this.store()
40 | }
41 |
42 | store() {
43 | window.localStorage.setItem(this.storageKey, JSON.stringify(this.dataMap))
44 | }
45 |
46 | getData(name: string): CmdHistory {
47 | let data = this.dataMap[name]
48 | if (data == null) {
49 | data = {
50 | cmdLog: [],
51 | cmdIdx: 0
52 | }
53 | this.dataMap[name] = data
54 | }
55 | return data
56 | }
57 |
58 | getLog(name: string) {
59 | let data = this.getData(name)
60 | if (!data.cmdLog) {
61 | data.cmdLog = []
62 | }
63 | return data.cmdLog
64 | }
65 |
66 | clear(name: string) {
67 | let data = this.getData(name)
68 | data.cmdLog = []
69 | data.cmdIdx = 0
70 | this.store()
71 | }
72 |
73 | clearAll() {
74 | this.dataMap = {}
75 | this.store()
76 | }
77 |
78 | getIdx(name: string) {
79 | let data = this.getData(name)
80 | return data.cmdIdx | 0
81 | }
82 |
83 | setIdx(name: string, idx: number) {
84 | this.getData(name).cmdIdx = idx
85 | }
86 | }
87 |
88 | export function initStore() {
89 | const configuration = getConfiguration();
90 | store.value = new TerminalStore(configuration.storeName, configuration.maxStoredCommandCountPerInstance)
91 | }
92 |
93 | export function getStore(): TerminalStore {
94 | if (store.value) {
95 | return store.value
96 | }
97 | throw new Error("The store must be initialized before reading")
98 | }
99 |
--------------------------------------------------------------------------------
/src/common/util.ts:
--------------------------------------------------------------------------------
1 | import {ScreenType} from "~/types";
2 |
3 | /**
4 | * 将空格、回车、Tab转译为html
5 | *
6 | * @param str
7 | * @returns {*|string}
8 | * @private
9 | */
10 | export function _html(str: string): string {
11 | return String(str)
12 | .replace(/&(?!\w+;)/g, '&')
13 | .replace(/ /g, ' ')
14 | .replace(//g, '>')
16 | .replace(/"/g, '"')
17 | .replace(/'/g, ''')
18 | .replace(/\n/g, ' ')
19 | .replace(/\t/g, ' ');
20 | }
21 |
22 | /**
23 | * 判断一个对象是否为逻辑上的空
24 | *
25 | * @param value
26 | * @returns {boolean|boolean}
27 | * @private
28 | */
29 | export function _isEmpty(value: string | object | undefined | null): boolean {
30 | return (
31 | value === undefined ||
32 | value === null ||
33 | (typeof value === "string" && value.trim().length === 0) ||
34 | (typeof value === "object" && Object.keys(value).length === 0)
35 | );
36 | }
37 |
38 | export function _nonEmpty(value: string | object | undefined | null) {
39 | return !_isEmpty(value)
40 | }
41 |
42 | export function _screenType(width: number = document.body.clientWidth): ScreenType {
43 | let result: ScreenType = {}
44 | if (width < 600) {
45 | result.xs = true
46 | } else if (width >= 600 && width < 960) {
47 | result.sm = true
48 | } else if (width >= 960 && width < 1264) {
49 | result.md = true
50 | } else if (width >= 1264 && width < 1904) {
51 | result.lg = true
52 | } else {
53 | result.xl = true
54 | }
55 | return result as ScreenType
56 | }
57 |
58 | export function _isSafari() {
59 | return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
60 | }
61 |
62 | export function _getByteLen(val: string) {
63 | if (val.match(/[\n\r]/)) {
64 | return 0
65 | }
66 | let len = 0;
67 | for (let i = 0; i < val.length; i++) {
68 | // eslint-disable-next-line no-control-regex
69 | if (val[i].match(/[^\x00-\xff]/ig) != null) //全角
70 | len += 2; //如果是全角,占用两个字节
71 | else len += 1; //半角占用一个字节
72 | }
73 | return len;
74 | }
75 |
76 | /**
77 | * 获取两个连续字符串的不同部分
78 | *
79 | * @param one
80 | * @param two
81 | * @returns {string}
82 | */
83 | export function _getDifferent(one: string, two: string) {
84 | if (one === two) {
85 | return '';
86 | }
87 | let i = 0, j = 0;
88 | let longOne = one.length > two.length ? one : two;
89 | let shortOne = one.length > two.length ? two : one;
90 |
91 | let diff = '', nextChar = '';
92 | let hasDiff = false;
93 | while (i < shortOne.length || j < longOne.length) {
94 | if (shortOne[i] === longOne[j]) {
95 | if (hasDiff) {
96 | break;
97 | }
98 | i++;
99 | j++;
100 | } else {
101 | if (i < shortOne.length - 1) {
102 | nextChar = shortOne[i + 1]
103 | }
104 | if (longOne[j] === nextChar || j >= longOne.length) {
105 | break;
106 | } else {
107 | diff += longOne[j];
108 | }
109 | j++;
110 | hasDiff = true;
111 | }
112 | }
113 | return diff;
114 | }
115 |
116 | export function _eventOn(dom: Document | Window, eventName: string, handler: EventListenerOrEventListenerObject) {
117 | dom && dom.addEventListener && dom.addEventListener(eventName, handler);
118 | }
119 |
120 | export function _eventOff(dom: Document | Window, eventName: string, handler: EventListenerOrEventListenerObject) {
121 | dom && dom.removeEventListener && dom.removeEventListener(eventName, handler);
122 | }
123 |
124 | export function _getClipboardText(): Promise {
125 | if (navigator && navigator.clipboard) {
126 | return navigator.clipboard.readText();
127 | } else {
128 | return new Promise((resolve, reject) => {
129 | try {
130 | let pasteTarget = document.createElement("div");
131 | pasteTarget.contentEditable = "true";
132 | let actElem = document.activeElement.appendChild(pasteTarget).parentNode;
133 | pasteTarget.focus();
134 | // 可能会失败
135 | document.execCommand("paste")
136 | let paste = pasteTarget.innerText;
137 | actElem.removeChild(pasteTarget);
138 | resolve(paste)
139 | } catch (e) {
140 | reject(e)
141 | }
142 | })
143 | }
144 | }
145 |
146 | export function _copyTextToClipboard(text: string | null): Promise {
147 | if (!text) {
148 | return
149 | }
150 | text = text.replace(/nbsp;/g, ' ')
151 | if (navigator && navigator.clipboard) {
152 | return navigator.clipboard.writeText(text)
153 | } else {
154 | let textArea = document.createElement("textarea")
155 | textArea.value = text
156 | textArea.style.position = "absolute"
157 | textArea.style.opacity = "0"
158 | textArea.style.left = "-999999px"
159 | textArea.style.top = "-999999px"
160 | document.body.appendChild(textArea)
161 | textArea.focus()
162 | textArea.select()
163 | document.execCommand('copy')
164 | textArea.remove()
165 | return Promise.resolve()
166 | }
167 | }
168 |
169 | export function _pointInRect(point, rect) {
170 | const {x, y} = point;
171 | const dx = rect.x, dy = rect.y, width = rect.width, height = rect.height;
172 | return x >= dx && x <= dx + width && y >= dy && y <= dy + height;
173 | }
174 |
175 | export function _getSelection() {
176 | if (window.getSelection) {
177 | return window.getSelection()
178 | } else {
179 | return document.getSelection()
180 | }
181 | }
182 |
183 | export function _parseToJson(obj: any) {
184 | if (typeof obj === 'object' && obj) {
185 | return obj;
186 | } else if (typeof obj === 'string') {
187 | try {
188 | return JSON.parse(obj);
189 | } catch (e) {
190 | return obj;
191 | }
192 | }
193 | }
194 |
195 | export function _openUrl(url: string, pushMessage: Function) {
196 | let match = /^((http|https):\/\/)?(([A-Za-z0-9]+-[A-Za-z0-9]+|[A-Za-z0-9]+)\.)+([A-Za-z]+)[/?:]?.*$/;
197 | if (match.test(url)) {
198 | if (!url.startsWith("http") && !url.startsWith("https")) {
199 | window.open(`http://${url}`)
200 | } else {
201 | window.open(url);
202 | }
203 | } else {
204 | pushMessage({
205 | class: 'error',
206 | type: 'normal',
207 | content: "Invalid website url"
208 | })
209 | }
210 | }
211 |
212 | /**
213 | * 默认命令行样式格式化实现,对部分关键符号高亮处理。
214 | *
215 | * 此方法会对高亮字符进行合并,适用于记录命令行
216 | *
217 | * @param cmd
218 | * @return {string}
219 | * @private
220 | */
221 | export function _defaultMergedCommandFormatter(cmd: string): string {
222 | // 过滤ASCII 160的空白字符串
223 | let split = cmd.replace(/\xA0/g, " ").split(" ")
224 | let formatted = ''
225 | let isCmdKey = true
226 |
227 | for (let i = 0; i < split.length; i++) {
228 | let char = _html(split[i])
229 | if (isCmdKey) {
230 | formatted += `${char} `
231 | isCmdKey = false
232 | } else if (char.startsWith("-")) {
233 | formatted += `${char} `
234 | } else if (char === '\r') {
235 | // \r\n换行
236 | if (i < split.length - 1 && split[i + 1] === '\n') {
237 | formatted += ` `
238 | i++
239 | }
240 | // \r换行
241 | else {
242 | formatted += ` `
243 | }
244 | } else if (char === '\n') {
245 | formatted += ` `
246 | } else if (char.length > 0) {
247 | if (char === '|') {
248 | isCmdKey = true
249 | formatted += `${char} `
250 | } else {
251 | formatted += ''
252 | let startNewCmdKey = false
253 | const charArr: string[] = [...char];
254 | charArr.forEach((ch, index) => {
255 | if (ch === ',') {
256 | formatted += `${ch} `
257 | } else if (ch === '|') {
258 | formatted += ch
259 |
260 | isCmdKey = true
261 | if (index < char.length - 1) {
262 | formatted += ``
263 | startNewCmdKey = true
264 | }
265 | } else {
266 | formatted += ch
267 | }
268 | if (index == charArr.length - 1 && ch != '|') {
269 | isCmdKey = false
270 | }
271 | });
272 |
273 | formatted += ' '
274 | if (startNewCmdKey) {
275 | formatted += ' '
276 | }
277 | }
278 | }
279 | if (i < split.length - 1) {
280 | formatted += " "
281 | }
282 | }
283 |
284 | return formatted
285 | }
286 |
287 |
288 | /**
289 | * 默认命令行样式格式化实现,对部分关键符号高亮出路。
290 | *
291 | * 此方法会对每个字符进行分割,由单独的span约束,适用于编辑命令行
292 | *
293 | * @param cmd
294 | * @private
295 | */
296 | export function _defaultSplittableCommandFormatter(cmd: string): string {
297 | // 过滤ASCII 160的空白字符串
298 | let split = cmd.replace(/\xA0/g, " ").split(" ")
299 | let formatted = ''
300 | let isCmdKey = true
301 |
302 | function splitFill(clazz: string | null, char: string) {
303 | for (let c of char) {
304 | formatted += `${_html(c)} `
305 | }
306 | }
307 |
308 | for (let i = 0; i < split.length; i++) {
309 | let srcChar: string = split[i]
310 | if (isCmdKey) {
311 |
312 | splitFill('t-cmd-key', srcChar)
313 |
314 | isCmdKey = false
315 | } else if (srcChar.startsWith("-")) {
316 | splitFill('t-cmd-arg', srcChar)
317 | } else if (srcChar.length > 0) {
318 | if (srcChar === '|') {
319 | isCmdKey = true
320 | splitFill(null, srcChar)
321 | } else {
322 | let startNewCmdKey = false
323 | const charArr: string[] = [...srcChar]
324 | charArr.forEach((ch, j) => {
325 | if (ch === ',') {
326 | splitFill('t-cmd-splitter', ch)
327 | } else if (ch === '|') {
328 | splitFill(null, ch)
329 |
330 | isCmdKey = true
331 | if (j < charArr.length - 1) {
332 | startNewCmdKey = true
333 | }
334 | } else {
335 | if (startNewCmdKey) {
336 | splitFill('t-cmd-key', ch)
337 | } else {
338 | splitFill(null, ch)
339 | }
340 | }
341 | if (j === charArr.length - 1 && ch !== '|') {
342 | isCmdKey = false
343 | }
344 | })
345 | }
346 | }
347 | if (i < split.length - 1) {
348 | splitFill(null, ' ')
349 | }
350 | }
351 | return formatted
352 | }
353 |
354 | /**
355 | * 判断一个Dom A是否是另一个Dom B的孩子节点
356 | *
357 | * @param target 目标Dom,A
358 | * @param parent 父级Dom,B
359 | * @param clazz 中断类,当检索到当前节点拥有这个 class 时就中断搜索,用于优化处理,避免搜索整个Dom树
360 | * @return {boolean}
361 | * @private
362 | */
363 | export function _isParentDom(target, parent, clazz = null) {
364 | while (target) {
365 | if (target === parent) {
366 | return true;
367 | }
368 |
369 | if (clazz && target.classList.contains(clazz)) {
370 | break
371 | }
372 | target = target.parentElement
373 | }
374 | return false;
375 | }
376 |
377 | export function _isPhone() {
378 | let info = navigator.userAgent;
379 | if (info) {
380 | return /mobile/i.test(info)
381 | }
382 | let screen: ScreenType = _screenType()
383 | return screen.xs || screen.sm
384 | }
385 |
386 | export function _isPad() {
387 | let info = navigator.userAgent;
388 | if (info) {
389 | return /pad/i.test(info)
390 | }
391 | return _screenType().sm
392 | }
393 |
394 | /**
395 | *
396 | *
397 | * methods: {
398 | * _someMethod: _debounce(function () {
399 | * // ...
400 | * }, 100)
401 | * }
402 | *
403 | *
404 | * @param fn
405 | * @param delay
406 | * @return {(function(): void)|*}
407 | * @private
408 | */
409 | export function _debounce(fn: Function, delay: number = 200) {
410 | let timer = null;
411 | return function () {
412 | let _this = this
413 | let args = arguments
414 | if (timer) {
415 | clearTimeout(timer)
416 | }
417 | timer = setTimeout(function () {
418 | fn.apply(_this, args);
419 | }, delay);
420 | };
421 | }
422 |
423 | /**
424 | * 将配置值转为像素度单位值
425 | * @param value {number | string} 配置值,如果为number值则单位默认为px,如果为string只支持百分比和px单位配置
426 | * @param parentPixel 父元素的像素度,用于百分比计算
427 | * @param defaultValue 默认值,如果解析失败则使用默认值
428 | */
429 | export function _parsePixelFromValue(value: number | string, parentPixel: number, defaultValue: number): number {
430 | let pixel: number
431 | if (value) {
432 | if (typeof value === 'string') {
433 | if (value.endsWith("%")) {
434 | pixel = (parentPixel * (parseFloat(value) / 100))
435 | } else if (value.endsWith("px")) {
436 | pixel = parseFloat(value)
437 | } else {
438 | pixel = parseFloat(value)
439 | }
440 | } else if (typeof value === 'number') {
441 | pixel = value
442 | } else {
443 | pixel = defaultValue
444 | }
445 | } else {
446 | pixel = defaultValue
447 | }
448 | return pixel
449 | }
450 |
451 | export function _hash(str: string) {
452 | // Step 1: Initialize hash value
453 | let hash = 5381;
454 |
455 | // Step 2: Compute hash
456 | for (let i = 0; i < str.length; i++) {
457 | hash = (hash * 33) ^ str.charCodeAt(i);
458 | }
459 |
460 | // Step 3: Convert hash to 32-bit unsigned integer
461 | hash = hash >>> 0;
462 |
463 | // Step 4: Convert hash to hexadecimal string
464 | return hash.toString(16);
465 | }
466 |
--------------------------------------------------------------------------------
/src/components/TEditor.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
39 |
40 | Cancel
43 |
44 | Save & Close
47 |
48 |
49 |
50 |
51 |
52 |
58 |
--------------------------------------------------------------------------------
/src/components/THeader.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
94 |
95 |
96 |
106 |
--------------------------------------------------------------------------------
/src/components/THelpBox.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
24 |
25 |
Usage: {{ content.usage }}
26 |
27 |
28 |
29 | Example: {{ it.cmd }}
{{ it.des }}
30 |
31 |
32 |
33 | eg{{ (content.example.length > 1 ? (idx + 1) : '') }}:
34 |
35 |
36 |
37 | {{ it.cmd }}
38 | {{ it.des }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
50 |
--------------------------------------------------------------------------------
/src/components/TViewerCode.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
{{ message.content }}
14 |
15 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/src/components/TViewerJson.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
--------------------------------------------------------------------------------
/src/components/TViewerNormal.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | {{ message.tag == null ? message.class : message.tag }}
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/TViewerTable.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | {{ it }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/css/ansi.css:
--------------------------------------------------------------------------------
1 | .t-ansi-0 {
2 | /* no style */
3 | }
4 |
5 | .t-ansi-1 {
6 | font-weight: bold;
7 | }
8 |
9 | .t-ansi-2 {
10 | font-weight: 200;
11 | }
12 |
13 | .t-ansi-3 {
14 | font-style:oblique;
15 | }
16 |
17 | .t-ansi-4 {
18 | text-decoration: underline;
19 | }
20 |
21 | @keyframes t-blink {
22 | 0% {
23 | opacity: 1;
24 | }
25 | 50% {
26 | opacity: 1;
27 | }
28 | 50.01% {
29 | opacity: 0;
30 | }
31 | 100% {
32 | opacity: 0;
33 | }
34 | }
35 |
36 | .t-ansi-5, .t-ansi-6 {
37 | animation: t-blink 1s linear infinite;
38 | -webkit-animation: t-blink 1s linear infinite;
39 | -moz-animation: t-blink 1s linear infinite;
40 | -ms-animation: t-blink 1s linear infinite;
41 | -o-animation: t-blink 1s linear infinite;
42 | }
43 |
44 | .t-ansi-7 {
45 | background-color: white;
46 | color: #1C1D21;
47 | }
48 |
49 | .t-ansi-8 {
50 | visibility: hidden;
51 | }
52 |
53 | .t-ansi-9 {
54 | text-decoration:line-through;
55 | }
56 |
57 | .t-ansi-21,.t-ansi-22 {
58 | font-weight: unset;
59 | }
60 |
61 | .t-ansi-23 {
62 | font-style:unset;
63 | }
64 |
65 | .t-ansi-24, .t-ansi-29 {
66 | text-decoration: none;
67 | }
68 |
69 | .t-ansi-25, .t-ansi-26 {
70 | animation: none;
71 | -webkit-animation: none;
72 | -moz-animation: none;
73 | -ms-animation: none;
74 | -o-animation: none;
75 | }
76 |
77 | .t-ansi-27 {
78 | background-color: inherit;
79 | color: inherit;
80 | }
81 |
82 | .t-ansi-28 {
83 | visibility: unset;
84 | }
85 |
86 | /* 前景色 */
87 | .t-ansi-30 {
88 | color: black;
89 | }
90 |
91 | .t-ansi-31 {
92 | color: #f10606;
93 | }
94 |
95 | .t-ansi-32 {
96 | color: #14cb14;
97 | }
98 |
99 | .t-ansi-33 {
100 | color: yellow;
101 | }
102 |
103 | .t-ansi-34 {
104 | color: #3993d4;
105 | }
106 |
107 | .t-ansi-35 {
108 | color: #bd12bd;
109 | }
110 |
111 | .t-ansi-36 {
112 | color: #0eb4b4;
113 | }
114 |
115 | .t-ansi-37 {
116 | color: white;
117 | }
118 |
119 | .t-ansi-90 {
120 | color: #585859;
121 | }
122 |
123 | .t-ansi-91 {
124 | color: #ef353a;
125 | }
126 |
127 | .t-ansi-92 {
128 | color: #4ec215;
129 | }
130 |
131 | .t-ansi-93 {
132 | color: #e3bd01;
133 | }
134 |
135 | .t-ansi-94 {
136 | color: #1faffe;
137 | }
138 |
139 | .t-ansi-95 {
140 | color: #eb7dec;
141 | }
142 |
143 | .t-ansi-96 {
144 | color: #00e3e4;
145 | }
146 |
147 | .t-ansi-97 {
148 | color: #fdfdfe;
149 | }
150 |
151 | /* 后景色 */
152 | .t-ansi-40 {
153 | background-color: black;
154 | }
155 |
156 | .t-ansi-41 {
157 | background-color: #f10606;
158 | }
159 |
160 | .t-ansi-42 {
161 | background-color: #14cb14;
162 | }
163 |
164 | .t-ansi-43 {
165 | background-color: yellow;
166 | }
167 |
168 | .t-ansi-44 {
169 | background-color: #3993d4;
170 | }
171 |
172 | .t-ansi-45 {
173 | background-color: #bd12bd;
174 | }
175 |
176 | .t-ansi-46 {
177 | background-color: #0eb4b4;
178 | }
179 |
180 | .t-ansi-47 {
181 | background-color: white;
182 | }
183 |
184 | .t-ansi-100 {
185 | background-color: #585859;
186 | }
187 |
188 | .t-ansi-101 {
189 | background-color: #ef353a;
190 | }
191 |
192 | .t-ansi-102 {
193 | background-color: #4ec215;
194 | }
195 |
196 | .t-ansi-103 {
197 | background-color: #e3bd01;
198 | }
199 |
200 | .t-ansi-104 {
201 | background-color: #1faffe;
202 | }
203 |
204 | .t-ansi-105 {
205 | background-color: #eb7dec;
206 | }
207 |
208 | .t-ansi-106 {
209 | background-color: #00e3e4;
210 | }
211 |
212 | .t-ansi-107 {
213 | background-color: #fdfdfe;
214 | }
215 |
216 | .t-ansi-line {
217 | min-height: var(--t-font-height);
218 | }
219 |
220 | .t-ansi-char {
221 | min-width: 7px;
222 | min-height: var(--t-font-height);
223 | font-size: inherit;
224 | display: inline;
225 | height: 100%;
226 | vertical-align: top;
227 | font-weight: bold;
228 | word-break: break-all;
229 | white-space: pre-wrap;
230 | }
231 |
--------------------------------------------------------------------------------
/src/css/scrollbar.css:
--------------------------------------------------------------------------------
1 | .t-window::-webkit-scrollbar,
2 | .t-code::-webkit-scrollbar,
3 | .t-text-editor::-webkit-scrollbar,
4 | .t-vue-codemirror .vue-codemirror .CodeMirror .CodeMirror-hscrollbar::-webkit-scrollbar,
5 | .t-window pre::-webkit-scrollbar,
6 | .t-cmd-tips-items::-webkit-scrollbar,
7 | .t-cmd-help::-webkit-scrollbar
8 | {
9 | width: 8px;
10 | height: 8px;
11 | }
12 |
13 | .t-window::-webkit-scrollbar-button,
14 | .t-code::-webkit-scrollbar-button,
15 | .t-text-editor::-webkit-scrollbar-button,
16 | .t-vue-codemirror .vue-codemirror .CodeMirror .CodeMirror-hscrollbar::-webkit-scrollbar-button,
17 | .t-window pre::-webkit-scrollbar-button,
18 | .t-cmd-tips-items::-webkit-scrollbar-button,
19 | .t-cmd-help::-webkit-scrollbar-button
20 | {
21 | width: 0;
22 | height: 0;
23 | display: none;
24 | }
25 |
26 | .t-window::-webkit-scrollbar-thumb,
27 | .t-code::-webkit-scrollbar-thumb,
28 | .t-text-editor::-webkit-scrollbar-thumb,
29 | .t-vue-codemirror .vue-codemirror .CodeMirror .CodeMirror-hscrollbar::-webkit-scrollbar-thumb,
30 | .t-window pre::-webkit-scrollbar-thumb,
31 | .t-cmd-tips-items::-webkit-scrollbar-thumb,
32 | .t-cmd-help::-webkit-scrollbar-thumb
33 | {
34 | border-radius: 6px;
35 | border-style: dashed;
36 | border-color: transparent;
37 | border-width: 2px;
38 | background-color: rgba(157, 165, 183, 0.4);
39 | background-clip: padding-box;
40 | }
41 |
42 | .t-window::-webkit-scrollbar-thumb:hover,
43 | .t-code::-webkit-scrollbar-thumb:hover,
44 | .t-text-editor::-webkit-scrollbar-thumb:hover,
45 | .t-vue-codemirror .vue-codemirror .CodeMirror .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover,
46 | .t-window pre::-webkit-scrollbar-thumb:hover,
47 | .t-cmd-tips-items::-webkit-scrollbar-thumb:hover,
48 | .t-cmd-help::-webkit-scrollbar-thumb:hover
49 | {
50 | background: rgba(157, 165, 183, 0.7);
51 | cursor: pointer;
52 | }
53 |
54 | .t-window::-webkit-scrollbar-track,
55 | .t-code::-webkit-scrollbar-track,
56 | .t-text-editor::-webkit-scrollbar-track,
57 | .t-vue-codemirror .vue-codemirror .CodeMirror .CodeMirror-hscrollbar::-webkit-scrollbar-track,
58 | .t-window pre::-webkit-scrollbar-track,
59 | .t-cmd-tips-items::-webkit-scrollbar-track,
60 | .t-cmd-help::-webkit-scrollbar-track
61 | {
62 | border-radius: 6px;
63 | }
64 |
--------------------------------------------------------------------------------
/src/css/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /*限定字体高度*/
3 | --t-font-height: 19px;
4 | /*限定字体大小*/
5 | --t-font-size: 16px;
6 | --t-cmd-tips-border-radius: 5px;
7 | }
8 |
9 | .t-log-box, .t-cmd-line {
10 | margin-block-start: 0;
11 | margin-block-end: 0;
12 | margin-inline-start: 0;
13 | margin-inline-end: 0;
14 | }
15 |
16 | .t-log-box {
17 | display: block;
18 | position: relative;
19 | line-height: var(--t-font-height);
20 | }
21 |
22 | .t-container {
23 | position: relative;
24 | margin: 0;
25 | padding: 0;
26 | border-radius: 15px;
27 | background-color: var(--t-main-background-color);
28 | box-shadow: var(--t-window-box-shadow);
29 | -moz-box-shadow: var(--t-window-box-shadow);
30 | -webkit-box-shadow: var(--t-window-box-shadow);
31 | -o-box-shadow: var(--t-window-box-shadow);
32 | }
33 |
34 | .t-header-container {
35 | position: relative;
36 | z-index: 2;
37 | top: 0;
38 | right: 0;
39 | left: 0;
40 | }
41 |
42 | .t-header {
43 | text-align: center;
44 | padding: 2px;
45 | background-color: var(--t-header-background-color);
46 | }
47 |
48 | .t-header-title {
49 | font-size: calc(var(--t-font-size) + 2px);
50 | font-weight: bold;
51 | margin: 5px;
52 | height: var(--t-font-height);
53 | line-height: var(--t-font-height);
54 | letter-spacing: 1px;
55 | color: var(--t-header-font-color);
56 | display: inline-block;
57 | cursor: pointer;
58 | text-shadow: 0 0 20px #41454a;
59 | }
60 |
61 | .t-header ul.t-shell-dots {
62 | position: absolute;
63 | top: 5px;
64 | left: 8px;
65 | padding-left: 0;
66 | margin: 0;
67 | }
68 |
69 | .t-header ul.t-shell-dots li {
70 | display: inline-block;
71 | width: 16px;
72 | height: 16px;
73 | border-radius: 10px;
74 | margin-left: 6px;
75 | margin-top: 4px;
76 | line-height: 16px;
77 | cursor: pointer;
78 | }
79 |
80 | .shell-dot-item {
81 | position: relative;
82 | }
83 |
84 | .t-header ul .t-shell-dots-red {
85 | background-color: #f14444;
86 | }
87 |
88 | .t-header ul .t-shell-dots-yellow {
89 | background-color: #f7db60;
90 | }
91 |
92 | .t-header ul .t-shell-dots-green {
93 | background-color: #23bd65;
94 | }
95 |
96 | .t-shell-dot {
97 | opacity: 0;
98 | transition: opacity 0.2s ease;
99 | -moz-transition: opacity 0.2s ease;
100 | -ms-transition: opacity 0.2s ease;
101 | -webkit-transition: opacity 0.2s ease;
102 | -o-transition: opacity 0.2s ease;
103 | margin-bottom: 0;
104 | position: absolute;
105 | left: 50%;
106 | margin-left: -5px;
107 | top: 50%;
108 | margin-top: -5px;
109 | }
110 |
111 | .t-shell-dots:hover .t-shell-dot {
112 | opacity: 1;
113 | }
114 |
115 | .t-shell-pin-icon {
116 | filter: drop-shadow(5px 30px 5px rgba(26, 58, 70, 0.8));
117 | -ms-filter: drop-shadow(5px 30px 5px rgba(26, 58, 70, 0.8));
118 | -webkit-filter: drop-shadow(5px 30px 5px rgba(26, 58, 70, 0.8));
119 | }
120 |
121 | .t-window,
122 | .t-ask-input,
123 | .t-window p,
124 | .t-window div,
125 | .t-crude-font {
126 | font-size: var(--t-font-size);
127 | font-family: Monaco, "Lucida Console", monospace;
128 | }
129 |
130 | .t-window {
131 | position: absolute;
132 | top: 0;
133 | left: 0;
134 | right: 0;
135 | overflow: auto;
136 | z-index: 1;
137 | max-height: none;
138 | min-height: 140px;
139 | padding: 0 0 0 20px;
140 | font-weight: 400;
141 | cursor: text;
142 | background-color: var(--t-main-background-color);
143 | color: var(--t-main-font-color);
144 | }
145 |
146 | .t-window p {
147 | overflow-wrap: break-word;
148 | word-break: break-all;
149 | }
150 |
151 | .t-window p .cmd {
152 | line-height: 24px;
153 | }
154 |
155 | @keyframes cursor-flash {
156 | 0%, 100% {
157 | opacity: 0;
158 | }
159 | 50% {
160 | opacity: 1;
161 | }
162 | }
163 |
164 | .t-window .t-cursor {
165 | position: absolute;
166 | }
167 |
168 | .t-window .t-cursor-blink {
169 | animation: cursor-flash 1s infinite;
170 | -webkit-animation: cursor-flash 1s infinite;
171 | -o-animation: cursor-flash 1s infinite;
172 | -moz-animation: cursor-flash 1s infinite;
173 | }
174 |
175 | .t-window .t-cursor-block {
176 | background-color: var(--t-cursor-color);
177 | }
178 |
179 | .t-window .t-cursor-underline::before {
180 | display: block;
181 | position: absolute;
182 | background-color: var(--t-cursor-color);
183 | width: 100%;
184 | height: 3px;
185 | z-index: 100;
186 | bottom: 0;
187 | left: 0;
188 | content: " ";
189 | }
190 |
191 | .t-window .t-cursor-bar::before {
192 | display: block;
193 | position: absolute;
194 | background-color: var(--t-cursor-color);
195 | width: 2px;
196 | height: 100%;
197 | z-index: 100;
198 | top: 0;
199 | left: 0;
200 | content: " ";
201 | }
202 |
203 | .t-window .t-cursor-none {
204 | display: none;
205 | }
206 |
207 | .t-a {
208 | color: var(--t-link-color);
209 | }
210 |
211 | .t-a:hover {
212 | color: var(--t-link-hover-color);
213 | }
214 |
215 | .t-ask-input {
216 | border: none;
217 | max-width: 300px;
218 | background: none;
219 | outline: none;
220 | padding: 0;
221 | display: inline-block;
222 | color: var(--t-main-font-color);
223 | }
224 |
225 | .t-ask-input:focus, .t-ask-input:focus-visible {
226 | border: none;
227 | outline: none;
228 | }
229 |
230 | .t-cmd-input {
231 | position: relative;
232 | border: none;
233 | width: 1px;
234 | height: 0;
235 | opacity: 0;
236 | cursor: text;
237 | padding: 1px 2px;
238 | -webkit-writing-mode: horizontal-tb !important;
239 | text-rendering: auto;
240 | letter-spacing: normal;
241 | word-spacing: normal;
242 | text-transform: none;
243 | text-indent: 0;
244 | text-shadow: none;
245 | display: inline-block;
246 | text-align: start;
247 | appearance: textfield;
248 | -webkit-rtl-ordering: logical;
249 | border-image: initial;
250 | word-wrap: break-word;
251 | margin: 0;
252 | background-color: var(--t-main-background-color);
253 | }
254 |
255 | .t-content-normal .success,
256 | .t-content-normal .error,
257 | .t-content-normal .warning,
258 | .t-content-normal .info,
259 | .t-content-normal .system {
260 | padding: 0 3px;
261 | color: var(--t-tag-font-color);
262 | }
263 |
264 | .t-content-normal .success {
265 | background-color: #27ae60;
266 | }
267 |
268 | .t-content-normal .error {
269 | background-color: #c0392b;
270 | }
271 |
272 | .t-content-normal .warning {
273 | background-color: #f39c12;
274 | }
275 |
276 | .t-content-normal .info {
277 | background-color: #2980b9;
278 | }
279 |
280 | .t-content-normal .system {
281 | background-color: #8697a2;
282 | }
283 |
284 | .t-crude-font {
285 | font-weight: 600;
286 | }
287 |
288 | .t-flag {
289 | opacity: 0;
290 | }
291 |
292 | .t-last-line {
293 | font-size: 0;
294 | word-spacing: 0;
295 | letter-spacing: 0;
296 | position: relative;
297 | margin-bottom: 15px;
298 | line-height: var(--t-font-height);
299 | }
300 |
301 | /*手机*/
302 | @media screen and (max-width: 768px ) {
303 | .t-window {
304 | padding: 0 0 0 15px;
305 | }
306 | }
307 |
308 | /*平板*/
309 | @media screen and (max-width: 992px) and (min-width: 768.1px) {
310 |
311 | }
312 |
313 | .t-cmd-line {
314 | font-size: 0;
315 | line-height: var(--t-font-height);
316 | }
317 |
318 | .t-cmd-line-content {
319 | font-size: var(--t-font-size);
320 | word-break: break-all;
321 | white-space: break-spaces;
322 | }
323 |
324 | .t-cmd-key {
325 | font-weight: 700;
326 | color: var(--t-cmd-key-color);
327 | }
328 |
329 | .t-cmd-arg {
330 | color: var(--t-cmd-arg-color);
331 | }
332 |
333 | .t-cmd-splitter {
334 | color: var(--t-cmd-splitter-color);
335 | }
336 |
337 | .t-help-list {
338 | margin: 0;
339 | list-style: none;
340 | padding-left: 0;
341 | display: inline-grid;
342 | display: -moz-inline-grid;
343 | display: -ms-inline-grid;
344 | }
345 |
346 | .t-help-list li {
347 | margin: 3px 0;
348 | }
349 |
350 | .t-cmd-help {
351 | position: absolute;
352 | top: 15px;
353 | right: 12px;
354 | z-index: 99;
355 | max-width: 50%;
356 | padding: 5px;
357 | overflow: auto;
358 | max-height: calc(100% - 60px);
359 | background-color: var(--t-cmd-help-background-color);
360 | color: var(--t-main-font-color);
361 | box-shadow: var(--t-cmd-help-box-shadow);
362 | }
363 |
364 | .t-cmd-help code {
365 | font-size: var(--t-font-size);
366 | border: none;
367 | padding: 2px 5px 2px 5px;
368 | background-color: var(--t-cmd-help-code-background-color) !important;
369 | }
370 |
371 | .t-cmd-help-eg {
372 | float: left;
373 | width: 30px;
374 | display: flex;
375 | font-size: var(--t-font-size);
376 | line-height: var(--t-font-height);
377 | }
378 |
379 | .t-cmd-help-example {
380 | float: left;
381 | width: calc(100% - 30px);
382 | display: flex
383 | }
384 |
385 | .t-cmd-help-des {
386 | margin-bottom: 10px;
387 | }
388 |
389 | .t-cmd-help-des-item {
390 | font-size: var(--t-font-size);
391 | }
392 |
393 | .t-pre-numbering {
394 | margin-top: 0;
395 | position: absolute;
396 | top: 0;
397 | left: -30px;
398 | width: 30px;
399 | text-align: center;
400 | padding: 1em 0;
401 | }
402 |
403 | .t-pre-numbering li {
404 | list-style: none;
405 | font-size: 1em;
406 | }
407 |
408 | .t-window pre {
409 | position: relative;
410 | margin: 0;
411 | overflow: auto;
412 | }
413 |
414 | .t-example-ul {
415 | padding: 0 0 0 10px;
416 | margin: 0;
417 | list-style: none;
418 | }
419 |
420 | .t-table {
421 | max-width: 100%;
422 | overflow: auto;
423 | padding: 0;
424 | margin: 0;
425 | }
426 |
427 | .t-border-dashed {
428 | border-collapse: collapse;
429 | border: var(--t-table-border);
430 | }
431 |
432 | .t-table thead {
433 | font-weight: 600;
434 | }
435 |
436 | .t-table, .t-table tr, .t-table td, .t-table tbody, .t-table thead {
437 | margin: 0;
438 | padding: 15px;
439 | }
440 |
441 | .t-code-inline {
442 | color: var(--t-code-inline-font-color);
443 | font-weight: 600;
444 | }
445 |
446 | .t-code {
447 | position: relative;
448 | max-height: 500px;
449 | overflow: auto;
450 | }
451 |
452 | .t-vue-codemirror div,
453 | .t-vue-highlight div {
454 | font-size: var(--t-font-size);
455 | }
456 |
457 | .t-code .t-vue-codemirror .vue-codemirror .CodeMirror {
458 | height: unset;
459 | border: none;
460 | }
461 |
462 | .t-text-editor-container {
463 | position: absolute;
464 | left: 0;
465 | top: 0;
466 | width: 100%;
467 | height: 100%;
468 | z-index: 1;
469 | }
470 |
471 | .t-text-editor {
472 | width: calc(100% - 10px);
473 | height: calc(100% - 35px);
474 | overflow: auto;
475 | resize: none;
476 | margin: 0;
477 | padding: 0 5px;
478 | border: none;
479 | font-size: var(--t-font-size);
480 | color: var(--t-main-font-color);
481 | background-color: var(--t-main-background-color);
482 | }
483 |
484 | .t-text-editor:focus-visible, .t-text-editor:focus {
485 | outline: none;
486 | outline-offset: unset;
487 | }
488 |
489 | .t-text-editor-floor {
490 | position: absolute;
491 | height: 35px;
492 | width: 100%;
493 | bottom: 0;
494 | left: 0;
495 | background-color: var(--t-text-editor-floor-background-color);
496 | }
497 |
498 | .t-text-editor-floor-btn {
499 | border: none;
500 | outline: none;
501 | margin-top: 10px;
502 | cursor: pointer;
503 | background-color: rgba(0, 0, 0, 0);
504 | }
505 |
506 | .t-close-btn {
507 | color: var(--t-text-editor-floor-close-btn-color);
508 | }
509 |
510 | .t-save-btn {
511 | color: var(--t-text-editor-floor-save-btn-color);
512 | }
513 |
514 | .t-text-editor-floor-btn:hover {
515 | color: var(--t-text-editor-floor-btn-hover-color);
516 | }
517 |
518 | .t-disable-select {
519 | user-select: none;
520 | -moz-user-select: none;
521 | -webkit-user-select: none;
522 | -ms-user-select: none;
523 | -khtml-user-selece: none;
524 | }
525 |
526 | .t-point {
527 | width: var(--t-font-height);
528 | height: var(--t-font-height);
529 | background-color: rgba(0, 0, 0, 0);
530 | position: absolute;
531 | z-index: 100;
532 | }
533 |
534 | .t-point-lt {
535 | left: calc(0px - var(--t-font-height) / 2);
536 | top: calc(0px - var(--t-font-height) / 2);
537 | cursor: nwse-resize;
538 | }
539 |
540 | .t-point-rt {
541 | left: calc(100% - var(--t-font-height) / 2);
542 | top: calc(0px - var(--t-font-height) / 2);
543 | cursor: nesw-resize;
544 | }
545 |
546 | .t-point-lb {
547 | left: calc(0px - var(--t-font-height) / 2);
548 | top: calc(100% - var(--t-font-height) / 2);
549 | cursor: nesw-resize;
550 | }
551 |
552 | .t-point-rb {
553 | left: calc(100% - var(--t-font-height) / 2);
554 | top: calc(100% - var(--t-font-height) / 2);
555 | cursor: nwse-resize;
556 | }
557 |
558 | .t-code-default {
559 | background-color: var(--t-code-default-background-color);
560 | }
561 |
562 | .t-log-box-hover-script:hover {
563 | background-color: var(--t-log-box-hover-script-background-color);
564 | }
565 |
566 | .t-log-box-folded:hover {
567 | background-color:var(--t-log-box-folded-hover-background-color);
568 | }
569 |
570 | .t-log-box-folded {
571 | height: var(--t-font-height);
572 | overflow-y: clip;
573 | background-color: var(--t-log-box-folded-background-color);
574 | cursor: pointer;
575 | }
576 |
577 | .t-log-fold-icon {
578 | position: absolute;
579 | width: 10px;
580 | height: 10px;
581 | left: -17px;
582 | top: 4px;
583 | border: 1px solid var(--t-log-fold-icon-border-color);
584 | text-align: center;
585 | line-height: 9px;
586 | background-color: var(--t-log-fold-icon-background-color);
587 | color: var(--t-log-fold-icon-color);
588 | cursor: pointer;
589 | user-select: none;
590 | z-index: 100;
591 | }
592 |
593 | .t-log-fold-icon-active {
594 | background-color: var(--t-log-fold-icon-active-background-color);
595 | color: var(--t-log-fold-icon-active-color);
596 | }
597 |
598 | .t-log-fold-line {
599 | position: absolute;
600 | height: calc(100% - 10px);
601 | width: 1px;
602 | background-color: var(--t-log-fold-line-color);
603 | left: -12px;
604 | top: 10px;
605 | }
606 |
607 | .t-cmd-tips {
608 | --t-cmd-tips-box-shadow: 5px 5px 15px 0 rgba(0, 0, 0, 0.3);
609 |
610 | position: absolute;
611 | display: block;
612 | z-index: 100;
613 | background-color: var(--t-cmd-tips-background-color);
614 | border-radius: var(--t-cmd-tips-border-radius);
615 | color: var(--t-cmd-tips-font-color);
616 | -ms-overflow-y: auto;
617 | cursor: context-menu;
618 | font-weight: normal;
619 | box-shadow: var(--t-cmd-tips-box-shadow);
620 | -moz-box-shadow: var(--t-cmd-tips-box-shadow);
621 | -webkit-box-shadow: var(--t-cmd-tips-box-shadow);
622 | -o-box-shadow: var(--t-cmd-tips-box-shadow);
623 | font-family: system-ui;
624 | }
625 |
626 | .t-cmd-tips-items {
627 | display: block;
628 | min-width: 280px;
629 | max-width: 500px;
630 | max-height: 200px;
631 | overflow-y: auto;
632 | padding: 5px;
633 | }
634 |
635 | .t-cmd-tips-footer {
636 | display: block;
637 | width: 100%;
638 | padding: 5px 0;
639 | text-indent: 6px;
640 | line-height: var(--t-font-height);
641 | background-color: var(--t-cmd-tips-footer-background-color);
642 | color: var(--t-cmd-tips-footer-font-color);
643 | font-size: 12px;
644 | border-bottom-left-radius: var(--t-cmd-tips-border-radius);
645 | -webkit-border-bottom-left-radius: var(--t-cmd-tips-border-radius);
646 | border-bottom-right-radius: var(--t-cmd-tips-border-radius);
647 | -webkit-border-bottom-right-radius: var(--t-cmd-tips-border-radius);
648 | }
649 |
650 | .t-cmd-tips-item {
651 | display: block;
652 | padding: 5px 8px;
653 | text-overflow:ellipsis;
654 | overflow:hidden;
655 | text-wrap: nowrap;
656 | color: var(--t-cmd-tips-des-font-color);
657 | border-radius: 5px;
658 | }
659 |
660 | .t-cmd-tips-item-first {
661 | border-top-left-radius: var(--t-cmd-tips-border-radius);
662 | -webkit-border-top-left-radius: var(--t-cmd-tips-border-radius);
663 | border-top-right-radius: var(--t-cmd-tips-border-radius);
664 | -webkit-border-top-right-radius: var(--t-cmd-tips-border-radius);
665 | }
666 |
667 | .t-cmd-tips-item-active {
668 | background-color: var(--t-cmd-tips-active-background-color);
669 | }
670 |
671 | .t-cmd-tips-content {
672 | font-weight: bold;
673 | color: var(--t-cmd-tips-content-font-color);
674 | cursor: context-menu;
675 | user-select: none;
676 | }
677 |
678 | .t-cmd-tips-des {
679 | color: var(--t-cmd-tips-des-font-color);
680 | margin-left: 8px;
681 | cursor: context-menu;
682 | user-select: none;
683 | }
684 |
685 | /*--------------------------json viewer style------------------------*/
686 | .t-json-container .jv-container.jv-light {
687 | border: none;
688 | background-color: var(--t-json-background-color);
689 | color: var(--t-main-font-color);
690 | }
691 |
692 | .t-json-container .jv-container .jv-code,
693 | .t-json-container .jv-container .jv-code.open {
694 | padding-bottom: 0;
695 | overflow: hidden;
696 | }
697 |
698 | .t-json-container .jv-container {
699 | display: inline-block;
700 | min-width: 300px;
701 | }
702 |
703 | .t-json-container .jv-container.jv-light .jv-item.jv-array,
704 | .t-json-container .jv-container.jv-light .jv-item.jv-object {
705 | color: var(--t-json-value-obj-color);
706 | }
707 |
708 | .t-json-container .jv-container.jv-light .jv-key {
709 | color: var(--t-main-font-color);
710 | }
711 |
712 | .t-json-container .jv-container.jv-light .jv-item.jv-boolean {
713 | color: var(--t-json-value-bool-color);
714 | }
715 |
716 | .t-json-container .jv-container.jv-light .jv-item.jv-number {
717 | color: var(--t-json-value-number-color);
718 | }
719 |
720 | .t-json-container .jv-container.jv-light .jv-ellipsis {
721 | color: var(--t-main-font-color);
722 | background-color: var(--t-json-ellipsis-background-color);
723 | }
724 |
725 | .t-json-container .jv-container .jv-more:after {
726 | background: var(--t-json-more-background-webkit);
727 | background: var(--t-json-more-background);
728 | }
729 |
730 | .t-json-deep-selector {
731 | margin-top: 8px;
732 | width: 75px;
733 | position: absolute;
734 | margin-left: -150px;
735 | font-size: var(--t-font-size);
736 | border-radius: 2px;
737 | cursor: pointer;
738 | border: 1px solid var(--t-json-deep-selector-border-color);
739 | }
740 |
741 | .t-json-deep-selector:focus,
742 | .t-json-deep-selector:focus-visible {
743 | outline: none;
744 | }
745 |
746 | /*--------------------------selection style------------------------*/
747 | .t-window div::selection,
748 | .t-window a::selection,
749 | .t-window span::selection,
750 | .t-window li::selection,
751 | .t-window p::selection,
752 | .t-window code::selection,
753 | .t-window td::selection,
754 | .t-window th::selection,
755 | .t-window br::selection {
756 | color: var(--t-selection-font-color);
757 | background-color: var(--t-selection-background-color);
758 | }
759 |
--------------------------------------------------------------------------------
/src/css/theme/dark.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --t-main-background-color: #191b24;
3 | --t-main-font-color: #fff;
4 | --t-window-box-shadow: 0 0 40px 1px rgb(0 0 0 / 20%);
5 | --t-header-background-color: #959598;
6 | --t-header-font-color: white;
7 | --t-tag-font-color: #fff;
8 | --t-cursor-color: #fff;
9 | --t-cmd-key-color: yellow;
10 | --t-cmd-arg-color: #c0c0ff;
11 | --t-cmd-splitter-color: #808085;
12 | --t-link-color: antiquewhite;
13 | --t-link-hover-color: white;
14 | --t-table-border: 1px dashed #fff;
15 | --t-selection-font-color: black;
16 | --t-selection-background-color: white;
17 | --t-code-inline-font-color: #00b10e;
18 | --t-cmd-help-background-color: black;
19 | --t-cmd-help-code-background-color: rgba(0, 0, 0, 0);
20 | --t-cmd-help-box-shadow: 0px 0px 0px 4px rgb(255 255 255 / 20%);
21 | --t-text-editor-floor-background-color: rgb(72 69 69);
22 | --t-text-editor-floor-close-btn-color: #bba9a9;
23 | --t-text-editor-floor-save-btn-color: #00b10e;
24 | --t-text-editor-floor-btn-hover-color: #befcff;
25 | --t-json-background-color: rgba(0, 0, 0, 0);
26 | --t-json-value-obj-color: #bdadad;
27 | --t-json-value-bool-color: #cdc83c;
28 | --t-json-value-number-color: #f3c7fb;
29 | --t-json-ellipsis-background-color: #674848;
30 | --t-json-more-background-webkit: -webkit-linear-gradient(top, rgba(0, 0, 0, 0) 20%, rgb(255 255 255 / 10%) 100%);
31 | --t-json-more-background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 20%, rgb(255 255 255 / 10%) 100%);
32 | --t-json-deep-selector-border-color: rgb(249 249 249 / 52%);
33 | --t-code-default-background-color: rgb(39 50 58);
34 | --t-log-box-hover-script-background-color: #2a2c34;
35 | --t-log-box-folded-background-color: #042f36;
36 | --t-log-box-folded-hover-background-color: #515157;
37 | --t-log-fold-icon-color: #4ca5c1;
38 | --t-log-fold-icon-background-color: #191b24;
39 | --t-log-fold-icon-border-color: #4ca5c1;
40 | --t-log-fold-icon-active-color: #191b24;
41 | --t-log-fold-icon-active-background-color: #4ca5c1;
42 | --t-log-fold-line-color: #4ca5c1;
43 | --t-cmd-tips-background-color: #544a4a;
44 | --t-cmd-tips-font-color: #fff;
45 | --t-cmd-tips-active-background-color: #5c6ec9;
46 | --t-cmd-tips-content-font-color: #fff;
47 | --t-cmd-tips-des-font-color: #cbb0b0;
48 | --t-cmd-tips-footer-font-color: #e3c2c2;
49 | --t-cmd-tips-footer-background-color: #546456;
50 | }
--------------------------------------------------------------------------------
/src/css/theme/light.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --t-main-background-color: #fff;
3 | --t-main-font-color: #000;
4 | --t-window-box-shadow: 0 0 40px 1px rgb(0 0 0 / 20%);
5 | --t-header-background-color: #4b474c;
6 | --t-header-font-color: white;
7 | --t-tag-font-color: #fff;
8 | --t-cursor-color: #000;
9 | --t-cmd-key-color: #834dff;
10 | --t-cmd-arg-color: #c0c0ff;
11 | --t-cmd-splitter-color: #808085;
12 | --t-link-color: #02505e;
13 | --t-link-hover-color: #17b2d2;
14 | --t-table-border: 1px dashed #565151;
15 | --t-selection-font-color: white;
16 | --t-selection-background-color: #2a2626;
17 | --t-code-inline-font-color: #00b10e;
18 | --t-cmd-help-background-color: white;
19 | --t-cmd-help-code-background-color: #f7f7f9;
20 | --t-cmd-help-box-shadow: 0px 0px 0px 4px rgb(0 0 0 / 20%);
21 | --t-text-editor-floor-background-color: white;
22 | --t-text-editor-floor-close-btn-color: #9a7070;
23 | --t-text-editor-floor-save-btn-color: #00b10e;
24 | --t-text-editor-floor-btn-hover-color: #652222;
25 | --t-json-background-color: rgba(0, 0, 0, 0);
26 | --t-json-value-obj-color: #bdadad;
27 | --t-json-value-bool-color: #cdc83c;
28 | --t-json-value-number-color: #a625be;
29 | --t-json-ellipsis-background-color: #f5f5f5;
30 | --t-json-more-background-webkit: -webkit-linear-gradient(top, rgba(0, 0, 0, 0) 20%, rgb(255 255 255 / 10%) 100%);
31 | --t-json-more-background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 20%, rgb(255 255 255 / 10%) 100%);
32 | --t-json-deep-selector-border-color: rgb(249 249 249 / 52%);
33 | --t-code-default-background-color: rgb(227 239 248);
34 | --t-log-box-hover-script-background-color: #f5f6f7;
35 | --t-log-box-folded-background-color: #e2eaeb;
36 | --t-log-box-folded-hover-background-color: #d3d6d7;
37 | --t-log-fold-icon-color: #4ca5c1;
38 | --t-log-fold-icon-background-color: white;
39 | --t-log-fold-icon-border-color: #4ca5c1;
40 | --t-log-fold-icon-active-color: white;
41 | --t-log-fold-icon-active-background-color: #4ca5c1;
42 | --t-log-fold-line-color: #4ca5c1;
43 | --t-cmd-tips-background-color: #ffffff;
44 | --t-cmd-tips-font-color: #000;
45 | --t-cmd-tips-active-background-color: #cbd3fd;
46 | --t-cmd-tips-content-font-color: #644c4c;
47 | --t-cmd-tips-des-font-color: #939393;
48 | --t-cmd-tips-footer-font-color: #6e5f5f;
49 | --t-cmd-tips-footer-background-color: #efefef;
50 | }
51 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import '@vue/runtime-core'
4 |
5 | export {}
6 |
7 | declare module '@vue/runtime-core' {
8 | export interface GlobalComponents {
9 | THeader: typeof import("~/components/THeader.vue")['default']
10 | TEditor: typeof import("~/components/TEditor.vue")['default']
11 | THelpBox: typeof import("~/components/THelpBox.vue")['default']
12 | TViewerCode: typeof import("~/components/TViewerCode.vue")['default']
13 | TViewerJson: typeof import("~/components/TViewerJson.vue")['default']
14 | TViewerNormal: typeof import("~/components/TViewerNormal.vue")['default']
15 | TViewerTable: typeof import("~/components/TViewerTable.vue")['default']
16 | JsonViewer: typeof import("vue-json-viewer")['default']
17 | Terminal: typeof import("~/Terminal.vue")['default']
18 | }
19 | }
20 |
21 | declare module "*.vue" {
22 | import { DefineComponent } from "vue";
23 | const component: DefineComponent<{}, {}, any>;
24 | export default component;
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import './css/scrollbar.css'
2 | import './css/ansi.css'
3 | import './css/style.css'
4 | import 'vue-json-viewer/style.css'
5 | import type {App} from 'vue'
6 | import TerminalApi, {configMaxStoredCommandCountPerInstance, configStoreName, configTheme} from "./common/api"
7 | import Terminal from "./Terminal.vue"
8 | import {TerminalAsk, TerminalFlash, VueWebTerminal} from "./types"
9 | import {getStore, initStore} from "~/common/store";
10 |
11 | const install = (app: App): void => {
12 | initStore()
13 | app.component(Terminal.__name as string, Terminal)
14 | }
15 |
16 | const createTerminal = (): VueWebTerminal => {
17 | return {
18 | install,
19 | configTheme,
20 | configStoreName,
21 | configMaxStoredCommandCountPerInstance
22 | }
23 | }
24 |
25 | export * from './types'
26 |
27 | export {
28 | Terminal,
29 | TerminalApi,
30 | TerminalAsk,
31 | TerminalFlash,
32 | createTerminal,
33 | getStore
34 | }
35 |
36 | export default Terminal
37 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import {App} from "vue";
2 |
3 | export type TerminalMessageClass = 'success' | 'error' | 'info' | 'warning' | 'system'
4 |
5 | export type TerminalMessageType = 'cmdLine' | 'normal' | 'json' | 'code' | 'table' | 'html' | 'ansi'
6 |
7 | export type TerminalCursorStyle = 'block' | 'underline' | 'bar' | 'none'
8 |
9 | export interface VueWebTerminal {
10 | install: (app: App) => void;
11 | /**
12 | * 自定义主题设置,也可以覆盖默认的 dark 和 light 主题
13 | * @param theme 主题名
14 | * @param css 主题css内容
15 | */
16 | configTheme: (theme: string, css: string) => void;
17 | /**
18 | * 配置 local storage 存储名
19 | * @param name 存储名
20 | */
21 | configStoreName: (name: string) => void;
22 | /**
23 | * 配置每个Terminal实力存储的指令记录数量
24 | * @param count
25 | */
26 | configMaxStoredCommandCountPerInstance: (count: number) => void;
27 | }
28 |
29 | export interface EditorConfig {
30 | open: boolean
31 | focus: boolean
32 | value: string
33 | onClose: null | Function
34 | onFocus?: Function
35 | onBlur?: Function
36 | }
37 |
38 | export type Position = {
39 | x: number
40 | y: number
41 | }
42 |
43 | export type DragConfig = {
44 | width: number | string
45 | height: number | string
46 | zIndex?: number
47 | init?: Position
48 | pinned?: boolean
49 | }
50 |
51 | export type ScreenType = {
52 | xs?: boolean
53 | sm?: boolean
54 | md?: boolean
55 | lg?: boolean
56 | xl?: boolean
57 | }
58 |
59 | export type Command = {
60 | key: string
61 | title?: string
62 | group?: string
63 | usage?: string
64 | description?: string
65 | example?: Array
66 | }
67 |
68 | export type CommandExample = {
69 | cmd: string
70 | des?: string
71 | }
72 |
73 | export type CmdHistory = {
74 | cmdLog: string[],
75 | cmdIdx: number
76 | }
77 |
78 | export type TerminalConfiguration = {
79 | storeName: string,
80 | maxStoredCommandCountPerInstance: number,
81 | themes: Record,
82 | }
83 |
84 | export type MessageContentTable = {
85 | head: string[],
86 | rows: string[][]
87 | }
88 |
89 | export type MessageGroup = {
90 | fold: boolean,
91 | logs: Message[],
92 | tag?: string
93 | }
94 |
95 | export type Message = {
96 | type?: TerminalMessageType
97 | content: string | number | object | MessageContentTable | Array
98 | class?: TerminalMessageClass
99 | tag?: string,
100 | depth?: number
101 | }
102 |
103 | export type AskConfig = {
104 | isPassword: boolean
105 | question: string,
106 | autoReview: boolean
107 | callback?: (value: string) => void
108 | }
109 |
110 | export type InputTipItem = {
111 | content: string,
112 | description?: string,
113 | command?: Command
114 | }
115 |
116 | export type CharWidth = {
117 | en?: number,
118 | cn?: number
119 | }
120 |
121 | export type TerminalElementInfo = {
122 | pos: Position,
123 | screenWidth: number,
124 | screenHeight: number,
125 | clientWidth: number,
126 | clientHeight: number,
127 | charWidth: CharWidth
128 | }
129 |
130 | export type CommandSortHandlerFunc = (a: any, b: any) => number
131 |
132 | export type InputFilterFunc = (str1: string, str2: string, event: InputEvent | CompositionEvent) => string | null
133 |
134 | export type CommandFormatterFunc = (cmd: string) => string
135 |
136 | export type TerminalApiListenerFunc = (type: string, options?: any) => any | void
137 |
138 | export type SuccessFunc = (message?: Message | Array | string | TerminalFlash | TerminalAsk) => void
139 |
140 | export type FailedFunc = (message: string) => void
141 |
142 | export type PushMessageBeforeFunc = (message: Message, name: String) => void
143 |
144 | /**
145 | * 提示选择处理函数
146 | *
147 | * @param command 当前用户输入的完整命令行
148 | * @param cursorIndex 当前光标所处位置
149 | * @param item 用户选择提示项
150 | * @param callback 填充结束后需调用此函数返回新的命令行
151 | */
152 | export type InputTipsSelectHandlerFunc = (command: string, cursorIndex: number, item: InputTipItem, callback: (cmd: string) => void) => void
153 |
154 | /**
155 | * 用户自定义命令搜索提示实现
156 | *
157 | * @param command 当前用户输入的完整命令行
158 | * @param cursorIndex 当前光标所处位置
159 | * @param commandStore 命令集合
160 | * @param callback 搜索结束回调,回调格式为一个数组
161 | */
162 | export type InputTipsSearchHandlerFunc = (command: string, cursorIndex: number, commandStore: Command[], callback: (tips: InputTipItem[], openTips?: boolean) => void) => void
163 |
164 | class TerminalCallback {
165 |
166 | onFinishListener: Function
167 |
168 | finish() {
169 | if (this.onFinishListener != null) {
170 | this.onFinishListener()
171 | }
172 | }
173 |
174 | onFinish(callback: Function) {
175 | this.onFinishListener = callback
176 | }
177 | }
178 |
179 | export class TerminalAsk extends TerminalCallback {
180 | handler: Function
181 |
182 | ask(options: AskConfig) {
183 | if (this.handler != null) {
184 | this.handler(options)
185 | }
186 | }
187 |
188 | onAsk(callback: (config: AskConfig) => void) {
189 | this.handler = callback
190 | }
191 | }
192 |
193 | export class TerminalFlash extends TerminalCallback {
194 | handler: Function
195 |
196 | flush(msg: string) {
197 | if (this.handler != null) {
198 | this.handler(msg)
199 | }
200 | }
201 |
202 | onFlush(callback: (msg: string) => void) {
203 | this.handler = callback
204 | }
205 | }
206 |
207 | export interface TerminalApiData {
208 | pool: {
209 | [key: string]: TerminalApiListenerFunc
210 | },
211 | configuration: TerminalConfiguration
212 | }
213 |
214 | export class TerminalApi {
215 |
216 | data: TerminalApiData
217 |
218 | constructor(data: TerminalApiData) {
219 | this.data = data
220 | }
221 |
222 | post(name: string = 'terminal', event: string, options?: any) {
223 | let listener: TerminalApiListenerFunc = this.data.pool[name]
224 | if (listener != null) {
225 | return listener(event, options)
226 | }
227 | }
228 |
229 | pushMessage(name: string, message: Message | Array | string): void {
230 | this.post(name, 'pushMessage', message)
231 | }
232 |
233 | appendMessage(name: string, message: string): void {
234 | this.post(name, 'appendMessage', message)
235 | }
236 |
237 | fullscreen(name: string): void {
238 | this.post(name, "fullscreen")
239 | }
240 |
241 | isFullscreen(name: string): boolean {
242 | return this.post(name, 'isFullscreen')
243 | }
244 |
245 | dragging(name: string, position: Position): void {
246 | this.post(name, 'dragging', position)
247 | }
248 |
249 | /**
250 | * Simulate trigger execution instructions
251 | *
252 | * @param name name of terminal
253 | * @param command content of the command
254 | * @return { boolean } Trigger success
255 | */
256 | execute(name: string, command: string): boolean {
257 | return this.post(name, 'execute', command)
258 | }
259 |
260 | focus(name: string, enforce?: boolean): void {
261 | this.post(name, 'focus', enforce)
262 | }
263 |
264 | elementInfo(name: string): TerminalElementInfo {
265 | return this.post(name, 'elementInfo')
266 | }
267 |
268 | textEditorOpen(name: string, setting?: EditorSetting): void {
269 | this.post(name, 'textEditorOpen', setting)
270 | }
271 |
272 | textEditorClose(name: string, closeCallbackParams?: any): string | undefined {
273 | return this.post(name, 'textEditorClose', closeCallbackParams)
274 | }
275 |
276 | clearLog(name: string, clearHistory?: boolean): void {
277 | this.post(name, 'clearLog', clearHistory)
278 | }
279 |
280 | getCommand(name: string): string {
281 | return this.post(name, 'getCommand')
282 | }
283 |
284 | setCommand(name: string, newCommand: string): void {
285 | this.post(name, 'setCommand', newCommand)
286 | }
287 |
288 | switchAllFoldState(name: string, foldStat: boolean): number {
289 | return this.post(name, 'switchAllFoldState', foldStat)
290 | }
291 |
292 | jumpToBottom(name: string, enforce: boolean): void {
293 | this.post(name, 'jumpToBottom', enforce)
294 | }
295 |
296 | getOutputs(name: string): MessageGroup[] {
297 | return this.post(name, 'getOutputs')
298 | }
299 | }
300 |
301 | export interface EditorSetting {
302 | content: string,
303 | onClose: Function
304 | }
305 |
--------------------------------------------------------------------------------
/test/App.vue:
--------------------------------------------------------------------------------
1 |
355 |
356 |
357 |
358 |
get command
359 |
set command
360 |
jump to bottom
361 |
364 |
focus
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
428 |
--------------------------------------------------------------------------------
/test/main.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import App from './App.vue'
3 | import {createApp} from "vue";
4 | import { createTerminal } from '../src'
5 |
6 | const app = createApp(App)
7 |
8 | const terminal = createTerminal()
9 | terminal.configStoreName("test-terminal")
10 | terminal.configMaxStoredCommandCountPerInstance(2)
11 |
12 | // 这行代码是一个错误示例
13 | // terminal.configTheme("custom", "s")
14 |
15 | app.use(terminal)
16 |
17 | app.mount('#app')
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "skipLibCheck": true,
9 | "paths": {
10 | "~/*": ["src/*"]
11 | },
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "noEmit": false,
19 | "emitDeclarationOnly": true,
20 | "declaration": true,
21 | "declarationDir": "lib"
22 | },
23 | "include": [
24 | "src/**/*.ts",
25 | "src/**/*.d.ts",
26 | "src/**/*.tsx",
27 | "src/**/*.vue"
28 | ],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {resolve} from 'path'
2 | import {defineConfig} from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import dts from 'vite-plugin-dts'
5 |
6 | import {visualizer} from 'rollup-plugin-visualizer'
7 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
8 |
9 | const pathSrc = resolve(__dirname, 'src')
10 |
11 | // https://vitejs.dev/config/
12 | export default defineConfig({
13 | server: {},
14 | plugins: [
15 | vue(),
16 | // 包体分析
17 | visualizer({
18 | filename: 'stats.html',
19 | open: false
20 | }),
21 | cssInjectedByJsPlugin(),
22 | dts()
23 | ],
24 | resolve: {
25 | alias: {
26 | '~/': `${pathSrc}/`,
27 | }
28 | },
29 | css: {
30 | preprocessorOptions: {
31 | scss: {},
32 | }
33 | },
34 | build: {
35 | lib: {
36 | entry: resolve(__dirname, 'src/index.ts'),
37 | // 暴露的全局变量
38 | name: 'Terminal',
39 | fileName: 'vue-web-terminal'
40 | },
41 | outDir: 'lib',
42 | rollupOptions: {
43 | // 不打包的依赖
44 | // https://rollupjs.org/configuration-options/
45 | external: ['vue'],
46 | output: {
47 | // https://rollupjs.org/configuration-options/#output-format
48 | format: 'es',
49 | // https://rollupjs.org/configuration-options/#output-exports
50 | exports: 'named',
51 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
52 | // https://rollupjs.org/configuration-options/#output-globals
53 | globals: {
54 | vue: 'Vue'
55 | },
56 | // manualChunks: undefined,
57 | }
58 | },
59 | minify: 'terser',
60 | terserOptions: {
61 | compress: {
62 | drop_console: true,
63 | drop_debugger: true
64 | },
65 | format: {
66 | // 删除注释
67 | comments: true
68 | }
69 | },
70 | commonjsOptions: {
71 | esmExternals: true
72 | }
73 | }
74 | })
75 |
--------------------------------------------------------------------------------
/vue-web-terminal.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------