@electerm/electerm-react 3.12.0 → 3.15.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.
- package/client/common/is-absolute-path.js +1 -1
- package/client/common/normalize-remote-path.js +20 -0
- package/client/common/resolve.js +16 -0
- package/client/components/ai/ai-chat-history-item.jsx +1 -2
- package/client/components/main/main.jsx +1 -1
- package/client/components/sftp/address-bar.jsx +22 -6
- package/client/components/sftp/file-item.jsx +31 -1
- package/client/components/sftp/file-read.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +38 -3
- package/client/components/sys-menu/icons-map.jsx +10 -2
- package/client/components/terminal/terminal-apis.js +9 -0
- package/client/components/terminal/terminal.jsx +111 -2
- package/client/components/terminal-info/base.jsx +41 -38
- package/client/components/terminal-info/log-path-edit.jsx +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure remote path always starts with /
|
|
3
|
+
* Windows drive letters like c: become /c:
|
|
4
|
+
* Also fixes mixed separators like /c:\windows → /c:/windows
|
|
5
|
+
* This is needed because SFTP protocol expects paths with leading /
|
|
6
|
+
* @param {String} path
|
|
7
|
+
* @return {String}
|
|
8
|
+
*/
|
|
9
|
+
export default function normalizeRemotePath (path) {
|
|
10
|
+
if (!path) return path
|
|
11
|
+
// Fix mixed separators: /c:\windows → /c:/windows
|
|
12
|
+
if (/^\/[a-zA-Z]:\\/.test(path)) {
|
|
13
|
+
return path.replace(/\\/g, '/')
|
|
14
|
+
}
|
|
15
|
+
// Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
|
|
16
|
+
if (/^[a-zA-Z]:/.test(path)) {
|
|
17
|
+
return '/' + path.replace(/\\/g, '/')
|
|
18
|
+
}
|
|
19
|
+
return path
|
|
20
|
+
}
|
package/client/common/resolve.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* @return {String}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export const isWslPath = (path) => /^\\\\(?:wsl\$|wsl\.localhost)\\/.test(path)
|
|
9
|
+
|
|
10
|
+
export const isWslDistroRoot = (path) => {
|
|
11
|
+
const trimmed = path.replace(/\\$/, '')
|
|
12
|
+
return /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(trimmed)
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export default function resolve (basePath, nameOrDot) {
|
|
9
16
|
const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
|
|
10
17
|
const isWin = basePath.includes('\\') || nameOrDot.includes('\\') || hasWinDrive(basePath) || hasWinDrive(nameOrDot)
|
|
@@ -15,7 +22,13 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
15
22
|
if (nameOrDot.startsWith('/')) {
|
|
16
23
|
return nameOrDot.replace(/\\/g, sep)
|
|
17
24
|
}
|
|
25
|
+
if (nameOrDot.startsWith('\\\\')) {
|
|
26
|
+
return nameOrDot
|
|
27
|
+
}
|
|
18
28
|
if (nameOrDot === '..') {
|
|
29
|
+
if (isWslDistroRoot(basePath)) {
|
|
30
|
+
return '/'
|
|
31
|
+
}
|
|
19
32
|
const baseEndsWithSep = basePath.endsWith(sep)
|
|
20
33
|
const parts = basePath.split(sep)
|
|
21
34
|
if (parts.length > 1) {
|
|
@@ -27,6 +40,9 @@ export default function resolve (basePath, nameOrDot) {
|
|
|
27
40
|
}
|
|
28
41
|
return '/'
|
|
29
42
|
}
|
|
43
|
+
if (isWslDistroRoot(basePath) && !basePath.endsWith(sep)) {
|
|
44
|
+
return basePath + sep + nameOrDot
|
|
45
|
+
}
|
|
30
46
|
const result = basePath.endsWith(sep) ? basePath + nameOrDot : basePath + sep + nameOrDot
|
|
31
47
|
return isWin && result.length === 3 && result.endsWith(':\\') ? '/' : result
|
|
32
48
|
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Tooltip
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import {
|
|
11
|
-
UserOutlined,
|
|
12
11
|
CopyOutlined,
|
|
13
12
|
CloseOutlined,
|
|
14
13
|
CaretDownOutlined,
|
|
@@ -172,7 +171,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
172
171
|
<span className='pointer mg1r' onClick={toggleOutput}>
|
|
173
172
|
{showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
|
|
174
173
|
</span>
|
|
175
|
-
<
|
|
174
|
+
<span>{prompt}</span>
|
|
176
175
|
{renderStopButton()}
|
|
177
176
|
</div>
|
|
178
177
|
),
|
|
@@ -282,7 +282,7 @@ export default auto(function Index (props) {
|
|
|
282
282
|
<InfoModal {...infoModalProps} />
|
|
283
283
|
<RightSidePanel {...rightPanelProps}>
|
|
284
284
|
<AIChat {...aiChatProps} />
|
|
285
|
-
<TerminalInfo {...terminalInfoProps} />
|
|
285
|
+
<TerminalInfo key={store.activeTabId} {...terminalInfoProps} />
|
|
286
286
|
</RightSidePanel>
|
|
287
287
|
<SshConfigLoadNotify {...sshConfigProps} />
|
|
288
288
|
<LoadSshConfigs
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
ReloadOutlined,
|
|
7
7
|
ArrowRightOutlined,
|
|
8
8
|
LoadingOutlined,
|
|
9
|
-
HomeOutlined
|
|
9
|
+
HomeOutlined,
|
|
10
|
+
PlusOutlined
|
|
10
11
|
} from '@ant-design/icons'
|
|
11
12
|
import {
|
|
12
13
|
Input,
|
|
@@ -74,7 +75,7 @@ function renderAddonBefore (props, realPath) {
|
|
|
74
75
|
)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
|
|
78
|
+
function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type, handleUploadFromBrowser) {
|
|
78
79
|
const handleClick = (e) => {
|
|
79
80
|
e.stopPropagation()
|
|
80
81
|
if (!isLoadingRemote) {
|
|
@@ -82,9 +83,24 @@ function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
return (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
<>
|
|
87
|
+
{
|
|
88
|
+
type === typeMap.local && window.et.isWebApp
|
|
89
|
+
? (
|
|
90
|
+
<PlusOutlined
|
|
91
|
+
className='mg1r'
|
|
92
|
+
onClick={(e) => {
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
handleUploadFromBrowser()
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
: null
|
|
99
|
+
}
|
|
100
|
+
<GoIcon
|
|
101
|
+
onClick={handleClick}
|
|
102
|
+
/>
|
|
103
|
+
</>
|
|
88
104
|
)
|
|
89
105
|
}
|
|
90
106
|
|
|
@@ -160,7 +176,7 @@ export default function AddressBar (props) {
|
|
|
160
176
|
onBlur={() => props.onInputBlur(type)}
|
|
161
177
|
disabled={loadingSftp}
|
|
162
178
|
suffix={
|
|
163
|
-
renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type)
|
|
179
|
+
renderAddonAfter(isLoadingRemote, onGoto, GoIcon, type, props.handleUploadFromBrowser)
|
|
164
180
|
}
|
|
165
181
|
/>
|
|
166
182
|
{renderHistory(props, type)}
|
|
@@ -14,6 +14,7 @@ import copy from 'json-deep-copy'
|
|
|
14
14
|
import { pick, some } from 'lodash-es'
|
|
15
15
|
import Input from '../common/input-auto-focus'
|
|
16
16
|
import resolve from '../../common/resolve'
|
|
17
|
+
import normalizeRemotePath from '../../common/normalize-remote-path'
|
|
17
18
|
import { addClass, removeClass } from '../../common/class'
|
|
18
19
|
import {
|
|
19
20
|
mode2permission,
|
|
@@ -604,7 +605,10 @@ export default class FileSection extends React.Component {
|
|
|
604
605
|
const { type, name, isParent } = file
|
|
605
606
|
const n = `${type}Path`
|
|
606
607
|
const path = isParent ? file.path : this.props[n]
|
|
607
|
-
|
|
608
|
+
let np = resolve(path, name)
|
|
609
|
+
if (type === typeMap.remote) {
|
|
610
|
+
np = normalizeRemotePath(np)
|
|
611
|
+
}
|
|
608
612
|
const op = this.props[type + 'Path']
|
|
609
613
|
this.props.modifier({
|
|
610
614
|
[n]: np,
|
|
@@ -882,6 +886,25 @@ export default class FileSection extends React.Component {
|
|
|
882
886
|
window.pre.showItemInFolder(p)
|
|
883
887
|
}
|
|
884
888
|
|
|
889
|
+
downloadFromBrowser = async () => {
|
|
890
|
+
const { path, name, isDirectory } = this.state.file
|
|
891
|
+
const p = resolve(path, name)
|
|
892
|
+
const url = '/api/download?path=' + encodeURIComponent(p)
|
|
893
|
+
const res = await window.fetch(url, {
|
|
894
|
+
headers: {
|
|
895
|
+
token: window.store?.config.tokenElecterm
|
|
896
|
+
}
|
|
897
|
+
})
|
|
898
|
+
const blob = await res.blob()
|
|
899
|
+
const a = document.createElement('a')
|
|
900
|
+
a.href = URL.createObjectURL(blob)
|
|
901
|
+
a.download = isDirectory ? name + '.tar.gz' : name
|
|
902
|
+
document.body.appendChild(a)
|
|
903
|
+
a.click()
|
|
904
|
+
document.body.removeChild(a)
|
|
905
|
+
URL.revokeObjectURL(a.href)
|
|
906
|
+
}
|
|
907
|
+
|
|
885
908
|
newItem = (isDirectory) => {
|
|
886
909
|
const { type } = this.state.file
|
|
887
910
|
const list = copy(this.props[type])
|
|
@@ -1060,6 +1083,13 @@ export default class FileSection extends React.Component {
|
|
|
1060
1083
|
text: e('showInDefaultFileMananger')
|
|
1061
1084
|
})
|
|
1062
1085
|
}
|
|
1086
|
+
if (isLocal && isRealFile && window.et.isWebApp) {
|
|
1087
|
+
res.push({
|
|
1088
|
+
func: 'downloadFromBrowser',
|
|
1089
|
+
icon: 'DownloadOutlined',
|
|
1090
|
+
text: 'Download from browser'
|
|
1091
|
+
})
|
|
1092
|
+
}
|
|
1063
1093
|
if (showEdit) {
|
|
1064
1094
|
res.push({
|
|
1065
1095
|
func: 'editFile',
|
|
@@ -7,6 +7,12 @@ import fs from '../../common/fs'
|
|
|
7
7
|
import { isWin } from '../../common/constants'
|
|
8
8
|
|
|
9
9
|
export const getFileExt = fileName => {
|
|
10
|
+
if (/^\\\\(?:wsl\$|wsl\.localhost)\\/.test(fileName)) {
|
|
11
|
+
return {
|
|
12
|
+
base: fileName,
|
|
13
|
+
ext: ''
|
|
14
|
+
}
|
|
15
|
+
}
|
|
10
16
|
const sep = '.'
|
|
11
17
|
const arr = fileName.split(sep)
|
|
12
18
|
const len = arr.length
|
|
@@ -82,12 +88,15 @@ export const getFolderFromFilePath = (filePath, isRemote) => {
|
|
|
82
88
|
const arr = filePath.split(sep)
|
|
83
89
|
const len = arr.length
|
|
84
90
|
const isWinDisk = isWin && filePath.endsWith(sep)
|
|
85
|
-
const
|
|
91
|
+
const isWslRoot = isWin && /^\\\\(?:wsl\$|wsl\.localhost)\\[^\\]+$/.test(filePath.replace(/\\$/, ''))
|
|
92
|
+
const path = (isWinDisk || isWslRoot)
|
|
86
93
|
? '/'
|
|
87
94
|
: arr.slice(0, len - 1).join(sep)
|
|
88
95
|
const name = isWinDisk
|
|
89
96
|
? filePath.replace(sep, '')
|
|
90
|
-
:
|
|
97
|
+
: isWslRoot
|
|
98
|
+
? filePath
|
|
99
|
+
: arr[len - 1]
|
|
91
100
|
|
|
92
101
|
return {
|
|
93
102
|
path,
|
|
@@ -29,6 +29,7 @@ import fs from '../../common/fs'
|
|
|
29
29
|
import ListTable from './list-table-ui'
|
|
30
30
|
import deepCopy from 'json-deep-copy'
|
|
31
31
|
import isValidPath from '../../common/is-valid-path'
|
|
32
|
+
import normalizeRemotePath from '../../common/normalize-remote-path'
|
|
32
33
|
import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
33
34
|
import * as owner from './owner-list'
|
|
34
35
|
import AddressBar from './address-bar'
|
|
@@ -292,6 +293,7 @@ export default class Sftp extends Component {
|
|
|
292
293
|
if (!path && this.sftp) {
|
|
293
294
|
path = await this.getPwd(this.props.tab.username)
|
|
294
295
|
}
|
|
296
|
+
path = normalizeRemotePath(path)
|
|
295
297
|
} else {
|
|
296
298
|
path = this.getLocalHome()
|
|
297
299
|
}
|
|
@@ -762,7 +764,7 @@ export default class Sftp extends Component {
|
|
|
762
764
|
|
|
763
765
|
if (!remotePath) {
|
|
764
766
|
if (startDirectory) {
|
|
765
|
-
remotePath = startDirectory
|
|
767
|
+
remotePath = normalizeRemotePath(startDirectory)
|
|
766
768
|
} else {
|
|
767
769
|
remotePath = await this.getPwd(username)
|
|
768
770
|
}
|
|
@@ -970,6 +972,31 @@ export default class Sftp extends Component {
|
|
|
970
972
|
})
|
|
971
973
|
}
|
|
972
974
|
|
|
975
|
+
handleUploadFromBrowser = () => {
|
|
976
|
+
const input = document.createElement('input')
|
|
977
|
+
input.type = 'file'
|
|
978
|
+
input.multiple = true
|
|
979
|
+
input.onchange = async () => {
|
|
980
|
+
const files = input.files
|
|
981
|
+
if (!files || !files.length) return
|
|
982
|
+
const { localPath } = this.state
|
|
983
|
+
for (const file of files) {
|
|
984
|
+
const formData = new FormData()
|
|
985
|
+
formData.append('file', file)
|
|
986
|
+
formData.append('path', localPath)
|
|
987
|
+
await window.fetch('/api/upload', {
|
|
988
|
+
method: 'POST',
|
|
989
|
+
body: formData,
|
|
990
|
+
headers: {
|
|
991
|
+
token: window.store?.config.tokenElecterm
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
this.localList()
|
|
996
|
+
}
|
|
997
|
+
input.click()
|
|
998
|
+
}
|
|
999
|
+
|
|
973
1000
|
parsePath = async (type, pth) => {
|
|
974
1001
|
const reg = /^%([^%]+)%/
|
|
975
1002
|
if (!reg.test(pth)) {
|
|
@@ -995,7 +1022,10 @@ export default class Sftp extends Component {
|
|
|
995
1022
|
const n = `${type}Path`
|
|
996
1023
|
const nt = n + 'Temp'
|
|
997
1024
|
const oldPath = this.state[type + 'Path']
|
|
998
|
-
|
|
1025
|
+
let np = await this.parsePath(type, this.state[nt])
|
|
1026
|
+
if (type === typeMap.remote) {
|
|
1027
|
+
np = normalizeRemotePath(np)
|
|
1028
|
+
}
|
|
999
1029
|
if (!isValidPath(np)) {
|
|
1000
1030
|
return notification.warning({
|
|
1001
1031
|
message: 'path not valid'
|
|
@@ -1003,6 +1033,7 @@ export default class Sftp extends Component {
|
|
|
1003
1033
|
}
|
|
1004
1034
|
this.setState({
|
|
1005
1035
|
[n]: np,
|
|
1036
|
+
[nt]: np,
|
|
1006
1037
|
[`${type}Keyword`]: ''
|
|
1007
1038
|
}, () => this[`${type}List`](undefined, undefined, oldPath))
|
|
1008
1039
|
}
|
|
@@ -1010,7 +1041,10 @@ export default class Sftp extends Component {
|
|
|
1010
1041
|
goParent = (type) => {
|
|
1011
1042
|
const n = `${type}Path`
|
|
1012
1043
|
const p = this.state[n]
|
|
1013
|
-
|
|
1044
|
+
let np = resolve(p, '..')
|
|
1045
|
+
if (type === typeMap.remote) {
|
|
1046
|
+
np = normalizeRemotePath(np)
|
|
1047
|
+
}
|
|
1014
1048
|
const op = this.state[n]
|
|
1015
1049
|
if (np !== p) {
|
|
1016
1050
|
this.setState({
|
|
@@ -1218,6 +1252,7 @@ export default class Sftp extends Component {
|
|
|
1218
1252
|
const addrProps = {
|
|
1219
1253
|
host,
|
|
1220
1254
|
type,
|
|
1255
|
+
handleUploadFromBrowser: this.handleUploadFromBrowser,
|
|
1221
1256
|
...pick(
|
|
1222
1257
|
this,
|
|
1223
1258
|
[
|
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
LockOutlined,
|
|
25
25
|
ReloadOutlined,
|
|
26
26
|
FileZipOutlined,
|
|
27
|
-
AppstoreOutlined
|
|
27
|
+
AppstoreOutlined,
|
|
28
|
+
SaveOutlined,
|
|
29
|
+
PlayCircleFilled,
|
|
30
|
+
StopOutlined,
|
|
31
|
+
DownloadOutlined
|
|
28
32
|
} from '@ant-design/icons'
|
|
29
33
|
import IconHolder from './icon-holder'
|
|
30
34
|
|
|
@@ -52,5 +56,9 @@ export default {
|
|
|
52
56
|
LockOutlined,
|
|
53
57
|
ReloadOutlined,
|
|
54
58
|
FileZipOutlined,
|
|
55
|
-
AppstoreOutlined
|
|
59
|
+
AppstoreOutlined,
|
|
60
|
+
SaveOutlined,
|
|
61
|
+
PlayCircleFilled,
|
|
62
|
+
StopOutlined,
|
|
63
|
+
DownloadOutlined
|
|
56
64
|
}
|
|
@@ -49,3 +49,12 @@ export function setTerminalLogPath (pid, logPath) {
|
|
|
49
49
|
action: 'set-terminal-log-path'
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
export function startTerminalLogFile (pid, logFilePath, addTimeStampToTermLog) {
|
|
54
|
+
return fetch({
|
|
55
|
+
pid,
|
|
56
|
+
logFilePath,
|
|
57
|
+
addTimeStampToTermLog,
|
|
58
|
+
action: 'start-terminal-log-file'
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
Dropdown
|
|
8
8
|
} from 'antd'
|
|
9
9
|
import message from '../common/message'
|
|
10
|
+
import { notification } from '../common/notification'
|
|
11
|
+
import ShowItem from '../common/show-item.jsx'
|
|
10
12
|
import Modal from '../common/modal'
|
|
11
13
|
import classnames from 'classnames'
|
|
12
14
|
import './terminal.styl'
|
|
@@ -30,10 +32,11 @@ import { XmodemClient } from './xmodem-client.js'
|
|
|
30
32
|
import DropFileModal from './drop-file-modal.jsx'
|
|
31
33
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
32
34
|
import NormalBuffer from './normal-buffer.jsx'
|
|
33
|
-
import { createTerm, resizeTerm } from './terminal-apis.js'
|
|
35
|
+
import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
|
|
34
36
|
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
35
37
|
import { KeywordHighlighterAddon } from './highlight-addon.js'
|
|
36
38
|
import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
|
|
39
|
+
import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
37
40
|
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
38
41
|
import AIIcon from '../icons/ai-icon.jsx'
|
|
39
42
|
import {
|
|
@@ -72,6 +75,9 @@ class Term extends Component {
|
|
|
72
75
|
saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
|
|
73
76
|
addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
|
|
74
77
|
logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
|
|
78
|
+
logFileName: '',
|
|
79
|
+
recording: false,
|
|
80
|
+
recordingFilePath: '',
|
|
75
81
|
passType: 'password',
|
|
76
82
|
lines: [],
|
|
77
83
|
searchResults: [],
|
|
@@ -676,8 +682,101 @@ class Term extends Component {
|
|
|
676
682
|
)
|
|
677
683
|
}
|
|
678
684
|
|
|
685
|
+
getTerminalBufferText = () => {
|
|
686
|
+
const { addTimeStampToTermLog } = this.state
|
|
687
|
+
const buffer = this.term.buffer.active
|
|
688
|
+
const len = buffer.length
|
|
689
|
+
const rawLines = []
|
|
690
|
+
for (let i = 0; i < len; i++) {
|
|
691
|
+
const line = buffer.getLine(i)
|
|
692
|
+
rawLines.push(line ? line.translateToString(false) : '')
|
|
693
|
+
}
|
|
694
|
+
// trim trailing blank lines before applying timestamps
|
|
695
|
+
while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
|
|
696
|
+
rawLines.pop()
|
|
697
|
+
}
|
|
698
|
+
if (!addTimeStampToTermLog) {
|
|
699
|
+
return rawLines.join('\n')
|
|
700
|
+
}
|
|
701
|
+
return rawLines.map(text => {
|
|
702
|
+
const now = new Date()
|
|
703
|
+
const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
|
|
704
|
+
return ts + text
|
|
705
|
+
}).join('\n')
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
syncTermInfo = (stateUpdate) => {
|
|
709
|
+
this.setState(stateUpdate)
|
|
710
|
+
const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
|
|
711
|
+
if (Object.keys(infoUpdate).length) {
|
|
712
|
+
refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
openLogSaveDialog = async (titleKey) => {
|
|
717
|
+
const { logName } = this.props
|
|
718
|
+
const result = await window.api.saveDialog({
|
|
719
|
+
title: e(titleKey),
|
|
720
|
+
defaultPath: logName + '.log',
|
|
721
|
+
filters: [
|
|
722
|
+
{ name: 'Log files', extensions: ['log'] }
|
|
723
|
+
],
|
|
724
|
+
properties: ['createDirectory', 'showOverwriteConfirmation']
|
|
725
|
+
})
|
|
726
|
+
if (result.canceled || !result.filePath) {
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
return result.filePath
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
onSaveTerminalLog = async () => {
|
|
733
|
+
const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
|
|
734
|
+
if (!filePath) {
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
const content = this.getTerminalBufferText()
|
|
738
|
+
await window.fs.writeFile(filePath, content).catch(window.store.onError)
|
|
739
|
+
const { addTimeStampToTermLog } = this.state
|
|
740
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
741
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
742
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
743
|
+
notification.success({
|
|
744
|
+
message: e('saveTerminalLogToFile'),
|
|
745
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
746
|
+
duration: 5
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
onRecord = async () => {
|
|
751
|
+
const filePath = await this.openLogSaveDialog('record')
|
|
752
|
+
if (!filePath) {
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
const { addTimeStampToTermLog } = this.state
|
|
756
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
757
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
758
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
759
|
+
this.setState({ recording: true, recordingFilePath: filePath })
|
|
760
|
+
notification.success({
|
|
761
|
+
message: e('record'),
|
|
762
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
763
|
+
duration: 5
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
onStopRecord = () => {
|
|
768
|
+
const { recordingFilePath } = this.state
|
|
769
|
+
toggleTerminalLog(this.pid).catch(window.store.onError)
|
|
770
|
+
this.syncTermInfo({ saveTerminalLogToFile: false })
|
|
771
|
+
this.setState({ recording: false, recordingFilePath: '' })
|
|
772
|
+
notification.success({
|
|
773
|
+
message: e('stopRecord'),
|
|
774
|
+
description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
679
778
|
renderContextMenu = () => {
|
|
680
|
-
const { hasSelection } = this.state
|
|
779
|
+
const { hasSelection, recording } = this.state
|
|
681
780
|
const copyed = true
|
|
682
781
|
const copyShortcut = this.getShortcut('terminal_copy')
|
|
683
782
|
const pasteShortcut = this.getShortcut('terminal_paste')
|
|
@@ -730,6 +829,16 @@ class Term extends Component {
|
|
|
730
829
|
icon: <iconsMap.SearchOutlined />,
|
|
731
830
|
label: e('search'),
|
|
732
831
|
extra: searchShortcut
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
key: 'onSaveTerminalLog',
|
|
835
|
+
icon: <iconsMap.SaveOutlined />,
|
|
836
|
+
label: e('saveTerminalLogToFile')
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
key: recording ? 'onStopRecord' : 'onRecord',
|
|
840
|
+
icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
|
|
841
|
+
label: e(recording ? 'stopRecord' : 'record')
|
|
733
842
|
}
|
|
734
843
|
]
|
|
735
844
|
if (isSerial) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Button
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import defaults from '../../common/default-setting'
|
|
11
|
-
import { toggleTerminalLog, toggleTerminalLogTimestamp
|
|
11
|
+
import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
|
|
12
12
|
import {
|
|
13
13
|
ClockCircleOutlined,
|
|
14
14
|
BorderlessTableOutlined,
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
PartitionOutlined
|
|
19
19
|
} from '@ant-design/icons'
|
|
20
20
|
import { refs } from '../common/ref'
|
|
21
|
-
import
|
|
21
|
+
import ShowItem from '../common/show-item'
|
|
22
|
+
import { osResolve } from '../../common/resolve'
|
|
23
|
+
import createDefaultLogPath from '../../common/default-log-path'
|
|
22
24
|
|
|
23
25
|
const e = window.translate
|
|
24
26
|
|
|
@@ -35,15 +37,20 @@ export default class TerminalInfoBase extends Component {
|
|
|
35
37
|
state = {
|
|
36
38
|
saveTerminalLogToFile: false,
|
|
37
39
|
addTimeStampToTermLog: false,
|
|
38
|
-
logPath: ''
|
|
40
|
+
logPath: '',
|
|
41
|
+
logFileName: ''
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
componentDidMount () {
|
|
45
|
+
const { pid } = this.props
|
|
46
|
+
refs.add('term-info-' + pid, this)
|
|
42
47
|
this.getState()
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
componentWillUnmount () {
|
|
46
51
|
clearTimeout(this.timer)
|
|
52
|
+
const { pid } = this.props
|
|
53
|
+
refs.remove('term-info-' + pid)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
handleToggleTimestamp = () => {
|
|
@@ -74,17 +81,6 @@ export default class TerminalInfoBase extends Component {
|
|
|
74
81
|
})
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
onLogPathChange = (v) => {
|
|
78
|
-
const { pid } = this.props
|
|
79
|
-
setTerminalLogPath(pid, v)
|
|
80
|
-
refs.get('term-' + pid)?.setState({
|
|
81
|
-
logPath: v
|
|
82
|
-
})
|
|
83
|
-
this.setState({
|
|
84
|
-
logPath: v
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
handleToggle = () => {
|
|
89
85
|
const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
|
|
90
86
|
const {
|
|
@@ -112,7 +108,8 @@ export default class TerminalInfoBase extends Component {
|
|
|
112
108
|
this.setState({
|
|
113
109
|
saveTerminalLogToFile: term.state.saveTerminalLogToFile,
|
|
114
110
|
addTimeStampToTermLog: term.state.addTimeStampToTermLog,
|
|
115
|
-
logPath: term.state.logPath
|
|
111
|
+
logPath: term.state.logPath,
|
|
112
|
+
logFileName: term.state.logFileName || ''
|
|
116
113
|
})
|
|
117
114
|
} else {
|
|
118
115
|
this.timer = setTimeout(this.getState, 100)
|
|
@@ -166,39 +163,45 @@ export default class TerminalInfoBase extends Component {
|
|
|
166
163
|
render () {
|
|
167
164
|
const {
|
|
168
165
|
id,
|
|
169
|
-
logName
|
|
170
|
-
pid
|
|
166
|
+
logName
|
|
171
167
|
} = this.props
|
|
172
|
-
const { saveTerminalLogToFile, logPath } = this.state
|
|
168
|
+
const { saveTerminalLogToFile, logPath, logFileName } = this.state
|
|
173
169
|
const name = e('saveTerminalLogToFile')
|
|
170
|
+
const base = logPath || createDefaultLogPath()
|
|
171
|
+
const fileName = logFileName || (logName + '.log')
|
|
172
|
+
const fullPath = osResolve(base, fileName)
|
|
174
173
|
return (
|
|
175
174
|
<div className='terminal-info-section terminal-info-base'>
|
|
176
|
-
<div className='
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
<div className='pd1b'>
|
|
176
|
+
<b>ID:</b> {id}
|
|
177
|
+
</div>
|
|
178
|
+
<div className='pd1b'>
|
|
179
|
+
<Switch
|
|
180
|
+
checkedChildren={name}
|
|
181
|
+
unCheckedChildren={name}
|
|
182
|
+
checked={saveTerminalLogToFile}
|
|
183
|
+
onChange={this.handleToggle}
|
|
184
|
+
className='mg1r mg1b'
|
|
185
|
+
/>
|
|
186
|
+
{
|
|
187
|
+
this.renderTimestamp()
|
|
188
|
+
}
|
|
190
189
|
</div>
|
|
190
|
+
{
|
|
191
|
+
saveTerminalLogToFile
|
|
192
|
+
? (
|
|
193
|
+
<div className='pd1b font-xs color-grey'>
|
|
194
|
+
{e('terminalLogPath')}: {fullPath} <ShowItem to={fullPath} />
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
: null
|
|
198
|
+
}
|
|
191
199
|
<div className='pd2y'>
|
|
192
200
|
{
|
|
193
201
|
this.renderInfoSelection()
|
|
194
202
|
}
|
|
195
203
|
</div>
|
|
196
|
-
|
|
197
|
-
pid={pid}
|
|
198
|
-
logPath={logPath}
|
|
199
|
-
logName={logName}
|
|
200
|
-
setLogPath={this.onLogPathChange}
|
|
201
|
-
/>
|
|
204
|
+
|
|
202
205
|
</div>
|
|
203
206
|
)
|
|
204
207
|
}
|
|
@@ -12,10 +12,11 @@ import { osResolve } from '../../common/resolve'
|
|
|
12
12
|
|
|
13
13
|
const e = window.translate
|
|
14
14
|
|
|
15
|
-
export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
|
|
15
|
+
export default function LogPathEdit ({ pid, logPath, logName, logFileName, setLogPath }) {
|
|
16
16
|
const defaultPath = createDefaultLogPath()
|
|
17
17
|
const base = logPath || defaultPath
|
|
18
|
-
const
|
|
18
|
+
const fileName = logFileName || (logName + '.log')
|
|
19
|
+
const fullPath = osResolve(base, fileName)
|
|
19
20
|
|
|
20
21
|
const testAndSet = async (v) => {
|
|
21
22
|
if (v) {
|