Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 22s
Python / lint (pull_request) Successful in 33s
Python / fmt (pull_request) Successful in 36s
Python / test (pull_request) Successful in 1m5s
C++ / build (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 1m11s
Rust / build (pull_request) Successful in 1m56s
Python / buildcheck (pull_request) Successful in 2m40s
Rust / clippy (pull_request) Successful in 1m48s
C++ / clang-tidy (pull_request) Successful in 3m42s

This commit is contained in:
2026-07-08 22:56:46 +02:00
43 changed files with 3050 additions and 2288 deletions
+15 -42
View File
@@ -11,8 +11,13 @@ Singleton {
id: root
property bool appleDisplayPresent: false
readonly property var ddcMonitorMap: {
const map = {};
for (const m of ddcMonitors)
map[m.connector] = m;
return map;
}
property list<var> ddcMonitors: []
property list<var> ddcServiceMon: []
readonly property list<Monitor> monitors: variants.instances
function decreaseBrightness(): void {
@@ -56,8 +61,6 @@ Singleton {
onMonitorsChanged: {
ddcMonitors = [];
ddcServiceMon = [];
ddcServiceProc.running = true;
ddcProc.running = true;
}
@@ -92,26 +95,6 @@ Singleton {
}
}
Process {
id: ddcServiceProc
command: ["ddcutil-client", "detect"]
// running: true
stdout: StdioCollector {
onStreamFinished: {
const t = text.replace(/\r\n/g, "\n").trim();
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
})).filter(d => d.display > 0);
root.ddcServiceMon = output;
}
}
}
CustomShortcut {
description: "Increase brightness"
name: "brightnessUp"
@@ -183,16 +166,12 @@ Singleton {
id: monitor
property real brightness
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
readonly property string busNum: ddcInfo?.busNum ?? ""
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
readonly property Process initProc: Process {
stdout: StdioCollector {
onStreamFinished: {
if (monitor.isDdcService) {
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
const val = parseInt(output.trim());
monitor.brightness = val / 100;
} else if (monitor.isAppleDisplay) {
if (monitor.isAppleDisplay) {
const val = parseInt(text.trim());
monitor.brightness = val / 101;
} else {
@@ -203,12 +182,11 @@ Singleton {
}
}
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
readonly property bool isDdcService: Config.services.ddcutilService
readonly property bool isDdc: ddcInfo !== null
required property ShellScreen modelData
property real queuedBrightness: NaN
readonly property Timer timer: Timer {
interval: 500
interval: 400
onTriggered: {
if (!isNaN(monitor.queuedBrightness)) {
@@ -219,9 +197,7 @@ Singleton {
}
function initBrightness(): void {
if (isDdcService)
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
else if (isAppleDisplay)
if (isAppleDisplay)
initProc.command = ["asdbctl", "get"];
else if (isDdc)
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
@@ -237,28 +213,25 @@ Singleton {
if (Math.round(brightness * 100) === rounded)
return;
if ((isDdc || isDdcService) && timer.running) {
if (isDdc && timer.running) {
queuedBrightness = value;
return;
}
brightness = value;
if (isDdcService)
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
else if (isAppleDisplay)
if (isAppleDisplay)
Quickshell.execDetached(["asdbctl", "set", rounded]);
else if (isDdc)
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
else
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
if (isDdc || isDdcService)
if (isDdc)
timer.restart();
}
Component.onCompleted: initBrightness()
onBusNumChanged: initBrightness()
onDisplayNumChanged: initBrightness()
}
}
+31 -3
View File
@@ -1,12 +1,12 @@
pragma Singleton
import QtQml
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import QtQml
import ZShell
import qs.Config
import qs.Components
import qs.Config
Singleton {
id: root
@@ -15,7 +15,24 @@ Singleton {
readonly property list<MprisPlayer> list: Mpris.players.values
property alias manualActive: props.manualActive
function getArtUrl(player: MprisPlayer): string {
if (!player)
return "";
if (player.trackArtUrl)
return player.trackArtUrl;
const url = player.metadata["xesam:url"] ?? "";
if (url.startsWith("https://www.youtube.com/watch")) {
// Fallback for youtube
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
}
return "";
}
function getIdentity(player: MprisPlayer): string {
if (!player)
return "";
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
return alias?.to ?? player.identity;
}
@@ -25,9 +42,12 @@ Singleton {
if (!Config.utilities.toasts.nowPlaying) {
return;
}
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
}
}
target: active
target: root.active
}
PersistentProperties {
@@ -38,8 +58,10 @@ Singleton {
reloadableId: "players"
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Toggle media playback"
// qmllint enable unresolved-type
name: "mediaToggle"
onPressed: {
@@ -49,8 +71,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Previous track"
// qmllint enable unresolved-type
name: "mediaPrev"
onPressed: {
@@ -60,8 +84,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Next track"
// qmllint enable unresolved-type
name: "mediaNext"
onPressed: {
@@ -71,8 +97,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Stop media playback"
// qmllint enable unresolved-type
name: "mediaStop"
onPressed: root.active?.stop()
+107 -32
View File
@@ -1,7 +1,7 @@
pragma Singleton
import Quickshell
import QtQuick
import Quickshell
import ZShell
import qs.Config
@@ -12,15 +12,15 @@ Singleton {
property var cc
property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
property list<var> forecast
property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
property string loc
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
readonly property string temp: `${cc?.tempC ?? 0}°C`
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string temp: formatTemp(cc?.tempC)
readonly property real windSpeed: cc?.windSpeed ?? 0
function fetchCityFromCoords(coords: string): void {
@@ -29,29 +29,48 @@ Singleton {
return;
}
const [lat, lon] = coords.split(",");
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
Requests.get(url, text => {
const [lat, lon] = coords.split(",").map(s => s.trim());
const lang = Qt.locale().name.split("_")[0] || "en";
const fallbackToBigDataCloud = () => {
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
Requests.get(fallbackUrl, text => {
const geo = JSON.parse(text);
const geoCity = geo.city || geo.locality;
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, city);
} else {
city = "Unknown City";
}
});
};
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
Requests.get(nominatimUrl, text => {
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
if (geo) {
const geoCity = geo.type === "city" ? geo.name : geo.city;
city = geoCity;
cachedCities.set(coords, geoCity);
} else {
city = "Unknown City";
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, city);
return;
}
}
});
fallbackToBigDataCloud();
}, fallbackToBigDataCloud);
}
function fetchCoordsFromCity(cityName: string): void {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
const lang = Qt.locale().name.split("_")[0] || "en";
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
Requests.get(url, text => {
const json = JSON.parse(text);
if (json.results && json.results.length > 0) {
const result = json.results[0];
loc = result.latitude + "," + result.longitude;
city = result.name;
city = fixCityName(result.name);
} else {
loc = "";
reload();
@@ -72,25 +91,21 @@ Singleton {
cc = {
weatherCode: json.current.weather_code,
weatherDesc: getWeatherCondition(json.current.weather_code),
tempC: Math.round(json.current.temperature_2m),
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
feelsLikeC: Math.round(json.current.apparent_temperature),
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
tempC: json.current.temperature_2m,
feelsLikeC: json.current.apparent_temperature,
humidity: json.current.relative_humidity_2m,
windSpeed: json.current.wind_speed_10m,
isDay: json.current.is_day,
sunrise: json.daily.sunrise[0],
sunset: json.daily.sunset[0]
sunrise: json.daily.sunrise[0].replace("T", " "),
sunset: json.daily.sunset[0].replace("T", " ")
};
const forecastList = [];
for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({
date: json.daily.time[i],
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
minTempC: Math.round(json.daily.temperature_2m_min[i]),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
date: json.daily.time[i].replace(/-/g, "/"),
maxTempC: json.daily.temperature_2m_max[i],
minTempC: json.daily.temperature_2m_min[i],
weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
});
@@ -99,7 +114,8 @@ Singleton {
const hourlyList = [];
const now = new Date();
for (let i = 0; i < json.hourly.time.length; i++) {
const time = new Date(json.hourly.time[i]);
const time = new Date(json.hourly.time[i].replace("T", " "));
if (time < now)
continue;
@@ -107,7 +123,7 @@ Singleton {
timestamp: json.hourly.time[i],
hour: time.getHours(),
tempC: Math.round(json.hourly.temperature_2m[i]),
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
precipChance: json.hourly.precipitation_probability[i],
weatherCode: json.hourly.weather_code[i],
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
});
@@ -116,6 +132,59 @@ Singleton {
});
}
function fixCityName(cityName: string): string {
if (!cityName)
return "";
const mapping = {
// Polish
"Poznan": "Poznań",
"Wroclaw": "Wrocław",
"Krakow": "Kraków",
"Gdansk": "Gdańsk",
"Lodz": "Łódź",
"Rzeszow": "Rzeszów",
"Torun": "Toruń",
"Bialystok": "Białystok",
"Czestochowa": "Częstochowa",
"Plock": "Płock",
"Ruda Slaska": "Ruda Śląska",
"Dabrowa Gornicza": "Dąbrowa Górnicza",
"Elblag": "Elbląg",
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
"Zielona Gora": "Zielona Góra",
"Slupsk": "Słupsk",
// German
"Munchen": "München",
"Koln": "Köln",
"Dusseldorf": "Düsseldorf",
"Nurnberg": "Nürnberg",
// French & Spanish & Portuguese
"Sao Paulo": "São Paulo",
"Montreal": "Montréal",
"Quebec": "Québec",
"Bogota": "Bogotá",
"Medellin": "Medellín",
"Cordoba": "Córdoba",
// Turkish
"Istanbul": "İstanbul",
"Izmir": "İzmir",
// Scandinavian & others
"Malmo": "Malmö",
"Goteborg": "Göteborg",
"Zurich": "Zürich",
"Geneve": "Genève"
};
return mapping[cityName] || cityName;
}
function formatTemp(temp: var): string {
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
}
function getWeatherCondition(code: string): string {
const conditions = {
"0": "Clear",
@@ -154,9 +223,9 @@ Singleton {
if (!loc || loc.indexOf(",") === -1)
return "";
const [lat, lon] = loc.split(",");
const [lat, lon] = loc.split(",").map(s => s.trim());
const baseUrl = "https://api.open-meteo.com/v1/forecast";
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
return baseUrl + "?" + params.join("&");
}
@@ -189,7 +258,14 @@ Singleton {
onLocChanged: fetchWeatherData()
// Refresh current location hourly
Connections {
function onWeatherLocationChanged(): void {
root.reload();
}
target: Config.services
}
Timer {
interval: 3600000 // 1 hour
repeat: true
@@ -200,6 +276,5 @@ Singleton {
ElapsedTimer {
id: timer
}
}