diff --git a/core/go.mod b/core/go.mod index 0db292627..617779bbd 100644 --- a/core/go.mod +++ b/core/go.mod @@ -83,7 +83,7 @@ require ( ) require ( - github.com/AvengeMedia/dankgo v1.6.3-0.20260925183658-cbde42a10318 + github.com/AvengeMedia/dankgo v1.6.3-0.20260925221540-1ea246498904 github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect diff --git a/core/go.sum b/core/go.sum index 5bbff3777..588826171 100644 --- a/core/go.sum +++ b/core/go.sum @@ -1,7 +1,7 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/AvengeMedia/dankgo v1.6.3-0.20260925183658-cbde42a10318 h1:tVRgu+ENpVIYCzHe9PHJ3BcMoBDW8OYTqtgsjBwuT6o= -github.com/AvengeMedia/dankgo v1.6.3-0.20260925183658-cbde42a10318/go.mod h1:Fj+FTCumlv8YjKmdv75DREJuGmqU9EnGpBroZuCWc/k= +github.com/AvengeMedia/dankgo v1.6.3-0.20260925221540-1ea246498904 h1:s773vsx6JeQjPphXKq3AhMZgiWMDfhjuS5Bw3KvBJ3c= +github.com/AvengeMedia/dankgo v1.6.3-0.20260925221540-1ea246498904/go.mod h1:Fj+FTCumlv8YjKmdv75DREJuGmqU9EnGpBroZuCWc/k= github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9 h1:ssoiAng7ng500BgtZa1riJH6kKudQVM8Uk9a9kF7mDA= github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9/go.mod h1:8TFIP72NLxCbogF2hHU6sXj3RoTbHQW8beeCrisHs/Y= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/flake.nix b/flake.nix index b480009bd..af74b1e19 100644 --- a/flake.nix +++ b/flake.nix @@ -111,7 +111,7 @@ inherit version; pname = "dms-shell"; src = ./core; - vendorHash = "sha256-Yl/5wAQGWwk7BW3vkEWHJtZU8xmIBGqFu81i8/7cquc="; + vendorHash = "sha256-EPkg9jw2GcdrFTJeOv45P3+j85ytfpKJ08Q5nqX6ZKE="; subPackages = [ "cmd/dms" ]; diff --git a/quickshell/Modules/Notepad/Notepad.qml b/quickshell/Modules/Notepad/Notepad.qml index 6278de371..3dc7babbe 100644 --- a/quickshell/Modules/Notepad/Notepad.qml +++ b/quickshell/Modules/Notepad/Notepad.qml @@ -8,6 +8,7 @@ import qs.Modals.Common import qs.Modals.FileBrowser import qs.Services import qs.Widgets +import "../../Common/Format.js" as Format Item { id: root @@ -244,14 +245,43 @@ Item { } } + function confirmLargeFile(path, size, onOpen, onCancel) { + largeFileConfirmLoader.active = true; + const confirm = largeFileConfirmLoader.item; + if (!confirm) + return; + root.confirmationDialogOpen = true; + confirm.showWithOptions({ + "title": I18n.tr("Open large file?", "notepad prompt before loading a file over the size limit"), + "message": I18n.tr("%1 is %2 and may use a lot of memory.", "notepad large file prompt, file name then size").arg(path.split('/').pop()).arg(Format.formatBytes(size)), + "confirmText": I18n.tr("Open"), + "onConfirm": () => { + root.confirmationDialogOpen = false; + NotepadStorageService.approveLargeFile(path); + onOpen(); + }, + "onCancel": () => { + root.confirmationDialogOpen = false; + onCancel(); + } + }); + } + function performLoadFromFile(fileUrl) { const filePath = fileUrl.toString().replace(/^file:\/\//, ''); const fileName = filePath.split('/').pop(); - loadFileView.path = ""; - loadFileView.path = filePath; + NotepadStorageService.readFileSize(filePath, size => { + if (NotepadStorageService.needsLargeFileConfirm(filePath, size)) { + root.confirmLargeFile(filePath, size, () => root.performLoadFromFile(fileUrl), () => {}); + return; + } - if (loadFileView.waitForJob()) { + loadFileView.path = ""; + loadFileView.path = filePath; + + if (!loadFileView.waitForJob()) + return; Qt.callLater(() => { var content = loadFileView.text(); if (currentTab && content !== undefined && content !== null) { @@ -274,7 +304,7 @@ Item { NotepadStorageService.clearConflict(); } }); - } + }); } Item { @@ -500,6 +530,14 @@ Item { root.showConflictBanner(diskContent); } + onLargeFileConfirmRequested: (tab, size) => { + if (!root.surfaceVisible) + return; + root.confirmLargeFile(tab.filePath, size, () => textEditor.loadCurrentTabContent(), () => { + root.performCloseTab(NotepadStorageService.tabs.findIndex(t => t.id === tab.id)); + }); + } + onAutoSaveRequested: root.autoSaveExternal() } } @@ -643,6 +681,15 @@ Item { } } + LazyLoader { + id: largeFileConfirmLoader + active: false + + ConfirmModal { + useOverlayLayer: true + } + } + LazyLoader { id: confirmationDialogLoader active: false diff --git a/quickshell/Modules/Notepad/NotepadTextEditor.qml b/quickshell/Modules/Notepad/NotepadTextEditor.qml index f78e0e842..af9db1b0a 100644 --- a/quickshell/Modules/Notepad/NotepadTextEditor.qml +++ b/quickshell/Modules/Notepad/NotepadTextEditor.qml @@ -60,6 +60,7 @@ Column { signal popoutRequested signal dockRequested signal conflictDetected(string diskContent) + signal largeFileConfirmRequested(var tab, real size) signal autoSaveRequested function hasUnsavedChanges() { @@ -110,6 +111,14 @@ Column { loadedTabId = requestedTabId; contentLoaded = true; syncContentToPlugin(); + }, size => { + if (requestId !== loadRequestId || !currentTab || currentTab.id !== requestedTabId) + return; + applyingShared = true; + lastSavedContent = ""; + textArea.text = ""; + applyingShared = false; + root.largeFileConfirmRequested(currentTab, size); }); } @@ -1071,7 +1080,7 @@ Column { FileView { id: externalWatch - path: (!root.externalWatchPaused && currentTab && !currentTab.isTemporary && currentTab.filePath) ? currentTab.filePath : "" + path: (!root.externalWatchPaused && currentTab && !currentTab.isTemporary && currentTab.filePath && root.loadedTabId === currentTab.id) ? currentTab.filePath : "" blockLoading: true preload: true watchChanges: true diff --git a/quickshell/Services/NotepadStorageService.qml b/quickshell/Services/NotepadStorageService.qml index 93d4558f9..9b1046170 100644 --- a/quickshell/Services/NotepadStorageService.qml +++ b/quickshell/Services/NotepadStorageService.qml @@ -13,6 +13,8 @@ Singleton { readonly property var log: Log.scoped("NotepadStorageService") property int refCount: 0 + readonly property int largeFileBytes: 5 * 1024 * 1024 + property var approvedLargeFiles: ({}) readonly property string baseDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell") readonly property string filesDir: baseDir + "/notepad-files" @@ -160,7 +162,7 @@ Singleton { return null; } - function loadTabContent(tabIndex, callback) { + function loadTabContent(tabIndex, callback, onLargeFile) { if (tabIndex < 0 || tabIndex >= tabs.length) { callback(""); return; @@ -172,12 +174,12 @@ Singleton { if (tabsBeingCreated[tab.id]) { Qt.callLater(() => { - loadTabContent(tabIndex, callback); + loadTabContent(tabIndex, callback, onLargeFile); }); return; } - Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => { + readFileSize(fullPath, size => { var currentTab = root.getTabById(requestTabId); var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : ""; @@ -186,18 +188,38 @@ Singleton { return; } - if (exitCode === 0) { - tabFileLoaderComponent.createObject(root, { - path: fullPath, - callback: callback - }); - } else { + if (size < 0) { log.warn("Tab file does not exist:", fullPath); callback(""); + return; } + + if (!currentTab.isTemporary && root.needsLargeFileConfirm(fullPath, size)) { + onLargeFile(size); + return; + } + + tabFileLoaderComponent.createObject(root, { + path: fullPath, + callback: callback + }); }); } + function readFileSize(path, callback) { + Proc.runCommand("", ["sh", "-c", "test -f \"$1\" && wc -c < \"$1\"", "sh", path], (output, exitCode) => { + callback(exitCode === 0 ? parseInt(output) : -1); + }); + } + + function needsLargeFileConfirm(path, size) { + return size > largeFileBytes && !approvedLargeFiles[path]; + } + + function approveLargeFile(path) { + approvedLargeFiles[path] = true; + } + function saveTabContent(tabIndex, content) { if (tabIndex < 0 || tabIndex >= tabs.length) return;