Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f181772
Type a regular file from its name
thisisgm Nov 11, 2026
96e5d10
Keep one copy of a listing
thisisgm Nov 11, 2026
ef1b871
Count a folder's items without sorting them
thisisgm Nov 11, 2026
c336463
Hold less for each entry of a listing
thisisgm Nov 11, 2026
e37c4e3
Read a file's type when a view asks for it
thisisgm Nov 11, 2026
82daa69
Rebuild a sidebar section only when its list changed
thisisgm Nov 11, 2026
d488df8
Ask Hyprland for its rounding when it changes
thisisgm Nov 11, 2026
764bc6c
Sort a large folder faster, in the same order
thisisgm Nov 11, 2026
1f7cbff
Read sizes and dates for the rows a view shows
thisisgm Nov 11, 2026
f45515f
Count a folder again when its date moved
thisisgm Nov 11, 2026
3905ff9
Watch Hyprland again after its events stop
thisisgm Nov 11, 2026
5e44c36
Ask for a text file's page once its date is read
thisisgm Nov 11, 2026
b3c92e2
Count a folder no view asked for again when it changed
thisisgm Nov 12, 2026
b0dee61
Read Hyprland's events a piece at a time
thisisgm Nov 12, 2026
e6e7ae8
Watch Hyprland again after a hang-up while in use
thisisgm Nov 12, 2026
777a05a
Keep trying Hyprland while in use until it answers
thisisgm Nov 12, 2026
a28e52e
Let a row be asked again after a failed re-read
thisisgm Nov 12, 2026
e1e9b3f
Keep a row's type once it is read from its name
thisisgm Nov 12, 2026
c933b27
Search without a stat for every entry
thisisgm Nov 11, 2026
263ca29
Keep the walked names while a search is open
thisisgm Nov 11, 2026
71eebe0
List the best matches first, a window at a time
thisisgm Nov 11, 2026
8e49454
Match a search along the path, not only the name
thisisgm Nov 11, 2026
2b42c37
Forgive one typo when no name spells the query
thisisgm Nov 11, 2026
5016dfa
Show the first matches while a big folder is still read
thisisgm Nov 11, 2026
4a7c2f5
Select every match, and keep a search's selection
thisisgm Nov 11, 2026
085c536
Keep a search answering while it is read again
thisisgm Nov 11, 2026
bcffc86
Read a search's folders again behind a later key
thisisgm Nov 12, 2026
fb97e06
Keep room for the query beside a search's status
thisisgm Nov 12, 2026
ad36628
Find a path whose folded letters are longer
thisisgm Nov 12, 2026
bdfcd55
Name the search index's numbers, a line a comment
thisisgm Nov 13, 2026
f7310fa
Follow no link on the way to a folder searched
thisisgm Nov 13, 2026
1f9db2f
Start a word at a capital beside letters past ASCII
thisisgm Nov 13, 2026
f7f7b22
Read at most two million names for a search
thisisgm Nov 13, 2026
354eebf
Pin what the search tests wait for
thisisgm Nov 13, 2026
62c64f3
Ask the search at every key, with no pause to wait out
thisisgm Nov 13, 2026
2c07f34
Keep the cursor and selection steady through a search
thisisgm Nov 13, 2026
46a1b52
Keep a folder's count in sight while it is counted again
thisisgm Nov 13, 2026
049ce67
Keep a selection made again after a search key
thisisgm Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/FolderPane.qml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ FocusScope {
// the folder. By path: Recents can list two files of the same name.
property string lastPath: ""
property string loadedPath: ""
// The query the rows last answered, the cursor's item when a search began (its end puts the cursor back there), and the best match an answer put the cursor on.
property string loadedQuery: ""
property string searchedFrom: ""
property string answerTop: ""
// Set when a click on empty space lets go of the cursor: a re-read or a
// switch of view leaves it let go of, until it moves again or another
// folder arrives.
Expand Down Expand Up @@ -280,19 +284,35 @@ FocusScope {
const pending = backend.takeSelection()
const same = backend.folder.path === pane.loadedPath
pane.loadedPath = backend.folder.path
// An answer to another query is another list: the cursor goes to its best match, and when the search ends, back to the item it left.
const query = backend.folder.query
const asked = same && query !== pane.loadedQuery
if (asked && pane.loadedQuery === "")
pane.searchedFrom = pane.lastPath
if (asked && query === "")
pane.lastPath = pane.searchedFrom
pane.loadedQuery = query
const answer = asked && query !== ""
const row = backend.folder.indexOfPath(pane.lastPath)
// Nothing is selected but the cursor's item, or that item is gone and nothing is selected.
const own = backend.folder.selectedCount === 1 ? backend.folder.selectedPaths()[0] === pane.lastPath : backend.folder.selectedCount === 0 && row < 0
// The cursor an answer put on its best match stays on the best match as the search reads on, until it's moved to another item.
const riding = same && !asked && query !== "" && !pane.cursorLetGo && pane.lastPath === pane.answerTop && own
if (pending || !same)
pane.cursorLetGo = false
if (pending)
view.item.select(backend.folder.indexOf(pending), false)
else if (!same)
view.item.select(-1, false)
else if (answer || riding)
view.item.select(-1, answer)
else if (!pane.cursorLetGo)
view.item.select(row >= 0 ? row : Math.min(view.item.currentIndex, backend.folder.count - 1), true)
// Remembered here too: put on the row it was already on, the
// cursor doesn't signal a move, and an item from the folder
// before would stay remembered.
pane.lastPath = backend.folder.get(view.item.currentIndex).path || ""
pane.answerTop = answer || riding ? pane.lastPath : pane.answerTop
}
}

Expand Down
26 changes: 24 additions & 2 deletions src/Format.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
// refreshed with the rest of the strings.

// Each unit is whole with its number, so a language can place and space it
// its own way ("1,5 Mo").
// its own way ("1,5 Mo"). Nothing for a size not read yet.
function size(bytes) {
if (bytes === undefined)
return ""
const units = [qsTr("%1 B"), qsTr("%1 KB"), qsTr("%1 MB"), qsTr("%1 GB"), qsTr("%1 TB")]
let i = 0
let value = bytes
Expand Down Expand Up @@ -38,6 +40,20 @@ function count(n) {
return n === 0 ? qsTr("No items") : n > 0 ? qsTr("%Ln items", "", n) : ""
}

// How many match a search of `here` and below; `best` is how many of them are rows when another order cut the list, or -1; `most` is how many names were read of a tree that holds more, or -1.
function matches(n, here, reading, best, most) {
if (reading)
return qsTr("%Ln matches so far, still reading %1 and below", "", n).arg(here)
// The counts go in first: a folder's name holding a marker is then never filled in again.
if (most >= 0 && best >= 0)
return qsTr("%Ln matches in %3 and below, the best %1 shown, only the nearest %2 items read", "", n).arg(best.toLocaleString(Qt.locale(), "f", 0)).arg(most.toLocaleString(Qt.locale(), "f", 0)).arg(here)
if (most >= 0)
return qsTr("%Ln matches in %2 and below, only the nearest %1 items read", "", n).arg(most.toLocaleString(Qt.locale(), "f", 0)).arg(here)
if (best >= 0)
return qsTr("%Ln matches in %2 and below, the best %1 shown", "", n).arg(best.toLocaleString(Qt.locale(), "f", 0)).arg(here)
return qsTr("%Ln matches in %1 and below", "", n).arg(here)
}

// What an item is and how big, under its name in the details and the
// preview; for a plugin's item, its subtitle.
function summary(item, type) {
Expand All @@ -47,19 +63,25 @@ function summary(item, type) {
return item.extension
if (item.isDir)
return item.itemCount === 0 ? qsTr("Folder · No items") : item.itemCount > 0 ? qsTr("Folder · %Ln items", "", item.itemCount) : qsTr("Folder")
return qsTr("%1 · %2").arg(type || item.mime).arg(size(item.size))
return item.size === undefined ? (type || item.mime) : qsTr("%1 · %2").arg(type || item.mime).arg(size(item.size))
}

// The file's modified time is part of the URL: Qt keeps the images it has
// shown by their URL, so a picture saved over would otherwise come back
// with its old pixels.
function thumb(path, mime, px, modified) {
// A time not read yet (undefined) gives no URL, or the picture would be asked for twice; null is a caller with no time.
if (modified === undefined)
return ""
const changed = modified ? modified.getTime() : 0
return "image://thumb/" + px + "/" + changed + "/" + encodeURIComponent(mime) + "/" + encodeURIComponent(path)
}

// A text file's first lines on a page in the theme's colors.
function page(path, px, modified, paper, ink) {
// As for a thumbnail: a time not read yet gives no URL, or the page would be read and drawn twice.
if (modified === undefined)
return ""
const changed = modified ? modified.getTime() : 0
const hex = (color) => String(color).slice(1)
return "image://thumb/page/" + px + "/" + changed + "/" + hex(paper) + "/" + hex(ink) + "/" + encodeURIComponent(path)
Expand Down
36 changes: 22 additions & 14 deletions src/PathBar.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import "Format.js" as Format

// The breadcrumb path from the left edge; view buttons on the right. Back and
// forward are keys (Alt+← / Alt+→) and the mouse's thumb buttons.
Expand Down Expand Up @@ -41,16 +42,10 @@ Rectangle {
// S and Shift+S, through the sort menu's orders.
function stepSort(by) { sortMenu.step(by) }
function endSearch() {
searchDelay.stop()
searchField.text = ""
backend.folder.query = ""
editDone()
}
Timer {
id: searchDelay
interval: 150
onTriggered: backend.folder.query = searchField.text.trim()
}
// Going to another folder ends the search there.
Connections {
target: backend.folder
Expand Down Expand Up @@ -93,11 +88,13 @@ Rectangle {
visible: bar.searching && !bar.editing
font.pixelSize: 14
// Room at the end for where it searches; padding isn't mirrored.
leftPadding: LayoutMirroring.enabled ? scope.implicitWidth + 24 : 12
rightPadding: LayoutMirroring.enabled ? 12 : scope.implicitWidth + 24
// Where it searches, at the end of the field. Unlike a
// placeholder, it stays while typing. Plugins' views have
// nothing below them.
leftPadding: LayoutMirroring.enabled ? scope.width + 24 : 12
rightPadding: LayoutMirroring.enabled ? 12 : scope.width + 24
// What the query keeps of a narrow field, some twenty letters: the status is cut short before it takes that.
readonly property int queryRoom: 160
// The padding at the query's end of the field, and the status's margin and its gap from the query at the other.
readonly property int besideStatus: 12 + 24
// Where it searches and, once answered, how many match: unlike a placeholder it stays while typing.
Text {
id: scope
// Recents, Trash and plugins' views are named in C++, which
Expand All @@ -109,14 +106,25 @@ Rectangle {
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: backend.path.startsWith("plugin:") ? qsTr("Search %1").arg(here) : qsTr("Search %1 and below").arg(here)
// What the field has left beside the query's room; in a field with room for both, the whole status as before.
width: Math.max(0, Math.min(implicitWidth, searchField.width - searchField.besideStatus - searchField.queryRoom))
elide: Text.ElideRight
// With no room at all it isn't drawn: cut to nothing, it could still leave its ellipsis beside the query.
visible: width > 0
text: backend.path.startsWith("plugin:") ? qsTr("Search %1").arg(here)
: backend.folder.query.length > 0 && backend.folder.matches >= 0
// Sorted another way than best first, the rows are the best of the matches, and it says so.
? Format.matches(backend.folder.matches, here, backend.folder.reading,
backend.folder.sortMode !== "name" && backend.folder.matches > backend.folder.count ? backend.folder.count : -1,
backend.folder.mostRead)
: qsTr("Search %1 and below").arg(here)
color: theme.secondaryForeground
font.pixelSize: 12
}
onTextEdited: searchDelay.restart()
// Each key asks at once: the model answers from the names it holds, and keys typed while it reads them are taken together when the read ends.
onTextEdited: backend.folder.query = text.trim()
// Enter or ↓ goes to the results, leaving the search up.
function toResults() {
searchDelay.stop()
backend.folder.query = text.trim()
if (backend.folder.query === "")
bar.endSearch()
Expand Down
2 changes: 1 addition & 1 deletion src/PreviewWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ ApplicationWindow {
height: 165
fillMode: Image.PreserveAspectCrop
asynchronous: true
source: Format.thumb(modelData, "image/*", 256)
source: Format.thumb(modelData, "image/*", 256, null)
}
}
}
Expand Down
Loading