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.
Files changed (49) hide show
  1. package/README.md +35 -2
  2. package/package.json +2 -2
  3. package/src/archive.js +5 -0
  4. package/src/archiveops.js +36 -5
  5. package/src/menu.js +2 -2
  6. package/src/meta.js +9 -3
  7. package/src/terminal-server.js +95 -0
  8. package/src/terminal-shell.js +26 -0
  9. package/src/trashbrowse.js +1 -0
  10. package/test/delete-trash.test.js +2 -2
  11. package/test/file-ops.test.js +66 -0
  12. package/test/file-url.test.js +24 -0
  13. package/test/manual-archive-here-ui.mjs +47 -0
  14. package/test/manual-folder-focus.mjs +63 -0
  15. package/test/manual-terminal-lifecycle.mjs +42 -0
  16. package/test/manual-terminal-navigation.mjs +48 -0
  17. package/test/manual-terminal-ui.mjs +47 -0
  18. package/test/trashbrowse.test.js +1 -1
  19. package/ui/ArchiveCards.qml +28 -1
  20. package/ui/ArchiveHereConfirm.qml +127 -0
  21. package/ui/Backend.qml +5 -1
  22. package/ui/ColumnPane.qml +2 -0
  23. package/ui/Opener.qml +16 -36
  24. package/ui/Pane.qml +25 -1
  25. package/ui/PaneRail.qml +32 -3
  26. package/ui/PaneWire.qml +2 -6
  27. package/ui/Preview.qml +3 -2
  28. package/ui/PreviewArchive.qml +26 -1
  29. package/ui/PreviewColumn.qml +2 -0
  30. package/ui/QuickLookArchive.qml +3 -0
  31. package/ui/SelectionPreview.qml +1 -1
  32. package/ui/Sidebar.qml +5 -9
  33. package/ui/TabBar.qml +3 -3
  34. package/ui/TerminalService.qml +69 -0
  35. package/ui/TerminalTab.qml +48 -0
  36. package/ui/TerminalTabs.qml +27 -0
  37. package/ui/WindowBody.qml +2 -0
  38. package/ui/boot/shell.qml +10 -1
  39. package/ui/js/Archive.js +1 -1
  40. package/ui/js/Drag.js +1 -5
  41. package/ui/js/Focus.js +2 -0
  42. package/ui/js/Format.js +10 -1
  43. package/ui/js/Keymap.js +2 -2
  44. package/ui/js/Messages.js +1 -1
  45. package/ui/js/Nav.js +1 -1
  46. package/ui/js/Tabs.js +15 -0
  47. package/ui/js/Tap.js +6 -0
  48. package/ui/js/Trash.js +4 -1
  49. 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
- return "file://" + encodeURI(path).replace(/#/g, "%23").replace(/\?/g, "%3F")
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
- var parts = path.split("/")
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
- return "file://" + encodeURI(path).replace(/#/g, "%23").replace(/\?/g, "%3F")
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":"tabNew","label":"New tab","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":"tabNew","label":"New tab","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)},
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 entrances always reach the opener, so Run asks through flea --open like Enter does.
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
- return snapshot.all ? Ops.deleteAllLine(snapshot.count, Format.size(snapshot.bytes || 0)) : Ops.deleteScopeLine(scope, snapshot.count)
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
@@ -226,3 +226,5 @@ PickerLifecycle 1.0 PickerLifecycle.qml
226
226
  ArchiveCompressDialog 1.0 ArchiveCompressDialog.qml
227
227
  ArchivePasswordDialog 1.0 ArchivePasswordDialog.qml
228
228
  ArchiveCards 1.0 ArchiveCards.qml
229
+
230
+ singleton TerminalService 1.0 TerminalService.qml