niuma-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,697 @@
1
+ <script setup lang="ts">
2
+ import { FitAddon } from '@xterm/addon-fit'
3
+ import type { ITheme } from 'xterm'
4
+ import { Terminal } from 'xterm'
5
+ import 'xterm/css/xterm.css'
6
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
7
+ import { useRsI18n } from '../composables/useRsI18n'
8
+ import {
9
+ beginClipboardPrefetch,
10
+ copyTextToClipboard,
11
+ readClipboardText,
12
+ } from '../utils/rs-clipboard'
13
+ import RsContextMenu from './RsContextMenu.vue'
14
+ import type { RsContextMenuItem } from './context-menu-utils'
15
+ import RsLoading from './RsLoading.vue'
16
+ import {
17
+ containsTuiRefreshSequence,
18
+ mergeTerminalTheme,
19
+ prepareTerminalForPtyWrite,
20
+ resolveTerminalTheme,
21
+ terminalShortcutLabel,
22
+ type RsTerminalAction,
23
+ type RsTerminalThemeMode,
24
+ } from './terminal-utils'
25
+ import {
26
+ attachWheelScrollGuard,
27
+ type RsTerminalWheelScrollModifier,
28
+ } from './terminal-wheel'
29
+
30
+ const TERMINAL_LINE_HEIGHT = 1.2
31
+
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ loading?: boolean
35
+ overlay?: string
36
+ inputEnabled?: boolean
37
+ cursorBlink?: boolean
38
+ fontFamily?: string
39
+ fontSize?: number
40
+ fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
41
+ fontWeightBold?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
42
+ allowTransparency?: boolean
43
+ /** auto 跟随 data-rs-theme;也可强制 light / dark */
44
+ themeMode?: RsTerminalThemeMode
45
+ /** 覆盖内置调色板中的部分 token */
46
+ theme?: Partial<ITheme>
47
+ contextMenu?: boolean
48
+ /** 右键菜单是否展示「询问 AI」(仅 emit,业务侧自行处理) */
49
+ showAskAi?: boolean
50
+ /**
51
+ * 右键是否自动选中光标下单词。
52
+ * SSH/vim/less 等 TUI 场景建议 false,避免冲掉用户已拖选的大段文本。
53
+ */
54
+ rightClickSelectsWord?: boolean
55
+ shortcuts?: boolean
56
+ scrollback?: number
57
+ /** 将裸 \\n 当作换行;PTY/SSH 建议 false,避免破坏 ncurses(top/vim) */
58
+ convertEol?: boolean
59
+ /** 奇数行斑马纹底色,提升长日志可读性 */
60
+ zebraStripes?: boolean
61
+ /** none:滚轮直接滚 scrollback;shift:仅 Shift+滚轮滚历史,普通滚轮发方向键(SSH/TUI 推荐) */
62
+ wheelScrollModifier?: RsTerminalWheelScrollModifier
63
+ /** TUI 全屏刷新时若视口不在底部,自动滚回底部(修复 top 表头丢失) */
64
+ snapViewportOnTuiWrite?: boolean
65
+ }>(),
66
+ {
67
+ loading: false,
68
+ overlay: '',
69
+ inputEnabled: true,
70
+ cursorBlink: true,
71
+ fontFamily:
72
+ '"SF Mono", "Cascadia Code", "Cascadia Mono", Consolas, "Liberation Mono", Menlo, monospace',
73
+ fontSize: 13,
74
+ fontWeight: '300',
75
+ fontWeightBold: '400',
76
+ allowTransparency: false,
77
+ themeMode: 'auto',
78
+ theme: () => ({}),
79
+ contextMenu: true,
80
+ showAskAi: false,
81
+ rightClickSelectsWord: true,
82
+ shortcuts: true,
83
+ scrollback: 5000,
84
+ convertEol: false,
85
+ zebraStripes: true,
86
+ wheelScrollModifier: 'none',
87
+ snapViewportOnTuiWrite: true,
88
+ },
89
+ )
90
+
91
+ const emit = defineEmits<{
92
+ ready: []
93
+ data: [data: string]
94
+ resize: [payload: { cols: number; rows: number }]
95
+ action: [action: RsTerminalAction]
96
+ /** 询问 AI:携带右键菜单打开时快照的选区(避免菜单点击后选区被清空) */
97
+ askAi: [text: string]
98
+ }>()
99
+
100
+ const { t } = useRsI18n()
101
+ const hostEl = ref<HTMLElement | null>(null)
102
+ const terminalReady = ref(false)
103
+ const hasSelection = ref(false)
104
+ /** 右键菜单打开瞬间的选区快照(capture 阶段,早于 xterm word-select) */
105
+ const menuSelectionSnapshot = ref('')
106
+ const resolvedThemeMode = ref(resolveTerminalTheme(props.themeMode))
107
+ /** fit 后量一次真实行高,避免亚像素漂移;非每帧更新 */
108
+ const zebraRowStepPx = ref<number | null>(null)
109
+
110
+ const showLoading = computed(() => !terminalReady.value || props.loading)
111
+
112
+ const zebraStyle = computed((): Record<string, string> | undefined => {
113
+ if (!props.zebraStripes) {
114
+ return undefined
115
+ }
116
+ const step = zebraRowStepPx.value ?? props.fontSize * TERMINAL_LINE_HEIGHT
117
+ return {
118
+ '--rs-terminal-font-size': `${props.fontSize}px`,
119
+ '--rs-terminal-line-height': String(TERMINAL_LINE_HEIGHT),
120
+ '--rs-terminal-zebra-step': `${step}px`,
121
+ }
122
+ })
123
+
124
+ const contextMenuItems = computed<RsContextMenuItem[]>(() => {
125
+ const items: RsContextMenuItem[] = [
126
+ {
127
+ key: 'copy',
128
+ label: t('terminal.copy', 'Copy'),
129
+ icon: 'copy',
130
+ shortcut: terminalShortcutLabel('C'),
131
+ disabled: false,
132
+ },
133
+ {
134
+ key: 'paste',
135
+ label: t('terminal.paste', 'Paste'),
136
+ icon: 'clipboard-paste',
137
+ shortcut: terminalShortcutLabel('V'),
138
+ },
139
+ {
140
+ key: 'selectAll',
141
+ label: t('terminal.selectAll', 'Select All'),
142
+ icon: 'square-mouse-pointer',
143
+ shortcut: terminalShortcutLabel('A'),
144
+ },
145
+ ]
146
+ if (props.showAskAi) {
147
+ items.push(
148
+ { key: 'sep-ai', label: '', separator: true },
149
+ {
150
+ key: 'askAi',
151
+ label: t('terminal.askAi', 'Ask AI'),
152
+ icon: 'bot',
153
+ disabled: !hasSelection.value && !menuSelectionSnapshot.value,
154
+ },
155
+ )
156
+ }
157
+ items.push(
158
+ { key: 'sep-1', label: '', separator: true },
159
+ {
160
+ key: 'clear',
161
+ label: t('terminal.clear', 'Clear Terminal'),
162
+ icon: 'eraser',
163
+ shortcut: terminalShortcutLabel('K'),
164
+ danger: true,
165
+ },
166
+ )
167
+ return items
168
+ })
169
+
170
+ let terminal: Terminal | null = null
171
+ let fitAddon: FitAddon | null = null
172
+ let resizeObserver: ResizeObserver | null = null
173
+ let themeObserver: MutationObserver | null = null
174
+ let detachWheelGuard: (() => void) | null = null
175
+ let lastGeometry = { cols: 0, rows: 0 }
176
+
177
+ function resolveAllowTransparency(): boolean {
178
+ return props.allowTransparency || props.zebraStripes
179
+ }
180
+
181
+ function buildXtermTheme(): ITheme {
182
+ // 主题背景保持不透明:反色行 (xterm-fg/bg-257) 依赖 opaque(background);
183
+ // 斑马纹通过 allowTransparency 让默认单元格透出底层 CSS 渐变。
184
+ return mergeTerminalTheme(resolvedThemeMode.value, props.theme)
185
+ }
186
+
187
+ function applyThemeToTerminal(): void {
188
+ if (!terminal) {
189
+ return
190
+ }
191
+ terminal.options.theme = buildXtermTheme()
192
+ }
193
+
194
+ function syncZebraRowStepFromDom(): void {
195
+ zebraRowStepPx.value = null
196
+ if (!props.zebraStripes || !hostEl.value) {
197
+ return
198
+ }
199
+ const rowEl = hostEl.value.querySelector<HTMLElement>('.xterm-rows > div, .xterm-row')
200
+ const height = rowEl?.offsetHeight ?? 0
201
+ if (height > 0) {
202
+ zebraRowStepPx.value = height
203
+ }
204
+ }
205
+
206
+ function attachWheelGuard(): void {
207
+ detachWheelGuard?.()
208
+ detachWheelGuard = null
209
+ if (!hostEl.value || props.wheelScrollModifier === 'none') {
210
+ return
211
+ }
212
+ detachWheelGuard = attachWheelScrollGuard(hostEl.value, {
213
+ modifier: () => props.wheelScrollModifier,
214
+ inputEnabled: () => props.inputEnabled,
215
+ onArrowKeys: (data) => {
216
+ if (props.inputEnabled) {
217
+ emit('data', data)
218
+ }
219
+ },
220
+ })
221
+ }
222
+
223
+ function refreshResolvedTheme(): void {
224
+ resolvedThemeMode.value = resolveTerminalTheme(props.themeMode)
225
+ applyThemeToTerminal()
226
+ }
227
+
228
+ function currentGeometry(): { cols: number; rows: number } | null {
229
+ if (!terminal) {
230
+ return null
231
+ }
232
+ return {
233
+ cols: terminal.cols || 80,
234
+ rows: terminal.rows || 24,
235
+ }
236
+ }
237
+
238
+ async function rafTwice(): Promise<void> {
239
+ await new Promise<void>((resolve) => {
240
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
241
+ })
242
+ }
243
+
244
+ function emitResizeIfChanged(): void {
245
+ const geometry = currentGeometry()
246
+ if (!geometry) {
247
+ return
248
+ }
249
+ if (geometry.cols === lastGeometry.cols && geometry.rows === lastGeometry.rows) {
250
+ return
251
+ }
252
+ lastGeometry = geometry
253
+ emit('resize', geometry)
254
+ }
255
+
256
+ async function fit(): Promise<void> {
257
+ if (!fitAddon || !terminal || !hostEl.value) {
258
+ return
259
+ }
260
+ const { clientWidth, clientHeight } = hostEl.value
261
+ if (clientWidth <= 0 || clientHeight <= 0) {
262
+ return
263
+ }
264
+ if (typeof document !== 'undefined' && document.fonts?.ready) {
265
+ await document.fonts.ready
266
+ }
267
+ if (!fitAddon || !terminal || !hostEl.value) {
268
+ return
269
+ }
270
+ await nextTick()
271
+ if (!fitAddon || !terminal) {
272
+ return
273
+ }
274
+ await rafTwice()
275
+ for (let attempt = 0; attempt < 4; attempt += 1) {
276
+ if (!fitAddon || !terminal) {
277
+ return
278
+ }
279
+ try {
280
+ fitAddon.fit()
281
+ } catch {
282
+ return
283
+ }
284
+ const dims = fitAddon.proposeDimensions()
285
+ if (dims && dims.cols > 0 && dims.rows > 0) {
286
+ break
287
+ }
288
+ await rafTwice()
289
+ }
290
+ if (!fitAddon || !terminal) {
291
+ return
292
+ }
293
+ emitResizeIfChanged()
294
+ syncZebraRowStepFromDom()
295
+ }
296
+
297
+ function write(data: string): void {
298
+ const term = terminal
299
+ if (!term) {
300
+ return
301
+ }
302
+ if (props.snapViewportOnTuiWrite && containsTuiRefreshSequence(data)) {
303
+ prepareTerminalForPtyWrite(term, data)
304
+ }
305
+ term.write(data)
306
+ }
307
+
308
+ function clear(): void {
309
+ terminal?.clear()
310
+ terminal?.clearSelection()
311
+ hasSelection.value = false
312
+ }
313
+
314
+ function focus(): void {
315
+ terminal?.focus()
316
+ }
317
+
318
+ function syncSelectionState(): void {
319
+ hasSelection.value = Boolean(terminal?.hasSelection())
320
+ }
321
+
322
+ /** 优先活选区;若被右键 word-select / 失焦冲掉,则回退菜单快照。 */
323
+ function resolveMenuSelectionText(): string {
324
+ const live = String(terminal?.getSelection?.() ?? '').trim()
325
+ const snap = menuSelectionSnapshot.value.trim()
326
+ if (live && (!snap || live.length >= snap.length)) {
327
+ return live
328
+ }
329
+ return snap
330
+ }
331
+
332
+ async function copySelection(): Promise<void> {
333
+ const text = resolveMenuSelectionText()
334
+ if (!text) {
335
+ return
336
+ }
337
+ if (await copyTextToClipboard(text)) {
338
+ emit('action', 'copy')
339
+ }
340
+ }
341
+
342
+ async function pasteFromClipboard(): Promise<void> {
343
+ if (!terminal || !props.inputEnabled) {
344
+ return
345
+ }
346
+ terminal.focus()
347
+ const textarea = terminal.element?.querySelector('textarea')
348
+ if (textarea instanceof HTMLTextAreaElement) {
349
+ textarea.focus()
350
+ }
351
+ const text = await readClipboardText()
352
+ if (!text) {
353
+ return
354
+ }
355
+ terminal.paste(text)
356
+ emit('action', 'paste')
357
+ }
358
+
359
+ function onTerminalContextMenu(): void {
360
+ void beginClipboardPrefetch()
361
+ // capture 早于 xterm 的 rightClickSelectsWord,先保住用户拖选(vim/less 大段文本)
362
+ const existing = String(terminal?.getSelection?.() ?? '').trim()
363
+ if (existing) {
364
+ menuSelectionSnapshot.value = existing
365
+ hasSelection.value = true
366
+ } else {
367
+ menuSelectionSnapshot.value = ''
368
+ }
369
+ // xterm 在 target 阶段才 rightClickSelect,延后同步;无先验选区时采用 word-select 结果
370
+ void nextTick(() => {
371
+ syncSelectionState()
372
+ const after = String(terminal?.getSelection?.() ?? '').trim()
373
+ // 仅在无快照,或 word-select 反而选出更长文本时更新;避免冲掉用户拖选
374
+ if (after && (!menuSelectionSnapshot.value || after.length > menuSelectionSnapshot.value.length)) {
375
+ menuSelectionSnapshot.value = after
376
+ }
377
+ if (menuSelectionSnapshot.value) {
378
+ hasSelection.value = true
379
+ }
380
+ })
381
+ }
382
+
383
+ function selectAll(): void {
384
+ terminal?.selectAll()
385
+ syncSelectionState()
386
+ emit('action', 'selectAll')
387
+ }
388
+
389
+ function clearTerminal(): void {
390
+ clear()
391
+ emit('action', 'clear')
392
+ }
393
+
394
+ async function runTerminalAction(action: RsTerminalAction): Promise<void> {
395
+ if (action === 'copy') {
396
+ await copySelection()
397
+ return
398
+ }
399
+ if (action === 'paste') {
400
+ await pasteFromClipboard()
401
+ return
402
+ }
403
+ if (action === 'selectAll') {
404
+ selectAll()
405
+ return
406
+ }
407
+ if (action === 'clear') {
408
+ clearTerminal()
409
+ return
410
+ }
411
+ if (action === 'askAi') {
412
+ const text = resolveMenuSelectionText()
413
+ emit('askAi', text)
414
+ emit('action', 'askAi')
415
+ }
416
+ }
417
+
418
+ function onContextMenuSelect(key: string): void {
419
+ void runTerminalAction(key as RsTerminalAction)
420
+ }
421
+
422
+ function attachShortcuts(): void {
423
+ if (!terminal || !props.shortcuts) {
424
+ return
425
+ }
426
+ terminal.attachCustomKeyEventHandler((event) => {
427
+ if (!props.shortcuts || event.type !== 'keydown') {
428
+ return true
429
+ }
430
+ const mod = event.metaKey || event.ctrlKey
431
+ if (!mod) {
432
+ return true
433
+ }
434
+ const key = event.key.toLowerCase()
435
+ if (key === 'c' && terminal?.hasSelection()) {
436
+ event.preventDefault()
437
+ event.stopPropagation()
438
+ void copySelection()
439
+ return false
440
+ }
441
+ if (key === 'v') {
442
+ event.preventDefault()
443
+ event.stopPropagation()
444
+ void pasteFromClipboard()
445
+ return false
446
+ }
447
+ if (key === 'a') {
448
+ event.preventDefault()
449
+ event.stopPropagation()
450
+ selectAll()
451
+ return false
452
+ }
453
+ if (key === 'k') {
454
+ event.preventDefault()
455
+ event.stopPropagation()
456
+ clearTerminal()
457
+ return false
458
+ }
459
+ return true
460
+ })
461
+ }
462
+
463
+ watch(
464
+ () => [props.cursorBlink, props.fontFamily, props.fontSize, props.fontWeight, props.fontWeightBold, props.allowTransparency, props.convertEol, props.themeMode, props.theme, props.zebraStripes, props.wheelScrollModifier] as const,
465
+ () => {
466
+ if (!terminal) {
467
+ return
468
+ }
469
+ refreshResolvedTheme()
470
+ terminal.options.cursorBlink = props.cursorBlink
471
+ terminal.options.fontFamily = props.fontFamily
472
+ terminal.options.fontSize = props.fontSize
473
+ terminal.options.fontWeight = props.fontWeight
474
+ terminal.options.fontWeightBold = props.fontWeightBold
475
+ terminal.options.allowTransparency = resolveAllowTransparency()
476
+ terminal.options.convertEol = props.convertEol
477
+ attachWheelGuard()
478
+ void fit()
479
+ },
480
+ { deep: true },
481
+ )
482
+
483
+ onMounted(async () => {
484
+ if (!hostEl.value) {
485
+ return
486
+ }
487
+ refreshResolvedTheme()
488
+ terminal = new Terminal({
489
+ cursorBlink: props.cursorBlink,
490
+ fontFamily: props.fontFamily,
491
+ fontSize: props.fontSize,
492
+ fontWeight: props.fontWeight,
493
+ fontWeightBold: props.fontWeightBold,
494
+ lineHeight: TERMINAL_LINE_HEIGHT,
495
+ allowTransparency: resolveAllowTransparency(),
496
+ drawBoldTextInBrightColors: true,
497
+ scrollback: props.scrollback,
498
+ convertEol: props.convertEol,
499
+ rightClickSelectsWord: props.rightClickSelectsWord,
500
+ theme: buildXtermTheme(),
501
+ })
502
+ fitAddon = new FitAddon()
503
+ terminal.loadAddon(fitAddon)
504
+ terminal.open(hostEl.value)
505
+ terminal.onData((data: string) => {
506
+ if (props.inputEnabled) {
507
+ emit('data', data)
508
+ }
509
+ })
510
+ terminal.onSelectionChange(() => {
511
+ syncSelectionState()
512
+ })
513
+ attachShortcuts()
514
+ await nextTick()
515
+ attachWheelGuard()
516
+ terminalReady.value = true
517
+ await fit()
518
+ emit('ready')
519
+
520
+ resizeObserver = new ResizeObserver(() => {
521
+ void fit()
522
+ })
523
+ resizeObserver.observe(hostEl.value)
524
+
525
+ themeObserver = new MutationObserver(() => {
526
+ if (props.themeMode === 'auto') {
527
+ refreshResolvedTheme()
528
+ }
529
+ })
530
+ themeObserver.observe(document.documentElement, {
531
+ attributes: true,
532
+ attributeFilter: ['data-rs-theme'],
533
+ })
534
+ })
535
+
536
+ onBeforeUnmount(() => {
537
+ themeObserver?.disconnect()
538
+ themeObserver = null
539
+ resizeObserver?.disconnect()
540
+ resizeObserver = null
541
+ detachWheelGuard?.()
542
+ detachWheelGuard = null
543
+ terminal?.dispose()
544
+ terminal = null
545
+ fitAddon = null
546
+ })
547
+
548
+ defineExpose({
549
+ write,
550
+ clear,
551
+ focus,
552
+ fit,
553
+ copySelection,
554
+ pasteFromClipboard,
555
+ selectAll,
556
+ getTerminal: () => terminal,
557
+ })
558
+ </script>
559
+
560
+ <template>
561
+ <RsContextMenu
562
+ :disabled="!contextMenu"
563
+ :items="contextMenuItems"
564
+ @select="onContextMenuSelect"
565
+ >
566
+ <div class="rs-terminal-shell">
567
+ <section
568
+ class="rs-terminal"
569
+ :class="{ 'rs-terminal--zebra': zebraStripes }"
570
+ :style="zebraStyle"
571
+ @click="focus"
572
+ @contextmenu.capture="onTerminalContextMenu"
573
+ >
574
+ <div ref="hostEl" class="rs-terminal__host" />
575
+ <RsLoading v-if="showLoading" class="rs-terminal__loading" />
576
+ <output v-if="overlay" class="rs-terminal__overlay">
577
+ {{ overlay }}
578
+ </output>
579
+ </section>
580
+ </div>
581
+ </RsContextMenu>
582
+ </template>
583
+
584
+ <style scoped>
585
+ .rs-terminal-shell {
586
+ display: block;
587
+ height: 100%;
588
+ min-height: 0;
589
+ }
590
+
591
+ .rs-terminal {
592
+ position: relative;
593
+ height: 100%;
594
+ min-height: 0;
595
+ border: 1px solid var(--rs-terminal-border);
596
+ border-radius: var(--rs-radius-md);
597
+ background: var(--rs-terminal-shell-bg, var(--rs-terminal-bg));
598
+ color: var(--rs-terminal-fg);
599
+ box-shadow:
600
+ inset 0 1px 0 color-mix(in srgb, var(--rs-terminal-fg) 5%, transparent),
601
+ inset 0 0 0 1px color-mix(in srgb, var(--rs-terminal-fg) 4%, transparent),
602
+ 0 1px 3px color-mix(in srgb, #000 32%, transparent);
603
+ overflow: hidden;
604
+ }
605
+
606
+ [data-rs-theme='light'] .rs-terminal {
607
+ box-shadow:
608
+ inset 0 1px 0 color-mix(in srgb, #fff 80%, transparent),
609
+ inset 0 0 0 1px color-mix(in srgb, #000 4%, transparent),
610
+ 0 1px 4px color-mix(in srgb, #000 8%, transparent);
611
+ }
612
+
613
+ .rs-terminal__host {
614
+ position: relative;
615
+ width: 100%;
616
+ height: 100%;
617
+ min-height: 0;
618
+ /* FitAddon 取整余量露底时,与单元格同色(勿用 shell 灰底) */
619
+ background: var(--rs-terminal-bg);
620
+ }
621
+
622
+ .rs-terminal__loading,
623
+ .rs-terminal__overlay {
624
+ position: absolute;
625
+ inset: 0;
626
+ display: flex;
627
+ align-items: center;
628
+ justify-content: center;
629
+ }
630
+
631
+ .rs-terminal__overlay {
632
+ padding: var(--rs-space-lg);
633
+ color: color-mix(in srgb, var(--rs-terminal-fg) 92%, #fff 8%);
634
+ background: color-mix(in srgb, var(--rs-terminal-bg) 84%, transparent);
635
+ text-align: center;
636
+ pointer-events: none;
637
+ font-family: var(--rs-font-mono);
638
+ font-size: var(--rs-font-size-xs);
639
+ font-weight: 400;
640
+ line-height: var(--rs-line-height-normal);
641
+ }
642
+
643
+ .rs-terminal :deep(.xterm) {
644
+ width: 100%;
645
+ height: 100%;
646
+ }
647
+
648
+ /*
649
+ * 滚动:保持 xterm 默认 overflow-y:scroll(预留槽位,Fit 更稳),
650
+ * 仅隐藏横向条,避免右下角出现「一点」水平滚动条。
651
+ * 不要用 width/height:!important 去撑 .xterm-screen——会和字符格几何打架。
652
+ */
653
+ .rs-terminal :deep(.xterm-viewport) {
654
+ overflow-x: hidden;
655
+ overflow-y: scroll;
656
+ scrollbar-width: thin;
657
+ scrollbar-color: color-mix(in srgb, var(--rs-terminal-fg) 28%, transparent) transparent;
658
+ }
659
+
660
+ .rs-terminal--zebra :deep(.xterm) {
661
+ font-size: var(--rs-terminal-font-size, 13px);
662
+ line-height: var(--rs-terminal-line-height, 1.2);
663
+ }
664
+
665
+ .rs-terminal--zebra :deep(.xterm-rows),
666
+ .rs-terminal--zebra :deep(.xterm-rows > div) {
667
+ background-color: transparent !important;
668
+ }
669
+
670
+ /*
671
+ * 斑马纹画在 viewport + background-attachment:local:
672
+ * 覆盖字符格取整后的右/下余量,并随 scrollback 一起滚。
673
+ */
674
+ .rs-terminal--zebra :deep(.xterm-viewport) {
675
+ background-color: transparent !important;
676
+ background-image: repeating-linear-gradient(
677
+ to bottom,
678
+ var(--rs-terminal-bg) 0,
679
+ var(--rs-terminal-bg) var(--rs-terminal-zebra-step, 1lh),
680
+ var(--rs-terminal-row-stripe) var(--rs-terminal-zebra-step, 1lh),
681
+ var(--rs-terminal-row-stripe) calc(var(--rs-terminal-zebra-step, 1lh) * 2)
682
+ );
683
+ background-attachment: local;
684
+ }
685
+
686
+ .rs-terminal--zebra :deep(.xterm-screen) {
687
+ background-color: transparent !important;
688
+ }
689
+
690
+ .rs-terminal :deep(.xterm-fg-257) {
691
+ color: var(--rs-terminal-bg) !important;
692
+ }
693
+
694
+ .rs-terminal :deep(.xterm-bg-257) {
695
+ background-color: var(--rs-terminal-fg) !important;
696
+ }
697
+ </style>