diff --git a/core/cmd/dms/commands_doctor.go b/core/cmd/dms/commands_doctor.go index 13e96a28..569fff16 100644 --- a/core/cmd/dms/commands_doctor.go +++ b/core/cmd/dms/commands_doctor.go @@ -649,6 +649,76 @@ func checkI2CAvailability() checkResult { return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"} } +func checkKImageFormats() checkResult { + url := doctorDocsURL + "#optional-features" + desc := "Extra image format support (AVIF, HEIF, JXL)" + + pluginDir := findQtPluginDir() + if pluginDir == "" { + return checkResult{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (qtpaths not found)", desc, url} + } + + imageFormatsDir := filepath.Join(pluginDir, "imageformats") + keyPlugins := []struct{ file, format string }{ + {"kimg_avif.so", "AVIF"}, + {"kimg_heif.so", "HEIF"}, + {"kimg_jxl.so", "JXL"}, + {"kimg_exr.so", "EXR"}, + } + + var found []string + for _, p := range keyPlugins { + if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil { + found = append(found, p.format) + } + } + + if len(found) == 0 { + return checkResult{catOptionalFeatures, "kimageformats", statusWarn, "Not installed", desc, url} + } + + details := "" + if doctorVerbose { + details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir) + } + + return checkResult{catOptionalFeatures, "kimageformats", statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url} +} + +func findQtPluginDir() string { + // Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups) + if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" { + for dir := range strings.SplitSeq(envPath, ":") { + if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil { + return dir + } + } + } + + // Try qtpaths + for _, cmd := range []string{"qtpaths6", "qtpaths"} { + if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil { + if dir := strings.TrimSpace(string(output)); dir != "" { + return dir + } + } + } + + // Fallback: common distro paths + for _, dir := range []string{ + "/usr/lib/qt6/plugins", + "/usr/lib64/qt6/plugins", + "/usr/lib/x86_64-linux-gnu/qt6/plugins", + "/usr/lib/aarch64-linux-gnu/qt6/plugins", + } { + if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil { + return dir + } + } + + return "" +} + func detectNetworkBackend(stackResult *network.DetectResult) string { switch stackResult.Backend { case network.BackendNetworkManager: @@ -703,6 +773,7 @@ func checkOptionalDependencies() []checkResult { results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL}) results = append(results, checkI2CAvailability()) + results = append(results, checkKImageFormats()) terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index 4cdcef7c..0b488025 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -64,7 +64,7 @@ Rectangle { if (!path) return false; var ext = path.split('.').pop().toLowerCase(); - return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0; + return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0; } DankRipple { diff --git a/quickshell/Modals/FileBrowser/FileBrowserContent.qml b/quickshell/Modals/FileBrowser/FileBrowserContent.qml index a396546e..6d31b210 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserContent.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserContent.qml @@ -280,6 +280,7 @@ FocusScope { showDirsFirst: true showDotAndDotDot: false showHidden: root.showHiddenFiles + caseSensitive: false nameFilters: fileExtensions showFiles: true showDirs: true diff --git a/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml b/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml index 03c76dfd..86fb9984 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml @@ -31,7 +31,7 @@ StyledRect { function determineFileType(fileName) { const ext = getFileExtension(fileName); - const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]; + const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"]; if (imageExts.includes(ext)) { return "image"; } @@ -119,7 +119,7 @@ StyledRect { id: gridPreviewImage anchors.fill: parent anchors.margins: 2 - property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"] + property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"] property int weExtIndex: 0 property string imagePath: { if (weMode && delegateRoot.fileIsDir) diff --git a/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml b/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml index bf2c0a68..ebc22046 100644 --- a/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml +++ b/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml @@ -30,7 +30,7 @@ StyledRect { function determineFileType(fileName) { const ext = getFileExtension(fileName); - const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]; + const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"]; if (imageExts.includes(ext)) { return "image"; } diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index 2ef74f21..9ce91530 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -128,7 +128,7 @@ FloatingWindow { browserIcon: "person" browserType: "profile" showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] onFileSelected: path => { PortalService.setProfileImage(path); close(); @@ -152,7 +152,7 @@ FloatingWindow { browserIcon: "wallpaper" browserType: "wallpaper" showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] onFileSelected: path => { SessionData.setWallpaper(path); close(); diff --git a/quickshell/Modules/DankDash/WallpaperTab.qml b/quickshell/Modules/DankDash/WallpaperTab.qml index 7eca04e8..9cc859bb 100644 --- a/quickshell/Modules/DankDash/WallpaperTab.qml +++ b/quickshell/Modules/DankDash/WallpaperTab.qml @@ -306,7 +306,8 @@ Item { showDirsFirst: false showDotAndDotDot: false showHidden: false - nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + caseSensitive: false + nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] showFiles: true showDirs: false sortField: FolderListModel.Name @@ -320,7 +321,7 @@ Item { browserIcon: "folder_open" browserType: "wallpaper" showHiddenFiles: false - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] parentPopout: root.parentPopout onFileSelected: path => { diff --git a/quickshell/Modules/Settings/WallpaperTab.qml b/quickshell/Modules/Settings/WallpaperTab.qml index 541d3b79..6315514b 100644 --- a/quickshell/Modules/Settings/WallpaperTab.qml +++ b/quickshell/Modules/Settings/WallpaperTab.qml @@ -1302,7 +1302,7 @@ Item { browserIcon: "wallpaper" browserType: "wallpaper" showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] onFileSelected: path => { if (SessionData.perMonitorWallpaper) { SessionData.setMonitorWallpaper(selectedMonitorName, path); @@ -1324,7 +1324,7 @@ Item { browserIcon: "light_mode" browserType: "wallpaper" showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] onFileSelected: path => { SessionData.wallpaperPathLight = path; SessionData.syncWallpaperForCurrentMode(); @@ -1344,7 +1344,7 @@ Item { browserIcon: "dark_mode" browserType: "wallpaper" showHiddenFiles: true - fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] + fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"] onFileSelected: path => { SessionData.wallpaperPathDark = path; SessionData.syncWallpaperForCurrentMode(); diff --git a/quickshell/Services/WallpaperCyclingService.qml b/quickshell/Services/WallpaperCyclingService.qml index 60af5cc4..ac1ce663 100644 --- a/quickshell/Services/WallpaperCyclingService.qml +++ b/quickshell/Services/WallpaperCyclingService.qml @@ -264,7 +264,7 @@ Singleton { } if (process) { - process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]; + process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \) 2>/dev/null | sort`]; process.targetScreenName = screenName; process.currentWallpaper = currentWallpaper; process.goToPrevious = false; @@ -272,7 +272,7 @@ Singleton { } } else { // Use global process for fallback - cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]; + cyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \) 2>/dev/null | sort`]; cyclingProcess.targetScreenName = screenName || ""; cyclingProcess.currentWallpaper = currentWallpaper; cyclingProcess.running = true; @@ -296,7 +296,7 @@ Singleton { } if (process) { - process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]; + process.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \) 2>/dev/null | sort`]; process.targetScreenName = screenName; process.currentWallpaper = currentWallpaper; process.goToPrevious = true; @@ -304,7 +304,7 @@ Singleton { } } else { // Use global process for fallback - prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]; + prevCyclingProcess.command = ["sh", "-c", `find "${wallpaperDir}" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \) 2>/dev/null | sort`]; prevCyclingProcess.targetScreenName = screenName || ""; prevCyclingProcess.currentWallpaper = currentWallpaper; prevCyclingProcess.running = true;