diff --git a/.config/quickshell/constants/WeatherIcons.qml b/.config/quickshell/constants/WeatherIcons.qml new file mode 100644 index 000000000..bd74d4e17 --- /dev/null +++ b/.config/quickshell/constants/WeatherIcons.qml @@ -0,0 +1,59 @@ +pragma Singleton + +import Quickshell + +Singleton { + // credits: calestia + // this snippet is taken from + // https://github.com/caelestia-dots/shell + readonly property var codeToName: ({ + "113": "clear_day", + "116": "partly_cloudy_day", + "119": "cloud", + "122": "cloud", + "143": "foggy", + "176": "rainy", + "179": "rainy", + "182": "rainy", + "185": "rainy", + "200": "thunderstorm", + "227": "cloudy_snowing", + "230": "snowing_heavy", + "248": "foggy", + "260": "foggy", + "263": "rainy", + "266": "rainy", + "281": "rainy", + "284": "rainy", + "293": "rainy", + "296": "rainy", + "299": "rainy", + "302": "weather_hail", + "305": "rainy", + "308": "weather_hail", + "311": "rainy", + "314": "rainy", + "317": "rainy", + "320": "cloudy_snowing", + "323": "cloudy_snowing", + "326": "cloudy_snowing", + "329": "snowing_heavy", + "332": "snowing_heavy", + "335": "snowing", + "338": "snowing_heavy", + "350": "rainy", + "353": "rainy", + "356": "rainy", + "359": "weather_hail", + "362": "rainy", + "365": "rainy", + "368": "cloudy_snowing", + "371": "snowing", + "374": "rainy", + "377": "rainy", + "386": "thunderstorm", + "389": "thunderstorm", + "392": "thunderstorm", + "395": "snowing" + }) +} diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 0a890c6ee..1eefb15d6 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -3,6 +3,7 @@ import "root:/services" import "root:/modules/common/" import "root:/modules/common/widgets" import "root:/modules/common/functions/color_utils.js" as ColorUtils +import "root:/modules/bar/weather" import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -27,7 +28,8 @@ Scope { color: Appearance.colors.colOutlineVariant } - Variants { // For each monitor + Variants { + // For each monitor model: { const screens = Quickshell.screens; const list = ConfigOptions.bar.screenList; @@ -42,12 +44,8 @@ Scope { property ShellScreen modelData property var brightnessMonitor: Brightness.getMonitorForScreen(modelData) - property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : - (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0 - readonly property int centerSideModuleWidth: - (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : - (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : - Appearance.sizes.barCenterSideModuleWidth + property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0 + readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth WlrLayershell.namespace: "quickshell:bar" implicitHeight: barHeight + Appearance.rounding.screenRounding @@ -74,7 +72,7 @@ Scope { } color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" height: barHeight - + MouseArea { // Left side | scroll to change brightness id: barLeftSideMouseArea anchors.left: parent.left @@ -87,21 +85,21 @@ Scope { acceptedButtons: Qt.LeftButton hoverEnabled: true propagateComposedEvents: true - onEntered: (event) => { - barLeftSideMouseArea.hovered = true + onEntered: event => { + barLeftSideMouseArea.hovered = true; } - onExited: (event) => { - barLeftSideMouseArea.hovered = false - barLeftSideMouseArea.trackingScroll = false + onExited: event => { + barLeftSideMouseArea.hovered = false; + barLeftSideMouseArea.trackingScroll = false; } - onPressed: (event) => { + onPressed: event => { if (event.button === Qt.LeftButton) { - Hyprland.dispatch('global quickshell:sidebarLeftOpen') + Hyprland.dispatch('global quickshell:sidebarLeftOpen'); } } // Scroll to change brightness WheelHandler { - onWheel: (event) => { + onWheel: event => { if (event.angleDelta.y < 0) barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05); else if (event.angleDelta.y > 0) @@ -113,17 +111,18 @@ Scope { } acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad } - onPositionChanged: (mouse) => { + onPositionChanged: mouse => { if (barLeftSideMouseArea.trackingScroll) { const dx = mouse.x - barLeftSideMouseArea.lastScrollX; const dy = mouse.y - barLeftSideMouseArea.lastScrollY; - if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) { - Hyprland.dispatch('global quickshell:osdBrightnessHide') + if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) { + Hyprland.dispatch('global quickshell:osdBrightnessHide'); barLeftSideMouseArea.trackingScroll = false; } } } - Item { // Left section + Item { + // Left section anchors.fill: parent implicitHeight: leftSectionRowLayout.implicitHeight implicitWidth: leftSectionRowLayout.implicitWidth @@ -135,22 +134,22 @@ Scope { side: "left" anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - } - + RowLayout { // Content id: leftSectionRowLayout anchors.fill: parent spacing: 10 - RippleButton { // Left sidebar button + RippleButton { + // Left sidebar button Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.leftMargin: Appearance.rounding.screenRounding Layout.fillWidth: false property real buttonPadding: 5 implicitWidth: distroIcon.width + buttonPadding * 2 implicitHeight: distroIcon.height + buttonPadding * 2 - + buttonRadius: Appearance.rounding.full colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1) colBackgroundHover: Appearance.colors.colLayer1Hover @@ -159,10 +158,10 @@ Scope { colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover colRippleToggled: Appearance.colors.colSecondaryContainerActive toggled: GlobalStates.sidebarLeftOpen - property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 + property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 onPressed: { - Hyprland.dispatch('global quickshell:sidebarLeftToggle') + Hyprland.dispatch('global quickshell:sidebarLeftToggle'); } CustomIcon { @@ -170,10 +169,9 @@ Scope { anchors.centerIn: parent width: 19.5 height: 19.5 - source: ConfigOptions.bar.topLeftIcon == 'distro' ? - SystemInfo.distroIcon : "spark-symbolic" + source: ConfigOptions.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : "spark-symbolic" } - + ColorOverlay { anchors.fill: distroIcon source: distroIcon @@ -211,34 +209,38 @@ Scope { visible: barRoot.useShortenedForm < 2 Layout.fillWidth: true } - } - VerticalBarSeparator {visible: ConfigOptions?.bar.borderless} + VerticalBarSeparator { + visible: ConfigOptions?.bar.borderless + } BarGroup { id: middleCenterGroup padding: workspacesWidget.widgetPadding Layout.fillHeight: true - + Workspaces { id: workspacesWidget bar: barRoot Layout.fillHeight: true - MouseArea { // Right-click to toggle overview + MouseArea { + // Right-click to toggle overview anchors.fill: parent acceptedButtons: Qt.RightButton - - onPressed: (event) => { + + onPressed: event => { if (event.button === Qt.RightButton) { - Hyprland.dispatch('global quickshell:overviewToggle') + Hyprland.dispatch('global quickshell:overviewToggle'); } } } } } - VerticalBarSeparator {visible: ConfigOptions?.bar.borderless} + VerticalBarSeparator { + visible: ConfigOptions?.bar.borderless + } MouseArea { id: rightCenterGroup @@ -248,13 +250,13 @@ Scope { Layout.fillHeight: true onPressed: { - Hyprland.dispatch('global quickshell:sidebarRightToggle') + Hyprland.dispatch('global quickshell:sidebarRightToggle'); } BarGroup { id: rightCenterGroupContent anchors.fill: parent - + ClockWidget { showDate: (ConfigOptions.bar.verbose && barRoot.useShortenedForm < 2) Layout.alignment: Qt.AlignVCenter @@ -273,6 +275,19 @@ Scope { } } + VerticalBarSeparator { + visible: ConfigOptions?.bar.borderless + } + + // Weather + BarGroup { + id: weatherGroupContent + Layout.fillHeight: true + Layout.alignment: Qt.AlignVCenter + WeatherBar { + visible: ConfigOptions.bar.weather.show + } + } } MouseArea { // Right side | scroll to change volume @@ -286,28 +301,27 @@ Scope { property real lastScrollX: 0 property real lastScrollY: 0 property bool trackingScroll: false - + acceptedButtons: Qt.LeftButton hoverEnabled: true propagateComposedEvents: true - onEntered: (event) => { - barRightSideMouseArea.hovered = true + onEntered: event => { + barRightSideMouseArea.hovered = true; } - onExited: (event) => { - barRightSideMouseArea.hovered = false - barRightSideMouseArea.trackingScroll = false + onExited: event => { + barRightSideMouseArea.hovered = false; + barRightSideMouseArea.trackingScroll = false; } - onPressed: (event) => { + onPressed: event => { if (event.button === Qt.LeftButton) { - Hyprland.dispatch('global quickshell:sidebarRightOpen') - } - else if (event.button === Qt.RightButton) { - MprisController.activePlayer.next() + Hyprland.dispatch('global quickshell:sidebarRightOpen'); + } else if (event.button === Qt.RightButton) { + MprisController.activePlayer.next(); } } // Scroll to change volume WheelHandler { - onWheel: (event) => { + onWheel: event => { const currentVolume = Audio.value; const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2; if (event.angleDelta.y < 0) @@ -321,12 +335,12 @@ Scope { } acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad } - onPositionChanged: (mouse) => { + onPositionChanged: mouse => { if (barRightSideMouseArea.trackingScroll) { const dx = mouse.x - barRightSideMouseArea.lastScrollX; const dy = mouse.y - barRightSideMouseArea.lastScrollY; - if (Math.sqrt(dx*dx + dy*dy) > osdHideMouseMoveThreshold) { - Hyprland.dispatch('global quickshell:osdVolumeHide') + if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) { + Hyprland.dispatch('global quickshell:osdVolumeHide'); barRightSideMouseArea.trackingScroll = false; } } @@ -336,7 +350,7 @@ Scope { anchors.fill: parent implicitHeight: rightSectionRowLayout.implicitHeight implicitWidth: rightSectionRowLayout.implicitWidth - + ScrollHint { reveal: barRightSideMouseArea.hovered icon: "volume_up" @@ -351,13 +365,13 @@ Scope { anchors.fill: parent spacing: 5 layoutDirection: Qt.RightToLeft - + RippleButton { // Right sidebar button id: rightSidebarButton Layout.margins: 4 Layout.rightMargin: Appearance.rounding.screenRounding Layout.fillHeight: true - implicitWidth: indicatorsRowLayout.implicitWidth + 10*2 + implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2 buttonRadius: Appearance.rounding.full colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1) colBackgroundHover: Appearance.colors.colLayer1Hover @@ -373,7 +387,7 @@ Scope { } onPressed: { - Hyprland.dispatch('global quickshell:sidebarRightToggle') + Hyprland.dispatch('global quickshell:sidebarRightToggle'); } RowLayout { @@ -381,7 +395,7 @@ Scope { anchors.centerIn: parent property real realSpacing: 15 spacing: 0 - + Revealer { reveal: Audio.sink?.audio?.muted ?? false Layout.fillHeight: true @@ -475,9 +489,6 @@ Scope { opacity: 1.0 - Appearance.transparency } } - } - } - } diff --git a/.config/quickshell/modules/bar/weather/WeatherBar.qml b/.config/quickshell/modules/bar/weather/WeatherBar.qml new file mode 100644 index 000000000..58f68ab45 --- /dev/null +++ b/.config/quickshell/modules/bar/weather/WeatherBar.qml @@ -0,0 +1,68 @@ +pragma ComponentBehavior: Bound +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/constants" +import "root:/services" +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + property real margin: 5 + implicitHeight: 32 + implicitWidth: mouseArea.implicitWidth + margin * 2 + + MouseArea { + id: mouseArea + property bool hovered: false + implicitWidth: rowLayout.implicitWidth + implicitHeight: rowLayout.implicitHeight + anchors.centerIn: root + + hoverEnabled: true + onEntered: { + popupLoader.item.visible = true; + } + onExited: { + popupLoader.item.visible = false; + } + + RowLayout { + id: rowLayout + + MaterialSymbol { + fill: 0 + text: WeatherIcons.codeToName[WeatherService.data.wCode] + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer1 + } + + StyledText { + visible: true + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer1 + text: WeatherService.data.temp + } + } + } + + LazyLoader { + id: popupLoader + active: true + + component: PopupWindow { + id: popupWindow + implicitWidth: weatherPopup.implicitWidth + implicitHeight: weatherPopup.implicitHeight + anchor.item: root + anchor.edges: Edges.Bottom + anchor.rect.x: (root.implicitWidth - popupWindow.implicitWidth) / 2 + anchor.rect.y: root.implicitHeight + 10 + color: "transparent" + WeatherPopup { + id: weatherPopup + } + } + } +} diff --git a/.config/quickshell/modules/bar/weather/WeatherCard.qml b/.config/quickshell/modules/bar/weather/WeatherCard.qml new file mode 100644 index 000000000..6910b2cc5 --- /dev/null +++ b/.config/quickshell/modules/bar/weather/WeatherCard.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Layouts + +import "root:/modules/common" +import "root:/modules/common/widgets" + +Rectangle { + id: root + radius: Appearance.rounding.verysmall + color: Appearance.colors.colLayer1 + border.color: Appearance.colors.colShadow + border.width: 1 + implicitWidth: columnLayout.implicitWidth * 2 + implicitHeight: columnLayout.implicitHeight * 2 + Layout.fillWidth: parent + + property alias title: title.text + property alias value: value.text + property alias symbol: symbol.text + + ColumnLayout { + id: columnLayout + anchors.fill: parent + spacing: -10 + RowLayout { + Layout.alignment: Qt.AlignHCenter + MaterialSymbol { + id: symbol + fill: 0 + iconSize: Appearance.font.pixelSize.normal + } + Text { + id: title + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colOnLayer2 + } + } + Text { + id: value + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer2 + } + } +} diff --git a/.config/quickshell/modules/bar/weather/WeatherPopup.qml b/.config/quickshell/modules/bar/weather/WeatherPopup.qml new file mode 100644 index 000000000..04f0c307a --- /dev/null +++ b/.config/quickshell/modules/bar/weather/WeatherPopup.qml @@ -0,0 +1,95 @@ +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" + +import QtQuick +import QtQuick.Layouts + +Rectangle { + id: root + readonly property real margin: 10 + implicitWidth: columnLayout.implicitWidth + margin * 2 + implicitHeight: columnLayout.implicitHeight + margin * 2 + color: Appearance.colors.colLayer0 + radius: 12 + clip: true + border.color: Appearance.colors.colShadow + border.width: 1 + + ColumnLayout { + id: columnLayout + spacing: 5 + anchors.centerIn: root + implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth) + implicitHeight: gridLayout.implicitHeight + + // Header + RowLayout { + id: header + spacing: 5 + Layout.fillWidth: parent + Layout.alignment: Qt.AlignHCenter + MaterialSymbol { + fill: 0 + text: "location_on" + iconSize: Appearance.font.pixelSize.huge + } + + Text { + text: WeatherService.data.city + font.pixelSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer0 + } + } + + // Metrics grid + GridLayout { + id: gridLayout + columns: 2 + rowSpacing: 5 + columnSpacing: 5 + uniformCellWidths: true + + WeatherCard { + title: "UV Index" + symbol: "wb_sunny" + value: WeatherService.data.uv + } + WeatherCard { + title: "Wind" + symbol: "air" + value: `(${WeatherService.data.windDir}) ${WeatherService.data.wind}` + } + WeatherCard { + title: "Precipitation" + symbol: "rainy_light" + value: WeatherService.data.precip + } + WeatherCard { + title: "Humidity" + symbol: "humidity_low" + value: WeatherService.data.humidity + } + WeatherCard { + title: "Visibility" + symbol: "visibility" + value: WeatherService.data.visib + } + WeatherCard { + title: "Pressure" + symbol: "readiness_score" + value: WeatherService.data.press + } + WeatherCard { + title: "Sunrise" + symbol: "wb_twilight" + value: WeatherService.data.sunrise + } + WeatherCard { + title: "Sunset" + symbol: "bedtime" + value: WeatherService.data.sunset + } + } + } +} diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml index 198d4affc..ad5395743 100644 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ b/.config/quickshell/modules/common/ConfigOptions.qml @@ -72,6 +72,16 @@ Singleton { property bool alwaysShowNumbers: false property int showNumberDelay: 300 // milliseconds } + property QtObject weather: QtObject { + property bool show: true + // for specific location checkout gps setting + property string city: "Istanbul" + // use uscs units + // by default use metric (SI) units + property bool useUSCS: false + // in minutes + property int fetchInterval: 10 + } } property QtObject battery: QtObject { @@ -161,4 +171,11 @@ Singleton { property QtObject hacks: QtObject { property int arbitraryRaceConditionDelay: 20 // milliseconds } + + // this is for weather and feature apis + property QtObject gps: QtObject { + property bool active: false + property real latitude: 41.27830580591624 + property real longitude: 28.730357071149154 + } } diff --git a/.config/quickshell/services/WeatherService.qml b/.config/quickshell/services/WeatherService.qml new file mode 100644 index 000000000..da418f724 --- /dev/null +++ b/.config/quickshell/services/WeatherService.qml @@ -0,0 +1,105 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick + +import "root:/modules/common" + +Singleton { + id: root + // 10 minute + readonly property int fetchInterval: ConfigOptions.bar.weather.fetchInterval * 60 * 1000 + property var data: ({ + uv: 0, + humidity: 0, + sunrise: 0, + sunset: 0, + windDir: 0, + wCode: 0, + city: 0, + wind: 0, + precip: 0, + visib: 0, + press: 0, + temp: 0 + }) + + function refineData(data) { + let temp = {}; + temp.uv = data?.current?.uvIndex || 0; + temp.humidity = (data?.current?.humidity || 0) + "%"; + temp.sunrise = data?.astronomy?.sunrise || "0.0"; + temp.sunset = data?.astronomy?.sunset || "0.0"; + temp.windDir = data?.current?.winddir16Point || "N"; + temp.wCode = data?.current?.weatherCode || "113"; + temp.city = data?.location?.areaName[0].value || "Istanbul"; + temp.temp = ""; + if (ConfigOptions.bar.weather.useUSCS) { + temp.wind = (data?.current?.windspeedMiles || 0) + " mph"; + temp.precip = (data?.current?.precipInches || 0) + " in"; + temp.visib = (data?.current?.visibilityMiles || 0) + " m"; + temp.press = (data?.current?.pressureInches || 0) + " psi"; + temp.temp += (data?.current?.temp_F || 0); + temp.temp += " (" + (data?.current?.FeelsLikeF || 0) + ") "; + temp.temp += "\u{02109}"; + } else { + temp.wind = (data?.current?.windspeedKmph || 0) + " km/h"; + temp.precip = (data?.current?.precipMM || 0) + " mm"; + temp.visib = (data?.current?.visibility || 0) + " km"; + temp.press = (data?.current?.pressure || 0) + " hPa"; + temp.temp += (data?.current?.temp_C || 0); + temp.temp += " (" + (data?.current?.FeelsLikeC || 0) + ") "; + temp.temp += "\u{02103}"; + } + root.data = temp; + } + + function getData() { + let command = "curl -s wttr.in"; + if (ConfigOptions.gps.active) { + command += `/${ConfigOptions.gps.latitude},${Config.gps.longitude}`; + } else { + command += `/${formatCityName(ConfigOptions.bar.weather.city)}`; + } + + // format as json + command += "?format=j1"; + command += " | "; + // only take the current weather, location, asytronmy data + command += "jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"; + fetcher.command[2] = command; + fetcher.running = true; + } + + function formatCityName(cityName) { + return cityName.trim().split(/\s+/).join('+'); + } + + Process { + id: fetcher + command: ["bash", "-c", ""] + stdout: StdioCollector { + onStreamFinished: { + if (text.length === 0) + return; + try { + const parsedData = JSON.parse(text); + root.refineData(parsedData); + // console.info(`[ data: ${JSON.stringify(parsedData)}`); + } catch (e) { + console.error(`[WeatherService] ${e.message}`); + } + } + } + } + + Timer { + running: true + repeat: true + interval: root.fetchInterval + triggeredOnStart: true + onTriggered: root.getData() + } +}