flea-qml 0.1.0 → 0.2.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/README.md +35 -2
- package/package.json +2 -2
- package/src/archive.js +5 -0
- package/src/archiveops.js +36 -5
- package/src/menu.js +2 -2
- package/src/meta.js +9 -3
- package/src/terminal-server.js +95 -0
- package/src/terminal-shell.js +26 -0
- package/src/trashbrowse.js +1 -0
- package/test/delete-trash.test.js +2 -2
- package/test/file-ops.test.js +66 -0
- package/test/file-url.test.js +24 -0
- package/test/manual-archive-here-ui.mjs +47 -0
- package/test/manual-folder-focus.mjs +63 -0
- package/test/manual-terminal-lifecycle.mjs +42 -0
- package/test/manual-terminal-navigation.mjs +48 -0
- package/test/manual-terminal-ui.mjs +47 -0
- package/test/trashbrowse.test.js +1 -1
- package/ui/ArchiveCards.qml +28 -1
- package/ui/ArchiveHereConfirm.qml +127 -0
- package/ui/Backend.qml +5 -1
- package/ui/ColumnPane.qml +2 -0
- package/ui/Opener.qml +16 -36
- package/ui/Pane.qml +25 -1
- package/ui/PaneRail.qml +32 -3
- package/ui/PaneWire.qml +2 -6
- package/ui/Preview.qml +3 -2
- package/ui/PreviewArchive.qml +26 -1
- package/ui/PreviewColumn.qml +2 -0
- package/ui/QuickLookArchive.qml +3 -0
- package/ui/SelectionPreview.qml +1 -1
- package/ui/Sidebar.qml +5 -9
- package/ui/TabBar.qml +3 -3
- package/ui/TerminalService.qml +69 -0
- package/ui/TerminalTab.qml +48 -0
- package/ui/TerminalTabs.qml +27 -0
- package/ui/WindowBody.qml +2 -0
- package/ui/boot/shell.qml +10 -1
- package/ui/js/Archive.js +1 -1
- package/ui/js/Drag.js +1 -5
- package/ui/js/Focus.js +2 -0
- package/ui/js/Format.js +10 -1
- package/ui/js/Keymap.js +2 -2
- package/ui/js/Messages.js +1 -1
- package/ui/js/Nav.js +1 -1
- package/ui/js/Tabs.js +15 -0
- package/ui/js/Tap.js +6 -0
- package/ui/js/Trash.js +4 -1
- package/ui/qmldir +2 -0
package/ui/TabBar.qml
CHANGED
|
@@ -128,7 +128,7 @@ Item {
|
|
|
128
128
|
|
|
129
129
|
// A lift captures the tab payload before the pane can change.
|
|
130
130
|
function tabLiftBegan(index) {
|
|
131
|
-
if (!Tabs.canLift(root.pane)) {
|
|
131
|
+
if (!Tabs.canLift(root.pane) || (root.tabs && root.tabs.items[index]?.terminalId)) {
|
|
132
132
|
root.dragFrom = -1
|
|
133
133
|
root.dropAt = -1
|
|
134
134
|
return
|
|
@@ -397,7 +397,7 @@ Item {
|
|
|
397
397
|
readonly property bool current: root.currentIndex === tab.index
|
|
398
398
|
readonly property Item focusFrame: tabFrame
|
|
399
399
|
// Every input named, so the label re-reads when a tab opens or the pane navigates.
|
|
400
|
-
readonly property string title: Tabs.label(
|
|
400
|
+
readonly property string title: (root.tabs && root.tabs.items[tab.index]?.terminalId ? "Terminal · " : "") + Tabs.label(
|
|
401
401
|
Tabs.pathAt(root.tabs, root.currentIndex, tab.index, root.path),
|
|
402
402
|
pane ? pane.home : "")
|
|
403
403
|
|
|
@@ -412,7 +412,7 @@ Item {
|
|
|
412
412
|
Flea.DropInto {
|
|
413
413
|
anchors.fill: parent
|
|
414
414
|
pane: root.pane
|
|
415
|
-
enabled: DragOut.searchTabEnabled(root.pane ? root.pane.searchMode : "", tab.current)
|
|
415
|
+
enabled: !(root.tabs && root.tabs.items[tab.index]?.terminalId) && DragOut.searchTabEnabled(root.pane ? root.pane.searchMode : "", tab.current)
|
|
416
416
|
switchesOnHover: true
|
|
417
417
|
refuseLoading: DragOut.refuseLoading(root.pane && root.pane.listInFlight, true, tab.current)
|
|
418
418
|
// The pane's drop path, not its drawn one: a tab selected by the hover switch is
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
pragma Singleton
|
|
2
|
+
import QtQuick
|
|
3
|
+
import Quickshell
|
|
4
|
+
import Quickshell.Io
|
|
5
|
+
|
|
6
|
+
// All terminal views share this one server; each has its own reconnect token/PTY.
|
|
7
|
+
QtObject {
|
|
8
|
+
id: root
|
|
9
|
+
property var tabs: ({})
|
|
10
|
+
property int serial: 0
|
|
11
|
+
property bool ready: false
|
|
12
|
+
property bool stopping: false
|
|
13
|
+
function acquire(path) {
|
|
14
|
+
var id = "terminal-" + (++serial)
|
|
15
|
+
var next = Object.assign({}, tabs)
|
|
16
|
+
next[id] = { path: path, url: "", error: "" }
|
|
17
|
+
tabs = next
|
|
18
|
+
if (!server.running && !stopping) server.running = true
|
|
19
|
+
if (ready) server.write(JSON.stringify({id:id,path:path}) + "\n")
|
|
20
|
+
return id
|
|
21
|
+
}
|
|
22
|
+
function release(id) {
|
|
23
|
+
var next = Object.assign({}, tabs)
|
|
24
|
+
delete next[id]
|
|
25
|
+
tabs = next
|
|
26
|
+
if (Object.keys(next).length === 0) {
|
|
27
|
+
ready = false
|
|
28
|
+
if (server.running && !stopping) {
|
|
29
|
+
stopping = true
|
|
30
|
+
server.write(JSON.stringify({shutdown:true}) + "\n")
|
|
31
|
+
}
|
|
32
|
+
} else if (ready) server.write(JSON.stringify({id:id,close:true}) + "\n")
|
|
33
|
+
}
|
|
34
|
+
function fail(message) {
|
|
35
|
+
ready = false
|
|
36
|
+
var next = {}
|
|
37
|
+
for (var id in tabs) next[id] = { path: tabs[id].path, url: "", error: message }
|
|
38
|
+
tabs = next
|
|
39
|
+
}
|
|
40
|
+
property var process: Process {
|
|
41
|
+
id: server
|
|
42
|
+
stdinEnabled: true
|
|
43
|
+
command: [Quickshell.env("BUNINU_BUN") || "bun", Quickshell.shellDir + "/../../src/terminal-server.js"]
|
|
44
|
+
stdout: SplitParser {
|
|
45
|
+
onRead: function(line) {
|
|
46
|
+
try {
|
|
47
|
+
var result = JSON.parse(line)
|
|
48
|
+
if (result.ready) {
|
|
49
|
+
if (root.stopping) return
|
|
50
|
+
root.ready = true
|
|
51
|
+
for (var id in root.tabs) if (!root.tabs[id].error) server.write(JSON.stringify({id:id,path:root.tabs[id].path}) + "\n")
|
|
52
|
+
} else if (result.id && root.tabs[result.id]) {
|
|
53
|
+
var next = Object.assign({}, root.tabs)
|
|
54
|
+
next[result.id] = { path: next[result.id].path, url: result.url || "", error: result.error || "" }
|
|
55
|
+
root.tabs = next
|
|
56
|
+
} else if (!result.id && result.error) root.fail(result.error)
|
|
57
|
+
} catch(e) { root.fail("Invalid terminal server response.") }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
onExited: {
|
|
61
|
+
if (root.stopping) {
|
|
62
|
+
root.stopping = false
|
|
63
|
+
root.ready = false
|
|
64
|
+
// A tab opened during shutdown waits for the old listener to exit.
|
|
65
|
+
if (Object.keys(root.tabs).length) Qt.callLater(function() { server.running = true })
|
|
66
|
+
} else root.fail("Terminal server exited.")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import QtWebEngine
|
|
3
|
+
import Quickshell
|
|
4
|
+
import "." as Flea
|
|
5
|
+
import Quickshell.Io
|
|
6
|
+
import qs.Commons
|
|
7
|
+
|
|
8
|
+
// Kept alive while another tab is selected; destroyed only when this tab closes.
|
|
9
|
+
Item {
|
|
10
|
+
id: root
|
|
11
|
+
property string directory: ""
|
|
12
|
+
property string sessionId: ""
|
|
13
|
+
readonly property var session: Flea.TerminalService.tabs[sessionId] || ({})
|
|
14
|
+
readonly property string address: session.url || ""
|
|
15
|
+
readonly property string status: session.error || "Starting terminal…"
|
|
16
|
+
property bool selected: false
|
|
17
|
+
function focusTerminal() {
|
|
18
|
+
if (selected && address.length) Qt.callLater(function() {
|
|
19
|
+
if (root.selected && root.address.length) web.forceActiveFocus()
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
onSelectedChanged: focusTerminal()
|
|
23
|
+
onAddressChanged: focusTerminal()
|
|
24
|
+
visible: selected
|
|
25
|
+
anchors.fill: parent
|
|
26
|
+
Rectangle { anchors.fill: parent; color: Theme.color.background }
|
|
27
|
+
WebEngineView {
|
|
28
|
+
id: web
|
|
29
|
+
anchors.fill: parent
|
|
30
|
+
visible: root.address.length > 0
|
|
31
|
+
url: root.address
|
|
32
|
+
backgroundColor: Theme.color.background
|
|
33
|
+
settings.javascriptEnabled: true
|
|
34
|
+
onLoadingChanged: function(info) { if(info.errorCode) console.warn("Terminal WebEngineView:", info.errorString) }
|
|
35
|
+
}
|
|
36
|
+
Text {
|
|
37
|
+
anchors.centerIn: parent
|
|
38
|
+
width: Math.max(0, parent.width - 32)
|
|
39
|
+
horizontalAlignment: Text.AlignHCenter
|
|
40
|
+
wrapMode: Text.Wrap
|
|
41
|
+
textFormat: Text.PlainText
|
|
42
|
+
color: Theme.color.foreground
|
|
43
|
+
font.family: Theme.font.family
|
|
44
|
+
font.pixelSize: Theme.font.body
|
|
45
|
+
visible: root.address.length === 0
|
|
46
|
+
text: root.status
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import "." as Flea
|
|
3
|
+
|
|
4
|
+
// Dynamic objects preserve each iframe/PTY when tab order or selection changes.
|
|
5
|
+
Item {
|
|
6
|
+
id: root
|
|
7
|
+
property var pane: null
|
|
8
|
+
property var sessions: ({})
|
|
9
|
+
function open(directory) {
|
|
10
|
+
var component = Qt.createComponent("TerminalTab.qml")
|
|
11
|
+
if (component.status !== Component.Ready) {
|
|
12
|
+
pane.message(component.errorString(), true)
|
|
13
|
+
return ""
|
|
14
|
+
}
|
|
15
|
+
var key = Flea.TerminalService.acquire(directory)
|
|
16
|
+
var item = component.createObject(root, { directory: directory, sessionId: key })
|
|
17
|
+
if (!item) { Flea.TerminalService.release(key); pane.message("Could not create the terminal view.", true); return "" }
|
|
18
|
+
item.selected = Qt.binding(function() { return root.pane.terminalId === key })
|
|
19
|
+
sessions[key] = item
|
|
20
|
+
return key
|
|
21
|
+
}
|
|
22
|
+
Component.onDestruction: { for (var key in sessions) Flea.TerminalService.release(key) }
|
|
23
|
+
function close(key) {
|
|
24
|
+
var item = sessions[key]
|
|
25
|
+
if (item) { Flea.TerminalService.release(key); delete sessions[key]; item.destroy() }
|
|
26
|
+
}
|
|
27
|
+
}
|
package/ui/WindowBody.qml
CHANGED
|
@@ -342,6 +342,7 @@ Rectangle {
|
|
|
342
342
|
view.openArchiveCards().askCompressPassword(primaryPane, paths, menuId)
|
|
343
343
|
}
|
|
344
344
|
onExtractPasswordRequested: function (path, dest, err) { view.openArchiveCards().openExtractPassword({ path: path, dest: dest }, primaryPane, err) }
|
|
345
|
+
onExtractHereRequested: function(path, entries) { view.openArchiveCards().askExtractHere(primaryPane, path, entries) }
|
|
345
346
|
onPermissionsRequested: function (path) { permissionsDialog.open(path, primaryPane) }
|
|
346
347
|
// Permissions040: the whole selection's paths for the multi-row card.
|
|
347
348
|
onPermissionsBatchRequested: function (paths) { permissionsDialog.openMany(paths, primaryPane) }
|
|
@@ -405,6 +406,7 @@ Rectangle {
|
|
|
405
406
|
view.openArchiveCards().askCompressPassword(otherPane, paths, menuId)
|
|
406
407
|
}
|
|
407
408
|
onExtractPasswordRequested: function (path, dest, err) { view.openArchiveCards().openExtractPassword({ path: path, dest: dest }, otherPane, err) }
|
|
409
|
+
onExtractHereRequested: function(path, entries) { view.openArchiveCards().askExtractHere(otherPane, path, entries) }
|
|
408
410
|
onPermissionsRequested: function(path) { permissionsDialog.open(path, otherPane) }
|
|
409
411
|
onPermissionsBatchRequested: function (paths) { permissionsDialog.openMany(paths, otherPane) }
|
|
410
412
|
onBulkRenameRequested: function (paths) { view.openBulkRename(paths, otherPane) }
|
package/ui/boot/shell.qml
CHANGED
|
@@ -45,7 +45,16 @@ ShellRoot {
|
|
|
45
45
|
|
|
46
46
|
// This directory cannot reach ui/js/Format.js through qs:, so its one call is written out.
|
|
47
47
|
function fileUrl(path) {
|
|
48
|
-
|
|
48
|
+
path = String(path)
|
|
49
|
+
// Only Windows paths use backslashes as separators; POSIX filenames may contain them.
|
|
50
|
+
var drive = /^[A-Za-z]:[\\/]/.test(path)
|
|
51
|
+
var unc = /^\\\\[^\\]/.test(path)
|
|
52
|
+
if (drive || unc) path = path.replace(/\\/g, "/")
|
|
53
|
+
var parts = path.split("/")
|
|
54
|
+
for (var i = 0; i < parts.length; i++) {
|
|
55
|
+
if (!(drive && i === 0)) parts[i] = encodeURIComponent(parts[i])
|
|
56
|
+
}
|
|
57
|
+
return (drive ? "file:///" : path.indexOf("//") === 0 ? "file:" : "file://") + parts.join("/")
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
// Before the first frame, so the window maps holding its chrome; see AGENTS.md "The first window".
|
package/ui/js/Archive.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
var SEVEN_TAILS = [".appimage", ".cpio", ".cab", ".jar", ".apk", ".rpm", ".deb", ".iso"]
|
|
9
9
|
|
|
10
10
|
// Longest form first, so ".tar.gz" matches before ".gz"; .rar is read here and never offered to write (src/backend/archive.rs).
|
|
11
|
-
var EXTENSIONS = [".tar.zst", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".tar", ".zip", ".7z", ".rar"].concat(SEVEN_TAILS)
|
|
11
|
+
var EXTENSIONS = [".tar.zst", ".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".tzst", ".tar", ".zip", ".7z", ".rar"].concat(SEVEN_TAILS)
|
|
12
12
|
|
|
13
13
|
// A disk image lists in the archive preview and is never extracted.
|
|
14
14
|
var LIST_ONLY = [".iso"]
|
package/ui/js/Drag.js
CHANGED
|
@@ -217,11 +217,7 @@ function isOwnDrag(payload) {
|
|
|
217
217
|
// A path as a file:// URI. Each component is encoded on its own: encodeURIComponent would escape the
|
|
218
218
|
// separators too, and encodeURI would leave a "#" or a "?" in a filename unescaped.
|
|
219
219
|
function uriFor(path) {
|
|
220
|
-
|
|
221
|
-
for (var i = 0; i < parts.length; i++) {
|
|
222
|
-
parts[i] = encodeURIComponent(parts[i])
|
|
223
|
-
}
|
|
224
|
-
return "file://" + parts.join("/")
|
|
220
|
+
return Format.fileUri(path)
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
// What the drag puts on the wire: the marker naming this drag as Flea's own, which the backend
|
package/ui/js/Focus.js
CHANGED
|
@@ -368,6 +368,8 @@ function sequenceAction(action, root) {
|
|
|
368
368
|
|
|
369
369
|
// Lifted whole from Pane.qml's Keys.onPressed, which had grown past its file's 400-line cap; returns whether the key was consumed.
|
|
370
370
|
function handleKey(event, root, sidebar, field) {
|
|
371
|
+
// Embedded terminals own ordinary keys; never turn shell typing into file operations.
|
|
372
|
+
if (root.terminalId && !field && !(sidebar && sidebar.activeFocus)) return false
|
|
371
373
|
if (root.selectionBand) {
|
|
372
374
|
if (event.key === Qt.Key_Escape)
|
|
373
375
|
root.selectionBand.cancel()
|
package/ui/js/Format.js
CHANGED
|
@@ -94,7 +94,16 @@ function isExecutable(mode) {
|
|
|
94
94
|
// Shared by Row.qml's iconSource and PreviewMedia.qml's player source: encodeURI leaves # and ?
|
|
95
95
|
// literal, which Qt then reads as a URL fragment or query rather than path bytes.
|
|
96
96
|
function fileUri(path) {
|
|
97
|
-
|
|
97
|
+
path = String(path)
|
|
98
|
+
// Only Windows paths use backslashes as separators; POSIX filenames may contain them.
|
|
99
|
+
var drive = /^[A-Za-z]:[\\/]/.test(path)
|
|
100
|
+
var unc = /^\\\\[^\\]/.test(path)
|
|
101
|
+
if (drive || unc) path = path.replace(/\\/g, "/")
|
|
102
|
+
var parts = path.split("/")
|
|
103
|
+
for (var i = 0; i < parts.length; i++) {
|
|
104
|
+
if (!(drive && i === 0)) parts[i] = encodeURIComponent(parts[i])
|
|
105
|
+
}
|
|
106
|
+
return (drive ? "file:///" : path.indexOf("//") === 0 ? "file:" : "file://") + parts.join("/")
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
// "3:05", or "1:03:05" once an hour is on the clock; mm/ss are always two digits, matching a media player's own clock rather than Format.date's prose.
|
package/ui/js/Keymap.js
CHANGED
|
@@ -31,8 +31,8 @@ var PRESET_KEYS = [
|
|
|
31
31
|
{"mods":"ctrl","key":"N","keys":"ctrl-n","action":"windowNew","label":"New window","frontend":"gui","context":"listing,rail","preset":"default","code":"Key_N","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_N,"mask":(Qt.ControlModifier)},
|
|
32
32
|
{"mods":"ctrl","key":"N","keys":"ctrl-n","action":"windowNew","label":"New window","frontend":"gui","context":"listing,rail","preset":"vim","code":"Key_N","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_N,"mask":(Qt.ControlModifier)},
|
|
33
33
|
{"mods":"ctrl","key":"N","keys":"ctrl-n","action":"windowNew","label":"New window","frontend":"gui","context":"listing,rail","preset":"windows","code":"Key_N","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_N,"mask":(Qt.ControlModifier)},
|
|
34
|
-
{"mods":"ctrl","key":"T","keys":"ctrl-t","action":"
|
|
35
|
-
{"mods":"ctrl","key":"T","keys":"ctrl-t","action":"
|
|
34
|
+
{"mods":"ctrl","key":"T","keys":"ctrl-t","action":"openTerminal","label":"Open terminal","frontend":"all","context":"listing,rail","preset":"mac","code":"Key_T","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_T,"mask":(Qt.ControlModifier)},
|
|
35
|
+
{"mods":"ctrl","key":"T","keys":"ctrl-t","action":"openTerminal","label":"Open terminal","frontend":"all","context":"listing,rail","preset":"windows","code":"Key_T","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_T,"mask":(Qt.ControlModifier)},
|
|
36
36
|
{"mods":"shift","key":"Delete","keys":"shift-delete","action":"deletePermanently","label":"Delete permanently","frontend":"all","context":"listing","preset":"all","code":"Key_Delete","text":"","ctrl":false,"shift":true,"alt":false,"super":false,"keycode":Qt.Key_Delete,"mask":(Qt.ShiftModifier)},
|
|
37
37
|
{"mods":"alt","key":"P","keys":"alt-p","action":"togglePreview","label":"Preview column","frontend":"all","context":"listing","preset":"all","code":"Key_P","text":"","ctrl":false,"shift":false,"alt":true,"super":false,"keycode":Qt.Key_P,"mask":(Qt.AltModifier)},
|
|
38
38
|
{"mods":"ctrl","key":"Space","keys":"ctrl-space","action":"loadPreview","label":"Preview","frontend":"all","context":"listing","preset":"all","code":"Key_Space","text":"","ctrl":true,"shift":false,"alt":false,"super":false,"keycode":Qt.Key_Space,"mask":(Qt.ControlModifier)},
|
package/ui/js/Messages.js
CHANGED
|
@@ -112,7 +112,7 @@ function route(root, message) {
|
|
|
112
112
|
root.fontLooked(message)
|
|
113
113
|
} else if (message.t === "meta") {
|
|
114
114
|
root.metaResult(message)
|
|
115
|
-
root.meta(message.row, message.w, message.h, message.orient || 1, message.ms, message.rate, message.entries, message.unpacked, message.afailed, message.names, message.lines, message.partial, message.lfailed === true, message.target, message.targetdir, message.owner || "")
|
|
115
|
+
root.meta(message.row, message.w, message.h, message.orient || 1, message.ms, message.rate, message.entries, message.unpacked, message.afailed, message.names, message.lines, message.partial, message.lfailed === true, message.target, message.targetdir, message.owner || "", typeof message.topEntries === "number" ? message.topEntries : -1)
|
|
116
116
|
} else if (message.t === "code") {
|
|
117
117
|
root.codeResult(message)
|
|
118
118
|
} else if (message.t === "html") {
|
package/ui/js/Nav.js
CHANGED
|
@@ -246,7 +246,7 @@ function openCursor(pane, opener) {
|
|
|
246
246
|
opener.open(path)
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
// The run card's
|
|
249
|
+
// The run card's open action uses the same xdg-open route as Enter.
|
|
250
250
|
function runCursor(pane, opener, verb) {
|
|
251
251
|
var row = openableRow(pane)
|
|
252
252
|
if (!row) return
|
package/ui/js/Tabs.js
CHANGED
|
@@ -42,6 +42,7 @@ function snapshot(pane, path, identity, dropped) {
|
|
|
42
42
|
var elsewhere = where !== pane.path || Search.live(pane) || dropped === true
|
|
43
43
|
return {
|
|
44
44
|
tabIdentity: identity || ++nextTabIdentity,
|
|
45
|
+
terminalId: pane.terminalId || "",
|
|
45
46
|
path: where,
|
|
46
47
|
history: pane.history.slice(),
|
|
47
48
|
forwardHistory: (pane.forwardHistory || []).slice(),
|
|
@@ -180,6 +181,7 @@ function restoreSelection(pane, selected, follows) {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
function apply(pane, item, dropped, cursor) {
|
|
184
|
+
pane.terminalId = item.terminalId || ""
|
|
183
185
|
// Issue 93: rows from a search dropped onto its scope, or a listing still out, are not the rows this tab left.
|
|
184
186
|
var same = pane.path === item.path && pane.showHidden === item.showHidden && dropped !== true && cursor === undefined && !pane.listInFlight
|
|
185
187
|
if (pane.tabs) pane.tabs.pendingCursorName = cursor === undefined ? null : cursor
|
|
@@ -305,6 +307,7 @@ function openNew(pane, where) {
|
|
|
305
307
|
var items = currentItems(pane, here)
|
|
306
308
|
var index = currentIndex(pane)
|
|
307
309
|
items[index] = snapshot(pane, here, items[index].tabIdentity, dropped)
|
|
310
|
+
pane.terminalId = ""
|
|
308
311
|
items.push(snapshot(pane, target, undefined, dropped))
|
|
309
312
|
pane.tabs = pack(items, items.length - 1)
|
|
310
313
|
dropCount(pane)
|
|
@@ -315,6 +318,17 @@ function openNew(pane, where) {
|
|
|
315
318
|
|
|
316
319
|
// Ctrl+Return on the cursor row, the keyboard twin of Tap.tappedTab's middle
|
|
317
320
|
// click: that directory in a new tab, leaving the cursor and selection alone.
|
|
321
|
+
function openTerminal(pane, path) {
|
|
322
|
+
if (count(pane) >= MAX) { sayCount(pane, "Nine tabs is the most."); return }
|
|
323
|
+
var id = pane.terminalHost.open(path)
|
|
324
|
+
if (!id) return
|
|
325
|
+
openNew(pane, path)
|
|
326
|
+
pane.terminalId = id
|
|
327
|
+
var items = pane.tabs.items.slice()
|
|
328
|
+
items[pane.tabs.index].terminalId = id
|
|
329
|
+
pane.tabs = pack(items, pane.tabs.index, pane.tabs)
|
|
330
|
+
}
|
|
331
|
+
|
|
318
332
|
function openCursorTab(pane) {
|
|
319
333
|
var row = pane.rowFor ? pane.rowFor(pane.cursorIndex) : null
|
|
320
334
|
if (!row || !row.d || typeof row.n !== "string") {
|
|
@@ -353,6 +367,7 @@ function closeAt(pane, i) {
|
|
|
353
367
|
return
|
|
354
368
|
}
|
|
355
369
|
var index = currentIndex(pane)
|
|
370
|
+
if (items[i].terminalId && pane.terminalHost) pane.terminalHost.close(items[i].terminalId)
|
|
356
371
|
items.splice(i, 1)
|
|
357
372
|
var next = index
|
|
358
373
|
if (i < index)
|
package/ui/js/Tap.js
CHANGED
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
// (ui/js/Focus.js leavesLine): the filter keeps its query standing and gives up its caret, and a Find
|
|
8
8
|
// line hands the keys to the results without running. Otherwise the next Space or letter is typed
|
|
9
9
|
// into the line the pointer has just left.
|
|
10
|
+
function focusListing(root) {
|
|
11
|
+
if (root.renameEditor && root.renameEditor()) return
|
|
12
|
+
root.focusView = "list"
|
|
13
|
+
if (root.listArea) root.listArea.forceActiveFocus()
|
|
14
|
+
}
|
|
10
15
|
function releaseLines(root) {
|
|
11
16
|
if (root.filterTyping) Filter.commit(root)
|
|
12
17
|
if (root.searchMode === Search.TYPING) root.searchMode = Search.RESULTS
|
|
18
|
+
focusListing(root)
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
// Hit the drawn label instead of the whole name slot, including centered grid captions.
|
package/ui/js/Trash.js
CHANGED
|
@@ -70,7 +70,10 @@ function confirmTitle(snapshot) {
|
|
|
70
70
|
|
|
71
71
|
function confirmBody(snapshot, scope) {
|
|
72
72
|
if (!filled(snapshot)) return ""
|
|
73
|
-
|
|
73
|
+
var body = snapshot.all ? Ops.deleteAllLine(snapshot.count, Format.size(snapshot.bytes || 0)) : Ops.deleteScopeLine(scope, snapshot.count)
|
|
74
|
+
if (snapshot.count === 1 && typeof snapshot.name === "string" && snapshot.name.length > 0)
|
|
75
|
+
body = "“" + snapshot.name + "”\n\n" + body
|
|
76
|
+
return body
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
// Where m opens: the cursor row's menu when no row is marked or the only mark is the cursor row, the marked rows' menu otherwise, the whole-Trash menu when nothing is listed.
|
package/ui/qmldir
CHANGED