@electerm/electerm-react 5.3.26 → 5.5.9

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 (146) hide show
  1. package/README.md +246 -12
  2. package/client/common/choose-save-folder.js +21 -2
  3. package/client/common/constants.js +20 -6
  4. package/client/common/create-title.jsx +6 -0
  5. package/client/common/default-setting.js +15 -0
  6. package/client/common/init-setting-item.js +5 -0
  7. package/client/common/platform.js +24 -0
  8. package/client/components/ai/agent-tool-call-card.jsx +10 -2
  9. package/client/components/ai/agent.js +4 -1
  10. package/client/components/ai/ai-attachments.js +87 -0
  11. package/client/components/ai/ai-chat-empty.jsx +43 -0
  12. package/client/components/ai/ai-chat-entry.jsx +13 -4
  13. package/client/components/ai/ai-chat-history-item.jsx +28 -4
  14. package/client/components/ai/ai-chat-history.jsx +2 -1
  15. package/client/components/ai/ai-chat.jsx +104 -13
  16. package/client/components/ai/ai-config-props.js +4 -130
  17. package/client/components/ai/ai-config.jsx +135 -15
  18. package/client/components/ai/ai-presets.js +173 -0
  19. package/client/components/ai/ai.styl +83 -0
  20. package/client/components/bg/css-overwrite.jsx +11 -0
  21. package/client/components/bookmark-form/bookmark-schema.js +4 -0
  22. package/client/components/bookmark-form/common/fields.jsx +3 -0
  23. package/client/components/bookmark-form/config/common-fields.js +12 -0
  24. package/client/components/bookmark-form/config/local.js +3 -2
  25. package/client/components/bookmark-form/config/serial.js +3 -2
  26. package/client/components/bookmark-form/config/ssh.js +2 -1
  27. package/client/components/bookmark-form/config/telnet.js +3 -2
  28. package/client/components/common/item-filter.jsx +76 -0
  29. package/client/components/common/item-filter.styl +54 -0
  30. package/client/components/common/lazy-boundary.jsx +34 -0
  31. package/client/components/common/opacity.jsx +3 -1
  32. package/client/components/footer/cmd-history.jsx +132 -45
  33. package/client/components/footer/cmd-history.styl +42 -4
  34. package/client/components/footer/footer-entry.jsx +35 -5
  35. package/client/components/footer/footer.styl +8 -1
  36. package/client/components/footer/multi-tab-run-modal.jsx +83 -0
  37. package/client/components/footer/tab-select.jsx +34 -17
  38. package/client/components/layout/layout.jsx +23 -31
  39. package/client/components/layout/pixed.js +3 -0
  40. package/client/components/main/main.jsx +55 -28
  41. package/client/components/main/term-fullscreen.styl +9 -3
  42. package/client/components/quick-commands/qm.styl +3 -2
  43. package/client/components/quick-commands/quick-command-create-modal.jsx +60 -0
  44. package/client/components/quick-commands/quick-commands-form-elem.jsx +10 -4
  45. package/client/components/rdp/file-transfer.js +4 -4
  46. package/client/components/remote-monitor/monitor-details.jsx +458 -0
  47. package/client/components/remote-monitor/monitor-details.styl +87 -0
  48. package/client/components/remote-monitor/monitor-model.js +764 -0
  49. package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
  50. package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
  51. package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
  52. package/client/components/remote-monitor/session-monitor.js +361 -0
  53. package/client/components/remote-monitor/use-monitor-details.js +10 -0
  54. package/client/components/remote-monitor/visibility.js +18 -0
  55. package/client/components/session/session-control.jsx +29 -2
  56. package/client/components/session/session.jsx +40 -18
  57. package/client/components/session/sessions.jsx +5 -2
  58. package/client/components/setting-panel/setting-common.jsx +25 -56
  59. package/client/components/setting-panel/setting-modal.jsx +57 -42
  60. package/client/components/setting-panel/setting-terminal.jsx +8 -2
  61. package/client/components/setting-panel/setting.styl +1 -1
  62. package/client/components/setting-panel/tab-triggers.jsx +71 -0
  63. package/client/components/sftp/file-compare-modal.jsx +6 -3
  64. package/client/components/sftp/file-item.jsx +5 -3
  65. package/client/components/sftp/owner-list.js +11 -2
  66. package/client/components/sftp/sftp-entry.jsx +4 -0
  67. package/client/components/side-panel-r/side-panel-r.jsx +2 -2
  68. package/client/components/sidebar/side-panel.jsx +1 -1
  69. package/client/components/sys-menu/menu-btn.jsx +30 -1
  70. package/client/components/tabs/index.jsx +2 -2
  71. package/client/components/tabs/tab-title.jsx +3 -2
  72. package/client/components/tabs/tab.jsx +5 -1
  73. package/client/components/terminal/attach-addon-custom.js +295 -49
  74. package/client/components/terminal/automation/index.js +64 -0
  75. package/client/components/terminal/automation/keys.js +49 -0
  76. package/client/components/terminal/automation/stream-matcher.js +82 -0
  77. package/client/components/terminal/automation/strip-ansi.js +19 -0
  78. package/client/components/terminal/automation/trigger-engine.js +233 -0
  79. package/client/components/terminal/automation/trigger-presets.js +110 -0
  80. package/client/components/terminal/command-tracker-addon.js +31 -1
  81. package/client/components/terminal/mixins/index.js +27 -0
  82. package/client/components/terminal/mixins/term-attach.js +123 -0
  83. package/client/components/terminal/mixins/term-config.jsx +84 -0
  84. package/client/components/terminal/mixins/term-content.js +126 -0
  85. package/client/components/terminal/mixins/term-context-menu.jsx +323 -0
  86. package/client/components/terminal/mixins/term-file-drop.js +149 -0
  87. package/client/components/terminal/mixins/term-init.js +122 -0
  88. package/client/components/terminal/mixins/term-log.jsx +96 -0
  89. package/client/components/terminal/mixins/term-resize.js +87 -0
  90. package/client/components/terminal/mixins/term-socket.js +433 -0
  91. package/client/components/terminal/mixins/term-suggestions.js +224 -0
  92. package/client/components/terminal/mixins/term-theme.js +190 -0
  93. package/client/components/terminal/mixins/term-touch.js +523 -0
  94. package/client/components/terminal/shell-detect.js +37 -0
  95. package/client/components/terminal/shell.js +0 -28
  96. package/client/components/terminal/shortcut-bar-defs.js +30 -4
  97. package/client/components/terminal/shortcut-bar-edit.jsx +40 -9
  98. package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
  99. package/client/components/terminal/shortcut-bar.jsx +17 -10
  100. package/client/components/terminal/shortcut-bar.styl +4 -0
  101. package/client/components/terminal/ssh-reload-state.js +96 -0
  102. package/client/components/terminal/startup-queue.js +433 -0
  103. package/client/components/terminal/terminal-interactive.jsx +32 -0
  104. package/client/components/terminal/terminal-select-text.jsx +69 -0
  105. package/client/components/terminal/terminal.jsx +227 -2064
  106. package/client/components/terminal/terminal.styl +61 -0
  107. package/client/components/terminal/transfer-client-base.js +5 -29
  108. package/client/components/terminal/trzsz-client.js +1 -3
  109. package/client/components/terminal/xmodem-client.js +1 -3
  110. package/client/components/terminal/xterm-loader.js +7 -0
  111. package/client/components/terminal/zmodem-client.js +1 -3
  112. package/client/components/terminal-info/base.jsx +18 -70
  113. package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
  114. package/client/components/terminal-info/terminal-info.jsx +50 -59
  115. package/client/components/terminal-info/terminal-info.styl +6 -49
  116. package/client/components/text-editor/open-text-editor.js +30 -0
  117. package/client/components/text-editor/text-editor-entry.jsx +17 -6
  118. package/client/components/text-editor/text-editor.jsx +14 -0
  119. package/client/components/triggers/trigger-editor.jsx +246 -0
  120. package/client/components/triggers/trigger-form.jsx +200 -0
  121. package/client/components/triggers/trigger-session-modal.jsx +199 -0
  122. package/client/components/triggers/trigger-setting-list.jsx +89 -0
  123. package/client/components/triggers/trigger-transport.jsx +60 -0
  124. package/client/components/triggers/triggers-field.jsx +19 -0
  125. package/client/components/widgets/widgets-list.jsx +11 -1
  126. package/client/css/mobile.styl +29 -8
  127. package/client/store/common.js +12 -1
  128. package/client/store/init-state.js +7 -0
  129. package/client/store/mcp-handler.js +158 -30
  130. package/client/store/setting.js +9 -0
  131. package/client/store/store.js +2 -0
  132. package/client/store/sync.js +2 -0
  133. package/client/store/tab.js +42 -3
  134. package/client/store/trigger.js +118 -0
  135. package/client/store/watch.js +13 -5
  136. package/client/views/index.pug +3 -0
  137. package/package.json +1 -1
  138. package/client/components/terminal-info/activity.jsx +0 -57
  139. package/client/components/terminal-info/data-cols-parser.jsx +0 -54
  140. package/client/components/terminal-info/disk.jsx +0 -45
  141. package/client/components/terminal-info/format-disks.js +0 -45
  142. package/client/components/terminal-info/network.jsx +0 -121
  143. package/client/components/terminal-info/resource.jsx +0 -99
  144. package/client/components/terminal-info/run-cmd.jsx +0 -263
  145. package/client/components/terminal-info/sys-info.jsx +0 -56
  146. package/client/components/terminal-info/up.jsx +0 -17
@@ -1,5 +1,5 @@
1
1
  import { auto } from 'manate/react'
2
- import { useEffect } from 'react'
2
+ import { lazy, Suspense, useEffect } from 'react'
3
3
  import Layout from '../layout/layout'
4
4
  import FileInfoModal from '../sftp/file-info-modal'
5
5
  import FileCompareModal from '../sftp/file-compare-modal'
@@ -10,7 +10,6 @@ import Sidebar from '../sidebar'
10
10
  import CssOverwrite from '../bg/css-overwrite'
11
11
  import UiTheme from './ui-theme'
12
12
  import CustomCss from '../bg/custom-css.jsx'
13
- import Resolutions from '../rdp/resolution-edit'
14
13
  import TerminalInteractive from '../terminal/terminal-interactive'
15
14
  import ConfirmModalStore from '../file-transfer/conflict-resolve.jsx'
16
15
  import TransferQueue from '../file-transfer/transfer-queue'
@@ -19,18 +18,21 @@ import TerminalCmdSuggestions from '../terminal/terminal-command-dropdown'
19
18
  import TransportsActionStore from '../file-transfer/transports-action-store.jsx'
20
19
  import classnames from 'classnames'
21
20
  import ShortcutControl from '../shortcuts/shortcut-control.jsx'
22
- import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
21
+ import {
22
+ footerHeight,
23
+ isMac,
24
+ isWin,
25
+ remoteMonitorBarHeight,
26
+ textTerminalBgValue
27
+ } from '../../common/constants'
23
28
  import { isAIDisabled } from '../../common/ai-feature'
24
29
  import { ConfigProvider } from 'antd'
25
30
  import { NotificationContainer } from '../common/notification'
26
- import InfoModal from '../sidebar/info-modal.jsx'
27
31
  import RightSidePanel from '../side-panel-r/side-panel-r'
28
32
  import ConnectionHoppingWarning from './connection-hopping-warnning'
29
33
  import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
30
34
  import LoadSshConfigs from '../ssh-config/load-ssh-configs'
31
35
  import AIChat from '../ai/ai-chat-entry'
32
- import AIConfigModal from '../ai/ai-config-modal'
33
- import Opacity from '../common/opacity'
34
36
  import MoveItemModal from '../tree-list/move-item-modal'
35
37
  import InputContextMenu from '../common/input-context-menu'
36
38
  import WorkspaceSaveModal from '../tabs/workspace-save-modal'
@@ -44,9 +46,17 @@ import deepCopy from 'json-deep-copy'
44
46
  import './wrapper.styl'
45
47
  import TerminalInfo from '../terminal-info/terminal-info-entry'
46
48
  import ShortcutBarEntry from '../terminal/shortcut-bar-entry'
49
+ import LazyBoundary from '../common/lazy-boundary'
50
+ import { isRemoteMonitorBarVisible } from '../remote-monitor/visibility'
47
51
  import '../../common/fs.js'
48
52
  import './term-fullscreen.styl'
49
53
 
54
+ const Resolutions = lazy(() => import('../rdp/resolution-edit'))
55
+ const InfoModal = lazy(() => import('../sidebar/info-modal.jsx'))
56
+ const AIConfigModal = lazy(() => import('../ai/ai-config-modal'))
57
+ // window opacity is electron-only: lazy so the web app never loads the chunk
58
+ const Opacity = lazy(() => import('../common/opacity'))
59
+
50
60
  export default auto(function Index (props) {
51
61
  useEffect(() => {
52
62
  const { store } = props
@@ -129,6 +139,7 @@ export default auto(function Index (props) {
129
139
  widgetInstances
130
140
  } = store
131
141
  const upgradeInfo = deepCopy(store.upgradeInfo)
142
+ const remoteMonitorBarVisible = isRemoteMonitorBarVisible(store)
132
143
  const cls = classnames({
133
144
  loaded: configLoaded,
134
145
  'not-webapp': !window.et.isWebApp,
@@ -147,12 +158,14 @@ export default auto(function Index (props) {
147
158
  'is-main': !isSecondInstance,
148
159
  'is-mobile': store.isMobile,
149
160
  'is-desktop': !store.isMobile,
150
- 'is-touch-device': store.isTouchDevice
161
+ 'is-touch-device': store.isTouchDevice,
162
+ 'remote-monitor-bar-on': remoteMonitorBarVisible
151
163
  })
152
164
  const ext1 = {
153
165
  className: cls,
154
166
  style: {
155
- '--left-side-bar-width': store.leftSideBarWidth + 'px'
167
+ '--left-side-bar-width': store.leftSideBarWidth + 'px',
168
+ '--footer-stack-height': `${footerHeight + (remoteMonitorBarVisible ? remoteMonitorBarHeight : 0)}px`
156
169
  }
157
170
  }
158
171
  // Get active tab IDs
@@ -242,20 +255,6 @@ export default auto(function Index (props) {
242
255
  title: rightPanelTitle,
243
256
  rightPanelTab
244
257
  }
245
- const terminalInfoProps = {
246
- rightPanelTab,
247
- ...deepCopy(store.terminalInfoProps),
248
- ...pick(
249
- config,
250
- [
251
- 'host',
252
- 'port',
253
- 'saveTerminalLogToFile',
254
- 'terminalInfos',
255
- 'sessionLogPath'
256
- ]
257
- )
258
- }
259
258
  const sshConfigProps = {
260
259
  ...pick(store, [
261
260
  'settingTab',
@@ -293,13 +292,23 @@ export default auto(function Index (props) {
293
292
  {...confsCss}
294
293
  configLoaded={configLoaded}
295
294
  />
296
- <Opacity opacity={config.opacity} />
295
+ {window.et.isWebApp
296
+ ? null
297
+ : (
298
+ <LazyBoundary>
299
+ <Suspense fallback={null}>
300
+ <Opacity opacity={config.opacity} />
301
+ </Suspense>
302
+ </LazyBoundary>
303
+ )}
297
304
  <TerminalInteractive />
298
305
  <UiTheme
299
306
  {...themeProps}
300
307
  />
301
308
  <CustomCss customCss={config.customCss} configLoaded={configLoaded} />
302
- <TextEditor />
309
+ {store.textEditorRequested && (
310
+ <TextEditor />
311
+ )}
303
312
  <UpdateCheck
304
313
  skipVersion={config.skipVersion}
305
314
  upgradeInfo={upgradeInfo}
@@ -325,11 +334,23 @@ export default auto(function Index (props) {
325
334
  config={config}
326
335
  />
327
336
  <Remote2RemoteHandlers />
328
- <Resolutions {...resProps} />
329
- <InfoModal {...infoModalProps} />
337
+ {openResolutionEdit && (
338
+ <LazyBoundary>
339
+ <Suspense fallback={null}>
340
+ <Resolutions {...resProps} />
341
+ </Suspense>
342
+ </LazyBoundary>
343
+ )}
344
+ {store.showInfoModal && (
345
+ <LazyBoundary>
346
+ <Suspense fallback={null}>
347
+ <InfoModal {...infoModalProps} />
348
+ </Suspense>
349
+ </LazyBoundary>
350
+ )}
330
351
  <RightSidePanel {...rightPanelProps}>
331
352
  {!isAIDisabled() && <AIChat {...aiChatProps} />}
332
- <TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
353
+ <TerminalInfo key={store.activeTabId} store={store} {...deepCopy(store.terminalInfoProps)} />
333
354
  </RightSidePanel>
334
355
  <SshConfigLoadNotify {...sshConfigProps} />
335
356
  <LoadSshConfigs
@@ -346,7 +367,13 @@ export default auto(function Index (props) {
346
367
  <BatchOpRunner />
347
368
  <ImportProgress />
348
369
  <ShortcutBarEntry store={store} />
349
- {!isAIDisabled() && <AIConfigModal store={store} />}
370
+ {!isAIDisabled() && store.showAIConfigModal && (
371
+ <LazyBoundary>
372
+ <Suspense fallback={null}>
373
+ <AIConfigModal store={store} />
374
+ </Suspense>
375
+ </LazyBoundary>
376
+ )}
350
377
  <UnixTimestampTooltip />
351
378
  </div>
352
379
  </ConfigProvider>
@@ -5,6 +5,7 @@
5
5
  .terminal-control
6
6
  .qm-wrap-tooltip
7
7
  .main-footer
8
+ .remote-monitor-bar
8
9
  .session-v-info
9
10
  display none
10
11
  // Mobile tabs carry the extra .mobile-tabs class, and mobile.styl uses
@@ -40,11 +41,16 @@
40
41
  // visible; the session shrinks to sit on top of the footer. RDP/VNC/Spice
41
42
  // fullscreen stays footer-less — inActiveTerminal is false for those tabs, so
42
43
  // the fs-with-footer class is absent there.
43
- // 36px matches footerHeight / the .main-footer height in footer.styl.
44
+ // --footer-stack-height includes the 36px footer and the optional monitor row.
44
45
  .fullscreen.fs-with-footer
45
46
  .main-footer
46
47
  display block
47
48
  left 0 !important
49
+ &.remote-monitor-bar-on
50
+ .remote-monitor-bar
51
+ display flex
52
+ left 0 !important
53
+ width 100% !important
48
54
  .qm-wrap-tooltip
49
55
  display block
50
56
  left 0 !important
@@ -53,10 +59,10 @@
53
59
  display none
54
60
  .session-wrap.session-current
55
61
  .term-wrap
56
- height calc(100% - 36px - var(--shortcut-bar-h, 0px)) !important
62
+ height calc(100% - var(--footer-stack-height, 36px) - var(--shortcut-bar-h, 0px) - var(--shortcut-bar-kb-offset, 0px)) !important
57
63
  // pinned quick-command bar (180px, same as quickCommandBoxHeight) sits
58
64
  // between terminal and footer — reserve its space too
59
65
  &.qm-pinned
60
66
  .session-wrap.session-current
61
67
  .term-wrap
62
- height calc(100% - 36px - 180px - var(--shortcut-bar-h, 0px)) !important
68
+ height calc(100% - var(--footer-stack-height, 36px) - 180px - var(--shortcut-bar-h, 0px) - var(--shortcut-bar-kb-offset, 0px)) !important
@@ -9,7 +9,8 @@
9
9
  right 0
10
10
  z-index 89
11
11
  height auto
12
- bottom 36px
12
+
13
+ bottom var(--footer-stack-height, 36px)
13
14
 
14
15
  .qm-pinned
15
16
  .qm-wrap-tooltip
@@ -101,4 +102,4 @@
101
102
  > .drag
102
103
  align-self flex-start
103
104
  > button:last-child
104
- align-self flex-end
105
+ align-self flex-end
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Create a quick command from outside the settings panel (e.g. from the
3
+ * command history action menu), reusing the very same form the settings panel
4
+ * uses — no fork of the form, so fields/validation stay in sync.
5
+ *
6
+ * The form is heavy (form, shortcuts editor, templates), and it otherwise only
7
+ * lives inside the lazily loaded quick commands setting tab, so lazy load it
8
+ * here as well.
9
+ */
10
+
11
+ import { lazy, Suspense } from 'react'
12
+ import {
13
+ Modal,
14
+ Spin
15
+ } from 'antd'
16
+ import LazyBoundary from '../common/lazy-boundary'
17
+ import generate from '../../common/uid'
18
+
19
+ const QuickCommandForm = lazy(() => import('./quick-commands-form'))
20
+
21
+ const e = window.translate
22
+
23
+ export default function QuickCommandCreateModal (props) {
24
+ const { store, command, onClose } = props
25
+ const formData = {
26
+ id: '',
27
+ name: e('newQuickCommand'),
28
+ commands: [{
29
+ command,
30
+ id: generate(),
31
+ delay: 100
32
+ }]
33
+ }
34
+ return (
35
+ <Modal
36
+ open
37
+ title={e('addQuickCommands')}
38
+ onCancel={onClose}
39
+ destroyOnHidden
40
+ footer={null}
41
+ width={560}
42
+ >
43
+ <LazyBoundary>
44
+ <Suspense
45
+ fallback={(
46
+ <div className='pd3 aligncenter'>
47
+ <Spin />
48
+ </div>
49
+ )}
50
+ >
51
+ <QuickCommandForm
52
+ store={store}
53
+ formData={formData}
54
+ onSaved={onClose}
55
+ />
56
+ </Suspense>
57
+ </LazyBoundary>
58
+ </Modal>
59
+ )
60
+ }
@@ -73,12 +73,18 @@ export default function QuickCommandForm (props) {
73
73
  store.editQuickCommand(formData.id, update)
74
74
  } else {
75
75
  store.addQuickCommand(update1)
76
- store.setSettingItem({
77
- id: '',
78
- name: e('newQuickCommand')
79
- })
76
+ // resetting the settings panel form only makes sense there; when the
77
+ // form is hosted elsewhere (e.g. the command history modal) the caller
78
+ // decides what happens next
79
+ if (!props.onSaved) {
80
+ store.setSettingItem({
81
+ id: '',
82
+ name: e('newQuickCommand')
83
+ })
84
+ }
80
85
  }
81
86
  message.success(e('saved'))
87
+ props.onSaved?.()
82
88
  }
83
89
  const initialValues = formData
84
90
  if (!initialValues.labels) {
@@ -7,6 +7,7 @@ import { getLocalFileInfo } from '../sftp/file-read'
7
7
  import { osResolve } from '../../common/resolve'
8
8
  import { filesize } from 'filesize'
9
9
  import sanitizeFilename from '../../common/sanitize-filename'
10
+ import { chooseSaveDirectory } from '../../common/choose-save-folder'
10
11
 
11
12
  const LOG_PREFIX = '[RDP-FILE-TRANSFER]'
12
13
 
@@ -231,23 +232,22 @@ export class FileTransferManager {
231
232
 
232
233
  async handleFileDownload (fileInfo) {
233
234
  try {
234
- const savePath = await window.api.openDialog({
235
+ const savePath = await chooseSaveDirectory({
235
236
  title: `Save ${fileInfo.name}`,
236
237
  message: `Choose where to save ${fileInfo.name}`,
237
- properties: ['openDirectory', 'createDirectory'],
238
238
  noBrowserTransfer: true
239
239
  }).catch((err) => {
240
240
  this.log(`Save dialog error: ${err.message}`, 'error')
241
241
  return false
242
242
  })
243
243
 
244
- if (!savePath || !savePath.length) {
244
+ if (!savePath) {
245
245
  this.log('Download cancelled by user', 'info')
246
246
  return
247
247
  }
248
248
 
249
249
  const safeName = sanitizeFilename(fileInfo.name)
250
- const fullPath = osResolve(savePath[0], safeName)
250
+ const fullPath = osResolve(savePath, safeName)
251
251
 
252
252
  const fd = await new Promise((resolve, reject) => {
253
253
  window.fs.open(fullPath, O_WRONLY | O_CREAT | O_TRUNC, (err, fd) => {