1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

core: add DL helper, apply to TrackArt OSD, DankLocationSearch

- unrelated change to add gsettingsOrDconf helpers
This commit is contained in:
bbedward
2026-02-10 15:42:40 -05:00
parent 2c360dc3e8
commit c783ff3dcf
12 changed files with 319 additions and 139 deletions

View File

@@ -524,5 +524,6 @@ func getCommonCommands() []*cobra.Command {
chromaCmd,
doctorCmd,
configCmd,
dlCmd,
}
}

View File

@@ -0,0 +1,99 @@
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var dlOutput string
var dlUserAgent string
var dlTimeout int
var dlIPv4Only bool
var dlCmd = &cobra.Command{
Use: "dl <url>",
Short: "Download a URL to stdout or file",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runDownload(args[0]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}
func init() {
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
}
func runDownload(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("invalid request: %w", err)
}
switch {
case dlUserAgent != "":
req.Header.Set("User-Agent", dlUserAgent)
default:
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
}
dialer := &net.Dialer{Timeout: 5 * time.Second}
transport := &http.Transport{DialContext: dialer.DialContext}
if dlIPv4Only {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp4", addr)
}
}
client := &http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("download failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
if dlOutput == "" {
_, err = io.Copy(os.Stdout, resp.Body)
return err
}
if dir := filepath.Dir(dlOutput); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("mkdir failed: %w", err)
}
}
f, err := os.Create(dlOutput)
if err != nil {
return fmt.Errorf("create failed: %w", err)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
os.Remove(dlOutput)
return fmt.Errorf("write failed: %w", err)
}
fmt.Println(dlOutput)
return nil
}

View File

@@ -652,16 +652,20 @@ func isDMSGTKActive(configDir string) bool {
}
func refreshGTK(mode ColorMode) {
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err)
}
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
log.Warnf("Failed to set gtk-theme: %v", err)
}
}
func refreshGTK4() {
output, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return
}
current := strings.Trim(strings.TrimSpace(string(output)), "'")
current := strings.Trim(output, "'")
var toggle string
if current == "prefer-dark" {
@@ -670,17 +674,22 @@ func refreshGTK4() {
toggle = "prefer-dark"
}
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", toggle).Run(); err != nil {
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
return
}
time.Sleep(50 * time.Millisecond)
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", current).Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
}
}
func refreshQt6ct() {
confPath := filepath.Join(utils.XDGConfigHome(), "qt6ct", "qt6ct.conf")
now := time.Now()
_ = os.Chtimes(confPath, now, now)
if err := os.Chtimes(confPath, now, now); err != nil {
log.Warnf("Failed to touch qt6ct.conf: %v", err)
}
}
func signalTerminals() {
@@ -716,8 +725,8 @@ func syncColorScheme(mode ColorMode) {
scheme = "default"
}
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil {
log.Warnf("Failed to sync color-scheme: %v", err)
}
}
@@ -767,7 +776,9 @@ func closestAdwaitaAccent(primaryHex string) string {
func syncAccentColor(primaryHex string) {
accent := closestAdwaitaAccent(primaryHex)
log.Infof("Setting GNOME accent color: %s", accent)
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "accent-color", accent).Run()
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
log.Warnf("Failed to set accent-color: %v", err)
}
}
type TemplateCheck struct {

View File

@@ -3,11 +3,11 @@ package screenshot
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type ThemeColors struct {
@@ -83,12 +83,11 @@ func getColorsFilePath() string {
}
func isLightMode() bool {
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
scheme, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
if err != nil {
return false
}
scheme := strings.TrimSpace(string(out))
switch scheme {
case "'prefer-light'", "'default'":
return true

View File

@@ -1,11 +1,9 @@
package freedesktop
import (
"context"
"fmt"
"os/exec"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/godbus/dbus/v5"
)
@@ -107,22 +105,8 @@ func (m *Manager) GetUserIconFile(username string) (string, error) {
}
func (m *Manager) SetIconTheme(iconTheme string) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
check := exec.CommandContext(ctx, "gsettings", "writable", "org.gnome.desktop.interface", "icon-theme")
if err := check.Run(); err == nil {
cmd := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.desktop.interface", "icon-theme", iconTheme)
if err := cmd.Run(); err != nil {
return fmt.Errorf("gsettings set failed: %w", err)
}
return nil
if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil {
return fmt.Errorf("failed to set icon theme: %w", err)
}
checkDconf := exec.CommandContext(ctx, "dconf", "write", "/org/gnome/desktop/interface/icon-theme", fmt.Sprintf("'%s'", iconTheme))
if err := checkDconf.Run(); err != nil {
return fmt.Errorf("both gsettings and dconf unavailable or failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,31 @@
package utils
import (
"fmt"
"os/exec"
"strings"
)
func dconfPath(schema, key string) string {
return "/" + strings.ReplaceAll(schema, ".", "/") + "/" + key
}
// GsettingsGet reads a gsettings value, falling back to dconf read.
func GsettingsGet(schema, key string) (string, error) {
if out, err := exec.Command("gsettings", "get", schema, key).Output(); err == nil {
return strings.TrimSpace(string(out)), nil
}
out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output()
if err != nil {
return "", fmt.Errorf("gsettings/dconf get failed for %s %s: %w", schema, key, err)
}
return strings.TrimSpace(string(out)), nil
}
// GsettingsSet writes a gsettings value, falling back to dconf write.
func GsettingsSet(schema, key, value string) error {
if err := exec.Command("gsettings", "set", schema, key, value).Run(); err == nil {
return nil
}
return exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run()
}

View File

@@ -853,7 +853,8 @@ Item {
icon: "folder",
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
};
var newSections;
@@ -1284,7 +1285,11 @@ Item {
},
actions: [],
primaryAction: null,
_diskCached: true
_diskCached: true,
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
});
}
sectionsData.push({
@@ -1293,7 +1298,8 @@ Item {
icon: s.icon || "",
priority: s.priority || 0,
items: items,
collapsed: false
collapsed: false,
flatStartIndex: 0
});
}
return sectionsData;

View File

@@ -1,6 +1,6 @@
.pragma library
.import "ControllerUtils.js" as Utils
.import "ControllerUtils.js" as Utils
function transformApp(app, override, defaultActions, primaryActionLabel) {
var appId = app.id || app.execString || app.exec || "";
@@ -31,7 +31,11 @@ function transformApp(app, override, defaultActions, primaryActionLabel) {
name: primaryActionLabel,
icon: "open_in_new",
action: "launch"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -66,7 +70,11 @@ function transformCoreApp(app, openLabel) {
name: openLabel,
icon: "open_in_new",
action: "launch"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -100,7 +108,11 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
name: openLabel,
icon: "open_in_new",
action: "execute"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -133,7 +145,11 @@ function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) {
name: openLabel,
icon: "open_in_new",
action: "open"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -166,7 +182,11 @@ function transformPluginItem(item, pluginId, selectLabel) {
name: selectLabel,
icon: "check",
action: "execute"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -188,7 +208,11 @@ function createCalculatorItem(calc, query, copyLabel) {
name: copyLabel,
icon: "content_copy",
action: "copy"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}
@@ -218,6 +242,10 @@ function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed,
name: browseLabel,
icon: "arrow_forward",
action: "browse_plugin"
}
},
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
};
}

View File

@@ -187,7 +187,8 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
icon: sectionOrder[i].icon,
priority: sectionOrder[i].priority,
items: [],
collapsed: false
collapsed: false,
flatStartIndex: 0
}
}
@@ -220,6 +221,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
function flattenSections(sections) {
var flat = []
flat._sectionBounds = null
var bounds = {}
for (var i = 0; i < sections.length; i++) {

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import qs.Common
import qs.Services
@@ -37,9 +36,23 @@ DankOSD {
}
}
property bool _pendingShow: false
onPlayerChanged: {
if (!player)
if (!player) {
_pendingShow = false;
hide();
}
}
Connections {
target: TrackArtService
function onLoadingChanged() {
if (!TrackArtService.loading && root._pendingShow) {
root._pendingShow = false;
root.show();
}
}
}
Connections {
@@ -48,10 +61,20 @@ DankOSD {
function handleUpdate() {
if (!root.player?.trackTitle)
return;
if (SettingsData.osdMediaPlaybackEnabled) {
TrackArtService.loadArtwork(player.trackArtUrl);
if (!SettingsData.osdMediaPlaybackEnabled)
return;
TrackArtService.loadArtwork(player.trackArtUrl);
if (!player.trackArtUrl || player.trackArtUrl === "") {
root.show();
return;
}
if (!TrackArtService.loading) {
root.show();
return;
}
root._pendingShow = true;
}
function onTrackArtUrlChanged() {

View File

@@ -3,62 +3,62 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import Quickshell.Io
import Quickshell.Services.Mpris
import qs.Common
Singleton {
id: root
property string _lastArtUrl: ""
property string _bgArtSource: ""
property string activeTrackArtFile: ""
property bool loading: false
function loadArtwork(url) {
if (!url || url == "") {
if (!url || url === "") {
_bgArtSource = "";
_lastArtUrl = "";
loading = false;
return;
}
if (url == _lastArtUrl)
if (url === _lastArtUrl)
return;
_lastArtUrl = url;
if (url.startsWith("http://") || url.startsWith("https://")) {
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
activeTrackArtFile = filename;
cleanupProcess.command = ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"];
cleanupProcess.running = true;
_bgArtSource = "";
loading = true;
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
imageDownloader.targetFile = filename;
imageDownloader.running = true;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url;
Proc.runCommand("trackart", ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl)
return;
if (exitCode === 0)
_bgArtSource = localUrl;
loading = false;
}, 200);
return;
}
// otherwise
_bgArtSource = url;
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
activeTrackArtFile = filename;
Proc.runCommand("trackart_cleanup", ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"], null, 0);
Proc.runCommand("trackart", ["dms", "dl", "-o", filename, "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", url], (output, exitCode) => {
const resultPath = output.trim();
if (resultPath !== filename)
return;
if (exitCode === 0)
_bgArtSource = "file://" + resultPath;
loading = false;
}, 200);
}
property MprisPlayer activePlayer: MprisController.activePlayer
onActivePlayerChanged: {
loadArtwork(activePlayer.trackArtUrl);
}
Process {
id: imageDownloader
running: false
property string targetFile: ""
onExited: exitCode => {
if (exitCode === 0 && targetFile)
_bgArtSource = "file://" + targetFile;
}
}
Process {
id: cleanupProcess
running: false
loadArtwork(activePlayer?.trackArtUrl ?? "");
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
@@ -13,7 +12,7 @@ Item {
onActiveFocusChanged: {
if (activeFocus) {
locationInput.forceActiveFocus()
locationInput.forceActiveFocus();
}
}
@@ -28,10 +27,10 @@ Item {
signal locationSelected(string displayName, string coordinates)
function resetSearchState() {
locationSearchTimer.stop()
dropdownHideTimer.stop()
isLoading = false
searchResultsModel.clear()
locationSearchTimer.stop();
dropdownHideTimer.stop();
isLoading = false;
searchResultsModel.clear();
}
width: parent.width
@@ -49,52 +48,49 @@ Item {
repeat: false
onTriggered: {
if (locationInput.text.length > 2) {
searchResultsModel.clear()
root.isLoading = true
const searchLocation = locationInput.text
root.currentSearchText = searchLocation
const encodedLocation = encodeURIComponent(searchLocation)
const curlCommand = `curl -4 -s --connect-timeout 5 --max-time 10 'https://nominatim.openstreetmap.org/search?q=${encodedLocation}&format=json&limit=5&addressdetails=1'`
Proc.runCommand("locationSearch", ["bash", "-c", curlCommand], (output, exitCode) => {
root.isLoading = false
searchResultsModel.clear();
root.isLoading = true;
const searchLocation = locationInput.text;
root.currentSearchText = searchLocation;
const encodedLocation = encodeURIComponent(searchLocation);
const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
Proc.runCommand("locationSearch", ["dms", "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
root.isLoading = false;
if (exitCode !== 0) {
searchResultsModel.clear()
return
searchResultsModel.clear();
return;
}
if (root.currentSearchText !== locationInput.text)
return
const raw = output.trim()
searchResultsModel.clear()
return;
const raw = output.trim();
searchResultsModel.clear();
if (!raw || raw[0] !== "[") {
return
return;
}
try {
const data = JSON.parse(raw)
const data = JSON.parse(raw);
if (data.length === 0) {
return
return;
}
for (var i = 0; i < Math.min(data.length, 5); i++) {
const location = data[i]
const location = data[i];
if (location.display_name && location.lat && location.lon) {
const parts = location.display_name.split(', ')
let cleanName = parts[0]
const parts = location.display_name.split(', ');
let cleanName = parts[0];
if (parts.length > 1) {
const state = parts[parts.length - 2]
const state = parts[parts.length - 2];
if (state && state !== cleanName)
cleanName += `, ${state}`
cleanName += `, ${state}`;
}
const query = `${location.lat},${location.lon}`
const query = `${location.lat},${location.lon}`;
searchResultsModel.append({
"name": cleanName,
"query": query
})
"name": cleanName,
"query": query
});
}
}
} catch (e) {
}
})
} catch (e) {}
});
}
}
}
@@ -107,7 +103,7 @@ Item {
repeat: false
onTriggered: {
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
root.resetSearchState()
root.resetSearchState();
}
}
@@ -132,23 +128,23 @@ Item {
keyNavigationBacktab: root.keyNavigationBacktab
onTextEdited: {
if (root._internalChange)
return
return;
if (getActiveFocus()) {
if (text.length > 2) {
root.isLoading = true
locationSearchTimer.restart()
root.isLoading = true;
locationSearchTimer.restart();
} else {
root.resetSearchState()
root.resetSearchState();
}
}
}
onFocusStateChanged: hasFocus => {
if (hasFocus) {
dropdownHideTimer.stop()
} else {
dropdownHideTimer.start()
}
}
if (hasFocus) {
dropdownHideTimer.stop();
} else {
dropdownHideTimer.start();
}
}
}
DankIcon {
@@ -187,13 +183,13 @@ Item {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.hovered = true
dropdownHideTimer.stop()
parent.hovered = true;
dropdownHideTimer.stop();
}
onExited: {
parent.hovered = false
parent.hovered = false;
if (!locationInput.getActiveFocus())
dropdownHideTimer.start()
dropdownHideTimer.start();
}
acceptedButtons: Qt.NoButton
}
@@ -245,14 +241,14 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root._internalChange = true
const selectedName = model.name
const selectedQuery = model.query
locationInput.text = selectedName
root.locationSelected(selectedName, selectedQuery)
root.resetSearchState()
locationInput.setFocus(false)
root._internalChange = false
root._internalChange = true;
const selectedName = model.name;
const selectedQuery = model.query;
locationInput.text = selectedName;
root.locationSelected(selectedName, selectedQuery);
root.resetSearchState();
locationInput.setFocus(false);
root._internalChange = false;
}
}
}