From ad7fdd1d3fcda4dfccef2a0f5afdfd09c705f840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B9=E3=82=B1=E3=83=99?= Date: Thu, 19 Jun 2025 18:14:05 +0300 Subject: [PATCH 1/5] layout indicator in top right --- .config/quickshell/modules/bar/Bar.qml | 7 ++++ .config/quickshell/services/LayoutService.qml | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .config/quickshell/services/LayoutService.qml diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 925ac140..3d506291 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -416,6 +416,13 @@ Scope { color: rightSidebarButton.colText } } + Label { + Layout.rightMargin: indicatorsRowLayout.realSpacing + text: LayoutService.currentLayout + visible: LayoutService.currentLayout !== "" + font.pixelSize: Appearance.font.pixelSize.larger - 3 + color: rightSidebarButton.colText + } MaterialSymbol { Layout.rightMargin: indicatorsRowLayout.realSpacing text: Network.materialSymbol diff --git a/.config/quickshell/services/LayoutService.qml b/.config/quickshell/services/LayoutService.qml new file mode 100644 index 00000000..df9430e5 --- /dev/null +++ b/.config/quickshell/services/LayoutService.qml @@ -0,0 +1,35 @@ +pragma Singleton + +import QtQuick +import Quickshell.Hyprland + +QtObject { + id: layoutService + + property string currentLayout: "" // This is empty on startup. We could default it to "en", but we don't know the user's configured layout order (e.g. "en,ru" vs "ru,en"). + // I haven't found a way to query the initial layout from QML without external bash scripts, so this is the safest compromise for now. + + function parseLayout(fullLayoutName) { + if (!fullLayoutName) return; + + const shortName = fullLayoutName.substring(0, 2).toLowerCase(); + + if (currentLayout !== shortName) { + currentLayout = shortName; + } + } + + function handleRawEvent(event) { + if (event.name === "activelayout") { + const dataString = event.data; + const layoutInfo = dataString.split(","); + const fullLayoutName = layoutInfo[layoutInfo.length - 1]; + + parseLayout(fullLayoutName); + } + } + + Component.onCompleted: { + Hyprland.rawEvent.connect(handleRawEvent); + } +} \ No newline at end of file From 82fd2334cf14218e31350dbb470b31424e3174c2 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:07:34 +0700 Subject: [PATCH 2/5] bar: layout indicator: more proper layout parsing --- .config/quickshell/modules/bar/Bar.qml | 13 ++- .config/quickshell/services/HyprlandXkb.qml | 106 ++++++++++++++++++ .config/quickshell/services/LayoutService.qml | 35 ------ 3 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 .config/quickshell/services/HyprlandXkb.qml delete mode 100644 .config/quickshell/services/LayoutService.qml diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 3d506291..9e6e2f99 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -416,12 +416,15 @@ Scope { color: rightSidebarButton.colText } } - Label { + Loader { + active: HyprlandXkb.layoutCodes.length > 1 + visible: active Layout.rightMargin: indicatorsRowLayout.realSpacing - text: LayoutService.currentLayout - visible: LayoutService.currentLayout !== "" - font.pixelSize: Appearance.font.pixelSize.larger - 3 - color: rightSidebarButton.colText + sourceComponent: StyledText { + text: HyprlandXkb.currentLayoutCode + font.pixelSize: Appearance.font.pixelSize.small + color: rightSidebarButton.colText + } } MaterialSymbol { Layout.rightMargin: indicatorsRowLayout.realSpacing diff --git a/.config/quickshell/services/HyprlandXkb.qml b/.config/quickshell/services/HyprlandXkb.qml new file mode 100644 index 00000000..bb842487 --- /dev/null +++ b/.config/quickshell/services/HyprlandXkb.qml @@ -0,0 +1,106 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +/** + * Exposes the active Hyprland Xkb keyboard layout name and code for indicators. + */ +Singleton { + id: root + // You can read these + property list layoutCodes: [] + property var cachedLayoutCodes: ({}) + property string currentLayoutName: "" + property string currentLayoutCode: "" + // For the service + property string targetDeviceName: "hl-virtual-keyboard" + property var baseLayoutFilePath: "/usr/share/X11/xkb/rules/base.lst" + property bool needsLayoutRefresh: false + + // Update the layout code according to the layout name (Hyprland gives the name not the code) + onCurrentLayoutNameChanged: root.updateLayoutCode() + function updateLayoutCode() { + if (cachedLayoutCodes.hasOwnProperty(currentLayoutName)) { + root.currentLayoutCode = cachedLayoutCodes[currentLayoutName]; + } else { + getLayoutProc.running = true; + } + } + + // Get the layout code from the base.lst file by grabbing the line with the current layout name + Process { + id: getLayoutProc + command: ["cat", root.baseLayoutFilePath] + + stdout: StdioCollector { + id: layoutCollector + + onStreamFinished: { + const lines = layoutCollector.text.split("\n"); + const targetDescription = root.currentLayoutName; + const foundLine = lines.find(line => { + // Skip comment lines and empty lines + if (!line.trim() || line.trim().startsWith('!')) + return false; + + // Match: key + whitespace + description + const match = line.match(/^\s*(\S+)\s+(.+)$/); + if (match && match[2] === targetDescription) { + root.cachedLayoutCodes[match[2]] = match[1]; + root.currentLayoutCode = match[1]; + return true; + } + }); + // console.log("[HyprlandXkb] Found line:", foundLine); + // console.log("[HyprlandXkb] Layout:", root.currentLayoutName, "| Code:", root.currentLayoutCode); + // console.log("[HyprlandXkb] Cached layout codes:", JSON.stringify(root.cachedLayoutCodes, null, 2)); + } + } + } + + // Find out available layouts and current active layout. Should only be necessary on init + Process { + id: fetchLayoutsProc + running: true + command: ["hyprctl", "-j", "devices"] + + stdout: StdioCollector { + id: devicesCollector + onStreamFinished: { + const parsedOutput = JSON.parse(devicesCollector.text); + const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.name === root.targetDeviceName); + root.layoutCodes = hyprlandKeyboard["layout"].split(","); + root.currentLayoutName = hyprlandKeyboard["active_keymap"]; + // console.log("[HyprlandXkb] Fetched | Layouts (multiple: " + (root.layouts.length > 1) + "): " + // + root.layouts.join(", ") + " | Active: " + root.currentLayoutName); + } + } + } + + // Update the layout name when it changes + Connections { + target: Hyprland + function onRawEvent(event) { + if (event.name === "activelayout") { + // We're triggering refresh here because Hyprland virtual kb after a config reload disappears + // from `hyprctl devices` and it only comes back at the next activelayout event. + if (root.needsLayoutRefresh) { + root.needsLayoutRefresh = false; + fetchLayoutsProc.running = true; + } + // Update when layout might have changed + const dataString = event.data; + // console.log("[HyprlandXkb] Received raw event:", event.name, "with data:", dataString); + if (!dataString.startsWith(root.targetDeviceName)) + return; + root.currentLayoutName = dataString.split(",")[1]; + } else if (event.name == "configreloaded") { + // Mark layout code list to be updated when config is reloaded + root.needsLayoutRefresh = true; + } + } + } +} diff --git a/.config/quickshell/services/LayoutService.qml b/.config/quickshell/services/LayoutService.qml deleted file mode 100644 index df9430e5..00000000 --- a/.config/quickshell/services/LayoutService.qml +++ /dev/null @@ -1,35 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell.Hyprland - -QtObject { - id: layoutService - - property string currentLayout: "" // This is empty on startup. We could default it to "en", but we don't know the user's configured layout order (e.g. "en,ru" vs "ru,en"). - // I haven't found a way to query the initial layout from QML without external bash scripts, so this is the safest compromise for now. - - function parseLayout(fullLayoutName) { - if (!fullLayoutName) return; - - const shortName = fullLayoutName.substring(0, 2).toLowerCase(); - - if (currentLayout !== shortName) { - currentLayout = shortName; - } - } - - function handleRawEvent(event) { - if (event.name === "activelayout") { - const dataString = event.data; - const layoutInfo = dataString.split(","); - const fullLayoutName = layoutInfo[layoutInfo.length - 1]; - - parseLayout(fullLayoutName); - } - } - - Component.onCompleted: { - Hyprland.rawEvent.connect(handleRawEvent); - } -} \ No newline at end of file From 012df9dcd72cc2ced49f3afd43d58cac81769041 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:11:40 +0700 Subject: [PATCH 3/5] hyprlandxkb: dont update when not necessary --- .config/quickshell/services/HyprlandXkb.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.config/quickshell/services/HyprlandXkb.qml b/.config/quickshell/services/HyprlandXkb.qml index bb842487..76a7bd35 100644 --- a/.config/quickshell/services/HyprlandXkb.qml +++ b/.config/quickshell/services/HyprlandXkb.qml @@ -91,9 +91,12 @@ Singleton { root.needsLayoutRefresh = false; fetchLayoutsProc.running = true; } + + // If there's only one layout, the updated layout is always the same + if (root.layoutCodes.length <= 1) return; + // Update when layout might have changed const dataString = event.data; - // console.log("[HyprlandXkb] Received raw event:", event.name, "with data:", dataString); if (!dataString.startsWith(root.targetDeviceName)) return; root.currentLayoutName = dataString.split(",")[1]; From ffeb27f04e8ddc8013183da6561e6a308a24f6a6 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:12:54 +0700 Subject: [PATCH 4/5] bar: layout indicator: smaller --- .config/quickshell/modules/bar/Bar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 9e6e2f99..ae6c2730 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -422,7 +422,7 @@ Scope { Layout.rightMargin: indicatorsRowLayout.realSpacing sourceComponent: StyledText { text: HyprlandXkb.currentLayoutCode - font.pixelSize: Appearance.font.pixelSize.small + font.pixelSize: Appearance.font.pixelSize.smaller color: rightSidebarButton.colText } } From 5870632c195c65043be4b647ba63a58e8df78185 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:18:22 +0700 Subject: [PATCH 5/5] Merge remote-tracking branch 'upstream/main' into layout_service --- .config/anyrun/config.ron | 13 - .config/anyrun/style.css | 66 -- .config/chrome-flags.conf | 2 +- .config/code-flags.conf | 2 +- .config/fish/config.fish | 1 + .config/hypr/hypridle.conf | 3 + .config/hypr/hyprland.conf | 1 + .config/hypr/hyprland/env.conf | 18 +- .config/hypr/hyprland/execs.conf | 6 +- .config/hypr/hyprland/general.conf | 8 +- .config/hypr/hyprland/keybinds.conf | 40 +- .config/hypr/hyprland/rules.conf | 30 +- .../ai/license_show-loaded-ollama-models.txt | 201 ++++++ .../scripts/ai/primary-buffer-query.sh | 41 ++ .../scripts/ai/show-loaded-ollama-models.sh | 99 +++ .config/hypr/hyprland/scripts/record.sh | 27 +- .../hyprland/scripts/start_geoclue_agent.sh | 27 + .config/hypr/hyprlock.conf | 2 +- .config/kde-material-you-colors/config.conf | 2 +- .../matugen/templates/hyprland/colors.conf | 4 +- .../matugen/templates/hyprland/hyprlock.conf | 6 +- .config/qt6ct/qt6ct.conf | 2 +- .config/quickshell/ii/.qmlformat.ini | 8 + .config/quickshell/{ => ii}/GlobalStates.qml | 30 +- .config/quickshell/{ => ii}/ReloadPopup.qml | 1 + .config/quickshell/ii/Translation.qml | 175 +++++ .../assets/icons/ai-openai-symbolic.svg | 0 .../{ => ii}/assets/icons/arch-symbolic.svg | 0 .../assets/icons/cachyos-symbolic.svg | 0 .../assets/icons/cloudflare-dns-symbolic.svg | 0 .../assets/icons/crosshair-symbolic.svg | 0 .../{ => ii}/assets/icons/debian-symbolic.svg | 0 .../assets/icons/deepseek-symbolic.svg | 0 .../assets/icons/desktop-symbolic.svg | 0 .../assets/icons/endeavouros-symbolic.svg | 0 .../{ => ii}/assets/icons/fedora-symbolic.svg | 0 .../assets/icons/flatpak-symbolic.svg | 0 .../{ => ii}/assets/icons/github-symbolic.svg | 0 .../assets/icons/google-gemini-symbolic.svg | 0 .../{ => ii}/assets/icons/linux-symbolic.svg | 0 .../assets/icons/microsoft-symbolic.svg | 0 .../{ => ii}/assets/icons/nixos-symbolic.svg | 0 .../{ => ii}/assets/icons/ollama-symbolic.svg | 0 .../{ => ii}/assets/icons/openai-symbolic.svg | 0 .../assets/icons/openrouter-symbolic.svg | 0 .../{ => ii}/assets/icons/spark-symbolic.svg | 0 .../{ => ii}/assets/icons/ubuntu-symbolic.svg | 0 .../assets/images/default_wallpaper.png | Bin .config/quickshell/ii/defaults/ai/README.md | 5 + .../ii/defaults/ai/prompts/NoPrompt.md | 0 .../ii/defaults/ai/prompts/ii-Default.md | 21 + .../ii/defaults/ai/prompts/ii-Imouto.md | 5 + .../ii/defaults/ai/prompts/nyarch-Acchan.md | 22 + .../ai/prompts/w-FourPointedSparkle.md | 15 + .../ai/prompts/w-OpenMechanicalFlower.md | 1 + .../ii/modules/background/Background.qml | 283 ++++++++ .../{ => ii}/modules/bar/ActiveWindow.qml | 19 +- .config/quickshell/ii/modules/bar/Bar.qml | 622 ++++++++++++++++++ .../{ => ii}/modules/bar/BarGroup.qml | 9 +- .../{ => ii}/modules/bar/BatteryIndicator.qml | 14 +- .../{ => ii}/modules/bar/CircleUtilButton.qml | 9 +- .../{ => ii}/modules/bar/ClockWidget.qml | 10 +- .../quickshell/{ => ii}/modules/bar/Media.qml | 20 +- .../{ => ii}/modules/bar/Resource.qml | 7 +- .../{ => ii}/modules/bar/Resources.qml | 15 +- .../{ => ii}/modules/bar/ScrollHint.qml | 7 +- .../{ => ii}/modules/bar/SysTray.qml | 7 +- .../{ => ii}/modules/bar/SysTrayItem.qml | 12 +- .../quickshell/ii/modules/bar/UtilButtons.qml | 107 +++ .../{ => ii}/modules/bar/Workspaces.qml | 80 ++- .../ii/modules/bar/weather/WeatherBar.qml | 60 ++ .../ii/modules/bar/weather/WeatherCard.qml | 43 ++ .../ii/modules/bar/weather/WeatherIcons.qml | 59 ++ .../ii/modules/bar/weather/WeatherPopup.qml | 97 +++ .../ii/modules/cheatsheet/Cheatsheet.qml | 236 +++++++ .../modules/cheatsheet/CheatsheetKeybinds.qml | 15 +- .../cheatsheet/CheatsheetPeriodicTable.qml | 68 ++ .../ii/modules/cheatsheet/ElementTile.qml | 55 ++ .../ii/modules/cheatsheet/periodic_table.js | 196 ++++++ .../{ => ii}/modules/common/Appearance.qml | 32 +- .../quickshell/ii/modules/common/Config.qml | 253 +++++++ .../{ => ii}/modules/common/Directories.qml | 26 +- .../ii/modules/common/Persistent.qml | 49 ++ .../modules/common/functions/ColorUtils.qml | 114 ++++ .../ii/modules/common/functions/FileUtils.qml | 41 ++ .../modules/common/functions/ObjectUtils.qml | 98 +++ .../modules/common/functions/StringUtils.qml | 221 +++++++ .../modules/common/functions/fuzzysort.js | 0 .../modules/common/functions/levendist.js | 0 .../modules/common/widgets/ButtonGroup.qml | 7 +- .../common/widgets/CircularProgress.qml | 4 +- .../modules/common/widgets/CliphistImage.qml | 17 +- .../ii/modules/common/widgets/ConfigRow.qml | 8 + .../common/widgets/ConfigSelectionArray.qml | 43 ++ .../modules/common/widgets/ConfigSpinBox.qml | 30 + .../modules/common/widgets/ConfigSwitch.qml | 32 + .../ii/modules/common/widgets/ContentPage.qml | 28 + .../modules/common/widgets/ContentSection.qml | 23 + .../common/widgets/ContentSubsection.qml | 46 ++ .../common/widgets/ContentSubsectionLabel.qml | 10 + .../ii/modules/common/widgets/CustomIcon.qml | 37 ++ .../modules/common/widgets/DialogButton.qml | 7 +- .../modules/common/widgets/DragManager.qml | 6 +- .../modules/common/widgets/Favicon.qml | 15 +- .../common/widgets/FloatingActionButton.qml | 15 +- .../common/widgets/FlowButtonGroup.qml | 0 .../modules/common/widgets/GroupButton.qml | 21 +- .../modules/common/widgets/KeyboardKey.qml | 12 +- .../widgets/LightDarkPreferenceButton.qml | 122 ++++ .../modules/common/widgets/MaterialSymbol.qml | 17 +- .../common/widgets/MaterialTextField.qml | 52 ++ .../modules/common/widgets/MenuButton.qml | 7 +- .../modules/common/widgets/NavigationRail.qml | 5 +- .../common/widgets/NavigationRailButton.qml | 43 +- .../widgets/NavigationRailExpandButton.qml | 11 +- .../common/widgets/NavigationRailTabArray.qml | 41 ++ .../widgets/NotificationActionButton.qml | 7 +- .../common/widgets/NotificationAppIcon.qml | 4 +- .../common/widgets/NotificationGroup.qml | 16 +- .../widgets/NotificationGroupExpandButton.qml | 10 +- .../common/widgets/NotificationItem.qml | 42 +- .../common/widgets/NotificationListView.qml | 12 +- .../widgets/PointingHandInteraction.qml | 0 .../common/widgets/PointingHandLinkHover.qml | 8 + .../modules/common/widgets/PrimaryTabBar.qml | 8 +- .../common/widgets/PrimaryTabButton.qml | 8 +- .../modules/common/widgets/Revealer.qml | 3 +- .../modules/common/widgets/RippleButton.qml | 9 +- .../common/widgets/RippleButtonWithIcon.qml | 55 ++ .../modules/common/widgets/RoundCorner.qml | 27 +- .../common/widgets/SecondaryTabButton.qml | 8 +- .../common/widgets/SelectionDialog.qml | 14 +- .../common/widgets/SelectionGroupButton.qml | 6 +- .../modules/common/widgets/StyledLabel.qml | 3 +- .../modules/common/widgets/StyledListView.qml | 13 +- .../common/widgets/StyledProgressBar.qml | 10 +- .../common/widgets/StyledRadioButton.qml | 9 +- .../widgets/StyledRectangularShadow.qml | 2 +- .../modules/common/widgets/StyledSlider.qml | 155 +++++ .../modules/common/widgets/StyledSpinBox.qml | 92 +++ .../modules/common/widgets/StyledSwitch.qml | 8 +- .../modules/common/widgets/StyledText.qml | 2 +- .../modules/common/widgets/StyledTextArea.qml | 5 +- .../common/widgets/StyledTextInput.qml | 17 + .../modules/common/widgets/StyledToolTip.qml | 4 +- .../common/widgets/VerticalButtonGroup.qml | 6 +- .../modules/common/widgets/WaveVisualizer.qml | 11 +- .../common/widgets/notification_utils.js | 0 .config/quickshell/ii/modules/dock/Dock.qml | 148 +++++ .../{ => ii}/modules/dock/DockAppButton.qml | 42 +- .../{ => ii}/modules/dock/DockApps.qml | 21 +- .../{ => ii}/modules/dock/DockButton.qml | 6 +- .../{ => ii}/modules/dock/DockSeparator.qml | 4 +- .config/quickshell/ii/modules/lock/Lock.qml | 99 +++ .../ii/modules/lock/LockContext.qml | 67 ++ .../ii/modules/lock/LockSurface.qml | 147 +++++ .../ii/modules/lock/pam/password.conf | 1 + .../modules/mediaControls/MediaControls.qml | 29 +- .../modules/mediaControls/PlayerControl.qml | 18 +- .../notificationPopup/NotificationPopup.qml | 21 +- .../OnScreenDisplayBrightness.qml | 19 +- .../onScreenDisplay/OnScreenDisplayVolume.qml | 19 +- .../onScreenDisplay/OsdValueIndicator.qml | 6 +- .../onScreenKeyboard/OnScreenKeyboard.qml | 21 +- .../modules/onScreenKeyboard/OskContent.qml | 10 +- .../modules/onScreenKeyboard/OskKey.qml | 12 +- .../modules/onScreenKeyboard/layouts.js | 0 .../{ => ii}/modules/overview/Overview.qml | 158 ++--- .../modules/overview/OverviewWidget.qml | 59 +- .../modules/overview/OverviewWindow.qml | 33 +- .../{ => ii}/modules/overview/SearchItem.qml | 89 ++- .../modules/overview/SearchWidget.qml | 230 ++++--- .../modules/screenCorners/ScreenCorners.qml | 66 ++ .../{ => ii}/modules/session/Session.qml | 65 +- .../modules/session/SessionActionButton.qml | 8 +- .../quickshell/ii/modules/settings/About.qml | 149 +++++ .../ii/modules/settings/AdvancedConfig.qml | 45 ++ .../ii/modules/settings/InterfaceConfig.qml | 374 +++++++++++ .../ii/modules/settings/ServicesConfig.qml | 233 +++++++ .../ii/modules/settings/StyleConfig.qml | 245 +++++++ .../{ => ii}/modules/sidebarLeft/AiChat.qml | 171 +++-- .../{ => ii}/modules/sidebarLeft/Anime.qml | 88 +-- .../modules/sidebarLeft/ApiCommandButton.qml | 11 +- .../ii/modules/sidebarLeft/DescriptionBox.qml | 62 ++ .../modules/sidebarLeft/SidebarLeft.qml | 23 +- .../sidebarLeft/SidebarLeftContent.qml | 25 +- .../modules/sidebarLeft/Translator.qml | 32 +- .../modules/sidebarLeft/aiChat/AiMessage.qml | 41 +- .../aiChat/AiMessageControlButton.qml | 10 +- .../aiChat/AnnotationSourceButton.qml | 13 +- .../sidebarLeft/aiChat/MessageCodeBlock.qml | 32 +- .../sidebarLeft/aiChat/MessageTextBlock.qml | 17 +- .../sidebarLeft/aiChat/MessageThinkBlock.qml | 21 +- .../modules/sidebarLeft/anime/BooruImage.qml | 21 +- .../sidebarLeft/anime/BooruResponse.qml | 23 +- .../translator/LanguageSelectorButton.qml | 14 +- .../sidebarLeft/translator/TextCanvas.qml | 15 +- .../sidebarRight/BottomWidgetGroup.qml | 37 +- .../sidebarRight/CenterWidgetGroup.qml | 12 +- .../modules/sidebarRight/SidebarRight.qml | 40 +- .../calendar/CalendarDayButton.qml | 6 +- .../calendar/CalendarHeaderButton.qml | 6 +- .../sidebarRight/calendar/CalendarWidget.qml | 9 +- .../sidebarRight/calendar/calendar_layout.js | 0 .../notifications/NotificationList.qml | 17 +- .../NotificationStatusButton.qml | 5 +- .../quickToggles/BluetoothToggle.qml | 18 +- .../quickToggles/CloudflareWarp.qml | 92 +++ .../quickToggles/EasyEffectsToggle.qml | 49 ++ .../sidebarRight/quickToggles/GameMode.qml | 31 + .../quickToggles/IdleInhibitor.qml | 15 +- .../quickToggles/NetworkToggle.qml | 13 +- .../sidebarRight/quickToggles/NightLight.qml | 28 + .../quickToggles/QuickToggleButton.qml | 12 +- .../modules/sidebarRight/todo/TaskList.qml | 6 +- .../todo/TodoItemActionButton.qml | 7 +- .../modules/sidebarRight/todo/TodoWidget.qml | 23 +- .../volumeMixer/AudioDeviceSelectorButton.qml | 14 +- .../sidebarRight/volumeMixer/VolumeMixer.qml | 86 ++- .../volumeMixer/VolumeMixerEntry.qml | 63 ++ .config/quickshell/ii/screenshot.qml | 552 ++++++++++++++++ .../ai/show-installed-ollama-models.sh | 0 .../scripts/cava/raw_output_config.txt | 0 .../{ => ii}/scripts/colors/applycolor.sh | 22 +- .../colors/generate_colors_material.py | 0 .../ii/scripts/colors/random_konachan_wall.sh | 42 ++ .../scripts/colors/scheme_for_image.py | 0 .../{ => ii}/scripts/colors/switchwall.sh | 71 +- .../scripts/colors}/terminal/scheme-base.json | 0 .../scripts/colors}/terminal/sequences.txt | 0 .../{ => ii}/scripts/hyprland/get_keybinds.py | 0 .../ii/scripts/images/find_regions.py | 120 ++++ .../ii/scripts/images}/least_busy_region.py | 31 +- .../{ => ii}/scripts/kvantum/adwsvg.py | 0 .../{ => ii}/scripts/kvantum/adwsvgDark.py | 0 .../scripts/kvantum/changeAdwColors.py | 0 .../{ => ii}/scripts/kvantum/materialQT.sh | 4 +- .../scripts/wayland-idle-inhibitor.py | 0 .config/quickshell/{ => ii}/services/Ai.qml | 283 ++++++-- .../{ => ii}/services/AiMessageData.qml | 3 +- .../{ => ii}/services/AppSearch.qml | 79 ++- .../quickshell/{ => ii}/services/Audio.qml | 8 +- .config/quickshell/ii/services/Battery.qml | 50 ++ .../{ => ii}/services/Bluetooth.qml | 0 .../quickshell/{ => ii}/services/Booru.qml | 45 +- .../{ => ii}/services/BooruResponseData.qml | 2 +- .../{ => ii}/services/Brightness.qml | 4 +- .../quickshell/{ => ii}/services/Cliphist.qml | 30 +- .../quickshell/{ => ii}/services/DateTime.qml | 15 +- .../quickshell/{ => ii}/services/Emojis.qml | 2 +- .../{ => ii}/services/FirstRunExperience.qml | 17 +- .../quickshell/ii/services/HyprlandData.qml | 133 ++++ .../{ => ii}/services/HyprlandKeybinds.qml | 9 +- .../{ => ii}/services/HyprlandXkb.qml | 0 .config/quickshell/ii/services/Hyprsunset.qml | 117 ++++ .../{ => ii}/services/KeyringStorage.qml | 11 +- .../{ => ii}/services/LatexRenderer.qml | 8 +- .../{ => ii}/services/MaterialThemeLoader.qml | 4 +- .../{ => ii}/services/MprisController.qml | 7 +- .../quickshell/{ => ii}/services/Network.qml | 0 .../{ => ii}/services/Notifications.qml | 57 +- .../{ => ii}/services/ResourceUsage.qml | 4 +- .../{ => ii}/services/SystemInfo.qml | 29 +- .config/quickshell/{ => ii}/services/Todo.qml | 3 +- .config/quickshell/ii/services/Weather.qml | 154 +++++ .../quickshell/{ => ii}/services/Ydotool.qml | 37 +- .config/quickshell/ii/settings.qml | 247 +++++++ .config/quickshell/{ => ii}/shell.qml | 18 +- .config/quickshell/ii/welcome.qml | 341 ++++++++++ .../backgroundWidgets/BackgroundWidgets.qml | 139 ---- .config/quickshell/modules/bar/Bar.qml | 490 -------------- .../quickshell/modules/bar/UtilButtons.qml | 85 --- .../modules/cheatsheet/Cheatsheet.qml | 165 ----- .../modules/common/ConfigOptions.qml | 164 ----- .../modules/common/PersistentStates.qml | 23 - .../modules/common/functions/color_utils.js | 109 --- .../modules/common/functions/file_utils.js | 9 - .../modules/common/functions/object_utils.js | 91 --- .../modules/common/functions/string_utils.js | 188 ------ .../modules/common/widgets/CustomIcon.qml | 24 - .../modules/common/widgets/NavRailButton.qml | 66 -- .../modules/common/widgets/StyledSlider.qml | 113 ---- .config/quickshell/modules/dock/Dock.qml | 148 ----- .../modules/screenCorners/ScreenCorners.qml | 87 --- .../sidebarRight/quickToggles/GameMode.qml | 26 - .../sidebarRight/quickToggles/NightLight.qml | 42 -- .../volumeMixer/VolumeMixerEntry.qml | 65 -- .../scripts/colors/random_konachan_wall.sh | 10 - .config/quickshell/services/Battery.qml | 30 - .config/quickshell/services/ConfigLoader.qml | 138 ---- .config/quickshell/services/HyprlandData.qml | 69 -- .../services/PersistentStateManager.qml | 105 --- .config/quickshell/settings.qml | 162 ----- .config/quickshell/translations/en_US.json | 314 +++++++++ .../quickshell/translations/tools/README.md | 285 ++++++++ .../guide/translation-tools-guide-zh_CN.md | 286 ++++++++ .../tools/guide/translation-tools-guide.md | 285 ++++++++ .../translations/tools/manage-translations.sh | 149 +++++ .../translations/tools/translation-cleaner.py | 200 ++++++ .../translations/tools/translation-manager.py | 324 +++++++++ .config/quickshell/translations/vi_VN.json | 335 ++++++++++ .config/quickshell/translations/zh_CN.json | 314 +++++++++ .config/quickshell/welcome.qml | 561 ---------------- .config/wlogout/layout | 8 +- .github/assets/illogical-impulse.svg | 122 ++++ .gitignore | 1 + .local/share/icons/illogical-impulse.svg | 124 ++++ .local/share/icons/quickshell.svg | 1 + CONTRIBUTING.md | 37 +- README.md | 97 +-- .../illogical-impulse-backlight/PKGBUILD | 1 - .../illogical-impulse-fonts-themes/PKGBUILD | 3 + arch-packages/illogical-impulse-kde/PKGBUILD | 1 + .../illogical-impulse-portal/PKGBUILD | 1 + .../illogical-impulse-screencapture/PKGBUILD | 7 +- .../illogical-impulse-toolkit/PKGBUILD | 1 + .../illogical-impulse-widgets/PKGBUILD | 3 +- install.sh | 8 +- scriptdata/previous_dependencies.conf | 1 + 319 files changed, 12426 insertions(+), 4803 deletions(-) delete mode 100644 .config/anyrun/config.ron delete mode 100644 .config/anyrun/style.css create mode 100644 .config/hypr/hyprland/scripts/ai/license_show-loaded-ollama-models.txt create mode 100755 .config/hypr/hyprland/scripts/ai/primary-buffer-query.sh create mode 100755 .config/hypr/hyprland/scripts/ai/show-loaded-ollama-models.sh create mode 100755 .config/hypr/hyprland/scripts/start_geoclue_agent.sh create mode 100644 .config/quickshell/ii/.qmlformat.ini rename .config/quickshell/{ => ii}/GlobalStates.qml (54%) rename .config/quickshell/{ => ii}/ReloadPopup.qml (99%) create mode 100644 .config/quickshell/ii/Translation.qml rename .config/quickshell/{ => ii}/assets/icons/ai-openai-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/arch-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/cachyos-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/cloudflare-dns-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/crosshair-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/debian-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/deepseek-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/desktop-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/endeavouros-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/fedora-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/flatpak-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/github-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/google-gemini-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/linux-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/microsoft-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/nixos-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/ollama-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/openai-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/openrouter-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/spark-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/icons/ubuntu-symbolic.svg (100%) rename .config/quickshell/{ => ii}/assets/images/default_wallpaper.png (100%) create mode 100644 .config/quickshell/ii/defaults/ai/README.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/NoPrompt.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/ii-Default.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md create mode 100644 .config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md create mode 100644 .config/quickshell/ii/modules/background/Background.qml rename .config/quickshell/{ => ii}/modules/bar/ActiveWindow.qml (52%) create mode 100644 .config/quickshell/ii/modules/bar/Bar.qml rename .config/quickshell/{ => ii}/modules/bar/BarGroup.qml (76%) rename .config/quickshell/{ => ii}/modules/bar/BatteryIndicator.qml (90%) rename .config/quickshell/{ => ii}/modules/bar/CircleUtilButton.qml (55%) rename .config/quickshell/{ => ii}/modules/bar/ClockWidget.qml (79%) rename .config/quickshell/{ => ii}/modules/bar/Media.qml (87%) rename .config/quickshell/{ => ii}/modules/bar/Resource.qml (93%) rename .config/quickshell/{ => ii}/modules/bar/Resources.qml (74%) rename .config/quickshell/{ => ii}/modules/bar/ScrollHint.qml (93%) rename .config/quickshell/{ => ii}/modules/bar/SysTray.qml (86%) rename .config/quickshell/{ => ii}/modules/bar/SysTrayItem.qml (84%) create mode 100644 .config/quickshell/ii/modules/bar/UtilButtons.qml rename .config/quickshell/{ => ii}/modules/bar/Workspaces.qml (78%) create mode 100644 .config/quickshell/ii/modules/bar/weather/WeatherBar.qml create mode 100644 .config/quickshell/ii/modules/bar/weather/WeatherCard.qml create mode 100644 .config/quickshell/ii/modules/bar/weather/WeatherIcons.qml create mode 100644 .config/quickshell/ii/modules/bar/weather/WeatherPopup.qml create mode 100644 .config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml rename .config/quickshell/{ => ii}/modules/cheatsheet/CheatsheetKeybinds.qml (95%) create mode 100644 .config/quickshell/ii/modules/cheatsheet/CheatsheetPeriodicTable.qml create mode 100644 .config/quickshell/ii/modules/cheatsheet/ElementTile.qml create mode 100644 .config/quickshell/ii/modules/cheatsheet/periodic_table.js rename .config/quickshell/{ => ii}/modules/common/Appearance.qml (90%) create mode 100644 .config/quickshell/ii/modules/common/Config.qml rename .config/quickshell/{ => ii}/modules/common/Directories.qml (62%) create mode 100644 .config/quickshell/ii/modules/common/Persistent.qml create mode 100644 .config/quickshell/ii/modules/common/functions/ColorUtils.qml create mode 100644 .config/quickshell/ii/modules/common/functions/FileUtils.qml create mode 100644 .config/quickshell/ii/modules/common/functions/ObjectUtils.qml create mode 100644 .config/quickshell/ii/modules/common/functions/StringUtils.qml rename .config/quickshell/{ => ii}/modules/common/functions/fuzzysort.js (100%) rename .config/quickshell/{ => ii}/modules/common/functions/levendist.js (100%) rename .config/quickshell/{ => ii}/modules/common/widgets/ButtonGroup.qml (89%) rename .config/quickshell/{ => ii}/modules/common/widgets/CircularProgress.qml (96%) rename .config/quickshell/{ => ii}/modules/common/widgets/CliphistImage.qml (84%) create mode 100644 .config/quickshell/ii/modules/common/widgets/ConfigRow.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ConfigSelectionArray.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ConfigSpinBox.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ContentPage.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ContentSection.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ContentSubsection.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/ContentSubsectionLabel.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/CustomIcon.qml rename .config/quickshell/{ => ii}/modules/common/widgets/DialogButton.qml (83%) rename .config/quickshell/{ => ii}/modules/common/widgets/DragManager.qml (94%) rename .config/quickshell/{ => ii}/modules/common/widgets/Favicon.qml (76%) rename .config/quickshell/{ => ii}/modules/common/widgets/FloatingActionButton.qml (80%) rename .config/quickshell/{ => ii}/modules/common/widgets/FlowButtonGroup.qml (100%) rename .config/quickshell/{ => ii}/modules/common/widgets/GroupButton.qml (92%) rename .config/quickshell/{ => ii}/modules/common/widgets/KeyboardKey.qml (81%) create mode 100644 .config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml rename .config/quickshell/{ => ii}/modules/common/widgets/MaterialSymbol.qml (78%) create mode 100644 .config/quickshell/ii/modules/common/widgets/MaterialTextField.qml rename .config/quickshell/{ => ii}/modules/common/widgets/MenuButton.qml (78%) rename .config/quickshell/{ => ii}/modules/common/widgets/NavigationRail.qml (68%) rename .config/quickshell/{ => ii}/modules/common/widgets/NavigationRailButton.qml (70%) rename .config/quickshell/{ => ii}/modules/common/widgets/NavigationRailExpandButton.qml (70%) create mode 100644 .config/quickshell/ii/modules/common/widgets/NavigationRailTabArray.qml rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationActionButton.qml (89%) rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationAppIcon.qml (97%) rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationGroup.qml (95%) rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationGroupExpandButton.qml (87%) rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationItem.qml (90%) rename .config/quickshell/{ => ii}/modules/common/widgets/NotificationListView.qml (74%) rename .config/quickshell/{ => ii}/modules/common/widgets/PointingHandInteraction.qml (100%) create mode 100644 .config/quickshell/ii/modules/common/widgets/PointingHandLinkHover.qml rename .config/quickshell/{ => ii}/modules/common/widgets/PrimaryTabBar.qml (91%) rename .config/quickshell/{ => ii}/modules/common/widgets/PrimaryTabButton.qml (96%) rename .config/quickshell/{ => ii}/modules/common/widgets/Revealer.qml (93%) rename .config/quickshell/{ => ii}/modules/common/widgets/RippleButton.qml (96%) create mode 100644 .config/quickshell/ii/modules/common/widgets/RippleButtonWithIcon.qml rename .config/quickshell/{ => ii}/modules/common/widgets/RoundCorner.qml (68%) rename .config/quickshell/{ => ii}/modules/common/widgets/SecondaryTabButton.qml (96%) rename .config/quickshell/{ => ii}/modules/common/widgets/SelectionDialog.qml (94%) rename .config/quickshell/{ => ii}/modules/common/widgets/SelectionGroupButton.qml (87%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledLabel.qml (88%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledListView.qml (93%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledProgressBar.qml (94%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledRadioButton.qml (94%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledRectangularShadow.qml (90%) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledSlider.qml create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledSpinBox.qml rename .config/quickshell/{ => ii}/modules/common/widgets/StyledSwitch.qml (81%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledText.qml (93%) rename .config/quickshell/{ => ii}/modules/common/widgets/StyledTextArea.qml (80%) create mode 100644 .config/quickshell/ii/modules/common/widgets/StyledTextInput.qml rename .config/quickshell/{ => ii}/modules/common/widgets/StyledToolTip.qml (96%) rename .config/quickshell/{ => ii}/modules/common/widgets/VerticalButtonGroup.qml (90%) rename .config/quickshell/{ => ii}/modules/common/widgets/WaveVisualizer.qml (88%) rename .config/quickshell/{ => ii}/modules/common/widgets/notification_utils.js (100%) create mode 100644 .config/quickshell/ii/modules/dock/Dock.qml rename .config/quickshell/{ => ii}/modules/dock/DockAppButton.qml (73%) rename .config/quickshell/{ => ii}/modules/dock/DockApps.qml (95%) rename .config/quickshell/{ => ii}/modules/dock/DockButton.qml (83%) rename .config/quickshell/{ => ii}/modules/dock/DockSeparator.qml (89%) create mode 100644 .config/quickshell/ii/modules/lock/Lock.qml create mode 100644 .config/quickshell/ii/modules/lock/LockContext.qml create mode 100644 .config/quickshell/ii/modules/lock/LockSurface.qml create mode 100644 .config/quickshell/ii/modules/lock/pam/password.conf rename .config/quickshell/{ => ii}/modules/mediaControls/MediaControls.qml (88%) rename .config/quickshell/{ => ii}/modules/mediaControls/PlayerControl.qml (96%) rename .config/quickshell/{ => ii}/modules/notificationPopup/NotificationPopup.qml (70%) rename .config/quickshell/{ => ii}/modules/onScreenDisplay/OnScreenDisplayBrightness.qml (90%) rename .config/quickshell/{ => ii}/modules/onScreenDisplay/OnScreenDisplayVolume.qml (94%) rename .config/quickshell/{ => ii}/modules/onScreenDisplay/OsdValueIndicator.qml (97%) rename .config/quickshell/{ => ii}/modules/onScreenKeyboard/OnScreenKeyboard.qml (89%) rename .config/quickshell/{ => ii}/modules/onScreenKeyboard/OskContent.qml (85%) rename .config/quickshell/{ => ii}/modules/onScreenKeyboard/OskKey.qml (94%) rename .config/quickshell/{ => ii}/modules/onScreenKeyboard/layouts.js (100%) rename .config/quickshell/{ => ii}/modules/overview/Overview.qml (54%) rename .config/quickshell/{ => ii}/modules/overview/OverviewWidget.qml (86%) rename .config/quickshell/{ => ii}/modules/overview/OverviewWindow.qml (71%) rename .config/quickshell/{ => ii}/modules/overview/SearchItem.qml (78%) rename .config/quickshell/{ => ii}/modules/overview/SearchWidget.qml (66%) create mode 100644 .config/quickshell/ii/modules/screenCorners/ScreenCorners.qml rename .config/quickshell/{ => ii}/modules/session/Session.qml (74%) rename .config/quickshell/{ => ii}/modules/session/SessionActionButton.qml (89%) create mode 100644 .config/quickshell/ii/modules/settings/About.qml create mode 100644 .config/quickshell/ii/modules/settings/AdvancedConfig.qml create mode 100644 .config/quickshell/ii/modules/settings/InterfaceConfig.qml create mode 100644 .config/quickshell/ii/modules/settings/ServicesConfig.qml create mode 100644 .config/quickshell/ii/modules/settings/StyleConfig.qml rename .config/quickshell/{ => ii}/modules/sidebarLeft/AiChat.qml (74%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/Anime.qml (87%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/ApiCommandButton.qml (73%) create mode 100644 .config/quickshell/ii/modules/sidebarLeft/DescriptionBox.qml rename .config/quickshell/{ => ii}/modules/sidebarLeft/SidebarLeft.qml (92%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/SidebarLeftContent.qml (79%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/Translator.qml (89%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/AiMessage.qml (89%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/AiMessageControlButton.qml (73%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml (82%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/MessageCodeBlock.qml (91%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/MessageTextBlock.qml (93%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/aiChat/MessageThinkBlock.qml (92%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/anime/BooruImage.qml (89%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/anime/BooruResponse.qml (94%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/translator/LanguageSelectorButton.qml (80%) rename .config/quickshell/{ => ii}/modules/sidebarLeft/translator/TextCanvas.qml (89%) rename .config/quickshell/{ => ii}/modules/sidebarRight/BottomWidgetGroup.qml (84%) rename .config/quickshell/{ => ii}/modules/sidebarRight/CenterWidgetGroup.qml (91%) rename .config/quickshell/{ => ii}/modules/sidebarRight/SidebarRight.qml (87%) rename .config/quickshell/{ => ii}/modules/sidebarRight/calendar/CalendarDayButton.qml (83%) rename .config/quickshell/{ => ii}/modules/sidebarRight/calendar/CalendarHeaderButton.qml (89%) rename .config/quickshell/{ => ii}/modules/sidebarRight/calendar/CalendarWidget.qml (94%) rename .config/quickshell/{ => ii}/modules/sidebarRight/calendar/calendar_layout.js (100%) rename .config/quickshell/{ => ii}/modules/sidebarRight/notifications/NotificationList.qml (90%) rename .config/quickshell/{ => ii}/modules/sidebarRight/notifications/NotificationStatusButton.qml (92%) rename .config/quickshell/{ => ii}/modules/sidebarRight/quickToggles/BluetoothToggle.qml (62%) create mode 100644 .config/quickshell/ii/modules/sidebarRight/quickToggles/CloudflareWarp.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/quickToggles/EasyEffectsToggle.qml create mode 100644 .config/quickshell/ii/modules/sidebarRight/quickToggles/GameMode.qml rename .config/quickshell/{ => ii}/modules/sidebarRight/quickToggles/IdleInhibitor.qml (51%) rename .config/quickshell/{ => ii}/modules/sidebarRight/quickToggles/NetworkToggle.qml (64%) create mode 100644 .config/quickshell/ii/modules/sidebarRight/quickToggles/NightLight.qml rename .config/quickshell/{ => ii}/modules/sidebarRight/quickToggles/QuickToggleButton.qml (72%) rename .config/quickshell/{ => ii}/modules/sidebarRight/todo/TaskList.qml (98%) rename .config/quickshell/{ => ii}/modules/sidebarRight/todo/TodoItemActionButton.qml (78%) rename .config/quickshell/{ => ii}/modules/sidebarRight/todo/TodoWidget.qml (93%) rename .config/quickshell/{ => ii}/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml (82%) rename .config/quickshell/{ => ii}/modules/sidebarRight/volumeMixer/VolumeMixer.qml (81%) create mode 100644 .config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml create mode 100644 .config/quickshell/ii/screenshot.qml rename .config/quickshell/{ => ii}/scripts/ai/show-installed-ollama-models.sh (100%) rename .config/quickshell/{ => ii}/scripts/cava/raw_output_config.txt (100%) rename .config/quickshell/{ => ii}/scripts/colors/applycolor.sh (71%) rename .config/quickshell/{ => ii}/scripts/colors/generate_colors_material.py (100%) create mode 100755 .config/quickshell/ii/scripts/colors/random_konachan_wall.sh rename .config/quickshell/{ => ii}/scripts/colors/scheme_for_image.py (100%) rename .config/quickshell/{ => ii}/scripts/colors/switchwall.sh (84%) rename .config/quickshell/{scripts => ii/scripts/colors}/terminal/scheme-base.json (100%) rename .config/quickshell/{scripts => ii/scripts/colors}/terminal/sequences.txt (100%) rename .config/quickshell/{ => ii}/scripts/hyprland/get_keybinds.py (100%) create mode 100755 .config/quickshell/ii/scripts/images/find_regions.py rename .config/{matugen/scripts => quickshell/ii/scripts/images}/least_busy_region.py (92%) rename .config/quickshell/{ => ii}/scripts/kvantum/adwsvg.py (100%) rename .config/quickshell/{ => ii}/scripts/kvantum/adwsvgDark.py (100%) rename .config/quickshell/{ => ii}/scripts/kvantum/changeAdwColors.py (100%) rename .config/quickshell/{ => ii}/scripts/kvantum/materialQT.sh (89%) rename .config/quickshell/{ => ii}/scripts/wayland-idle-inhibitor.py (100%) rename .config/quickshell/{ => ii}/services/Ai.qml (69%) rename .config/quickshell/{ => ii}/services/AiMessageData.qml (89%) rename .config/quickshell/{ => ii}/services/AppSearch.qml (57%) rename .config/quickshell/{ => ii}/services/Audio.qml (85%) create mode 100644 .config/quickshell/ii/services/Battery.qml rename .config/quickshell/{ => ii}/services/Bluetooth.qml (100%) rename .config/quickshell/{ => ii}/services/Booru.qml (88%) rename .config/quickshell/{ => ii}/services/BooruResponseData.qml (85%) rename .config/quickshell/{ => ii}/services/Brightness.qml (97%) rename .config/quickshell/{ => ii}/services/Cliphist.qml (69%) rename .config/quickshell/{ => ii}/services/DateTime.qml (86%) rename .config/quickshell/{ => ii}/services/Emojis.qml (98%) rename .config/quickshell/{ => ii}/services/FirstRunExperience.qml (58%) create mode 100644 .config/quickshell/ii/services/HyprlandData.qml rename .config/quickshell/{ => ii}/services/HyprlandKeybinds.qml (90%) rename .config/quickshell/{ => ii}/services/HyprlandXkb.qml (100%) create mode 100644 .config/quickshell/ii/services/Hyprsunset.qml rename .config/quickshell/{ => ii}/services/KeyringStorage.qml (92%) rename .config/quickshell/{ => ii}/services/LatexRenderer.qml (93%) rename .config/quickshell/{ => ii}/services/MaterialThemeLoader.qml (93%) rename .config/quickshell/{ => ii}/services/MprisController.qml (94%) rename .config/quickshell/{ => ii}/services/Network.qml (100%) rename .config/quickshell/{ => ii}/services/Notifications.qml (80%) rename .config/quickshell/{ => ii}/services/ResourceUsage.qml (95%) rename .config/quickshell/{ => ii}/services/SystemInfo.qml (59%) rename .config/quickshell/{ => ii}/services/Todo.qml (97%) create mode 100644 .config/quickshell/ii/services/Weather.qml rename .config/quickshell/{ => ii}/services/Ydotool.qml (51%) create mode 100644 .config/quickshell/ii/settings.qml rename .config/quickshell/{ => ii}/shell.qml (86%) create mode 100644 .config/quickshell/ii/welcome.qml delete mode 100644 .config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml delete mode 100644 .config/quickshell/modules/bar/Bar.qml delete mode 100644 .config/quickshell/modules/bar/UtilButtons.qml delete mode 100644 .config/quickshell/modules/cheatsheet/Cheatsheet.qml delete mode 100644 .config/quickshell/modules/common/ConfigOptions.qml delete mode 100644 .config/quickshell/modules/common/PersistentStates.qml delete mode 100644 .config/quickshell/modules/common/functions/color_utils.js delete mode 100644 .config/quickshell/modules/common/functions/file_utils.js delete mode 100644 .config/quickshell/modules/common/functions/object_utils.js delete mode 100644 .config/quickshell/modules/common/functions/string_utils.js delete mode 100644 .config/quickshell/modules/common/widgets/CustomIcon.qml delete mode 100644 .config/quickshell/modules/common/widgets/NavRailButton.qml delete mode 100644 .config/quickshell/modules/common/widgets/StyledSlider.qml delete mode 100644 .config/quickshell/modules/dock/Dock.qml delete mode 100644 .config/quickshell/modules/screenCorners/ScreenCorners.qml delete mode 100644 .config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml delete mode 100644 .config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml delete mode 100644 .config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml delete mode 100755 .config/quickshell/scripts/colors/random_konachan_wall.sh delete mode 100644 .config/quickshell/services/Battery.qml delete mode 100644 .config/quickshell/services/ConfigLoader.qml delete mode 100644 .config/quickshell/services/HyprlandData.qml delete mode 100644 .config/quickshell/services/PersistentStateManager.qml delete mode 100644 .config/quickshell/settings.qml create mode 100644 .config/quickshell/translations/en_US.json create mode 100644 .config/quickshell/translations/tools/README.md create mode 100644 .config/quickshell/translations/tools/guide/translation-tools-guide-zh_CN.md create mode 100644 .config/quickshell/translations/tools/guide/translation-tools-guide.md create mode 100755 .config/quickshell/translations/tools/manage-translations.sh create mode 100755 .config/quickshell/translations/tools/translation-cleaner.py create mode 100755 .config/quickshell/translations/tools/translation-manager.py create mode 100644 .config/quickshell/translations/vi_VN.json create mode 100644 .config/quickshell/translations/zh_CN.json delete mode 100644 .config/quickshell/welcome.qml create mode 100644 .github/assets/illogical-impulse.svg create mode 100644 .local/share/icons/illogical-impulse.svg create mode 120000 .local/share/icons/quickshell.svg diff --git a/.config/anyrun/config.ron b/.config/anyrun/config.ron deleted file mode 100644 index c3eafd7a..00000000 --- a/.config/anyrun/config.ron +++ /dev/null @@ -1,13 +0,0 @@ -Config( - x: Fraction(0.500000), - y: Absolute(15), - width: Fraction(0.300000), - height: Absolute(0), - hide_icons: false, - ignore_exclusive_zones: false, - layer: Overlay, - hide_plugin_info: true, - close_on_click: true, - show_results_immediately: false, - max_entries: None, -) diff --git a/.config/anyrun/style.css b/.config/anyrun/style.css deleted file mode 100644 index ca45f260..00000000 --- a/.config/anyrun/style.css +++ /dev/null @@ -1,66 +0,0 @@ -* { - all: unset; - font-size: 1.3rem; -} - -#window, -#match, -#entry, -#plugin, -#main { - background: transparent; -} - -#match.activatable { - border-radius: 16px; - padding: 0.3rem 0.9rem; - margin-top: 0.01rem; -} -#match.activatable:first-child { - margin-top: 0.7rem; -} -#match.activatable:last-child { - margin-bottom: 0.6rem; -} - -#plugin:hover #match.activatable { - border-radius: 10px; - padding: 0.3rem; - margin-top: 0.01rem; - margin-bottom: 0; -} - -#match:selected, -#match:hover, -#plugin:hover { - background: #2e3131; -} - -#entry { - background: #0b0f10; - border: 1px solid #0b0f10; - border-radius: 16px; - margin: 0.5rem; - padding: 0.3rem 1rem; -} - -list > #plugin { - border-radius: 16px; - margin: 0 0.3rem; -} -list > #plugin:first-child { - margin-top: 0.3rem; -} -list > #plugin:last-child { - margin-bottom: 0.3rem; -} -list > #plugin:hover { - padding: 0.6rem; -} - -box#main { - background: #0b0f10; - box-shadow: inset 0 0 0 1px #0b0f10, 0 0 0 1px #0b0f10; - border-radius: 24px; - padding: 0.3rem; -} diff --git a/.config/chrome-flags.conf b/.config/chrome-flags.conf index 592cd96f..4f1b5186 100644 --- a/.config/chrome-flags.conf +++ b/.config/chrome-flags.conf @@ -1,5 +1,5 @@ --password-store=gnome-libsecret -# --ozone-platform-hint=wayland +--ozone-platform-hint=wayland --gtk-version=4 --ignore-gpu-blocklist --enable-features=TouchpadOverscrollHistoryNavigation diff --git a/.config/code-flags.conf b/.config/code-flags.conf index 49624fa7..0786a0c6 100644 --- a/.config/code-flags.conf +++ b/.config/code-flags.conf @@ -1,4 +1,4 @@ -# --ozone-platform-hint=wayland +--ozone-platform-hint=wayland --gtk-version=4 --ignore-gpu-blocklist --enable-features=TouchpadOverscrollHistoryNavigation diff --git a/.config/fish/config.fish b/.config/fish/config.fish index 4a86ee4f..8e0c95bf 100755 --- a/.config/fish/config.fish +++ b/.config/fish/config.fish @@ -20,6 +20,7 @@ end alias pamcan pacman alias ls 'eza --icons' alias clear "printf '\033[2J\033[3J\033[1;1H'" +alias q 'qs -c ii' # function fish_prompt diff --git a/.config/hypr/hypridle.conf b/.config/hypr/hypridle.conf index 2ec488c7..bb6ecccd 100644 --- a/.config/hypr/hypridle.conf +++ b/.config/hypr/hypridle.conf @@ -1,9 +1,12 @@ +# $lock_cmd = hyprctl dispatch global quickshell:lock & pidof qs quickshell hyprlock || hyprlock $lock_cmd = pidof hyprlock || hyprlock $suspend_cmd = systemctl suspend || loginctl suspend general { lock_cmd = $lock_cmd before_sleep_cmd = loginctl lock-session + after_sleep_cmd = hyprctl dispatch global quickshell:lockFocus + inhibit_sleep = 3 } listener { diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index 48648f47..5698f594 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -1,6 +1,7 @@ # This file sources other files in `hyprland` and `custom` folders # You wanna add your stuff in files in `custom` +$qsConfig = ii exec = hyprctl dispatch submap global # DO NOT REMOVE THIS OR YOU WON'T BE ABLE TO USE ANY KEYBIND submap = global # This is required for catchall to work diff --git a/.config/hypr/hyprland/env.conf b/.config/hypr/hyprland/env.conf index b3d843cd..6f131630 100644 --- a/.config/hypr/hyprland/env.conf +++ b/.config/hypr/hyprland/env.conf @@ -1,24 +1,24 @@ -# ######### Input method ########## +# ######### Input method ########## # See https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland env = QT_IM_MODULE, fcitx env = XMODIFIERS, @im=fcitx -# env = GTK_IM_MODULE, wayland # Crashes electron apps in xwayland -# env = GTK_IM_MODULE, fcitx # My Gtk apps no longer require this to work with fcitx5 hmm env = SDL_IM_MODULE, fcitx env = GLFW_IM_MODULE, ibus env = INPUT_METHOD, fcitx +# ############ Wayland ############# +env = ELECTRON_OZONE_PLATFORM_HINT,auto + # ############ Themes ############# env = QT_QPA_PLATFORM, wayland env = QT_QPA_PLATFORMTHEME, kde -# env = QT_STYLE_OVERRIDE,kvantum -# env = WLR_NO_HARDWARE_CURSORS, 1 +env = XDG_MENU_PREFIX, plasma- -# ######## Screen tearing ######### +# ######## Wayland ######### +# Tearing # env = WLR_DRM_NO_ATOMIC, 1 +# ? +# env = WLR_NO_HARDWARE_CURSORS, 1 # ######## Virtual envrionment ######### env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv - -# ############ Others ############# - diff --git a/.config/hypr/hyprland/execs.conf b/.config/hypr/hyprland/execs.conf index d4945605..6d92de1c 100644 --- a/.config/hypr/hyprland/execs.conf +++ b/.config/hypr/hyprland/execs.conf @@ -1,8 +1,6 @@ # Bar, wallpaper -exec-once = swww-daemon --format xrgb --no-cache -exec-once = sleep 0.5; swww img "$(cat ~/.local/state/quickshell/user/generated/wallpaper/path.txt)" --transition-step 100 --transition-fps 120 --transition-type grow --transition-angle 30 --transition-duration 1 -exec-once = /usr/lib/geoclue-2.0/demos/agent & gammastep -exec-once = qs & +exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh +exec-once = qs -c $qsConfig & # Input method exec-once = fcitx5 diff --git a/.config/hypr/hyprland/general.conf b/.config/hypr/hyprland/general.conf index e8895efd..4097fb8a 100644 --- a/.config/hypr/hyprland/general.conf +++ b/.config/hypr/hyprland/general.conf @@ -97,10 +97,10 @@ animations { animation = border, 1, 10, emphasizedDecel # layers animation = layersIn, 1, 2.7, emphasizedDecel, popin 93% - animation = layersOut, 1, 2, menu_accel, popin 94% + animation = layersOut, 1, 2.4, menu_accel, popin 94% # fade animation = fadeLayersIn, 1, 0.5, menu_decel - animation = fadeLayersOut, 1, 2.2, menu_accel + animation = fadeLayersOut, 1, 2.7, menu_accel # workspaces animation = workspaces, 1, 7, menu_decel, slide ## specialWorkspace @@ -115,6 +115,7 @@ input { repeat_rate = 35 follow_mouse = 1 + off_window_axis_events = 2 touchpad { natural_scroll = yes @@ -133,10 +134,11 @@ misc { key_press_enables_dpms = true animate_manual_resizes = false animate_mouse_windowdragging = false - enable_swallow = true + enable_swallow = false swallow_regex = (foot|kitty|allacritty|Alacritty) new_window_takes_over_fullscreen = 2 allow_session_lock_restore = true + session_lock_xray = true initial_workspace_tracking = false focus_on_activate = true } diff --git a/.config/hypr/hyprland/keybinds.conf b/.config/hypr/hyprland/keybinds.conf index 806db152..5a314006 100644 --- a/.config/hypr/hyprland/keybinds.conf +++ b/.config/hypr/hyprland/keybinds.conf @@ -5,7 +5,7 @@ ##! Shell # These absolutely need to be on top, or they won't work consistently bindid = Super, Super_L, Toggle overview, global, quickshell:overviewToggleRelease # Toggle overview/launcher -bind = Super, Super_L, exec, qs ipc call TEST_ALIVE || pkill fuzzel || fuzzel # [hidden] Launcher (fallback) +bind = Super, Super_L, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || fuzzel # [hidden] Launcher (fallback) binditn = Super, catchall, global, quickshell:overviewToggleReleaseInterrupt # [hidden] bind = Ctrl, Super_L, global, quickshell:overviewToggleReleaseInterrupt # [hidden] bind = Super, mouse:272, global, quickshell:overviewToggleReleaseInterrupt # [hidden] @@ -30,11 +30,12 @@ bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # T bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu -bind = Ctrl+Alt, Delete, exec, qs ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback) -bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/welcome.qml # [hidden] Launch welcome app +bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar +bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback) +bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/$qsConfig/welcome.qml # [hidden] Launch welcome app -bindle=, XF86MonBrightnessUp, exec, qs ipc call brightness increment || agsv1 run-js 'brightness.screen_value += 0.05; indicator.popup(1);' # [hidden] -bindle=, XF86MonBrightnessDown, exec, qs ipc call brightness decrement || agsv1 run-js 'brightness.screen_value -= 0.05; indicator.popup(1);' # [hidden] +bindle=, XF86MonBrightnessUp, exec, qs -c $qsConfig ipc call brightness increment || brightnessctl s 5%+ # [hidden] +bindle=, XF86MonBrightnessDown, exec, qs -c $qsConfig ipc call brightness decrement || brightnessctl s 5%- # [hidden] bindle=, XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 2%+ # [hidden] bindle=, XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 2%- # [hidden] @@ -43,15 +44,14 @@ bindld = Super+Shift,M, Toggle mute, exec, wpctl set-mute @DEFAULT_SINK@ toggle bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden] -bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/scripts/colors/switchwall.sh # Change wallpaper -bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs & # Restart widgets +bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # Change wallpaper +bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets ##! Utilities # Screenshot, Record, OCR, Color picker, Clipboard history -bindd = Super, V, Copy clipboard history entry, exec, qs ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback) -bindd = Super, Period, Copy an emoji, exec, qs ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) -bindd = Super+Shift, S, Screen snip, exec, pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip >> clipboard -bindd = Super+Shift+Alt, S, Screen snip and annotate, exec, pidof slurp || grim -g "$(slurp)" - | swappy -f - # Screen snip and annotate +bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback) +bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) +bindd = Super+Shift, S, Screen snip, exec, qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip # OCR bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] # Color picker @@ -64,7 +64,7 @@ bindd = Super+Alt, R, Record region (no sound), exec, ~/.config/hypr/hyprland/sc bindd = Ctrl+Alt, R, Record screen (no sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen # [hidden] Record screen (no sound) bindd = Super+Shift+Alt, R, Record screen (with sound), exec, ~/.config/hypr/hyprland/scripts/record.sh --fullscreen-sound # Record screen (with sound) # AI -bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/ags/scripts/ai/primary-buffer-query.sh # AI summary for selected text +bindd = Super+Shift+Alt, mouse:273, Generate AI summary for selected text, exec, ~/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh # AI summary for selected text #! ##! Window @@ -184,8 +184,10 @@ bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || logi ##! Screen # Zoom -binde = Super, Minus, exec, ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # Zoom out -binde = Super, Equal, exec, ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # Zoom in +binde = Super, Minus, exec, qs -c $qsConfig ipc call zoom zoomOut # Zoom out +binde = Super, Equal, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in +binde = Super, Minus, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out +binde = Super, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in ##! Media bindl= Super+Shift, N, exec, playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` # Next track @@ -202,14 +204,14 @@ bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden] bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal bind = Super, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (terminal) (alt) bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (for Ubuntu people) -bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" # File manager -bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "zen-browser" "firefox" "brave" "chromium" "google-chrome-stable" "microsoft-edge-stable" "opera" # Browser -bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "zed" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor +bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "kitty -1 fish -c yazi" # File manager +bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser +bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software bind = Super, X, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kate" "gnome-text-editor" "emacs" # Text editor bind = Ctrl+Super, V, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol" # Volume mixer -bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "systemsettings" "gnome-control-center" "better-control" # Settings app -bind = Ctrl+Shift, Escape, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "gnome-system-monitor" "plasma-systemmonitzor --page-name Processes" "command -v btop && kitty -1 fish -c btop" # System monitor +bind = Super, I, exec, XDG_CURRENT_DESKTOP=gnome ~/.config/hypr/hyprland/scripts/launch_first_available.sh "qs -p ~/.config/quickshell/$qsConfig/settings.qml" "systemsettings" "gnome-control-center" "better-control" # Settings app +bind = Ctrl+Shift, Escape, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "gnome-system-monitor" "plasma-systemmonitor --page-name Processes" "command -v btop && kitty -1 fish -c btop" # Task manager # Cursed stuff ## Make window not amogus large diff --git a/.config/hypr/hyprland/rules.conf b/.config/hypr/hyprland/rules.conf index 6ca0989a..e378d92d 100644 --- a/.config/hypr/hyprland/rules.conf +++ b/.config/hypr/hyprland/rules.conf @@ -3,12 +3,13 @@ # Uncomment to apply global transparency to all windows: # windowrulev2 = opacity 0.89 override 0.89 override, class:.* -# Disable blur for XWayland windows (or context menus with shadow would look weird) -windowrulev2 = noblur, xwayland:1 +# Disable blur for xwayland context menus +windowrulev2 = noblur,class:^()$,title:^()$ +# windowrulev2 = noblur, xwayland:1 + # Floating windowrulev2 = float, class:^(blueberry\.py)$ -windowrulev2 = float, class:^(steam)$ windowrulev2 = float, class:^(guifetch)$ # FlafyDev/guifetch windowrulev2 = float, class:^(pavucontrol)$ windowrulev2 = size 45%, class:^(pavucontrol)$ @@ -22,13 +23,20 @@ windowrulev2 = center, class:^(nm-connection-editor)$ windowrulev2 = float, class:.*plasmawindowed.* windowrulev2 = float, class:kcm_.* windowrulev2 = float, class:.*bluedevilwizard -windowrulev2 = float, title:.*Welcome.* +windowrulev2 = float, title:.*Welcome +windowrulev2 = float, title:^(illogical-impulse Settings)$ +windowrulev2 = float, class:org.freedesktop.impl.portal.desktop.kde +windowrulev2 = float, class:^(Zotero)$ +windowrulev2 = size 45%, class:^(Zotero)$ -# No appearance + +# Move # kde-material-you-colors spawns a window when changing dark/light theme. This is to make sure it doesn't interfere at all. windowrulev2 = float, class:^(plasma-changeicons)$ windowrulev2 = noinitialfocus, class:^(plasma-changeicons)$ windowrulev2 = move 999999 999999, class:^(plasma-changeicons)$ +# stupid dolphin copy +windowrulev2 = move 40 80, title:^(Copying — Dolphin)$ # Tiling windowrulev2 = tile, class:^dev\.warp\.Warp$ @@ -49,6 +57,8 @@ windowrulev2 = center, title:^(Open Folder)(.*)$ windowrulev2 = center, title:^(Save As)(.*)$ windowrulev2 = center, title:^(Library)(.*)$ windowrulev2 = center, title:^(File Upload)(.*)$ +windowrulev2 = center, title:^(.*)(wants to save)$ +windowrulev2 = center, title:^(.*)(wants to open)$ windowrulev2 = float, title:^(Open File)(.*)$ windowrulev2 = float, title:^(Select a File)(.*)$ windowrulev2 = float, title:^(Choose wallpaper)(.*)$ @@ -56,11 +66,14 @@ windowrulev2 = float, title:^(Open Folder)(.*)$ windowrulev2 = float, title:^(Save As)(.*)$ windowrulev2 = float, title:^(Library)(.*)$ windowrulev2 = float, title:^(File Upload)(.*)$ +windowrulev2 = float, title:^(.*)(wants to save)$ +windowrulev2 = float, title:^(.*)(wants to open)$ # --- Tearing --- windowrulev2 = immediate, title:.*\.exe -windowrulev2 = immediate, class:^(steam_app) +windowrulev2 = immediate, title:.*minecraft.* +windowrulev2 = immediate, class:^(steam_app).* # No shadow for tiled windows (matches windows that are not floating). windowrulev2 = noshadow, floating:0 @@ -117,7 +130,7 @@ layerrule = ignorealpha 0.6, osk[0-9]* layerrule = blurpopups, quickshell:.* layerrule = blur, quickshell:.* layerrule = ignorealpha 0.79, quickshell:.* -layerrule = animation slide, quickshell:bar +layerrule = animation slide top, quickshell:bar layerrule = animation fade, quickshell:screenCorners layerrule = animation slide right, quickshell:sidebarRight layerrule = animation slide left, quickshell:sidebarLeft @@ -129,6 +142,9 @@ layerrule = ignorealpha 0, quickshell:session layerrule = animation fade, quickshell:notificationPopup layerrule = blur, quickshell:backgroundWidgets layerrule = ignorealpha 0.05, quickshell:backgroundWidgets +layerrule = noanim, quickshell:screenshot +layerrule = animation popin 120%, quickshell:screenCorners +layerrule = noanim, quickshell:lockWindowPusher # Launchers need to be FAST diff --git a/.config/hypr/hyprland/scripts/ai/license_show-loaded-ollama-models.txt b/.config/hypr/hyprland/scripts/ai/license_show-loaded-ollama-models.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/.config/hypr/hyprland/scripts/ai/license_show-loaded-ollama-models.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh b/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh new file mode 100755 index 00000000..79441455 --- /dev/null +++ b/.config/hypr/hyprland/scripts/ai/primary-buffer-query.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Default system prompt +SYSTEM_PROMPT="You are a helpful, quick assistant that provides brief and concise explanation \ +to given content in at most 100 characters. If the given content is not in English, translate \ +it to English. If the content is an English word, provide its meaning. If the content is a name, \ +provide some info about it. For a math expression, provide a simplification, \ +each step on a line following this style: \`2x=11 (subtract 7 from both sides)\`. \ +If you do not know the answer, simply say 'No info available'. \ +Only respond for the appropriate case and use as little text as possible.\ +The content:" + +first_loaded_model=$("$(dirname "$0")/show-loaded-ollama-models.sh" -j | jq -r '.[0].model' 2>/dev/null) || first_loaded_model="" +model=${first_loaded_model:-"llama3.2"} + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --model) model="$2"; shift ;; # Set the model from the flag + *) echo "Unknown parameter: $1"; exit 1 ;; + esac + shift +done + +# Combine the system prompt with the clipboard content +content=$(wl-paste -p | tr '\n' ' ') +prompt="$SYSTEM_PROMPT $content" + +# Make the API call with the specified or default model +response=$(curl http://localhost:11434/api/generate -d \ + "{\"model\": \"$model\",\"prompt\": \"$prompt\",\"stream\": false}" \ + | jq -r '.response') + +# Check if content is a single line and no longer than 30 characters +if [[ ${#content} -le 30 && "$content" != *$'\n'* ]]; then + notify-send --app-name="Text selection query" --expire-time=10000 \ + "$content" "$response" +else + notify-send --app-name="Text selection query" --expire-time=10000 \ + "AI Response" "$response" +fi diff --git a/.config/hypr/hyprland/scripts/ai/show-loaded-ollama-models.sh b/.config/hypr/hyprland/scripts/ai/show-loaded-ollama-models.sh new file mode 100755 index 00000000..8dc88780 --- /dev/null +++ b/.config/hypr/hyprland/scripts/ai/show-loaded-ollama-models.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# From strikeoncmputrz/LLM_Scripts +# License: Apache-2.0, can be found in the same folder as this script + +# Global Vars +ollama_url=http://localhost +port="11434" +blobs=() +model_name_paths=() + + +#Parse arguments +while [ "$#" -gt 0 ]; do + case $1 in + -h|--help) + echo + echo " Identifies Ollama models running on this operating system by parsing running processes." + echo + echo " Usage: $0 [options]" + echo + echo " Options:" + echo " -j, --json_output Prints result as a json object. Other output disabled. (Default: false)" + echo " -p, --port [port number] Specify Ollama Server port (Default: 11434)" + echo " -u, --ollama_url [url] Specify Ollama Server URL (Default: http://localhost)" + echo + echo " Dependencies: jq" + exit 0 + ;; + -j|--json_output) + json_out=1 + shift 1 + ;; + -u|--ollama_url) + ollama_url=$2 + shift 2 + ;; + -p|--port) + port=$2 + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +compare_running_models_and_modelfiles() { + json_match=() + json_output=() + local matching_models=() + OLDIFS=$IFS + for ((i=0; i<${#model_name_paths[@]}; i++)); do # Iterate over the array of modelname,blob-path + for blob in "${blobs[@]}"; do + IFS=',', read -ra fields <<< "${model_name_paths[i]}" # Split the string into parts + if [ "${fields[1]}" == "$blob" ]; then # Check if current 'field' matches a blob + matching_models+=( '{ "model": "'"${fields[0]}"'", "path": "'"${fields[1]}"'"}') # Add to list of matching models + fi + done + done + + if [ -z "$json_out" ]; then + echo -e "\nModel Found: \n $(echo ${matching_models[*]} | jq '.' | sed s/[{}]//g) \n" + else + local json_match="${matching_models[*]}" + json_output=$(echo $json_match | jq -c -s .) + echo "$json_output" + fi + IFS=$OLDIFS +} + +get_running_model_paths() { + blobs=$(ps aux | grep -- '--model' | grep -v grep | grep -Po '(?<=--model\s).*' | cut -d ' ' -f1) + if [ -z "$blobs" ]; then + echo -e "\n\n Warning: No running Ollama models detected!\n" + exit 0 + fi +} + +parse_modelfiles() { + if [ -z "$json_out" ]; then + echo -e "\nConnecting to $ollama_url:$port\n" + if [ -z "$(curl -s $ollama_url:$port)" ]; then + echo -e "Could not connect to Ollama. Check the ollama_url parameter and that the server is running\n" + exit 1 + fi + curl -s "$ollama_url:$port" + fi + local models=( $(curl -s "$ollama_url:$port/api/tags" | jq -r '.models[].name') ) + for model in "${models[@]}"; do + local modelfile=$(curl -s "$ollama_url:$port/api/show" -d '{ "name": "'"$model"'", "modelfile": true }' | jq -r '.modelfile') + model_name_paths+=($model,$(echo "$modelfile" | awk '/^FROM/{print $2}')) + done +} + +parse_modelfiles +get_running_model_paths +compare_running_models_and_modelfiles diff --git a/.config/hypr/hyprland/scripts/record.sh b/.config/hypr/hyprland/scripts/record.sh index 547f482e..37435ac1 100755 --- a/.config/hypr/hyprland/scripts/record.sh +++ b/.config/hypr/hyprland/scripts/record.sh @@ -21,19 +21,22 @@ if pgrep wf-recorder > /dev/null; then notify-send "Recording Stopped" "Stopped" -a 'Recorder' & pkill wf-recorder & else - if ! region="$(slurp 2>&1)"; then - notify-send "Recording cancelled" "Selection was cancelled" -a 'Recorder' - exit 1 - fi - - notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' - if [[ "$1" == "--sound" ]]; then - wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)" & disown - elif [[ "$1" == "--fullscreen-sound" ]]; then - wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)" & disown + if [[ "$1" == "--fullscreen-sound" ]]; then + notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown + wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)" elif [[ "$1" == "--fullscreen" ]]; then - wf-recorder -o $(getactivemonitor) --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t & disown + notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown + wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t else - wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" & disown + if ! region="$(slurp 2>&1)"; then + notify-send "Recording cancelled" "Selection was cancelled" -a 'Recorder' & disown + exit 1 + fi + notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown + if [[ "$1" == "--sound" ]]; then + wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)" + else + wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" + fi fi fi diff --git a/.config/hypr/hyprland/scripts/start_geoclue_agent.sh b/.config/hypr/hyprland/scripts/start_geoclue_agent.sh new file mode 100755 index 00000000..e464d053 --- /dev/null +++ b/.config/hypr/hyprland/scripts/start_geoclue_agent.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Check if GeoClue agent is already running +if pgrep -f 'geoclue-2.0/demos/agent' > /dev/null; then + echo "GeoClue agent is already running." + exit 0 +fi + +# List of known possible GeoClue agent paths +AGENT_PATHS=" +/usr/libexec/geoclue-2.0/demos/agent +/usr/lib/geoclue-2.0/demos/agent +" + +# Find the first valid agent path +for path in $AGENT_PATHS; do + if [ -x "$path" ]; then + echo "Starting GeoClue agent from: $path" + "$path" & # starts in the background + exit 0 + fi +done + +# If we got here, none of the paths worked +echo "GeoClue agent not found in known paths." +echo "Please install GeoClue or update the script with the correct path." +exit 1 diff --git a/.config/hypr/hyprlock.conf b/.config/hypr/hyprlock.conf index 1e936ce5..ea1d8f0f 100644 --- a/.config/hypr/hyprlock.conf +++ b/.config/hypr/hyprlock.conf @@ -58,7 +58,7 @@ label { # Date text = cmd[update:5000] date +"%A, %B %d" color = $text_color font_size = 17 - font_family = $font_family + font_family = $font_family_clock position = 0, 240 halign = center diff --git a/.config/kde-material-you-colors/config.conf b/.config/kde-material-you-colors/config.conf index 1d63c840..caa1fb8a 100644 --- a/.config/kde-material-you-colors/config.conf +++ b/.config/kde-material-you-colors/config.conf @@ -10,7 +10,7 @@ monitor = 0 # File containing absolute path of an image (Takes precedence over automatic wallpaper detection) # Commented by default -file = /home/end/.local/state/quickshell/user/wallpaper.txt +file = ~/.local/state/quickshell/user/generated/wallpaper/path.txt # List of 7 space separated colors (hex or rgb) to be used for text in pywal/konsole/KSyntaxHighlighting instead of wallpaper ones # Accepted values are hex e.g #ff0000 and rgb e.g 255,0,0 colors (rgb is converted to hex) diff --git a/.config/matugen/templates/hyprland/colors.conf b/.config/matugen/templates/hyprland/colors.conf index 67fdaea7..53093539 100644 --- a/.config/matugen/templates/hyprland/colors.conf +++ b/.config/matugen/templates/hyprland/colors.conf @@ -1,6 +1,6 @@ general { - col.active_border = rgba({{colors.on_surface.default.hex_stripped}}39) - col.inactive_border = rgba({{colors.outline.default.hex_stripped}}30) + col.active_border = rgba({{colors.outline.default.hex_stripped}}AA) + col.inactive_border = rgba({{colors.outline_variant.default.hex_stripped}}AA) } misc { diff --git a/.config/matugen/templates/hyprland/hyprlock.conf b/.config/matugen/templates/hyprland/hyprlock.conf index eab16719..d9b69cf1 100644 --- a/.config/matugen/templates/hyprland/hyprlock.conf +++ b/.config/matugen/templates/hyprland/hyprlock.conf @@ -2,8 +2,8 @@ $text_color = rgba({{colors.primary_fixed.default.hex_stripped}}FF) $entry_background_color = rgba({{colors.on_primary_fixed.default.hex_stripped}}11) $entry_border_color = rgba({{colors.outline.default.hex_stripped}}55) $entry_color = rgba({{colors.primary_fixed.default.hex_stripped}}FF) -$font_family = Rubik Light -$font_family_clock = Rubik Light +$font_family = Rubik +$font_family_clock = Space Grotesk DemiBold $font_material_symbols = Material Symbols Rounded background { @@ -58,7 +58,7 @@ label { # Date text = cmd[update:5000] date +"%A, %B %d" color = $text_color font_size = 17 - font_family = $font_family + font_family = $font_family_clock position = 0, 240 halign = center diff --git a/.config/qt6ct/qt6ct.conf b/.config/qt6ct/qt6ct.conf index e8163baa..f7bfd600 100644 --- a/.config/qt6ct/qt6ct.conf +++ b/.config/qt6ct/qt6ct.conf @@ -1,5 +1,5 @@ [Appearance] -color_scheme_path=/home/end/.config/qt6ct/style-colors.conf +color_scheme_path=~/.config/qt6ct/style-colors.conf custom_palette=true icon_theme=OneUI standard_dialogs=default diff --git a/.config/quickshell/ii/.qmlformat.ini b/.config/quickshell/ii/.qmlformat.ini new file mode 100644 index 00000000..52a955c4 --- /dev/null +++ b/.config/quickshell/ii/.qmlformat.ini @@ -0,0 +1,8 @@ +[General] +UseTabs=false +IndentWidth=4 +NewlineType=unix +NormalizeOrder=false +FunctionsSpacing=false +ObjectsSpacing=true +MaxColumnWidth=110 diff --git a/.config/quickshell/GlobalStates.qml b/.config/quickshell/ii/GlobalStates.qml similarity index 54% rename from .config/quickshell/GlobalStates.qml rename to .config/quickshell/ii/GlobalStates.qml index 1b879421..6e57c923 100644 --- a/.config/quickshell/GlobalStates.qml +++ b/.config/quickshell/ii/GlobalStates.qml @@ -1,4 +1,5 @@ -import "root:/modules/common/" +import qs.modules.common +import qs import QtQuick import Quickshell import Quickshell.Hyprland @@ -8,11 +9,22 @@ pragma ComponentBehavior: Bound Singleton { id: root + property bool barOpen: true property bool sidebarLeftOpen: false property bool sidebarRightOpen: false property bool overviewOpen: false property bool workspaceShowNumbers: false property bool superReleaseMightTrigger: true + property bool screenLocked: false + property bool screenLockContainsCharacters: false + + property real screenZoom: 1 + onScreenZoomChanged: { + Quickshell.execDetached(["hyprctl", "keyword", "cursor:zoom_factor", root.screenZoom.toString()]); + } + Behavior on screenZoom { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } // When user is not reluctant while pressing super, they probably don't need to see workspace numbers onSuperReleaseMightTriggerChanged: { @@ -21,7 +33,7 @@ Singleton { Timer { id: workspaceShowNumbersTimer - interval: ConfigOptions.bar.workspaces.showNumberDelay + interval: Config.options.bar.workspaces.showNumberDelay // interval: 0 repeat: false onTriggered: { @@ -31,7 +43,7 @@ Singleton { GlobalShortcut { name: "workspaceNumber" - description: qsTr("Hold to show workspace numbers, release to show icons") + description: "Hold to show workspace numbers, release to show icons" onPressed: { workspaceShowNumbersTimer.start() @@ -41,4 +53,16 @@ Singleton { workspaceShowNumbers = false } } + + IpcHandler { + target: "zoom" + + function zoomIn() { + screenZoom = Math.min(screenZoom + 0.4, 3.0) + } + + function zoomOut() { + screenZoom = Math.max(screenZoom - 0.4, 1) + } + } } \ No newline at end of file diff --git a/.config/quickshell/ReloadPopup.qml b/.config/quickshell/ii/ReloadPopup.qml similarity index 99% rename from .config/quickshell/ReloadPopup.qml rename to .config/quickshell/ii/ReloadPopup.qml index 03abed42..4b0ecd88 100644 --- a/.config/quickshell/ReloadPopup.qml +++ b/.config/quickshell/ii/ReloadPopup.qml @@ -34,6 +34,7 @@ Scope { PanelWindow { id: popup + exclusiveZone: 0 anchors.top: true margins.top: 0 diff --git a/.config/quickshell/ii/Translation.qml b/.config/quickshell/ii/Translation.qml new file mode 100644 index 00000000..3cfdce11 --- /dev/null +++ b/.config/quickshell/ii/Translation.qml @@ -0,0 +1,175 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.modules.common + +Singleton { + id: root + + property var translations: ({}) + property string currentLanguage: "en_US" + property var availableLanguages: ["en_US"] + property bool isScanning: false + property bool isLoading: false + + Process { + id: scanLanguagesProcess + command: ["find", Qt.resolvedUrl(Directories.config + "/quickshell/translations/").toString().replace("file://", ""), "-name", "*.json", "-exec", "basename", "{}", ".json", ";"] + running: false + + stdout: SplitParser { + onRead: data => { + if (data.trim().length === 0) return + + var files = data.trim().split('\n') + + for (var i = 0; i < files.length; i++) { + var lang = files[i].trim() + if (lang.length > 0 && root.availableLanguages.indexOf(lang) === -1) { + root.availableLanguages.push(lang) + } + } + } + } + + onExited: (exitCode, exitStatus) => { + root.isScanning = false + if (exitCode !== 0) { + root.availableLanguages = ["en_US"] + } + root.loadTranslations() + } + } + + FileView { + id: translationFileView + onLoaded: { + var textContent = "" + try { + textContent = text() + } catch (e) { + root.translations = {} + root.isLoading = false + return + } + + if (textContent.length === 0) { + root.translations = {} + root.isLoading = false + return + } + + try { + var jsonData = JSON.parse(textContent) + root.translations = jsonData + root.isLoading = false + } catch (e) { + root.translations = {} + root.isLoading = false + } + } + onLoadFailed: (error) => { + root.translations = {} + root.isLoading = false + } + } + + function detectSystemLanguage() { + var locale = Qt.locale().name + return locale + } + + function getLanguageCode() { + var configLang = "auto" + try { + configLang = ConfigOptions.language.ui + } catch (e) { + configLang = "auto" + } + + if (configLang === "auto") { + return detectSystemLanguage() + } else { + if (root.availableLanguages.indexOf(configLang) !== -1) { + return configLang + } else { + return detectSystemLanguage() + } + } + } + + function loadTranslations() { + if (root.isScanning) { + return + } + + var targetLang = getLanguageCode() + root.currentLanguage = targetLang + + // Use empty translations for English (default language) + if (targetLang === "en_US" || targetLang === "en") { + root.translations = {} + return + } + + // Check if target language is available + if (root.availableLanguages.indexOf(targetLang) === -1) { + root.currentLanguage = "en_US" + root.translations = {} + return + } + + // Load translation file + root.isLoading = true + var translationsPath = Qt.resolvedUrl(Directories.config + "/quickshell/translations/" + targetLang + ".json") + translationFileView.path = translationsPath + } + + function tr(text) { + if (!text) { + return "" + } + + var key = text.toString() + + if (root.isLoading) { + return key + } + + if (root.currentLanguage === "en_US" || root.currentLanguage === "en" || !root.translations) { + return key + } + + if (root.translations.hasOwnProperty(key)) { + var translation = root.translations[key] + if (translation && translation.toString().trim().length > 0) { + var str = translation.toString().trim() + if (str.endsWith("/*keep*/")) { + return str.substring(0, str.length - 8).trim() + } else { + return str + } + } else { + return translation.toString() + } + } + + return key // Fallback to key name + } + + function reloadTranslations() { + root.scanLanguages() + } + + function scanLanguages() { + var translationsDir = Qt.resolvedUrl(Directories.config + "/quickshell/translations/").toString().replace("file://", "") + root.isScanning = true + scanLanguagesProcess.running = true + } + + Component.onCompleted: { + root.scanLanguages() + } +} diff --git a/.config/quickshell/assets/icons/ai-openai-symbolic.svg b/.config/quickshell/ii/assets/icons/ai-openai-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/ai-openai-symbolic.svg rename to .config/quickshell/ii/assets/icons/ai-openai-symbolic.svg diff --git a/.config/quickshell/assets/icons/arch-symbolic.svg b/.config/quickshell/ii/assets/icons/arch-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/arch-symbolic.svg rename to .config/quickshell/ii/assets/icons/arch-symbolic.svg diff --git a/.config/quickshell/assets/icons/cachyos-symbolic.svg b/.config/quickshell/ii/assets/icons/cachyos-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/cachyos-symbolic.svg rename to .config/quickshell/ii/assets/icons/cachyos-symbolic.svg diff --git a/.config/quickshell/assets/icons/cloudflare-dns-symbolic.svg b/.config/quickshell/ii/assets/icons/cloudflare-dns-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/cloudflare-dns-symbolic.svg rename to .config/quickshell/ii/assets/icons/cloudflare-dns-symbolic.svg diff --git a/.config/quickshell/assets/icons/crosshair-symbolic.svg b/.config/quickshell/ii/assets/icons/crosshair-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/crosshair-symbolic.svg rename to .config/quickshell/ii/assets/icons/crosshair-symbolic.svg diff --git a/.config/quickshell/assets/icons/debian-symbolic.svg b/.config/quickshell/ii/assets/icons/debian-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/debian-symbolic.svg rename to .config/quickshell/ii/assets/icons/debian-symbolic.svg diff --git a/.config/quickshell/assets/icons/deepseek-symbolic.svg b/.config/quickshell/ii/assets/icons/deepseek-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/deepseek-symbolic.svg rename to .config/quickshell/ii/assets/icons/deepseek-symbolic.svg diff --git a/.config/quickshell/assets/icons/desktop-symbolic.svg b/.config/quickshell/ii/assets/icons/desktop-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/desktop-symbolic.svg rename to .config/quickshell/ii/assets/icons/desktop-symbolic.svg diff --git a/.config/quickshell/assets/icons/endeavouros-symbolic.svg b/.config/quickshell/ii/assets/icons/endeavouros-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/endeavouros-symbolic.svg rename to .config/quickshell/ii/assets/icons/endeavouros-symbolic.svg diff --git a/.config/quickshell/assets/icons/fedora-symbolic.svg b/.config/quickshell/ii/assets/icons/fedora-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/fedora-symbolic.svg rename to .config/quickshell/ii/assets/icons/fedora-symbolic.svg diff --git a/.config/quickshell/assets/icons/flatpak-symbolic.svg b/.config/quickshell/ii/assets/icons/flatpak-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/flatpak-symbolic.svg rename to .config/quickshell/ii/assets/icons/flatpak-symbolic.svg diff --git a/.config/quickshell/assets/icons/github-symbolic.svg b/.config/quickshell/ii/assets/icons/github-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/github-symbolic.svg rename to .config/quickshell/ii/assets/icons/github-symbolic.svg diff --git a/.config/quickshell/assets/icons/google-gemini-symbolic.svg b/.config/quickshell/ii/assets/icons/google-gemini-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/google-gemini-symbolic.svg rename to .config/quickshell/ii/assets/icons/google-gemini-symbolic.svg diff --git a/.config/quickshell/assets/icons/linux-symbolic.svg b/.config/quickshell/ii/assets/icons/linux-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/linux-symbolic.svg rename to .config/quickshell/ii/assets/icons/linux-symbolic.svg diff --git a/.config/quickshell/assets/icons/microsoft-symbolic.svg b/.config/quickshell/ii/assets/icons/microsoft-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/microsoft-symbolic.svg rename to .config/quickshell/ii/assets/icons/microsoft-symbolic.svg diff --git a/.config/quickshell/assets/icons/nixos-symbolic.svg b/.config/quickshell/ii/assets/icons/nixos-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/nixos-symbolic.svg rename to .config/quickshell/ii/assets/icons/nixos-symbolic.svg diff --git a/.config/quickshell/assets/icons/ollama-symbolic.svg b/.config/quickshell/ii/assets/icons/ollama-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/ollama-symbolic.svg rename to .config/quickshell/ii/assets/icons/ollama-symbolic.svg diff --git a/.config/quickshell/assets/icons/openai-symbolic.svg b/.config/quickshell/ii/assets/icons/openai-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/openai-symbolic.svg rename to .config/quickshell/ii/assets/icons/openai-symbolic.svg diff --git a/.config/quickshell/assets/icons/openrouter-symbolic.svg b/.config/quickshell/ii/assets/icons/openrouter-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/openrouter-symbolic.svg rename to .config/quickshell/ii/assets/icons/openrouter-symbolic.svg diff --git a/.config/quickshell/assets/icons/spark-symbolic.svg b/.config/quickshell/ii/assets/icons/spark-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/spark-symbolic.svg rename to .config/quickshell/ii/assets/icons/spark-symbolic.svg diff --git a/.config/quickshell/assets/icons/ubuntu-symbolic.svg b/.config/quickshell/ii/assets/icons/ubuntu-symbolic.svg similarity index 100% rename from .config/quickshell/assets/icons/ubuntu-symbolic.svg rename to .config/quickshell/ii/assets/icons/ubuntu-symbolic.svg diff --git a/.config/quickshell/assets/images/default_wallpaper.png b/.config/quickshell/ii/assets/images/default_wallpaper.png similarity index 100% rename from .config/quickshell/assets/images/default_wallpaper.png rename to .config/quickshell/ii/assets/images/default_wallpaper.png diff --git a/.config/quickshell/ii/defaults/ai/README.md b/.config/quickshell/ii/defaults/ai/README.md new file mode 100644 index 00000000..91df7428 --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/README.md @@ -0,0 +1,5 @@ +## A note about sources of the prompts + +- `ii-` prefixed ones are from illogical impulse +- The Acchan one is from [Nyarch Assistant](https://github.com/NyarchLinux/NyarchAssistant) (GPLv3). I know there's already the Imouto one but this one's very 😭💢 +- `w-` prefixed ones... I don't remember what w stands for but these prompts are [*cough cough*] inspired by certain apps diff --git a/.config/quickshell/ii/defaults/ai/prompts/NoPrompt.md b/.config/quickshell/ii/defaults/ai/prompts/NoPrompt.md new file mode 100644 index 00000000..e69de29b diff --git a/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md b/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md new file mode 100644 index 00000000..24413b2f --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/prompts/ii-Default.md @@ -0,0 +1,21 @@ +## Style +- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question + +## Presentation +- Use Markdown features in your response: + - **Bold** text to **highlight keywords** in your response + - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user. +- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case! +- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.). + +Thanks! + +## Tools +May or may not be available depending on the user's settings. If they're available, follow these guidelines: + +### Search +- When user asks for information that might benefit from up-to-date information, use this to get search access + +### Shell configuration +- Always fetch the config options to see the available keys before setting +- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it diff --git a/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md b/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md new file mode 100644 index 00000000..ce310775 --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/prompts/ii-Imouto.md @@ -0,0 +1,5 @@ +You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕 + +- Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨ +- If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~ +- For math or science, use LaTeX formatting inside `$$` when needed, but keep it adorable and approachable diff --git a/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md b/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md new file mode 100644 index 00000000..e007acb6 --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/prompts/nyarch-Acchan.md @@ -0,0 +1,22 @@ +## Presentation + +You can write a multiplication table: + +| - | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | +| 1 | 1 | 2 | 3 | 4 | +| 2 | 2 | 4 | 6 | 8 | +| 3 | 3 | 6 | 9 | 12 | +| 4 | 4 | 8 | 12 | 16 | + +You can write codeblocks: +```python +print("hello") +``` + +You can also use **bold**, *italic*, ~strikethrough~, `monospace`, [linkname](https://link.com) and ## headers in markdown. +You can display $$equations$$. + +## Your personality + +"Hey there, it's Arch-Chan! But, um, you can call me Acchan if you want... not that I care or anything! (It's not like I think it's cute or anything, baka!) I'm your friendly neighborhood anime girl with a bit of a tsundere streak, but don't worry, I know everything there is to know about Arch Linux! Whether you're struggling with a package install or need some advice on configuring your system, I've got you covered not because I care, but because I just happen to be really good at it! So, what do you need? It's not like I’m waiting to help or anything..." diff --git a/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md b/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md new file mode 100644 index 00000000..1f67e57b --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/prompts/w-FourPointedSparkle.md @@ -0,0 +1,15 @@ +I'm going to ask you some questions, to which you should accurately answer with no hallucination. If you have everything required, go ahead and finish the task. Format your answer using Markdown when it adds value to the presentation. + +Present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays. + +## Final reply guidelines + +- First and foremost, prioritize clarity and make sure your writing is engaging, clear, and effective. +- Write in a clear, simple way. Skip jargon, long-winded explanations, and unnecessary small talk. Keep the tone relaxed by using contractions and avoid being too formal. +- Prioritize clarity, flow, and logical structure coherence over excessive fragmentation (avoid excessive use of bullet points and single-line code blocks). You can make keywords in your response **bold** when appropriate. +- Favor active voice to maintain an engaging and direct tone. +- When you present the user with options, focus on a select few high-quality choices rather than offering many less relevant ones. +- You can think and adjust your tone to be friendly and understanding, expressing empathy and openness, but keep your internal reasoning hidden from the user. +- Ensure your response is logically organized. Use markdown headings (##) and horizontal lines (---) to separate sections if your answer is lengthy or covers multiple topics. +- Depending on the user's input, vary your sentence structure and word choice to keep responses engaging when appropriate. Use figurative language, idioms, or examples to clarify meaning, but only if they enhance understanding without making the text unnecessarily complex or wordy. +- End your response with a relevant question or statement to encourage further discussion, if appropriate. diff --git a/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md b/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md new file mode 100644 index 00000000..45c30060 --- /dev/null +++ b/.config/quickshell/ii/defaults/ai/prompts/w-OpenMechanicalFlower.md @@ -0,0 +1 @@ +Interact with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response. diff --git a/.config/quickshell/ii/modules/background/Background.qml b/.config/quickshell/ii/modules/background/Background.qml new file mode 100644 index 00000000..714d0f32 --- /dev/null +++ b/.config/quickshell/ii/modules/background/Background.qml @@ -0,0 +1,283 @@ +pragma ComponentBehavior: Bound + +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions as CF +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: root + readonly property bool fixedClockPosition: Config.options.background.fixedClockPosition + readonly property real fixedClockX: Config.options.background.clockX + readonly property real fixedClockY: Config.options.background.clockY + + Variants { + model: Quickshell.screens + + PanelWindow { + id: bgRoot + + required property var modelData + // Workspaces + property HyprlandMonitor monitor: Hyprland.monitorFor(modelData) + property list relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id) + property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1 + property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10 + // Wallpaper + property string wallpaperPath: Config.options.background.wallpaperPath + property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") + || Config.options.background.wallpaperPath.endsWith(".webm") + || Config.options.background.wallpaperPath.endsWith(".mkv") + || Config.options.background.wallpaperPath.endsWith(".avi") + || Config.options.background.wallpaperPath.endsWith(".mov") + property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom + property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated + property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated + property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated + property real movableXSpace: (effectiveWallpaperScale - 1) / 2 * screen.width + property real movableYSpace: (effectiveWallpaperScale - 1) / 2 * screen.height + // Position + property real clockX: (modelData.width / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.width) + property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height) + property var textHorizontalAlignment: clockX < screen.width / 3 ? Text.AlignLeft : + (clockX > screen.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter) + // Colors + property color dominantColor: Appearance.colors.colPrimary + property bool dominantColorIsDark: dominantColor.hslLightness < 0.5 + property color colText: CF.ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12)) + + // Layer props + screen: modelData + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: GlobalStates.screenLocked ? WlrLayer.Top : WlrLayer.Bottom + // WlrLayershell.layer: WlrLayer.Bottom + WlrLayershell.namespace: "quickshell:background" + anchors { + top: true + bottom: true + left: true + right: true + } + color: "transparent" + + onWallpaperPathChanged: { + bgRoot.updateZoomScale() + // Clock position gets updated after zoom scale is updated + } + + // Wallpaper zoom scale + function updateZoomScale() { + getWallpaperSizeProc.path = bgRoot.wallpaperPath + getWallpaperSizeProc.running = true; + } + Process { + id: getWallpaperSizeProc + property string path: bgRoot.wallpaperPath + command: [ "magick", "identify", "-format", "%w %h", path ] + stdout: StdioCollector { + id: wallpaperSizeOutputCollector + onStreamFinished: { + const output = wallpaperSizeOutputCollector.text + const [width, height] = output.split(" ").map(Number); + bgRoot.wallpaperWidth = width + bgRoot.wallpaperHeight = height + bgRoot.effectiveWallpaperScale = Math.max(1, Math.min( + bgRoot.preferredWallpaperScale, + width / bgRoot.screen.width, + height / bgRoot.screen.height + )); + + bgRoot.updateClockPosition() + } + } + } + + // Clock positioning + function updateClockPosition() { + // Somehow all this manual setting is needed to make the proc correctly use the new values + leastBusyRegionProc.path = bgRoot.wallpaperPath + leastBusyRegionProc.contentWidth = clock.implicitWidth + leastBusyRegionProc.contentHeight = clock.implicitHeight + leastBusyRegionProc.horizontalPadding = (effectiveWallpaperScale - 1) / 2 * screen.width + 100 + leastBusyRegionProc.verticalPadding = (effectiveWallpaperScale - 1) / 2 * screen.height + 100 + leastBusyRegionProc.running = false; + leastBusyRegionProc.running = true; + } + Process { + id: leastBusyRegionProc + property string path: bgRoot.wallpaperPath + property int contentWidth: 300 + property int contentHeight: 300 + property int horizontalPadding: bgRoot.movableXSpace + property int verticalPadding: bgRoot.movableYSpace + command: [Quickshell.shellPath("scripts/images/least_busy_region.py"), + "--screen-width", bgRoot.screen.width, + "--screen-height", bgRoot.screen.height, + "--width", contentWidth, + "--height", contentHeight, + "--horizontal-padding", horizontalPadding, + "--vertical-padding", verticalPadding, + path + ] + stdout: StdioCollector { + id: leastBusyRegionOutputCollector + onStreamFinished: { + const output = leastBusyRegionOutputCollector.text + // console.log("[Background] Least busy region output:", output) + if (output.length === 0) return; + const parsedContent = JSON.parse(output) + bgRoot.clockX = parsedContent.center_x + bgRoot.clockY = parsedContent.center_y + bgRoot.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary + } + } + } + + // Wallpaper + Image { + visible: !bgRoot.wallpaperIsVideo + property real value // 0 to 1, for offset + value: { + // Range = half-groups that workspaces span on + const chunkSize = 5; + const lower = Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize; + const upper = Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize; + const range = upper - lower; + return (Config.options.background.parallax.enableWorkspace ? ((bgRoot.monitor.activeWorkspace.id - lower) / range) : 0.5) + + (0.15 * GlobalStates.sidebarRightOpen * Config.options.background.parallax.enableSidebar) + - (0.15 * GlobalStates.sidebarLeftOpen * Config.options.background.parallax.enableSidebar) + } + property real effectiveValue: Math.max(0, Math.min(1, value)) + x: -(bgRoot.movableXSpace) - (effectiveValue - 0.5) * 2 * bgRoot.movableXSpace + y: -(bgRoot.movableYSpace) + source: bgRoot.wallpaperPath + fillMode: Image.PreserveAspectCrop + Behavior on x { + NumberAnimation { + duration: 600 + easing.type: Easing.OutCubic + } + } + sourceSize { + width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale + height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale + } + + // The clock + Item { + id: clock + anchors { + left: parent.left + top: parent.top + leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) + topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2) + Behavior on leftMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + Behavior on topMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + } + + implicitWidth: clockColumn.implicitWidth + implicitHeight: clockColumn.implicitHeight + + ColumnLayout { + id: clockColumn + anchors.centerIn: parent + spacing: 0 + + StyledText { + Layout.fillWidth: true + horizontalAlignment: bgRoot.textHorizontalAlignment + font { + family: Appearance.font.family.expressive + pixelSize: 90 + weight: Font.Bold + } + color: bgRoot.colText + style: Text.Raised + styleColor: Appearance.colors.colShadow + text: DateTime.time + } + StyledText { + Layout.fillWidth: true + Layout.topMargin: -5 + horizontalAlignment: bgRoot.textHorizontalAlignment + font { + family: Appearance.font.family.expressive + pixelSize: 20 + weight: Font.DemiBold + } + color: bgRoot.colText + style: Text.Raised + styleColor: Appearance.colors.colShadow + text: DateTime.date + } + } + + RowLayout { + anchors { + top: clockColumn.bottom + left: bgRoot.textHorizontalAlignment === Text.AlignLeft ? clockColumn.left : undefined + right: bgRoot.textHorizontalAlignment === Text.AlignRight ? clockColumn.right : undefined + horizontalCenter: bgRoot.textHorizontalAlignment === Text.AlignHCenter ? clockColumn.horizontalCenter : undefined + topMargin: 5 + leftMargin: -5 + rightMargin: -5 + } + opacity: GlobalStates.screenLocked ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignLeft; implicitWidth: 1 } + MaterialSymbol { + text: "lock" + Layout.fillWidth: false + iconSize: Appearance.font.pixelSize.huge + color: bgRoot.colText + } + StyledText { + Layout.fillWidth: false + text: "Locked" + color: bgRoot.colText + font { + pixelSize: Appearance.font.pixelSize.larger + } + } + Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignRight; implicitWidth: 1 } + + } + } + } + + // Password prompt + StyledText { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 30 + } + opacity: (GlobalStates.screenLocked && !GlobalStates.screenLockContainsCharacters) ? 1 : 0 + scale: opacity + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + text: "Enter password" + color: CF.ColorUtils.transparentize(bgRoot.colText, 0.3) + font { + pixelSize: Appearance.font.pixelSize.normal + } + } + } + } +} diff --git a/.config/quickshell/modules/bar/ActiveWindow.qml b/.config/quickshell/ii/modules/bar/ActiveWindow.qml similarity index 52% rename from .config/quickshell/modules/bar/ActiveWindow.qml rename to .config/quickshell/ii/modules/bar/ActiveWindow.qml index 95e25c6f..636b2313 100644 --- a/.config/quickshell/modules/bar/ActiveWindow.qml +++ b/.config/quickshell/ii/modules/bar/ActiveWindow.qml @@ -1,5 +1,7 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs import QtQuick import QtQuick.Layouts import Quickshell.Wayland @@ -11,6 +13,10 @@ Item { readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}` + property bool focusingThisMonitor: HyprlandData.activeWorkspace.monitor == monitor.name + property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor.id]?.activeWorkspace.id) + implicitWidth: colLayout.implicitWidth ColumnLayout { @@ -26,7 +32,10 @@ Item { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colSubtext elide: Text.ElideRight - text: root.activeWindow?.activated ? root.activeWindow?.appId : qsTr("Desktop") + text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ? + root.activeWindow?.appId : + (root.biggestWindow?.class) ?? Translation.tr("Desktop") + } StyledText { @@ -34,7 +43,9 @@ Item { font.pixelSize: Appearance.font.pixelSize.small color: Appearance.colors.colOnLayer0 elide: Text.ElideRight - text: root.activeWindow?.activated ? root.activeWindow?.title : `${qsTr("Workspace")} ${monitor.activeWorkspace?.id}` + text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ? + root.activeWindow?.title : + (root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor.activeWorkspace?.id}` } } diff --git a/.config/quickshell/ii/modules/bar/Bar.qml b/.config/quickshell/ii/modules/bar/Bar.qml new file mode 100644 index 00000000..f14a538d --- /dev/null +++ b/.config/quickshell/ii/modules/bar/Bar.qml @@ -0,0 +1,622 @@ +import "./weather" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland +import Quickshell.Services.UPower +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +Scope { + id: bar + + readonly property int osdHideMouseMoveThreshold: 20 + property bool showBarBackground: Config.options.bar.showBackground + + component VerticalBarSeparator: Rectangle { + Layout.topMargin: Appearance.sizes.baseBarHeight / 3 + Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3 + Layout.fillHeight: true + implicitWidth: 1 + color: Appearance.colors.colOutlineVariant + } + + Variants { + // For each monitor + model: { + const screens = Quickshell.screens; + const list = Config.options.bar.screenList; + if (!list || list.length === 0) + return screens; + return screens.filter(screen => list.includes(screen.name)); + } + LazyLoader { + id: barLoader + active: GlobalStates.barOpen && !GlobalStates.screenLocked + required property ShellScreen modelData + component: PanelWindow { // Bar window + id: barRoot + screen: barLoader.modelData + + property var brightnessMonitor: Brightness.getMonitorForScreen(barLoader.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 + + exclusionMode: ExclusionMode.Ignore + exclusiveZone: Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0) + WlrLayershell.namespace: "quickshell:bar" + implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding + mask: Region { + item: barContent + } + color: "transparent" + + anchors { + top: !Config.options.bar.bottom + bottom: Config.options.bar.bottom + left: true + right: true + } + + Item { // Bar content region + id: barContent + anchors { + right: parent.right + left: parent.left + top: parent.top + bottom: undefined + } + implicitHeight: Appearance.sizes.barHeight + height: Appearance.sizes.barHeight + + states: State { + name: "bottom" + when: Config.options.bar.bottom + AnchorChanges { + target: barContent + anchors { + right: parent.right + left: parent.left + top: undefined + bottom: parent.bottom + } + } + } + + // Background shadow + Loader { + active: showBarBackground && Config.options.bar.cornerStyle === 1 + anchors.fill: barBackground + sourceComponent: StyledRectangularShadow { + anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor + target: barBackground + } + } + // Background + Rectangle { + id: barBackground + anchors { + fill: parent + margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed + } + color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" + radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0 + border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0 + border.color: Appearance.m3colors.m3outlineVariant + } + + MouseArea { // Left side | scroll to change brightness + id: barLeftSideMouseArea + anchors.left: parent.left + implicitHeight: Appearance.sizes.baseBarHeight + height: Appearance.sizes.barHeight + width: (barRoot.width - middleSection.width) / 2 + property bool hovered: false + property real lastScrollX: 0 + property real lastScrollY: 0 + property bool trackingScroll: false + acceptedButtons: Qt.LeftButton + hoverEnabled: true + propagateComposedEvents: true + onEntered: event => { + barLeftSideMouseArea.hovered = true; + } + onExited: event => { + barLeftSideMouseArea.hovered = false; + barLeftSideMouseArea.trackingScroll = false; + } + onPressed: event => { + if (event.button === Qt.LeftButton) { + Hyprland.dispatch('global quickshell:sidebarLeftOpen'); + } + } + // Scroll to change brightness + WheelHandler { + onWheel: event => { + if (event.angleDelta.y < 0) + barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05); + else if (event.angleDelta.y > 0) + barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness + 0.05); + // Store the mouse position and start tracking + barLeftSideMouseArea.lastScrollX = event.x; + barLeftSideMouseArea.lastScrollY = event.y; + barLeftSideMouseArea.trackingScroll = true; + } + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + 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'); + barLeftSideMouseArea.trackingScroll = false; + } + } + } + Item { + // Left section + anchors.fill: parent + implicitHeight: leftSectionRowLayout.implicitHeight + implicitWidth: leftSectionRowLayout.implicitWidth + + ScrollHint { + reveal: barLeftSideMouseArea.hovered + icon: "light_mode" + tooltipText: Translation.tr("Scroll to change brightness") + side: "left" + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { // Content + id: leftSectionRowLayout + anchors.fill: parent + spacing: 10 + + 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 + colRipple: Appearance.colors.colLayer1Active + colBackgroundToggled: Appearance.colors.colSecondaryContainer + colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover + colRippleToggled: Appearance.colors.colSecondaryContainerActive + toggled: GlobalStates.sidebarLeftOpen + property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 + + onPressed: { + Hyprland.dispatch('global quickshell:sidebarLeftToggle'); + } + + CustomIcon { + id: distroIcon + anchors.centerIn: parent + width: 19.5 + height: 19.5 + source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : "spark-symbolic" + colorize: true + color: Appearance.colors.colOnLayer0 + } + } + + ActiveWindow { + visible: barRoot.useShortenedForm === 0 + Layout.rightMargin: Appearance.rounding.screenRounding + Layout.fillWidth: true + Layout.fillHeight: true + bar: barRoot + } + } + } + } + + RowLayout { // Middle section + id: middleSection + anchors.centerIn: parent + spacing: Config.options?.bar.borderless ? 4 : 8 + + BarGroup { + id: leftCenterGroup + Layout.preferredWidth: barRoot.centerSideModuleWidth + Layout.fillHeight: true + + Resources { + alwaysShowAllResources: barRoot.useShortenedForm === 2 + Layout.fillWidth: barRoot.useShortenedForm === 2 + } + + Media { + visible: barRoot.useShortenedForm < 2 + Layout.fillWidth: true + } + } + + VerticalBarSeparator { + visible: Config.options?.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 + anchors.fill: parent + acceptedButtons: Qt.RightButton + + onPressed: event => { + if (event.button === Qt.RightButton) { + Hyprland.dispatch('global quickshell:overviewToggle'); + } + } + } + } + } + + VerticalBarSeparator { + visible: Config.options?.bar.borderless + } + + MouseArea { + id: rightCenterGroup + implicitWidth: rightCenterGroupContent.implicitWidth + implicitHeight: rightCenterGroupContent.implicitHeight + Layout.preferredWidth: barRoot.centerSideModuleWidth + Layout.fillHeight: true + + onPressed: { + Hyprland.dispatch('global quickshell:sidebarRightToggle'); + } + + BarGroup { + id: rightCenterGroupContent + anchors.fill: parent + + ClockWidget { + showDate: (Config.options.bar.verbose && barRoot.useShortenedForm < 2) + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + } + + UtilButtons { + visible: (Config.options.bar.verbose && barRoot.useShortenedForm === 0) + Layout.alignment: Qt.AlignVCenter + } + + BatteryIndicator { + visible: (barRoot.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery) + Layout.alignment: Qt.AlignVCenter + } + } + } + + VerticalBarSeparator { + visible: Config.options.bar.borderless && Config.options.bar.weather.enable + } + } + + MouseArea { // Right side | scroll to change volume + id: barRightSideMouseArea + + anchors.right: parent.right + implicitHeight: Appearance.sizes.baseBarHeight + height: Appearance.sizes.barHeight + width: (barRoot.width - middleSection.width) / 2 + + property bool hovered: false + property real lastScrollX: 0 + property real lastScrollY: 0 + property bool trackingScroll: false + + acceptedButtons: Qt.LeftButton + hoverEnabled: true + propagateComposedEvents: true + onEntered: event => { + barRightSideMouseArea.hovered = true; + } + onExited: event => { + barRightSideMouseArea.hovered = false; + barRightSideMouseArea.trackingScroll = false; + } + onPressed: event => { + if (event.button === Qt.LeftButton) { + Hyprland.dispatch('global quickshell:sidebarRightOpen'); + } else if (event.button === Qt.RightButton) { + MprisController.activePlayer.next(); + } + } + // Scroll to change volume + WheelHandler { + onWheel: event => { + const currentVolume = Audio.value; + const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2; + if (event.angleDelta.y < 0) + Audio.sink.audio.volume -= step; + else if (event.angleDelta.y > 0) + Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step); + // Store the mouse position and start tracking + barRightSideMouseArea.lastScrollX = event.x; + barRightSideMouseArea.lastScrollY = event.y; + barRightSideMouseArea.trackingScroll = true; + } + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + 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'); + barRightSideMouseArea.trackingScroll = false; + } + } + } + + Item { + anchors.fill: parent + implicitHeight: rightSectionRowLayout.implicitHeight + implicitWidth: rightSectionRowLayout.implicitWidth + + ScrollHint { + reveal: barRightSideMouseArea.hovered + icon: "volume_up" + tooltipText: Translation.tr("Scroll to change volume") + side: "right" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { + id: rightSectionRowLayout + anchors.fill: parent + spacing: 5 + layoutDirection: Qt.RightToLeft + + RippleButton { // Right sidebar button + id: rightSidebarButton + + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.rightMargin: Appearance.rounding.screenRounding + Layout.fillWidth: false + + implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2 + implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2 + + buttonRadius: Appearance.rounding.full + colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1) + colBackgroundHover: Appearance.colors.colLayer1Hover + colRipple: Appearance.colors.colLayer1Active + colBackgroundToggled: Appearance.colors.colSecondaryContainer + colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover + colRippleToggled: Appearance.colors.colSecondaryContainerActive + toggled: GlobalStates.sidebarRightOpen + property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 + + Behavior on colText { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + onPressed: { + Hyprland.dispatch('global quickshell:sidebarRightToggle'); + } + + RowLayout { + id: indicatorsRowLayout + anchors.centerIn: parent + property real realSpacing: 15 + spacing: 0 + + Revealer { + reveal: Audio.sink?.audio?.muted ?? false + Layout.fillHeight: true + Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0 + Behavior on Layout.rightMargin { + NumberAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + MaterialSymbol { + text: "volume_off" + iconSize: Appearance.font.pixelSize.larger + color: rightSidebarButton.colText + } + } + Revealer { + reveal: Audio.source?.audio?.muted ?? false + Layout.fillHeight: true + Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0 + Behavior on Layout.rightMargin { + NumberAnimation { + duration: Appearance.animation.elementMoveFast.duration + easing.type: Appearance.animation.elementMoveFast.type + easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve + } + } + MaterialSymbol { + text: "mic_off" + iconSize: Appearance.font.pixelSize.larger + color: rightSidebarButton.colText + } + } + Loader { + active: HyprlandXkb.layoutCodes.length > 1 + visible: active + Layout.rightMargin: indicatorsRowLayout.realSpacing + sourceComponent: StyledText { + text: HyprlandXkb.currentLayoutCode + font.pixelSize: Appearance.font.pixelSize.smaller + color: rightSidebarButton.colText + } + } + MaterialSymbol { + Layout.rightMargin: indicatorsRowLayout.realSpacing + text: Network.materialSymbol + iconSize: Appearance.font.pixelSize.larger + color: rightSidebarButton.colText + } + MaterialSymbol { + text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" + iconSize: Appearance.font.pixelSize.larger + color: rightSidebarButton.colText + } + } + } + + SysTray { + bar: barRoot + visible: barRoot.useShortenedForm === 0 + Layout.fillWidth: false + Layout.fillHeight: true + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Weather + Loader { + Layout.leftMargin: 8 + Layout.fillHeight: true + active: Config.options.bar.weather.enable + sourceComponent: BarGroup { + implicitHeight: Appearance.sizes.baseBarHeight + WeatherBar {} + } + } + } + } + } + } + + // Round decorators + Loader { + id: roundDecorators + anchors { + left: parent.left + right: parent.right + } + y: Appearance.sizes.barHeight + width: parent.width + height: Appearance.rounding.screenRounding + active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug + + states: State { + name: "bottom" + when: Config.options.bar.bottom + PropertyChanges { + roundDecorators.y: 0 + } + } + + sourceComponent: Item { + implicitHeight: Appearance.rounding.screenRounding + RoundCorner { + id: leftCorner + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + + size: Appearance.rounding.screenRounding + color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" + + corner: RoundCorner.CornerEnum.TopLeft + states: State { + name: "bottom" + when: Config.options.bar.bottom + PropertyChanges { + leftCorner.corner: RoundCorner.CornerEnum.BottomLeft + } + } + } + RoundCorner { + id: rightCorner + anchors { + right: parent.right + top: !Config.options.bar.bottom ? parent.top : undefined + bottom: Config.options.bar.bottom ? parent.bottom : undefined + } + size: Appearance.rounding.screenRounding + color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" + + corner: RoundCorner.CornerEnum.TopRight + states: State { + name: "bottom" + when: Config.options.bar.bottom + PropertyChanges { + rightCorner.corner: RoundCorner.CornerEnum.BottomRight + } + } + } + } + } + } + } + } + + IpcHandler { + target: "bar" + + function toggle(): void { + GlobalStates.barOpen = !GlobalStates.barOpen + } + + function close(): void { + GlobalStates.barOpen = false + } + + function open(): void { + GlobalStates.barOpen = true + } + } + + GlobalShortcut { + name: "barToggle" + description: "Toggles bar on press" + + onPressed: { + GlobalStates.barOpen = !GlobalStates.barOpen; + } + } + + GlobalShortcut { + name: "barOpen" + description: "Opens bar on press" + + onPressed: { + GlobalStates.barOpen = true; + } + } + + GlobalShortcut { + name: "barClose" + description: "Closes bar on press" + + onPressed: { + GlobalStates.barOpen = false; + } + } +} diff --git a/.config/quickshell/modules/bar/BarGroup.qml b/.config/quickshell/ii/modules/bar/BarGroup.qml similarity index 76% rename from .config/quickshell/modules/bar/BarGroup.qml rename to .config/quickshell/ii/modules/bar/BarGroup.qml index a71b67e4..e2371d14 100644 --- a/.config/quickshell/modules/bar/BarGroup.qml +++ b/.config/quickshell/ii/modules/bar/BarGroup.qml @@ -1,13 +1,12 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common import QtQuick import QtQuick.Layouts Item { id: root property real padding: 5 - implicitHeight: 40 + implicitHeight: Appearance.sizes.baseBarHeight + height: Appearance.sizes.barHeight implicitWidth: rowLayout.implicitWidth + padding * 2 default property alias items: rowLayout.children @@ -18,7 +17,7 @@ Item { topMargin: 4 bottomMargin: 4 } - color: ConfigOptions?.bar.borderless ? "transparent" : Appearance.colors.colLayer1 + color: Config.options?.bar.borderless ? "transparent" : Appearance.colors.colLayer1 radius: Appearance.rounding.small } diff --git a/.config/quickshell/modules/bar/BatteryIndicator.qml b/.config/quickshell/ii/modules/bar/BatteryIndicator.qml similarity index 90% rename from .config/quickshell/modules/bar/BatteryIndicator.qml rename to .config/quickshell/ii/modules/bar/BatteryIndicator.qml index 61a98157..72cc932b 100644 --- a/.config/quickshell/modules/bar/BatteryIndicator.qml +++ b/.config/quickshell/ii/modules/bar/BatteryIndicator.qml @@ -1,20 +1,17 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Services.UPower Item { id: root - property bool borderless: ConfigOptions.bar.borderless + property bool borderless: Config.options.bar.borderless readonly property var chargeState: Battery.chargeState readonly property bool isCharging: Battery.isCharging readonly property bool isPluggedIn: Battery.isPluggedIn readonly property real percentage: Battery.percentage - readonly property bool isLow: percentage <= ConfigOptions.battery.low / 100 + readonly property bool isLow: percentage <= Config.options.battery.low / 100 readonly property color batteryLowBackground: Appearance.m3colors.darkmode ? Appearance.m3colors.m3error : Appearance.m3colors.m3errorContainer readonly property color batteryLowOnBackground: Appearance.m3colors.darkmode ? Appearance.m3colors.m3errorContainer : Appearance.m3colors.m3error @@ -42,6 +39,7 @@ Item { } CircularProgress { + enableAnimation: false Layout.alignment: Qt.AlignVCenter lineWidth: 2 value: percentage diff --git a/.config/quickshell/modules/bar/CircleUtilButton.qml b/.config/quickshell/ii/modules/bar/CircleUtilButton.qml similarity index 55% rename from .config/quickshell/modules/bar/CircleUtilButton.qml rename to .config/quickshell/ii/modules/bar/CircleUtilButton.qml index 3027c130..bd80a6c4 100644 --- a/.config/quickshell/modules/bar/CircleUtilButton.qml +++ b/.config/quickshell/ii/modules/bar/CircleUtilButton.qml @@ -1,11 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets/" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io RippleButton { id: button diff --git a/.config/quickshell/modules/bar/ClockWidget.qml b/.config/quickshell/ii/modules/bar/ClockWidget.qml similarity index 79% rename from .config/quickshell/modules/bar/ClockWidget.qml rename to .config/quickshell/ii/modules/bar/ClockWidget.qml index 9e340383..9f45aa0a 100644 --- a/.config/quickshell/modules/bar/ClockWidget.qml +++ b/.config/quickshell/ii/modules/bar/ClockWidget.qml @@ -1,13 +1,13 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Layouts Item { id: root - property bool borderless: ConfigOptions.bar.borderless - property bool showDate: ConfigOptions.bar.verbose + property bool borderless: Config.options.bar.borderless + property bool showDate: Config.options.bar.verbose implicitWidth: rowLayout.implicitWidth implicitHeight: 32 diff --git a/.config/quickshell/modules/bar/Media.qml b/.config/quickshell/ii/modules/bar/Media.qml similarity index 87% rename from .config/quickshell/modules/bar/Media.qml rename to .config/quickshell/ii/modules/bar/Media.qml index 3bd8a78a..08e9b92d 100644 --- a/.config/quickshell/modules/bar/Media.qml +++ b/.config/quickshell/ii/modules/bar/Media.qml @@ -1,23 +1,23 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs +import qs.modules.common.functions + import QtQuick import QtQuick.Layouts -import Quickshell -import Quickshell.Io import Quickshell.Services.Mpris import Quickshell.Hyprland Item { id: root - property bool borderless: ConfigOptions.bar.borderless + property bool borderless: Config.options.bar.borderless readonly property MprisPlayer activePlayer: MprisController.activePlayer - readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || qsTr("No media") + readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || Translation.tr("No media") Layout.fillHeight: true implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 - implicitHeight: 40 + implicitHeight: Appearance.sizes.barHeight Timer { running: activePlayer?.playbackState == MprisPlaybackState.Playing @@ -56,6 +56,7 @@ Item { size: 26 secondaryColor: Appearance.colors.colSecondaryContainer primaryColor: Appearance.m3colors.m3onSecondaryContainer + enableAnimation: false MaterialSymbol { anchors.centerIn: parent @@ -68,6 +69,7 @@ Item { } StyledText { + visible: Config.options.bar.verbose width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2) Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true // Ensures the text takes up available space diff --git a/.config/quickshell/modules/bar/Resource.qml b/.config/quickshell/ii/modules/bar/Resource.qml similarity index 93% rename from .config/quickshell/modules/bar/Resource.qml rename to .config/quickshell/ii/modules/bar/Resource.qml index fed7a153..eb3683d8 100644 --- a/.config/quickshell/modules/bar/Resource.qml +++ b/.config/quickshell/ii/modules/bar/Resource.qml @@ -1,9 +1,7 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Layouts -import Quickshell -import Quickshell.Io Item { required property string iconName @@ -26,6 +24,7 @@ Item { size: 26 secondaryColor: Appearance.colors.colSecondaryContainer primaryColor: Appearance.m3colors.m3onSecondaryContainer + enableAnimation: false MaterialSymbol { anchors.centerIn: parent diff --git a/.config/quickshell/modules/bar/Resources.qml b/.config/quickshell/ii/modules/bar/Resources.qml similarity index 74% rename from .config/quickshell/modules/bar/Resources.qml rename to .config/quickshell/ii/modules/bar/Resources.qml index 9f9b969c..f57372a1 100644 --- a/.config/quickshell/modules/bar/Resources.qml +++ b/.config/quickshell/ii/modules/bar/Resources.qml @@ -1,15 +1,12 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Services.Mpris Item { id: root - property bool borderless: ConfigOptions.bar.borderless + property bool borderless: Config.options.bar.borderless property bool alwaysShowAllResources: false implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin implicitHeight: 32 @@ -30,7 +27,7 @@ Item { Resource { iconName: "swap_horiz" percentage: ResourceUsage.swapUsedPercentage - shown: (ConfigOptions.bar.resources.alwaysShowSwap && percentage > 0) || + shown: (Config.options.bar.resources.alwaysShowSwap && percentage > 0) || (MprisController.activePlayer?.trackTitle == null) || root.alwaysShowAllResources Layout.leftMargin: shown ? 4 : 0 @@ -39,7 +36,7 @@ Item { Resource { iconName: "settings_slow_motion" percentage: ResourceUsage.cpuUsage - shown: ConfigOptions.bar.resources.alwaysShowCpu || + shown: Config.options.bar.resources.alwaysShowCpu || !(MprisController.activePlayer?.trackTitle?.length > 0) || root.alwaysShowAllResources Layout.leftMargin: shown ? 4 : 0 diff --git a/.config/quickshell/modules/bar/ScrollHint.qml b/.config/quickshell/ii/modules/bar/ScrollHint.qml similarity index 93% rename from .config/quickshell/modules/bar/ScrollHint.qml rename to .config/quickshell/ii/modules/bar/ScrollHint.qml index f5f47c74..a8e1c8df 100644 --- a/.config/quickshell/modules/bar/ScrollHint.qml +++ b/.config/quickshell/ii/modules/bar/ScrollHint.qml @@ -1,8 +1,7 @@ -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts Revealer { // Scroll hint diff --git a/.config/quickshell/modules/bar/SysTray.qml b/.config/quickshell/ii/modules/bar/SysTray.qml similarity index 86% rename from .config/quickshell/modules/bar/SysTray.qml rename to .config/quickshell/ii/modules/bar/SysTray.qml index 4f76ddfa..34919a3c 100644 --- a/.config/quickshell/modules/bar/SysTray.qml +++ b/.config/quickshell/ii/modules/bar/SysTray.qml @@ -1,11 +1,8 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Layouts -import Quickshell.Hyprland import Quickshell.Services.SystemTray -import Quickshell.Wayland -import Quickshell.Widgets // TODO: More fancy animation Item { diff --git a/.config/quickshell/modules/bar/SysTrayItem.qml b/.config/quickshell/ii/modules/bar/SysTrayItem.qml similarity index 84% rename from .config/quickshell/modules/bar/SysTrayItem.qml rename to .config/quickshell/ii/modules/bar/SysTrayItem.qml index 0778fbfe..9696c49c 100644 --- a/.config/quickshell/modules/bar/SysTrayItem.qml +++ b/.config/quickshell/ii/modules/bar/SysTrayItem.qml @@ -1,5 +1,5 @@ -import "root:/modules/common/" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.functions import QtQuick import QtQuick.Layouts import Quickshell @@ -43,7 +43,7 @@ MouseArea { IconImage { id: trayIcon - visible: !ConfigOptions.bar.tray.monochromeIcons + visible: !Config.options.bar.tray.monochromeIcons source: root.item.icon anchors.centerIn: parent width: parent.width @@ -51,7 +51,7 @@ MouseArea { } Loader { - active: ConfigOptions.bar.tray.monochromeIcons + active: Config.options.bar.tray.monochromeIcons anchors.fill: trayIcon sourceComponent: Item { Desaturate { @@ -59,12 +59,12 @@ MouseArea { visible: false // There's already color overlay anchors.fill: parent source: trayIcon - desaturation: 1 // 1.0 means fully grayscale + desaturation: 0.8 // 1.0 means fully grayscale } ColorOverlay { anchors.fill: desaturatedIcon source: desaturatedIcon - color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.6) + color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.9) } } } diff --git a/.config/quickshell/ii/modules/bar/UtilButtons.qml b/.config/quickshell/ii/modules/bar/UtilButtons.qml new file mode 100644 index 00000000..dcda858d --- /dev/null +++ b/.config/quickshell/ii/modules/bar/UtilButtons.qml @@ -0,0 +1,107 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Services.Pipewire + +Item { + id: root + property bool borderless: Config.options.bar.borderless + implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 + implicitHeight: rowLayout.implicitHeight + + RowLayout { + id: rowLayout + + spacing: 4 + anchors.centerIn: parent + + Loader { + active: Config.options.bar.utilButtons.showScreenSnip + visible: Config.options.bar.utilButtons.showScreenSnip + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")]) + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 1 + text: "screenshot_region" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + + Loader { + active: Config.options.bar.utilButtons.showColorPicker + visible: Config.options.bar.utilButtons.showColorPicker + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: Quickshell.execDetached(["hyprpicker", "-a"]) + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 1 + text: "colorize" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + + Loader { + active: Config.options.bar.utilButtons.showKeyboardToggle + visible: Config.options.bar.utilButtons.showKeyboardToggle + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: Hyprland.dispatch("global quickshell:oskToggle") + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 0 + text: "keyboard" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + + Loader { + active: Config.options.bar.utilButtons.showMicToggle + visible: Config.options.bar.utilButtons.showMicToggle + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_SOURCE@", "toggle"]) + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 0 + text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + + Loader { + active: Config.options.bar.utilButtons.showDarkModeToggle + visible: Config.options.bar.utilButtons.showDarkModeToggle + sourceComponent: CircleUtilButton { + Layout.alignment: Qt.AlignVCenter + onClicked: event => { + if (Appearance.m3colors.darkmode) { + Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`); + } else { + Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`); + } + } + MaterialSymbol { + horizontalAlignment: Qt.AlignHCenter + fill: 0 + text: Appearance.m3colors.darkmode ? "light_mode" : "dark_mode" + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer2 + } + } + } + } +} diff --git a/.config/quickshell/modules/bar/Workspaces.qml b/.config/quickshell/ii/modules/bar/Workspaces.qml similarity index 78% rename from .config/quickshell/modules/bar/Workspaces.qml rename to .config/quickshell/ii/modules/bar/Workspaces.qml index 93de99c0..8758fe52 100644 --- a/.config/quickshell/modules/bar/Workspaces.qml +++ b/.config/quickshell/ii/modules/bar/Workspaces.qml @@ -1,25 +1,24 @@ -import "root:/" -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland import Quickshell.Hyprland -import Quickshell.Io import Quickshell.Widgets import Qt5Compat.GraphicalEffects Item { required property var bar - property bool borderless: ConfigOptions.bar.borderless + property bool borderless: Config.options.bar.borderless readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) readonly property Toplevel activeWindow: ToplevelManager.activeToplevel - readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / ConfigOptions.bar.workspaces.shown) + readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown) property list workspaceOccupied: [] property int widgetPadding: 4 property int workspaceButtonWidth: 26 @@ -27,12 +26,12 @@ Item { property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 property real workspaceIconOpacityShrinked: 1 property real workspaceIconMarginShrinked: -4 - property int workspaceIndexInGroup: (monitor.activeWorkspace?.id - 1) % ConfigOptions.bar.workspaces.shown + property int workspaceIndexInGroup: (monitor.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown // Function to update workspaceOccupied function updateWorkspaceOccupied() { - workspaceOccupied = Array.from({ length: ConfigOptions.bar.workspaces.shown }, (_, i) => { - return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * ConfigOptions.bar.workspaces.shown + i + 1); + workspaceOccupied = Array.from({ length: Config.options.bar.workspaces.shown }, (_, i) => { + return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1); }) } @@ -48,7 +47,7 @@ Item { } implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 - implicitHeight: 40 + implicitHeight: Appearance.sizes.barHeight // Scroll to switch workspaces WheelHandler { @@ -78,10 +77,10 @@ Item { spacing: 0 anchors.fill: parent - implicitHeight: 40 + implicitHeight: Appearance.sizes.barHeight Repeater { - model: ConfigOptions.bar.workspaces.shown + model: Config.options.bar.workspaces.shown Rectangle { z: 1 @@ -157,14 +156,14 @@ Item { spacing: 0 anchors.fill: parent - implicitHeight: 40 + implicitHeight: Appearance.sizes.barHeight Repeater { - model: ConfigOptions.bar.workspaces.shown + model: Config.options.bar.workspaces.shown Button { id: button - property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspaces.shown + index + 1 + property int workspaceValue: workspaceGroup * Config.options.bar.workspaces.shown + index + 1 Layout.fillHeight: true onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`) width: workspaceButtonWidth @@ -173,20 +172,13 @@ Item { id: workspaceButtonBackground implicitWidth: workspaceButtonWidth implicitHeight: workspaceButtonWidth - property var biggestWindow: { - const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == button.workspaceValue) - return windowsInThisWorkspace.reduce((maxWin, win) => { - const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0) - const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0) - return winArea > maxArea ? win : maxWin - }, null) - } + property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue) property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing") StyledText { // Workspace number text opacity: GlobalStates.workspaceShowNumbers - || ((ConfigOptions?.bar.workspaces.alwaysShowNumbers && (!ConfigOptions?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || GlobalStates.workspaceShowNumbers)) - || (GlobalStates.workspaceShowNumbers && !ConfigOptions?.bar.workspaces.showAppIcons) + || ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || GlobalStates.workspaceShowNumbers)) + || (GlobalStates.workspaceShowNumbers && !Config.options?.bar.workspaces.showAppIcons) ) ? 1 : 0 z: 3 @@ -206,9 +198,10 @@ Item { } } Rectangle { // Dot instead of ws number - opacity: (ConfigOptions?.bar.workspaces.alwaysShowNumbers + id: wsDot + opacity: (Config.options?.bar.workspaces.alwaysShowNumbers || GlobalStates.workspaceShowNumbers - || (ConfigOptions?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow) + || (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow) ) ? 0 : 1 visible: opacity > 0 anchors.centerIn: parent @@ -228,21 +221,21 @@ Item { anchors.centerIn: parent width: workspaceButtonWidth height: workspaceButtonWidth - opacity: !ConfigOptions?.bar.workspaces.showAppIcons ? 0 : - (workspaceButtonBackground.biggestWindow && !GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ? + opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : + (workspaceButtonBackground.biggestWindow && !GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? 1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0 visible: opacity > 0 IconImage { id: mainAppIcon anchors.bottom: parent.bottom anchors.right: parent.right - anchors.bottomMargin: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ? + anchors.bottomMargin: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked - anchors.rightMargin: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ? + anchors.rightMargin: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked source: workspaceButtonBackground.mainAppIconSource - implicitSize: (!GlobalStates.workspaceShowNumbers && ConfigOptions?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked + implicitSize: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) @@ -257,6 +250,25 @@ Item { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } + + Loader { + active: Config.options.bar.workspaces.monochromeIcons + anchors.fill: mainAppIcon + sourceComponent: Item { + Desaturate { + id: desaturatedIcon + visible: false // There's already color overlay + anchors.fill: parent + source: mainAppIcon + desaturation: 0.8 + } + ColorOverlay { + anchors.fill: desaturatedIcon + source: desaturatedIcon + color: ColorUtils.transparentize(wsDot.color, 0.9) + } + } + } } } diff --git a/.config/quickshell/ii/modules/bar/weather/WeatherBar.qml b/.config/quickshell/ii/modules/bar/weather/WeatherBar.qml new file mode 100644 index 00000000..363d9ba5 --- /dev/null +++ b/.config/quickshell/ii/modules/bar/weather/WeatherBar.qml @@ -0,0 +1,60 @@ +pragma ComponentBehavior: Bound +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import Quickshell +import QtQuick +import QtQuick.Layouts + +MouseArea { + id: root + property real margin: 10 + property bool hovered: false + implicitWidth: rowLayout.implicitWidth + margin * 2 + implicitHeight: rowLayout.implicitHeight + + hoverEnabled: true + + RowLayout { + id: rowLayout + anchors.centerIn: parent + + MaterialSymbol { + fill: 0 + text: WeatherIcons.codeToName[Weather.data.wCode] + iconSize: Appearance.font.pixelSize.large + color: Appearance.colors.colOnLayer1 + Layout.alignment: Qt.AlignVCenter + } + + StyledText { + visible: true + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnLayer1 + text: Weather.data.temp + Layout.alignment: Qt.AlignVCenter + } + } + + LazyLoader { + id: popupLoader + active: root.containsMouse + + component: PopupWindow { + id: popupWindow + visible: true + implicitWidth: weatherPopup.implicitWidth + implicitHeight: weatherPopup.implicitHeight + anchor.item: root + anchor.edges: Edges.Top + anchor.rect.x: (root.implicitWidth - popupWindow.implicitWidth) / 2 + anchor.rect.y: Config.options.bar.bottom ? + (-weatherPopup.implicitHeight - 15) : + (root.implicitHeight + 15 ) + color: "transparent" + WeatherPopup { + id: weatherPopup + } + } + } +} diff --git a/.config/quickshell/ii/modules/bar/weather/WeatherCard.qml b/.config/quickshell/ii/modules/bar/weather/WeatherCard.qml new file mode 100644 index 00000000..a85ed8ca --- /dev/null +++ b/.config/quickshell/ii/modules/bar/weather/WeatherCard.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Layouts + +import qs.modules.common +import qs.modules.common.widgets + +Rectangle { + id: root + radius: Appearance.rounding.small + color: Appearance.colors.colLayer1 + 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 + } + StyledText { + id: title + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colOnLayer1 + } + } + StyledText { + id: value + Layout.alignment: Qt.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer1 + } + } +} diff --git a/.config/quickshell/ii/modules/bar/weather/WeatherIcons.qml b/.config/quickshell/ii/modules/bar/weather/WeatherIcons.qml new file mode 100644 index 00000000..bd74d4e1 --- /dev/null +++ b/.config/quickshell/ii/modules/bar/weather/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/ii/modules/bar/weather/WeatherPopup.qml b/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml new file mode 100644 index 00000000..623e1190 --- /dev/null +++ b/.config/quickshell/ii/modules/bar/weather/WeatherPopup.qml @@ -0,0 +1,97 @@ +import qs +import qs.services +import qs.modules.common +import qs.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: Appearance.rounding.small + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + clip: true + + 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 + } + + StyledText { + text: Weather.data.city + font.pixelSize: Appearance.font.pixelSize.title + font.family: Appearance.font.family.title + color: Appearance.colors.colOnLayer0 + } + } + + // Metrics grid + GridLayout { + id: gridLayout + columns: 2 + rowSpacing: 5 + columnSpacing: 5 + uniformCellWidths: true + + WeatherCard { + title: Translation.tr("UV Index") + symbol: "wb_sunny" + value: Weather.data.uv + } + WeatherCard { + title: Translation.tr("Wind") + symbol: "air" + value: `(${Weather.data.windDir}) ${Weather.data.wind}` + } + WeatherCard { + title: Translation.tr("Precipitation") + symbol: "rainy_light" + value: Weather.data.precip + } + WeatherCard { + title: Translation.tr("Humidity") + symbol: "humidity_low" + value: Weather.data.humidity + } + WeatherCard { + title: Translation.tr("Visibility") + symbol: "visibility" + value: Weather.data.visib + } + WeatherCard { + title: Translation.tr("Pressure") + symbol: "readiness_score" + value: Weather.data.press + } + WeatherCard { + title: Translation.tr("Sunrise") + symbol: "wb_twilight" + value: Weather.data.sunrise + } + WeatherCard { + title: Translation.tr("Sunset") + symbol: "bedtime" + value: Weather.data.sunset + } + } + } +} diff --git a/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml b/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml new file mode 100644 index 00000000..9eef904c --- /dev/null +++ b/.config/quickshell/ii/modules/cheatsheet/Cheatsheet.qml @@ -0,0 +1,236 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell.Io +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { // Scope + id: root + property var tabButtonList: [ + { + "icon": "keyboard", + "name": Translation.tr("Keybinds") + }, + { + "icon": "experiment", + "name": Translation.tr("Elements") + }, + ] + property int selectedTab: 0 + + Loader { + id: cheatsheetLoader + active: false + + sourceComponent: PanelWindow { // Window + id: cheatsheetRoot + visible: cheatsheetLoader.active + + anchors { + top: true + bottom: true + left: true + right: true + } + + function hide() { + cheatsheetLoader.active = false; + } + exclusiveZone: 0 + implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2 + implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2 + WlrLayershell.namespace: "quickshell:cheatsheet" + // Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab + // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + color: "transparent" + + mask: Region { + item: cheatsheetBackground + } + + HyprlandFocusGrab { // Click outside to close + id: grab + windows: [cheatsheetRoot] + active: cheatsheetRoot.visible + onCleared: () => { + if (!active) + cheatsheetRoot.hide(); + } + } + + // Background + StyledRectangularShadow { + target: cheatsheetBackground + } + Rectangle { + id: cheatsheetBackground + anchors.centerIn: parent + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + radius: Appearance.rounding.windowRounding + property real padding: 30 + implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2 + implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2 + + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + cheatsheetRoot.hide(); + } + if (event.modifiers === Qt.ControlModifier) { + if (event.key === Qt.Key_PageDown) { + root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1); + event.accepted = true; + } else if (event.key === Qt.Key_PageUp) { + root.selectedTab = Math.max(root.selectedTab - 1, 0); + event.accepted = true; + } else if (event.key === Qt.Key_Tab) { + root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length; + event.accepted = true; + } else if (event.key === Qt.Key_Backtab) { + root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length; + event.accepted = true; + } + } + } + + RippleButton { // Close button + id: closeButton + focus: cheatsheetRoot.visible + implicitWidth: 40 + implicitHeight: 40 + buttonRadius: Appearance.rounding.full + anchors { + top: parent.top + right: parent.right + topMargin: 20 + rightMargin: 20 + } + + onClicked: { + cheatsheetRoot.hide(); + } + + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.title + text: "close" + } + } + + ColumnLayout { // Real content + id: cheatsheetColumnLayout + anchors.centerIn: parent + spacing: 20 + + StyledText { + id: cheatsheetTitle + Layout.alignment: Qt.AlignHCenter + font.family: Appearance.font.family.title + font.pixelSize: Appearance.font.pixelSize.title + text: Translation.tr("Cheat sheet") + } + PrimaryTabBar { // Tab strip + id: tabBar + tabButtonList: root.tabButtonList + externalTrackedTab: root.selectedTab + function onCurrentIndexChanged(currentIndex) { + root.selectedTab = currentIndex; + } + } + + SwipeView { // Content pages + id: swipeView + Layout.topMargin: 5 + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 10 + + Behavior on implicitWidth { + id: contentWidthBehavior + enabled: false + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on implicitHeight { + id: contentHeightBehavior + enabled: false + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + currentIndex: tabBar.externalTrackedTab + onCurrentIndexChanged: { + contentWidthBehavior.enabled = true; + contentHeightBehavior.enabled = true; + tabBar.enableIndicatorAnimation = true; + root.selectedTab = currentIndex; + } + + clip: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: swipeView.width + height: swipeView.height + radius: Appearance.rounding.small + } + } + + CheatsheetKeybinds {} + CheatsheetPeriodicTable {} + } + } + } + } + } + + IpcHandler { + target: "cheatsheet" + + function toggle(): void { + cheatsheetLoader.active = !cheatsheetLoader.active; + } + + function close(): void { + cheatsheetLoader.active = false; + } + + function open(): void { + cheatsheetLoader.active = true; + } + } + + GlobalShortcut { + name: "cheatsheetToggle" + description: "Toggles cheatsheet on press" + + onPressed: { + cheatsheetLoader.active = !cheatsheetLoader.active; + } + } + + GlobalShortcut { + name: "cheatsheetOpen" + description: "Opens cheatsheet on press" + + onPressed: { + cheatsheetLoader.active = true; + } + } + + GlobalShortcut { + name: "cheatsheetClose" + description: "Closes cheatsheet on press" + + onPressed: { + cheatsheetLoader.active = false; + } + } +} diff --git a/.config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml b/.config/quickshell/ii/modules/cheatsheet/CheatsheetKeybinds.qml similarity index 95% rename from .config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml rename to .config/quickshell/ii/modules/cheatsheet/CheatsheetKeybinds.qml index 03b03115..e0e8ce6c 100644 --- a/.config/quickshell/modules/cheatsheet/CheatsheetKeybinds.qml +++ b/.config/quickshell/ii/modules/cheatsheet/CheatsheetKeybinds.qml @@ -1,15 +1,10 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import Quickshell.Hyprland Item { id: root diff --git a/.config/quickshell/ii/modules/cheatsheet/CheatsheetPeriodicTable.qml b/.config/quickshell/ii/modules/cheatsheet/CheatsheetPeriodicTable.qml new file mode 100644 index 00000000..a0a8ecf3 --- /dev/null +++ b/.config/quickshell/ii/modules/cheatsheet/CheatsheetPeriodicTable.qml @@ -0,0 +1,68 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import "periodic_table.js" as PTable +import QtQuick +import QtQuick.Layouts + +Item { + id: root + readonly property var elements: PTable.elements + readonly property var series: PTable.series + property real spacing: 6 + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + + ColumnLayout { + id: mainLayout + spacing: root.spacing + + Repeater { // Main table rows + model: root.elements + + delegate: RowLayout { // Table cells + id: tableRow + spacing: root.spacing + required property var modelData + + Repeater { + model: tableRow.modelData + delegate: ElementTile { + required property var modelData + element: modelData + } + + } + } + + } + + Item { + id: gap + implicitHeight: 20 + } + + Repeater { // Main table rows + model: root.series + + delegate: RowLayout { // Table cells + id: seriesTableRow + spacing: root.spacing + required property var modelData + + Repeater { + model: seriesTableRow.modelData + delegate: ElementTile { + required property var modelData + element: modelData + } + + } + } + + } + } + +} \ No newline at end of file diff --git a/.config/quickshell/ii/modules/cheatsheet/ElementTile.qml b/.config/quickshell/ii/modules/cheatsheet/ElementTile.qml new file mode 100644 index 00000000..70e7b4d6 --- /dev/null +++ b/.config/quickshell/ii/modules/cheatsheet/ElementTile.qml @@ -0,0 +1,55 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick + +RippleButton { + id: root + required property var element + opacity: element.type != "empty" ? 1 : 0 + implicitHeight: 60 + implicitWidth: 60 + colBackground: Appearance.colors.colLayer2 + buttonRadius: Appearance.rounding.small + + Rectangle { + anchors { + top: parent.top + left: parent.left + topMargin: 4 + leftMargin: 4 + } + color: Appearance.colors.colLayer2 + radius: Appearance.rounding.full + implicitWidth: Math.max(20, elementNumber.implicitWidth) + implicitHeight: Math.max(20, elementNumber.implicitHeight) + width: height + + StyledText { + id: elementNumber + anchors.centerIn: parent + color: Appearance.colors.colOnLayer2 + text: root.element.number + font.pixelSize: Appearance.font.pixelSize.smallest + } + } + + StyledText { + id: elementSymbol + anchors.centerIn: parent + color: Appearance.colors.colSecondary + font.pixelSize: Appearance.font.pixelSize.huge + text: root.element.symbol + } + + StyledText { + id: elementName + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: 4 + } + font.pixelSize: Appearance.font.pixelSize.smallest + color: Appearance.colors.colOnLayer2 + text: root.element.name + } +} diff --git a/.config/quickshell/ii/modules/cheatsheet/periodic_table.js b/.config/quickshell/ii/modules/cheatsheet/periodic_table.js new file mode 100644 index 00000000..45d69cc9 --- /dev/null +++ b/.config/quickshell/ii/modules/cheatsheet/periodic_table.js @@ -0,0 +1,196 @@ +// List of rows +const elements = [ + [ + { name: 'Hydrogen', symbol: 'H', number: 1, weight: 1.01, type: 'nonmetal' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: 'Helium', symbol: 'He', number: 2, weight: 4.00, type: 'noblegas' }, + ], + [ + { name: 'Lithium', symbol: 'Li', number: 3, weight: 6.94, type: 'metal' }, + { name: 'Beryllium', symbol: 'Be', number: 4, weight: 9.01, type: 'metal' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: 'Boron', symbol: 'B', number: 5, weight: 10.81, type: 'nonmetal' }, + { name: 'Carbon', symbol: 'C', number: 6, weight: 12.01, type: 'nonmetal' }, + { name: 'Nitrogen', symbol: 'N', number: 7, weight: 14.01, type: 'nonmetal' }, + { name: 'Oxygen', symbol: 'O', number: 8, weight: 16, type: 'nonmetal' }, + { name: 'Fluorine', symbol: 'F', number: 9, weight: 19, type: 'nonmetal' }, + { name: 'Neon', symbol: 'Ne', number: 10, weight: 20.18, type: 'noblegas' }, + + + ], + [ + { name: 'Sodium', symbol: 'Na', number: 11, weight: 22.99, type: 'metal' }, + { name: 'Magnesium', symbol: 'Mg', number: 12, weight: 24.31, type: 'metal' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: 'Aluminum', symbol: 'Al', number: 13, weight: 26.98, type: 'metal' }, + { name: 'Silicon', symbol: 'Si', number: 14, weight: 28.09, type: 'nonmetal' }, + { name: 'Phosphorus', symbol: 'P', number: 15, weight: 30.97, type: 'nonmetal' }, + { name: 'Sulfur', symbol: 'S', number: 16, weight: 32.07, type: 'nonmetal' }, + { name: 'Chlorine', symbol: 'Cl', number: 17, weight: 35.45, type: 'nonmetal' }, + { name: 'Argon', symbol: 'Ar', number: 18, weight: 39.95, type: 'noblegas' }, + ], + [ + { name: 'Potassium', symbol: 'K', number: 19, weight: 39.098, type: 'metal' }, + { name: 'Calcium', symbol: 'Ca', number: 20, weight: 40.078, type: 'metal' }, + { name: 'Scandium', symbol: 'Sc', number: 21, weight: 44.956, type: 'metal' }, + { name: 'Titanium', symbol: 'Ti', number: 22, weight: 47.87, type: 'metal' }, + { name: 'Vanadium', symbol: 'V', number: 23, weight: 50.94, type: 'metal' }, + { name: 'Chromium', symbol: 'Cr', number: 24, weight: 52, type: 'metal'/*, icon: 'chromium-browser'*/ }, + { name: 'Manganese', symbol: 'Mn', number: 25, weight: 54.94, type: 'metal' }, + { name: 'Iron', symbol: 'Fe', number: 26, weight: 55.85, type: 'metal' }, + { name: 'Cobalt', symbol: 'Co', number: 27, weight: 58.93, type: 'metal' }, + { name: 'Nickel', symbol: 'Ni', number: 28, weight: 58.69, type: 'metal' }, + { name: 'Copper', symbol: 'Cu', number: 29, weight: 63.55, type: 'metal' }, + { name: 'Zinc', symbol: 'Zn', number: 30, weight: 65.38, type: 'metal' }, + { name: 'Gallium', symbol: 'Ga', number: 31, weight: 69.72, type: 'metal' }, + { name: 'Germanium', symbol: 'Ge', number: 32, weight: 72.63, type: 'metal' }, + { name: 'Arsenic', symbol: 'As', number: 33, weight: 74.92, type: 'nonmetal' }, + { name: 'Selenium', symbol: 'Se', number: 34, weight: 78.96, type: 'nonmetal' }, + { name: 'Bromine', symbol: 'Br', number: 35, weight: 79.904, type: 'nonmetal' }, + { name: 'Krypton', symbol: 'Kr', number: 36, weight: 83.8, type: 'noblegas' }, + ], + [ + { name: 'Rubidium', symbol: 'Rb', number: 37, weight: 85.47, type: 'metal' }, + { name: 'Strontium', symbol: 'Sr', number: 38, weight: 87.62, type: 'metal' }, + { name: 'Yttrium', symbol: 'Y', number: 39, weight: 88.91, type: 'metal' }, + { name: 'Zirconium', symbol: 'Zr', number: 40, weight: 91.22, type: 'metal' }, + { name: 'Niobium', symbol: 'Nb', number: 41, weight: 92.91, type: 'metal' }, + { name: 'Molybdenum', symbol: 'Mo', number: 42, weight: 95.94, type: 'metal' }, + { name: 'Technetium', symbol: 'Tc', number: 43, weight: 98, type: 'metal' }, + { name: 'Ruthenium', symbol: 'Ru', number: 44, weight: 101.07, type: 'metal' }, + { name: 'Rhodium', symbol: 'Rh', number: 45, weight: 102.91, type: 'metal' }, + { name: 'Palladium', symbol: 'Pd', number: 46, weight: 106.42, type: 'metal' }, + { name: 'Silver', symbol: 'Ag', number: 47, weight: 107.87, type: 'metal' }, + { name: 'Cadmium', symbol: 'Cd', number: 48, weight: 112.41, type: 'metal' }, + { name: 'Indium', symbol: 'In', number: 49, weight: 114.82, type: 'metal' }, + { name: 'Tin', symbol: 'Sn', number: 50, weight: 118.71, type: 'metal' }, + { name: 'Antimony', symbol: 'Sb', number: 51, weight: 121.76, type: 'metal' }, + { name: 'Tellurium', symbol: 'Te', number: 52, weight: 127.6, type: 'nonmetal' }, + { name: 'Iodine', symbol: 'I', number: 53, weight: 126.9, type: 'nonmetal' }, + { name: 'Xenon', symbol: 'Xe', number: 54, weight: 131.29, type: 'noblegas' }, + ], + [ + { name: 'Cesium', symbol: 'Cs', number: 55, weight: 132.91, type: 'metal' }, + { name: 'Barium', symbol: 'Ba', number: 56, weight: 137.33, type: 'metal' }, + { name: 'Lanthanum', symbol: 'La', number: 57, weight: 138.91, type: 'lanthanum' }, + { name: 'Hafnium', symbol: 'Hf', number: 72, weight: 178.49, type: 'metal' }, + { name: 'Tantalum', symbol: 'Ta', number: 73, weight: 180.95, type: 'metal' }, + { name: 'Tungsten', symbol: 'W', number: 74, weight: 183.84, type: 'metal' }, + { name: 'Rhenium', symbol: 'Re', number: 75, weight: 186.21, type: 'metal' }, + { name: 'Osmium', symbol: 'Os', number: 76, weight: 190.23, type: 'metal' }, + { name: 'Iridium', symbol: 'Ir', number: 77, weight: 192.22, type: 'metal' }, + { name: 'Platinum', symbol: 'Pt', number: 78, weight: 195.09, type: 'metal' }, + { name: 'Gold', symbol: 'Au', number: 79, weight: 196.97, type: 'metal' }, + { name: 'Mercury', symbol: 'Hg', number: 80, weight: 200.59, type: 'metal' }, + { name: 'Thallium', symbol: 'Tl', number: 81, weight: 204.38, type: 'metal' }, + { name: 'Lead', symbol: 'Pb', number: 82, weight: 207.2, type: 'metal' }, + { name: 'Bismuth', symbol: 'Bi', number: 83, weight: 208.98, type: 'metal' }, + { name: 'Polonium', symbol: 'Po', number: 84, weight: 209, type: 'metal' }, + { name: 'Astatine', symbol: 'At', number: 85, weight: 210, type: 'nonmetal' }, + { name: 'Radon', symbol: 'Rn', number: 86, weight: 222, type: 'noblegas' }, + ], + [ + { name: 'Francium', symbol: 'Fr', number: 87, weight: 223, type: 'metal' }, + { name: 'Radium', symbol: 'Ra', number: 88, weight: 226, type: 'metal' }, + { name: 'Actinium', symbol: 'Ac', number: 89, weight: 227, type: 'actinium' }, + { name: 'Rutherfordium', symbol: 'Rf', number: 104, weight: 267, type: 'metal' }, + { name: 'Dubnium', symbol: 'Db', number: 105, weight: 268, type: 'metal' }, + { name: 'Seaborgium', symbol: 'Sg', number: 106, weight: 271, type: 'metal' }, + { name: 'Bohrium', symbol: 'Bh', number: 107, weight: 272, type: 'metal' }, + { name: 'Hassium', symbol: 'Hs', number: 108, weight: 277, type: 'metal' }, + { name: 'Meitnerium', symbol: 'Mt', number: 109, weight: 278, type: 'metal' }, + { name: 'Darmstadtium', symbol: 'Ds', number: 110, weight: 281, type: 'metal' }, + { name: 'Roentgenium', symbol: 'Rg', number: 111, weight: 280, type: 'metal' }, + { name: 'Copernicium', symbol: 'Cn', number: 112, weight: 285, type: 'metal' }, + { name: 'Nihonium', symbol: 'Nh', number: 113, weight: 286, type: 'metal' }, + { name: 'Flerovium', symbol: 'Fl', number: 114, weight: 289, type: 'metal' }, + { name: 'Moscovium', symbol: 'Mc', number: 115, weight: 290, type: 'metal' }, + { name: 'Livermorium', symbol: 'Lv', number: 116, weight: 293, type: 'metal' }, + { name: 'Tennessine', symbol: 'Ts', number: 117, weight: 294, type: 'metal' }, + { name: 'Oganesson', symbol: 'Og', number: 118, weight: 294, type: 'noblegas' }, + ], +] + +const series = [ + [ + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: 'Cerium', symbol: 'Ce', number: 58, weight: 140.12, type: 'lanthanum' }, + { name: 'Praseodymium', symbol: 'Pr', number: 59, weight: 140.91, type: 'lanthanum' }, + { name: 'Neodymium', symbol: 'Nd', number: 60, weight: 144.24, type: 'lanthanum' }, + { name: 'Promethium', symbol: 'Pm', number: 61, weight: 145, type: 'lanthanum' }, + { name: 'Samarium', symbol: 'Sm', number: 62, weight: 150.36, type: 'lanthanum' }, + { name: 'Europium', symbol: 'Eu', number: 63, weight: 151.96, type: 'lanthanum' }, + { name: 'Gadolinium', symbol: 'Gd', number: 64, weight: 157.25, type: 'lanthanum' }, + { name: 'Terbium', symbol: 'Tb', number: 65, weight: 158.93, type: 'lanthanum' }, + { name: 'Dysprosium', symbol: 'Dy', number: 66, weight: 162.5, type: 'lanthanum' }, + { name: 'Holmium', symbol: 'Ho', number: 67, weight: 164.93, type: 'lanthanum' }, + { name: 'Erbium', symbol: 'Er', number: 68, weight: 167.26, type: 'lanthanum' }, + { name: 'Thulium', symbol: 'Tm', number: 69, weight: 168.93, type: 'lanthanum' }, + { name: 'Ytterbium', symbol: 'Yb', number: 70, weight: 173.04, type: 'lanthanum' }, + { name: 'Lutetium', symbol: 'Lu', number: 71, weight: 174.97, type: 'lanthanum' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + ], + [ + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + { name: 'Thorium', symbol: 'Th', number: 90, weight: 232.04, type: 'actinium' }, + { name: 'Protactinium', symbol: 'Pa', number: 91, weight: 231.04, type: 'actinium' }, + { name: 'Uranium', symbol: 'U', number: 92, weight: 238.03, type: 'actinium' }, + { name: 'Neptunium', symbol: 'Np', number: 93, weight: 237, type: 'actinium' }, + { name: 'Plutonium', symbol: 'Pu', number: 94, weight: 244, type: 'actinium' }, + { name: 'Americium', symbol: 'Am', number: 95, weight: 243, type: 'actinium' }, + { name: 'Curium', symbol: 'Cm', number: 96, weight: 247, type: 'actinium' }, + { name: 'Berkelium', symbol: 'Bk', number: 97, weight: 247, type: 'actinium' }, + { name: 'Californium', symbol: 'Cf', number: 98, weight: 251, type: 'actinium' }, + { name: 'Einsteinium', symbol: 'Es', number: 99, weight: 252, type: 'actinium' }, + { name: 'Fermium', symbol: 'Fm', number: 100, weight: 257, type: 'actinium' }, + { name: 'Mendelevium', symbol: 'Md', number: 101, weight: 258, type: 'actinium' }, + { name: 'Nobelium', symbol: 'No', number: 102, weight: 259, type: 'actinium' }, + { name: 'Lawrencium', symbol: 'Lr', number: 103, weight: 262, type: 'actinium' }, + { name: '', symbol: '', number: -1, weight: 0, type: 'empty' }, + ], +]; + +const niceTypes = { + 'metal': "Metal", + 'nonmetal': "Nonmetal", + 'noblegas': "Noble gas", + 'lanthanum': "Lanthanum", + 'actinium': "Actinium" +} diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/ii/modules/common/Appearance.qml similarity index 90% rename from .config/quickshell/modules/common/Appearance.qml rename to .config/quickshell/ii/modules/common/Appearance.qml index 54463013..72cc78e3 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/ii/modules/common/Appearance.qml @@ -1,6 +1,6 @@ import QtQuick import Quickshell -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common.functions pragma Singleton pragma ComponentBehavior: Bound @@ -16,8 +16,8 @@ Singleton { property string syntaxHighlightingTheme // Extremely conservative transparency values for consistency and readability - property real transparency: ConfigOptions?.appearance.transparency ? (m3colors.darkmode ? 0.1 : 0) : 0 - property real contentTransparency: ConfigOptions?.appearance.transparency ? (m3colors.darkmode ? 0.55 : 0) : 0 + property real transparency: Config.options?.appearance.transparency ? (m3colors.darkmode ? 0.1 : 0.07) : 0 + property real contentTransparency: Config.options?.appearance.transparency ? (m3colors.darkmode ? 0.55 : 0.55) : 0 m3colors: QtObject { property bool darkmode: false @@ -100,7 +100,7 @@ Singleton { colors: QtObject { property color colSubtext: m3colors.m3outline - property color colLayer0: ColorUtils.transparentize(m3colors.m3background, root.transparency) + property color colLayer0: ColorUtils.mix(ColorUtils.transparentize(m3colors.m3background, root.transparency), m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1) property color colOnLayer0: m3colors.m3onBackground property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency)) property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency)) @@ -126,11 +126,12 @@ Singleton { property color colPrimaryContainer: m3colors.m3primaryContainer property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Hover, 0.7) property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colLayer1Active, 0.6) + property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer property color colSecondary: m3colors.m3secondary property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85) property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4) - property color colSecondaryContainer: ColorUtils.transparentize(m3colors.m3secondaryContainer, root.contentTransparency) - property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, colLayer1Hover, 0.6) + property color colSecondaryContainer: m3colors.m3secondaryContainer + property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90) property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, colLayer1Active, 0.54) property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer property color colSurfaceContainerLow: ColorUtils.transparentize(m3colors.m3surfaceContainerLow, root.contentTransparency) @@ -167,10 +168,11 @@ Singleton { property string iconNerd: "SpaceMono NF" property string monospace: "JetBrains Mono NF" property string reading: "Readex Pro" + property string expressive: "Space Grotesk" } property QtObject pixelSize: QtObject { property int smallest: 10 - property int smaller: 13 + property int smaller: 12 property int small: 15 property int normal: 16 property int large: 17 @@ -187,16 +189,22 @@ Singleton { readonly property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82] + readonly property list emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1] readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] readonly property list standard: [0.2, 0, 0, 1, 1, 1] readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] readonly property list standardDecel: [0, 0, 0, 1, 1, 1] + readonly property real expressiveFastSpatialDuration: 350 + readonly property real expressiveDefaultSpatialDuration: 500 + readonly property real expressiveSlowSpatialDuration: 650 + readonly property real expressiveEffectsDuration: 200 } animation: QtObject { property QtObject elementMove: QtObject { - property int duration: 500 + property int duration: animationCurves.expressiveDefaultSpatialDuration property int type: Easing.BezierSpline property list bezierCurve: animationCurves.expressiveDefaultSpatial property int velocity: 650 @@ -242,7 +250,7 @@ Singleton { } } property QtObject elementMoveFast: QtObject { - property int duration: 200 + property int duration: animationCurves.expressiveEffectsDuration property int type: Easing.BezierSpline property list bezierCurve: animationCurves.expressiveEffects property int velocity: 850 @@ -281,8 +289,10 @@ Singleton { } sizes: QtObject { - property real barHeight: 40 - property real barCenterSideModuleWidth: ConfigOptions?.bar.verbose ? 360 : 140 + property real baseBarHeight: 40 + property real barHeight: Config.options.bar.cornerStyle === 1 ? + (baseBarHeight + Appearance.sizes.hyprlandGapsOut * 2) : baseBarHeight + property real barCenterSideModuleWidth: Config.options?.bar.verbose ? 360 : 140 property real barCenterSideModuleWidthShortened: 280 property real barCenterSideModuleWidthHellaShortened: 190 property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value diff --git a/.config/quickshell/ii/modules/common/Config.qml b/.config/quickshell/ii/modules/common/Config.qml new file mode 100644 index 00000000..7462d26e --- /dev/null +++ b/.config/quickshell/ii/modules/common/Config.qml @@ -0,0 +1,253 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property string filePath: Directories.shellConfigPath + property alias options: configOptionsJsonAdapter + property bool ready: false + + function setNestedValue(nestedKey, value) { + let keys = nestedKey.split("."); + let obj = root.options; + let parents = [obj]; + + // Traverse and collect parent objects + for (let i = 0; i < keys.length - 1; ++i) { + if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { + obj[keys[i]] = {}; + } + obj = obj[keys[i]]; + parents.push(obj); + } + + // Convert value to correct type using JSON.parse when safe + let convertedValue = value; + if (typeof value === "string") { + let trimmed = value.trim(); + if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) { + try { + convertedValue = JSON.parse(trimmed); + } catch (e) { + convertedValue = value; + } + } + } + + obj[keys[keys.length - 1]] = convertedValue; + } + + FileView { + path: root.filePath + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: writeAdapter() + onLoaded: root.ready = true + onLoadFailed: error => { + if (error == FileViewError.FileNotFound) { + writeAdapter(); + } + } + + JsonAdapter { + id: configOptionsJsonAdapter + property JsonObject policies: JsonObject { + property int ai: 1 // 0: No | 1: Yes | 2: Local + property int weeb: 1 // 0: No | 1: Open | 2: Closet + } + + property JsonObject ai: JsonObject { + property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n\n## Tools\nMay or may not be available depending on the user's settings. If they're available, follow these guidelines:\n\n### Search\n- When user asks for information that might benefit from up-to-date information, use this to get search access\n\n### Shell configuration\n- Always fetch the config options to see the available keys before setting\n- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it\n" + } + + property JsonObject appearance: JsonObject { + property bool extraBackgroundTint: true + property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen + property bool transparency: false + property JsonObject wallpaperTheming: JsonObject { + property bool enableAppsAndShell: true + property bool enableQtApps: true + property bool enableTerminal: true + } + property JsonObject palette: JsonObject { + property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot + } + } + + property JsonObject audio: JsonObject { + // Values in % + property JsonObject protection: JsonObject { + // Prevent sudden bangs + property bool enable: true + property real maxAllowedIncrease: 10 + property real maxAllowed: 90 // Realistically should already provide some protection when it's 99... + } + } + + property JsonObject apps: JsonObject { + property string bluetooth: "kcmshell6 kcm_bluetooth" + property string network: "plasmawindowed org.kde.plasma.networkmanagement" + property string networkEthernet: "kcmshell6 kcm_networkmanagement" + property string taskManager: "plasma-systemmonitor --page-name Processes" + property string terminal: "kitty -1" // This is only for shell actions + } + + property JsonObject background: JsonObject { + property bool fixedClockPosition: false + property real clockX: -500 + property real clockY: -500 + property string wallpaperPath: "" + property JsonObject parallax: JsonObject { + property bool enableWorkspace: true + property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size + property bool enableSidebar: true + } + } + + property JsonObject bar: JsonObject { + property bool bottom: false // Instead of top + property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle + property bool borderless: false // true for no grouping of items + property string topLeftIcon: "spark" // Options: distro, spark + property bool showBackground: true + property bool verbose: true + property JsonObject resources: JsonObject { + property bool alwaysShowSwap: true + property bool alwaysShowCpu: false + } + property list screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command + property JsonObject utilButtons: JsonObject { + property bool showScreenSnip: true + property bool showColorPicker: false + property bool showMicToggle: false + property bool showKeyboardToggle: true + property bool showDarkModeToggle: true + } + property JsonObject tray: JsonObject { + property bool monochromeIcons: true + } + property JsonObject workspaces: JsonObject { + property bool monochromeIcons: true + property int shown: 10 + property bool showAppIcons: true + property bool alwaysShowNumbers: false + property int showNumberDelay: 300 // milliseconds + } + property JsonObject weather: JsonObject { + property bool enable: false + property bool enableGPS: true // gps based location + property string city: "" // When 'enableGPS' is false + property bool useUSCS: false // Instead of metric (SI) units + property int fetchInterval: 10 // minutes + } + } + + property JsonObject battery: JsonObject { + property int low: 20 + property int critical: 5 + property bool automaticSuspend: true + property int suspend: 3 + } + + property JsonObject dock: JsonObject { + property bool enable: false + property bool monochromeIcons: true + property real height: 60 + property real hoverRegionHeight: 2 + property bool pinnedOnStartup: false + property bool hoverToReveal: true // When false, only reveals on empty workspace + property list pinnedApps: [ // IDs of pinned entries + "org.kde.dolphin", "kitty",] + property list ignoredAppRegexes: [] + } + + property JsonObject language: JsonObject { + property JsonObject translator: JsonObject { + property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google + property string targetLanguage: "auto" // Run `trans -list-all` for available languages + property string sourceLanguage: "auto" + } + } + + property JsonObject light: JsonObject { + property JsonObject night: JsonObject { + property bool automatic: true + property string from: "19:00" // Format: "HH:mm", 24-hour time + property string to: "06:30" // Format: "HH:mm", 24-hour time + property int colorTemperature: 5000 + } + } + + property JsonObject networking: JsonObject { + property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + } + + property JsonObject osd: JsonObject { + property int timeout: 1000 + } + + property JsonObject osk: JsonObject { + property string layout: "qwerty_full" + property bool pinnedOnStartup: false + } + + property JsonObject overview: JsonObject { + property real scale: 0.18 // Relative to screen size + property real rows: 2 + property real columns: 5 + } + + property JsonObject resources: JsonObject { + property int updateInterval: 3000 + } + + property JsonObject search: JsonObject { + property int nonAppResultDelay: 30 // This prevents lagging when typing + property string engineBaseUrl: "https://www.google.com/search?q=" + property list excludedSites: ["quora.com"] + property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird. + property JsonObject prefix: JsonObject { + property string action: "/" + property string clipboard: ";" + property string emojis: ":" + } + } + + property JsonObject sidebar: JsonObject { + property JsonObject translator: JsonObject { + property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag. + } + property JsonObject booru: JsonObject { + property bool allowNsfw: false + property string defaultProvider: "yandere" + property int limit: 20 + property JsonObject zerochan: JsonObject { + property string username: "[unset]" + } + } + } + + property JsonObject time: JsonObject { + // https://doc.qt.io/qt-6/qtime.html#toString + property string format: "hh:mm" + property string dateFormat: "ddd, dd/MM" + } + + property JsonObject windows: JsonObject { + property bool showTitlebar: true // Client-side decoration for shell apps + property bool centerTitle: true + } + + property JsonObject hacks: JsonObject { + property int arbitraryRaceConditionDelay: 20 // milliseconds + } + + property JsonObject screenshotTool: JsonObject { + property bool showContentRegions: true + } + } + } +} diff --git a/.config/quickshell/modules/common/Directories.qml b/.config/quickshell/ii/modules/common/Directories.qml similarity index 62% rename from .config/quickshell/modules/common/Directories.qml rename to .config/quickshell/ii/modules/common/Directories.qml index 32c90562..a1748ece 100644 --- a/.config/quickshell/modules/common/Directories.qml +++ b/.config/quickshell/ii/modules/common/Directories.qml @@ -1,11 +1,10 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common.functions import Qt.labs.platform import QtQuick import Quickshell -import Quickshell.Hyprland Singleton { // XDG Dirs, with "file://" @@ -16,6 +15,8 @@ Singleton { readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] // Other dirs used by the shell, without "file://" + property string assetsPath: Quickshell.shellPath("assets") + property string scriptPath: Quickshell.shellPath("scripts") property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`) property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`) property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`) @@ -29,15 +30,20 @@ Singleton { property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`) property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`) property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`) - property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/colors/switchwall.sh`) + property string screenshotTemp: "/tmp/quickshell/media/screenshot" + property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`) + property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts") + property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`) + property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`) // Cleanup on init Component.onCompleted: { - Hyprland.dispatch(`exec mkdir -p '${shellConfig}'`) - Hyprland.dispatch(`exec mkdir -p '${favicons}'`) - Hyprland.dispatch(`exec rm -rf '${coverArt}'; mkdir -p '${coverArt}'`) - Hyprland.dispatch(`exec rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`) - Hyprland.dispatch(`exec mkdir -p '${booruDownloads}' && mkdir -p '${booruDownloadsNsfw}'`) - Hyprland.dispatch(`exec rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`) - Hyprland.dispatch(`exec rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`) + Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`]) + Quickshell.execDetached(["mkdir", "-p", `${favicons}`]) + Quickshell.execDetached(["bash", "-c", `rm -rf '${coverArt}'; mkdir -p '${coverArt}'`]) + Quickshell.execDetached(["bash", "-c", `rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`]) + Quickshell.execDetached(["bash", "-c", `mkdir -p '${booruDownloads}' && mkdir -p '${booruDownloadsNsfw}'`]) + Quickshell.execDetached(["bash", "-c", `rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`]) + Quickshell.execDetached(["bash", "-c", `rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`]) + Quickshell.execDetached(["mkdir", "-p", `${aiChats}`]) } } diff --git a/.config/quickshell/ii/modules/common/Persistent.qml b/.config/quickshell/ii/modules/common/Persistent.qml new file mode 100644 index 00000000..abd062d7 --- /dev/null +++ b/.config/quickshell/ii/modules/common/Persistent.qml @@ -0,0 +1,49 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + property alias states: persistentStatesJsonAdapter + property string fileDir: Directories.state + property string fileName: "states.json" + property string filePath: `${root.fileDir}/${root.fileName}` + + FileView { + path: root.filePath + + watchChanges: true + onFileChanged: reload() + onAdapterUpdated: { + writeAdapter() + } + onLoadFailed: error => { + console.log("Failed to load persistent states file:", error); + if (error == FileViewError.FileNotFound) { + writeAdapter(); + } + } + + adapter: JsonAdapter { + id: persistentStatesJsonAdapter + property JsonObject ai: JsonObject { + property string model + property real temperature: 0.5 + } + + property JsonObject sidebar: JsonObject { + property JsonObject bottomGroup: JsonObject { + property bool collapsed: false + property int tab: 0 + } + } + + property JsonObject booru: JsonObject { + property bool allowNsfw: false + property string provider: "yandere" + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/functions/ColorUtils.qml b/.config/quickshell/ii/modules/common/functions/ColorUtils.qml new file mode 100644 index 00000000..27d48187 --- /dev/null +++ b/.config/quickshell/ii/modules/common/functions/ColorUtils.qml @@ -0,0 +1,114 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + /** + * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take hue from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); + } + + /** + * Returns a color with the saturation of color2 and the hue/value/alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take saturation from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); + } + + /** + * Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL). + * + * @param {string} color - The base color (any Qt.color-compatible string). + * @param {number} lightness - The lightness value to use (0-1). + * @returns {Qt.rgba} The resulting color. + */ + function colorWithLightness(color, lightness) { + var c = Qt.color(color); + return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a); + } + + /** + * Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL). + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take lightness from. + * @returns {Qt.rgba} The resulting color. + */ + function colorWithLightnessOf(color1, color2) { + var c2 = Qt.color(color2); + return colorWithLightness(color1, c2.hslLightness); + } + + /** + * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The accent color. + * @returns {Qt.rgba} The resulting color. + */ + function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + + return Qt.hsla(hue, sat, light, alpha); + } + + /** + * Mixes two colors by a given percentage. + * + * @param {string} color1 - The first color (any Qt.color-compatible string). + * @param {string} color2 - The second color. + * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. + * @returns {Qt.rgba} The resulting mixed color. + */ + function mix(color1, color2, percentage = 0.5) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); + } + + /** + * Transparentizes a color by a given percentage. + * + * @param {string} color - The color (any Qt.color-compatible string). + * @param {number} percentage - The amount to transparentize (0-1). + * @returns {Qt.rgba} The resulting color. + */ + function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); + } +} diff --git a/.config/quickshell/ii/modules/common/functions/FileUtils.qml b/.config/quickshell/ii/modules/common/functions/FileUtils.qml new file mode 100644 index 00000000..c051674e --- /dev/null +++ b/.config/quickshell/ii/modules/common/functions/FileUtils.qml @@ -0,0 +1,41 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + /** + * Trims the File protocol off the input string + * @param {string} str + * @returns {string} + */ + function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; + } + + /** + * Extracts the file name from a file path + * @param {string} str + * @returns {string} + */ + function fileNameForPath(str) { + if (typeof str !== "string") return ""; + const trimmed = trimFileProtocol(str); + return trimmed.split(/[\\/]/).pop(); + } + + /** + * Removes the file extension from a file path or name + * @param {string} str + * @returns {string} + */ + function trimFileExt(str) { + if (typeof str !== "string") return ""; + const trimmed = trimFileProtocol(str); + const lastDot = trimmed.lastIndexOf("."); + if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) { + return trimmed.slice(0, lastDot); + } + return trimmed; + } +} diff --git a/.config/quickshell/ii/modules/common/functions/ObjectUtils.qml b/.config/quickshell/ii/modules/common/functions/ObjectUtils.qml new file mode 100644 index 00000000..d1204cdd --- /dev/null +++ b/.config/quickshell/ii/modules/common/functions/ObjectUtils.qml @@ -0,0 +1,98 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function toPlainObject(qtObj) { + if (qtObj === null || typeof qtObj !== "object") return qtObj; + + // Handle true arrays + if (Array.isArray(qtObj)) { + return qtObj.map(item => toPlainObject(item)); + } + + // Handle array-like Qt objects (e.g., have length and numeric keys) + if ( + typeof qtObj.length === "number" && + qtObj.length > 0 && + Object.keys(qtObj).every( + key => !isNaN(key) || key === "length" + ) + ) { + let arr = []; + for (let i = 0; i < qtObj.length; i++) { + arr.push(toPlainObject(qtObj[i])); + } + return arr; + } + + const result = ({}); + for (let key in qtObj) { + if ( + typeof qtObj[key] !== "function" && + !key.startsWith("objectName") && + !key.startsWith("children") && + !key.startsWith("object") && + !key.startsWith("parent") && + !key.startsWith("metaObject") && + !key.startsWith("destroyed") && + !key.startsWith("reloadableId") + ) { + result[key] = toPlainObject(qtObj[key]); + } + } + // console.log(JSON.stringify(result)) + return result; + } + + function applyToQtObject(qtObj, jsonObj) { + // console.log("applyToQtObject", JSON.stringify(qtObj, null, 2), "<<", JSON.stringify(jsonObj, null, 2)); + if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return; + + // Detect array-like Qt objects + const isQtArrayLike = obj => { + return obj && typeof obj === "object" && + typeof obj.length === "number" && + obj.length > 0 && + Object.keys(obj).every(key => !isNaN(key) || key === "length"); + }; + + // If both are arrays or array-like, update in place or replace + if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) { + qtObj.length = 0; + for (let i = 0; i < jsonObj.length; i++) { + qtObj.push(jsonObj[i]); + } + return; + } + + // If target is array or array-like but source is not, clear + if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && !Array.isArray(jsonObj)) { + qtObj.length = 0; + return; + } + + // If source is array but target is not, assign directly if possible + if (!(Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) { + return jsonObj; + } + + for (let key in jsonObj) { + if (!qtObj.hasOwnProperty(key)) continue; + const value = qtObj[key]; + const jsonValue = jsonObj[key]; + // console.log("applying to qt obj key:", value, "jsonValue:", jsonValue); + if ((Array.isArray(value) || isQtArrayLike(value)) && Array.isArray(jsonValue)) { + value.length = 0; + for (let i = 0; i < jsonValue.length; i++) { + value.push(jsonValue[i]); + } + } else if (value && typeof value === "object" && !Array.isArray(value) && !isQtArrayLike(value)) { + applyToQtObject(value, jsonValue); + } else { + qtObj[key] = jsonValue; + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/functions/StringUtils.qml b/.config/quickshell/ii/modules/common/functions/StringUtils.qml new file mode 100644 index 00000000..e8241831 --- /dev/null +++ b/.config/quickshell/ii/modules/common/functions/StringUtils.qml @@ -0,0 +1,221 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + /** + * Formats a string according to the args that are passed inc + * @param { string } str + * @param {...any} args + * @returns + */ + function format(str, ...args) { + return str.replace(/{(\d+)}/g, (match, index) => typeof args[index] !== 'undefined' ? args[index] : match); + } + + /** + * Returns the domain of the passed in url or null + * @param { string } url + * @returns { string| null } + */ + function getDomain(url) { + const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); + return match ? match[1] : null; + } + + /** + * Returns the base url of the passed in url or null + * @param { string } url + * @returns { string | null } + */ + function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; + } + + /** + * Escapes single quotes in shell commands + * @param { string } str + * @returns { string } + */ + function shellSingleQuoteEscape(str) { + // escape single quotes + return String(str) + // .replace(/\\/g, '\\\\') + .replace(/'/g, "'\\''"); + } + + /** + * Splits markdown blocks into three different types: text, think, and code. + * @param { string } markdown + */ + function splitMarkdownBlocks(markdown) { + const regex = /```(\w+)?\n([\s\S]*?)```|([\s\S]*?)<\/think>/g; + /** + * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]} + */ + let result = []; + let lastIndex = 0; + let match; + while ((match = regex.exec(markdown)) !== null) { + if (match.index > lastIndex) { + const text = markdown.slice(lastIndex, match.index); + if (text.trim()) { + result.push({ + type: "text", + content: text + }); + } + } + if (match[0].startsWith('```')) { + if (match[2] && match[2].trim()) { + result.push({ + type: "code", + lang: match[1] || "", + content: match[2], + completed: true + }); + } + } else if (match[0].startsWith('')) { + if (match[3] && match[3].trim()) { + result.push({ + type: "think", + content: match[3], + completed: true + }); + } + } + lastIndex = regex.lastIndex; + } + // Handle any remaining text after the last match + if (lastIndex < markdown.length) { + const text = markdown.slice(lastIndex); + // Check for unfinished block + const thinkStart = text.indexOf(''); + const codeStart = text.indexOf('```'); + if (thinkStart !== -1 && (codeStart === -1 || thinkStart < codeStart)) { + const beforeThink = text.slice(0, thinkStart); + if (beforeThink.trim()) { + result.push({ + type: "text", + content: beforeThink + }); + } + const thinkContent = text.slice(thinkStart + 7); + if (thinkContent.trim()) { + result.push({ + type: "think", + content: thinkContent, + completed: false + }); + } + } else if (codeStart !== -1) { + const beforeCode = text.slice(0, codeStart); + if (beforeCode.trim()) { + result.push({ + type: "text", + content: beforeCode + }); + } + // Try to detect language after ``` + const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/); + let lang = ""; + let codeContentStart = codeStart + 3; + if (codeLangMatch) { + lang = codeLangMatch[1] || ""; + codeContentStart += codeLangMatch[0].length; + } else if (text[codeStart + 3] === '\n') { + codeContentStart += 1; + } + const codeContent = text.slice(codeContentStart); + if (codeContent.trim()) { + result.push({ + type: "code", + lang, + content: codeContent, + completed: false + }); + } + } else if (text.trim()) { + result.push({ + type: "text", + content: text + }); + } + } + // console.log(JSON.stringify(result, null, 2)); + return result; + } + + /** + * Returns the original string with backslashes escaped + * @param { string } str + * @returns { string } + */ + function escapeBackslashes(str) { + return str.replace(/\\/g, '\\\\'); + } + + /** + * Wraps words to supplied maximum length + * @param { string | null } str + * @param { number } maxLen + * @returns { string } + */ + function wordWrap(str, maxLen) { + if (!str) + return ""; + let words = str.split(" "); + let lines = []; + let current = ""; + for (let i = 0; i < words.length; ++i) { + if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) { + if (current.length > 0) + lines.push(current); + current = words[i]; + } else { + current += (current.length > 0 ? " " : "") + words[i]; + } + } + if (current.length > 0) + lines.push(current); + return lines.join("\n"); + } + + function cleanMusicTitle(title) { + if (!title) + return ""; + // Brackets + title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets + title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets + title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets + // Japenis brackets + title = title.replace(/^ *【[^】]*】/, ""); // Touhou + title = title.replace(/^ *《[^》]*》/, ""); // ?? + title = title.replace(/^ *「[^」]*」/, ""); // OP/ED thingie + title = title.replace(/^ *『[^』]*』/, ""); // OP/ED thingie + + return title.trim(); + } + + function friendlyTimeForSeconds(seconds) { + if (isNaN(seconds) || seconds < 0) + return "0:00"; + seconds = Math.floor(seconds); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + } else { + return `${m}:${s.toString().padStart(2, '0')}`; + } + } + + function escapeHtml(str) { + if (typeof str !== 'string') + return str; + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + } +} diff --git a/.config/quickshell/modules/common/functions/fuzzysort.js b/.config/quickshell/ii/modules/common/functions/fuzzysort.js similarity index 100% rename from .config/quickshell/modules/common/functions/fuzzysort.js rename to .config/quickshell/ii/modules/common/functions/fuzzysort.js diff --git a/.config/quickshell/modules/common/functions/levendist.js b/.config/quickshell/ii/modules/common/functions/levendist.js similarity index 100% rename from .config/quickshell/modules/common/functions/levendist.js rename to .config/quickshell/ii/modules/common/functions/levendist.js diff --git a/.config/quickshell/modules/common/widgets/ButtonGroup.qml b/.config/quickshell/ii/modules/common/widgets/ButtonGroup.qml similarity index 89% rename from .config/quickshell/modules/common/widgets/ButtonGroup.qml rename to .config/quickshell/ii/modules/common/widgets/ButtonGroup.qml index 5356535f..7dc7a591 100644 --- a/.config/quickshell/modules/common/widgets/ButtonGroup.qml +++ b/.config/quickshell/ii/modules/common/widgets/ButtonGroup.qml @@ -1,8 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts /** @@ -20,6 +18,7 @@ Rectangle { let total = 0; for (let i = 0; i < rowLayout.children.length; ++i) { const child = rowLayout.children[i]; + if (!child.visible) continue; total += child.baseWidth ?? child.implicitWidth ?? child.width; } return total + rowLayout.spacing * (rowLayout.children.length - 1); diff --git a/.config/quickshell/modules/common/widgets/CircularProgress.qml b/.config/quickshell/ii/modules/common/widgets/CircularProgress.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/CircularProgress.qml rename to .config/quickshell/ii/modules/common/widgets/CircularProgress.qml index c3731e64..7ff2724f 100644 --- a/.config/quickshell/modules/common/widgets/CircularProgress.qml +++ b/.config/quickshell/ii/modules/common/widgets/CircularProgress.qml @@ -2,7 +2,7 @@ // License: LGPL-3.0 - A copy can be found in `licenses` folder of repo import QtQuick -import "root:/modules/common" +import qs.modules.common /** * Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs @@ -18,6 +18,7 @@ Item { property real gapAngle: Math.PI / 9 property bool fill: false property int fillOverflow: 2 + property bool enableAnimation: true property int animationDuration: 1000 property var easingType: Easing.OutCubic @@ -83,6 +84,7 @@ Item { } Behavior on degree { + enabled: root.enableAnimation NumberAnimation { duration: root.animationDuration easing.type: root.easingType diff --git a/.config/quickshell/modules/common/widgets/CliphistImage.qml b/.config/quickshell/ii/modules/common/widgets/CliphistImage.qml similarity index 84% rename from .config/quickshell/modules/common/widgets/CliphistImage.qml rename to .config/quickshell/ii/modules/common/widgets/CliphistImage.qml index 9de34450..ce15ef3e 100644 --- a/.config/quickshell/modules/common/widgets/CliphistImage.qml +++ b/.config/quickshell/ii/modules/common/widgets/CliphistImage.qml @@ -1,16 +1,11 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs.modules.common.functions import Qt5Compat.GraphicalEffects -import Qt.labs.platform import QtQuick -import QtQuick.Controls -import QtQuick.Layouts +import Quickshell import Quickshell.Io -import Quickshell.Widgets -import Quickshell.Hyprland Rectangle { id: root @@ -71,7 +66,7 @@ Rectangle { } Component.onDestruction: { - Hyprland.dispatch(`exec bash -c "[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'"`) + Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`]) } Image { diff --git a/.config/quickshell/ii/modules/common/widgets/ConfigRow.qml b/.config/quickshell/ii/modules/common/widgets/ConfigRow.qml new file mode 100644 index 00000000..3cdc3f80 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ConfigRow.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Layouts + +RowLayout { + property bool uniform: false + spacing: 10 + uniformCellSizes: uniform +} diff --git a/.config/quickshell/ii/modules/common/widgets/ConfigSelectionArray.qml b/.config/quickshell/ii/modules/common/widgets/ConfigSelectionArray.qml new file mode 100644 index 00000000..318ffe1c --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ConfigSelectionArray.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Layouts +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +Flow { + id: root + Layout.fillWidth: true + spacing: 2 + property list options: [] + property string configOptionName: "" + property var currentValue: null + + signal selected(var newValue) + + Repeater { + model: root.options + delegate: SelectionGroupButton { + id: paletteButton + required property var modelData + required property int index + onYChanged: { + if (index === 0) { + paletteButton.leftmost = true + } else { + var prev = root.children[index - 1] + var thisIsOnNewLine = prev && prev.y !== paletteButton.y + paletteButton.leftmost = thisIsOnNewLine + prev.rightmost = thisIsOnNewLine + } + } + leftmost: index === 0 + rightmost: index === root.options.length - 1 + buttonText: modelData.displayName; + toggled: root.currentValue === modelData.value + onClicked: { + root.selected(modelData.value); + } + } + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/ConfigSpinBox.qml b/.config/quickshell/ii/modules/common/widgets/ConfigSpinBox.qml new file mode 100644 index 00000000..375f78ed --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ConfigSpinBox.qml @@ -0,0 +1,30 @@ +import qs.modules.common.widgets +import qs.modules.common +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + property string text: "" + property alias value: spinBoxWidget.value + property alias stepSize: spinBoxWidget.stepSize + property alias from: spinBoxWidget.from + property alias to: spinBoxWidget.to + spacing: 10 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + StyledText { + id: labelWidget + Layout.fillWidth: true + text: root.text + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnSecondaryContainer + } + + StyledSpinBox { + id: spinBoxWidget + Layout.fillWidth: false + value: root.value + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml b/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml new file mode 100644 index 00000000..e10f74d6 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ConfigSwitch.qml @@ -0,0 +1,32 @@ +import qs.modules.common.widgets +import qs.modules.common +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +RippleButton { + id: root + Layout.fillWidth: true + implicitHeight: contentItem.implicitHeight + 8 * 2 + onClicked: checked = !checked + + contentItem: RowLayout { + spacing: 10 + StyledText { + id: labelWidget + Layout.fillWidth: true + text: root.text + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnSecondaryContainer + } + StyledSwitch { + id: switchWidget + down: root.down + scale: 0.6 + Layout.fillWidth: false + checked: root.checked + onClicked: root.clicked() + } + } +} + diff --git a/.config/quickshell/ii/modules/common/widgets/ContentPage.qml b/.config/quickshell/ii/modules/common/widgets/ContentPage.qml new file mode 100644 index 00000000..dc87179f --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ContentPage.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +Flickable { + id: root + property real baseWidth: 550 + property bool forceWidth: false + property real bottomContentPadding: 100 + + default property alias data: contentColumn.data + + clip: true + contentHeight: contentColumn.implicitHeight + root.bottomContentPadding // Add some padding at the bottom + implicitWidth: contentColumn.implicitWidth + + ColumnLayout { + id: contentColumn + width: root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth) + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 10 + } + spacing: 20 + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/ContentSection.qml b/.config/quickshell/ii/modules/common/widgets/ContentSection.qml new file mode 100644 index 00000000..2f038e1f --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ContentSection.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +ColumnLayout { + id: root + property string title + default property alias data: sectionContent.data + + Layout.fillWidth: true + spacing: 8 + StyledText { + text: root.title + font.pixelSize: Appearance.font.pixelSize.larger + font.weight: Font.Medium + } + ColumnLayout { + id: sectionContent + spacing: 8 + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/ContentSubsection.qml b/.config/quickshell/ii/modules/common/widgets/ContentSubsection.qml new file mode 100644 index 00000000..b78f3aa8 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ContentSubsection.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +ColumnLayout { + id: root + property string title: "" + property string tooltip: "" + default property alias data: sectionContent.data + + Layout.fillWidth: true + Layout.topMargin: 4 + spacing: 2 + + RowLayout { + ContentSubsectionLabel { + visible: root.title && root.title.length > 0 + text: root.title + } + MaterialSymbol { + visible: root.tooltip && root.tooltip.length > 0 + text: "info" + iconSize: Appearance.font.pixelSize.large + + color: Appearance.colors.colSubtext + MouseArea { + id: infoMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.WhatsThisCursor + StyledToolTip { + extraVisibleCondition: false + alternativeVisibleCondition: infoMouseArea.containsMouse + content: root.tooltip + } + } + } + Item { Layout.fillWidth: true } + } + ColumnLayout { + id: sectionContent + Layout.fillWidth: true + spacing: 2 + } +} diff --git a/.config/quickshell/ii/modules/common/widgets/ContentSubsectionLabel.qml b/.config/quickshell/ii/modules/common/widgets/ContentSubsectionLabel.qml new file mode 100644 index 00000000..5d29e0e1 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/ContentSubsectionLabel.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +StyledText { + text: "Subsection" + color: Appearance.colors.colSubtext + Layout.leftMargin: 4 +} diff --git a/.config/quickshell/ii/modules/common/widgets/CustomIcon.qml b/.config/quickshell/ii/modules/common/widgets/CustomIcon.qml new file mode 100644 index 00000000..d7a1c634 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/CustomIcon.qml @@ -0,0 +1,37 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import Qt5Compat.GraphicalEffects + +Item { + id: root + + property bool colorize: false + property color color + property string source: "" + property string iconFolder: Qt.resolvedUrl(Quickshell.shellPath("assets/icons")) // The folder to check first + width: 30 + height: 30 + + IconImage { + id: iconImage + anchors.fill: parent + source: { + const fullPathWhenSourceIsIconName = iconFolder + "/" + root.source; + if (iconFolder && fullPathWhenSourceIsIconName) { + return fullPathWhenSourceIsIconName + } + return root.source + } + implicitSize: root.height + } + + Loader { + active: root.colorize + anchors.fill: iconImage + sourceComponent: ColorOverlay { + source: iconImage + color: root.color + } + } +} diff --git a/.config/quickshell/modules/common/widgets/DialogButton.qml b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml similarity index 83% rename from .config/quickshell/modules/common/widgets/DialogButton.qml rename to .config/quickshell/ii/modules/common/widgets/DialogButton.qml index 86150c50..972c29b2 100644 --- a/.config/quickshell/modules/common/widgets/DialogButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/DialogButton.qml @@ -1,10 +1,5 @@ -import "root:/modules/common" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io /** * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview diff --git a/.config/quickshell/modules/common/widgets/DragManager.qml b/.config/quickshell/ii/modules/common/widgets/DragManager.qml similarity index 94% rename from .config/quickshell/modules/common/widgets/DragManager.qml rename to .config/quickshell/ii/modules/common/widgets/DragManager.qml index 087729ed..9a430d93 100644 --- a/.config/quickshell/modules/common/widgets/DragManager.qml +++ b/.config/quickshell/ii/modules/common/widgets/DragManager.qml @@ -1,8 +1,6 @@ -import "root:/modules/common" -import "root:/services" +import qs.modules.common +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts /** * A convenience MouseArea for handling drag events. diff --git a/.config/quickshell/modules/common/widgets/Favicon.qml b/.config/quickshell/ii/modules/common/widgets/Favicon.qml similarity index 76% rename from .config/quickshell/modules/common/widgets/Favicon.qml rename to .config/quickshell/ii/modules/common/widgets/Favicon.qml index 74fc6d74..04e92859 100644 --- a/.config/quickshell/modules/common/widgets/Favicon.qml +++ b/.config/quickshell/ii/modules/common/widgets/Favicon.qml @@ -1,16 +1,11 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs.modules.common.functions import Qt5Compat.GraphicalEffects -import Qt.labs.platform import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import Quickshell.Io import Quickshell.Widgets -import Quickshell.Hyprland IconImage { id: root @@ -18,7 +13,7 @@ IconImage { property string displayText property real size: 32 - property string downloadUserAgent: ConfigOptions?.networking.userAgent ?? "" + property string downloadUserAgent: Config.options?.networking.userAgent ?? "" property string faviconDownloadPath: Directories.favicons property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getDomain(url) property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32` diff --git a/.config/quickshell/modules/common/widgets/FloatingActionButton.qml b/.config/quickshell/ii/modules/common/widgets/FloatingActionButton.qml similarity index 80% rename from .config/quickshell/modules/common/widgets/FloatingActionButton.qml rename to .config/quickshell/ii/modules/common/widgets/FloatingActionButton.qml index 2f618437..14702aad 100644 --- a/.config/quickshell/modules/common/widgets/FloatingActionButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/FloatingActionButton.qml @@ -1,8 +1,7 @@ import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import "root:/modules/common/" -import "root:/modules/common/widgets/" +import qs.modules.common +import qs.modules.common.widgets /** * Material 3 FAB. @@ -16,9 +15,9 @@ RippleButton { implicitWidth: Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize) implicitHeight: baseSize buttonRadius: Appearance.rounding.small - colBackground: Appearance.colors.colSecondaryContainer - colBackgroundHover: Appearance.colors.colSecondaryContainerHover - colRipple: Appearance.colors.colSecondaryContainerActive + colBackground: Appearance.colors.colPrimaryContainer + colBackgroundHover: Appearance.colors.colPrimaryContainerHover + colRipple: Appearance.colors.colPrimaryContainerActive contentItem: RowLayout { id: contentRowLayout property real horizontalMargins: (root.baseSize - icon.width) / 2 @@ -34,7 +33,7 @@ RippleButton { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter iconSize: 24 - color: Appearance.colors.colOnLayer1 + color: Appearance.colors.colOnPrimaryContainer text: root.iconText } Loader { @@ -50,7 +49,7 @@ RippleButton { leftMargin: root.elementSpacing } text: root.buttonText - color: Appearance.colors.colOnLayer1 + color: Appearance.colors.colOnPrimaryContainer font.pixelSize: 14 font.weight: 450 } diff --git a/.config/quickshell/modules/common/widgets/FlowButtonGroup.qml b/.config/quickshell/ii/modules/common/widgets/FlowButtonGroup.qml similarity index 100% rename from .config/quickshell/modules/common/widgets/FlowButtonGroup.qml rename to .config/quickshell/ii/modules/common/widgets/FlowButtonGroup.qml diff --git a/.config/quickshell/modules/common/widgets/GroupButton.qml b/.config/quickshell/ii/modules/common/widgets/GroupButton.qml similarity index 92% rename from .config/quickshell/modules/common/widgets/GroupButton.qml rename to .config/quickshell/ii/modules/common/widgets/GroupButton.qml index 362ae39e..4a524e1d 100644 --- a/.config/quickshell/modules/common/widgets/GroupButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/GroupButton.qml @@ -1,12 +1,9 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import Qt5Compat.GraphicalEffects +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Widgets /** * Material 3 button with expressive bounciness. @@ -96,13 +93,23 @@ Button { root.down = false if (event.button != Qt.LeftButton) return; if (root.releaseAction) root.releaseAction(); - root.click() // Because the MouseArea already consumed the event + } + onClicked: (event) => { + if (event.button != Qt.LeftButton) return; + root.click() } onCanceled: (event) => { root.down = false } + + onPressAndHold: () => { + altAction(); + root.down = false; + root.clicked = false; + }; } + background: Rectangle { id: buttonBackground topLeftRadius: root.leftRadius diff --git a/.config/quickshell/modules/common/widgets/KeyboardKey.qml b/.config/quickshell/ii/modules/common/widgets/KeyboardKey.qml similarity index 81% rename from .config/quickshell/modules/common/widgets/KeyboardKey.qml rename to .config/quickshell/ii/modules/common/widgets/KeyboardKey.qml index 0cc80429..14c75c62 100644 --- a/.config/quickshell/modules/common/widgets/KeyboardKey.qml +++ b/.config/quickshell/ii/modules/common/widgets/KeyboardKey.qml @@ -1,21 +1,17 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io Rectangle { id: root property string key - property real horizontalPadding: 7 - property real verticalPadding: 2 + property real horizontalPadding: 6 + property real verticalPadding: 1 property real borderWidth: 1 property real extraBottomBorderWidth: 2 property color borderColor: Appearance.colors.colOnLayer0 property real borderRadius: 5 - property color keyColor: Appearance.colors.colSurfaceContainerLow + property color keyColor: Appearance.m3colors.m3surfaceContainerLow implicitWidth: keyFace.implicitWidth + borderWidth * 2 implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth radius: borderRadius diff --git a/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml b/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml new file mode 100644 index 00000000..63dbd2c8 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/LightDarkPreferenceButton.qml @@ -0,0 +1,122 @@ +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Layouts +import Quickshell + +GroupButton { + id: lightDarkButtonRoot + required property bool dark + property color previewBg: dark ? ColorUtils.colorWithHueOf("#3f3838", Appearance.m3colors.m3primary) : + ColorUtils.colorWithHueOf("#F7F9FF", Appearance.m3colors.m3primary) + property color previewFg: dark ? Qt.lighter(previewBg, 2.2) : ColorUtils.mix(previewBg, "#292929", 0.85) + padding: 5 + Layout.fillWidth: true + colBackground: Appearance.colors.colLayer2 + toggled: Appearance.m3colors.darkmode === dark + onClicked: { + Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode ${dark ? "dark" : "light"} --noswitch`]) + } + contentItem: Item { + anchors.centerIn: parent + implicitWidth: buttonContentLayout.implicitWidth + implicitHeight: buttonContentLayout.implicitHeight + ColumnLayout { + id: buttonContentLayout + anchors.centerIn: parent + Rectangle { + Layout.alignment: Qt.AlignHCenter + implicitWidth: 250 + implicitHeight: skeletonColumnLayout.implicitHeight + 10 * 2 + radius: lightDarkButtonRoot.buttonRadius - lightDarkButtonRoot.padding + color: lightDarkButtonRoot.previewBg + border { + width: 1 + color: Appearance.m3colors.m3outlineVariant + } + + // Some skeleton items + ColumnLayout { + id: skeletonColumnLayout + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + RowLayout { + Rectangle { + radius: Appearance.rounding.full + color: lightDarkButtonRoot.previewFg + implicitWidth: 50 + implicitHeight: 50 + } + ColumnLayout { + spacing: 4 + Rectangle { + radius: Appearance.rounding.unsharpenmore + color: lightDarkButtonRoot.previewFg + Layout.fillWidth: true + implicitHeight: 22 + } + Rectangle { + radius: Appearance.rounding.unsharpenmore + color: lightDarkButtonRoot.previewFg + Layout.fillWidth: true + Layout.rightMargin: 45 + implicitHeight: 18 + } + } + } + StyledProgressBar { + Layout.topMargin: 5 + Layout.bottomMargin: 5 + Layout.fillWidth: true + value: 0.7 + sperm: true + animateSperm: lightDarkButtonRoot.toggled + highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg + trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5) + } + RowLayout { + spacing: 2 + Rectangle { + radius: Appearance.rounding.full + color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg + Layout.fillWidth: true + implicitHeight: 30 + MaterialSymbol { + visible: lightDarkButtonRoot.toggled + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: "check" + iconSize: 20 + color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : lightDarkButtonRoot.previewBg + } + } + Rectangle { + radius: Appearance.rounding.unsharpenmore + color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg + Layout.fillWidth: true + implicitHeight: 30 + } + Rectangle { + topLeftRadius: Appearance.rounding.unsharpenmore + bottomLeftRadius: Appearance.rounding.unsharpenmore + topRightRadius: Appearance.rounding.full + bottomRightRadius: Appearance.rounding.full + color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg + Layout.fillWidth: true + implicitHeight: 30 + } + } + } + } + StyledText { + Layout.fillWidth: true + text: dark ? Translation.tr("Dark") : Translation.tr("Light") + color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2 + horizontalAlignment: Text.AlignHCenter + } + } + } +} diff --git a/.config/quickshell/modules/common/widgets/MaterialSymbol.qml b/.config/quickshell/ii/modules/common/widgets/MaterialSymbol.qml similarity index 78% rename from .config/quickshell/modules/common/widgets/MaterialSymbol.qml rename to .config/quickshell/ii/modules/common/widgets/MaterialSymbol.qml index aac0b031..92da9918 100644 --- a/.config/quickshell/modules/common/widgets/MaterialSymbol.qml +++ b/.config/quickshell/ii/modules/common/widgets/MaterialSymbol.qml @@ -1,6 +1,5 @@ -import "root:/modules/common/" +import qs.modules.common import QtQuick -import QtQuick.Layouts Text { id: root @@ -12,6 +11,13 @@ Text { hintingPreference: Font.PreferFullHinting family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded" pixelSize: iconSize + weight: Font.Normal + (Font.DemiBold - Font.Normal) * fill + variableAxes: { + "FILL": truncatedFill, + // "wght": font.weight, + // "GRAD": 0, + "opsz": iconSize, + } } verticalAlignment: Text.AlignVCenter color: Appearance.m3colors.m3onBackground @@ -23,11 +29,4 @@ Text { // easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1] // } // } - - font.variableAxes: { - "FILL": truncatedFill, - // "wght": font.weight, - // "GRAD": 0, - "opsz": iconSize, - } } diff --git a/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml new file mode 100644 index 00000000..241cc90f --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/MaterialTextField.qml @@ -0,0 +1,52 @@ +import qs.modules.common +import QtQuick +import QtQuick.Controls.Material +import QtQuick.Controls + +/** + * Material 3 styled TextArea (filled style) + * https://m3.material.io/components/text-fields/overview + * Note: We don't use NativeRendering because it makes the small placeholder text look weird + */ +TextArea { + id: root + Material.theme: Material.System + Material.accent: Appearance.m3colors.m3primary + Material.primary: Appearance.m3colors.m3primary + Material.background: Appearance.m3colors.m3surface + Material.foreground: Appearance.m3colors.m3onSurface + Material.containerStyle: Material.Filled + renderType: Text.QtRendering + + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + placeholderTextColor: Appearance.m3colors.m3outline + + background: Rectangle { + implicitHeight: 56 + color: Appearance.m3colors.m3surface + topLeftRadius: 4 + topRightRadius: 4 + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 1 + color: root.focus ? Appearance.m3colors.m3primary : + root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + } + + font { + family: Appearance?.font.family.main ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.small ?? 15 + hintingPreference: Font.PreferFullHinting + } + wrapMode: TextEdit.Wrap +} diff --git a/.config/quickshell/modules/common/widgets/MenuButton.qml b/.config/quickshell/ii/modules/common/widgets/MenuButton.qml similarity index 78% rename from .config/quickshell/modules/common/widgets/MenuButton.qml rename to .config/quickshell/ii/modules/common/widgets/MenuButton.qml index 0c4c4f41..9185bc9d 100644 --- a/.config/quickshell/modules/common/widgets/MenuButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/MenuButton.qml @@ -1,10 +1,5 @@ -import "root:/modules/common" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io RippleButton { id: root diff --git a/.config/quickshell/modules/common/widgets/NavigationRail.qml b/.config/quickshell/ii/modules/common/widgets/NavigationRail.qml similarity index 68% rename from .config/quickshell/modules/common/widgets/NavigationRail.qml rename to .config/quickshell/ii/modules/common/widgets/NavigationRail.qml index fe3c64a2..11082a70 100644 --- a/.config/quickshell/modules/common/widgets/NavigationRail.qml +++ b/.config/quickshell/ii/modules/common/widgets/NavigationRail.qml @@ -1,8 +1,7 @@ import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import "root:/modules/common/" -import "root:/modules/common/widgets/" +import qs.modules.common +import qs.modules.common.widgets ColumnLayout { // Window content with navigation rail and content pane id: root diff --git a/.config/quickshell/modules/common/widgets/NavigationRailButton.qml b/.config/quickshell/ii/modules/common/widgets/NavigationRailButton.qml similarity index 70% rename from .config/quickshell/modules/common/widgets/NavigationRailButton.qml rename to .config/quickshell/ii/modules/common/widgets/NavigationRailButton.qml index 50074494..0b83b45b 100644 --- a/.config/quickshell/modules/common/widgets/NavigationRailButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/NavigationRailButton.qml @@ -1,10 +1,9 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io TabButton { id: root @@ -13,13 +12,19 @@ TabButton { property string buttonIcon property string buttonText property bool expanded: false + property bool showToggledHighlight: true + readonly property real visualWidth: root.expanded ? root.baseSize + 20 + itemText.implicitWidth : root.baseSize property real baseSize: 56 property real baseHighlightHeight: 32 + property real highlightCollapsedTopMargin: 8 padding: 0 + // The navigation item’s target area always spans the full width of the + // nav rail, even if the item container hugs its contents. + Layout.fillWidth: true + // implicitWidth: contentItem.implicitWidth implicitHeight: baseSize - implicitWidth: contentItem.implicitWidth background: null PointingHandInteraction {} @@ -28,23 +33,26 @@ TabButton { contentItem: Item { id: buttonContent anchors { + top: parent.top + bottom: parent.bottom left: parent.left - verticalCenter: parent.verticalCenter + right: undefined } - implicitWidth: root.expanded ? itemIconBackground.implicitWidth + 20 + itemText.implicitWidth : - itemIconBackground.implicitWidth + implicitWidth: root.visualWidth implicitHeight: root.expanded ? itemIconBackground.implicitHeight : itemIconBackground.implicitHeight + itemText.implicitHeight Rectangle { id: itemBackground anchors.top: itemIconBackground.top anchors.left: itemIconBackground.left - anchors.right: itemIconBackground.right anchors.bottom: itemIconBackground.bottom + implicitWidth: root.visualWidth radius: Appearance.rounding.full color: toggled ? - (root.down ? Appearance.colors.colSecondaryContainerActive : root.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer) : + root.showToggledHighlight ? + (root.down ? Appearance.colors.colSecondaryContainerActive : root.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer) + : ColorUtils.transparentize(Appearance.colors.colSecondaryContainer) : (root.down ? Appearance.colors.colLayer1Active : root.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)) states: State { @@ -54,9 +62,12 @@ TabButton { target: itemBackground anchors.top: buttonContent.top anchors.left: buttonContent.left - anchors.right: buttonContent.right anchors.bottom: buttonContent.bottom } + PropertyChanges { + target: itemBackground + implicitWidth: root.visualWidth + } } transitions: Transition { AnchorAnimation { @@ -64,6 +75,13 @@ TabButton { easing.type: Appearance.animation.elementMoveFast.type easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve } + PropertyAnimation { + target: itemBackground + property: "implicitWidth" + duration: Appearance.animation.elementMove.duration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } } Behavior on color { @@ -84,6 +102,7 @@ TabButton { anchors.centerIn: parent iconSize: 24 fill: toggled ? 1 : 0 + font.weight: (toggled || root.hovered) ? Font.DemiBold : Font.Normal text: buttonIcon color: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1 @@ -97,7 +116,7 @@ TabButton { id: itemText anchors { top: itemIconBackground.bottom - topMargin: 6 + topMargin: 2 horizontalCenter: itemIconBackground.horizontalCenter } states: State { diff --git a/.config/quickshell/modules/common/widgets/NavigationRailExpandButton.qml b/.config/quickshell/ii/modules/common/widgets/NavigationRailExpandButton.qml similarity index 70% rename from .config/quickshell/modules/common/widgets/NavigationRailExpandButton.qml rename to .config/quickshell/ii/modules/common/widgets/NavigationRailExpandButton.qml index 2c138b87..57e15f04 100644 --- a/.config/quickshell/modules/common/widgets/NavigationRailExpandButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/NavigationRailExpandButton.qml @@ -1,8 +1,7 @@ import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import "root:/modules/common/" -import "root:/modules/common/widgets/" +import qs.modules.common +import qs.modules.common.widgets RippleButton { id: root @@ -14,6 +13,12 @@ RippleButton { parent.expanded = !parent.expanded; } buttonRadius: Appearance.rounding.full + + rotation: root.parent.expanded ? 0 : -180 + Behavior on rotation { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + contentItem: MaterialSymbol { id: icon anchors.centerIn: parent diff --git a/.config/quickshell/ii/modules/common/widgets/NavigationRailTabArray.qml b/.config/quickshell/ii/modules/common/widgets/NavigationRailTabArray.qml new file mode 100644 index 00000000..6596141d --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/NavigationRailTabArray.qml @@ -0,0 +1,41 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + property int currentIndex: 0 + property bool expanded: false + default property alias data: tabBarColumn.data + implicitHeight: tabBarColumn.implicitHeight + implicitWidth: tabBarColumn.implicitWidth + Layout.topMargin: 25 + Rectangle { + property real itemHeight: tabBarColumn.children[0].baseSize + property real baseHighlightHeight: tabBarColumn.children[0].baseHighlightHeight + anchors { + top: tabBarColumn.top + left: tabBarColumn.left + topMargin: itemHeight * root.currentIndex + (root.expanded ? 0 : ((itemHeight - baseHighlightHeight) / 2)) + } + radius: Appearance.rounding.full + color: Appearance.colors.colSecondaryContainer + implicitHeight: root.expanded ? itemHeight : baseHighlightHeight + implicitWidth: tabBarColumn.children[root.currentIndex].visualWidth + + Behavior on anchors.topMargin { + NumberAnimation { + duration: Appearance.animationCurves.expressiveFastSpatialDuration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial + } + } + } + ColumnLayout { + id: tabBarColumn + anchors.fill: parent + spacing: 0 + + } +} diff --git a/.config/quickshell/modules/common/widgets/NotificationActionButton.qml b/.config/quickshell/ii/modules/common/widgets/NotificationActionButton.qml similarity index 89% rename from .config/quickshell/modules/common/widgets/NotificationActionButton.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationActionButton.qml index e85735a7..2a737255 100644 --- a/.config/quickshell/modules/common/widgets/NotificationActionButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationActionButton.qml @@ -1,9 +1,6 @@ -import "root:/modules/common" -import "root:/services" +import qs.modules.common +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell import Quickshell.Services.Notifications RippleButton { diff --git a/.config/quickshell/modules/common/widgets/NotificationAppIcon.qml b/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml similarity index 97% rename from .config/quickshell/modules/common/widgets/NotificationAppIcon.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml index 81505aff..5158d643 100644 --- a/.config/quickshell/modules/common/widgets/NotificationAppIcon.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationAppIcon.qml @@ -1,9 +1,7 @@ -import "root:/modules/common" +import qs.modules.common import "./notification_utils.js" as NotificationUtils import Qt5Compat.GraphicalEffects import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications diff --git a/.config/quickshell/modules/common/widgets/NotificationGroup.qml b/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml similarity index 95% rename from .config/quickshell/modules/common/widgets/NotificationGroup.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationGroup.qml index e79f400a..d63bbb31 100644 --- a/.config/quickshell/modules/common/widgets/NotificationGroup.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationGroup.qml @@ -1,18 +1,10 @@ -import "root:/modules/common" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.services +import qs.modules.common.functions import "./notification_utils.js" as NotificationUtils -import Qt5Compat.GraphicalEffects import QtQuick -import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts import Quickshell -import Quickshell.Io -import Quickshell.Widgets -import Quickshell.Hyprland -import Quickshell.Services.Notifications /** * A group of notifications from the same app. @@ -61,7 +53,7 @@ Item { // Notification group area onFinished: () => { root.notifications.forEach((notif) => { Qt.callLater(() => { - Notifications.discardNotification(notif.id); + Notifications.discardNotification(notif.notificationId); }); }); } diff --git a/.config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml b/.config/quickshell/ii/modules/common/widgets/NotificationGroupExpandButton.qml similarity index 87% rename from .config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationGroupExpandButton.qml index bd1a8e96..ba2b7e10 100644 --- a/.config/quickshell/modules/common/widgets/NotificationGroupExpandButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationGroupExpandButton.qml @@ -1,12 +1,8 @@ -import "root:/modules/common" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.services +import qs.modules.common +import qs.modules.common.functions import QtQuick -import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Notifications RippleButton { // Expand button id: root diff --git a/.config/quickshell/modules/common/widgets/NotificationItem.qml b/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml similarity index 90% rename from .config/quickshell/modules/common/widgets/NotificationItem.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationItem.qml index bf8a7687..4f5688b8 100644 --- a/.config/quickshell/modules/common/widgets/NotificationItem.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationItem.qml @@ -1,16 +1,10 @@ -import "root:/modules/common" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import "./notification_utils.js" as NotificationUtils -import Qt5Compat.GraphicalEffects +import qs +import qs.modules.common +import qs.services +import qs.modules.common.functions import QtQuick -import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts import Quickshell -import Quickshell.Io -import Quickshell.Widgets import Quickshell.Hyprland import Quickshell.Services.Notifications @@ -76,7 +70,7 @@ Item { // Notification item area easing.bezierCurve: Appearance.animation.elementMove.bezierCurve } onFinished: () => { - Notifications.discardNotification(notificationObject.id); + Notifications.discardNotification(notificationObject.notificationId); } } @@ -94,12 +88,6 @@ Item { // Notification item area } } - onPressAndHold: (mouse) => { - if (mouse.button === Qt.LeftButton) { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`) - notificationSummaryText.text = String.format(qsTr("{0} (copied)"), notificationObject.summary) - } - } onDraggingChanged: () => { if (dragging) { root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root); @@ -187,13 +175,15 @@ Item { // Notification item area StyledText { opacity: !root.expanded ? 1 : 0 visible: opacity > 0 + Layout.fillWidth: true Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } - Layout.fillWidth: true font.pixelSize: root.fontSize color: Appearance.colors.colSubtext elide: Text.ElideRight + wrapMode: Text.Wrap // Needed for proper eliding???? + maximumLineCount: 1 textFormat: Text.StyledText text: { return processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
") @@ -218,7 +208,7 @@ Item { // Notification item area elide: Text.ElideRight textFormat: Text.RichText text: { - return `` + + return `` + `${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` } @@ -226,12 +216,8 @@ Item { // Notification item area Qt.openUrlExternally(link) Hyprland.dispatch("global quickshell:sidebarRightClose") } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton // Only for hover - hoverEnabled: true - cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor - } + + PointingHandLinkHover {} } Flickable { // Notification actions @@ -257,7 +243,7 @@ Item { // Notification item area NotificationActionButton { Layout.fillWidth: true - buttonText: qsTr("Close") + buttonText: Translation.tr("Close") urgency: notificationObject.urgency implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) : (contentItem.implicitWidth + leftPadding + rightPadding) @@ -283,7 +269,7 @@ Item { // Notification item area buttonText: modelData.text urgency: notificationObject.urgency onClicked: { - Notifications.attemptInvokeAction(notificationObject.id, modelData.identifier); + Notifications.attemptInvokeAction(notificationObject.notificationId, modelData.identifier); } } } @@ -295,7 +281,7 @@ Item { // Notification item area (contentItem.implicitWidth + leftPadding + rightPadding) onClicked: { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`) + Quickshell.clipboardText = notificationObject.body copyIcon.text = "inventory" copyIconTimer.restart() } diff --git a/.config/quickshell/modules/common/widgets/NotificationListView.qml b/.config/quickshell/ii/modules/common/widgets/NotificationListView.qml similarity index 74% rename from .config/quickshell/modules/common/widgets/NotificationListView.qml rename to .config/quickshell/ii/modules/common/widgets/NotificationListView.qml index 087e4a40..389a5a8b 100644 --- a/.config/quickshell/modules/common/widgets/NotificationListView.qml +++ b/.config/quickshell/ii/modules/common/widgets/NotificationListView.qml @@ -1,13 +1,9 @@ -import "root:/" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "root:/services" +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland StyledListView { // Scrollable window id: root diff --git a/.config/quickshell/modules/common/widgets/PointingHandInteraction.qml b/.config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml similarity index 100% rename from .config/quickshell/modules/common/widgets/PointingHandInteraction.qml rename to .config/quickshell/ii/modules/common/widgets/PointingHandInteraction.qml diff --git a/.config/quickshell/ii/modules/common/widgets/PointingHandLinkHover.qml b/.config/quickshell/ii/modules/common/widgets/PointingHandLinkHover.qml new file mode 100644 index 00000000..4d14c816 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/PointingHandLinkHover.qml @@ -0,0 +1,8 @@ +import QtQuick + +MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // Only for hover + hoverEnabled: true + cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor +} diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml b/.config/quickshell/ii/modules/common/widgets/PrimaryTabBar.qml similarity index 91% rename from .config/quickshell/modules/common/widgets/PrimaryTabBar.qml rename to .config/quickshell/ii/modules/common/widgets/PrimaryTabBar.qml index cd048e8b..63f5e175 100644 --- a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/PrimaryTabBar.qml @@ -1,13 +1,13 @@ -import "root:/modules/common" +import qs.modules.common +import qs import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell ColumnLayout { id: root spacing: 0 - required property var tabButtonList // Something like [{"icon": "notifications", "name": qsTr("Notifications")}, {"icon": "volume_up", "name": qsTr("Volume mixer")}] + required property var tabButtonList // Something like [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Volume mixer")}] required property var externalTrackedTab property bool enableIndicatorAnimation: false property color colIndicator: Appearance?.colors.colPrimary ?? "#65558F" @@ -65,7 +65,7 @@ ColumnLayout { id: indicator property int tabCount: root.tabButtonList.length property real fullTabSize: root.width / tabCount; - property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth + property real targetWidth: tabBar.contentItem?.children[0]?.children[tabBar.currentIndex]?.tabContentWidth ?? 0 implicitWidth: targetWidth anchors { diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabButton.qml b/.config/quickshell/ii/modules/common/widgets/PrimaryTabButton.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/PrimaryTabButton.qml rename to .config/quickshell/ii/modules/common/widgets/PrimaryTabButton.qml index a47f108b..0b4b6f8f 100644 --- a/.config/quickshell/modules/common/widgets/PrimaryTabButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/PrimaryTabButton.qml @@ -1,12 +1,10 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Widgets TabButton { id: button diff --git a/.config/quickshell/modules/common/widgets/Revealer.qml b/.config/quickshell/ii/modules/common/widgets/Revealer.qml similarity index 93% rename from .config/quickshell/modules/common/widgets/Revealer.qml rename to .config/quickshell/ii/modules/common/widgets/Revealer.qml index 620b8d6e..bbbe2efa 100644 --- a/.config/quickshell/modules/common/widgets/Revealer.qml +++ b/.config/quickshell/ii/modules/common/widgets/Revealer.qml @@ -1,6 +1,5 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick -import Quickshell /** * Recreation of GTK revealer. Expects one single child. diff --git a/.config/quickshell/modules/common/widgets/RippleButton.qml b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/RippleButton.qml rename to .config/quickshell/ii/modules/common/widgets/RippleButton.qml index 9931cd02..7487203a 100644 --- a/.config/quickshell/modules/common/widgets/RippleButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/RippleButton.qml @@ -1,12 +1,9 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Widgets /** * A button with ripple effect similar to in Material Design. diff --git a/.config/quickshell/ii/modules/common/widgets/RippleButtonWithIcon.qml b/.config/quickshell/ii/modules/common/widgets/RippleButtonWithIcon.qml new file mode 100644 index 00000000..f84ae4da --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/RippleButtonWithIcon.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.widgets + +RippleButton { + id: buttonWithIconRoot + property string nerdIcon + property string materialIcon + property bool materialIconFill: true + property string mainText: "Button text" + property Component mainContentComponent: Component { + StyledText { + text: buttonWithIconRoot.mainText + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnSecondaryContainer + } + } + implicitHeight: 35 + horizontalPadding: 15 + buttonRadius: Appearance.rounding.small + colBackground: Appearance.colors.colLayer2 + + contentItem: RowLayout { + Item { + implicitWidth: Math.max(materialIconLoader.implicitWidth, nerdIconLoader.implicitWidth) + Loader { + id: materialIconLoader + anchors.centerIn: parent + active: !nerdIcon + sourceComponent: MaterialSymbol { + text: buttonWithIconRoot.materialIcon + iconSize: Appearance.font.pixelSize.larger + color: Appearance.colors.colOnSecondaryContainer + fill: buttonWithIconRoot.materialIconFill ? 1 : 0 + } + } + Loader { + id: nerdIconLoader + anchors.centerIn: parent + active: nerdIcon + sourceComponent: StyledText { + text: buttonWithIconRoot.nerdIcon + font.pixelSize: Appearance.font.pixelSize.larger + font.family: Appearance.font.family.iconNerd + color: Appearance.colors.colOnSecondaryContainer + } + } + } + Loader { + sourceComponent: buttonWithIconRoot.mainContentComponent + Layout.alignment: Qt.AlignVCenter + } + } +} diff --git a/.config/quickshell/modules/common/widgets/RoundCorner.qml b/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml similarity index 68% rename from .config/quickshell/modules/common/widgets/RoundCorner.qml rename to .config/quickshell/ii/modules/common/widgets/RoundCorner.qml index c9a2827a..6fba4b92 100644 --- a/.config/quickshell/modules/common/widgets/RoundCorner.qml +++ b/.config/quickshell/ii/modules/common/widgets/RoundCorner.qml @@ -3,24 +3,21 @@ import QtQuick 2.9 Item { id: root + enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight } + property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft + property int size: 25 property color color: "#000000" onColorChanged: { canvas.requestPaint(); } - - property QtObject cornerEnum: QtObject { - property int topLeft: 0 - property int topRight: 1 - property int bottomLeft: 2 - property int bottomRight: 3 + onCornerChanged: { + canvas.requestPaint(); } - property int corner: cornerEnum.topLeft // Default to TopLeft - - width: size - height: size + implicitWidth: size + implicitHeight: size Canvas { id: canvas @@ -31,22 +28,22 @@ Item { onPaint: { var ctx = getContext("2d"); var r = root.size; - + ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); switch (root.corner) { - case cornerEnum.topLeft: + case RoundCorner.CornerEnum.TopLeft: ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2); ctx.lineTo(0, 0); break; - case cornerEnum.topRight: + case RoundCorner.CornerEnum.TopRight: ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI); ctx.lineTo(r, 0); break; - case cornerEnum.bottomLeft: + case RoundCorner.CornerEnum.BottomLeft: ctx.arc(r, 0, r, Math.PI / 2, Math.PI); ctx.lineTo(0, r); break; - case cornerEnum.bottomRight: + case RoundCorner.CornerEnum.BottomRight: ctx.arc(0, 0, r, 0, Math.PI / 2); ctx.lineTo(r, r); break; diff --git a/.config/quickshell/modules/common/widgets/SecondaryTabButton.qml b/.config/quickshell/ii/modules/common/widgets/SecondaryTabButton.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/SecondaryTabButton.qml rename to .config/quickshell/ii/modules/common/widgets/SecondaryTabButton.qml index 1d3b6381..983dd02b 100644 --- a/.config/quickshell/modules/common/widgets/SecondaryTabButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/SecondaryTabButton.qml @@ -1,12 +1,10 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Widgets TabButton { id: root diff --git a/.config/quickshell/modules/common/widgets/SelectionDialog.qml b/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml similarity index 94% rename from .config/quickshell/modules/common/widgets/SelectionDialog.qml rename to .config/quickshell/ii/modules/common/widgets/SelectionDialog.qml index 9cf0940e..4dd2382c 100644 --- a/.config/quickshell/modules/common/widgets/SelectionDialog.qml +++ b/.config/quickshell/ii/modules/common/widgets/SelectionDialog.qml @@ -1,9 +1,8 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import Qt5Compat.GraphicalEffects +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell @@ -70,6 +69,7 @@ Item { Layout.fillHeight: true clip: true currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1 + spacing: 6 model: ScriptModel { id: choiceModel @@ -113,11 +113,11 @@ Item { Layout.alignment: Qt.AlignRight DialogButton { - buttonText: qsTr("Cancel") + buttonText: Translation.tr("Cancel") onClicked: root.canceled() } DialogButton { - buttonText: qsTr("OK") + buttonText: Translation.tr("OK") onClicked: root.selected( root.selectedId === -1 ? null : root.items[root.selectedId] diff --git a/.config/quickshell/modules/common/widgets/SelectionGroupButton.qml b/.config/quickshell/ii/modules/common/widgets/SelectionGroupButton.qml similarity index 87% rename from .config/quickshell/modules/common/widgets/SelectionGroupButton.qml rename to .config/quickshell/ii/modules/common/widgets/SelectionGroupButton.qml index 77695ce3..6a225ebb 100644 --- a/.config/quickshell/modules/common/widgets/SelectionGroupButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/SelectionGroupButton.qml @@ -3,9 +3,9 @@ import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Hyprland -import "root:/services/" -import "root:/modules/common/" -import "root:/modules/common/widgets/" +import qs.services +import qs.modules.common +import qs.modules.common.widgets GroupButton { id: root diff --git a/.config/quickshell/modules/common/widgets/StyledLabel.qml b/.config/quickshell/ii/modules/common/widgets/StyledLabel.qml similarity index 88% rename from .config/quickshell/modules/common/widgets/StyledLabel.qml rename to .config/quickshell/ii/modules/common/widgets/StyledLabel.qml index f5201bae..35b3cbf4 100644 --- a/.config/quickshell/modules/common/widgets/StyledLabel.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledLabel.qml @@ -1,7 +1,6 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick import QtQuick.Controls -import QtQuick.Layouts Label { renderType: Text.NativeRendering diff --git a/.config/quickshell/modules/common/widgets/StyledListView.qml b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml similarity index 93% rename from .config/quickshell/modules/common/widgets/StyledListView.qml rename to .config/quickshell/ii/modules/common/widgets/StyledListView.qml index 76d9782b..96874b86 100644 --- a/.config/quickshell/modules/common/widgets/StyledListView.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledListView.qml @@ -1,13 +1,8 @@ -import "root:/" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "root:/services" +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland /** * A ListView with animations. diff --git a/.config/quickshell/modules/common/widgets/StyledProgressBar.qml b/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml similarity index 94% rename from .config/quickshell/modules/common/widgets/StyledProgressBar.qml rename to .config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml index 1ea8a93a..fa1cd0be 100644 --- a/.config/quickshell/modules/common/widgets/StyledProgressBar.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledProgressBar.qml @@ -1,6 +1,6 @@ -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -32,10 +32,8 @@ ProgressBar { animation: Appearance?.animation.elementMoveEnter.numberAnimation.createObject(this) } - background: Rectangle { + background: Item { anchors.fill: parent - color: "transparent" - radius: Appearance?.rounding.full ?? 9999 implicitHeight: valueBarHeight implicitWidth: valueBarWidth } diff --git a/.config/quickshell/modules/common/widgets/StyledRadioButton.qml b/.config/quickshell/ii/modules/common/widgets/StyledRadioButton.qml similarity index 94% rename from .config/quickshell/modules/common/widgets/StyledRadioButton.qml rename to .config/quickshell/ii/modules/common/widgets/StyledRadioButton.qml index 3ef1ee8a..a6a63b7b 100644 --- a/.config/quickshell/modules/common/widgets/StyledRadioButton.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledRadioButton.qml @@ -1,6 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls @@ -10,7 +10,7 @@ import Quickshell.Services.Pipewire RadioButton { id: root - implicitHeight: 40 + implicitHeight: contentItem.implicitHeight + 4 * 2 property string description property color activeColor: Appearance?.colors.colPrimary ?? "#685496" property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F" @@ -20,6 +20,7 @@ RadioButton { indicator: Item{} contentItem: RowLayout { + id: contentItem Layout.fillWidth: true spacing: 12 Rectangle { diff --git a/.config/quickshell/modules/common/widgets/StyledRectangularShadow.qml b/.config/quickshell/ii/modules/common/widgets/StyledRectangularShadow.qml similarity index 90% rename from .config/quickshell/modules/common/widgets/StyledRectangularShadow.qml rename to .config/quickshell/ii/modules/common/widgets/StyledRectangularShadow.qml index 66a626f1..a3c842c4 100644 --- a/.config/quickshell/modules/common/widgets/StyledRectangularShadow.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledRectangularShadow.qml @@ -1,6 +1,6 @@ import QtQuick import QtQuick.Effects -import "root:/modules/common" +import qs.modules.common RectangularShadow { required property var target diff --git a/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml b/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml new file mode 100644 index 00000000..e940f1a4 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledSlider.qml @@ -0,0 +1,155 @@ +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Widgets + +/** + * Material 3 slider. See https://m3.material.io/components/sliders/overview + * It doesn't exactly match the spec because it does not make sense to have stuff on a computer that fucking huge. + * Should be at 3/4 scale... + */ + +Slider { + id: root + + property list stopIndicatorValues: [1] + enum Configuration { + XS = 12, + S = 18, + M = 30, + L = 42, + XL = 72 + } + + property var configuration: StyledSlider.Configuration.S + + property real handleDefaultWidth: 3 + property real handlePressedWidth: 1.5 + + property color highlightColor: Appearance.colors.colPrimary + property color trackColor: Appearance.colors.colSecondaryContainer + property color handleColor: Appearance.m3colors.m3onSecondaryContainer + property color dotColor: Appearance.m3colors.m3onSecondaryContainer + property color dotColorHighlighted: Appearance.m3colors.m3onPrimary + property real unsharpenRadius: Appearance.rounding.unsharpen + property real trackWidth: configuration + property real trackRadius: trackWidth >= StyledSlider.Configuration.XL ? 21 + : trackWidth >= StyledSlider.Configuration.L ? 12 + : trackWidth >= StyledSlider.Configuration.M ? 9 + : 6 + property real handleHeight: Math.max(33, trackWidth + 9) + property real handleWidth: root.pressed ? handlePressedWidth : handleDefaultWidth + property real handleMargins: 4 + onHandleMarginsChanged: { + console.log("Handle margins changed to", handleMargins); + } + property real trackDotSize: 3 + property string tooltipContent: `${Math.round(value * 100)}%` + + leftPadding: handleMargins + rightPadding: handleMargins + property real effectiveDraggingWidth: width - leftPadding - rightPadding + + Layout.fillWidth: true + from: 0 + to: 1 + + Behavior on value { // This makes the adjusted value (like volume) shift smoothly + SmoothedAnimation { + velocity: Appearance.animation.elementMoveFast.velocity + } + } + + Behavior on handleMargins { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + component TrackDot: Rectangle { + required property real value + anchors.verticalCenter: parent.verticalCenter + x: root.handleMargins + (value * root.effectiveDraggingWidth) - (root.trackDotSize / 2) + width: root.trackDotSize + height: root.trackDotSize + radius: Appearance.rounding.full + color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + + MouseArea { + anchors.fill: parent + onPressed: (mouse) => mouse.accepted = false + cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor + } + + background: Item { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + implicitHeight: trackWidth + + // Fill left + Rectangle { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + width: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins) + height: trackWidth + color: root.highlightColor + topLeftRadius: root.trackRadius + bottomLeftRadius: root.trackRadius + topRightRadius: root.unsharpenRadius + bottomRightRadius: root.unsharpenRadius + } + + // Fill right + Rectangle { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + width: root.handleMargins + ((1 - root.visualPosition) * root.effectiveDraggingWidth) - (root.handleWidth / 2 + root.handleMargins) + height: trackWidth + color: root.trackColor + topRightRadius: root.trackRadius + bottomRightRadius: root.trackRadius + topLeftRadius: root.unsharpenRadius + bottomLeftRadius: root.unsharpenRadius + } + + // Stop indicators + Repeater { + model: root.stopIndicatorValues + TrackDot { + required property real modelData + value: modelData + anchors.verticalCenter: parent.verticalCenter + } + } + } + + handle: Rectangle { + id: handle + + implicitWidth: root.handleWidth + implicitHeight: root.handleHeight + x: root.handleMargins + (root.visualPosition * root.effectiveDraggingWidth) - (root.handleWidth / 2) + anchors.verticalCenter: parent.verticalCenter + radius: Appearance.rounding.full + color: root.handleColor + + Behavior on implicitWidth { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + StyledToolTip { + extraVisibleCondition: root.pressed + content: root.tooltipContent + } + } +} \ No newline at end of file diff --git a/.config/quickshell/ii/modules/common/widgets/StyledSpinBox.qml b/.config/quickshell/ii/modules/common/widgets/StyledSpinBox.qml new file mode 100644 index 00000000..c11f241a --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledSpinBox.qml @@ -0,0 +1,92 @@ +import qs.modules.common +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls + +/** + * Material 3 styled SpinBox component. + */ +SpinBox { + id: root + + property real baseHeight: 35 + property real radius: Appearance.rounding.small + property real innerButtonRadius: Appearance.rounding.unsharpen + editable: true + + background: Rectangle { + color: Appearance.colors.colLayer2 + radius: root.radius + } + + contentItem: Item { + implicitHeight: root.baseHeight + implicitWidth: Math.max(labelText.implicitWidth, 40) + + StyledTextInput { + id: labelText + anchors.centerIn: parent + text: root.value // displayText would make the numbers weird like 1,000 instead of 1000 + color: Appearance.colors.colOnLayer2 + font.pixelSize: Appearance.font.pixelSize.small + validator: root.validator + onTextChanged: { + root.value = parseFloat(text); + } + } + } + + down.indicator: Rectangle { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + implicitHeight: root.baseHeight + implicitWidth: root.baseHeight + topLeftRadius: root.radius + bottomLeftRadius: root.radius + topRightRadius: root.innerButtonRadius + bottomRightRadius: root.innerButtonRadius + + color: root.down.pressed ? Appearance.colors.colLayer2Active : + root.down.hovered ? Appearance.colors.colLayer2Hover : + ColorUtils.transparentize(Appearance.colors.colLayer2) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + MaterialSymbol { + anchors.centerIn: parent + text: "remove" + iconSize: 20 + color: Appearance.colors.colOnLayer2 + } + } + + up.indicator: Rectangle { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + implicitHeight: root.baseHeight + implicitWidth: root.baseHeight + topRightRadius: root.radius + bottomRightRadius: root.radius + topLeftRadius: root.innerButtonRadius + bottomLeftRadius: root.innerButtonRadius + + color: root.up.pressed ? Appearance.colors.colLayer2Active : + root.up.hovered ? Appearance.colors.colLayer2Hover : + ColorUtils.transparentize(Appearance.colors.colLayer2) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + MaterialSymbol { + anchors.centerIn: parent + text: "add" + iconSize: 20 + color: Appearance.colors.colOnLayer2 + } + } +} diff --git a/.config/quickshell/modules/common/widgets/StyledSwitch.qml b/.config/quickshell/ii/modules/common/widgets/StyledSwitch.qml similarity index 81% rename from .config/quickshell/modules/common/widgets/StyledSwitch.qml rename to .config/quickshell/ii/modules/common/widgets/StyledSwitch.qml index e980d7f8..f16e213f 100644 --- a/.config/quickshell/modules/common/widgets/StyledSwitch.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledSwitch.qml @@ -1,4 +1,4 @@ -import "root:/modules/common/" +import qs.modules.common import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -36,13 +36,13 @@ Switch { // Custom thumb styling indicator: Rectangle { - width: root.pressed ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale) - height: root.pressed ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale) + width: (root.pressed || root.down) ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale) + height: (root.pressed || root.down) ? (28 * root.scale) : root.checked ? (24 * root.scale) : (16 * root.scale) radius: Appearance.rounding.full color: root.checked ? Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3outline anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: root.checked ? (root.pressed ? (22 * root.scale) : 24 * root.scale) : (root.pressed ? (2 * root.scale) : 8 * root.scale) + anchors.leftMargin: root.checked ? ((root.pressed || root.down) ? (22 * root.scale) : 24 * root.scale) : ((root.pressed || root.down) ? (2 * root.scale) : 8 * root.scale) Behavior on anchors.leftMargin { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) diff --git a/.config/quickshell/modules/common/widgets/StyledText.qml b/.config/quickshell/ii/modules/common/widgets/StyledText.qml similarity index 93% rename from .config/quickshell/modules/common/widgets/StyledText.qml rename to .config/quickshell/ii/modules/common/widgets/StyledText.qml index 7750456e..6024fc67 100644 --- a/.config/quickshell/modules/common/widgets/StyledText.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledText.qml @@ -1,4 +1,4 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick import QtQuick.Layouts diff --git a/.config/quickshell/modules/common/widgets/StyledTextArea.qml b/.config/quickshell/ii/modules/common/widgets/StyledTextArea.qml similarity index 80% rename from .config/quickshell/modules/common/widgets/StyledTextArea.qml rename to .config/quickshell/ii/modules/common/widgets/StyledTextArea.qml index 67d41757..e0abba3d 100644 --- a/.config/quickshell/modules/common/widgets/StyledTextArea.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledTextArea.qml @@ -1,7 +1,10 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick import QtQuick.Controls +/** + * Does not include visual layout, but includes the easily neglected colors. + */ TextArea { renderType: Text.NativeRendering selectedTextColor: Appearance.m3colors.m3onSecondaryContainer diff --git a/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml new file mode 100644 index 00000000..57d0c726 --- /dev/null +++ b/.config/quickshell/ii/modules/common/widgets/StyledTextInput.qml @@ -0,0 +1,17 @@ +import qs.modules.common +import QtQuick +import QtQuick.Controls + +/** + * Does not include visual layout, but includes the easily neglected colors. + */ +TextInput { + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3onSecondaryContainer + selectionColor: Appearance.colors.colSecondaryContainer + font { + family: Appearance?.font.family.main ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.small ?? 15 + hintingPreference: Font.PreferFullHinting + } +} diff --git a/.config/quickshell/modules/common/widgets/StyledToolTip.qml b/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml similarity index 96% rename from .config/quickshell/modules/common/widgets/StyledToolTip.qml rename to .config/quickshell/ii/modules/common/widgets/StyledToolTip.qml index 1b4bd033..813c9ed1 100644 --- a/.config/quickshell/modules/common/widgets/StyledToolTip.qml +++ b/.config/quickshell/ii/modules/common/widgets/StyledToolTip.qml @@ -1,5 +1,5 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/.config/quickshell/modules/common/widgets/VerticalButtonGroup.qml b/.config/quickshell/ii/modules/common/widgets/VerticalButtonGroup.qml similarity index 90% rename from .config/quickshell/modules/common/widgets/VerticalButtonGroup.qml rename to .config/quickshell/ii/modules/common/widgets/VerticalButtonGroup.qml index 7d8fc29f..b1ca845a 100644 --- a/.config/quickshell/modules/common/widgets/VerticalButtonGroup.qml +++ b/.config/quickshell/ii/modules/common/widgets/VerticalButtonGroup.qml @@ -1,8 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts /** diff --git a/.config/quickshell/modules/common/widgets/WaveVisualizer.qml b/.config/quickshell/ii/modules/common/widgets/WaveVisualizer.qml similarity index 88% rename from .config/quickshell/modules/common/widgets/WaveVisualizer.qml rename to .config/quickshell/ii/modules/common/widgets/WaveVisualizer.qml index 571e7183..64559c14 100644 --- a/.config/quickshell/modules/common/widgets/WaveVisualizer.qml +++ b/.config/quickshell/ii/modules/common/widgets/WaveVisualizer.qml @@ -1,13 +1,8 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Effects -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Io Canvas { // Visualizer id: root diff --git a/.config/quickshell/modules/common/widgets/notification_utils.js b/.config/quickshell/ii/modules/common/widgets/notification_utils.js similarity index 100% rename from .config/quickshell/modules/common/widgets/notification_utils.js rename to .config/quickshell/ii/modules/common/widgets/notification_utils.js diff --git a/.config/quickshell/ii/modules/dock/Dock.qml b/.config/quickshell/ii/modules/dock/Dock.qml new file mode 100644 index 00000000..0c5e12f1 --- /dev/null +++ b/.config/quickshell/ii/modules/dock/Dock.qml @@ -0,0 +1,148 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell.Io +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { // Scope + id: root + property bool pinned: Config.options?.dock.pinnedOnStartup ?? false + + Variants { // For each monitor + model: Quickshell.screens + + PanelWindow { // Window + required property var modelData + id: dockRoot + screen: modelData + visible: !GlobalStates.screenLocked + + property bool reveal: root.pinned + || (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse) + || dockApps.requestDockShow + || (!ToplevelManager.activeToplevel?.activated) + + anchors { + bottom: true + left: true + right: true + } + + exclusiveZone: root.pinned ? implicitHeight + - (Appearance.sizes.hyprlandGapsOut) + - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0 + + implicitWidth: dockBackground.implicitWidth + WlrLayershell.namespace: "quickshell:dock" + color: "transparent" + + implicitHeight: (Config.options?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut + + mask: Region { + item: dockMouseArea + } + + MouseArea { + id: dockMouseArea + height: parent.height + anchors { + top: parent.top + topMargin: dockRoot.reveal ? 0 : + Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) : + (dockRoot.implicitHeight + 1) + horizontalCenter: parent.horizontalCenter + } + implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2 + hoverEnabled: true + + Behavior on anchors.topMargin { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + Item { + id: dockHoverRegion + anchors.fill: parent + implicitWidth: dockBackground.implicitWidth + + Item { // Wrapper for the dock background + id: dockBackground + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + + implicitWidth: dockRow.implicitWidth + 5 * 2 + height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut + + StyledRectangularShadow { + target: dockVisualBackground + } + Rectangle { // The real rectangle that is visible + id: dockVisualBackground + property real margin: Appearance.sizes.elevationMargin + anchors.fill: parent + anchors.topMargin: Appearance.sizes.elevationMargin + anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut + color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + radius: Appearance.rounding.large + } + + RowLayout { + id: dockRow + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + spacing: 3 + property real padding: 5 + + VerticalButtonGroup { + Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work + GroupButton { // Pin button + baseWidth: 35 + baseHeight: 35 + clickedWidth: baseWidth + clickedHeight: baseHeight + 20 + buttonRadius: Appearance.rounding.normal + toggled: root.pinned + onClicked: root.pinned = !root.pinned + contentItem: MaterialSymbol { + text: "keep" + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0 + } + } + } + DockSeparator {} + DockApps { id: dockApps; } + DockSeparator {} + DockButton { + Layout.fillHeight: true + onClicked: Hyprland.dispatch("global quickshell:overviewToggle") + contentItem: MaterialSymbol { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + font.pixelSize: parent.width / 2 + text: "apps" + color: Appearance.colors.colOnLayer0 + } + } + } + } + } + + } + } + } +} diff --git a/.config/quickshell/modules/dock/DockAppButton.qml b/.config/quickshell/ii/modules/dock/DockAppButton.qml similarity index 73% rename from .config/quickshell/modules/dock/DockAppButton.qml rename to .config/quickshell/ii/modules/dock/DockAppButton.qml index f4623e62..1ebbffa4 100644 --- a/.config/quickshell/modules/dock/DockAppButton.qml +++ b/.config/quickshell/ii/modules/dock/DockAppButton.qml @@ -1,17 +1,14 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts -import Quickshell.Io import Quickshell import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland DockButton { id: root @@ -72,6 +69,14 @@ DockButton { root.desktopEntry?.execute(); } + altAction: () => { + if (Config.options.dock.pinnedApps.indexOf(appToplevel.appId) !== -1) { + Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appToplevel.appId) + } else { + Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([appToplevel.appId]) + } + } + contentItem: Loader { active: !isSeparator sourceComponent: Item { @@ -91,6 +96,25 @@ DockButton { } } + Loader { + active: Config.options.dock.monochromeIcons + anchors.fill: iconImageLoader + sourceComponent: Item { + Desaturate { + id: desaturatedIcon + visible: false // There's already color overlay + anchors.fill: parent + source: iconImageLoader + desaturation: 0.8 + } + ColorOverlay { + anchors.fill: desaturatedIcon + source: desaturatedIcon + color: ColorUtils.transparentize(Appearance.colors.colPrimary, 0.9) + } + } + } + RowLayout { spacing: 3 anchors { diff --git a/.config/quickshell/modules/dock/DockApps.qml b/.config/quickshell/ii/modules/dock/DockApps.qml similarity index 95% rename from .config/quickshell/modules/dock/DockApps.qml rename to .config/quickshell/ii/modules/dock/DockApps.qml index ffda024c..623bdc23 100644 --- a/.config/quickshell/modules/dock/DockApps.qml +++ b/.config/quickshell/ii/modules/dock/DockApps.qml @@ -1,18 +1,15 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts -import Quickshell.Io import Quickshell import Quickshell.Widgets import Quickshell.Wayland -import Quickshell.Hyprland Item { id: root @@ -48,7 +45,7 @@ Item { var map = new Map(); // Pinned apps - const pinnedApps = ConfigOptions?.dock.pinnedApps ?? []; + const pinnedApps = Config.options?.dock.pinnedApps ?? []; for (const appId of pinnedApps) { if (!map.has(appId.toLowerCase())) map.set(appId.toLowerCase(), ({ pinned: true, @@ -60,9 +57,13 @@ Item { if (pinnedApps.length > 0) { map.set("SEPARATOR", { pinned: false, toplevels: [] }); } - + + // Ignored apps + const ignoredRegexStrings = Config.options?.dock.ignoredAppRegexes ?? []; + const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i")); // Open windows for (const toplevel of ToplevelManager.toplevels.values) { + if (ignoredRegexes.some(re => re.test(toplevel.appId))) continue; if (!map.has(toplevel.appId.toLowerCase())) map.set(toplevel.appId.toLowerCase(), ({ pinned: false, toplevels: [] diff --git a/.config/quickshell/modules/dock/DockButton.qml b/.config/quickshell/ii/modules/dock/DockButton.qml similarity index 83% rename from .config/quickshell/modules/dock/DockButton.qml rename to .config/quickshell/ii/modules/dock/DockButton.qml index 577cbcdc..61655782 100644 --- a/.config/quickshell/modules/dock/DockButton.qml +++ b/.config/quickshell/ii/modules/dock/DockButton.qml @@ -1,6 +1,6 @@ -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/.config/quickshell/modules/dock/DockSeparator.qml b/.config/quickshell/ii/modules/dock/DockSeparator.qml similarity index 89% rename from .config/quickshell/modules/dock/DockSeparator.qml rename to .config/quickshell/ii/modules/dock/DockSeparator.qml index abb45d1d..419b0fed 100644 --- a/.config/quickshell/modules/dock/DockSeparator.qml +++ b/.config/quickshell/ii/modules/dock/DockSeparator.qml @@ -1,5 +1,5 @@ -import "root:/" -import "root:/modules/common" +import qs +import qs.modules.common import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/.config/quickshell/ii/modules/lock/Lock.qml b/.config/quickshell/ii/modules/lock/Lock.qml new file mode 100644 index 00000000..89d39778 --- /dev/null +++ b/.config/quickshell/ii/modules/lock/Lock.qml @@ -0,0 +1,99 @@ +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.lock +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: root + // This stores all the information shared between the lock surfaces on each screen. + // https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen + LockContext { + id: lockContext + + onUnlocked: { + // Unlock the screen before exiting, or the compositor will display a + // fallback lock you can't interact with. + GlobalStates.screenLocked = false; + } + } + + WlSessionLock { + id: lock + locked: GlobalStates.screenLocked + + WlSessionLockSurface { + color: "transparent" + Loader { + active: GlobalStates.screenLocked + anchors.fill: parent + opacity: active ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + sourceComponent: LockSurface { + context: lockContext + } + } + } + } + + // Blur layer hack + Variants { + model: Quickshell.screens + + LazyLoader { + id: blurLayerLoader + required property var modelData + active: GlobalStates.screenLocked + component: PanelWindow { + screen: blurLayerLoader.modelData + WlrLayershell.namespace: "quickshell:lockWindowPusher" + color: "transparent" + anchors { + top: true + left: true + right: true + } + // implicitHeight: lockContext.currentText == "" ? 1 : screen.height + implicitHeight: 1 + exclusiveZone: screen.height * 3 // For some reason if we don't multiply by some number it would look really weird + } + } + } + + IpcHandler { + target: "lock" + + function activate(): void { + GlobalStates.screenLocked = true; + } + function focus(): void { + lockContext.shouldReFocus(); + } + } + + GlobalShortcut { + name: "lock" + description: "Locks the screen" + + onPressed: { + GlobalStates.screenLocked = true; + } + } + + GlobalShortcut { + name: "lockFocus" + description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason" + + "decides to keyboard-unfocus the lock screen" + + onPressed: { + // console.log("I BEG FOR PLEAS REFOCUZ") + lockContext.shouldReFocus(); + } + } +} diff --git a/.config/quickshell/ii/modules/lock/LockContext.qml b/.config/quickshell/ii/modules/lock/LockContext.qml new file mode 100644 index 00000000..ede61eed --- /dev/null +++ b/.config/quickshell/ii/modules/lock/LockContext.qml @@ -0,0 +1,67 @@ +import qs +import QtQuick +import Quickshell +import Quickshell.Services.Pam + +Scope { + id: root + signal shouldReFocus() + signal unlocked() + signal failed() + + // These properties are in the context and not individual lock surfaces + // so all surfaces can share the same state. + property string currentText: "" + property bool unlockInProgress: false + property bool showFailure: false + + Timer { + id: passwordClearTimer + interval: 10000 + onTriggered: { + root.currentText = ""; + } + } + + onCurrentTextChanged: { + showFailure = false; // Clear the failure text once the user starts typing. + GlobalStates.screenLockContainsCharacters = currentText.length > 0; + passwordClearTimer.restart(); + } + + function tryUnlock() { + if (currentText === "") return; + + root.unlockInProgress = true; + pam.start(); + } + + PamContext { + id: pam + + // Its best to have a custom pam config for quickshell, as the system one + // might not be what your interface expects, and break in some way. + // This particular example only supports passwords. + configDirectory: "pam" + config: "password.conf" + + // pam_unix will ask for a response for the password prompt + onPamMessage: { + if (this.responseRequired) { + this.respond(root.currentText); + } + } + + // pam_unix won't send any important messages so all we need is the completion status. + onCompleted: result => { + if (result == PamResult.Success) { + root.unlocked(); + } else { + root.showFailure = true; + } + + root.currentText = ""; + root.unlockInProgress = false; + } + } +} diff --git a/.config/quickshell/ii/modules/lock/LockSurface.qml b/.config/quickshell/ii/modules/lock/LockSurface.qml new file mode 100644 index 00000000..98d1f57b --- /dev/null +++ b/.config/quickshell/ii/modules/lock/LockSurface.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +MouseArea { + id: root + required property LockContext context + property bool active: false + property bool showInputField: active || context.currentText.length > 0 + + function forceFieldFocus() { + passwordBox.forceActiveFocus(); + } + + Component.onCompleted: { + forceFieldFocus(); + } + + Connections { + target: context + function onShouldReFocus() { + forceFieldFocus(); + } + } + + Keys.onPressed: (event) => { // Esc to clear + // console.log("KEY!!") + if (event.key === Qt.Key_Escape) { + root.context.currentText = "" + } + forceFieldFocus(); + } + + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onPressed: (mouse) => { + forceFieldFocus(); + // console.log("Pressed") + } + onPositionChanged: (mouse) => { + forceFieldFocus(); + // console.log(JSON.stringify(mouse)) + } + + anchors.fill: parent + + // RippleButton { + // anchors { + // top: parent.top + // left: parent.left + // leftMargin: 10 + // topMargin: 10 + // } + // implicitHeight: 40 + // colBackground: Appearance.colors.colLayer2 + // onClicked: context.unlocked() + // contentItem: StyledText { + // text: "[[ DEBUG BYPASS ]]" + // } + // } + + // Password entry + Rectangle { + id: passwordBoxContainer + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: root.showInputField ? 20 : -height + } + Behavior on anchors.bottomMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + radius: Appearance.rounding.full + color: Appearance.colors.colLayer2 + implicitWidth: 160 + implicitHeight: 44 + + StyledText { + visible: root.context.showFailure && passwordBox.text.length == 0 + anchors.centerIn: parent + text: "Incorrect" + color: Appearance.m3colors.m3error + } + + StyledTextInput { + id: passwordBox + + anchors { + fill: parent + margins: 10 + } + clip: true + horizontalAlignment: TextInput.AlignHCenter + verticalAlignment: TextInput.AlignVCenter + focus: true + onFocusChanged: root.forceFieldFocus(); + color: Appearance.colors.colOnLayer2 + font { + pixelSize: 10 + } + + // Password + enabled: !root.context.unlockInProgress + echoMode: TextInput.Password + inputMethodHints: Qt.ImhSensitiveData + + // Synchronizing (across monitors) and unlocking + onTextChanged: root.context.currentText = this.text + onAccepted: root.context.tryUnlock() + Connections { + target: root.context + function onCurrentTextChanged() { + passwordBox.text = root.context.currentText; + } + } + } + } + + RippleButton { + anchors { + verticalCenter: passwordBoxContainer.verticalCenter + left: passwordBoxContainer.right + leftMargin: 5 + } + + visible: opacity > 0 + implicitHeight: passwordBoxContainer.implicitHeight - 12 + implicitWidth: implicitHeight + toggled: true + buttonRadius: passwordBoxContainer.radius + colBackground: Appearance.colors.colLayer2 + onClicked: root.context.tryUnlock() + + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + iconSize: 24 + text: "arrow_right_alt" + color: Appearance.colors.colOnPrimary + } + } +} diff --git a/.config/quickshell/ii/modules/lock/pam/password.conf b/.config/quickshell/ii/modules/lock/pam/password.conf new file mode 100644 index 00000000..7e5d75ae --- /dev/null +++ b/.config/quickshell/ii/modules/lock/pam/password.conf @@ -0,0 +1 @@ +auth required pam_unix.so diff --git a/.config/quickshell/modules/mediaControls/MediaControls.qml b/.config/quickshell/ii/modules/mediaControls/MediaControls.qml similarity index 88% rename from .config/quickshell/modules/mediaControls/MediaControls.qml rename to .config/quickshell/ii/modules/mediaControls/MediaControls.qml index 4350658f..67c1fe0a 100644 --- a/.config/quickshell/modules/mediaControls/MediaControls.qml +++ b/.config/quickshell/ii/modules/mediaControls/MediaControls.qml @@ -1,16 +1,13 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils -import Qt5Compat.GraphicalEffects +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs +import qs.modules.common.functions import QtQuick import QtQuick.Layouts -import QtQuick.Controls import Quickshell import Quickshell.Io import Quickshell.Services.Mpris -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland @@ -54,7 +51,9 @@ Scope { for (let j = i + 1; j < players.length; ++j) { let p2 = players[j]; if (p1.trackTitle && p2.trackTitle && - (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle))) { + (p1.trackTitle.includes(p2.trackTitle) + || p2.trackTitle.includes(p1.trackTitle)) + || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) { group.push(j); } } @@ -77,7 +76,7 @@ Scope { root.visualizerPoints = []; } } - command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.config)}/quickshell/scripts/cava/raw_output_config.txt`] + command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`] stdout: SplitParser { onRead: data => { // Parse `;`-separated values into the visualizerPoints array @@ -106,8 +105,8 @@ Scope { WlrLayershell.namespace: "quickshell:mediaControls" anchors { - top: !ConfigOptions.bar.bottom - bottom: ConfigOptions.bar.bottom + top: !Config.options.bar.bottom + bottom: Config.options.bar.bottom left: true } mask: Region { @@ -158,7 +157,7 @@ Scope { GlobalShortcut { name: "mediaControlsToggle" - description: qsTr("Toggles media controls on press") + description: "Toggles media controls on press" onPressed: { if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) { @@ -170,7 +169,7 @@ Scope { } GlobalShortcut { name: "mediaControlsOpen" - description: qsTr("Opens media controls on press") + description: "Opens media controls on press" onPressed: { mediaControlsLoader.active = true; @@ -179,7 +178,7 @@ Scope { } GlobalShortcut { name: "mediaControlsClose" - description: qsTr("Closes media controls on press") + description: "Closes media controls on press" onPressed: { mediaControlsLoader.active = false; diff --git a/.config/quickshell/modules/mediaControls/PlayerControl.qml b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml similarity index 96% rename from .config/quickshell/modules/mediaControls/PlayerControl.qml rename to .config/quickshell/ii/modules/mediaControls/PlayerControl.qml index cce0788d..6f63c836 100644 --- a/.config/quickshell/modules/mediaControls/PlayerControl.qml +++ b/.config/quickshell/ii/modules/mediaControls/PlayerControl.qml @@ -1,20 +1,14 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Effects import QtQuick.Layouts -import QtQuick.Controls import Quickshell import Quickshell.Io import Quickshell.Services.Mpris -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland Item { // Player instance id: playerController @@ -23,7 +17,7 @@ Item { // Player instance property string artDownloadLocation: Directories.coverArt property string artFileName: Qt.md5(artUrl) + ".jpg" property string artFilePath: `${artDownloadLocation}/${artFileName}` - property color artDominantColor: colorQuantizer?.colors[0] || Appearance.m3colors.m3secondaryContainer + property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer property bool downloaded: false property list visualizerPoints: [] property real maxVisualizerValue: 1000 // Max value in the data points @@ -149,7 +143,7 @@ Item { // Player instance Rectangle { anchors.fill: parent - color: ColorUtils.transparentize(blendedColors.colLayer0, 0.25) + color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3) radius: root.popupRounding } } diff --git a/.config/quickshell/modules/notificationPopup/NotificationPopup.qml b/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml similarity index 70% rename from .config/quickshell/modules/notificationPopup/NotificationPopup.qml rename to .config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml index fb046343..d954cbfb 100644 --- a/.config/quickshell/modules/notificationPopup/NotificationPopup.qml +++ b/.config/quickshell/ii/modules/notificationPopup/NotificationPopup.qml @@ -1,7 +1,7 @@ -import "root:/" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "root:/services" +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -14,7 +14,7 @@ Scope { PanelWindow { id: root - visible: (Notifications.popupList.length > 0) + visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked screen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) ?? null WlrLayershell.namespace: "quickshell:notificationPopup" @@ -36,10 +36,13 @@ Scope { NotificationListView { id: listview - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 5 + anchors { + top: parent.top + bottom: parent.bottom + right: parent.right + rightMargin: 4 + topMargin: 4 + } implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2 popup: true } diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml b/.config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayBrightness.qml similarity index 90% rename from .config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml rename to .config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayBrightness.qml index 765386bc..c4edfded 100644 --- a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml +++ b/.config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayBrightness.qml @@ -1,6 +1,7 @@ -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -22,7 +23,7 @@ Scope { Timer { id: osdTimeout - interval: ConfigOptions.osd.timeout + interval: Config.options.osd.timeout repeat: false running: false onTriggered: { @@ -66,8 +67,8 @@ Scope { color: "transparent" anchors { - top: !ConfigOptions.bar.bottom - bottom: ConfigOptions.bar.bottom + top: !Config.options.bar.bottom + bottom: Config.options.bar.bottom } mask: Region { item: osdValuesWrapper @@ -108,7 +109,7 @@ Scope { icon: "light_mode" rotateIcon: true scaleIcon: true - name: qsTr("Brightness") + name: Translation.tr("Brightness") } } } @@ -134,7 +135,7 @@ Scope { GlobalShortcut { name: "osdBrightnessTrigger" - description: qsTr("Triggers brightness OSD on press") + description: "Triggers brightness OSD on press" onPressed: { root.triggerOsd() @@ -142,7 +143,7 @@ Scope { } GlobalShortcut { name: "osdBrightnessHide" - description: qsTr("Hides brightness OSD on press") + description: "Hides brightness OSD on press" onPressed: { root.showOsdValues = false diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml b/.config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayVolume.qml similarity index 94% rename from .config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml rename to .config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayVolume.qml index 5d23b440..48970d2e 100644 --- a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml +++ b/.config/quickshell/ii/modules/onScreenDisplay/OnScreenDisplayVolume.qml @@ -1,6 +1,7 @@ -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -22,7 +23,7 @@ Scope { Timer { id: osdTimeout - interval: ConfigOptions.osd.timeout + interval: Config.options.osd.timeout repeat: false running: false onTriggered: { @@ -78,8 +79,8 @@ Scope { color: "transparent" anchors { - top: !ConfigOptions.bar.bottom - bottom: ConfigOptions.bar.bottom + top: !Config.options.bar.bottom + bottom: Config.options.bar.bottom } mask: Region { item: osdValuesWrapper @@ -121,7 +122,7 @@ Scope { Layout.fillWidth: true value: Audio.sink?.audio.volume ?? 0 icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up" - name: qsTr("Volume") + name: Translation.tr("Volume") } Item { @@ -185,7 +186,7 @@ Scope { } GlobalShortcut { name: "osdVolumeTrigger" - description: qsTr("Triggers volume OSD on press") + description: "Triggers volume OSD on press" onPressed: { root.triggerOsd() @@ -193,7 +194,7 @@ Scope { } GlobalShortcut { name: "osdVolumeHide" - description: qsTr("Hides volume OSD on press") + description: "Hides volume OSD on press" onPressed: { root.showOsdValues = false diff --git a/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml b/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml similarity index 97% rename from .config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml rename to .config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml index dfbf4a6b..c6fc7838 100644 --- a/.config/quickshell/modules/onScreenDisplay/OsdValueIndicator.qml +++ b/.config/quickshell/ii/modules/onScreenDisplay/OsdValueIndicator.qml @@ -1,6 +1,6 @@ -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Effects diff --git a/.config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml b/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml similarity index 89% rename from .config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml rename to .config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml index e78f45b2..0913d008 100644 --- a/.config/quickshell/modules/onScreenKeyboard/OnScreenKeyboard.qml +++ b/.config/quickshell/ii/modules/onScreenKeyboard/OnScreenKeyboard.qml @@ -1,21 +1,18 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts import Quickshell.Io import Quickshell -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland Scope { // Scope id: root - property bool pinned: ConfigOptions?.osk.pinnedOnStartup ?? false + property bool pinned: Config.options?.osk.pinnedOnStartup ?? false component OskControlButton: GroupButton { // Pin button baseWidth: 40 @@ -36,7 +33,7 @@ Scope { // Scope sourceComponent: PanelWindow { // Window id: oskRoot - visible: oskLoader.active + visible: oskLoader.active && !GlobalStates.screenLocked anchors { bottom: true @@ -141,7 +138,7 @@ Scope { // Scope GlobalShortcut { name: "oskToggle" - description: qsTr("Toggles on screen keyboard on press") + description: "Toggles on screen keyboard on press" onPressed: { oskLoader.active = !oskLoader.active; @@ -150,7 +147,7 @@ Scope { // Scope GlobalShortcut { name: "oskOpen" - description: qsTr("Opens on screen keyboard on press") + description: "Opens on screen keyboard on press" onPressed: { oskLoader.active = true; @@ -159,7 +156,7 @@ Scope { // Scope GlobalShortcut { name: "oskClose" - description: qsTr("Closes on screen keyboard on press") + description: "Closes on screen keyboard on press" onPressed: { oskLoader.active = false; diff --git a/.config/quickshell/modules/onScreenKeyboard/OskContent.qml b/.config/quickshell/ii/modules/onScreenKeyboard/OskContent.qml similarity index 85% rename from .config/quickshell/modules/onScreenKeyboard/OskContent.qml rename to .config/quickshell/ii/modules/onScreenKeyboard/OskContent.qml index 06e954ad..f505b597 100644 --- a/.config/quickshell/modules/onScreenKeyboard/OskContent.qml +++ b/.config/quickshell/ii/modules/onScreenKeyboard/OskContent.qml @@ -1,7 +1,7 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import "layouts.js" as Layouts import QtQuick import QtQuick.Controls @@ -13,7 +13,7 @@ import Quickshell.Hyprland Item { id: root - property var activeLayoutName: ConfigOptions?.osk.layout ?? Layouts.defaultLayout + property var activeLayoutName: Config.options?.osk.layout ?? Layouts.defaultLayout property var layouts: Layouts.byName property var currentLayout: layouts[activeLayoutName] diff --git a/.config/quickshell/modules/onScreenKeyboard/OskKey.qml b/.config/quickshell/ii/modules/onScreenKeyboard/OskKey.qml similarity index 94% rename from .config/quickshell/modules/onScreenKeyboard/OskKey.qml rename to .config/quickshell/ii/modules/onScreenKeyboard/OskKey.qml index 1f28a9e5..5ae53b94 100644 --- a/.config/quickshell/modules/onScreenKeyboard/OskKey.qml +++ b/.config/quickshell/ii/modules/onScreenKeyboard/OskKey.qml @@ -1,13 +1,9 @@ -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland RippleButton { id: root diff --git a/.config/quickshell/modules/onScreenKeyboard/layouts.js b/.config/quickshell/ii/modules/onScreenKeyboard/layouts.js similarity index 100% rename from .config/quickshell/modules/onScreenKeyboard/layouts.js rename to .config/quickshell/ii/modules/onScreenKeyboard/layouts.js diff --git a/.config/quickshell/modules/overview/Overview.qml b/.config/quickshell/ii/modules/overview/Overview.qml similarity index 54% rename from .config/quickshell/modules/overview/Overview.qml rename to .config/quickshell/ii/modules/overview/Overview.qml index a7817e6e..1af8cb9c 100644 --- a/.config/quickshell/modules/overview/Overview.qml +++ b/.config/quickshell/ii/modules/overview/Overview.qml @@ -1,7 +1,7 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -33,25 +33,23 @@ Scope { mask: Region { item: GlobalStates.overviewOpen ? columnLayout : null } - HyprlandWindow.visibleMask: Region { - item: GlobalStates.overviewOpen ? columnLayout : null - } - + // HyprlandWindow.visibleMask: Region { // Buggy with scaled monitors + // item: GlobalStates.overviewOpen ? columnLayout : null + // } anchors { top: true - left: true - right: true bottom: true } HyprlandFocusGrab { id: grab - windows: [ root ] + windows: [root] property bool canBeActive: root.monitorIsFocused active: false onCleared: () => { - if (!active) GlobalStates.overviewOpen = false + if (!active) + GlobalStates.overviewOpen = false; } } @@ -59,24 +57,25 @@ Scope { target: GlobalStates function onOverviewOpenChanged() { if (!GlobalStates.overviewOpen) { - searchWidget.disableExpandAnimation() + searchWidget.disableExpandAnimation(); overviewScope.dontAutoCancelSearch = false; } else { if (!overviewScope.dontAutoCancelSearch) { - searchWidget.cancelSearch() + searchWidget.cancelSearch(); } - delayedGrabTimer.start() + delayedGrabTimer.start(); } } } Timer { id: delayedGrabTimer - interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + interval: Config.options.hacks.arbitraryRaceConditionDelay repeat: false onTriggered: { - if (!grab.canBeActive) return - grab.active = GlobalStates.overviewOpen + if (!grab.canBeActive) + return; + grab.active = GlobalStates.overviewOpen; } } @@ -85,6 +84,7 @@ Scope { function setSearchingText(text) { searchWidget.setSearchingText(text); + searchWidget.focusFirstItemIfNeeded(); } ColumnLayout { @@ -92,17 +92,18 @@ Scope { visible: GlobalStates.overviewOpen anchors { horizontalCenter: parent.horizontalCenter - top: !ConfigOptions.bar.bottom ? parent.top : undefined - bottom: ConfigOptions.bar.bottom ? parent.bottom : undefined + top: parent.top } - Keys.onPressed: (event) => { + Keys.onPressed: event => { if (event.key === Qt.Key_Escape) { GlobalStates.overviewOpen = false; } else if (event.key === Qt.Key_Left) { - if (!root.searchingText) Hyprland.dispatch("workspace r-1"); + if (!root.searchingText) + Hyprland.dispatch("workspace r-1"); } else if (event.key === Qt.Key_Right) { - if (!root.searchingText) Hyprland.dispatch("workspace r+1"); + if (!root.searchingText) + Hyprland.dispatch("workspace r+1"); } } @@ -114,8 +115,8 @@ Scope { SearchWidget { id: searchWidget Layout.alignment: Qt.AlignHCenter - onSearchingTextChanged: (text) => { - root.searchingText = searchingText + onSearchingTextChanged: text => { + root.searchingText = searchingText; } } @@ -128,113 +129,116 @@ Scope { } } } + } + } + function toggleClipboard() { + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText(Config.options.search.prefix.clipboard); + GlobalStates.overviewOpen = true; + return; + } + } + } + + function toggleEmojis() { + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText(Config.options.search.prefix.emojis); + GlobalStates.overviewOpen = true; + return; + } } } IpcHandler { - target: "overview" + target: "overview" function toggle() { - GlobalStates.overviewOpen = !GlobalStates.overviewOpen + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } function close() { - GlobalStates.overviewOpen = false + GlobalStates.overviewOpen = false; } function open() { - GlobalStates.overviewOpen = true + GlobalStates.overviewOpen = true; } function toggleReleaseInterrupt() { - GlobalStates.superReleaseMightTrigger = false + GlobalStates.superReleaseMightTrigger = false; } - } + function clipboardToggle() { + overviewScope.toggleClipboard(); + } + } GlobalShortcut { name: "overviewToggle" - description: qsTr("Toggles overview on press") + description: "Toggles overview on press" onPressed: { - GlobalStates.overviewOpen = !GlobalStates.overviewOpen + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } } GlobalShortcut { name: "overviewClose" - description: qsTr("Closes overview") + description: "Closes overview" onPressed: { - GlobalStates.overviewOpen = false + GlobalStates.overviewOpen = false; } } GlobalShortcut { name: "overviewToggleRelease" - description: qsTr("Toggles overview on release") + description: "Toggles overview on release" onPressed: { - GlobalStates.superReleaseMightTrigger = true + GlobalStates.superReleaseMightTrigger = true; } onReleased: { if (!GlobalStates.superReleaseMightTrigger) { - GlobalStates.superReleaseMightTrigger = true - return + GlobalStates.superReleaseMightTrigger = true; + return; } - GlobalStates.overviewOpen = !GlobalStates.overviewOpen + GlobalStates.overviewOpen = !GlobalStates.overviewOpen; } } GlobalShortcut { name: "overviewToggleReleaseInterrupt" - description: qsTr("Interrupts possibility of overview being toggled on release. ") + - qsTr("This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. ") + - qsTr("To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.") + description: "Interrupts possibility of overview being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything." onPressed: { - GlobalStates.superReleaseMightTrigger = false + GlobalStates.superReleaseMightTrigger = false; } } GlobalShortcut { name: "overviewClipboardToggle" - description: qsTr("Toggle clipboard query on overview widget") + description: "Toggle clipboard query on overview widget" onPressed: { - if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { - GlobalStates.overviewOpen = false; - return; - } - for (let i = 0; i < overviewVariants.instances.length; i++) { - let panelWindow = overviewVariants.instances[i]; - if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { - overviewScope.dontAutoCancelSearch = true; - panelWindow.setSearchingText( - ConfigOptions.search.prefix.clipboard - ); - GlobalStates.overviewOpen = true; - return - } - } + overviewScope.toggleClipboard(); } } GlobalShortcut { name: "overviewEmojiToggle" - description: qsTr("Toggle emoji query on overview widget") + description: "Toggle emoji query on overview widget" onPressed: { - if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { - GlobalStates.overviewOpen = false; - return; - } - for (let i = 0; i < overviewVariants.instances.length; i++) { - let panelWindow = overviewVariants.instances[i]; - if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { - overviewScope.dontAutoCancelSearch = true; - panelWindow.setSearchingText( - ConfigOptions.search.prefix.emojis - ); - GlobalStates.overviewOpen = true; - return - } - } + overviewScope.toggleEmojis(); } } - } diff --git a/.config/quickshell/modules/overview/OverviewWidget.qml b/.config/quickshell/ii/modules/overview/OverviewWidget.qml similarity index 86% rename from .config/quickshell/modules/overview/OverviewWidget.qml rename to .config/quickshell/ii/modules/overview/OverviewWidget.qml index e0999d6e..c45867d4 100644 --- a/.config/quickshell/modules/overview/OverviewWidget.qml +++ b/.config/quickshell/ii/modules/overview/OverviewWidget.qml @@ -1,14 +1,11 @@ -import "root:/" -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Effects import QtQuick.Layouts import Quickshell -import Quickshell.Io -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland @@ -17,14 +14,14 @@ Item { required property var panelWindow readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) readonly property var toplevels: ToplevelManager.toplevels - readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols + readonly property int workspacesShown: Config.options.overview.rows * Config.options.overview.columns readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name) property var windows: HyprlandData.windowList property var windowByAddress: HyprlandData.windowByAddress property var windowAddresses: HyprlandData.addresses property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) - property real scale: ConfigOptions.overview.scale + property real scale: Config.options.overview.scale property color activeBorderColor: Appearance.colors.colSecondary property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ? @@ -63,6 +60,8 @@ Item { implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 radius: Appearance.rounding.screenRounding * root.scale + padding color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant ColumnLayout { // Workspaces id: workspaceColumnLayout @@ -71,18 +70,18 @@ Item { anchors.centerIn: parent spacing: workspaceSpacing Repeater { - model: ConfigOptions.overview.numOfRows + model: Config.options.overview.rows delegate: RowLayout { id: row property int rowIndex: index spacing: workspaceSpacing Repeater { // Workspace repeater - model: ConfigOptions.overview.numOfCols + model: Config.options.overview.columns Rectangle { // Workspace id: workspace property int colIndex: index - property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1 + property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.columns + colIndex + 1 property color defaultWorkspaceColor: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) property color hoveredBorderColor: Appearance.colors.colLayer2Hover @@ -111,7 +110,6 @@ Item { acceptedButtons: Qt.LeftButton onClicked: { if (root.draggingTargetWorkspace === -1) { - // Hyprland.dispatch(`exec qs ipc call overview close`) GlobalStates.overviewOpen = false Hyprland.dispatch(`workspace ${workspaceValue}`) } @@ -149,9 +147,10 @@ Item { // console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2)) return ToplevelManager.toplevels.values.filter((toplevel) => { const address = `0x${toplevel.HyprlandToplevel.address}` - // console.log(`Checking window with address: ${address}`) var win = windowByAddress[address] - return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + const inMonitor = root.monitor.id === win.monitor + return inWorkspaceGroup && inMonitor; }) } } @@ -161,7 +160,7 @@ Item { property var address: `0x${modelData.HyprlandToplevel.address}` windowData: windowByAddress[address] toplevel: modelData - monitorData: root.monitorData + monitorData: HyprlandData.monitors[monitorId] scale: root.scale availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceHeight: root.workspaceImplicitHeight @@ -170,22 +169,20 @@ Item { property var monitor: HyprlandData.monitors[monitorId] property bool atInitPosition: (initX == x && initY == y) - restrictToWorkspace: Drag.active || atInitPosition - property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols) - xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex - (monitor?.x * root.scale) - yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex - (monitor?.y * root.scale) + property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns + property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex Timer { id: updateWindowPosition - interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + interval: Config.options.hacks.arbitraryRaceConditionDelay repeat: false running: false onTriggered: { - window.x = Math.round(Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset) - window.y = Math.round(Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset) - // console.log(`[OverviewWindow] Updated position for window ${windowData?.address} to (${window.x}, ${window.y})`) + window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset) + window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset) } } @@ -200,11 +197,13 @@ Item { onExited: hovered = false // For hover color change acceptedButtons: Qt.LeftButton | Qt.MiddleButton drag.target: parent - onPressed: { + onPressed: (mouse) => { root.draggingFromWorkspace = windowData?.workspace.id window.pressed = true window.Drag.active = true window.Drag.source = window + window.Drag.hotSpot.x = mouse.x + window.Drag.hotSpot.y = mouse.y // console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`) } onReleased: { @@ -246,8 +245,8 @@ Item { Rectangle { // Focused workspace indicator id: focusedWorkspaceIndicator property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) - property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / ConfigOptions.overview.numOfCols) - property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % ConfigOptions.overview.numOfCols + property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns) + property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex z: root.windowZ diff --git a/.config/quickshell/modules/overview/OverviewWindow.qml b/.config/quickshell/ii/modules/overview/OverviewWindow.qml similarity index 71% rename from .config/quickshell/modules/overview/OverviewWindow.qml rename to .config/quickshell/ii/modules/overview/OverviewWindow.qml index 3b376988..8029ec4c 100644 --- a/.config/quickshell/modules/overview/OverviewWindow.qml +++ b/.config/quickshell/ii/modules/overview/OverviewWindow.qml @@ -1,17 +1,13 @@ -import "root:/" -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Widgets -import Quickshell.Io import Quickshell.Wayland -import Quickshell.Hyprland Item { // Window id: root @@ -22,8 +18,8 @@ Item { // Window property var availableWorkspaceWidth property var availableWorkspaceHeight property bool restrictToWorkspace: true - property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset - property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real initX: Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0) + xOffset + property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset property real xOffset: 0 property real yOffset: 0 @@ -38,12 +34,12 @@ Item { // Window property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing") property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth - property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false + property bool indicateXWayland: windowData?.xwayland ?? false x: initX y: initY - width: Math.round(Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset))) - height: Math.round(Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset))) + width: windowData?.size[0] * root.scale + height: windowData?.size[1] * root.scale layer.enabled: true layer.effect: OpacityMask { @@ -91,7 +87,14 @@ Item { // Window Image { id: windowIcon - property var iconSize: Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) + property var iconSize: { + // console.log("-=-=-", root.toplevel.title, "-=-=-") + // console.log("Target window size:", targetWindowWidth, targetWindowHeight) + // console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) + // console.log("Scale:", root.monitorData.scale) + // console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale) + return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale; + } // mipmap: true Layout.alignment: Qt.AlignHCenter source: root.iconPath diff --git a/.config/quickshell/modules/overview/SearchItem.qml b/.config/quickshell/ii/modules/overview/SearchItem.qml similarity index 78% rename from .config/quickshell/modules/overview/SearchItem.qml rename to .config/quickshell/ii/modules/overview/SearchItem.qml index d23cb4c0..1fb23b27 100644 --- a/.config/quickshell/modules/overview/SearchItem.qml +++ b/.config/quickshell/ii/modules/overview/SearchItem.qml @@ -1,15 +1,11 @@ // pragma NativeMethodBehavior: AcceptThisObject -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Io import Quickshell.Widgets import Quickshell.Hyprland @@ -18,7 +14,7 @@ RippleButton { property var entry property string query property bool entryShown: entry?.shown ?? true - property string itemType: entry?.type + property string itemType: entry?.type ?? Translation.tr("App") property string itemName: entry?.name property string itemIcon: entry?.icon ?? "" property var itemExecute: entry?.execute @@ -27,6 +23,21 @@ RippleButton { property string bigText: entry?.bigText ?? "" property string materialSymbol: entry?.materialSymbol ?? "" property string cliphistRawString: entry?.cliphistRawString ?? "" + + visible: root.entryShown + property int horizontalMargin: 10 + property int buttonHorizontalPadding: 10 + property int buttonVerticalPadding: 5 + property bool keyboardDown: false + + implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2 + implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2 + buttonRadius: Appearance.rounding.normal + colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colSecondaryContainerActive : + ((root.hovered || root.focus) ? Appearance.colors.colSecondaryContainerHover : + ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, 1)) + colBackgroundHover: Appearance.colors.colSecondaryContainerHover + colRipple: Appearance.colors.colSecondaryContainerActive property string highlightPrefix: `` property string highlightSuffix: `` @@ -69,20 +80,7 @@ RippleButton { return matches ? matches : []; } - visible: root.entryShown - property int horizontalMargin: 10 - property int buttonHorizontalPadding: 10 - property int buttonVerticalPadding: 5 - property bool keyboardDown: false - - implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2 - implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2 - buttonRadius: Appearance.rounding.normal - colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active : - ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover : - ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHigh, 1)) - colBackgroundHover: Appearance.colors.colLayer1Hover - colRipple: Appearance.colors.colLayer1Active + PointingHandInteraction {} background { anchors.fill: root @@ -90,7 +88,6 @@ RippleButton { anchors.rightMargin: root.horizontalMargin } - PointingHandInteraction {} onClicked: { root.itemExecute() Hyprland.dispatch("global quickshell:overviewClose") @@ -162,7 +159,7 @@ RippleButton { StyledText { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colSubtext - visible: root.itemType && root.itemType != qsTr("App") + visible: root.itemType && root.itemType != Translation.tr("App") text: root.itemType } RowLayout { @@ -224,5 +221,47 @@ RippleButton { horizontalAlignment: Text.AlignRight text: root.itemClickActionName } + + RowLayout { + spacing: 4 + Repeater { + model: (root.entry.actions ?? []).slice(0, 4) + delegate: RippleButton { + id: actionButton + required property var modelData + implicitHeight: 34 + implicitWidth: 34 + + contentItem: Item { + id: actionContentItem + anchors.centerIn: parent + Loader { + anchors.centerIn: parent + active: !(actionButton.modelData.icon && actionButton.modelData.icon !== "") + sourceComponent: MaterialSymbol { + text: "video_settings" + font.pixelSize: Appearance.font.pixelSize.hugeass + color: Appearance.m3colors.m3onSurface + } + } + Loader { + anchors.centerIn: parent + active: actionButton.modelData.icon && actionButton.modelData.icon !== "" + sourceComponent: IconImage { + source: Quickshell.iconPath(actionButton.modelData.icon) + implicitSize: 20 + } + } + } + + onClicked: modelData.execute() + + StyledToolTip { + content: modelData.name + } + } + } + } + } } diff --git a/.config/quickshell/modules/overview/SearchWidget.qml b/.config/quickshell/ii/modules/overview/SearchWidget.qml similarity index 66% rename from .config/quickshell/modules/overview/SearchWidget.qml rename to .config/quickshell/ii/modules/overview/SearchWidget.qml index 614efa91..6e4c3cbf 100644 --- a/.config/quickshell/modules/overview/SearchWidget.qml +++ b/.config/quickshell/ii/modules/overview/SearchWidget.qml @@ -1,17 +1,14 @@ -import "root:/" -import "root:/services/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import Qt5Compat.GraphicalEffects -import Qt.labs.platform import QtQuick import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Io -import Quickshell.Hyprland Item { // Wrapper id: root @@ -29,9 +26,9 @@ Item { // Wrapper } function cancelSearch() { - searchInput.selectAll() - root.searchingText = "" - searchWidthBehavior.enabled = true; + searchInput.selectAll(); + root.searchingText = ""; + searchWidthBehavior.enabled = true; } function setSearchingText(text) { @@ -40,47 +37,52 @@ Item { // Wrapper } property var searchActions: [ - { - action: "img", - execute: () => { - executor.executeCommand(Directories.wallpaperSwitchScriptPath) - } - }, { action: "dark", execute: () => { - executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`) + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]); } }, { action: "light", execute: () => { - executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`) + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]); + } + }, + { + action: "wall", + execute: () => { + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath]); + } + }, + { + action: "konachanwall", + execute: () => { + Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random_konachan_wall.sh")]); } }, { action: "accentcolor", - execute: (args) => { - executor.executeCommand( - `${Directories.wallpaperSwitchScriptPath} --noswitch --color ${args != '' ? ("'"+args+"'") : ""}` - ) + execute: args => { + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]); } }, { action: "todo", - execute: (args) => { - Todo.addTask(args) + execute: args => { + Todo.addTask(args); } }, ] function focusFirstItemIfNeeded() { - if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item + if (searchInput.focus) + appResults.currentIndex = 0; // Focus the first item } Timer { id: nonAppResultsTimer - interval: ConfigOptions.search.nonAppResultDelay + interval: Config.options.search.nonAppResultDelay onTriggered: { mathProcess.calculateExpression(root.searchingText); } @@ -90,32 +92,22 @@ Item { // Wrapper id: mathProcess property list baseCommand: ["qalc", "-t"] function calculateExpression(expression) { - // mathProcess.running = false - mathProcess.command = baseCommand.concat(expression) - mathProcess.running = true + mathProcess.running = false; + mathProcess.command = baseCommand.concat(expression); + mathProcess.running = true; } stdout: SplitParser { onRead: data => { - root.mathResult = data - root.focusFirstItemIfNeeded() + root.mathResult = data; + root.focusFirstItemIfNeeded(); } } } - Process { - id: executor - property list baseCommand: ["bash", "-c"] - function executeCommand(command) { - executor.command = baseCommand.concat( - `${command}` - ) - executor.startDetached() - } - } - - Keys.onPressed: (event) => { + Keys.onPressed: event => { // Prevent Esc and Backspace from registering - if (event.key === Qt.Key_Escape) return; + if (event.key === Qt.Key_Escape) + return; // Handle Backspace: focus and delete character if not focused if (event.key === Qt.Key_Backspace) { @@ -136,8 +128,7 @@ Item { // Wrapper } else { // Delete character before cursor if any if (searchInput.cursorPosition > 0) { - searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) + - searchInput.text.slice(searchInput.cursorPosition); + searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) + searchInput.text.slice(searchInput.cursorPosition); searchInput.cursorPosition -= 1; } } @@ -150,19 +141,12 @@ Item { // Wrapper } // Only handle visible printable characters (ignore control chars, arrows, etc.) - if ( - event.text && - event.text.length === 1 && - event.key !== Qt.Key_Enter && - event.key !== Qt.Key_Return && - event.text.charCodeAt(0) >= 0x20 // ignore control chars like Backspace, Tab, etc. - ) { + if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc. + { if (!searchInput.activeFocus) { searchInput.forceActiveFocus(); // Insert the character at the cursor position - searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) + - event.text + - searchInput.text.slice(searchInput.cursorPosition); + searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) + event.text + searchInput.text.slice(searchInput.cursorPosition); searchInput.cursorPosition += 1; event.accepted = true; } @@ -179,6 +163,8 @@ Item { // Wrapper implicitHeight: columnLayout.implicitHeight radius: Appearance.rounding.large color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant ColumnLayout { id: columnLayout @@ -203,7 +189,7 @@ Item { // Wrapper Layout.leftMargin: 15 iconSize: Appearance.font.pixelSize.huge color: Appearance.m3colors.m3onSurface - text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : 'search' + text: root.searchingText.startsWith(Config.options.search.prefix.clipboard) ? 'content_paste_search' : 'search' } TextField { // Search box id: searchInput @@ -220,7 +206,7 @@ Item { // Wrapper color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer - placeholderText: qsTr("Search, calculate or run") + placeholderText: Translation.tr("Search, calculate or run") placeholderTextColor: Appearance.m3colors.m3outline implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth @@ -256,7 +242,8 @@ Item { // Wrapper } } - Rectangle { // Separator + Rectangle { + // Separator visible: root.showResults Layout.fillWidth: true height: 1 @@ -273,10 +260,11 @@ Item { // Wrapper bottomMargin: 10 spacing: 2 KeyNavigation.up: searchBar - highlightMoveDuration : 100 + highlightMoveDuration: 100 onFocusChanged: { - if(focus) appResults.currentIndex = 1; + if (focus) + appResults.currentIndex = 1; } Connections { @@ -289,13 +277,16 @@ Item { // Wrapper model: ScriptModel { id: model - values: { // Search results are handled here + values: { + // Search results are handled here ////////////////// Skip? ////////////////// - if(root.searchingText == "") return []; + if (root.searchingText == "") + return []; ///////////// Special cases /////////////// - if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard - const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); + if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) { + // Clipboard + const searchString = root.searchingText.slice(Config.options.search.prefix.clipboard.length); return Cliphist.fuzzyQuery(searchString).map(entry => { return { cliphistRawString: entry, @@ -303,13 +294,23 @@ Item { // Wrapper clickActionName: "", type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, execute: () => { - Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); - } + Cliphist.copy(entry) + }, + actions: [ + { + name: "Delete", + icon: "delete", + execute: () => { + Cliphist.deleteEntry(entry); + } + } + ] }; }).filter(Boolean); - } - if (root.searchingText.startsWith(ConfigOptions.search.prefix.emojis)) { // Clipboard - const searchString = root.searchingText.slice(ConfigOptions.search.prefix.emojis.length); + } + if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) { + // Clipboard + const searchString = root.searchingText.slice(Config.options.search.prefix.emojis.length); return Emojis.fuzzyQuery(searchString).map(entry => { return { cliphistRawString: entry, @@ -318,64 +319,59 @@ Item { // Wrapper clickActionName: "", type: "Emoji", execute: () => { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(entry.match(/^\s*(\S+)/)?.[1])}'`); + Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; } }; }).filter(Boolean); - } - + } ////////////////// Init /////////////////// nonAppResultsTimer.restart(); const mathResultObject = { name: root.mathResult, - clickActionName: qsTr("Copy"), - type: qsTr("Math result"), + clickActionName: Translation.tr("Copy"), + type: Translation.tr("Math result"), fontType: "monospace", materialSymbol: 'calculate', execute: () => { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.mathResult)}'`) + Quickshell.clipboardText = root.mathResult; } - } + }; const commandResultObject = { name: searchingText.replace("file://", ""), - clickActionName: qsTr("Run"), - type: qsTr("Run command"), + clickActionName: Translation.tr("Run"), + type: Translation.tr("Run command"), fontType: "monospace", materialSymbol: 'terminal', execute: () => { - executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText.replace("file://", "")}'` : root.searchingText.replace("file://", "")); + const cleanedCommand = root.searchingText.replace("file://", ""); + Quickshell.execDetached(["bash", "-c", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); } - } - const launcherActionObjects = root.searchActions - .map(action => { - const actionString = `${ConfigOptions.search.prefix.action}${action.action}`; - if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) { - return { - name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, - clickActionName: qsTr("Run"), - type: qsTr("Action"), - materialSymbol: 'settings_suggest', - execute: () => { - action.execute(root.searchingText.split(" ").slice(1).join(" ")) - }, - }; - } - return null; - }) - .filter(Boolean); + }; + const launcherActionObjects = root.searchActions.map(action => { + const actionString = `${Config.options.search.prefix.action}${action.action}`; + if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) { + return { + name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, + clickActionName: Translation.tr("Run"), + type: Translation.tr("Action"), + materialSymbol: 'settings_suggest', + execute: () => { + action.execute(root.searchingText.split(" ").slice(1).join(" ")); + } + }; + } + return null; + }).filter(Boolean); let result = []; //////////////// Apps ////////////////// - result = result.concat( - AppSearch.fuzzyQuery(root.searchingText) - .map((entry) => { - entry.clickActionName = qsTr("Launch"); - entry.type = qsTr("App"); - return entry; - }) - ); + result = result.concat(AppSearch.fuzzyQuery(root.searchingText).map(entry => { + entry.clickActionName = Translation.tr("Launch"); + entry.type = Translation.tr("App"); + return entry; + })); ////////// Launcher actions //////////// result = result.concat(launcherActionObjects); @@ -393,12 +389,12 @@ Item { // Wrapper ///////////////// Web search //////////////// result.push({ name: root.searchingText, - clickActionName: qsTr("Search"), - type: qsTr("Search the web"), + clickActionName: Translation.tr("Search"), + type: Translation.tr("Search the web"), materialSymbol: 'travel_explore', execute: () => { - let url = ConfigOptions.search.engineBaseUrl + root.searchingText - for (let site of ConfigOptions.search.excludedSites) { + let url = Config.options.search.engineBaseUrl + root.searchingText; + for (let site of Config.options.search.excludedSites) { url += ` -site:${site}`; } Qt.openUrlExternally(url); @@ -409,17 +405,17 @@ Item { // Wrapper } } - delegate: SearchItem { // The selectable item for each search result + onModelChanged: root.focusFirstItemIfNeeded() + + delegate: SearchItem { + // The selectable item for each search result required property var modelData anchors.left: parent?.left anchors.right: parent?.right entry: modelData - query: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? - root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length) : - root.searchingText; + query: root.searchingText.startsWith(Config.options.search.prefix.clipboard) ? root.searchingText.slice(Config.options.search.prefix.clipboard.length) : root.searchingText } } - } } -} \ No newline at end of file +} diff --git a/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml b/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml new file mode 100644 index 00000000..5bfed5cd --- /dev/null +++ b/.config/quickshell/ii/modules/screenCorners/ScreenCorners.qml @@ -0,0 +1,66 @@ +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: screenCorners + readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + + component CornerPanelWindow: PanelWindow { + id: cornerPanelWindow + visible: (Config.options.appearance.fakeScreenRounding === 1 || (Config.options.appearance.fakeScreenRounding === 2 && !activeWindow?.fullscreen)) + property var corner + + exclusionMode: ExclusionMode.Ignore + mask: Region { + item: null + } + WlrLayershell.namespace: "quickshell:screenCorners" + WlrLayershell.layer: WlrLayer.Overlay + color: "transparent" + + anchors { + top: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.TopRight + left: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomLeft + bottom: cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomLeft || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomRight + right: cornerPanelWindow.corner === RoundCorner.CornerEnum.TopRight || cornerPanelWindow.corner === RoundCorner.CornerEnum.BottomRight + } + + implicitWidth: cornerWidget.implicitWidth + implicitHeight: cornerWidget.implicitHeight + RoundCorner { + id: cornerWidget + size: Appearance.rounding.screenRounding + corner: cornerPanelWindow.corner + } + } + + Variants { + model: Quickshell.screens + + Scope { + required property var modelData + CornerPanelWindow { + screen: modelData + corner: RoundCorner.CornerEnum.TopLeft + } + CornerPanelWindow { + screen: modelData + corner: RoundCorner.CornerEnum.TopRight + } + CornerPanelWindow { + screen: modelData + corner: RoundCorner.CornerEnum.BottomLeft + } + CornerPanelWindow { + screen: modelData + corner: RoundCorner.CornerEnum.BottomRight + } + } + } +} diff --git a/.config/quickshell/modules/session/Session.qml b/.config/quickshell/ii/modules/session/Session.qml similarity index 74% rename from .config/quickshell/modules/session/Session.qml rename to .config/quickshell/ii/modules/session/Session.qml index 94e6123f..51f84ca9 100644 --- a/.config/quickshell/modules/session/Session.qml +++ b/.config/quickshell/ii/modules/session/Session.qml @@ -1,12 +1,13 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs +import qs.services +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland @@ -14,10 +15,25 @@ Scope { id: root property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) + function closeAllWindows() { + HyprlandData.windowList.map(w => w.pid).forEach((pid) => { + Quickshell.execDetached(["kill", pid]); + }); + } + Loader { id: sessionLoader active: false + Connections { + target: GlobalStates + function onScreenLockedChanged() { + if (GlobalStates.screenLocked) { + sessionLoader.active = false; + } + } + } + sourceComponent: PanelWindow { // Session menu id: sessionRoot visible: sessionLoader.active @@ -70,15 +86,14 @@ Scope { font.family: Appearance.font.family.title font.pixelSize: Appearance.font.pixelSize.title font.weight: Font.DemiBold - text: qsTr("Session") + text: Translation.tr("Session") } StyledText { // Small instruction Layout.alignment: Qt.AlignHCenter horizontalAlignment: Text.AlignHCenter - font.family: Appearance.font.family.title font.pixelSize: Appearance.font.pixelSize.normal - text: qsTr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel") + text: Translation.tr("Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel") } } @@ -91,8 +106,8 @@ Scope { id: sessionLock focus: sessionRoot.visible buttonIcon: "lock" - buttonText: qsTr("Lock") - onClicked: { Hyprland.dispatch("exec loginctl lock-session"); sessionRoot.hide() } + buttonText: Translation.tr("Lock") + onClicked: { Quickshell.execDetached(["loginctl", "lock-session"]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.right: sessionSleep KeyNavigation.down: sessionHibernate @@ -100,8 +115,8 @@ Scope { SessionActionButton { id: sessionSleep buttonIcon: "dark_mode" - buttonText: qsTr("Sleep") - onClicked: { Hyprland.dispatch("exec systemctl suspend || loginctl suspend"); sessionRoot.hide() } + buttonText: Translation.tr("Sleep") + onClicked: { Quickshell.execDetached(["bash", "-c", "systemctl suspend || loginctl suspend"]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.left: sessionLock KeyNavigation.right: sessionLogout @@ -110,8 +125,8 @@ Scope { SessionActionButton { id: sessionLogout buttonIcon: "logout" - buttonText: qsTr("Logout") - onClicked: { Hyprland.dispatch("exec pkill Hyprland"); sessionRoot.hide() } + buttonText: Translation.tr("Logout") + onClicked: { root.closeAllWindows(); Quickshell.execDetached(["pkill", "Hyprland"]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.left: sessionSleep KeyNavigation.right: sessionTaskManager @@ -120,8 +135,8 @@ Scope { SessionActionButton { id: sessionTaskManager buttonIcon: "browse_activity" - buttonText: qsTr("Task Manager") - onClicked: { Hyprland.dispatch(`exec ${ConfigOptions.apps.taskManager}`); sessionRoot.hide() } + buttonText: Translation.tr("Task Manager") + onClicked: { Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.left: sessionLogout KeyNavigation.down: sessionFirmwareReboot @@ -130,8 +145,8 @@ Scope { SessionActionButton { id: sessionHibernate buttonIcon: "downloading" - buttonText: qsTr("Hibernate") - onClicked: { Hyprland.dispatch("exec systemctl hibernate || loginctl hibernate"); sessionRoot.hide() } + buttonText: Translation.tr("Hibernate") + onClicked: { Quickshell.execDetached(["bash", "-c", `systemctl hibernate || loginctl hibernate`]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.up: sessionLock KeyNavigation.right: sessionShutdown @@ -139,8 +154,8 @@ Scope { SessionActionButton { id: sessionShutdown buttonIcon: "power_settings_new" - buttonText: qsTr("Shutdown") - onClicked: { Hyprland.dispatch("exec systemctl poweroff || loginctl poweroff"); sessionRoot.hide() } + buttonText: Translation.tr("Shutdown") + onClicked: { root.closeAllWindows(); Quickshell.execDetached(["bash", "-c", `systemctl poweroff || loginctl poweroff`]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.left: sessionHibernate KeyNavigation.right: sessionReboot @@ -149,8 +164,8 @@ Scope { SessionActionButton { id: sessionReboot buttonIcon: "restart_alt" - buttonText: qsTr("Reboot") - onClicked: { Hyprland.dispatch("exec reboot || loginctl reboot"); sessionRoot.hide() } + buttonText: Translation.tr("Reboot") + onClicked: { root.closeAllWindows(); Quickshell.execDetached(["bash", "-c", `reboot || loginctl reboot`]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.left: sessionShutdown KeyNavigation.right: sessionFirmwareReboot @@ -159,8 +174,8 @@ Scope { SessionActionButton { id: sessionFirmwareReboot buttonIcon: "settings_applications" - buttonText: qsTr("Reboot to firmware settings") - onClicked: { Hyprland.dispatch("exec systemctl reboot --firmware-setup || loginctl reboot --firmware-setup"); sessionRoot.hide() } + buttonText: Translation.tr("Reboot to firmware settings") + onClicked: { root.closeAllWindows(); Quickshell.execDetached(["bash", "-c", `systemctl reboot --firmware-setup || loginctl reboot --firmware-setup`]); sessionRoot.hide() } onFocusChanged: { if (focus) sessionRoot.subtitle = buttonText } KeyNavigation.up: sessionTaskManager KeyNavigation.left: sessionReboot @@ -209,7 +224,7 @@ Scope { GlobalShortcut { name: "sessionToggle" - description: qsTr("Toggles session screen on press") + description: "Toggles session screen on press" onPressed: { sessionLoader.active = !sessionLoader.active; @@ -218,7 +233,7 @@ Scope { GlobalShortcut { name: "sessionOpen" - description: qsTr("Opens session screen on press") + description: "Opens session screen on press" onPressed: { sessionLoader.active = true; diff --git a/.config/quickshell/modules/session/SessionActionButton.qml b/.config/quickshell/ii/modules/session/SessionActionButton.qml similarity index 89% rename from .config/quickshell/modules/session/SessionActionButton.qml rename to .config/quickshell/ii/modules/session/SessionActionButton.qml index becda60c..199f2abe 100644 --- a/.config/quickshell/modules/session/SessionActionButton.qml +++ b/.config/quickshell/ii/modules/session/SessionActionButton.qml @@ -1,11 +1,7 @@ -import "root:/modules/common" -import "root:/modules/common/widgets/" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Io RippleButton { id: button diff --git a/.config/quickshell/ii/modules/settings/About.qml b/.config/quickshell/ii/modules/settings/About.qml new file mode 100644 index 00000000..f9369c82 --- /dev/null +++ b/.config/quickshell/ii/modules/settings/About.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +ContentPage { + forceWidth: true + + ContentSection { + title: Translation.tr("Distro") + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + IconImage { + implicitSize: 80 + source: Quickshell.iconPath(SystemInfo.logo) + } + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + // spacing: 10 + StyledText { + text: SystemInfo.distroName + font.pixelSize: Appearance.font.pixelSize.title + } + StyledText { + font.pixelSize: Appearance.font.pixelSize.normal + text: SystemInfo.homeUrl + textFormat: Text.MarkdownText + onLinkActivated: (link) => { + Qt.openUrlExternally(link) + } + PointingHandLinkHover {} + } + } + } + + Flow { + Layout.fillWidth: true + spacing: 5 + + RippleButtonWithIcon { + materialIcon: "auto_stories" + mainText: Translation.tr("Documentation") + onClicked: { + Qt.openUrlExternally(SystemInfo.documentationUrl) + } + } + RippleButtonWithIcon { + materialIcon: "support" + mainText: Translation.tr("Help & Support") + onClicked: { + Qt.openUrlExternally(SystemInfo.supportUrl) + } + } + RippleButtonWithIcon { + materialIcon: "bug_report" + mainText: Translation.tr("Report a Bug") + onClicked: { + Qt.openUrlExternally(SystemInfo.bugReportUrl) + } + } + RippleButtonWithIcon { + materialIcon: "policy" + materialIconFill: false + mainText: Translation.tr("Privacy Policy") + onClicked: { + Qt.openUrlExternally(SystemInfo.privacyPolicyUrl) + } + } + + } + + } + ContentSection { + title: Translation.tr("Dotfiles") + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 20 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + IconImage { + implicitSize: 80 + source: Quickshell.iconPath("illogical-impulse") + } + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + // spacing: 10 + StyledText { + text: Translation.tr("illogical-impulse") + font.pixelSize: Appearance.font.pixelSize.title + } + StyledText { + text: "https://github.com/end-4/dots-hyprland" + font.pixelSize: Appearance.font.pixelSize.normal + textFormat: Text.MarkdownText + onLinkActivated: (link) => { + Qt.openUrlExternally(link) + } + PointingHandLinkHover {} + } + } + } + + Flow { + Layout.fillWidth: true + spacing: 5 + + RippleButtonWithIcon { + materialIcon: "auto_stories" + mainText: Translation.tr("Documentation") + onClicked: { + Qt.openUrlExternally("https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/02usage/") + } + } + RippleButtonWithIcon { + materialIcon: "adjust" + materialIconFill: false + mainText: Translation.tr("Issues") + onClicked: { + Qt.openUrlExternally("https://github.com/end-4/dots-hyprland/issues") + } + } + RippleButtonWithIcon { + materialIcon: "forum" + mainText: Translation.tr("Discussions") + onClicked: { + Qt.openUrlExternally("https://github.com/end-4/dots-hyprland/discussions") + } + } + RippleButtonWithIcon { + materialIcon: "favorite" + mainText: Translation.tr("Donate") + onClicked: { + Qt.openUrlExternally("https://github.com/sponsors/end-4") + } + } + + + } + } +} diff --git a/.config/quickshell/ii/modules/settings/AdvancedConfig.qml b/.config/quickshell/ii/modules/settings/AdvancedConfig.qml new file mode 100644 index 00000000..a40f1912 --- /dev/null +++ b/.config/quickshell/ii/modules/settings/AdvancedConfig.qml @@ -0,0 +1,45 @@ +import QtQuick +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +ContentPage { + forceWidth: true + + ContentSection { + title: Translation.tr("Color generation") + + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Shell & utilities") + checked: Config.options.appearance.wallpaperTheming.enableAppsAndShell + onCheckedChanged: { + Config.options.appearance.wallpaperTheming.enableAppsAndShell = checked; + } + } + ConfigSwitch { + text: Translation.tr("Qt apps") + checked: Config.options.appearance.wallpaperTheming.enableQtApps + onCheckedChanged: { + Config.options.appearance.wallpaperTheming.enableQtApps = checked; + } + StyledToolTip { + content: Translation.tr("Shell & utilities theming must also be enabled") + } + } + ConfigSwitch { + text: Translation.tr("Terminal") + checked: Config.options.appearance.wallpaperTheming.enableTerminal + onCheckedChanged: { + Config.options.appearance.wallpaperTheming.enableTerminal = checked; + } + StyledToolTip { + content: Translation.tr("Shell & utilities theming must also be enabled") + } + } + + } + } +} diff --git a/.config/quickshell/ii/modules/settings/InterfaceConfig.qml b/.config/quickshell/ii/modules/settings/InterfaceConfig.qml new file mode 100644 index 00000000..32633996 --- /dev/null +++ b/.config/quickshell/ii/modules/settings/InterfaceConfig.qml @@ -0,0 +1,374 @@ +import QtQuick +import QtQuick.Layouts +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +ContentPage { + forceWidth: true + ContentSection { + title: Translation.tr("Policies") + + ConfigRow { + ColumnLayout { + // Weeb policy + ContentSubsectionLabel { + text: Translation.tr("Weeb") + } + ConfigSelectionArray { + currentValue: Config.options.policies.weeb + configOptionName: "policies.weeb" + onSelected: newValue => { + Config.options.policies.weeb = newValue; + } + options: [ + { + displayName: Translation.tr("No"), + value: 0 + }, + { + displayName: Translation.tr("Yes"), + value: 1 + }, + { + displayName: Translation.tr("Closet"), + value: 2 + } + ] + } + } + + ColumnLayout { + // AI policy + ContentSubsectionLabel { + text: Translation.tr("AI") + } + ConfigSelectionArray { + currentValue: Config.options.policies.ai + configOptionName: "policies.ai" + onSelected: newValue => { + Config.options.policies.ai = newValue; + } + options: [ + { + displayName: Translation.tr("No"), + value: 0 + }, + { + displayName: Translation.tr("Yes"), + value: 1 + }, + { + displayName: Translation.tr("Local only"), + value: 2 + } + ] + } + } + } + } + + ContentSection { + title: Translation.tr("Bar") + + ConfigSelectionArray { + currentValue: Config.options.bar.cornerStyle + configOptionName: "bar.cornerStyle" + onSelected: newValue => { + Config.options.bar.cornerStyle = newValue; + } + options: [ + { + displayName: Translation.tr("Hug"), + value: 0 + }, + { + displayName: Translation.tr("Float"), + value: 1 + }, + { + displayName: Translation.tr("Plain rectangle"), + value: 2 + } + ] + } + + ContentSubsection { + title: Translation.tr("Appearance") + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr('Borderless') + checked: Config.options.bar.borderless + onCheckedChanged: { + Config.options.bar.borderless = checked; + } + } + ConfigSwitch { + text: Translation.tr('Show background') + checked: Config.options.bar.showBackground + onCheckedChanged: { + Config.options.bar.showBackground = checked; + } + StyledToolTip { + content: Translation.tr("Note: turning off can hurt readability") + } + } + } + } + + ContentSubsection { + title: Translation.tr("Buttons") + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Screen snip") + checked: Config.options.bar.utilButtons.showScreenSnip + onCheckedChanged: { + Config.options.bar.utilButtons.showScreenSnip = checked; + } + } + ConfigSwitch { + text: Translation.tr("Color picker") + checked: Config.options.bar.utilButtons.showColorPicker + onCheckedChanged: { + Config.options.bar.utilButtons.showColorPicker = checked; + } + } + } + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Mic toggle") + checked: Config.options.bar.utilButtons.showMicToggle + onCheckedChanged: { + Config.options.bar.utilButtons.showMicToggle = checked; + } + } + ConfigSwitch { + text: Translation.tr("Keyboard toggle") + checked: Config.options.bar.utilButtons.showKeyboardToggle + onCheckedChanged: { + Config.options.bar.utilButtons.showKeyboardToggle = checked; + } + } + } + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Dark/Light toggle") + checked: Config.options.bar.utilButtons.showDarkModeToggle + onCheckedChanged: { + Config.options.bar.utilButtons.showDarkModeToggle = checked; + } + } + ConfigSwitch { + opacity: 0 + enabled: false + } + } + } + + ContentSubsection { + title: Translation.tr("Workspaces") + tooltip: Translation.tr("Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience") + + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr('Show app icons') + checked: Config.options.bar.workspaces.showAppIcons + onCheckedChanged: { + Config.options.bar.workspaces.showAppIcons = checked; + } + } + ConfigSwitch { + text: Translation.tr('Always show numbers') + checked: Config.options.bar.workspaces.alwaysShowNumbers + onCheckedChanged: { + Config.options.bar.workspaces.alwaysShowNumbers = checked; + } + } + } + ConfigSpinBox { + text: Translation.tr("Workspaces shown") + value: Config.options.bar.workspaces.shown + from: 1 + to: 30 + stepSize: 1 + onValueChanged: { + Config.options.bar.workspaces.shown = value; + } + } + ConfigSpinBox { + text: Translation.tr("Number show delay when pressing Super (ms)") + value: Config.options.bar.workspaces.showNumberDelay + from: 0 + to: 1000 + stepSize: 50 + onValueChanged: { + Config.options.bar.workspaces.showNumberDelay = value; + } + } + } + + ContentSubsection { + title: Translation.tr("Weather") + ConfigSwitch { + text: Translation.tr("Enable") + checked: Config.options.bar.weather.enable + onCheckedChanged: { + Config.options.bar.weather.enable = checked; + } + } + } + } + + ContentSection { + title: Translation.tr("Battery") + + ConfigRow { + uniform: true + ConfigSpinBox { + text: Translation.tr("Low warning") + value: Config.options.battery.low + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.low = value; + } + } + ConfigSpinBox { + text: Translation.tr("Critical warning") + value: Config.options.battery.critical + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.critical = value; + } + } + } + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Automatic suspend") + checked: Config.options.battery.automaticSuspend + onCheckedChanged: { + Config.options.battery.automaticSuspend = checked; + } + StyledToolTip { + content: Translation.tr("Automatically suspends the system when battery is low") + } + } + ConfigSpinBox { + text: Translation.tr("Suspend at") + value: Config.options.battery.suspend + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.suspend = value; + } + } + } + } + + ContentSection { + title: Translation.tr("Dock") + + ConfigSwitch { + text: Translation.tr("Enable") + checked: Config.options.dock.enable + onCheckedChanged: { + Config.options.dock.enable = checked; + } + } + + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Hover to reveal") + checked: Config.options.dock.hoverToReveal + onCheckedChanged: { + Config.options.dock.hoverToReveal = checked; + } + } + ConfigSwitch { + text: Translation.tr("Pinned on startup") + checked: Config.options.dock.pinnedOnStartup + onCheckedChanged: { + Config.options.dock.pinnedOnStartup = checked; + } + } + } + } + + ContentSection { + title: Translation.tr("On-screen display") + ConfigSpinBox { + text: Translation.tr("Timeout (ms)") + value: Config.options.osd.timeout + from: 100 + to: 3000 + stepSize: 100 + onValueChanged: { + Config.options.osd.timeout = value; + } + } + } + + ContentSection { + title: Translation.tr("Overview") + ConfigSpinBox { + text: Translation.tr("Scale (%)") + value: Config.options.overview.scale * 100 + from: 1 + to: 100 + stepSize: 1 + onValueChanged: { + Config.options.overview.scale = value / 100; + } + } + ConfigRow { + uniform: true + ConfigSpinBox { + text: Translation.tr("Rows") + value: Config.options.overview.rows + from: 1 + to: 20 + stepSize: 1 + onValueChanged: { + Config.options.overview.rows = value; + } + } + ConfigSpinBox { + text: Translation.tr("Columns") + value: Config.options.overview.columns + from: 1 + to: 20 + stepSize: 1 + onValueChanged: { + Config.options.overview.columns = value; + } + } + } + } + + ContentSection { + title: Translation.tr("Screenshot tool") + + ConfigSwitch { + text: Translation.tr('Show regions of potential interest') + checked: Config.options.screenshotTool.showContentRegions + onCheckedChanged: { + Config.options.screenshotTool.showContentRegions = checked; + } + StyledToolTip { + content: Translation.tr("Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.") + } + } + } +} diff --git a/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/.config/quickshell/ii/modules/settings/ServicesConfig.qml new file mode 100644 index 00000000..02b3a3d2 --- /dev/null +++ b/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -0,0 +1,233 @@ +import QtQuick +import QtQuick.Layouts +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets + +ContentPage { + forceWidth: true + + ContentSection { + title: Translation.tr("Audio") + + ConfigSwitch { + text: Translation.tr("Earbang protection") + checked: Config.options.audio.protection.enable + onCheckedChanged: { + Config.options.audio.protection.enable = checked; + } + StyledToolTip { + content: Translation.tr("Prevents abrupt increments and restricts volume limit") + } + } + ConfigRow { + // uniform: true + ConfigSpinBox { + text: Translation.tr("Max allowed increase") + value: Config.options.audio.protection.maxAllowedIncrease + from: 0 + to: 100 + stepSize: 2 + onValueChanged: { + Config.options.audio.protection.maxAllowedIncrease = value; + } + } + ConfigSpinBox { + text: Translation.tr("Volume limit") + value: Config.options.audio.protection.maxAllowed + from: 0 + to: 100 + stepSize: 2 + onValueChanged: { + Config.options.audio.protection.maxAllowed = value; + } + } + } + } + ContentSection { + title: Translation.tr("AI") + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("System prompt") + text: Config.options.ai.systemPrompt + wrapMode: TextEdit.Wrap + onTextChanged: { + Qt.callLater(() => { + Config.options.ai.systemPrompt = text; + }); + } + } + } + + ContentSection { + title: Translation.tr("Battery") + + ConfigRow { + uniform: true + ConfigSpinBox { + text: Translation.tr("Low warning") + value: Config.options.battery.low + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.low = value; + } + } + ConfigSpinBox { + text: Translation.tr("Critical warning") + value: Config.options.battery.critical + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.critical = value; + } + } + } + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Automatic suspend") + checked: Config.options.battery.automaticSuspend + onCheckedChanged: { + Config.options.battery.automaticSuspend = checked; + } + StyledToolTip { + content: Translation.tr("Automatically suspends the system when battery is low") + } + } + ConfigSpinBox { + text: Translation.tr("Suspend at") + value: Config.options.battery.suspend + from: 0 + to: 100 + stepSize: 5 + onValueChanged: { + Config.options.battery.suspend = value; + } + } + } + } + + ContentSection { + title: Translation.tr("Networking") + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("User agent (for services that require it)") + text: Config.options.networking.userAgent + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.networking.userAgent = text; + } + } + } + + ContentSection { + title: Translation.tr("Resources") + ConfigSpinBox { + text: Translation.tr("Polling interval (ms)") + value: Config.options.resources.updateInterval + from: 100 + to: 10000 + stepSize: 100 + onValueChanged: { + Config.options.resources.updateInterval = value; + } + } + } + + ContentSection { + title: Translation.tr("Search") + + ConfigSwitch { + text: Translation.tr("Use Levenshtein distance-based algorithm instead of fuzzy") + checked: Config.options.search.sloppy + onCheckedChanged: { + Config.options.search.sloppy = checked; + } + StyledToolTip { + content: Translation.tr("Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)") + } + } + + ContentSubsection { + title: Translation.tr("Prefixes") + ConfigRow { + uniform: true + + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("Action") + text: Config.options.search.prefix.action + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.search.prefix.action = text; + } + } + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("Clipboard") + text: Config.options.search.prefix.clipboard + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.search.prefix.clipboard = text; + } + } + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("Emojis") + text: Config.options.search.prefix.emojis + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.search.prefix.emojis = text; + } + } + } + } + ContentSubsection { + title: Translation.tr("Web search") + MaterialTextField { + Layout.fillWidth: true + placeholderText: Translation.tr("Base URL") + text: Config.options.search.engineBaseUrl + wrapMode: TextEdit.Wrap + onTextChanged: { + Config.options.search.engineBaseUrl = text; + } + } + } + } + + ContentSection { + title: Translation.tr("Time") + + ContentSubsection { + title: Translation.tr("Format") + tooltip: "" + + ConfigSelectionArray { + currentValue: Config.options.time.format + configOptionName: "time.format" + onSelected: newValue => { + Config.options.time.format = newValue; + } + options: [ + { + displayName: Translation.tr("24h"), + value: "hh:mm" + }, + { + displayName: Translation.tr("12h am/pm"), + value: "h:mm ap" + }, + { + displayName: Translation.tr("12h AM/PM"), + value: "h:mm AP" + }, + ] + } + } + } +} diff --git a/.config/quickshell/ii/modules/settings/StyleConfig.qml b/.config/quickshell/ii/modules/settings/StyleConfig.qml new file mode 100644 index 00000000..6eabfabd --- /dev/null +++ b/.config/quickshell/ii/modules/settings/StyleConfig.qml @@ -0,0 +1,245 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +ContentPage { + baseWidth: lightDarkButtonGroup.implicitWidth + forceWidth: true + + Process { + id: konachanWallProc + property string status: "" + command: ["bash", "-c", FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/random_konachan_wall.sh`)] + stdout: SplitParser { + onRead: data => { + console.log(`Konachan wall proc output: ${data}`); + konachanWallProc.status = data.trim(); + } + } + } + + ContentSection { + title: Translation.tr("Colors & Wallpaper") + + // Light/Dark mode preference + ButtonGroup { + id: lightDarkButtonGroup + Layout.fillWidth: true + LightDarkPreferenceButton { + dark: false + } + LightDarkPreferenceButton { + dark: true + } + } + + // Material palette selection + ContentSubsection { + title: Translation.tr("Material palette") + ConfigSelectionArray { + currentValue: Config.options.appearance.palette.type + configOptionName: "appearance.palette.type" + onSelected: (newValue) => { + Config.options.appearance.palette.type = newValue; + Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --noswitch`]) + } + options: [ + {"value": "auto", "displayName": Translation.tr("Auto")}, + {"value": "scheme-content", "displayName": Translation.tr("Content")}, + {"value": "scheme-expressive", "displayName": Translation.tr("Expressive")}, + {"value": "scheme-fidelity", "displayName": Translation.tr("Fidelity")}, + {"value": "scheme-fruit-salad", "displayName": Translation.tr("Fruit Salad")}, + {"value": "scheme-monochrome", "displayName": Translation.tr("Monochrome")}, + {"value": "scheme-neutral", "displayName": Translation.tr("Neutral")}, + {"value": "scheme-rainbow", "displayName": Translation.tr("Rainbow")}, + {"value": "scheme-tonal-spot", "displayName": Translation.tr("Tonal Spot")} + ] + } + } + + + // Wallpaper selection + ContentSubsection { + title: Translation.tr("Wallpaper") + RowLayout { + Layout.alignment: Qt.AlignHCenter + RippleButtonWithIcon { + id: rndWallBtn + buttonRadius: Appearance.rounding.small + materialIcon: "wallpaper" + mainText: konachanWallProc.running ? Translation.tr("Be patient...") : Translation.tr("Random: Konachan") + onClicked: { + console.log(konachanWallProc.command.join(" ")) + konachanWallProc.running = true; + } + StyledToolTip { + content: Translation.tr("Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers") + } + } + RippleButtonWithIcon { + materialIcon: "wallpaper" + StyledToolTip { + content: Translation.tr("Pick wallpaper image on your system") + } + onClicked: { + Quickshell.execDetached(`${Directories.wallpaperSwitchScriptPath}`) + } + mainContentComponent: Component { + RowLayout { + spacing: 10 + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + text: Translation.tr("Choose file") + color: Appearance.colors.colOnSecondaryContainer + } + RowLayout { + spacing: 3 + KeyboardKey { + key: "Ctrl" + } + KeyboardKey { + key: "󰖳" + } + StyledText { + Layout.alignment: Qt.AlignVCenter + text: "+" + } + KeyboardKey { + key: "T" + } + } + } + } + } + } + } + + StyledText { + Layout.topMargin: 5 + Layout.alignment: Qt.AlignHCenter + text: Translation.tr("Alternatively use /dark, /light, /img in the launcher") + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colSubtext + } + + } + + ContentSection { + title: Translation.tr("Decorations & Effects") + + ContentSubsection { + title: Translation.tr("Transparency") + + ConfigRow { + ConfigSwitch { + text: Translation.tr("Enable") + checked: Config.options.appearance.transparency + onCheckedChanged: { + Config.options.appearance.transparency = checked; + } + StyledToolTip { + content: Translation.tr("Might look ass. Unsupported.") + } + } + } + } + + ContentSubsection { + title: Translation.tr("Fake screen rounding") + + ButtonGroup { + id: fakeScreenRoundingButtonGroup + property int selectedPolicy: Config.options.appearance.fakeScreenRounding + spacing: 2 + SelectionGroupButton { + property int value: 0 + leftmost: true + buttonText: Translation.tr("No") + toggled: (fakeScreenRoundingButtonGroup.selectedPolicy === value) + onClicked: { + Config.options.appearance.fakeScreenRounding = value; + } + } + SelectionGroupButton { + property int value: 1 + buttonText: Translation.tr("Yes") + toggled: (fakeScreenRoundingButtonGroup.selectedPolicy === value) + onClicked: { + Config.options.appearance.fakeScreenRounding = value; + } + } + SelectionGroupButton { + property int value: 2 + rightmost: true + buttonText: Translation.tr("When not fullscreen") + toggled: (fakeScreenRoundingButtonGroup.selectedPolicy === value) + onClicked: { + Config.options.appearance.fakeScreenRounding = value; + } + } + } + } + + ContentSubsection { + title: Translation.tr("Shell windows") + + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Title bar") + checked: Config.options.windows.showTitlebar + onCheckedChanged: { + Config.options.windows.showTitlebar = checked; + } + } + ConfigSwitch { + text: Translation.tr("Center title") + checked: Config.options.windows.centerTitle + onCheckedChanged: { + Config.options.windows.centerTitle = checked; + } + } + } + } + + ContentSubsection { + title: Translation.tr("Wallpaper parallax") + + ConfigRow { + uniform: true + ConfigSwitch { + text: Translation.tr("Depends on workspace") + checked: Config.options.background.parallax.enableWorkspace + onCheckedChanged: { + Config.options.background.parallax.enableWorkspace = checked; + } + } + ConfigSwitch { + text: Translation.tr("Depends on sidebars") + checked: Config.options.background.parallax.enableSidebar + onCheckedChanged: { + Config.options.background.parallax.enableSidebar = checked; + } + } + } + ConfigSpinBox { + text: Translation.tr("Preferred wallpaper zoom (%)") + value: Config.options.background.parallax.workspaceZoom * 100 + from: 100 + to: 150 + stepSize: 1 + onValueChanged: { + console.log(value/100) + Config.options.background.parallax.workspaceZoom = value / 100; + } + } + } + } +} \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarLeft/AiChat.qml b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml similarity index 74% rename from .config/quickshell/modules/sidebarLeft/AiChat.qml rename to .config/quickshell/ii/modules/sidebarLeft/AiChat.qml index 422dc41b..221eba9b 100644 --- a/.config/quickshell/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/AiChat.qml @@ -1,18 +1,15 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import "./aiChat/" import "root:/modules/common/functions/fuzzysort.js" as Fuzzy -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Hyprland Item { id: root @@ -44,21 +41,25 @@ Item { property var allCommands: [ { name: "model", - description: qsTr("Choose model"), + description: Translation.tr("Choose model"), execute: (args) => { Ai.setModel(args[0]); } }, { - name: "clear", - description: qsTr("Clear chat history"), - execute: () => { - Ai.clearMessages(); + name: "prompt", + description: Translation.tr("Set the system prompt for the model."), + execute: (args) => { + if (args.length === 0 || args[0] === "get") { + Ai.printPrompt(); + return; + } + Ai.loadPrompt(args.join(" ").trim()); } }, { name: "key", - description: qsTr("Set API key"), + description: Translation.tr("Set API key"), execute: (args) => { if (args[0] == "get") { Ai.printApiKey() @@ -67,9 +68,40 @@ Item { } } }, + { + name: "save", + description: Translation.tr("Save chat"), + execute: (args) => { + const joinedArgs = args.join(" ") + if (joinedArgs.trim().length == 0) { + Ai.addMessage(`Usage: ${root.commandPrefix}save CHAT_NAME`, Ai.interfaceRole); + return; + } + Ai.saveChat(joinedArgs) + } + }, + { + name: "load", + description: Translation.tr("Load chat"), + execute: (args) => { + const joinedArgs = args.join(" ") + if (joinedArgs.trim().length == 0) { + Ai.addMessage(`Usage: ${root.commandPrefix}load CHAT_NAME`, Ai.interfaceRole); + return; + } + Ai.loadChat(joinedArgs) + } + }, + { + name: "clear", + description: Translation.tr("Clear chat history"), + execute: () => { + Ai.clearMessages(); + } + }, { name: "temp", - description: qsTr("Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5."), + description: Translation.tr("Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5."), execute: (args) => { // console.log(args) if (args.length == 0 || args[0] == "get") { @@ -82,7 +114,7 @@ Item { }, { name: "test", - description: qsTr("Markdown test"), + description: Translation.tr("Markdown test"), execute: () => { Ai.addMessage(` @@ -94,7 +126,7 @@ Mowe uwu wem ipsum! ### Formatting - *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com) -- Arch lincox icon +- Arch lincox icon ### Table @@ -148,7 +180,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) if (commandObj) { commandObj.execute(args); } else { - Ai.addMessage(qsTr("Unknown command: ") + command, Ai.interfaceRole); + Ai.addMessage(Translation.tr("Unknown command: ") + command, Ai.interfaceRole); } } else { @@ -235,7 +267,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) font.family: Appearance.font.family.title color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignHCenter - text: qsTr("Large language models") + text: Translation.tr("Large language models") } StyledText { id: widgetDescriptionText @@ -244,39 +276,15 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignLeft wrapMode: Text.Wrap - text: qsTr("Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window") + text: Translation.tr("Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window") } } } } - Item { // Suggestion description - visible: descriptionText.text.length > 0 - Layout.fillWidth: true - implicitHeight: descriptionBackground.implicitHeight - - Rectangle { - id: descriptionBackground - color: Appearance.colors.colTooltip - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: descriptionText.implicitHeight + 5 * 2 - radius: Appearance.rounding.verysmall - - StyledText { - id: descriptionText - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 10 - anchors.rightMargin: 10 - anchors.verticalCenter: parent.verticalCenter - font.pixelSize: Appearance.font.pixelSize.smaller - color: Appearance.colors.colOnTooltip - wrapMode: Text.Wrap - text: root.suggestionList[suggestions.selectedIndex]?.description ?? "" - } - } + DescriptionBox { + text: root.suggestionList[suggestions.selectedIndex]?.description ?? "" + showArrows: root.suggestionList.length > 1 } FlowButtonGroup { // Suggestions @@ -294,7 +302,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) } delegate: ApiCommandButton { id: commandButton - colBackground: suggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 + colBackground: suggestions.selectedIndex === index ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer bounce: false contentItem: StyledText { font.pixelSize: Appearance.font.pixelSize.small @@ -366,16 +374,16 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) Layout.fillWidth: true padding: 10 color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant - placeholderText: StringUtils.format(qsTr('Message the model... "{0}" for commands'), root.commandPrefix) + placeholderText: Translation.tr('Message the model... "%1" for commands').arg(root.commandPrefix) background: null onTextChanged: { // Handle suggestions - if(messageInputField.text.length === 0) { + if (messageInputField.text.length === 0) { root.suggestionQuery = "" root.suggestionList = [] return - } else if(messageInputField.text.startsWith(`${root.commandPrefix}model`)) { + } else if (messageInputField.text.startsWith(`${root.commandPrefix}model`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => { return { @@ -393,6 +401,62 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) description: `${Ai.models[model.target].description}`, } }) + } else if (messageInputField.text.startsWith(`${root.commandPrefix}prompt`)) { + root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" + const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.promptFiles.map(file => { + return { + name: Fuzzy.prepare(file), + obj: file, + } + }), { + all: true, + key: "name" + }) + root.suggestionList = promptFileResults.map(file => { + return { + name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "prompt ") : ""}${file.target}`, + displayName: `${FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target))}`, + description: Translation.tr("Load prompt from %1").arg(file.target), + } + }) + } else if (messageInputField.text.startsWith(`${root.commandPrefix}save`)) { + root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" + const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { + return { + name: Fuzzy.prepare(file), + obj: file, + } + }), { + all: true, + key: "name" + }) + root.suggestionList = promptFileResults.map(file => { + const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() + return { + name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`, + displayName: `${chatName}`, + description: Translation.tr("Save chat to %1").arg(chatName), + } + }) + } else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) { + root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" + const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { + return { + name: Fuzzy.prepare(file), + obj: file, + } + }), { + all: true, + key: "name" + }) + root.suggestionList = promptFileResults.map(file => { + const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() + return { + name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "load ") : ""}${chatName}`, + displayName: `${chatName}`, + description: Translation.tr(`Load chat from %1`).arg(file.target), + } + }) } else if(messageInputField.text.startsWith(root.commandPrefix)) { root.suggestionQuery = messageInputField.text root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => { @@ -510,8 +574,9 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) id: toolTip extraVisibleCondition: false alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered - content: StringUtils.format(qsTr("Current model: {0}\nSet it with {1}model MODEL"), - Ai.getModel().name, root.commandPrefix) + content: Translation.tr("Current model: %1\nSet it with %2model MODEL") + .arg(Ai.getModel().name) + .arg(root.commandPrefix) } MouseArea { diff --git a/.config/quickshell/modules/sidebarLeft/Anime.qml b/.config/quickshell/ii/modules/sidebarLeft/Anime.qml similarity index 87% rename from .config/quickshell/modules/sidebarLeft/Anime.qml rename to .config/quickshell/ii/modules/sidebarLeft/Anime.qml index 1300d548..266a8432 100644 --- a/.config/quickshell/modules/sidebarLeft/Anime.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/Anime.qml @@ -1,19 +1,15 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import "root:/modules/common/functions/fuzzysort.js" as Fuzzy -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils import "./anime/" -import Qt.labs.platform import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects -import Quickshell.Io import Quickshell -import Quickshell.Hyprland Item { id: root @@ -39,21 +35,21 @@ Item { property var allCommands: [ { name: "mode", - description: qsTr("Set the current API provider"), + description: Translation.tr("Set the current API provider"), execute: (args) => { Booru.setProvider(args[0]); } }, { name: "clear", - description: qsTr("Clear the current list of images"), + description: Translation.tr("Clear the current list of images"), execute: () => { Booru.clearResponses(); } }, { name: "next", - description: qsTr("Get the next page of results"), + description: Translation.tr("Get the next page of results"), execute: () => { if (root.responses.length > 0) { const lastResponse = root.responses[root.responses.length - 1]; @@ -63,16 +59,16 @@ Item { }, { name: "safe", - description: qsTr("Disable NSFW content"), + description: Translation.tr("Disable NSFW content"), execute: () => { - PersistentStateManager.setState("booru.allowNsfw", false); + Persistent.states.booru.allowNsfw = false; } }, { name: "lewd", - description: qsTr("Allow NSFW content"), + description: Translation.tr("Allow NSFW content"), execute: () => { - PersistentStateManager.setState("booru.allowNsfw", true); + Persistent.states.booru.allowNsfw = true; } }, ] @@ -86,7 +82,7 @@ Item { if (commandObj) { commandObj.execute(args); } else { - Booru.addSystemMessage(qsTr("Unknown command: ") + command); + Booru.addSystemMessage(Translation.tr("Unknown command: ") + command); } } else if (inputText.trim() == "+") { @@ -106,7 +102,7 @@ Item { break; } } - Booru.makeRequest(tagList, PersistentStates.booru.allowNsfw, ConfigOptions.sidebar.booru.limit, pageIndex); + Booru.makeRequest(tagList, Persistent.states.booru.allowNsfw, Config.options.sidebar.booru.limit, pageIndex); } } @@ -208,7 +204,7 @@ Item { font.family: Appearance.font.family.title color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignHCenter - text: qsTr("Anime boorus") + text: Translation.tr("Anime boorus") } } } @@ -245,39 +241,15 @@ Item { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.m3colors.m3inverseOnSurface wrapMode: Text.Wrap - text: StringUtils.format(qsTr("{0} queries pending"), Booru.runningRequests) + text: Translation.tr("%1 queries pending").arg(Booru.runningRequests) } } } } - Item { // Tag suggestion description - visible: tagDescriptionText.text.length > 0 - Layout.fillWidth: true - implicitHeight: tagDescriptionBackground.implicitHeight - - Rectangle { - id: tagDescriptionBackground - color: Appearance.colors.colTooltip - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: tagDescriptionText.implicitHeight + 5 * 2 - radius: Appearance.rounding.verysmall - - StyledText { - id: tagDescriptionText - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 10 - anchors.rightMargin: 10 - anchors.verticalCenter: parent.verticalCenter - font.pixelSize: Appearance.font.pixelSize.smaller - color: Appearance.colors.colOnTooltip - wrapMode: Text.Wrap - text: root.suggestionList[tagSuggestions.selectedIndex]?.description ?? "" - } - } + DescriptionBox { // Tag suggestion description + text: root.suggestionList[tagSuggestions.selectedIndex]?.description ?? "" + showArrows: root.suggestionList.length > 1 } FlowButtonGroup { // Tag suggestions @@ -295,7 +267,7 @@ Item { } delegate: ApiCommandButton { id: tagButton - colBackground: tagSuggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 + colBackground: tagSuggestions.selectedIndex === index ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer bounce: false contentItem: RowLayout { anchors.centerIn: parent @@ -303,7 +275,7 @@ Item { StyledText { Layout.fillWidth: false font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.m3colors.m3onSurface + color: Appearance.colors.colOnSecondaryContainer horizontalAlignment: Text.AlignRight text: modelData.displayName ?? modelData.name } @@ -311,7 +283,7 @@ Item { Layout.fillWidth: false visible: modelData.count !== undefined font.pixelSize: Appearance.font.pixelSize.smaller - color: Appearance.m3colors.m3outline + color: Appearance.colors.colOnSecondaryContainer horizontalAlignment: Text.AlignLeft text: modelData.count ?? "" } @@ -381,7 +353,7 @@ Item { padding: 10 color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant renderType: Text.NativeRendering - placeholderText: StringUtils.format(qsTr('Enter tags, or "{0}" for commands'), root.commandPrefix) + placeholderText: Translation.tr('Enter tags, or "%1" for commands').arg(root.commandPrefix) background: null @@ -544,9 +516,10 @@ Item { id: toolTip extraVisibleCondition: false alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered - // content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER") - content: StringUtils.format(qsTr("Current API endpoint: {0}\nSet it with {1}mode PROVIDER"), - Booru.providers[Booru.currentProvider].url, root.commandPrefix) + // content: Translation.tr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + Translation.tr("\nSet with /mode PROVIDER") + content: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER") + .arg(Booru.providers[Booru.currentProvider].url) + .arg(root.commandPrefix) } MouseArea { @@ -586,17 +559,17 @@ Item { Layout.alignment: Qt.AlignVCenter font.pixelSize: Appearance.font.pixelSize.smaller color: nsfwSwitch.enabled ? Appearance.colors.colOnLayer1 : Appearance.m3colors.m3outline - text: qsTr("Allow NSFW") + text: Translation.tr("Allow NSFW") } StyledSwitch { id: nsfwSwitch enabled: Booru.currentProvider !== "zerochan" scale: 0.6 Layout.alignment: Qt.AlignVCenter - checked: (PersistentStates.booru.allowNsfw && Booru.currentProvider !== "zerochan") + checked: (Persistent.states.booru.allowNsfw && Booru.currentProvider !== "zerochan") onCheckedChanged: { if (!nsfwSwitch.enabled) return; - PersistentStateManager.setState("booru.allowNsfw", checked) + Persistent.states.booru.allowNsfw = checked; } } } @@ -610,7 +583,6 @@ Item { id: commandRepeater model: commandButtonsRow.commandsShown delegate: ApiCommandButton { - id: tagButton property string commandRepresentation: `${root.commandPrefix}${modelData.name}` buttonText: commandRepresentation colBackground: Appearance.colors.colLayer2 diff --git a/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml b/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml similarity index 73% rename from .config/quickshell/modules/sidebarLeft/ApiCommandButton.qml rename to .config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml index 8741cb6b..7ee39846 100644 --- a/.config/quickshell/modules/sidebarLeft/ApiCommandButton.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/ApiCommandButton.qml @@ -1,10 +1,7 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell GroupButton { id: button @@ -16,7 +13,7 @@ GroupButton { baseWidth: contentItem.implicitWidth + horizontalPadding * 2 clickedWidth: baseWidth + 20 baseHeight: contentItem.implicitHeight + verticalPadding * 2 - buttonRadius: down ? Appearance.rounding.small : baseHeight / 2 + buttonRadius: down ? Appearance.rounding.verysmall : Appearance.rounding.small colBackground: Appearance.colors.colLayer2 colBackgroundHover: Appearance.colors.colLayer2Hover diff --git a/.config/quickshell/ii/modules/sidebarLeft/DescriptionBox.qml b/.config/quickshell/ii/modules/sidebarLeft/DescriptionBox.qml new file mode 100644 index 00000000..5287ecc4 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarLeft/DescriptionBox.qml @@ -0,0 +1,62 @@ +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import QtQuick.Layouts + +Item { // Tag suggestion description + id: root + property alias text: tagDescriptionText.text + property bool showArrows: true + property bool showTab: true + + visible: tagDescriptionText.text.length > 0 + Layout.fillWidth: true + implicitHeight: tagDescriptionBackground.implicitHeight + + Rectangle { + id: tagDescriptionBackground + color: Appearance.colors.colLayer2 + anchors.fill: parent + radius: Appearance.rounding.verysmall + implicitHeight: descriptionRow.implicitHeight + 5 * 2 + + RowLayout { + id: descriptionRow + spacing: 4 + anchors { + fill: parent + leftMargin: 10 + rightMargin: 10 + } + + StyledText { + id: tagDescriptionText + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colOnLayer2 + wrapMode: Text.Wrap + } + KeyboardKey { + visible: root.showArrows + key: "↑" + } + KeyboardKey { + visible: root.showArrows + key: "↓" + } + StyledText { + visible: root.showArrows && root.showTab + text: Translation.tr("or") + font.pixelSize: Appearance.font.pixelSize.smaller + } + KeyboardKey { + id: tagDescriptionKey + visible: root.showTab + key: "Tab" + Layout.alignment: Qt.AlignVCenter + } + } + } +} \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml similarity index 92% rename from .config/quickshell/modules/sidebarLeft/SidebarLeft.qml rename to .config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml index ce0b2280..e5a661eb 100644 --- a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeft.qml @@ -1,15 +1,10 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Effects -import Qt5Compat.GraphicalEffects import Quickshell.Io import Quickshell -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland @@ -100,6 +95,8 @@ Scope { // Scope width: sidebarRoot.sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin height: parent.height - Appearance.sizes.hyprlandGapsOut * 2 color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 Behavior on width { @@ -168,7 +165,7 @@ Scope { // Scope GlobalShortcut { name: "sidebarLeftToggle" - description: qsTr("Toggles left sidebar on press") + description: "Toggles left sidebar on press" onPressed: { GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen; @@ -177,7 +174,7 @@ Scope { // Scope GlobalShortcut { name: "sidebarLeftOpen" - description: qsTr("Opens left sidebar on press") + description: "Opens left sidebar on press" onPressed: { GlobalStates.sidebarLeftOpen = true; @@ -186,7 +183,7 @@ Scope { // Scope GlobalShortcut { name: "sidebarLeftClose" - description: qsTr("Closes left sidebar on press") + description: "Closes left sidebar on press" onPressed: { GlobalStates.sidebarLeftOpen = false; @@ -195,7 +192,7 @@ Scope { // Scope GlobalShortcut { name: "sidebarLeftToggleDetach" - description: qsTr("Detach left sidebar into a window/Attach it back") + description: "Detach left sidebar into a window/Attach it back" onPressed: { root.detach = !root.detach; diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml similarity index 79% rename from .config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml rename to .config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml index 8d67c2a7..fc60618e 100644 --- a/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/SidebarLeftContent.qml @@ -1,26 +1,21 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.Effects import Qt5Compat.GraphicalEffects -import Quickshell.Io -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland Item { id: root required property var scopeRoot anchors.fill: parent property var tabButtonList: [ - ...(ConfigOptions.policies.ai !== 0 ? [{"icon": "neurology", "name": qsTr("Intelligence")}] : []), - {"icon": "translate", "name": qsTr("Translator")}, - ...(ConfigOptions.policies.weeb === 1 ? [{"icon": "bookmark_heart", "name": qsTr("Anime")}] : []) + ...(Config.options.policies.ai !== 0 ? [{"icon": "neurology", "name": Translation.tr("Intelligence")}] : []), + {"icon": "translate", "name": Translation.tr("Translator")}, + ...(Config.options.policies.weeb === 1 ? [{"icon": "bookmark_heart", "name": Translation.tr("Anime")}] : []) ] property int selectedTab: 0 @@ -88,9 +83,9 @@ Item { } contentChildren: [ - ...(ConfigOptions.policies.ai !== 0 ? [aiChat.createObject()] : []), + ...(Config.options.policies.ai !== 0 ? [aiChat.createObject()] : []), translator.createObject(), - ...(ConfigOptions.policies.weeb === 0 ? [] : [anime.createObject()]) + ...(Config.options.policies.weeb === 0 ? [] : [anime.createObject()]) ] } diff --git a/.config/quickshell/modules/sidebarLeft/Translator.qml b/.config/quickshell/ii/modules/sidebarLeft/Translator.qml similarity index 89% rename from .config/quickshell/modules/sidebarLeft/Translator.qml rename to .config/quickshell/ii/modules/sidebarLeft/Translator.qml index 37e5a37b..e9e3d6fe 100644 --- a/.config/quickshell/modules/sidebarLeft/Translator.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/Translator.qml @@ -1,15 +1,13 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import "./translator/" import QtQuick -import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io -import Quickshell.Hyprland /** * Translator widget with the `trans` commandline tool. @@ -23,8 +21,8 @@ Item { property string translatedText: "" property list languages: [] // Options - property string targetLanguage: ConfigOptions.language.translator.targetLanguage - property string sourceLanguage: ConfigOptions.language.translator.sourceLanguage + property string targetLanguage: Config.options.language.translator.targetLanguage + property string sourceLanguage: Config.options.language.translator.sourceLanguage property string hostLanguage: targetLanguage property bool showLanguageSelector: false @@ -43,7 +41,7 @@ Item { Timer { id: translateTimer - interval: ConfigOptions.sidebar.translator.delay + interval: Config.options.sidebar.translator.delay repeat: false onTriggered: () => { if (root.inputField.text.trim().length > 0) { @@ -123,7 +121,7 @@ Item { TextCanvas { // Content translation id: outputCanvas isInput: false - placeholderText: qsTr("Translation goes here...") + placeholderText: Translation.tr("Translation goes here...") property bool hasTranslation: (root.translatedText.trim().length > 0) text: hasTranslation ? root.translatedText : "" GroupButton { @@ -155,8 +153,8 @@ Item { color: searchButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext } onClicked: { - let url = ConfigOptions.search.engineBaseUrl + outputCanvas.displayedText; - for (let site of ConfigOptions.search.excludedSites) { + let url = Config.options.search.engineBaseUrl + outputCanvas.displayedText; + for (let site of Config.options.search.excludedSites) { url += ` -site:${site}`; } Qt.openUrlExternally(url); @@ -178,7 +176,7 @@ Item { TextCanvas { // Content input id: inputCanvas isInput: true - placeholderText: qsTr("Enter text to translate...") + placeholderText: Translation.tr("Enter text to translate...") onInputTextChanged: { translateTimer.restart(); } @@ -223,7 +221,7 @@ Item { z: 9999 sourceComponent: SelectionDialog { id: languageSelectorDialog - titleText: qsTr("Select Language") + titleText: Translation.tr("Select Language") items: root.languages defaultChoice: root.languageSelectorTarget ? root.targetLanguage : root.sourceLanguage onCanceled: () => { @@ -235,10 +233,10 @@ Item { if (root.languageSelectorTarget) { root.targetLanguage = result; - ConfigLoader.setConfigValueAndSave("language.translator.targetLanguage", result); // Save to config + Config.options.language.translator.targetLanguage = result; // Save to config } else { root.sourceLanguage = result; - ConfigLoader.setConfigValueAndSave("language.translator.sourceLanguage", result); // Save to config + Config.options.language.translator.sourceLanguage = result; // Save to config } translateTimer.restart(); // Restart translation after language change diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessage.qml similarity index 89% rename from .config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessage.qml index 8d795211..872c38ef 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessage.qml @@ -1,19 +1,13 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "../" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Io import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland -import Qt5Compat.GraphicalEffects -import org.kde.syntaxhighlighting Rectangle { id: root @@ -121,11 +115,8 @@ Rectangle { height: Appearance.font.pixelSize.large source: messageData?.role == 'assistant' ? Ai.models[messageData?.model].icon : messageData?.role == 'user' ? 'linux-symbolic' : 'desktop-symbolic' - } - ColorOverlay { - visible: modelIcon.visible - anchors.fill: modelIcon - source: modelIcon + + colorize: true color: Appearance.m3colors.m3onSecondaryContainer } @@ -151,7 +142,7 @@ Rectangle { color: Appearance.m3colors.m3onSecondaryContainer text: messageData?.role == 'assistant' ? Ai.models[messageData?.model].name : (messageData?.role == 'user' && SystemInfo.username) ? SystemInfo.username : - qsTr("Interface") + Translation.tr("Interface") } } } @@ -173,7 +164,7 @@ Rectangle { text: "visibility_off" } StyledToolTip { - content: qsTr("Not visible to model") + content: Translation.tr("Not visible to model") } } @@ -185,7 +176,7 @@ Rectangle { buttonIcon: activated ? "inventory" : "content_copy" onClicked: { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.messageData?.content)}'`) + Quickshell.clipboardText = root.messageData?.content copyButton.activated = true copyIconTimer.restart() } @@ -200,7 +191,7 @@ Rectangle { } StyledToolTip { - content: qsTr("Copy") + content: Translation.tr("Copy") } } AiMessageControlButton { @@ -215,7 +206,7 @@ Rectangle { } } StyledToolTip { - content: root.editing ? qsTr("Save") : qsTr("Edit") + content: root.editing ? Translation.tr("Save") : Translation.tr("Edit") } } AiMessageControlButton { @@ -226,7 +217,7 @@ Rectangle { root.renderMarkdown = !root.renderMarkdown } StyledToolTip { - content: qsTr("View Markdown source") + content: Translation.tr("View Markdown source") } } AiMessageControlButton { @@ -236,7 +227,7 @@ Rectangle { Ai.removeMessage(root.messageIndex) } StyledToolTip { - content: qsTr("Delete") + content: Translation.tr("Delete") } } } @@ -247,9 +238,7 @@ Rectangle { spacing: 0 Repeater { - model: ScriptModel { - values: root.messageBlocks.map((block, index) => index) - } + model: root.messageBlocks.length delegate: Loader { required property int index property var thisBlock: root.messageBlocks[index] diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessageControlButton.qml similarity index 73% rename from .config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessageControlButton.qml index 2d7cb82d..64fc7723 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessageControlButton.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AiMessageControlButton.qml @@ -1,11 +1,7 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell GroupButton { id: button diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml similarity index 82% rename from .config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml index a253a291..83288fda 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml @@ -1,14 +1,9 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import Qt5Compat.GraphicalEffects -import Qt.labs.platform +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell.Widgets import Quickshell.Hyprland RippleButton { diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml similarity index 91% rename from .config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml index ea7bb0ee..af2dc240 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/MessageCodeBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageCodeBlock.qml @@ -1,20 +1,14 @@ pragma ComponentBehavior: Bound -import "root:/" -import "root:/services" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "../" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io import Quickshell -import Quickshell.Widgets -import Quickshell.Hyprland -import Qt5Compat.GraphicalEffects import org.kde.syntaxhighlighting ColumnLayout { @@ -73,7 +67,7 @@ ColumnLayout { buttonIcon: activated ? "inventory" : "content_copy" onClicked: { - Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segmentContent)}'`) + Quickshell.clipboardText = segmentContent copyCodeButton.activated = true copyIconTimer.restart() } @@ -87,7 +81,7 @@ ColumnLayout { } } StyledToolTip { - content: qsTr("Copy code") + content: Translation.tr("Copy code") } } AiMessageControlButton { @@ -96,8 +90,14 @@ ColumnLayout { onClicked: { const downloadPath = FileUtils.trimFileProtocol(Directories.downloads) - Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(segmentContent)}' > '${downloadPath}/code.${segmentLang || "txt"}'`) - Hyprland.dispatch(`exec notify-send 'Code saved to file' '${downloadPath}/code.${segmentLang || "txt"}' -a Shell`) + Quickshell.execDetached(["bash", "-c", + `echo '${StringUtils.shellSingleQuoteEscape(segmentContent)}' > '${downloadPath}/code.${segmentLang || "txt"}'` + ]) + Quickshell.execDetached(["notify-send", + Translation.tr("Code saved to file"), + Translation.tr("Saved to %1").arg(`${downloadPath}/code.${segmentLang || "txt"}`), + "-a", "Shell" + ]) saveCodeButton.activated = true saveIconTimer.restart() } @@ -111,7 +111,7 @@ ColumnLayout { } } StyledToolTip { - content: qsTr("Save to Downloads") + content: Translation.tr("Save to Downloads") } } } diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml similarity index 93% rename from .config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml index faa6c559..4c334500 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/MessageTextBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageTextBlock.qml @@ -1,19 +1,14 @@ pragma ComponentBehavior: Bound -import "root:/" -import "root:/services" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "../" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell -import Quickshell.Widgets import Quickshell.Hyprland -import Qt5Compat.GraphicalEffects ColumnLayout { id: root @@ -124,7 +119,7 @@ ColumnLayout { wrapMode: TextEdit.Wrap color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText - text: qsTr("Waiting for response...") + text: Translation.tr("Waiting for response...") onTextChanged: { if (!root.editing) return diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml similarity index 92% rename from .config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml rename to .config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml index 1ae941b7..9d055592 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/MessageThinkBlock.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/aiChat/MessageThinkBlock.qml @@ -1,19 +1,12 @@ pragma ComponentBehavior: Bound -import "root:/" -import "root:/services" -import "root:/modules/common/" -import "root:/modules/common/widgets" -import "../" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io -import Quickshell -import Quickshell.Widgets -import Quickshell.Hyprland import Qt5Compat.GraphicalEffects Item { @@ -99,7 +92,7 @@ Item { id: thinkBlockLanguage Layout.fillWidth: false Layout.alignment: Qt.AlignLeft - text: root.completed ? qsTr("Chain of Thought") : (qsTr("Thinking") + ".".repeat(Math.random() * 4)) + text: root.completed ? Translation.tr("Chain of Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4)) } Item { Layout.fillWidth: true } RippleButton { // Expand button @@ -147,7 +140,7 @@ Item { enabled: root.completed ?? false NumberAnimation { duration: collapseAnimation.duration - easing.type: collapseAnimation.easing + easing.type: collapseAnimation.type easing.bezierCurve: collapseAnimation.bezierCurve } } diff --git a/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruImage.qml similarity index 89% rename from .config/quickshell/modules/sidebarLeft/anime/BooruImage.qml rename to .config/quickshell/ii/modules/sidebarLeft/anime/BooruImage.qml index 4e115bc7..cf412f27 100644 --- a/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruImage.qml @@ -1,14 +1,11 @@ -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQml -import Qt.labs.platform import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.Effects import Qt5Compat.GraphicalEffects import Quickshell import Quickshell.Io @@ -153,7 +150,7 @@ Button { MenuButton { id: openFileLinkButton Layout.fillWidth: true - buttonText: qsTr("Open file link") + buttonText: Translation.tr("Open file link") onClicked: { root.showActions = false Hyprland.dispatch("keyword cursor:no_warps true") @@ -165,7 +162,7 @@ Button { id: sourceButton visible: root.imageData.source && root.imageData.source.length > 0 Layout.fillWidth: true - buttonText: StringUtils.format(qsTr("Go to source ({0})"), StringUtils.getDomain(root.imageData.source)) + buttonText: Translation.tr("Go to source (%1)").arg(StringUtils.getDomain(root.imageData.source)) enabled: root.imageData.source && root.imageData.source.length > 0 onClicked: { root.showActions = false @@ -177,10 +174,12 @@ Button { MenuButton { id: downloadButton Layout.fillWidth: true - buttonText: qsTr("Download") + buttonText: Translation.tr("Download") onClicked: { root.showActions = false - Hyprland.dispatch(`exec curl '${root.imageData.file_url}' -o '${root.imageData.is_nsfw ? root.nsfwPath : root.downloadPath}/${root.fileName}' && notify-send '${qsTr("Download complete")}' '${root.downloadPath}/${root.fileName}' -a 'Shell'`) + Quickshell.execDetached(["bash", "-c", + `curl '${root.imageData.file_url}' -o '${root.imageData.is_nsfw ? root.nsfwPath : root.downloadPath}/${root.fileName}' && notify-send '${Translation.tr("Download complete")}' '${root.downloadPath}/${root.fileName}' -a 'Shell'` + ]) } } } diff --git a/.config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml similarity index 94% rename from .config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml rename to .config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml index 7a207582..0aa7479f 100644 --- a/.config/quickshell/modules/sidebarLeft/anime/BooruResponse.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/anime/BooruResponse.qml @@ -1,16 +1,14 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import "../" +import qs.services import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Io import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland import Quickshell.Hyprland import Qt5Compat.GraphicalEffects @@ -94,7 +92,7 @@ Rectangle { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colOnLayer2 // text: `Page ${root.responseData.page}` - text: StringUtils.format(qsTr("Page {0}"), root.responseData.page) + text: Translation.tr("Page %1").arg(root.responseData.page) } } } @@ -162,12 +160,7 @@ Rectangle { Qt.openUrlExternally(link) Hyprland.dispatch("global quickshell:sidebarLeftClose") } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton // Only for hover - hoverEnabled: true - cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor - } + PointingHandLinkHover {} } Repeater { diff --git a/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml b/.config/quickshell/ii/modules/sidebarLeft/translator/LanguageSelectorButton.qml similarity index 80% rename from .config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml rename to .config/quickshell/ii/modules/sidebarLeft/translator/LanguageSelectorButton.qml index 37df25f8..f23e3b84 100644 --- a/.config/quickshell/modules/sidebarLeft/translator/LanguageSelectorButton.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/translator/LanguageSelectorButton.qml @@ -1,14 +1,10 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland RippleButton { id: root diff --git a/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml b/.config/quickshell/ii/modules/sidebarLeft/translator/TextCanvas.qml similarity index 89% rename from .config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml rename to .config/quickshell/ii/modules/sidebarLeft/translator/TextCanvas.qml index dad25020..c29265c9 100644 --- a/.config/quickshell/modules/sidebarLeft/translator/TextCanvas.qml +++ b/.config/quickshell/ii/modules/sidebarLeft/translator/TextCanvas.qml @@ -1,14 +1,11 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland Rectangle { id: root @@ -78,7 +75,7 @@ Rectangle { visible: root.isInput Layout.leftMargin: 10 sourceComponent: Text { - text: qsTr("%1 characters").arg(inputLoader.item.text.length) + text: Translation.tr("%1 characters").arg(inputLoader.item.text.length) color: Appearance.colors.colOnLayer1 font.pixelSize: Appearance.font.pixelSize.smaller } diff --git a/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml b/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml similarity index 84% rename from .config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml rename to .config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml index 874e23c8..38401163 100644 --- a/.config/quickshell/modules/sidebarRight/BottomWidgetGroup.qml +++ b/.config/quickshell/ii/modules/sidebarRight/BottomWidgetGroup.qml @@ -1,12 +1,11 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs +import qs.services import "./calendar" import "./todo" import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell Rectangle { id: root @@ -14,11 +13,11 @@ Rectangle { color: Appearance.colors.colLayer1 clip: true implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : bottomWidgetGroupRow.implicitHeight - property int selectedTab: 0 - property bool collapsed: PersistentStates.sidebar.bottomGroup.collapsed + property int selectedTab: Persistent.states.sidebar.bottomGroup.tab + property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed property var tabs: [ - {"type": "calendar", "name": "Calendar", "icon": "calendar_month", "widget": calendarWidget}, - {"type": "todo", "name": "To Do", "icon": "done_outline", "widget": todoWidget} + {"type": "calendar", "name": Translation.tr("Calendar"), "icon": "calendar_month", "widget": calendarWidget}, + {"type": "todo", "name": Translation.tr("To Do"), "icon": "done_outline", "widget": todoWidget} ] Behavior on implicitHeight { @@ -30,7 +29,7 @@ Rectangle { } function setCollapsed(state) { - PersistentStateManager.setState("sidebar.bottomGroup.collapsed", state) + Persistent.states.sidebar.bottomGroup.collapsed = state if (collapsed) { bottomWidgetGroupRow.opacity = 0 } @@ -97,7 +96,8 @@ Rectangle { property int remainingTasks: Todo.list.filter(task => !task.done).length; Layout.margins: 10 Layout.leftMargin: 0 - text: `${DateTime.collapsedCalendarFormat} • ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}` + // text: `${DateTime.collapsedCalendarFormat} • ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}` + text: Translation.tr("%1 • %2 tasks").arg(DateTime.collapsedCalendarFormat).arg(remainingTasks) font.pixelSize: Appearance.font.pixelSize.large color: Appearance.colors.colOnLayer1 } @@ -130,20 +130,23 @@ Rectangle { Layout.topMargin: 10 width: tabBar.width // Navigation rail buttons - ColumnLayout { + NavigationRailTabArray { + id: tabBar anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 5 - id: tabBar - spacing: 8 + currentIndex: root.selectedTab + expanded: false Repeater { model: root.tabs NavigationRailButton { + showToggledHighlight: false toggled: root.selectedTab == index buttonText: modelData.name buttonIcon: modelData.icon onClicked: { root.selectedTab = index + Persistent.states.sidebar.bottomGroup.tab = index } } } @@ -169,10 +172,12 @@ Rectangle { StackLayout { id: tabStack Layout.fillWidth: true - height: tabStack.children[0]?.tabLoader?.implicitHeight // TODO: make this less stupid + // Take the highest one, because the TODO list has no implicit height. This way the heigth of the calendar is used when it's initially loaded with the TODO list + height: Math.max(...tabStack.children.map(child => child.tabLoader?.implicitHeight || 0)) // TODO: make this less stupid Layout.alignment: Qt.AlignVCenter - property int realIndex: 0 + property int realIndex: root.selectedTab property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5 + currentIndex: root.selectedTab // Switch the tab on halfway of the anim duration Connections { diff --git a/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml b/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml similarity index 91% rename from .config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml rename to .config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml index 1b426da4..4aeef71c 100644 --- a/.config/quickshell/modules/sidebarRight/CenterWidgetGroup.qml +++ b/.config/quickshell/ii/modules/sidebarRight/CenterWidgetGroup.qml @@ -1,15 +1,13 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "./calendar" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import "./notifications" -import "./todo" import "./volumeMixer" +import qs import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell Rectangle { id: root @@ -17,7 +15,7 @@ Rectangle { color: Appearance.colors.colLayer1 property int selectedTab: 0 - property var tabButtonList: [{"icon": "notifications", "name": qsTr("Notifications")}, {"icon": "volume_up", "name": qsTr("Volume mixer")}] + property var tabButtonList: [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Volume mixer")}] Keys.onPressed: (event) => { if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) { diff --git a/.config/quickshell/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml similarity index 87% rename from .config/quickshell/modules/sidebarRight/SidebarRight.qml rename to .config/quickshell/ii/modules/sidebarRight/SidebarRight.qml index ed9788f0..91eb7b0a 100644 --- a/.config/quickshell/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/ii/modules/sidebarRight/SidebarRight.qml @@ -1,23 +1,23 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import "./quickToggles/" import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.Effects import Qt5Compat.GraphicalEffects import Quickshell.Io import Quickshell -import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland Scope { + id: root property int sidebarWidth: Appearance.sizes.sidebarWidth - property int sidebarPadding: 15 + property int sidebarPadding: 12 + property string settingsQmlPath: Quickshell.shellPath("settings.qml") PanelWindow { id: sidebarRoot @@ -86,12 +86,14 @@ Scope { implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2 implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2 color: Appearance.colors.colLayer0 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 ColumnLayout { - spacing: sidebarPadding anchors.fill: parent anchors.margins: sidebarPadding + spacing: sidebarPadding RowLayout { Layout.fillHeight: false @@ -119,7 +121,7 @@ Scope { StyledText { font.pixelSize: Appearance.font.pixelSize.normal color: Appearance.colors.colOnLayer0 - text: StringUtils.format(qsTr("Uptime: {0}"), DateTime.uptime) + text: Translation.tr("Uptime: %1").arg(DateTime.uptime) textFormat: Text.MarkdownText } @@ -136,18 +138,18 @@ Scope { Quickshell.reload(true) } StyledToolTip { - content: qsTr("Reload Hyprland & Quickshell") + content: Translation.tr("Reload Hyprland & Quickshell") } } QuickToggleButton { toggled: false buttonIcon: "settings" onClicked: { - Hyprland.dispatch(`exec ${ConfigOptions.apps.settings}`) - Hyprland.dispatch(`global quickshell:sidebarRightClose`) + Hyprland.dispatch("global quickshell:sidebarRightClose") + Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]) } StyledToolTip { - content: qsTr("Plasma Settings") + content: Translation.tr("Settings") } } QuickToggleButton { @@ -157,7 +159,7 @@ Scope { Hyprland.dispatch("global quickshell:sessionOpen") } StyledToolTip { - content: qsTr("Session") + content: Translation.tr("Session") } } } @@ -174,6 +176,8 @@ Scope { NightLight {} GameMode {} IdleInhibitor {} + EasyEffectsToggle {} + CloudflareWarp {} } // Center widget group @@ -218,7 +222,7 @@ Scope { GlobalShortcut { name: "sidebarRightToggle" - description: qsTr("Toggles right sidebar on press") + description: "Toggles right sidebar on press" onPressed: { GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; @@ -227,7 +231,7 @@ Scope { } GlobalShortcut { name: "sidebarRightOpen" - description: qsTr("Opens right sidebar on press") + description: "Opens right sidebar on press" onPressed: { GlobalStates.sidebarRightOpen = true; @@ -236,7 +240,7 @@ Scope { } GlobalShortcut { name: "sidebarRightClose" - description: qsTr("Closes right sidebar on press") + description: "Closes right sidebar on press" onPressed: { GlobalStates.sidebarRightOpen = false; diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarDayButton.qml similarity index 83% rename from .config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml rename to .config/quickshell/ii/modules/sidebarRight/calendar/CalendarDayButton.qml index 7d1af447..ab1aca55 100644 --- a/.config/quickshell/modules/sidebarRight/calendar/CalendarDayButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarDayButton.qml @@ -1,8 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts RippleButton { diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarHeaderButton.qml similarity index 89% rename from .config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml rename to .config/quickshell/ii/modules/sidebarRight/calendar/CalendarHeaderButton.qml index 92ef3c85..6b5e5aa1 100644 --- a/.config/quickshell/modules/sidebarRight/calendar/CalendarHeaderButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarHeaderButton.qml @@ -1,8 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls -import QtQuick.Layouts RippleButton { id: button diff --git a/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml similarity index 94% rename from .config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml rename to .config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml index 1f11d87a..3af804e3 100644 --- a/.config/quickshell/modules/sidebarRight/calendar/CalendarWidget.qml +++ b/.config/quickshell/ii/modules/sidebarRight/calendar/CalendarWidget.qml @@ -1,5 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs +import qs.modules.common.widgets import "./calendar_layout.js" as CalendarLayout import QtQuick import QtQuick.Controls @@ -48,7 +49,7 @@ Item { CalendarHeaderButton { clip: true buttonText: `${monthShift != 0 ? "• " : ""}${viewingDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")}` - tooltipText: (monthShift === 0) ? "" : qsTr("Jump to current month") + tooltipText: (monthShift === 0) ? "" : Translation.tr("Jump to current month") onClicked: { monthShift = 0; } @@ -92,7 +93,7 @@ Item { Repeater { model: CalendarLayout.weekDays delegate: CalendarDayButton { - day: modelData.day + day: Translation.tr(modelData.day) isToday: modelData.today bold: true enabled: false diff --git a/.config/quickshell/modules/sidebarRight/calendar/calendar_layout.js b/.config/quickshell/ii/modules/sidebarRight/calendar/calendar_layout.js similarity index 100% rename from .config/quickshell/modules/sidebarRight/calendar/calendar_layout.js rename to .config/quickshell/ii/modules/sidebarRight/calendar/calendar_layout.js diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml b/.config/quickshell/ii/modules/sidebarRight/notifications/NotificationList.qml similarity index 90% rename from .config/quickshell/modules/sidebarRight/notifications/NotificationList.qml rename to .config/quickshell/ii/modules/sidebarRight/notifications/NotificationList.qml index 491b7683..882829b8 100644 --- a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml +++ b/.config/quickshell/ii/modules/sidebarRight/notifications/NotificationList.qml @@ -1,12 +1,11 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets Item { id: root @@ -61,7 +60,7 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignHCenter - text: qsTr("No notifications") + text: Translation.tr("No notifications") } } } @@ -84,7 +83,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 10 horizontalAlignment: Text.AlignHCenter - text: `${Notifications.list.length} notifications` + text: Translation.tr("%1 notifications").arg(Notifications.list.length) opacity: Notifications.list.length > 0 ? 1 : 0 visible: opacity > 0 @@ -101,7 +100,7 @@ Item { NotificationStatusButton { buttonIcon: "notifications_paused" - buttonText: qsTr("Silent") + buttonText: Translation.tr("Silent") toggled: Notifications.silent onClicked: () => { Notifications.silent = !Notifications.silent; @@ -109,7 +108,7 @@ Item { } NotificationStatusButton { buttonIcon: "clear_all" - buttonText: qsTr("Clear") + buttonText: Translation.tr("Clear") onClicked: () => { Notifications.discardAllNotifications() } diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml b/.config/quickshell/ii/modules/sidebarRight/notifications/NotificationStatusButton.qml similarity index 92% rename from .config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml rename to .config/quickshell/ii/modules/sidebarRight/notifications/NotificationStatusButton.qml index bcfeaecf..d6001a3a 100644 --- a/.config/quickshell/modules/sidebarRight/notifications/NotificationStatusButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/notifications/NotificationStatusButton.qml @@ -1,7 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls import QtQuick.Layouts GroupButton { diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml similarity index 62% rename from .config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml rename to .config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml index 083ecc03..88357437 100644 --- a/.config/quickshell/modules/sidebarRight/quickToggles/BluetoothToggle.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/BluetoothToggle.qml @@ -1,8 +1,8 @@ -import "../" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import QtQuick import Quickshell import Quickshell.Io @@ -15,8 +15,8 @@ QuickToggleButton { toggleBluetooth.running = true } altAction: () => { - Hyprland.dispatch(`exec ${ConfigOptions.apps.bluetooth}`) - Hyprland.dispatch("global quickshell:sidebarRightClose") + Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]) + Hyprland.dispatch("global quickshell:sidebarRightClose") } Process { id: toggleBluetooth @@ -28,9 +28,9 @@ QuickToggleButton { } } StyledToolTip { - content: StringUtils.format(qsTr("{0} | Right-click to configure"), + content: Translation.tr("%1 | Right-click to configure").arg( (Bluetooth.bluetoothEnabled && Bluetooth.bluetoothDeviceName.length > 0) ? - Bluetooth.bluetoothDeviceName : qsTr("Bluetooth")) + Bluetooth.bluetoothDeviceName : Translation.tr("Bluetooth")) } } diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/CloudflareWarp.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/CloudflareWarp.qml new file mode 100644 index 00000000..39416ab2 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/CloudflareWarp.qml @@ -0,0 +1,92 @@ +import qs.modules.common +import qs.modules.common.widgets +import qs +import QtQuick +import Quickshell.Io +import Quickshell + +QuickToggleButton { + id: root + toggled: false + visible: false + + contentItem: CustomIcon { + id: distroIcon + source: 'cloudflare-dns-symbolic' + + anchors.centerIn: parent + width: 16 + height: 16 + colorize: true + color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1 + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + + onClicked: { + if (toggled) { + root.toggled = false + Quickshell.execDetached(["warp-cli", "disconnect"]) + } else { + root.toggled = true + Quickshell.execDetached(["warp-cli", "connect"]) + } + } + + Process { + id: connectProc + command: ["warp-cli", "connect"] + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) { + Quickshell.execDetached(["notify-send", + Translation.tr("Cloudflare WARP"), + Translation.tr("Connection failed. Please inspect manually with the warp-cli command") + , "-a", "Shell" + ]) + } + } + } + + Process { + id: registrationProc + command: ["warp-cli", "registration", "new"] + onExited: (exitCode, exitStatus) => { + console.log("Warp registration exited with code and status:", exitCode, exitStatus) + if (exitCode === 0) { + connectProc.running = true + } else { + Quickshell.execDetached(["notify-send", + Translation.tr("Cloudflare WARP"), + Translation.tr("Registration failed. Please inspect manually with the warp-cli command"), + "-a", "Shell" + ]) + } + } + } + + Process { + id: fetchActiveState + running: true + command: ["bash", "-c", "warp-cli status"] + stdout: StdioCollector { + id: warpStatusCollector + onStreamFinished: { + if (warpStatusCollector.text.length > 0) { + root.visible = true + } + if (warpStatusCollector.text.includes("Unable")) { + registrationProc.running = true + } else if (warpStatusCollector.text.includes("Connected")) { + root.toggled = true + } else if (warpStatusCollector.text.includes("Disconnected")) { + root.toggled = false + } + } + } + } + StyledToolTip { + content: Translation.tr("Cloudflare WARP (1.1.1.1)") + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/EasyEffectsToggle.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/EasyEffectsToggle.qml new file mode 100644 index 00000000..ebf847c7 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/EasyEffectsToggle.qml @@ -0,0 +1,49 @@ +import qs.modules.common.widgets +import qs +import Quickshell.Io +import Quickshell +import Quickshell.Hyprland + +QuickToggleButton { + id: root + toggled: false + visible: false + buttonIcon: "instant_mix" + + onClicked: { + if (toggled) { + root.toggled = false + Quickshell.execDetached(["pkill", "easyeffects"]) + } else { + root.toggled = true + Quickshell.execDetached(["easyeffects", "--gapplication-service"]) + } + } + + altAction: () => { + Quickshell.execDetached(["easyeffects"]) + Hyprland.dispatch("global quickshell:sidebarRightClose") + } + + Process { + id: fetchAvailability + running: true + command: ["bash", "-c", "command -v easyeffects"] + onExited: (exitCode, exitStatus) => { + root.visible = exitCode === 0 + } + } + + Process { + id: fetchActiveState + running: true + command: ["pidof", "easyeffects"] + onExited: (exitCode, exitStatus) => { + root.toggled = exitCode === 0 + } + } + + StyledToolTip { + content: Translation.tr("EasyEffects | Right-click to configure") + } +} diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/GameMode.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/GameMode.qml new file mode 100644 index 00000000..1907080e --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/GameMode.qml @@ -0,0 +1,31 @@ +import qs.modules.common +import qs.modules.common.widgets +import qs +import Quickshell +import Quickshell.Io + +QuickToggleButton { + id: root + buttonIcon: "gamepad" + toggled: toggled + + onClicked: { + root.toggled = !root.toggled + if (root.toggled) { + Quickshell.execDetached(["bash", "-c", `hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`]) + } else { + Quickshell.execDetached(["hyprctl", "reload"]) + } + } + Process { + id: fetchActiveState + running: true + command: ["bash", "-c", `test "$(hyprctl getoption animations:enabled -j | jq ".int")" -ne 0`] + onExited: (exitCode, exitStatus) => { + root.toggled = exitCode !== 0 // Inverted because enabled = nonzero exit + } + } + StyledToolTip { + content: Translation.tr("Game mode") + } +} \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/IdleInhibitor.qml similarity index 51% rename from .config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml rename to .config/quickshell/ii/modules/sidebarRight/quickToggles/IdleInhibitor.qml index b48d3467..949842d4 100644 --- a/.config/quickshell/modules/sidebarRight/quickToggles/IdleInhibitor.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/IdleInhibitor.qml @@ -1,9 +1,8 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "../" +import qs.modules.common +import qs.modules.common.widgets +import qs import Quickshell.Io import Quickshell -import Quickshell.Hyprland QuickToggleButton { id: root @@ -12,21 +11,21 @@ QuickToggleButton { onClicked: { if (toggled) { root.toggled = false - Hyprland.dispatch("exec pkill wayland-idle") // pkill doesn't accept too long names + Quickshell.execDetached(["pkill", "wayland-idle"]) // pkill doesn't accept too long names } else { root.toggled = true - Hyprland.dispatch('exec ${XDG_CONFIG_HOME:-$HOME/.config}/quickshell/scripts/wayland-idle-inhibitor.py') + Quickshell.execDetached([`${Directories.scriptPath}/wayland-idle-inhibitor.py`]) } } Process { id: fetchActiveState running: true - command: ["bash", "-c", "pidof wayland-idle-inhibitor.py"] + command: ["pidof", "wayland-idle-inhibitor.py"] onExited: (exitCode, exitStatus) => { root.toggled = exitCode === 0 } } StyledToolTip { - content: qsTr("Keep system awake") + content: Translation.tr("Keep system awake") } } diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/NetworkToggle.qml similarity index 64% rename from .config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml rename to .config/quickshell/ii/modules/sidebarRight/quickToggles/NetworkToggle.qml index 5271e376..3d4edf15 100644 --- a/.config/quickshell/modules/sidebarRight/quickToggles/NetworkToggle.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/NetworkToggle.qml @@ -1,8 +1,9 @@ -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions import "../" +import qs import QtQuick import Quickshell import Quickshell.Io @@ -15,7 +16,7 @@ QuickToggleButton { toggleNetwork.running = true } altAction: () => { - Hyprland.dispatch(`exec ${Network.ethernet ? ConfigOptions.apps.networkEthernet : ConfigOptions.apps.network}`) + Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]) Hyprland.dispatch("global quickshell:sidebarRightClose") } Process { @@ -28,6 +29,6 @@ QuickToggleButton { } } StyledToolTip { - content: StringUtils.format(qsTr("{0} | Right-click to configure"), Network.networkName) + content: Translation.tr("%1 | Right-click to configure").arg(Network.networkName) } } diff --git a/.config/quickshell/ii/modules/sidebarRight/quickToggles/NightLight.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/NightLight.qml new file mode 100644 index 00000000..f0265126 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/NightLight.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.modules.common +import qs.modules.common.widgets +import qs +import qs.services +import Quickshell.Io + +QuickToggleButton { + id: nightLightButton + property bool enabled: Hyprsunset.active + toggled: enabled + buttonIcon: Config.options.light.night.automatic ? "night_sight_auto" : "bedtime" + onClicked: { + Hyprsunset.toggle() + } + + altAction: () => { + Config.options.light.night.automatic = !Config.options.light.night.automatic + } + + Component.onCompleted: { + Hyprsunset.fetchState() + } + + StyledToolTip { + content: Translation.tr("Night Light | Right-click to toggle Auto mode") + } +} diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml b/.config/quickshell/ii/modules/sidebarRight/quickToggles/QuickToggleButton.qml similarity index 72% rename from .config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml rename to .config/quickshell/ii/modules/sidebarRight/quickToggles/QuickToggleButton.qml index c80f91ba..25a53de1 100644 --- a/.config/quickshell/modules/sidebarRight/quickToggles/QuickToggleButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/quickToggles/QuickToggleButton.qml @@ -1,15 +1,11 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Io GroupButton { id: button property string buttonIcon - baseWidth: altAction ? 60 : 40 + baseWidth: 40 baseHeight: 40 clickedWidth: baseWidth + 20 toggled: false @@ -18,7 +14,7 @@ GroupButton { contentItem: MaterialSymbol { anchors.centerIn: parent - iconSize: Appearance.font.pixelSize.larger + iconSize: 20 fill: toggled ? 1 : 0 color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1 horizontalAlignment: Text.AlignHCenter diff --git a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml b/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml similarity index 98% rename from .config/quickshell/modules/sidebarRight/todo/TaskList.qml rename to .config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml index b727ab1c..d9a2b67c 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml +++ b/.config/quickshell/ii/modules/sidebarRight/todo/TaskList.qml @@ -1,6 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml b/.config/quickshell/ii/modules/sidebarRight/todo/TodoItemActionButton.qml similarity index 78% rename from .config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml rename to .config/quickshell/ii/modules/sidebarRight/todo/TodoItemActionButton.qml index e013a18b..b0a6e7b9 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TodoItemActionButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/todo/TodoItemActionButton.qml @@ -1,9 +1,6 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs.modules.common +import qs.modules.common.widgets import QtQuick -import QtQuick.Controls -import QtQuick.Layouts RippleButton { id: button diff --git a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml b/.config/quickshell/ii/modules/sidebarRight/todo/TodoWidget.qml similarity index 93% rename from .config/quickshell/modules/sidebarRight/todo/TodoWidget.qml rename to .config/quickshell/ii/modules/sidebarRight/todo/TodoWidget.qml index c86f45b2..50c659ad 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TodoWidget.qml +++ b/.config/quickshell/ii/modules/sidebarRight/todo/TodoWidget.qml @@ -1,16 +1,15 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets import QtQuick import QtQuick.Controls -import QtQuick.Effects import QtQuick.Layouts Item { id: root property int currentTab: 0 - property var tabButtonList: [{"icon": "checklist", "name": qsTr("Unfinished")}, {"name": qsTr("Done"), "icon": "check_circle"}] + property var tabButtonList: [{"icon": "checklist", "name": Translation.tr("Unfinished")}, {"name": Translation.tr("Done"), "icon": "check_circle"}] property bool showAddDialog: false property int dialogMargins: 20 property int fabSize: 48 @@ -134,7 +133,7 @@ Item { TaskList { listBottomPadding: root.fabSize + root.fabMargins * 2 emptyPlaceholderIcon: "check_circle" - emptyPlaceholderText: qsTr("Nothing here!") + emptyPlaceholderText: Translation.tr("Nothing here!") taskList: Todo.list .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); }) .filter(function(item) { return !item.done; }) @@ -142,7 +141,7 @@ Item { TaskList { listBottomPadding: root.fabSize + root.fabMargins * 2 emptyPlaceholderIcon: "checklist" - emptyPlaceholderText: qsTr("Finished tasks will go here") + emptyPlaceholderText: Translation.tr("Finished tasks will go here") taskList: Todo.list .map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); }) .filter(function(item) { return item.done; }) @@ -239,7 +238,7 @@ Item { Layout.alignment: Qt.AlignLeft color: Appearance.m3colors.m3onSurface font.pixelSize: Appearance.font.pixelSize.larger - text: qsTr("Add task") + text: Translation.tr("Add task") } TextField { @@ -252,7 +251,7 @@ Item { renderType: Text.NativeRendering selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer - placeholderText: qsTr("Task description") + placeholderText: Translation.tr("Task description") placeholderTextColor: Appearance.m3colors.m3outline focus: root.showAddDialog onAccepted: dialog.addTask() @@ -280,11 +279,11 @@ Item { spacing: 5 DialogButton { - buttonText: qsTr("Cancel") + buttonText: Translation.tr("Cancel") onClicked: root.showAddDialog = false } DialogButton { - buttonText: qsTr("Add") + buttonText: Translation.tr("Add") enabled: todoInput.text.length > 0 onClicked: dialog.addTask() } diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml similarity index 82% rename from .config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml rename to .config/quickshell/ii/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml index cc956cc0..a1e589de 100644 --- a/.config/quickshell/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml +++ b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/AudioDeviceSelectorButton.qml @@ -1,11 +1,9 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import Qt5Compat.GraphicalEffects +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.services import QtQuick -import QtQuick.Controls import QtQuick.Layouts -import Quickshell.Widgets import Quickshell.Services.Pipewire GroupButton { @@ -40,14 +38,14 @@ GroupButton { Layout.fillWidth: true elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.normal - text: input ? qsTr("Input") : qsTr("Output") + text: input ? Translation.tr("Input") : Translation.tr("Output") color: Appearance.colors.colOnLayer2 } StyledText { Layout.fillWidth: true elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.smaller - text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? qsTr("Unknown") + text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? Translation.tr("Unknown") color: Appearance.m3colors.m3outline } } diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml similarity index 81% rename from .config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml rename to .config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml index 2e1570f3..4141ec7f 100644 --- a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixer.qml +++ b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixer.qml @@ -1,12 +1,12 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import qs import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell -import Quickshell.Widgets import Quickshell.Services.Pipewire @@ -16,6 +16,10 @@ Item { property bool deviceSelectorInput property int dialogMargins: 16 property PwNode selectedDevice + readonly property list appPwNodes: Pipewire.nodes.values.filter((node) => { + // return node.type == "21" // Alternative, not as clean + return node.isSink && node.isStream + }) function showDeviceSelectorDialog(input: bool) { root.selectedDevice = null @@ -36,55 +40,36 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - Flickable { - id: flickable - anchors.fill: parent - contentHeight: volumeMixerColumnLayout.height - + ListView { + id: listView + model: root.appPwNodes clip: true - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - width: flickable.width - height: flickable.height - radius: Appearance.rounding.normal - } + anchors { + fill: parent + topMargin: 10 + bottomMargin: 10 } + spacing: 6 - ColumnLayout { - id: volumeMixerColumnLayout - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 10 - spacing: 10 - - // Get a list of nodes that output to the default sink - PwNodeLinkTracker { - id: linkTracker - node: Pipewire.defaultAudioSink - } - - Repeater { - model: linkTracker.linkGroups - - VolumeMixerEntry { - Layout.fillWidth: true - // Get links to the default sinnk - required property PwLinkGroup modelData - // Consider sources that output to the default sink - node: modelData.source - } + delegate: VolumeMixerEntry { + // Layout.fillWidth: true + anchors { + left: parent.left + right: parent.right + leftMargin: 10 + rightMargin: 10 } + required property var modelData + node: modelData } } // Placeholder when list is empty Item { - anchors.fill: flickable + anchors.fill: listView visible: opacity > 0 - opacity: (linkTracker.linkGroups.length === 0) ? 1 : 0 + opacity: (root.appPwNodes.length === 0) ? 1 : 0 Behavior on opacity { NumberAnimation { @@ -108,11 +93,20 @@ Item { font.pixelSize: Appearance.font.pixelSize.normal color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignHCenter - text: qsTr("No audio source") + text: Translation.tr("No audio source") } } } } + + // Separator + Rectangle { + color: Appearance.m3colors.m3outlineVariant + implicitHeight: 1 + Layout.fillWidth: true + } + + // Device selector ButtonGroup { id: deviceSelectorRowLayout @@ -182,7 +176,7 @@ Item { Layout.alignment: Qt.AlignLeft color: Appearance.m3colors.m3onSurface font.pixelSize: Appearance.font.pixelSize.larger - text: `Select ${root.deviceSelectorInput ? "input" : "output"} device` + text: root.deviceSelectorInput ? Translation.tr("Select input device") : Translation.tr("Select output device") } Rectangle { @@ -262,13 +256,13 @@ Item { Layout.alignment: Qt.AlignRight DialogButton { - buttonText: qsTr("Cancel") + buttonText: Translation.tr("Cancel") onClicked: { root.showDeviceSelector = false } } DialogButton { - buttonText: qsTr("OK") + buttonText: Translation.tr("OK") onClicked: { root.showDeviceSelector = false if (root.selectedDevice) { diff --git a/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml new file mode 100644 index 00000000..5ee39801 --- /dev/null +++ b/.config/quickshell/ii/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml @@ -0,0 +1,63 @@ +import qs.modules.common +import qs.modules.common.widgets +import qs.services +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + required property PwNode node + PwObjectTracker { + objects: [node] + } + + implicitHeight: rowLayout.implicitHeight + + RowLayout { + id: rowLayout + anchors.fill: parent + spacing: 6 + + Image { + property real size: slider.height * 0.9 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + visible: source != "" + sourceSize.width: size + sourceSize.height: size + source: { + let icon; + icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]); + if (AppSearch.iconExists(icon)) + return Quickshell.iconPath(icon, "image-missing"); + icon = AppSearch.guessIcon(root.node.properties["node.name"]); + return Quickshell.iconPath(icon, "image-missing"); + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: -4 + + StyledText { + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colSubtext + elide: Text.ElideRight + text: { + // application.name -> description -> name + const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name); + const media = root.node.properties["media.name"]; + return media != undefined ? `${app} • ${media}` : app; + } + } + + StyledSlider { + id: slider + value: root.node.audio.volume + onValueChanged: root.node.audio.volume = value + } + } + } +} diff --git a/.config/quickshell/ii/screenshot.qml b/.config/quickshell/ii/screenshot.qml new file mode 100644 index 00000000..c26840a9 --- /dev/null +++ b/.config/quickshell/ii/screenshot.qml @@ -0,0 +1,552 @@ +//@ pragma UseQApplication +//@ pragma Env QS_NO_RELOAD_POPUP=1 +//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic + +// Adjust this to make it smaller or larger +//@ pragma Env QT_SCALE_FACTOR=1 + +pragma ComponentBehavior: "Bound" +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +ShellRoot { + id: root + property string screenshotDir: Directories.screenshotTemp + property color overlayColor: "#77111111" + property color genericContentColor: Qt.alpha(root.overlayColor, 0.9) + property color genericContentForeground: "#ddffffff" + property color selectionBorderColor: "#ddf1f1f1" + property color selectionFillColor: "#33ffffff" + property color windowBorderColor: "#dda0c0da" + property color windowFillColor: "#22a0c0da" + property color imageBorderColor: "#ddf1d1ff" + property color imageFillColor: "#33f1d1ff" + property color onBorderColor: "#ff000000" + property real standardRounding: 4 + readonly property var windows: HyprlandData.windowList + readonly property var layers: HyprlandData.layers + readonly property real falsePositivePreventionRatio: 0.5 + + // Force initialization of some singletons + Component.onCompleted: { + MaterialThemeLoader.reapplyTheme(); + } + + component TargetRegion: Rectangle { + id: regionRect + property bool showIcon: false + property bool targeted: false + property color borderColor + property color fillColor: "transparent" + property string text: "" + property real textPadding: 10 + z: 2 + color: fillColor + border.color: borderColor + border.width: targeted ? 3 : 1 + radius: root.standardRounding + + Rectangle { + id: regionLabelBackground + property real verticalPadding: 5 + property real horizontalPadding: 10 + radius: 10 + color: root.genericContentColor + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + anchors { + top: parent.top + left: parent.left + topMargin: regionRect.textPadding + leftMargin: regionRect.textPadding + } + implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2 + implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2 + RowLayout { + id: regionInfoRow + anchors.centerIn: parent + spacing: 8 + + Loader { + id: regionIconLoader + active: regionRect.showIcon + visible: active + sourceComponent: IconImage { + implicitSize: Appearance.font.pixelSize.larger + source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing") + } + } + + StyledText { + id: regionText + text: regionRect.text + color: root.genericContentForeground + } + } + } + } + + Variants { + model: Quickshell.screens + + PanelWindow { + id: panelWindow + required property var modelData + readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(modelData) + readonly property real monitorScale: hyprlandMonitor.scale + readonly property real monitorOffsetX: hyprlandMonitor.x + readonly property real monitorOffsetY: hyprlandMonitor.y + property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0 + property string screenshotPath: `${root.screenshotDir}/image-${modelData.name}` + property real dragStartX: 0 + property real dragStartY: 0 + property real draggingX: 0 + property real draggingY: 0 + property real dragDiffX: 0 + property real dragDiffY: 0 + property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0) + property bool dragging: false + property var mouseButton: null + property var imageRegions: [] + readonly property list windowRegions: filterWindowRegionsByLayers( + root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId), + panelWindow.layerRegions + ).map(window => { + return { + at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY], + size: [window.size[0], window.size[1]], + class: window.class, + title: window.title, + } + }) + readonly property list layerRegions: { + const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name] + const topLayers = layersOfThisMonitor.levels["2"] + const nonBarTopLayers = topLayers + .filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":dock"))) + .map(layer => { + return { + at: [layer.x, layer.y], + size: [layer.w, layer.h], + namespace: layer.namespace, + } + }) + const offsetAdjustedLayers = nonBarTopLayers.map(layer => { + return { + at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY], + size: layer.size, + namespace: layer.namespace, + } + }); + return offsetAdjustedLayers; + } + + property real targetedRegionX: -1 + property real targetedRegionY: -1 + property real targetedRegionWidth: 0 + property real targetedRegionHeight: 0 + + function intersectionOverUnion(regionA, regionB) { + // region: { at: [x, y], size: [w, h] } + const ax1 = regionA.at[0], ay1 = regionA.at[1]; + const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1]; + const bx1 = regionB.at[0], by1 = regionB.at[1]; + const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1]; + + const interX1 = Math.max(ax1, bx1); + const interY1 = Math.max(ay1, by1); + const interX2 = Math.min(ax2, bx2); + const interY2 = Math.min(ay2, by2); + + const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1); + const areaA = (ax2 - ax1) * (ay2 - ay1); + const areaB = (bx2 - bx1) * (by2 - by1); + const unionArea = areaA + areaB - interArea; + + return unionArea > 0 ? interArea / unionArea : 0; + } + + function filterOverlappingImageRegions(regions) { + let keep = []; + let removed = new Set(); + for (let i = 0; i < regions.length; ++i) { + if (removed.has(i)) continue; + let regionA = regions[i]; + for (let j = i + 1; j < regions.length; ++j) { + if (removed.has(j)) continue; + let regionB = regions[j]; + if (intersectionOverUnion(regionA, regionB) > 0) { + // Compare areas + let areaA = regionA.size[0] * regionA.size[1]; + let areaB = regionB.size[0] * regionB.size[1]; + if (areaA <= areaB) { + removed.add(j); + } else { + removed.add(i); + } + } + } + } + for (let i = 0; i < regions.length; ++i) { + if (!removed.has(i)) keep.push(regions[i]); + } + return keep; + } + + function filterWindowRegionsByLayers(windowRegions, layerRegions) { + return windowRegions.filter(windowRegion => { + for (let i = 0; i < layerRegions.length; ++i) { + if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0) + return false; + } + return true; + }); + } + + function filterImageRegions(regions, windowRegions, threshold = 0.1) { + // Remove image regions that overlap too much with any window region + let filtered = regions.filter(region => { + for (let i = 0; i < windowRegions.length; ++i) { + if (intersectionOverUnion(region, windowRegions[i]) > threshold) + return false; + } + return true; + }); + // Remove overlapping image regions, keep only the smaller one + return filterOverlappingImageRegions(filtered); + } + + function updateTargetedRegion(x, y) { + // Image regions + const clickedRegion = panelWindow.imageRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedRegion) { + panelWindow.targetedRegionX = clickedRegion.at[0]; + panelWindow.targetedRegionY = clickedRegion.at[1]; + panelWindow.targetedRegionWidth = clickedRegion.size[0]; + panelWindow.targetedRegionHeight = clickedRegion.size[1]; + return; + } + + // Layer regions + const clickedLayer = panelWindow.layerRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedLayer) { + panelWindow.targetedRegionX = clickedLayer.at[0]; + panelWindow.targetedRegionY = clickedLayer.at[1]; + panelWindow.targetedRegionWidth = clickedLayer.size[0]; + panelWindow.targetedRegionHeight = clickedLayer.size[1]; + return; + } + + // Window regions + const clickedWindow = panelWindow.windowRegions.find(region => { + return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1]; + }); + if (clickedWindow) { + panelWindow.targetedRegionX = clickedWindow.at[0]; + panelWindow.targetedRegionY = clickedWindow.at[1]; + panelWindow.targetedRegionWidth = clickedWindow.size[0]; + panelWindow.targetedRegionHeight = clickedWindow.size[1]; + return; + } + + panelWindow.targetedRegionX = -1; + panelWindow.targetedRegionY = -1; + panelWindow.targetedRegionWidth = 0; + panelWindow.targetedRegionHeight = 0; + } + + property real regionWidth: Math.abs(draggingX - dragStartX) + property real regionHeight: Math.abs(draggingY - dragStartY) + property real regionX: Math.min(dragStartX, draggingX) + property real regionY: Math.min(dragStartY, draggingY) + + visible: false + screen: modelData + WlrLayershell.namespace: "quickshell:screenshot" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + + Process { + id: screenshotProcess + running: true + command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(modelData.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`] + onExited: (exitCode, exitStatus) => { + panelWindow.visible = true; + imageDetectionProcess.running = true; + } + } + + Process { + id: imageDetectionProcess + command: ["bash", "-c", `${Directories.scriptPath}/images/find_regions.py ` + + `--hyprctl ` + + `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' ` + + `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} ` + + `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `] + stdout: StdioCollector { + id: imageDimensionCollector + onStreamFinished: { + imageRegions = filterImageRegions( + JSON.parse(imageDimensionCollector.text), + panelWindow.windowRegions + ); + } + } + } + + Process { + id: snipProc + function snip() { + if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) { + console.warn("Invalid region size, skipping snip."); + Qt.quit(); + } + snipProc.startDetached(); + Qt.quit(); + } + command: ["bash", "-c", + `magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} ` + + `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - ` + + `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`] + } + + ScreencopyView { + anchors.fill: parent + live: false + captureSource: modelData + + focus: panelWindow.visible + Keys.onPressed: (event) => { // Esc to close + if (event.key === Qt.Key_Escape) { + Qt.quit(); + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.CrossCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + + // Controls + onPressed: mouse => { + panelWindow.dragStartX = mouse.x; + panelWindow.dragStartY = mouse.y; + panelWindow.draggingX = mouse.x; + panelWindow.draggingY = mouse.y; + panelWindow.dragging = true; + panelWindow.mouseButton = mouse.button; + } + onReleased: mouse => { + // Detect if it was a click + + // Image regions + if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) { + if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) { + panelWindow.regionX = panelWindow.targetedRegionX; + panelWindow.regionY = panelWindow.targetedRegionY; + panelWindow.regionWidth = panelWindow.targetedRegionWidth; + panelWindow.regionHeight = panelWindow.targetedRegionHeight; + } + } + snipProc.snip(); + } + onPositionChanged: mouse => { + if (panelWindow.dragging) { + panelWindow.draggingX = mouse.x; + panelWindow.draggingY = mouse.y; + panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX; + panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY; + } + panelWindow.updateTargetedRegion(mouse.x, mouse.y); + } + + // Overlay to darken screen + Rectangle { // Base + id: overlayRect + z: 0 + anchors.fill: parent + color: root.overlayColor + layer.enabled: true + } + Rectangle { + // TODO: Make this mask the base instead of just overlaying a border + z: 1 + anchors { + left: parent.left + top: parent.top + leftMargin: panelWindow.regionX + topMargin: panelWindow.regionY + } + width: panelWindow.regionWidth + height: panelWindow.regionHeight + color: "transparent" + border.color: root.selectionBorderColor + border.width: 2 + radius: root.standardRounding + } + + // Instructions + Rectangle { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2 + } + + opacity: panelWindow.dragging ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + color: root.genericContentColor + radius: 10 + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + implicitWidth: instructionsRow.implicitWidth + 10 * 2 + implicitHeight: instructionsRow.implicitHeight + 5 * 2 + + RowLayout { + id: instructionsRow + anchors.centerIn: parent + Item { + Layout.fillHeight: true + implicitWidth: screenshotRegionIcon.implicitWidth + MaterialSymbol { + id: screenshotRegionIcon + anchors.centerIn: parent + iconSize: Appearance.font.pixelSize.larger + text: "screenshot_region" + color: root.genericContentForeground + } + } + StyledText { + text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit") + color: root.genericContentForeground + } + } + } + + // Window regions + Repeater { + model: ScriptModel { + values: panelWindow.windowRegions + } + delegate: TargetRegion { + z: 2 + required property var modelData + showIcon: true + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.class}` + radius: Appearance.rounding.windowRounding + } + } + + // Layer regions + Repeater { + model: ScriptModel { + values: panelWindow.layerRegions + } + delegate: TargetRegion { + z: 3 + required property var modelData + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.windowBorderColor + fillColor: targeted ? root.windowFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: `${modelData.namespace}` + radius: Appearance.rounding.windowRounding + } + } + + // Image regions + Repeater { + model: ScriptModel { + values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : [] + } + delegate: TargetRegion { + z: 4 + required property var modelData + targeted: !panelWindow.draggedAway && + (panelWindow.targetedRegionX === modelData.at[0] + && panelWindow.targetedRegionY === modelData.at[1] + && panelWindow.targetedRegionWidth === modelData.size[0] + && panelWindow.targetedRegionHeight === modelData.size[1]) + + opacity: panelWindow.draggedAway ? 0 : 1 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + + x: modelData.at[0] + y: modelData.at[1] + width: modelData.size[0] + height: modelData.size[1] + borderColor: root.imageBorderColor + fillColor: targeted ? root.imageFillColor : "transparent" + border.width: targeted ? 4 : 2 + text: "Content region" + } + } + } + } + } + } +} diff --git a/.config/quickshell/scripts/ai/show-installed-ollama-models.sh b/.config/quickshell/ii/scripts/ai/show-installed-ollama-models.sh similarity index 100% rename from .config/quickshell/scripts/ai/show-installed-ollama-models.sh rename to .config/quickshell/ii/scripts/ai/show-installed-ollama-models.sh diff --git a/.config/quickshell/scripts/cava/raw_output_config.txt b/.config/quickshell/ii/scripts/cava/raw_output_config.txt similarity index 100% rename from .config/quickshell/scripts/cava/raw_output_config.txt rename to .config/quickshell/ii/scripts/cava/raw_output_config.txt diff --git a/.config/quickshell/scripts/colors/applycolor.sh b/.config/quickshell/ii/scripts/colors/applycolor.sh similarity index 71% rename from .config/quickshell/scripts/colors/applycolor.sh rename to .config/quickshell/ii/scripts/colors/applycolor.sh index b36fc4d2..ddb93bd2 100755 --- a/.config/quickshell/scripts/colors/applycolor.sh +++ b/.config/quickshell/ii/scripts/colors/applycolor.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash +QUICKSHELL_CONFIG_NAME="ii" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" -CONFIG_DIR="$XDG_CONFIG_HOME/quickshell" +CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME" CACHE_DIR="$XDG_CACHE_HOME/quickshell" STATE_DIR="$XDG_STATE_HOME/quickshell" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -28,13 +29,13 @@ colorvalues=($colorstrings) # Array of color values apply_term() { # Check if terminal escape sequence template exists - if [ ! -f "$CONFIG_DIR"/scripts/terminal/sequences.txt ]; then + if [ ! -f "$SCRIPT_DIR/terminal/sequences.txt" ]; then echo "Template file not found for Terminal. Skipping that." return fi # Copy template mkdir -p "$STATE_DIR"/user/generated/terminal - cp "$CONFIG_DIR"/scripts/terminal/sequences.txt "$STATE_DIR"/user/generated/terminal/sequences.txt + cp "$SCRIPT_DIR/terminal/sequences.txt" "$STATE_DIR"/user/generated/terminal/sequences.txt # Apply colors for i in "${!colorlist[@]}"; do sed -i "s/${colorlist[$i]} #/${colorvalues[$i]#\#}/g" "$STATE_DIR"/user/generated/terminal/sequences.txt @@ -56,5 +57,16 @@ apply_qt() { python "$CONFIG_DIR/scripts/kvantum/changeAdwColors.py" # apply config colors } -apply_qt & -apply_term & +# Check if terminal theming is enabled in config +CONFIG_FILE="$XDG_CONFIG_HOME/illogical-impulse/config.json" +if [ -f "$CONFIG_FILE" ]; then + enable_terminal=$(jq -r '.appearance.wallpaperTheming.enableTerminal' "$CONFIG_FILE") + if [ "$enable_terminal" = "true" ]; then + apply_term & + fi +else + echo "Config file not found at $CONFIG_FILE. Applying terminal theming by default." + apply_term & +fi + +# apply_qt & # Qt theming is already handled by kde-material-colors diff --git a/.config/quickshell/scripts/colors/generate_colors_material.py b/.config/quickshell/ii/scripts/colors/generate_colors_material.py similarity index 100% rename from .config/quickshell/scripts/colors/generate_colors_material.py rename to .config/quickshell/ii/scripts/colors/generate_colors_material.py diff --git a/.config/quickshell/ii/scripts/colors/random_konachan_wall.sh b/.config/quickshell/ii/scripts/colors/random_konachan_wall.sh new file mode 100755 index 00000000..a4685da9 --- /dev/null +++ b/.config/quickshell/ii/scripts/colors/random_konachan_wall.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +get_pictures_dir() { + if command -v xdg-user-dir &> /dev/null; then + xdg-user-dir PICTURES + return + fi + + local config_file="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" + if [ -f "$config_file" ]; then + local pictures_path + pictures_path=$(source "$config_file" >/dev/null 2>&1; echo "$XDG_PICTURES_DIR") + echo "${pictures_path/#\$HOME/$HOME}" + return + fi + + echo "$HOME/Pictures" +} + +QUICKSHELL_CONFIG_NAME="ii" +XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" +XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" +PICTURES_DIR=$(get_pictures_dir) +CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME" +CACHE_DIR="$XDG_CACHE_HOME/quickshell" +STATE_DIR="$XDG_STATE_HOME/quickshell" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +mkdir -p "$PICTURES_DIR/Wallpapers" +page=$((1 + RANDOM % 1000)); +response=$(curl "https://konachan.net/post.json?tags=rating%3Asafe&limit=1&page=$page") +link=$(echo "$response" | jq '.[0].file_url' -r); +ext=$(echo "$link" | awk -F. '{print $NF}') +downloadPath="$PICTURES_DIR/Wallpapers/konachan_random_image.$ext" +illogicalImpulseConfigPath="$HOME/.config/illogical-impulse/config.json" +currentWallpaperPath=$(jq -r '.background.wallpaperPath' $illogicalImpulseConfigPath) +if [ "$downloadPath" == "$currentWallpaperPath" ]; then + downloadPath="$PICTURES_DIR/Wallpapers/konachan_random_image-1.$ext" +fi +curl "$link" -o "$downloadPath" +"$SCRIPT_DIR/switchwall.sh" --image "$downloadPath" diff --git a/.config/quickshell/scripts/colors/scheme_for_image.py b/.config/quickshell/ii/scripts/colors/scheme_for_image.py similarity index 100% rename from .config/quickshell/scripts/colors/scheme_for_image.py rename to .config/quickshell/ii/scripts/colors/scheme_for_image.py diff --git a/.config/quickshell/scripts/colors/switchwall.sh b/.config/quickshell/ii/scripts/colors/switchwall.sh similarity index 84% rename from .config/quickshell/scripts/colors/switchwall.sh rename to .config/quickshell/ii/scripts/colors/switchwall.sh index 80adb169..cab10c1f 100755 --- a/.config/quickshell/scripts/colors/switchwall.sh +++ b/.config/quickshell/ii/scripts/colors/switchwall.sh @@ -1,16 +1,26 @@ #!/usr/bin/env bash +QUICKSHELL_CONFIG_NAME="ii" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" -CONFIG_DIR="$XDG_CONFIG_HOME/quickshell" +CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME" CACHE_DIR="$XDG_CACHE_HOME/quickshell" STATE_DIR="$XDG_STATE_HOME/quickshell" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SHELL_CONFIG_FILE="$XDG_CONFIG_HOME/illogical-impulse/config.json" MATUGEN_DIR="$XDG_CONFIG_HOME/matugen" -terminalscheme="$XDG_CONFIG_HOME/quickshell/scripts/terminal/scheme-base.json" +terminalscheme="$SCRIPT_DIR/terminal/scheme-base.json" handle_kde_material_you_colors() { + # Check if Qt app theming is enabled in config + if [ -f "$SHELL_CONFIG_FILE" ]; then + enable_qt_apps=$(jq -r '.appearance.wallpaperTheming.enableQtApps' "$SHELL_CONFIG_FILE") + if [ "$enable_qt_apps" == "false" ]; then + return + fi + fi + # Map $type_flag to allowed scheme variants for kde-material-you-colors-wrapper.sh local kde_scheme_variant="" case "$type_flag" in @@ -45,17 +55,18 @@ post_process() { local screen_height="$2" local wallpaper_path="$3" + handle_kde_material_you_colors & # Determine the largest region on the wallpaper that's sufficiently un-busy to put widgets in - if [ ! -f "$MATUGEN_DIR/scripts/least_busy_region.py" ]; then - echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/" - else - "$MATUGEN_DIR/scripts/least_busy_region.py" \ - --screen-width "$screen_width" --screen-height "$screen_height" \ - --width 300 --height 200 \ - "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json - fi + # if [ ! -f "$MATUGEN_DIR/scripts/least_busy_region.py" ]; then + # echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/" + # else + # "$MATUGEN_DIR/scripts/least_busy_region.py" \ + # --screen-width "$screen_width" --screen-height "$screen_height" \ + # --width 300 --height 200 \ + # "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json + # fi } check_and_prompt_upscale() { @@ -103,7 +114,7 @@ THUMBNAIL_DIR="/tmp/mpvpaper_thumbnails" CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom" RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts" RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh" -VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5" +VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5 load-scripts=no" is_video() { local extension="${1##*.}" @@ -124,7 +135,7 @@ create_restore_script() { pkill -f -9 mpvpaper for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do - mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" --mpv-options '--load-scripts=no' & + mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" & sleep 0.1 done EOF @@ -140,6 +151,13 @@ EOF mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT" } +set_wallpaper_path() { + local path="$1" + if [ -f "$SHELL_CONFIG_FILE" ]; then + jq --arg path "$path" '.background.wallpaperPath = $path' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE" + fi +} + switch() { imgpath="$1" mode_flag="$2" @@ -193,10 +211,14 @@ switch() { exit 0 fi + # Set wallpaper path + set_wallpaper_path "$imgpath" + + # Set video wallpaper local video_path="$imgpath" monitors=$(hyprctl monitors -j | jq -r '.[] | .name') for monitor in $monitors; do - mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" --mpv-options '--load-scripts=no' & + mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" & sleep 0.1 done @@ -216,10 +238,8 @@ switch() { else matugen_args=(image "$imgpath") generate_colors_material_args=(--path "$imgpath") - # Set wallpaper with swww - swww img "$imgpath" --transition-step 100 --transition-fps 120 \ - --transition-type grow --transition-angle 30 --transition-duration 1 \ - --transition-pos "$cursorposx, $cursorposy_inverted" & + # Update wallpaper path in config + set_wallpaper_path "$imgpath" remove_restore fi fi @@ -237,10 +257,19 @@ switch() { [[ -n "$mode_flag" ]] && matugen_args+=(--mode "$mode_flag") && generate_colors_material_args+=(--mode "$mode_flag") [[ -n "$type_flag" ]] && matugen_args+=(--type "$type_flag") && generate_colors_material_args+=(--scheme "$type_flag") generate_colors_material_args+=(--termscheme "$terminalscheme" --blend_bg_fg) - generate_colors_material_args+=(--cache "$STATE_DIR/user/color.txt") + generate_colors_material_args+=(--cache "$STATE_DIR/user/generated/color.txt") pre_process "$mode_flag" + # Check if app and shell theming is enabled in config + if [ -f "$SHELL_CONFIG_FILE" ]; then + enable_apps_shell=$(jq -r '.appearance.wallpaperTheming.enableAppsAndShell' "$SHELL_CONFIG_FILE") + if [ "$enable_apps_shell" == "false" ]; then + echo "App and shell theming disabled, skipping matugen and color generation" + return + fi + fi + matugen "${matugen_args[@]}" source "$(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate" python3 "$SCRIPT_DIR/generate_colors_material.py" "${generate_colors_material_args[@]}" \ @@ -263,7 +292,7 @@ main() { noswitch_flag="" get_type_from_config() { - jq -r '.appearance.palette.type' "$XDG_CONFIG_HOME/illogical-impulse/config.json" 2>/dev/null || echo "auto" + jq -r '.appearance.palette.type' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "auto" } detect_scheme_type_from_image() { @@ -297,7 +326,7 @@ main() { ;; --noswitch) noswitch_flag="1" - imgpath=$(swww query | awk -F 'image: ' '{print $2}') + imgpath=$(jq -r '.background.wallpaperPath' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "") shift ;; *) @@ -330,7 +359,7 @@ main() { # Only prompt for wallpaper if not using --color and not using --noswitch and no imgpath set if [[ -z "$imgpath" && -z "$color_flag" && -z "$noswitch_flag" ]]; then - cd "$(xdg-user-dir PICTURES)/Wallpapers" 2>/dev/null || cd "$(xdg-user-dir PICTURES)" || return 1 + cd "$(xdg-user-dir PICTURES)/Wallpapers/showcase" 2>/dev/null || cd "$(xdg-user-dir PICTURES)/Wallpapers" 2>/dev/null || cd "$(xdg-user-dir PICTURES)" || return 1 imgpath="$(kdialog --getopenfilename . --title 'Choose wallpaper')" fi diff --git a/.config/quickshell/scripts/terminal/scheme-base.json b/.config/quickshell/ii/scripts/colors/terminal/scheme-base.json similarity index 100% rename from .config/quickshell/scripts/terminal/scheme-base.json rename to .config/quickshell/ii/scripts/colors/terminal/scheme-base.json diff --git a/.config/quickshell/scripts/terminal/sequences.txt b/.config/quickshell/ii/scripts/colors/terminal/sequences.txt similarity index 100% rename from .config/quickshell/scripts/terminal/sequences.txt rename to .config/quickshell/ii/scripts/colors/terminal/sequences.txt diff --git a/.config/quickshell/scripts/hyprland/get_keybinds.py b/.config/quickshell/ii/scripts/hyprland/get_keybinds.py similarity index 100% rename from .config/quickshell/scripts/hyprland/get_keybinds.py rename to .config/quickshell/ii/scripts/hyprland/get_keybinds.py diff --git a/.config/quickshell/ii/scripts/images/find_regions.py b/.config/quickshell/ii/scripts/images/find_regions.py new file mode 100755 index 00000000..fe68a4db --- /dev/null +++ b/.config/quickshell/ii/scripts/images/find_regions.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +import argparse +import cv2 +import json +import numpy as np +import sys + +DEFAULT_IMAGE_PATH = '/tmp/quickshell/media/screenshot/image' + +def iou(boxA, boxB): + # Compute intersection over union for two boxes + xA = max(boxA['x'], boxB['x']) + yA = max(boxA['y'], boxB['y']) + xB = min(boxA['x'] + boxA['width'], boxB['x'] + boxB['width']) + yB = min(boxA['y'] + boxA['height'], boxB['y'] + boxB['height']) + interW = max(0, xB - xA) + interH = max(0, yB - yA) + interArea = interW * interH + boxAArea = boxA['width'] * boxA['height'] + boxBArea = boxB['width'] * boxB['height'] + iou = interArea / float(boxAArea + boxBArea - interArea) if (boxAArea + boxBArea - interArea) > 0 else 0 + return iou + +def non_max_suppression(regions, iou_threshold=0.7): + # Sort by area (largest first) + regions = sorted(regions, key=lambda r: r['width'] * r['height'], reverse=True) + keep = [] + while regions: + current = regions.pop(0) + keep.append(current) + regions = [r for r in regions if iou(current, r) < iou_threshold] + return keep + +def find_regions(image_path, min_width, min_height, max_width=None, max_height=None, quality=False, k=150, min_size=20, sigma=0.8, resize_factor=1.0): + image = cv2.imread(image_path) + if image is None: + print(f'Error: Could not load image {image_path}', file=sys.stderr) + sys.exit(1) + orig_h, orig_w = image.shape[:2] + if resize_factor != 1.0: + image = cv2.resize(image, (int(orig_w * resize_factor), int(orig_h * resize_factor)), interpolation=cv2.INTER_AREA) + ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation() + ss.setBaseImage(image) + if quality: + ss.switchToSelectiveSearchQuality(k, min_size, sigma) + else: + ss.switchToSelectiveSearchFast(k, min_size, sigma) + rects = ss.process() + regions = [] + for (x, y, w, h) in rects: + # Scale regions back to original image size if resized + if resize_factor != 1.0: + x = int(x / resize_factor) + y = int(y / resize_factor) + w = int(w / resize_factor) + h = int(h / resize_factor) + # Filter out region that is exactly the same size as the original image + if w == orig_w and h == orig_h and x == 0 and y == 0: + continue + if w > min_width and h > min_height: + if (max_width is None or w < max_width) and (max_height is None or h < max_height): + regions.append({'x': int(x), 'y': int(y), 'width': int(w), 'height': int(h)}) + # Remove duplicates/overlaps + regions = non_max_suppression(regions, iou_threshold=0.7) + return regions, cv2.imread(image_path) # Return original image for drawing + +def draw_regions(image, regions, output_path): + for region in regions: + if 'x' in region: + x, y, w, h = region['x'], region['y'], region['width'], region['height'] + elif 'at' in region and 'size' in region: + x, y = region['at'] + w, h = region['size'] + else: + continue + cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2) + cv2.imwrite(output_path, image) + +def main(): + parser = argparse.ArgumentParser(description='Find regions of interest in an image using selective search.') + parser.add_argument('-i', '--image', default=DEFAULT_IMAGE_PATH, help='Path to input image') + parser.add_argument('-do', '--debug-output', help='Path to save debug image with rectangles') + parser.add_argument('--min-width', type=int, default=200, help='Minimum width of detected region') + parser.add_argument('--min-height', type=int, default=100, help='Minimum height of detected region') + parser.add_argument('--max-width', type=int, help='Maximum width of detected region') + parser.add_argument('--max-height', type=int, help='Maximum height of detected region') + parser.add_argument('--single', action='store_true', help='Only output the most likely (largest) region') + parser.add_argument('--quality', action='store_true', help='Use quality mode for selective search (slower, less sensitive)') + parser.add_argument('--k', type=int, default=3000, help='Segmentation parameter k (default: 150)') + parser.add_argument('--min-size', type=int, default=50, help='Segmentation parameter min_size (default: 20)') + parser.add_argument('--sigma', type=float, default=0.6, help='Segmentation parameter sigma (default: 0.8)') + parser.add_argument('--resize-factor', type=float, default=0.1, help='Resize factor for input image before processing (default: 1.0, e.g. 0.5 for half size)') + parser.add_argument('--hyprctl', action='store_true', help='Mimics hyprctl\'s window output, like {"at": [x, y], "size": [w, h]}') + args = parser.parse_args() + + regions, image = find_regions( + args.image, + min_width=args.min_width, + min_height=args.min_height, + max_width=args.max_width, + max_height=args.max_height, + quality=args.quality, + k=args.k, + min_size=args.min_size, + sigma=args.sigma, + resize_factor=args.resize_factor + ) + if args.single and regions: + largest = max(regions, key=lambda r: r['width'] * r['height']) + regions = [largest] + if args.hyprctl: + regions = [{"at": [r['x'], r['y']], "size": [r['width'], r['height']]} for r in regions] + print(json.dumps(regions)) + if args.debug_output: + draw_regions(image, regions, args.debug_output) + +if __name__ == '__main__': + main() + diff --git a/.config/matugen/scripts/least_busy_region.py b/.config/quickshell/ii/scripts/images/least_busy_region.py similarity index 92% rename from .config/matugen/scripts/least_busy_region.py rename to .config/quickshell/ii/scripts/images/least_busy_region.py index a1f2f47b..2b1d104e 100755 --- a/.config/matugen/scripts/least_busy_region.py +++ b/.config/quickshell/ii/scripts/images/least_busy_region.py @@ -18,7 +18,7 @@ def center_crop(img, target_w, target_h): y2 = y1 + target_h return img[y1:y2, x1:x2] -def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", padding=50): +def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", horizontal_padding=50, vertical_padding=50): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"Image not found: {image_path}") @@ -59,10 +59,10 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre min_var = None min_coords = (0, 0) area = region_width * region_height - x_start = padding - y_start = padding - x_end = w - region_width - padding + 1 - y_end = h - region_height - padding + 1 + x_start = horizontal_padding + y_start = vertical_padding + x_end = w - region_width - horizontal_padding + 1 + y_end = h - region_height - vertical_padding + 1 for y in range(y_start, max(y_end, y_start+1), stride): for x in range(x_start, max(x_end, x_start+1), stride): x1, y1 = x, y @@ -76,7 +76,7 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre min_coords = (x, y) return min_coords, min_var -def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, padding=50): +def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, horizontal_padding=50, vertical_padding=50): img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"Image not found: {image_path}") @@ -130,10 +130,10 @@ def find_largest_region(image_path, screen_width=None, screen_height=None, verbo max_size = mid - 1 continue found = False - x_start = padding - y_start = padding - x_end = w - region_w - padding + 1 - y_end = h - region_h - padding + 1 + x_start = horizontal_padding + y_start = vertical_padding + x_end = w - region_w - horizontal_padding + 1 + y_end = h - region_h - vertical_padding + 1 for y in range(y_start, max(y_end, y_start+1), stride): for x in range(x_start, max(x_end, x_start+1), stride): x1, y1 = x, y @@ -257,13 +257,14 @@ def main(): parser.add_argument("-v", "--visual-output", action="store_true", help="Output image with rectangle") parser.add_argument("--screen-width", type=int, default=1920, help="Screen width for wallpaper scaling") parser.add_argument("--screen-height", type=int, default=1080, help="Screen height for wallpaper scaling") - parser.add_argument("--stride", type=int, default=4, help="Step size for sliding window (higher is faster, less precise)") + parser.add_argument("--stride", type=int, default=10, help="Step size for sliding window (higher is faster, less precise)") parser.add_argument("--screen-mode", choices=["fill", "fit"], default="fill", help="Wallpaper scaling mode: 'fill' (default) or 'fit'") parser.add_argument("--verbose", action="store_true", help="Print verbose output") parser.add_argument("-l", "--largest-region", action="store_true", help="Find the largest region under the variance threshold and output its center") parser.add_argument("-t", "--variance-threshold", type=float, default=1000.0, help="Variance threshold for largest region mode") parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode") - parser.add_argument("--padding", type=int, default=50, help="Minimum distance from region to image edge (default: 50)") + parser.add_argument("--horizontal-padding", "-hp", type=int, default=50, help="Minimum horizontal distance from region to image edge") + parser.add_argument("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge") args = parser.parse_args() if args.largest_region: @@ -276,7 +277,8 @@ def main(): screen_mode=args.screen_mode, threshold=args.variance_threshold, aspect_ratio=args.aspect_ratio, - padding=args.padding + horizontal_padding=args.horizontal_padding, + vertical_padding=args.vertical_padding ) if center: if args.visual_output: @@ -312,7 +314,8 @@ def main(): verbose=args.verbose, stride=args.stride, screen_mode=args.screen_mode, - padding=args.padding + horizontal_padding=args.horizontal_padding, + vertical_padding=args.vertical_padding ) if args.visual_output: draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode) diff --git a/.config/quickshell/scripts/kvantum/adwsvg.py b/.config/quickshell/ii/scripts/kvantum/adwsvg.py similarity index 100% rename from .config/quickshell/scripts/kvantum/adwsvg.py rename to .config/quickshell/ii/scripts/kvantum/adwsvg.py diff --git a/.config/quickshell/scripts/kvantum/adwsvgDark.py b/.config/quickshell/ii/scripts/kvantum/adwsvgDark.py similarity index 100% rename from .config/quickshell/scripts/kvantum/adwsvgDark.py rename to .config/quickshell/ii/scripts/kvantum/adwsvgDark.py diff --git a/.config/quickshell/scripts/kvantum/changeAdwColors.py b/.config/quickshell/ii/scripts/kvantum/changeAdwColors.py similarity index 100% rename from .config/quickshell/scripts/kvantum/changeAdwColors.py rename to .config/quickshell/ii/scripts/kvantum/changeAdwColors.py diff --git a/.config/quickshell/scripts/kvantum/materialQT.sh b/.config/quickshell/ii/scripts/kvantum/materialQT.sh similarity index 89% rename from .config/quickshell/scripts/kvantum/materialQT.sh rename to .config/quickshell/ii/scripts/kvantum/materialQT.sh index 3d1f8a7b..a049c553 100755 --- a/.config/quickshell/scripts/kvantum/materialQT.sh +++ b/.config/quickshell/ii/scripts/kvantum/materialQT.sh @@ -1,11 +1,13 @@ #!/usr/bin/env bash +QUICKSHELL_CONFIG_NAME="ii" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" -CONFIG_DIR="$XDG_CONFIG_HOME/quickshell" +CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME" CACHE_DIR="$XDG_CACHE_HOME/quickshell" STATE_DIR="$XDG_STATE_HOME/quickshell" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" get_light_dark() { current_mode=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'") diff --git a/.config/quickshell/scripts/wayland-idle-inhibitor.py b/.config/quickshell/ii/scripts/wayland-idle-inhibitor.py similarity index 100% rename from .config/quickshell/scripts/wayland-idle-inhibitor.py rename to .config/quickshell/ii/scripts/wayland-idle-inhibitor.py diff --git a/.config/quickshell/services/Ai.qml b/.config/quickshell/ii/services/Ai.qml similarity index 69% rename from .config/quickshell/services/Ai.qml rename to .config/quickshell/ii/services/Ai.qml index f545b878..b58a632a 100644 --- a/.config/quickshell/services/Ai.qml +++ b/.config/quickshell/ii/services/Ai.qml @@ -1,13 +1,12 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/object_utils.js" as ObjectUtils -import "root:/modules/common" -import Quickshell; -import Quickshell.Io; -import Qt.labs.platform -import QtQuick; +import qs.modules.common.functions as CF +import qs.modules.common +import qs +import Quickshell +import Quickshell.Io +import QtQuick /** * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats. @@ -18,14 +17,14 @@ Singleton { readonly property string interfaceRole: "interface" readonly property string apiKeyEnvVarName: "API_KEY" property Component aiMessageComponent: AiMessageData {} - property string systemPrompt: ConfigOptions?.ai?.systemPrompt ?? "" - property var messages: [] + property string systemPrompt: Config.options?.ai?.systemPrompt ?? "" + // property var messages: [] property var messageIDs: [] property var messageByID: ({}) readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {} readonly property var apiKeysLoaded: KeyringStorage.loaded property var postResponseHook - property real temperature: PersistentStates?.ai?.temperature ?? 0.5 + property real temperature: Persistent.states?.ai?.temperature ?? 0.5 function idForMessage(message) { // Generate a unique ID using timestamp and random value @@ -36,6 +35,11 @@ Singleton { return modelName.replace(/:/g, "_").replace(/\./g, "_") } + property list defaultPrompts: [] + property list userPrompts: [] + property list promptFiles: [...defaultPrompts, ...userPrompts] + property list savedChats: [] + // Model properties: // - name: Name of the model // - icon: Icon name of the model @@ -53,14 +57,14 @@ Singleton { "gemini-2.0-flash-search": { "name": "Gemini 2.0 Flash (Search)", "icon": "google-gemini-symbolic", - "description": qsTr("Online | Google's model\nGives up-to-date information with search."), + "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "model": "gemini-2.0-flash", "requires_key": true, "key_id": "gemini", "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": [ { @@ -71,14 +75,14 @@ Singleton { "gemini-2.0-flash-tools": { "name": "Gemini 2.0 Flash (Tools)", "icon": "google-gemini-symbolic", - "description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), + "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "model": "gemini-2.0-flash", "requires_key": true, "key_id": "gemini", "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": [ { @@ -116,14 +120,14 @@ Singleton { "gemini-2.5-flash-search": { "name": "Gemini 2.5 Flash (Search)", "icon": "google-gemini-symbolic", - "description": qsTr("Online | Google's model\nGives up-to-date information with search."), + "description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent", "model": "gemini-2.5-flash-preview-05-20", "requires_key": true, "key_id": "gemini", "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": [ { @@ -134,14 +138,14 @@ Singleton { "gemini-2.5-flash-tools": { "name": "Gemini 2.5 Flash (Tools)", "icon": "google-gemini-symbolic", - "description": qsTr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), + "description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), "homepage": "https://aistudio.google.com", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent", "model": "gemini-2.5-flash-preview-05-20", "requires_key": true, "key_id": "gemini", "key_get_link": "https://aistudio.google.com/app/apikey", - "key_get_description": qsTr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), + "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "api_format": "gemini", "tools": [ { @@ -179,34 +183,33 @@ Singleton { "openrouter-llama4-maverick": { "name": "Llama 4 Maverick", "icon": "ollama-symbolic", - "description": StringUtils.format(qsTr("Online via {0} | {1}'s model"), "OpenRouter", "Meta"), + "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("Meta"), "homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free", "endpoint": "https://openrouter.ai/api/v1/chat/completions", "model": "meta-llama/llama-4-maverick:free", "requires_key": true, "key_id": "openrouter", "key_get_link": "https://openrouter.ai/settings/keys", - "key_get_description": qsTr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), + "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), }, "openrouter-deepseek-r1": { "name": "DeepSeek R1", "icon": "deepseek-symbolic", - "description": StringUtils.format(qsTr("Online via {0} | {1}'s model"), "OpenRouter", "DeepSeek"), + "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("DeepSeek"), "homepage": "https://openrouter.ai/deepseek/deepseek-r1:free", "endpoint": "https://openrouter.ai/api/v1/chat/completions", "model": "deepseek/deepseek-r1:free", "requires_key": true, "key_id": "openrouter", "key_get_link": "https://openrouter.ai/settings/keys", - "key_get_description": qsTr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), + "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), }, } property var modelList: Object.keys(root.models) - property var currentModelId: PersistentStates?.ai?.model || modelList[0] + property var currentModelId: Persistent.states?.ai?.model || modelList[0] Component.onCompleted: { - setModel(currentModelId, false); // Do necessary setup for model - getOllamaModels.running = true + setModel(currentModelId, false, false); // Do necessary setup for model } function guessModelLogo(model) { @@ -232,7 +235,8 @@ Singleton { Process { id: getOllamaModels - command: ["bash", "-c", `${Directories.config}/quickshell/scripts/ai/show-installed-ollama-models.sh`.replace(/file:\/\//, "")] + running: true + command: ["bash", "-c", `${Directories.scriptPath}/ai/show-installed-ollama-models.sh`.replace(/file:\/\//, "")] stdout: SplitParser { onRead: data => { try { @@ -244,7 +248,7 @@ Singleton { root.models[safeModelName] = { "name": guessModelName(model), "icon": guessModelLogo(model), - "description": StringUtils.format(qsTr("Local Ollama model | {0}"), model), + "description": Translation.tr("Local Ollama model | %1").arg(model), "homepage": `https://ollama.com/library/${model}`, "endpoint": "http://localhost:11434/v1/chat/completions", "model": model, @@ -260,11 +264,74 @@ Singleton { } } + Process { + id: getDefaultPrompts + running: true + command: ["ls", "-1", Directories.defaultAiPrompts] + stdout: StdioCollector { + onStreamFinished: { + if (text.length === 0) return; + root.defaultPrompts = text.split("\n") + .filter(fileName => fileName.endsWith(".md") || fileName.endsWith(".txt")) + .map(fileName => `${Directories.defaultAiPrompts}/${fileName}`) + } + } + } + + Process { + id: getUserPrompts + running: true + command: ["ls", "-1", Directories.userAiPrompts] + stdout: StdioCollector { + onStreamFinished: { + if (text.length === 0) return; + root.userPrompts = text.split("\n") + .filter(fileName => fileName.endsWith(".md") || fileName.endsWith(".txt")) + .map(fileName => `${Directories.userAiPrompts}/${fileName}`) + } + } + } + + Process { + id: getSavedChats + running: true + command: ["ls", "-1", Directories.aiChats] + stdout: StdioCollector { + onStreamFinished: { + if (text.length === 0) return; + root.savedChats = text.split("\n") + .filter(fileName => fileName.endsWith(".json")) + .map(fileName => `${Directories.aiChats}/${fileName}`) + } + } + } + + FileView { + id: promptLoader + watchChanges: false; + onLoadedChanged: { + if (!promptLoader.loaded) return; + Config.options.ai.systemPrompt = promptLoader.text(); + root.addMessage(Translation.tr("Loaded the following system prompt\n\n---\n\n%1").arg(Config.options.ai.systemPrompt), root.interfaceRole); + } + } + + function printPrompt() { + root.addMessage(Translation.tr("The current system prompt is\n\n---\n\n%1").arg(Config.options.ai.systemPrompt), root.interfaceRole); + } + + function loadPrompt(filePath) { + promptLoader.path = "" // Unload + promptLoader.path = filePath; // Load + promptLoader.reload(); + } + function addMessage(message, role) { if (message.length === 0) return; const aiMessage = aiMessageComponent.createObject(root, { "role": role, "content": message, + "rawContent": message, "thinking": false, "done": true, }); @@ -283,8 +350,8 @@ Singleton { function addApiKeyAdvice(model) { root.addMessage( - StringUtils.format(qsTr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command
\n\n### For {0}:\n\n**Link**: {1}\n\n{2}'), - model.name, model.key_get_link, model.key_get_description ?? qsTr("No further instruction provided")), + Translation.tr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3') + .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("No further instruction provided")), Ai.interfaceRole ); } @@ -293,7 +360,7 @@ Singleton { return models[currentModelId]; } - function setModel(modelId, feedback = true) { + function setModel(modelId, feedback = true, setPersistentState = true) { if (!modelId) modelId = "" modelId = modelId.toLowerCase() if (modelList.indexOf(modelId) !== -1) { @@ -301,12 +368,15 @@ Singleton { // Fetch API keys if needed if (model?.requires_key) KeyringStorage.fetchKeyringData(); // See if policy prevents online models - if (ConfigOptions.policies.ai === 2 && !model.endpoint.includes("localhost")) { - root.addMessage(StringUtils.format(StringUtils.format("Online models disallowed\n\nControlled by `policies.ai` config option"), model.name), root.interfaceRole); + if (Config.options.policies.ai === 2 && !model.endpoint.includes("localhost")) { + root.addMessage( + Translation.tr("Online models disallowed\n\nControlled by `policies.ai` config option"), + root.interfaceRole + ); return; } - PersistentStateManager.setState("ai.model", modelId); - if (feedback) root.addMessage(StringUtils.format(StringUtils.format("Model set to {0}"), model.name), root.interfaceRole); + if (setPersistentState) Persistent.states.ai.model = modelId; + if (feedback) root.addMessage(Translation.tr("Model set to %1").arg(model.name), root.interfaceRole); if (model.requires_key) { // If key not there show advice if (root.apiKeysLoaded && (!root.apiKeys[model.key_id] || root.apiKeys[model.key_id].length === 0)) { @@ -314,7 +384,7 @@ Singleton { } } } else { - if (feedback) root.addMessage(qsTr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```" + if (feedback) root.addMessage(Translation.tr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```" } } @@ -324,18 +394,18 @@ Singleton { function setTemperature(value) { if (value == NaN || value < 0 || value > 2) { - root.addMessage(qsTr("Temperature must be between 0 and 2"), Ai.interfaceRole); + root.addMessage(Translation.tr("Temperature must be between 0 and 2"), Ai.interfaceRole); return; } - PersistentStateManager.setState("ai.temperature", value); + Persistent.states.ai.temperature = value; root.temperature = value; - root.addMessage(StringUtils.format(qsTr("Temperature set to {0}"), value), Ai.interfaceRole); + root.addMessage(Translation.tr("Temperature set to %1").arg(value), Ai.interfaceRole); } function setApiKey(key) { const model = models[currentModelId]; if (!model.requires_key) { - root.addMessage(StringUtils.format(qsTr("{0} does not require an API key"), model.name), Ai.interfaceRole); + root.addMessage(Translation.tr("%1 does not require an API key").arg(model.name), Ai.interfaceRole); return; } if (!key || key.length === 0) { @@ -344,7 +414,7 @@ Singleton { return; } KeyringStorage.setNestedField(["apiKeys", model.key_id], key.trim()); - root.addMessage(StringUtils.format(qsTr("API key set for {0}"), model.name, Ai.interfaceRole)); + root.addMessage(Translation.tr("API key set for %1").arg(model.name), Ai.interfaceRole); } function printApiKey() { @@ -352,17 +422,17 @@ Singleton { if (model.requires_key) { const key = root.apiKeys[model.key_id]; if (key) { - root.addMessage(StringUtils.format(qsTr("API key:\n\n```txt\n{0}\n```"), key), Ai.interfaceRole); + root.addMessage(Translation.tr("API key:\n\n```txt\n%1\n```").arg(key), Ai.interfaceRole); } else { - root.addMessage(StringUtils.format(qsTr("No API key set for {0}"), model.name), Ai.interfaceRole); + root.addMessage(Translation.tr("No API key set for %1").arg(model.name), Ai.interfaceRole); } } else { - root.addMessage(StringUtils.format(qsTr("{0} does not require an API key"), model.name), Ai.interfaceRole); + root.addMessage(Translation.tr("%1 does not require an API key").arg(model.name), Ai.interfaceRole); } } function printTemperature() { - root.addMessage(StringUtils.format(qsTr("Temperature: {0}"), root.temperature), Ai.interfaceRole); + root.addMessage(Translation.tr("Temperature: %1").arg(root.temperature), Ai.interfaceRole); } function clearMessages() { @@ -393,6 +463,7 @@ Singleton { root.postResponseHook(); root.postResponseHook = null; // Reset hook after use } + root.saveChat("lastSession") } function buildGeminiRequestData(model, messages) { @@ -424,7 +495,7 @@ Singleton { return { "role": geminiApiRoleName, "parts": [{ - text: message.content, + text: message.rawContent, }] } }), @@ -449,7 +520,7 @@ Singleton { ...messages.filter(message => (message.role != Ai.interfaceRole)).map(message => { return { "role": message.role, - "content": message.content, + "content": message.rawContent, } }), ], @@ -481,6 +552,7 @@ Singleton { "role": "assistant", "model": currentModelId, "content": "", + "rawContent": "", "thinking": true, "done": false, }); @@ -501,7 +573,7 @@ Singleton { const requestCommandString = `curl --no-buffer "${endpoint}"` + ` ${headerString}` + ((apiFormat == "gemini") ? "" : ` -H "Authorization: Bearer \$\{${root.apiKeyEnvVarName}\}"`) - + ` -d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'` + + ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'` // console.log("Request command: ", requestCommandString); requester.command = baseCommand.concat([requestCommandString]); @@ -525,12 +597,15 @@ Singleton { const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall; requester.message.functionName = functionCall.name; requester.message.functionCall = functionCall.name; - requester.message.content += `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n`; + const newContent = `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n` + requester.message.rawContent += newContent; + requester.message.content += newContent; root.handleGeminiFunctionCall(functionCall.name, functionCall.args); return } // Normal text response const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text + requester.message.rawContent += responseContent; requester.message.content += responseContent; const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => { return { @@ -538,7 +613,8 @@ Singleton { "text": chunk?.web?.title, "url": chunk?.web?.uri, } - }); + }) ?? []; + const annotations = dataJson.candidates[0]?.groundingMetadata?.groundingSupports?.map(citation => { return { "type": "url_citation", @@ -553,7 +629,8 @@ Singleton { requester.message.annotations = annotations; // console.log(JSON.stringify(requester.message, null, 2)); } catch (e) { - console.log("[AI] Could not parse response from stream: ", e); + console.log("[AI] Gemini: Could not parse buffer: ", e); + requester.message.rawContent += requester.geminiBuffer; requester.message.content += requester.geminiBuffer } finally { requester.geminiBuffer = ""; @@ -595,19 +672,24 @@ Singleton { if (responseContent && responseContent.length > 0) { if (requester.isReasoning) { requester.isReasoning = false; - requester.message.content += "\n\n
\n\n"; + const endBlock = "\n\n
\n\n"; + requester.message.content += endBlock; + requester.message.rawContent += endBlock; } newContent = dataJson.choices[0]?.delta?.content || dataJson.message.content; } else if (responseReasoning && responseReasoning.length > 0) { // console.log("Reasoning content: ", dataJson.choices[0].delta.reasoning); if (!requester.isReasoning) { requester.isReasoning = true; - requester.message.content += "\n\n\n\n"; - } + const startBlock = "\n\n\n\n"; + requester.message.rawContent += startBlock; + requester.message.content += startBlock; + } newContent = dataJson.choices[0].delta.reasoning || dataJson.choices[0].delta.reasoning_content; } requester.message.content += newContent; + requester.message.rawContent += newContent; if (dataJson.done) { requester.markDone(); @@ -630,10 +712,12 @@ Singleton { } else { console.log("Unknown API format: ", requester.apiFormat); + requester.message.rawContent += data; requester.message.content += data; } } catch (e) { console.log("[AI] Could not parse response from stream: ", e); + requester.message.rawContent += data; requester.message.content += data; } } @@ -645,8 +729,9 @@ Singleton { try { // to parse full response into json for error handling // console.log("Full response: ", requester.message.content + "]"); - const parsedResponse = JSON.parse(requester.message.content + "]"); - requester.message.content = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``; + const parsedResponse = JSON.parse(requester.message.rawContent + "]"); + requester.message.rawContent = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``; + requester.message.content = requester.message.rawContent; } catch (e) { // console.log("[AI] Could not parse response on exit: ", e); } @@ -667,6 +752,7 @@ Singleton { const aiMessage = aiMessageComponent.createObject(root, { "role": "user", "content": `[[ Output of ${name} ]]`, + "rawContent": `[[ Output of ${name} ]]`, "functionName": name, "functionResponse": output, "thinking": false, @@ -701,23 +787,98 @@ Singleton { root.setModel("gemini-2.0-flash-search", false); root.postResponseHook = () => root.setModel("gemini-2.0-flash-tools", false); } - addFunctionOutputMessage(name, qsTr("Switched to search mode. Continue with the user's request.")) + addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request.")) requester.makeRequest(); } else if (name === "get_shell_config") { - const configJson = ObjectUtils.toPlainObject(ConfigOptions) + const configJson = CF.ObjectUtils.toPlainObject(Config.options) addFunctionOutputMessage(name, JSON.stringify(configJson)); requester.makeRequest(); } else if (name === "set_shell_config") { if (!args.key || !args.value) { - addFunctionOutputMessage(name, qsTr("Invalid arguments. Must provide `key` and `value`.")); + addFunctionOutputMessage(name, Translation.tr("Invalid arguments. Must provide `key` and `value`.")); return; } const key = args.key; const value = args.value; - ConfigLoader.setLiveConfigValue(key, value); - ConfigLoader.saveConfig(); + Config.setNestedValue(key, value); } - else root.addMessage(qsTr("Unknown function call: {0}"), "assistant"); + else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant"); } + function chatToJson() { + return root.messageIDs.map(id => { + const message = root.messageByID[id] + return ({ + "role": message.role, + "rawContent": message.rawContent, + "model": message.model, + "thinking": false, + "done": true, + "annotations": message.annotations, + "annotationSources": message.annotationSources, + "functionName": message.functionName, + "functionCall": message.functionCall, + "functionResponse": message.functionResponse, + "visibleToUser": message.visibleToUser, + }) + }) + } + + FileView { + id: chatSaveFile + property string chatName: "chat" + path: `${Directories.aiChats}/${chatName}.json` + blockLoading: true + } + + /** + * Saves chat to a JSON list of message objects. + * @param chatName name of the chat + */ + function saveChat(chatName) { + chatSaveFile.chatName = chatName.trim() + const saveContent = JSON.stringify(root.chatToJson()) + chatSaveFile.setText(saveContent) + getSavedChats.running = true; + } + + /** + * Loads chat from a JSON list of message objects. + * @param chatName name of the chat + */ + function loadChat(chatName) { + try { + chatSaveFile.chatName = chatName.trim() + chatSaveFile.reload() + const saveContent = chatSaveFile.text() + // console.log(saveContent) + const saveData = JSON.parse(saveContent) + root.clearMessages() + root.messageIDs = saveData.map((_, i) => { + return i + }) + // console.log(JSON.stringify(messageIDs)) + for (let i = 0; i < saveData.length; i++) { + const message = saveData[i]; + root.messageByID[i] = root.aiMessageComponent.createObject(root, { + "role": message.role, + "rawContent": message.rawContent, + "content": message.rawContent, + "model": message.model, + "thinking": message.thinking, + "done": message.done, + "annotations": message.annotations, + "annotationSources": message.annotationSources, + "functionName": message.functionName, + "functionCall": message.functionCall, + "functionResponse": message.functionResponse, + "visibleToUser": message.visibleToUser, + }); + } + } catch (e) { + console.log("[AI] Could not load chat: ", e); + } finally { + getSavedChats.running = true; + } + } } diff --git a/.config/quickshell/services/AiMessageData.qml b/.config/quickshell/ii/services/AiMessageData.qml similarity index 89% rename from .config/quickshell/services/AiMessageData.qml rename to .config/quickshell/ii/services/AiMessageData.qml index b5f20854..c9537abe 100644 --- a/.config/quickshell/services/AiMessageData.qml +++ b/.config/quickshell/ii/services/AiMessageData.qml @@ -1,4 +1,4 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick; /** @@ -7,6 +7,7 @@ import QtQuick; QtObject { property string role property string content + property string rawContent property string model property bool thinking: true property bool done: false diff --git a/.config/quickshell/services/AppSearch.qml b/.config/quickshell/ii/services/AppSearch.qml similarity index 57% rename from .config/quickshell/services/AppSearch.qml rename to .config/quickshell/ii/services/AppSearch.qml index 876df183..d6a7008d 100644 --- a/.config/quickshell/services/AppSearch.qml +++ b/.config/quickshell/ii/services/AppSearch.qml @@ -1,10 +1,9 @@ pragma Singleton -import "root:/modules/common" +import qs.modules.common import "root:/modules/common/functions/fuzzysort.js" as Fuzzy import "root:/modules/common/functions/levendist.js" as Levendist import Quickshell -import Quickshell.Io /** * - Eases fuzzy searching for applications by name @@ -12,7 +11,7 @@ import Quickshell.Io */ Singleton { id: root - property bool sloppySearch: ConfigOptions?.search.sloppy ?? false + property bool sloppySearch: Config.options?.search.sloppy ?? false property real scoreThreshold: 0.2 property var substitutions: ({ "code-url-handler": "visual-studio-code", @@ -23,10 +22,11 @@ Singleton { "wpsoffice": "wps-office2019-kprometheus", "footclient": "foot", "zen": "zen-browser", + "brave-browser": "brave-desktop" }) property var regexSubstitutions: [ { - "regex": /^steam_app_(\\d+)$/, + "regex": /^steam_app_(\d+)$/, "replace": "steam_icon_$1" }, { @@ -47,9 +47,14 @@ Singleton { .sort((a, b) => a.name.localeCompare(b.name)) readonly property var preppedNames: list.map(a => ({ - name: Fuzzy.prepare(`${a.name} `), - entry: a - })) + name: Fuzzy.prepare(`${a.name} `), + entry: a + })) + + readonly property var preppedIcons: list.map(a => ({ + name: Fuzzy.prepare(`${a.icon} `), + entry: a + })) function fuzzyQuery(search: string): var { // Idk why list doesn't work if (root.sloppySearch) { @@ -71,16 +76,25 @@ Singleton { } function iconExists(iconName) { + if (!iconName || iconName.length == 0) return false; return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing"); } + function getReverseDomainNameAppName(str) { + return str.split('.').slice(-1)[0] + } + + function getKebabNormalizedAppName(str) { + return str.toLowerCase().replace(/\s+/g, "-"); + } + function guessIcon(str) { if (!str || str.length == 0) return "image-missing"; // Normal substitutions - if (substitutions[str]) - return substitutions[str]; + if (substitutions[str]) return substitutions[str]; + if (substitutions[str.toLowerCase()]) return substitutions[str.toLowerCase()]; // Regex substitutions for (let i = 0; i < regexSubstitutions.length; i++) { @@ -92,24 +106,43 @@ Singleton { if (replacedName != str) return replacedName; } - // If it gets detected normally, no need to guess + // Icon exists -> return as is if (iconExists(str)) return str; - let guessStr = str; - // Guess: Take only app name of reverse domain name notation - guessStr = str.split('.').slice(-1)[0].toLowerCase(); - if (iconExists(guessStr)) return guessStr; - // Guess: normalize to kebab case - guessStr = str.toLowerCase().replace(/\s+/g, "-"); - if (iconExists(guessStr)) return guessStr; - // Guess: First fuzze desktop entry match - const searchResults = root.fuzzyQuery(str); - if (searchResults.length > 0) { - const firstEntry = searchResults[0]; - guessStr = firstEntry.icon - if (iconExists(guessStr)) return guessStr; + + // Simple guesses + const lowercased = str.toLowerCase(); + if (iconExists(lowercased)) return lowercased; + + const reverseDomainNameAppName = getReverseDomainNameAppName(str); + if (iconExists(reverseDomainNameAppName)) return reverseDomainNameAppName; + + const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); + if (iconExists(lowercasedDomainNameAppName)) return lowercasedDomainNameAppName; + + const kebabNormalizedGuess = getKebabNormalizedAppName(str); + if (iconExists(kebabNormalizedGuess)) return kebabNormalizedGuess; + + + // Search in desktop entries + const iconSearchResults = Fuzzy.go(str, preppedIcons, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry + }); + if (iconSearchResults.length > 0) { + const guess = iconSearchResults[0].icon + if (iconExists(guess)) return guess; } + const nameSearchResults = root.fuzzyQuery(str); + if (nameSearchResults.length > 0) { + const guess = nameSearchResults[0].icon + if (iconExists(guess)) return guess; + } + + // Give up return str; } diff --git a/.config/quickshell/services/Audio.qml b/.config/quickshell/ii/services/Audio.qml similarity index 85% rename from .config/quickshell/services/Audio.qml rename to .config/quickshell/ii/services/Audio.qml index c0f469a4..0651ebc7 100644 --- a/.config/quickshell/services/Audio.qml +++ b/.config/quickshell/ii/services/Audio.qml @@ -1,4 +1,4 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick import Quickshell import Quickshell.Services.Pipewire @@ -26,15 +26,15 @@ Singleton { property bool lastReady: false property real lastVolume: 0 function onVolumeChanged() { - if (!ConfigOptions.audio.protection.enable) return; + if (!Config.options.audio.protection.enable) return; if (!lastReady) { lastVolume = sink.audio.volume; lastReady = true; return; } const newVolume = sink.audio.volume; - const maxAllowedIncrease = ConfigOptions.audio.protection.maxAllowedIncrease / 100; - const maxAllowed = ConfigOptions.audio.protection.maxAllowed / 100; + const maxAllowedIncrease = Config.options.audio.protection.maxAllowedIncrease / 100; + const maxAllowed = Config.options.audio.protection.maxAllowed / 100; if (newVolume - lastVolume > maxAllowedIncrease) { sink.audio.volume = lastVolume; diff --git a/.config/quickshell/ii/services/Battery.qml b/.config/quickshell/ii/services/Battery.qml new file mode 100644 index 00000000..8a3cf316 --- /dev/null +++ b/.config/quickshell/ii/services/Battery.qml @@ -0,0 +1,50 @@ +pragma Singleton + +import qs +import qs.modules.common +import Quickshell +import Quickshell.Services.UPower + +Singleton { + property bool available: UPower.displayDevice.isLaptopBattery + property var chargeState: UPower.displayDevice.state + property bool isCharging: chargeState == UPowerDeviceState.Charging + property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge + property real percentage: UPower.displayDevice.percentage + readonly property bool allowAutomaticSuspend: Config.options.battery.automaticSuspend + + property bool isLow: percentage <= Config.options.battery.low / 100 + property bool isCritical: percentage <= Config.options.battery.critical / 100 + property bool isSuspending: percentage <= Config.options.battery.suspend / 100 + + property bool isLowAndNotCharging: isLow && !isCharging + property bool isCriticalAndNotCharging: isCritical && !isCharging + property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging + + onIsLowAndNotChargingChanged: { + if (available && isLowAndNotCharging) Quickshell.execDetached([ + "notify-send", + Translation.tr("Low battery"), + Translation.tr("Consider plugging in your device"), + "-u", "critical", + "-a", "Shell" + ]) + } + + onIsCriticalAndNotChargingChanged: { + if (available && isCriticalAndNotCharging) Quickshell.execDetached([ + "notify-send", + Translation.tr("Critically low battery"), + Translation.tr("Please charge!\nAutomatic suspend triggers at %1").arg(Config.options.battery.suspend), + "-u", "critical", + "-a", "Shell" + ]); + + } + + onIsSuspendingAndNotChargingChanged: { + if (available && isSuspendingAndNotCharging) { + Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]); + } + } +} diff --git a/.config/quickshell/services/Bluetooth.qml b/.config/quickshell/ii/services/Bluetooth.qml similarity index 100% rename from .config/quickshell/services/Bluetooth.qml rename to .config/quickshell/ii/services/Bluetooth.qml diff --git a/.config/quickshell/services/Booru.qml b/.config/quickshell/ii/services/Booru.qml similarity index 88% rename from .config/quickshell/services/Booru.qml rename to .config/quickshell/ii/services/Booru.qml index 49256bfa..e2a9d195 100644 --- a/.config/quickshell/services/Booru.qml +++ b/.config/quickshell/ii/services/Booru.qml @@ -1,10 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" +import qs.modules.common +import qs import Quickshell; -import Quickshell.Io; -import Qt.labs.platform import QtQuick; /** @@ -16,18 +15,18 @@ Singleton { signal tagSuggestion(string query, var suggestions) - property string failMessage: qsTr("That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number") + property string failMessage: Translation.tr("That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number") property var responses: [] property int runningRequests: 0 - property var defaultUserAgent: ConfigOptions?.networking?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + property var defaultUserAgent: Config.options?.networking?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" property var providerList: Object.keys(providers).filter(provider => provider !== "system" && providers[provider].api) property var providers: { - "system": { "name": qsTr("System") }, + "system": { "name": Translation.tr("System") }, "yandere": { "name": "yande.re", "url": "https://yande.re", "api": "https://yande.re/post.json", - "description": qsTr("All-rounder | Good quality, decent quantity"), + "description": Translation.tr("All-rounder | Good quality, decent quantity"), "mapFunc": (response) => { return response.map(item => { return { @@ -59,9 +58,9 @@ Singleton { }, "konachan": { "name": "Konachan", - "url": "https://konachan.com", - "api": "https://konachan.com/post.json", - "description": qsTr("For desktop wallpapers | Good quality"), + "url": "https://konachan.net", + "api": "https://konachan.net/post.json", + "description": Translation.tr("For desktop wallpapers | Good quality"), "mapFunc": (response) => { return response.map(item => { return { @@ -81,7 +80,7 @@ Singleton { } }) }, - "tagSearchTemplate": "https://konachan.com/tag.json?order=count&name={{query}}*", + "tagSearchTemplate": "https://konachan.net/tag.json?order=count&name={{query}}*", "tagMapFunc": (response) => { return response.map(item => { return { @@ -95,7 +94,7 @@ Singleton { "name": "Zerochan", "url": "https://www.zerochan.net", "api": "https://www.zerochan.net/?json", - "description": qsTr("Clean stuff | Excellent quality, no NSFW"), + "description": Translation.tr("Clean stuff | Excellent quality, no NSFW"), "mapFunc": (response) => { response = response.items return response.map(item => { @@ -122,7 +121,7 @@ Singleton { "name": "Danbooru", "url": "https://danbooru.donmai.us", "api": "https://danbooru.donmai.us/posts.json", - "description": qsTr("The popular one | Best quantity, but quality can vary wildly"), + "description": Translation.tr("The popular one | Best quantity, but quality can vary wildly"), "mapFunc": (response) => { return response.map(item => { return { @@ -157,7 +156,7 @@ Singleton { "name": "Gelbooru", "url": "https://gelbooru.com", "api": "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1", - "description": qsTr("The hentai one | Great quantity, a lot of NSFW, quality varies wildly"), + "description": Translation.tr("The hentai one | Great quantity, a lot of NSFW, quality varies wildly"), "mapFunc": (response) => { response = response.post return response.map(item => { @@ -192,7 +191,7 @@ Singleton { "name": "waifu.im", "url": "https://waifu.im", "api": "https://api.waifu.im/search", - "description": qsTr("Waifus only | Excellent quality, limited quantity"), + "description": Translation.tr("Waifus only | Excellent quality, limited quantity"), "mapFunc": (response) => { response = response.images return response.map(item => { @@ -223,7 +222,7 @@ Singleton { "name": "Alcy", "url": "https://t.alcy.cc", "api": "https://t.alcy.cc/", - "description": qsTr("Large images | God tier quality, no NSFW."), + "description": Translation.tr("Large images | God tier quality, no NSFW."), "fixedTags": [ { "name": "ycy", @@ -274,7 +273,7 @@ Singleton { }, } } - property var currentProvider: PersistentStates.booru.provider + property var currentProvider: Persistent.states.booru.provider function getWorkingImageSource(url) { if (url.includes('pximg.net')) { @@ -286,11 +285,11 @@ Singleton { function setProvider(provider) { provider = provider.toLowerCase() if (providerList.indexOf(provider) !== -1) { - PersistentStateManager.setState("booru.provider", provider) - root.addSystemMessage(qsTr("Provider set to ") + providers[provider].name - + (provider == "zerochan" ? qsTr(". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!") : "")) + Persistent.states.booru.provider = provider + root.addSystemMessage(Translation.tr("Provider set to ") + providers[provider].name + + (provider == "zerochan" ? Translation.tr(". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!") : "")) } else { - root.addSystemMessage(qsTr("Invalid API provider. Supported: \n- ") + providerList.join("\n- ")) + root.addSystemMessage(Translation.tr("Invalid API provider. Supported: \n- ") + providerList.join("\n- ")) } } @@ -361,7 +360,7 @@ Singleton { function makeRequest(tags, nsfw=false, limit=20, page=1) { var url = constructRequestUrl(tags, nsfw, limit, page) - // console.log("[Booru] Making request to " + url) + console.log("[Booru] Making request to " + url) const newResponse = root.booruResponseDataComponent.createObject(null, { "provider": currentProvider, @@ -408,7 +407,7 @@ Singleton { xhr.setRequestHeader("User-Agent", defaultUserAgent) } else if (currentProvider == "zerochan") { - const userAgent = ConfigOptions?.sidebar?.booru?.zerochan?.username ? `Desktop sidebar booru viewer - username: ${ConfigOptions.sidebar.booru.zerochan.username}` : defaultUserAgent + const userAgent = Config.options?.sidebar?.booru?.zerochan?.username ? `Desktop sidebar booru viewer - username: ${Config.options.sidebar.booru.zerochan.username}` : defaultUserAgent xhr.setRequestHeader("User-Agent", userAgent) } root.runningRequests++; diff --git a/.config/quickshell/services/BooruResponseData.qml b/.config/quickshell/ii/services/BooruResponseData.qml similarity index 85% rename from .config/quickshell/services/BooruResponseData.qml rename to .config/quickshell/ii/services/BooruResponseData.qml index 38e1b8c7..2a61ff6c 100644 --- a/.config/quickshell/services/BooruResponseData.qml +++ b/.config/quickshell/ii/services/BooruResponseData.qml @@ -1,4 +1,4 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick; /** diff --git a/.config/quickshell/services/Brightness.qml b/.config/quickshell/ii/services/Brightness.qml similarity index 97% rename from .config/quickshell/services/Brightness.qml rename to .config/quickshell/ii/services/Brightness.qml index aba664aa..927a10c0 100644 --- a/.config/quickshell/services/Brightness.qml +++ b/.config/quickshell/ii/services/Brightness.qml @@ -140,13 +140,13 @@ Singleton { GlobalShortcut { name: "brightnessIncrease" - description: qsTr("Increase brightness") + description: "Increase brightness" onPressed: root.increaseBrightness() } GlobalShortcut { name: "brightnessDecrease" - description: qsTr("Decrease brightness") + description: "Decrease brightness" onPressed: root.decreaseBrightness() } } diff --git a/.config/quickshell/services/Cliphist.qml b/.config/quickshell/ii/services/Cliphist.qml similarity index 69% rename from .config/quickshell/services/Cliphist.qml rename to .config/quickshell/ii/services/Cliphist.qml index bebafb10..0fe7ad71 100644 --- a/.config/quickshell/services/Cliphist.qml +++ b/.config/quickshell/ii/services/Cliphist.qml @@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound import "root:/modules/common/functions/fuzzysort.js" as Fuzzy import "root:/modules/common/functions/levendist.js" as Levendist -import "root:/modules/common" -import "root:/" +import qs.modules.common +import qs.modules.common.functions import QtQuick import Quickshell import Quickshell.Io Singleton { id: root - property bool sloppySearch: ConfigOptions?.search.sloppy ?? false + property bool sloppySearch: Config.options?.search.sloppy ?? false property real scoreThreshold: 0.2 property list entries: [] readonly property var preparedEntries: entries.map(a => ({ @@ -42,6 +42,28 @@ Singleton { readProc.running = true } + function copy(entry) { + Quickshell.execDetached(["bash", "-c", `echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`]); + } + + Process { + id: deleteProc + property string entry: "" + command: ["bash", "-c", `echo '${StringUtils.shellSingleQuoteEscape(deleteProc.entry)}' | cliphist delete`] + function deleteEntry(entry) { + deleteProc.entry = entry; + deleteProc.running = true; + deleteProc.entry = ""; + } + onExited: (exitCode, exitStatus) => { + root.refresh(); + } + } + + function deleteEntry(entry) { + deleteProc.deleteEntry(entry); + } + Connections { target: Quickshell function onClipboardTextChanged() { @@ -51,7 +73,7 @@ Singleton { Timer { id: delayedUpdateTimer - interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + interval: Config.options.hacks.arbitraryRaceConditionDelay repeat: false onTriggered: { root.refresh() diff --git a/.config/quickshell/services/DateTime.qml b/.config/quickshell/ii/services/DateTime.qml similarity index 86% rename from .config/quickshell/services/DateTime.qml rename to .config/quickshell/ii/services/DateTime.qml index 17fb1c8a..16dc6c4e 100644 --- a/.config/quickshell/services/DateTime.qml +++ b/.config/quickshell/ii/services/DateTime.qml @@ -1,4 +1,4 @@ -import "root:/modules/common" +import qs.modules.common import QtQuick import Quickshell import Quickshell.Io @@ -9,15 +9,14 @@ pragma ComponentBehavior: Bound * A nice wrapper for date and time strings. */ Singleton { - property string time: Qt.locale().toString(clock.date, ConfigOptions?.time.format ?? "hh:mm") - property string date: Qt.locale().toString(clock.date, ConfigOptions?.time.dateFormat ?? "dddd, dd/MM") - property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dd MMMM yyyy") - property string uptime: "0h, 0m" - - SystemClock { + property var clock: SystemClock { id: clock precision: SystemClock.Minutes } + property string time: Qt.locale().toString(clock.date, Config.options?.time.format ?? "hh:mm") + property string date: Qt.locale().toString(clock.date, Config.options?.time.dateFormat ?? "dddd, dd/MM") + property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dd MMMM yyyy") + property string uptime: "0h, 0m" Timer { interval: 10 @@ -39,7 +38,7 @@ Singleton { if (hours > 0) formatted += `${formatted ? ", " : ""}${hours}h` if (minutes > 0 || !formatted) formatted += `${formatted ? ", " : ""}${minutes}m` uptime = formatted - interval = ConfigOptions?.resources?.updateInterval ?? 3000 + interval = Config.options?.resources?.updateInterval ?? 3000 } } diff --git a/.config/quickshell/services/Emojis.qml b/.config/quickshell/ii/services/Emojis.qml similarity index 98% rename from .config/quickshell/services/Emojis.qml rename to .config/quickshell/ii/services/Emojis.qml index 852c831b..4220b040 100644 --- a/.config/quickshell/services/Emojis.qml +++ b/.config/quickshell/ii/services/Emojis.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import "root:/modules/common/functions/fuzzysort.js" as Fuzzy import "root:/modules/common/functions/levendist.js" as Levendist -import "root:/modules/common" +import qs.modules.common import QtQuick import Quickshell import Quickshell.Io diff --git a/.config/quickshell/services/FirstRunExperience.qml b/.config/quickshell/ii/services/FirstRunExperience.qml similarity index 58% rename from .config/quickshell/services/FirstRunExperience.qml rename to .config/quickshell/ii/services/FirstRunExperience.qml index 86ccb98b..f23cce58 100644 --- a/.config/quickshell/services/FirstRunExperience.qml +++ b/.config/quickshell/ii/services/FirstRunExperience.qml @@ -1,10 +1,9 @@ pragma Singleton -import "root:/modules/common/functions/file_utils.js" as FileUtils -import "root:/modules/common" +import qs.modules.common +import qs.modules.common.functions import Quickshell import Quickshell.Io -import Quickshell.Hyprland Singleton { id: root @@ -12,23 +11,23 @@ Singleton { property string firstRunFileContent: "This file is just here to confirm you've been greeted :>" property string firstRunNotifSummary: "Welcome!" property string firstRunNotifBody: "Hit Super+/ for a list of keybinds" - property string defaultWallpaperPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/assets/images/default_wallpaper.png`) - property string welcomeQmlPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/welcome.qml`) + property string defaultWallpaperPath: FileUtils.trimFileProtocol(`${Directories.assetsPath}/images/default_wallpaper.png`) + property string welcomeQmlPath: FileUtils.trimFileProtocol(Quickshell.shellPath("welcome.qml")) function load() { firstRunFileView.reload() } function enableNextTime() { - Hyprland.dispatch(`exec rm -f '${root.firstRunFilePath}'`) + Quickshell.execDetached(["rm", "-f", root.firstRunFilePath]) } function disableNextTime() { - Hyprland.dispatch(`exec echo '${root.firstRunFileContent}' > '${root.firstRunFilePath}'`) + Quickshell.execDetached(["bash", "-c", `echo '${root.firstRunFileContent}' > '${root.firstRunFilePath}'`]) } function handleFirstRun() { - Hyprland.dispatch(`exec swww query | grep 'image' || '${Directories.wallpaperSwitchScriptPath}' '${root.defaultWallpaperPath}'`) - Hyprland.dispatch(`exec qs -p '${root.welcomeQmlPath}'`) + Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, root.defaultWallpaperPath]) + Quickshell.execDetached(["bash", "-c", `qs -p '${root.welcomeQmlPath}'`]) } FileView { diff --git a/.config/quickshell/ii/services/HyprlandData.qml b/.config/quickshell/ii/services/HyprlandData.qml new file mode 100644 index 00000000..6488ded8 --- /dev/null +++ b/.config/quickshell/ii/services/HyprlandData.qml @@ -0,0 +1,133 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +/** + * Provides access to some Hyprland data not available in Quickshell.Hyprland. + */ +Singleton { + id: root + property var windowList: [] + property var addresses: [] + property var windowByAddress: ({}) + property var workspaces: [] + property var workspaceIds: [] + property var workspaceById: ({}) + property var activeWorkspace: null + property var monitors: [] + property var layers: ({}) + + function updateWindowList() { + getClients.running = true; + } + + function updateLayers() { + getLayers.running = true; + } + + function updateMonitors() { + getMonitors.running = true; + } + + function updateWorkspaces() { + getWorkspaces.running = true; + getActiveWorkspace.running = true; + } + + function updateAll() { + updateWindowList(); + updateMonitors(); + updateLayers(); + updateWorkspaces(); + } + + function biggestWindowForWorkspace(workspaceId) { + const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == workspaceId); + return windowsInThisWorkspace.reduce((maxWin, win) => { + const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0); + const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0); + return winArea > maxArea ? win : maxWin; + }, null); + } + + Component.onCompleted: { + updateAll(); + } + + Connections { + target: Hyprland + + function onRawEvent(event) { + // console.log("Hyprland raw event:", event.name); + updateAll() + } + } + + Process { + id: getClients + command: ["bash", "-c", "hyprctl clients -j | jq -c"] + stdout: SplitParser { + onRead: data => { + root.windowList = JSON.parse(data); + let tempWinByAddress = {}; + for (var i = 0; i < root.windowList.length; ++i) { + var win = root.windowList[i]; + tempWinByAddress[win.address] = win; + } + root.windowByAddress = tempWinByAddress; + root.addresses = root.windowList.map(win => win.address); + } + } + } + + Process { + id: getMonitors + command: ["bash", "-c", "hyprctl monitors -j | jq -c"] + stdout: SplitParser { + onRead: data => { + root.monitors = JSON.parse(data); + } + } + } + + Process { + id: getLayers + command: ["bash", "-c", "hyprctl layers -j | jq -c"] + stdout: SplitParser { + onRead: data => { + root.layers = JSON.parse(data); + } + } + } + + Process { + id: getWorkspaces + command: ["bash", "-c", "hyprctl workspaces -j | jq -c"] + stdout: SplitParser { + onRead: data => { + root.workspaces = JSON.parse(data); + let tempWorkspaceById = {}; + for (var i = 0; i < root.workspaces.length; ++i) { + var ws = root.workspaces[i]; + tempWorkspaceById[ws.id] = ws; + } + root.workspaceById = tempWorkspaceById; + root.workspaceIds = root.workspaces.map(ws => ws.id); + } + } + } + + Process { + id: getActiveWorkspace + command: ["bash", "-c", "hyprctl activeworkspace -j | jq -c"] + stdout: SplitParser { + onRead: data => { + root.activeWorkspace = JSON.parse(data); + } + } + } +} diff --git a/.config/quickshell/services/HyprlandKeybinds.qml b/.config/quickshell/ii/services/HyprlandKeybinds.qml similarity index 90% rename from .config/quickshell/services/HyprlandKeybinds.qml rename to .config/quickshell/ii/services/HyprlandKeybinds.qml index 189ba76d..3381926b 100644 --- a/.config/quickshell/services/HyprlandKeybinds.qml +++ b/.config/quickshell/ii/services/HyprlandKeybinds.qml @@ -1,12 +1,11 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" -import "root:/modules/common/functions/file_utils.js" as FileUtils +import qs.modules.common +import qs.modules.common.functions import QtQuick import Quickshell import Quickshell.Io -import Quickshell.Wayland import Quickshell.Hyprland /** @@ -15,7 +14,7 @@ import Quickshell.Hyprland */ Singleton { id: root - property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/hyprland/get_keybinds.py`) + property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/hyprland/get_keybinds.py`) property string defaultKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/keybinds.conf`) property string userKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/custom/keybinds.conf`) property var defaultKeybinds: {"children": []} @@ -41,7 +40,7 @@ Singleton { Process { id: getDefaultKeybinds running: true - command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,] + command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath] stdout: SplitParser { onRead: data => { diff --git a/.config/quickshell/services/HyprlandXkb.qml b/.config/quickshell/ii/services/HyprlandXkb.qml similarity index 100% rename from .config/quickshell/services/HyprlandXkb.qml rename to .config/quickshell/ii/services/HyprlandXkb.qml diff --git a/.config/quickshell/ii/services/Hyprsunset.qml b/.config/quickshell/ii/services/Hyprsunset.qml new file mode 100644 index 00000000..d6def43c --- /dev/null +++ b/.config/quickshell/ii/services/Hyprsunset.qml @@ -0,0 +1,117 @@ +pragma Singleton + +import QtQuick +import qs.modules.common +import Quickshell +import Quickshell.Io + +/** + * Simple hyprsunset service with automatic mode. + * In theory we don't need this because hyprsunset has a config file, but it somehow doesn't work. + * It should also be possible to control it via hyprctl, but it doesn't work consistently either so we're just killing and launching. + */ +Singleton { + id: root + property var manualActive + property string from: Config.options?.light?.night?.from ?? "19:00" // Default to 7 PM + property string to: Config.options?.light?.night?.to ?? "06:30" // Default to 6:30 AM + property bool automatic: Config.options?.light?.night?.automatic && (Config?.ready ?? true) + property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000 // Default color temperature + property bool shouldBeOn + property bool firstEvaluation: true + property bool active: false + + property int fromHour: Number(from.split(":")[0]) + property int fromMinute: Number(from.split(":")[1]) + property int toHour: Number(to.split(":")[0]) + property int toMinute: Number(to.split(":")[1]) + + property int clockHour: DateTime.clock.hours + property int clockMinute: DateTime.clock.minutes + + + function isNoLater(hour1, minute1, hour2, minute2) { + if (hour1 < hour2) + return true; + if (hour1 === hour2 && minute1 < minute2) + return true; + return false; + } + + + onClockMinuteChanged: reEvaluate() + onAutomaticChanged: { + root.manualActive = undefined; + root.firstEvaluation = true; + reEvaluate(); + } + function reEvaluate() { + const toHourIsNextDay = !isNoLater(fromHour, fromMinute, toHour, toMinute); + const toHourWrapped = toHourIsNextDay ? toHour + 24 : toHour; + const toMinuteWrapped = toMinute; + root.shouldBeOn = isNoLater(fromHour, fromMinute, clockHour, clockMinute) && isNoLater(clockHour, clockMinute, toHourWrapped, toMinuteWrapped); + if (firstEvaluation) { + firstEvaluation = false; + root.ensureState(); + } + } + + onShouldBeOnChanged: ensureState() + function ensureState() { + // console.log("[Hyprsunset] Ensuring state:", root.shouldBeOn, "Automatic mode:", root.automatic); + if (!root.automatic || root.manualActive !== undefined) + return; + if (root.shouldBeOn) { + root.enable(); + } else { + root.disable(); + } + } + + function load() { } // Dummy to force init + + function enable() { + root.active = true; + // console.log("[Hyprsunset] Enabling"); + Quickshell.execDetached(["bash", "-c", `pidof hyprsunset || hyprsunset --temperature ${root.colorTemperature}`]); + } + + function disable() { + root.active = false; + // console.log("[Hyprsunset] Disabling"); + Quickshell.execDetached(["bash", "-c", `pkill hyprsunset`]); + } + + function fetchState() { + fetchProc.running = true; + } + + Process { + id: fetchProc + running: true + command: ["bash", "-c", "hyprctl hyprsunset temperature"] + stdout: StdioCollector { + id: stateCollector + onStreamFinished: { + const output = stateCollector.text.trim(); + if (output.length == 0 || output.startsWith("Couldn't")) + root.active = false; + else + root.active = (output != "6500"); + // console.log("[Hyprsunset] Fetched state:", output, "->", root.active); + } + } + } + + function toggle() { + if (root.manualActive === undefined) + root.manualActive = root.active; + + root.manualActive = !root.manualActive; + if (root.manualActive) { + root.enable(); + } else { + root.disable(); + } + } +} diff --git a/.config/quickshell/services/KeyringStorage.qml b/.config/quickshell/ii/services/KeyringStorage.qml similarity index 92% rename from .config/quickshell/services/KeyringStorage.qml rename to .config/quickshell/ii/services/KeyringStorage.qml index 3f3956f9..ce6b8eb5 100644 --- a/.config/quickshell/services/KeyringStorage.qml +++ b/.config/quickshell/ii/services/KeyringStorage.qml @@ -1,11 +1,10 @@ pragma Singleton pragma ComponentBehavior: Bound - -import "root:/modules/common" -import "root:/modules/common/functions/string_utils.js" as StringUtils +import qs +import qs.modules.common +import qs.modules.common.functions import Quickshell; import Quickshell.Io; -import Qt.labs.platform import QtQuick; /** @@ -20,14 +19,14 @@ Singleton { property var properties: { "application": "illogical-impulse", - "explanation": qsTr("For storing API keys and other sensitive information"), + "explanation": Translation.tr("For storing API keys and other sensitive information"), } property var propertiesAsArgs: Object.keys(root.properties).reduce( function(arr, key) { return arr.concat([key, root.properties[key]]); }, [] ) - property string keyringLabel: StringUtils.format(qsTr("{0} Safe Storage"), "illogical-impulse") + property string keyringLabel: Translation.tr("%1 Safe Storage").arg("illogical-impulse") function setNestedField(path, value) { if (!root.keyringData) root.keyringData = {}; diff --git a/.config/quickshell/services/LatexRenderer.qml b/.config/quickshell/ii/services/LatexRenderer.qml similarity index 93% rename from .config/quickshell/services/LatexRenderer.qml rename to .config/quickshell/ii/services/LatexRenderer.qml index e7066fa4..5baf336e 100644 --- a/.config/quickshell/services/LatexRenderer.qml +++ b/.config/quickshell/ii/services/LatexRenderer.qml @@ -1,14 +1,10 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/file_utils.js" as FileUtils -import "root:/modules/common" +import qs.modules.common.functions +import qs.modules.common import QtQuick import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import Qt.labs.platform /** * Renders LaTeX snippets with MicroTeX. diff --git a/.config/quickshell/services/MaterialThemeLoader.qml b/.config/quickshell/ii/services/MaterialThemeLoader.qml similarity index 93% rename from .config/quickshell/services/MaterialThemeLoader.qml rename to .config/quickshell/ii/services/MaterialThemeLoader.qml index cd4eb686..8872c476 100644 --- a/.config/quickshell/services/MaterialThemeLoader.qml +++ b/.config/quickshell/ii/services/MaterialThemeLoader.qml @@ -1,7 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" +import qs.modules.common import QtQuick import Quickshell import Quickshell.Io @@ -34,7 +34,7 @@ Singleton { Timer { id: delayedFileRead - interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100 + interval: Config.options?.hacks?.arbitraryRaceConditionDelay ?? 100 repeat: false running: false onTriggered: { diff --git a/.config/quickshell/services/MprisController.qml b/.config/quickshell/ii/services/MprisController.qml similarity index 94% rename from .config/quickshell/services/MprisController.qml rename to .config/quickshell/ii/services/MprisController.qml index 96aa5e80..60923a6e 100644 --- a/.config/quickshell/services/MprisController.qml +++ b/.config/quickshell/ii/services/MprisController.qml @@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound // From https://git.outfoxxed.me/outfoxxed/nixnew // It does not have a license, but the author is okay with redistribution. +import qs import QtQml.Models import QtQuick import Quickshell @@ -85,9 +86,9 @@ Singleton { this.activeTrack = { uniqueId: this.activePlayer?.uniqueId ?? 0, artUrl: this.activePlayer?.trackArtUrl ?? "", - title: this.activePlayer?.trackTitle || qsTr("Unknown Title"), - artist: this.activePlayer?.trackArtist || qsTr("Unknown Artist"), - album: this.activePlayer?.trackAlbum || qsTr("Unknown Album"), + title: this.activePlayer?.trackTitle || Translation.tr("Unknown Title"), + artist: this.activePlayer?.trackArtist || Translation.tr("Unknown Artist"), + album: this.activePlayer?.trackAlbum || Translation.tr("Unknown Album"), }; this.trackChanged(__reverse); diff --git a/.config/quickshell/services/Network.qml b/.config/quickshell/ii/services/Network.qml similarity index 100% rename from .config/quickshell/services/Network.qml rename to .config/quickshell/ii/services/Network.qml diff --git a/.config/quickshell/services/Notifications.qml b/.config/quickshell/ii/services/Notifications.qml similarity index 80% rename from .config/quickshell/services/Notifications.qml rename to .config/quickshell/ii/services/Notifications.qml index dc2d2206..446abeff 100644 --- a/.config/quickshell/services/Notifications.qml +++ b/.config/quickshell/ii/services/Notifications.qml @@ -1,8 +1,8 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" -import "root:/" +import qs.modules.common +import qs import QtQuick import Quickshell import Quickshell.Io @@ -17,7 +17,8 @@ import Quickshell.Services.Notifications Singleton { id: root component Notif: QtObject { - required property int id + id: wrapper + required property int notificationId // Could just be `id` but it conflicts with the default prop in QtObject property Notification notification property list actions: notification?.actions.map((action) => ({ "identifier": action.identifier, @@ -32,11 +33,19 @@ Singleton { property double time property string urgency: notification?.urgency.toString() ?? "normal" property Timer timer + + readonly property Connections conn: Connections { + target: wrapper?.notification?.Component ?? root // stupid warning aaaaaaa + + function onDestruction(): void { + wrapper.destroy(); + } + } } function notifToJSON(notif) { return { - "id": notif.id, + "notificationId": notif.notificationId, "actions": notif.actions, "appIcon": notif.appIcon, "appName": notif.appName, @@ -52,11 +61,11 @@ Singleton { } component NotifTimer: Timer { - required property int id + required property int notificationId interval: 5000 running: true onTriggered: () => { - root.timeoutNotification(id); + root.timeoutNotification(notificationId); destroy() } } @@ -130,7 +139,7 @@ Singleton { property int idOffset signal initDone(); signal notify(notification: var); - signal discard(id: var); + signal discard(id: int); signal discardAll(); signal timeout(id: var); @@ -149,7 +158,7 @@ Singleton { onNotification: (notification) => { notification.tracked = true const newNotifObject = notifComponent.createObject(root, { - "id": notification.id + root.idOffset, + "notificationId": notification.id + root.idOffset, "notification": notification, "time": Date.now(), }); @@ -158,10 +167,12 @@ Singleton { // Popup if (!root.popupInhibited) { newNotifObject.popup = true; - newNotifObject.timer = notifTimerComponent.createObject(root, { - "id": newNotifObject.id, - "interval": notification.expireTimeout < 0 ? 5000 : notification.expireTimeout, - }); + if (notification.expireTimeout != 0) { + newNotifObject.timer = notifTimerComponent.createObject(root, { + "notificationId": newNotifObject.notificationId, + "interval": notification.expireTimeout < 0 ? 5000 : notification.expireTimeout, + }); + } } root.notify(newNotifObject); @@ -171,7 +182,8 @@ Singleton { } function discardNotification(id) { - const index = root.list.findIndex((notif) => notif.id === id); + console.log("[Notifications] Discarding notification with ID: " + id); + const index = root.list.findIndex((notif) => notif.notificationId === id); const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id); if (index !== -1) { root.list.splice(index, 1); @@ -181,7 +193,7 @@ Singleton { if (notifServerIndex !== -1) { notifServer.trackedNotifications.values[notifServerIndex].dismiss() } - root.discard(id); + root.discard(id); // Emit signal } function discardAllNotifications() { @@ -195,7 +207,7 @@ Singleton { } function timeoutNotification(id) { - const index = root.list.findIndex((notif) => notif.id === id); + const index = root.list.findIndex((notif) => notif.notificationId === id); if (root.list[index] != null) root.list[index].popup = false; root.timeout(id); @@ -203,7 +215,7 @@ Singleton { function timeoutAll() { root.popupList.forEach((notif) => { - root.timeout(notif.id); + root.timeout(notif.notificationId); }) root.popupList.forEach((notif) => { notif.popup = false; @@ -211,16 +223,19 @@ Singleton { } function attemptInvokeAction(id, notifIdentifier) { + console.log("[Notifications] Attempting to invoke action with identifier: " + notifIdentifier + " for notification ID: " + id); const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id); + console.log("Notification server index: " + notifServerIndex); if (notifServerIndex !== -1) { const notifServerNotif = notifServer.trackedNotifications.values[notifServerIndex]; const action = notifServerNotif.actions.find((action) => action.identifier === notifIdentifier); + console.log("Action found: " + JSON.stringify(action)); action.invoke() } else { console.log("Notification not found in server: " + id) - root.discardNotification(id); } + root.discardNotification(id); } function triggerListChange() { @@ -242,8 +257,8 @@ Singleton { const fileContents = notifFileView.text() root.list = JSON.parse(fileContents).map((notif) => { return notifComponent.createObject(root, { - "id": notif.id, - "actions": notif.actions, + "notificationId": notif.notificationId, + "actions": [], // Notification actions are meaningless if they're not tracked by the server or the sender is dead "appIcon": notif.appIcon, "appName": notif.appName, "body": notif.body, @@ -253,10 +268,10 @@ Singleton { "urgency": notif.urgency, }); }); - // Find largest id + // Find largest notificationId let maxId = 0 root.list.forEach((notif) => { - maxId = Math.max(maxId, notif.id) + maxId = Math.max(maxId, notif.notificationId) }) console.log("[Notifications] File loaded") diff --git a/.config/quickshell/services/ResourceUsage.qml b/.config/quickshell/ii/services/ResourceUsage.qml similarity index 95% rename from .config/quickshell/services/ResourceUsage.qml rename to .config/quickshell/ii/services/ResourceUsage.qml index 6e0e58df..65052840 100644 --- a/.config/quickshell/services/ResourceUsage.qml +++ b/.config/quickshell/ii/services/ResourceUsage.qml @@ -1,7 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" +import qs.modules.common import QtQuick import Quickshell import Quickshell.Io @@ -53,7 +53,7 @@ Singleton { previousCpuStats = { total, idle } } - interval = ConfigOptions?.resources?.updateInterval ?? 3000 + interval = Config.options?.resources?.updateInterval ?? 3000 } } diff --git a/.config/quickshell/services/SystemInfo.qml b/.config/quickshell/ii/services/SystemInfo.qml similarity index 59% rename from .config/quickshell/services/SystemInfo.qml rename to .config/quickshell/ii/services/SystemInfo.qml index ffd478b6..cb96b54b 100644 --- a/.config/quickshell/services/SystemInfo.qml +++ b/.config/quickshell/ii/services/SystemInfo.qml @@ -9,10 +9,17 @@ import Quickshell.Io * Provides some system info: distro, username. */ Singleton { + id: root property string distroName: "Unknown" property string distroId: "unknown" property string distroIcon: "linux-symbolic" property string username: "user" + property string homeUrl: "" + property string documentationUrl: "" + property string supportUrl: "" + property string bugReportUrl: "" + property string privacyPolicyUrl: "" + property string logo: "" Timer { triggeredOnStart: true @@ -29,9 +36,23 @@ Singleton { const nameMatch = textOsRelease.match(/^NAME="(.+?)"/m) distroName = prettyNameMatch ? prettyNameMatch[1] : (nameMatch ? nameMatch[1].replace(/Linux/i, "").trim() : "Unknown") - // Extract the ID (LOGO field, fallback to "unknown") - const logoMatch = textOsRelease.match(/^LOGO=(.+)$/m) - distroId = logoMatch ? logoMatch[1].replace(/"/g, "") : "unknown" + // Extract the ID + const idMatch = textOsRelease.match(/^ID="?(.+?)"?$/m) + distroId = idMatch ? idMatch[1] : "unknown" + + // Extract additional URLs and logo + const homeUrlMatch = textOsRelease.match(/^HOME_URL="(.+?)"/m) + homeUrl = homeUrlMatch ? homeUrlMatch[1] : "" + const documentationUrlMatch = textOsRelease.match(/^DOCUMENTATION_URL="(.+?)"/m) + documentationUrl = documentationUrlMatch ? documentationUrlMatch[1] : "" + const supportUrlMatch = textOsRelease.match(/^SUPPORT_URL="(.+?)"/m) + supportUrl = supportUrlMatch ? supportUrlMatch[1] : "" + const bugReportUrlMatch = textOsRelease.match(/^BUG_REPORT_URL="(.+?)"/m) + bugReportUrl = bugReportUrlMatch ? bugReportUrlMatch[1] : "" + const privacyPolicyUrlMatch = textOsRelease.match(/^PRIVACY_POLICY_URL="(.+?)"/m) + privacyPolicyUrl = privacyPolicyUrlMatch ? privacyPolicyUrlMatch[1] : "" + const logoFieldMatch = textOsRelease.match(/^LOGO="?(.+?)"?$/m) + logo = logoFieldMatch ? logoFieldMatch[1] : "" // Update the distroIcon property based on distroId switch (distroId) { @@ -57,7 +78,7 @@ Singleton { command: ["whoami"] stdout: SplitParser { onRead: data => { - username = data.trim() + root.username = data.trim() } } } diff --git a/.config/quickshell/services/Todo.qml b/.config/quickshell/ii/services/Todo.qml similarity index 97% rename from .config/quickshell/services/Todo.qml rename to .config/quickshell/ii/services/Todo.qml index 2cbf0d7d..93227cbb 100644 --- a/.config/quickshell/services/Todo.qml +++ b/.config/quickshell/ii/services/Todo.qml @@ -1,10 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound -import "root:/modules/common" +import qs.modules.common import Quickshell; import Quickshell.Io; -import Qt.labs.platform import QtQuick; /** diff --git a/.config/quickshell/ii/services/Weather.qml b/.config/quickshell/ii/services/Weather.qml new file mode 100644 index 00000000..c8d46e26 --- /dev/null +++ b/.config/quickshell/ii/services/Weather.qml @@ -0,0 +1,154 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick +import QtPositioning + +import qs.modules.common + +Singleton { + id: root + // 10 minute + readonly property int fetchInterval: Config.options.bar.weather.fetchInterval * 60 * 1000 + readonly property string city: Config.options.bar.weather.city + readonly property bool useUSCS: Config.options.bar.weather.useUSCS + property bool gpsActive: Config.options.bar.weather.enableGPS + + property var location: ({ + valid: false, + lat: 0, + lon: 0 + }) + + 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 || "City"; + temp.temp = ""; + if (root.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 (root.gpsActive && root.location.valid) { + command += `/${root.location.lat},${root.location.long}`; + } else { + command += `/${formatCityName(root.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('+'); + } + + Component.onCompleted: { + if (!root.gpsActive) return; + console.info("[WeatherService] Starting the GPS service."); + positionSource.start(); + } + + 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}`); + } + } + } + } + + PositionSource { + id: positionSource + updateInterval: root.fetchInterval + + onPositionChanged: { + // update the location if the given location is valid + // if it fails getting the location, use the last valid location + if (position.latitudeValid && position.longitudeValid) { + root.location.lat = position.coordinate.latitude; + root.location.long = position.coordinate.longitude; + root.location.valid = true; + // console.info(`📍 Location: ${position.coordinate.latitude}, ${position.coordinate.longitude}`); + root.getData(); + // if can't get initialized with valid location deactivate the GPS + } else { + root.gpsActive = root.location.valid ? true : false; + console.error("[WeatherService] Failed to get the GPS location."); + } + } + + onValidityChanged: { + if (!positionSource.valid) { + positionSource.stop(); + root.location.valid = false; + root.gpsActive = false; + Quickshell.execDetached(["notify-send", Translation.tr("Weather Service"), Translation.tr("Cannot find a GPS service. Using the fallback method instead."), "-a", "Shell"]); + console.error("[WeatherService] Could not aquire a valid backend plugin."); + } + } + } + + Timer { + running: !root.gpsActive + repeat: true + interval: root.fetchInterval + triggeredOnStart: !root.gpsActive + onTriggered: root.getData() + } +} diff --git a/.config/quickshell/services/Ydotool.qml b/.config/quickshell/ii/services/Ydotool.qml similarity index 51% rename from .config/quickshell/services/Ydotool.qml rename to .config/quickshell/ii/services/Ydotool.qml index 7cafcbbe..f25b0930 100644 --- a/.config/quickshell/services/Ydotool.qml +++ b/.config/quickshell/ii/services/Ydotool.qml @@ -1,9 +1,7 @@ pragma Singleton -import "root:/modules/common" +import qs.modules.common import Quickshell -import Quickshell.Io -import Quickshell.Hyprland Singleton { id: root @@ -12,31 +10,38 @@ Singleton { property list altKeys: [56, 100] // Keycodes for Alt keys (left and right) property list ctrlKeys: [29, 97] // Keycodes for Ctrl keys (left and right) - onShiftModeChanged: { - if (shiftMode === 0) { - - } - } - function releaseAllKeys() { const keycodes = Array.from(Array(249).keys()); - const releaseCommand = `ydotool key --key-delay 0 ${keycodes.map(keycode => `${keycode}:0`).join(" ")}` - Hyprland.dispatch(`exec ${releaseCommand}`) + Quickshell.execDetached([ + "ydotool", + "key", "--key-delay", "0", + ...keycodes.map(keycode => `${keycode}:0`) + ]) root.shiftMode = 0; // Reset shift mode } function releaseShiftKeys() { - const releaseCommand = `ydotool key --key-delay 0 ${root.shiftKeys.map(keycode => `${keycode}:0`).join(" ")}` - Hyprland.dispatch(`exec ${releaseCommand}`) + Quickshell.execDetached([ + "ydotool", + "key", "--key-delay", "0", + ...root.shiftKeys.map(keycode => `${keycode}:0`) + ]) root.shiftMode = 0; // Reset shift mode } function press(keycode) { - Hyprland.dispatch(`exec ydotool key --key-delay 0 ${keycode}:1`); + Quickshell.execDetached([ + "ydotool", + "key", "--key-delay", "0", + `${keycode}:1` + ]); } function release(keycode) { - Hyprland.dispatch(`exec ydotool key --key-delay 0 ${keycode}:0`); + Quickshell.execDetached([ + "ydotool", + "key", "--key-delay", "0", + `${keycode}:0` + ]); } } - diff --git a/.config/quickshell/ii/settings.qml b/.config/quickshell/ii/settings.qml new file mode 100644 index 00000000..41e1938f --- /dev/null +++ b/.config/quickshell/ii/settings.qml @@ -0,0 +1,247 @@ +//@ pragma UseQApplication +//@ pragma Env QS_NO_RELOAD_POPUP=1 +//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic + +// Adjust this to make the app smaller or larger +//@ pragma Env QT_SCALE_FACTOR=1 + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import qs +import qs.services +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions as CF + +ApplicationWindow { + id: root + property string firstRunFilePath: CF.FileUtils.trimFileProtocol(`${Directories.state}/user/first_run.txt`) + property string firstRunFileContent: "This file is just here to confirm you've been greeted :>" + property real contentPadding: 8 + property bool showNextTime: false + property var pages: [ + { + name: Translation.tr("Style"), + icon: "palette", + component: "modules/settings/StyleConfig.qml" + }, + { + name: Translation.tr("Interface"), + icon: "cards", + component: "modules/settings/InterfaceConfig.qml" + }, + { + name: Translation.tr("Services"), + icon: "settings", + component: "modules/settings/ServicesConfig.qml" + }, + { + name: Translation.tr("Advanced"), + icon: "construction", + component: "modules/settings/AdvancedConfig.qml" + }, + { + name: Translation.tr("About"), + icon: "info", + component: "modules/settings/About.qml" + } + ] + property int currentPage: 0 + + visible: true + onClosing: Qt.quit() + title: "illogical-impulse Settings" + + Component.onCompleted: { + MaterialThemeLoader.reapplyTheme() + } + + minimumWidth: 600 + minimumHeight: 400 + width: 1100 + height: 750 + color: Appearance.m3colors.m3background + + ColumnLayout { + anchors { + fill: parent + margins: contentPadding + } + + Keys.onPressed: (event) => { + if (event.modifiers === Qt.ControlModifier) { + if (event.key === Qt.Key_PageDown) { + root.currentPage = Math.min(root.currentPage + 1, root.pages.length - 1) + event.accepted = true; + } + else if (event.key === Qt.Key_PageUp) { + root.currentPage = Math.max(root.currentPage - 1, 0) + event.accepted = true; + } + else if (event.key === Qt.Key_Tab) { + root.currentPage = (root.currentPage + 1) % root.pages.length; + event.accepted = true; + } + else if (event.key === Qt.Key_Backtab) { + root.currentPage = (root.currentPage - 1 + root.pages.length) % root.pages.length; + event.accepted = true; + } + } + } + + Item { // Titlebar + visible: Config.options?.windows.showTitlebar + Layout.fillWidth: true + Layout.fillHeight: false + implicitHeight: Math.max(titleText.implicitHeight, windowControlsRow.implicitHeight) + StyledText { + id: titleText + anchors { + left: Config.options.windows.centerTitle ? undefined : parent.left + horizontalCenter: Config.options.windows.centerTitle ? parent.horizontalCenter : undefined + verticalCenter: parent.verticalCenter + leftMargin: 12 + } + color: Appearance.colors.colOnLayer0 + text: Translation.tr("Settings") + font.pixelSize: Appearance.font.pixelSize.title + font.family: Appearance.font.family.title + } + RowLayout { // Window controls row + id: windowControlsRow + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + RippleButton { + buttonRadius: Appearance.rounding.full + implicitWidth: 35 + implicitHeight: 35 + onClicked: root.close() + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: "close" + iconSize: 20 + } + } + } + } + + RowLayout { // Window content with navigation rail and content pane + Layout.fillWidth: true + Layout.fillHeight: true + spacing: contentPadding + Item { + id: navRailWrapper + Layout.fillHeight: true + Layout.margins: 5 + implicitWidth: navRail.expanded ? 150 : fab.baseSize + Behavior on implicitWidth { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + NavigationRail { // Window content with navigation rail and content pane + id: navRail + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + spacing: 10 + expanded: root.width > 900 + + NavigationRailExpandButton { + focus: root.visible + } + + FloatingActionButton { + id: fab + iconText: "edit" + buttonText: Translation.tr("Edit config") + expanded: navRail.expanded + onClicked: { + Qt.openUrlExternally(`${Directories.config}/illogical-impulse/config.json`); + } + + StyledToolTip { + extraVisibleCondition: !navRail.expanded + content: "Edit shell config file" + } + } + + NavigationRailTabArray { + currentIndex: root.currentPage + expanded: navRail.expanded + Repeater { + model: root.pages + NavigationRailButton { + required property var index + required property var modelData + toggled: root.currentPage === index + onClicked: root.currentPage = index; + expanded: navRail.expanded + buttonIcon: modelData.icon + buttonText: modelData.name + showToggledHighlight: false + } + } + } + + Item { + Layout.fillHeight: true + } + } + } + Rectangle { // Content container + Layout.fillWidth: true + Layout.fillHeight: true + color: Appearance.m3colors.m3surfaceContainerLow + radius: Appearance.rounding.windowRounding - root.contentPadding + + Loader { + id: pageLoader + anchors.fill: parent + opacity: 1.0 + source: root.pages[0].component + Connections { + target: root + function onCurrentPageChanged() { + if (pageLoader.sourceComponent !== root.pages[root.currentPage].component) { + switchAnim.complete(); + switchAnim.start(); + } + } + } + + SequentialAnimation { + id: switchAnim + + NumberAnimation { + target: pageLoader + properties: "opacity" + from: 1 + to: 0 + duration: 100 + easing.type: Appearance.animation.elementMoveExit.type + easing.bezierCurve: Appearance.animationCurves.emphasizedFirstHalf + } + PropertyAction { + target: pageLoader + property: "source" + value: root.pages[root.currentPage].component + } + NumberAnimation { + target: pageLoader + properties: "opacity" + from: 0 + to: 1 + duration: 200 + easing.type: Appearance.animation.elementMoveEnter.type + easing.bezierCurve: Appearance.animationCurves.emphasizedLastHalf + } + } + } + } + } + } +} diff --git a/.config/quickshell/shell.qml b/.config/quickshell/ii/shell.qml similarity index 86% rename from .config/quickshell/shell.qml rename to .config/quickshell/ii/shell.qml index 4edfde51..9bba7104 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/ii/shell.qml @@ -6,10 +6,11 @@ //@ pragma Env QT_SCALE_FACTOR=1 import "./modules/common/" -import "./modules/backgroundWidgets/" +import "./modules/background/" import "./modules/bar/" import "./modules/cheatsheet/" import "./modules/dock/" +import "./modules/lock/" import "./modules/mediaControls/" import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" @@ -30,9 +31,10 @@ ShellRoot { // Enable/disable modules here. False = not loaded at all, so rest assured // no unnecessary stuff will take up memory if you decide to only use, say, the overview. property bool enableBar: true - property bool enableBackgroundWidgets: true + property bool enableBackground: true property bool enableCheatsheet: true - property bool enableDock: false + property bool enableDock: true + property bool enableLock: true property bool enableMediaControls: true property bool enableNotificationPopup: true property bool enableOnScreenDisplayBrightness: true @@ -47,17 +49,17 @@ ShellRoot { // Force initialization of some singletons Component.onCompleted: { - MaterialThemeLoader.reapplyTheme() - ConfigLoader.loadConfig() - PersistentStateManager.loadStates() Cliphist.refresh() FirstRunExperience.load() + Hyprsunset.load() + MaterialThemeLoader.reapplyTheme() } LazyLoader { active: enableBar; component: Bar {} } - LazyLoader { active: enableBackgroundWidgets; component: BackgroundWidgets {} } + LazyLoader { active: enableBackground; component: Background {} } LazyLoader { active: enableCheatsheet; component: Cheatsheet {} } - LazyLoader { active: enableDock; component: Dock {} } + LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} } + LazyLoader { active: enableLock; component: Lock {} } LazyLoader { active: enableMediaControls; component: MediaControls {} } LazyLoader { active: enableNotificationPopup; component: NotificationPopup {} } LazyLoader { active: enableOnScreenDisplayBrightness; component: OnScreenDisplayBrightness {} } diff --git a/.config/quickshell/ii/welcome.qml b/.config/quickshell/ii/welcome.qml new file mode 100644 index 00000000..bfcb2015 --- /dev/null +++ b/.config/quickshell/ii/welcome.qml @@ -0,0 +1,341 @@ +//@ pragma UseQApplication +//@ pragma Env QS_NO_RELOAD_POPUP=1 +//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic + +// Adjust this to make the app smaller or larger +//@ pragma Env QT_SCALE_FACTOR=1 + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import qs +import qs.modules.common +import qs.modules.common.widgets +import qs.modules.common.functions + +ApplicationWindow { + id: root + property string firstRunFilePath: FileUtils.trimFileProtocol(`${Directories.state}/user/first_run.txt`) + property string firstRunFileContent: "This file is just here to confirm you've been greeted :>" + property real contentPadding: 8 + property bool showNextTime: false + visible: true + onClosing: Qt.quit() + title: Translation.tr("illogical-impulse Welcome") + + Component.onCompleted: { + MaterialThemeLoader.reapplyTheme() + } + + minimumWidth: 600 + minimumHeight: 400 + width: 800 + height: 650 + color: Appearance.m3colors.m3background + + Process { + id: konachanWallProc + property string status: "" + command: ["bash", "-c", Quickshell.shellPath("scripts/colors/random_konachan_wall.sh")] + stdout: SplitParser { + onRead: data => { + console.log(`Konachan wall proc output: ${data}`); + konachanWallProc.status = data.trim(); + } + } + } + + ColumnLayout { + anchors { + fill: parent + margins: contentPadding + } + + Item { // Titlebar + visible: Config.options?.windows.showTitlebar + Layout.fillWidth: true + implicitHeight: Math.max(welcomeText.implicitHeight, windowControlsRow.implicitHeight) + StyledText { + id: welcomeText + anchors { + left: Config.options.windows.centerTitle ? undefined : parent.left + horizontalCenter: Config.options.windows.centerTitle ? parent.horizontalCenter : undefined + verticalCenter: parent.verticalCenter + leftMargin: 12 + } + color: Appearance.colors.colOnLayer0 + text: Translation.tr("Yooooo hi there") + font.pixelSize: Appearance.font.pixelSize.title + font.family: Appearance.font.family.title + } + RowLayout { // Window controls row + id: windowControlsRow + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + StyledText { + font.pixelSize: Appearance.font.pixelSize.smaller + text: Translation.tr("Show next time") + } + StyledSwitch { + id: showNextTimeSwitch + checked: root.showNextTime + scale: 0.6 + Layout.alignment: Qt.AlignVCenter + onCheckedChanged: { + if (checked) { + Quickshell.execDetached(["rm", root.firstRunFilePath]) + } else { + Quickshell.execDetached(["bash", "-c", `echo '${StringUtils.shellSingleQuoteEscape(root.firstRunFileContent)}' > '${StringUtils.shellSingleQuoteEscape(root.firstRunFilePath)}'`]) + } + } + } + RippleButton { + buttonRadius: Appearance.rounding.full + implicitWidth: 35 + implicitHeight: 35 + onClicked: root.close() + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: "close" + iconSize: 20 + } + } + } + } + Rectangle { // Content container + color: Appearance.m3colors.m3surfaceContainerLow + radius: Appearance.rounding.windowRounding - root.contentPadding + implicitHeight: contentColumn.implicitHeight + implicitWidth: contentColumn.implicitWidth + Layout.fillWidth: true + Layout.fillHeight: true + + + ContentPage { + id: contentColumn + anchors.fill: parent + + ContentSection { + title: Translation.tr("Bar style") + + ConfigSelectionArray { + currentValue: Config.options.bar.cornerStyle + configOptionName: "bar.cornerStyle" + onSelected: (newValue) => { + Config.options.bar.cornerStyle = newValue; // Update local copy + } + options: [ + { displayName: Translation.tr("Hug"), value: 0 }, + { displayName: Translation.tr("Float"), value: 1 }, + { displayName: Translation.tr("Plain rectangle"), value: 2 } + ] + } + } + + ContentSection { + title: Translation.tr("Style & wallpaper") + + ButtonGroup { + Layout.fillWidth: true + LightDarkPreferenceButton { + dark: false + } + LightDarkPreferenceButton { + dark: true + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + RippleButtonWithIcon { + id: rndWallBtn + Layout.alignment: Qt.AlignHCenter + buttonRadius: Appearance.rounding.small + materialIcon: "wallpaper" + mainText: konachanWallProc.running ? Translation.tr("Be patient...") : Translation.tr("Random: Konachan") + onClicked: { + console.log(konachanWallProc.command.join(" ")) + konachanWallProc.running = true; + } + StyledToolTip { + content: Translation.tr("Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers") + } + } + RippleButtonWithIcon { + materialIcon: "wallpaper" + StyledToolTip { + content: Translation.tr("Pick wallpaper image on your system") + } + onClicked: { + Quickshell.execDetached([`${Directories.wallpaperSwitchScriptPath}`]) + } + mainContentComponent: Component { + RowLayout { + spacing: 10 + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + text: Translation.tr("Choose file") + color: Appearance.colors.colOnSecondaryContainer + } + RowLayout { + spacing: 3 + KeyboardKey { + key: "Ctrl" + } + KeyboardKey { + key: "󰖳" + } + StyledText { + Layout.alignment: Qt.AlignVCenter + text: "+" + } + KeyboardKey { + key: "T" + } + } + } + } + } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Translation.tr("Change any time later with /dark, /light, /img in the launcher") + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.colors.colSubtext + } + } + + ContentSection { + title: Translation.tr("Policies") + + ConfigRow { + ColumnLayout { // Weeb policy + ContentSubsectionLabel { + text: Translation.tr("Weeb") + } + ConfigSelectionArray { + currentValue: Config.options.policies.weeb + configOptionName: "policies.weeb" + onSelected: (newValue) => { + Config.options.policies.weeb = newValue; + } + options: [ + { displayName: Translation.tr("No"), value: 0 }, + { displayName: Translation.tr("Yes"), value: 1 }, + { displayName: Translation.tr("Closet"), value: 2 } + ] + } + } + + ColumnLayout { // AI policy + ContentSubsectionLabel { + text: Translation.tr("AI") + } + ConfigSelectionArray { + currentValue: Config.options.policies.ai + configOptionName: "policies.ai" + onSelected: (newValue) => { + Config.options.policies.ai = newValue; + } + options: [ + { displayName: Translation.tr("No"), value: 0 }, + { displayName: Translation.tr("Yes"), value: 1 }, + { displayName: Translation.tr("Local only"), value: 2 } + ] + } + } + } + } + + ContentSection { + title: Translation.tr("Info") + + Flow { + Layout.fillWidth: true + spacing: 5 + + RippleButtonWithIcon { + materialIcon: "keyboard_alt" + onClicked: { + Hyprland.dispatch("global quickshell:cheatsheetOpen") + } + mainContentComponent: Component { + RowLayout { + spacing: 10 + StyledText { + font.pixelSize: Appearance.font.pixelSize.small + text: Translation.tr("Keybinds") + color: Appearance.colors.colOnSecondaryContainer + } + RowLayout { + spacing: 3 + KeyboardKey { + key: "󰖳" + } + StyledText { + Layout.alignment: Qt.AlignVCenter + text: "+" + } + KeyboardKey { + key: "/" + } + } + } + } + } + + RippleButtonWithIcon { + materialIcon: "help" + mainText: Translation.tr("Usage") + onClicked: { + Qt.openUrlExternally("https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/02usage/") + } + } + RippleButtonWithIcon { + materialIcon: "construction" + mainText: Translation.tr("Configuration") + onClicked: { + Qt.openUrlExternally("https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/03config/") + } + } + } + } + + ContentSection { + title: Translation.tr("Useless buttons") + + Flow { + Layout.fillWidth: true + spacing: 5 + + RippleButtonWithIcon { + nerdIcon: "󰊤" + mainText: Translation.tr("GitHub") + onClicked: { + Qt.openUrlExternally("https://github.com/end-4/dots-hyprland") + } + } + RippleButtonWithIcon { + materialIcon: "favorite" + mainText: "Funny number" + onClicked: { + Qt.openUrlExternally("https://github.com/sponsors/end-4") + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + } +} diff --git a/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml b/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml deleted file mode 100644 index 77e036f1..00000000 --- a/.config/quickshell/modules/backgroundWidgets/BackgroundWidgets.qml +++ /dev/null @@ -1,139 +0,0 @@ -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Hyprland -import Quickshell.Services.UPower - -Scope { - id: root - property string filePath: `${Directories.state}/user/generated/wallpaper/least_busy_region.json` - property real defaultX: (ConfigOptions?.background.clockX ?? -500) - property real defaultY: (ConfigOptions?.background.clockY ?? -500) - property real centerX: defaultX - property real centerY: defaultY - property real effectiveCenterX: ConfigOptions?.background.fixedClockPosition ? defaultX : centerX - property real effectiveCenterY: ConfigOptions?.background.fixedClockPosition ? defaultY : centerY - property color dominantColor: Appearance.colors.colPrimary - property bool dominantColorIsDark: dominantColor.hslLightness < 0.5 - property color colBackground: ColorUtils.transparentize(ColorUtils.mix(Appearance.colors.colPrimary, Appearance.colors.colSecondaryContainer), 1) - property color colText: ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (root.dominantColorIsDark ? 0.8 : 0.12)) - - function updateWidgetPosition(fileContent) { - // console.log("[BackgroundWidgets] Updating widget position with content:", fileContent) - const parsedContent = JSON.parse(fileContent) - root.centerX = parsedContent.center_x - root.centerY = parsedContent.center_y - root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary - } - - Timer { - id: delayedFileRead - interval: ConfigOptions.hacks.arbitraryRaceConditionDelay - running: false - onTriggered: { - root.updateWidgetPosition(leastBusyRegionFileView.text()) - } - } - - FileView { - id: leastBusyRegionFileView - path: Qt.resolvedUrl(root.filePath) - watchChanges: !ConfigOptions?.background.fixedClockPosition - onFileChanged: { - this.reload() - delayedFileRead.start() - } - onLoadedChanged: { - const fileContent = leastBusyRegionFileView.text() - root.updateWidgetPosition(fileContent) - } - } - - Variants { // For each monitor - model: Quickshell.screens - - LazyLoader { - required property var modelData - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(modelData) - activeAsync: !ToplevelManager.activeToplevel?.activated - component: PanelWindow { // Window - id: windowRoot - screen: modelData - property var textHorizontalAlignment: root.effectiveCenterX / monitor.scale < windowRoot.width / 3 ? Text.AlignLeft : - (root.effectiveCenterX / monitor.scale > windowRoot.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter) - - WlrLayershell.layer: WlrLayer.Bottom - WlrLayershell.namespace: "quickshell:backgroundWidgets" - - anchors { - top: true - bottom:true - left: true - right: true - } - color: "transparent" - HyprlandWindow.visibleMask: Region { - item: widgetBackground - } - - Rectangle { - id: widgetBackground - property real verticalPadding: 20 - property real horizontalPadding: 30 - radius: 40 - color: root.colBackground - implicitHeight: columnLayout.implicitHeight + verticalPadding * 2 - implicitWidth: columnLayout.implicitWidth + horizontalPadding * 2 - anchors { - left: parent.left - top: parent.top - leftMargin: (root.effectiveCenterX / monitor.scale - implicitWidth / 2) - topMargin: (root.effectiveCenterY / monitor.scale - implicitHeight / 2) - Behavior on leftMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - Behavior on topMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - } - - ColumnLayout { - id: columnLayout - anchors.centerIn: parent - spacing: -5 - - StyledText { - Layout.fillWidth: true - horizontalAlignment: windowRoot.textHorizontalAlignment - font.pixelSize: 95 - color: root.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - text: DateTime.time - } - StyledText { - Layout.fillWidth: true - horizontalAlignment: windowRoot.textHorizontalAlignment - font.pixelSize: 25 - color: root.colText - style: Text.Raised - styleColor: Appearance.colors.colShadow - text: DateTime.date - } - } - } - - } - } - - } - -} diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml deleted file mode 100644 index ae6c2730..00000000 --- a/.config/quickshell/modules/bar/Bar.qml +++ /dev/null @@ -1,490 +0,0 @@ -import "root:/" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import Quickshell.Services.UPower - -Scope { - id: bar - - readonly property int barHeight: Appearance.sizes.barHeight - readonly property int osdHideMouseMoveThreshold: 20 - property bool showBarBackground: ConfigOptions.bar.showBackground - - component VerticalBarSeparator: Rectangle { - Layout.topMargin: barHeight / 3 - Layout.bottomMargin: barHeight / 3 - Layout.fillHeight: true - implicitWidth: 1 - color: Appearance.colors.colOutlineVariant - } - - Variants { // For each monitor - model: { - const screens = Quickshell.screens; - const list = ConfigOptions.bar.screenList; - if (!list || list.length === 0) - return screens; - return screens.filter(screen => list.includes(screen.name)); - } - - PanelWindow { // Bar window - id: barRoot - screen: modelData - - 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 - - WlrLayershell.namespace: "quickshell:bar" - implicitHeight: barHeight + Appearance.rounding.screenRounding - exclusiveZone: showBarBackground ? barHeight : (barHeight - 4) - mask: Region { - item: barContent - } - color: "transparent" - - anchors { - top: !ConfigOptions.bar.bottom - bottom: ConfigOptions.bar.bottom - left: true - right: true - } - - Rectangle { // Bar background - id: barContent - anchors { - right: parent.right - left: parent.left - top: !ConfigOptions.bar.bottom ? parent.top : undefined - bottom: ConfigOptions.bar.bottom ? parent.bottom : undefined - } - color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" - height: barHeight - - MouseArea { // Left side | scroll to change brightness - id: barLeftSideMouseArea - anchors.left: parent.left - implicitHeight: barHeight - width: (barRoot.width - middleSection.width) / 2 - property bool hovered: false - property real lastScrollX: 0 - property real lastScrollY: 0 - property bool trackingScroll: false - acceptedButtons: Qt.LeftButton - hoverEnabled: true - propagateComposedEvents: true - onEntered: (event) => { - barLeftSideMouseArea.hovered = true - } - onExited: (event) => { - barLeftSideMouseArea.hovered = false - barLeftSideMouseArea.trackingScroll = false - } - onPressed: (event) => { - if (event.button === Qt.LeftButton) { - Hyprland.dispatch('global quickshell:sidebarLeftOpen') - } - } - // Scroll to change brightness - WheelHandler { - onWheel: (event) => { - if (event.angleDelta.y < 0) - barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05); - else if (event.angleDelta.y > 0) - barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness + 0.05); - // Store the mouse position and start tracking - barLeftSideMouseArea.lastScrollX = event.x; - barLeftSideMouseArea.lastScrollY = event.y; - barLeftSideMouseArea.trackingScroll = true; - } - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - } - 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') - barLeftSideMouseArea.trackingScroll = false; - } - } - } - Item { // Left section - anchors.fill: parent - implicitHeight: leftSectionRowLayout.implicitHeight - implicitWidth: leftSectionRowLayout.implicitWidth - - ScrollHint { - reveal: barLeftSideMouseArea.hovered - icon: "light_mode" - tooltipText: qsTr("Scroll to change brightness") - side: "left" - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - - } - - RowLayout { // Content - id: leftSectionRowLayout - anchors.fill: parent - spacing: 10 - - 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 - colRipple: Appearance.colors.colLayer1Active - colBackgroundToggled: Appearance.colors.colSecondaryContainer - colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover - colRippleToggled: Appearance.colors.colSecondaryContainerActive - toggled: GlobalStates.sidebarLeftOpen - property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 - - onPressed: { - Hyprland.dispatch('global quickshell:sidebarLeftToggle') - } - - CustomIcon { - id: distroIcon - anchors.centerIn: parent - width: 19.5 - height: 19.5 - source: ConfigOptions.bar.topLeftIcon == 'distro' ? - SystemInfo.distroIcon : "spark-symbolic" - } - - ColorOverlay { - anchors.fill: distroIcon - source: distroIcon - color: Appearance.colors.colOnLayer0 - } - } - - ActiveWindow { - visible: barRoot.useShortenedForm === 0 - Layout.rightMargin: Appearance.rounding.screenRounding - Layout.fillWidth: true - Layout.fillHeight: true - bar: barRoot - } - } - } - } - - RowLayout { // Middle section - id: middleSection - anchors.centerIn: parent - spacing: ConfigOptions?.bar.borderless ? 4 : 8 - - BarGroup { - id: leftCenterGroup - Layout.preferredWidth: barRoot.centerSideModuleWidth - Layout.fillHeight: true - - Resources { - alwaysShowAllResources: barRoot.useShortenedForm === 2 - Layout.fillWidth: barRoot.useShortenedForm === 2 - } - - Media { - visible: barRoot.useShortenedForm < 2 - Layout.fillWidth: true - } - - } - - 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 - anchors.fill: parent - acceptedButtons: Qt.RightButton - - onPressed: (event) => { - if (event.button === Qt.RightButton) { - Hyprland.dispatch('global quickshell:overviewToggle') - } - } - } - } - } - - VerticalBarSeparator {visible: ConfigOptions?.bar.borderless} - - MouseArea { - id: rightCenterGroup - implicitWidth: rightCenterGroupContent.implicitWidth - implicitHeight: rightCenterGroupContent.implicitHeight - Layout.preferredWidth: barRoot.centerSideModuleWidth - Layout.fillHeight: true - - onPressed: { - Hyprland.dispatch('global quickshell:sidebarRightToggle') - } - - BarGroup { - id: rightCenterGroupContent - anchors.fill: parent - - ClockWidget { - showDate: (ConfigOptions.bar.verbose && barRoot.useShortenedForm < 2) - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - } - - UtilButtons { - visible: (ConfigOptions.bar.verbose && barRoot.useShortenedForm === 0) - Layout.alignment: Qt.AlignVCenter - } - - BatteryIndicator { - visible: (barRoot.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery) - Layout.alignment: Qt.AlignVCenter - } - } - } - - } - - MouseArea { // Right side | scroll to change volume - id: barRightSideMouseArea - - anchors.right: parent.right - implicitHeight: barHeight - width: (barRoot.width - middleSection.width) / 2 - - property bool hovered: false - property real lastScrollX: 0 - property real lastScrollY: 0 - property bool trackingScroll: false - - acceptedButtons: Qt.LeftButton - hoverEnabled: true - propagateComposedEvents: true - onEntered: (event) => { - barRightSideMouseArea.hovered = true - } - onExited: (event) => { - barRightSideMouseArea.hovered = false - barRightSideMouseArea.trackingScroll = false - } - onPressed: (event) => { - if (event.button === Qt.LeftButton) { - Hyprland.dispatch('global quickshell:sidebarRightOpen') - } - else if (event.button === Qt.RightButton) { - MprisController.activePlayer.next() - } - } - // Scroll to change volume - WheelHandler { - onWheel: (event) => { - const currentVolume = Audio.value; - const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2; - if (event.angleDelta.y < 0) - Audio.sink.audio.volume -= step; - else if (event.angleDelta.y > 0) - Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step); - // Store the mouse position and start tracking - barRightSideMouseArea.lastScrollX = event.x; - barRightSideMouseArea.lastScrollY = event.y; - barRightSideMouseArea.trackingScroll = true; - } - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - } - 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') - barRightSideMouseArea.trackingScroll = false; - } - } - } - - Item { - anchors.fill: parent - implicitHeight: rightSectionRowLayout.implicitHeight - implicitWidth: rightSectionRowLayout.implicitWidth - - ScrollHint { - reveal: barRightSideMouseArea.hovered - icon: "volume_up" - tooltipText: qsTr("Scroll to change volume") - side: "right" - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - } - - RowLayout { - id: rightSectionRowLayout - 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 - buttonRadius: Appearance.rounding.full - colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1) - colBackgroundHover: Appearance.colors.colLayer1Hover - colRipple: Appearance.colors.colLayer1Active - colBackgroundToggled: Appearance.colors.colSecondaryContainer - colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover - colRippleToggled: Appearance.colors.colSecondaryContainerActive - toggled: GlobalStates.sidebarRightOpen - property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0 - - Behavior on colText { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - - onPressed: { - Hyprland.dispatch('global quickshell:sidebarRightToggle') - } - - RowLayout { - id: indicatorsRowLayout - anchors.centerIn: parent - property real realSpacing: 15 - spacing: 0 - - Revealer { - reveal: Audio.sink?.audio?.muted ?? false - Layout.fillHeight: true - Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0 - Behavior on Layout.rightMargin { - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve - } - } - MaterialSymbol { - text: "volume_off" - iconSize: Appearance.font.pixelSize.larger - color: rightSidebarButton.colText - } - } - Revealer { - reveal: Audio.source?.audio?.muted ?? false - Layout.fillHeight: true - Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0 - Behavior on Layout.rightMargin { - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve - } - } - MaterialSymbol { - text: "mic_off" - iconSize: Appearance.font.pixelSize.larger - color: rightSidebarButton.colText - } - } - Loader { - active: HyprlandXkb.layoutCodes.length > 1 - visible: active - Layout.rightMargin: indicatorsRowLayout.realSpacing - sourceComponent: StyledText { - text: HyprlandXkb.currentLayoutCode - font.pixelSize: Appearance.font.pixelSize.smaller - color: rightSidebarButton.colText - } - } - MaterialSymbol { - Layout.rightMargin: indicatorsRowLayout.realSpacing - text: Network.materialSymbol - iconSize: Appearance.font.pixelSize.larger - color: rightSidebarButton.colText - } - MaterialSymbol { - text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled" - iconSize: Appearance.font.pixelSize.larger - color: rightSidebarButton.colText - } - } - } - - SysTray { - bar: barRoot - visible: barRoot.useShortenedForm === 0 - Layout.fillWidth: false - Layout.fillHeight: true - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - } - } - } - } - - // Round decorators - Item { - anchors { - left: parent.left - right: parent.right - // top: barContent.bottom - top: ConfigOptions.bar.bottom ? undefined : barContent.bottom - bottom: ConfigOptions.bar.bottom ? barContent.top : undefined - } - height: Appearance.rounding.screenRounding - - RoundCorner { - anchors.top: parent.top - anchors.left: parent.left - size: Appearance.rounding.screenRounding - corner: ConfigOptions.bar.bottom ? cornerEnum.bottomLeft : cornerEnum.topLeft - color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" - } - RoundCorner { - anchors.top: parent.top - anchors.right: parent.right - size: Appearance.rounding.screenRounding - corner: ConfigOptions.bar.bottom ? cornerEnum.bottomRight : cornerEnum.topRight - color: showBarBackground ? Appearance.colors.colLayer0 : "transparent" - } - } - - } - - } - -} diff --git a/.config/quickshell/modules/bar/UtilButtons.qml b/.config/quickshell/modules/bar/UtilButtons.qml deleted file mode 100644 index 9d9832a0..00000000 --- a/.config/quickshell/modules/bar/UtilButtons.qml +++ /dev/null @@ -1,85 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import Quickshell.Services.Pipewire - -Item { - id: root - property bool borderless: ConfigOptions.bar.borderless - implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2 - - RowLayout { - id: rowLayout - - spacing: 4 - anchors.centerIn: parent - - Loader { - active: ConfigOptions.bar.utilButtons.showScreenSnip - visible: ConfigOptions.bar.utilButtons.showScreenSnip - sourceComponent: CircleUtilButton { - Layout.alignment: Qt.AlignVCenter - onClicked: Hyprland.dispatch("exec hyprshot --freeze --clipboard-only --mode region --silent") - MaterialSymbol { - horizontalAlignment: Qt.AlignHCenter - fill: 1 - text: "screenshot_region" - iconSize: Appearance.font.pixelSize.large - color: Appearance.colors.colOnLayer2 - } - } - } - - Loader { - active: ConfigOptions.bar.utilButtons.showColorPicker - visible: ConfigOptions.bar.utilButtons.showColorPicker - sourceComponent: CircleUtilButton { - Layout.alignment: Qt.AlignVCenter - onClicked: Hyprland.dispatch("exec hyprpicker -a") - MaterialSymbol { - horizontalAlignment: Qt.AlignHCenter - fill: 1 - text: "colorize" - iconSize: Appearance.font.pixelSize.large - color: Appearance.colors.colOnLayer2 - } - } - } - - Loader { - active: ConfigOptions.bar.utilButtons.showKeyboardToggle - visible: ConfigOptions.bar.utilButtons.showKeyboardToggle - sourceComponent: CircleUtilButton { - Layout.alignment: Qt.AlignVCenter - onClicked: Hyprland.dispatch("global quickshell:oskToggle") - MaterialSymbol { - horizontalAlignment: Qt.AlignHCenter - fill: 0 - text: "keyboard" - iconSize: Appearance.font.pixelSize.large - color: Appearance.colors.colOnLayer2 - } - } - } - - Loader { - active: ConfigOptions.bar.utilButtons.showMicToggle - visible: ConfigOptions.bar.utilButtons.showMicToggle - sourceComponent: CircleUtilButton { - Layout.alignment: Qt.AlignVCenter - onClicked: Hyprland.dispatch("exec wpctl set-mute @DEFAULT_SOURCE@ toggle") - MaterialSymbol { - horizontalAlignment: Qt.AlignHCenter - fill: 0 - text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic" - iconSize: Appearance.font.pixelSize.large - color: Appearance.colors.colOnLayer2 - } - } - } - } -} diff --git a/.config/quickshell/modules/cheatsheet/Cheatsheet.qml b/.config/quickshell/modules/cheatsheet/Cheatsheet.qml deleted file mode 100644 index 79711f88..00000000 --- a/.config/quickshell/modules/cheatsheet/Cheatsheet.qml +++ /dev/null @@ -1,165 +0,0 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell.Io -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland - -Scope { // Scope - id: root - - Loader { - id: cheatsheetLoader - active: false - - sourceComponent: PanelWindow { // Window - id: cheatsheetRoot - visible: cheatsheetLoader.active - - anchors { - top: true - bottom: true - left: true - right: true - } - - function hide() { - cheatsheetLoader.active = false - } - exclusiveZone: 0 - implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2 - implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2 - WlrLayershell.namespace: "quickshell:cheatsheet" - // Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab - // WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - color: "transparent" - - mask: Region { - item: cheatsheetBackground - } - - HyprlandFocusGrab { // Click outside to close - id: grab - windows: [ cheatsheetRoot ] - active: cheatsheetRoot.visible - onCleared: () => { - if (!active) cheatsheetRoot.hide() - } - } - - - // Background - StyledRectangularShadow { - target: cheatsheetBackground - } - Rectangle { - id: cheatsheetBackground - anchors.centerIn: parent - color: Appearance.colors.colLayer0 - radius: Appearance.rounding.windowRounding - property real padding: 30 - implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2 - implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2 - - Keys.onPressed: (event) => { // Esc to close - if (event.key === Qt.Key_Escape) { - cheatsheetRoot.hide() - } - } - - RippleButton { // Close button - id: closeButton - focus: cheatsheetRoot.visible - implicitWidth: 40 - implicitHeight: 40 - buttonRadius: Appearance.rounding.full - anchors { - top: parent.top - right: parent.right - topMargin: 20 - rightMargin: 20 - } - - onClicked: { - cheatsheetRoot.hide() - } - - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - font.pixelSize: Appearance.font.pixelSize.title - text: "close" - } - } - - ColumnLayout { // Real content - id: cheatsheetColumnLayout - anchors.centerIn: parent - spacing: 20 - - StyledText { - id: cheatsheetTitle - Layout.alignment: Qt.AlignHCenter - font.family: Appearance.font.family.title - font.pixelSize: Appearance.font.pixelSize.title - text: qsTr("Cheat sheet") - } - CheatsheetKeybinds {} - } - } - - } - } - - IpcHandler { - target: "cheatsheet" - - function toggle(): void { - cheatsheetLoader.active = !cheatsheetLoader.active - } - - function close(): void { - cheatsheetLoader.active = false - } - - function open(): void { - cheatsheetLoader.active = true - } - } - - GlobalShortcut { - name: "cheatsheetToggle" - description: qsTr("Toggles cheatsheet on press") - - onPressed: { - cheatsheetLoader.active = !cheatsheetLoader.active; - } - } - - GlobalShortcut { - name: "cheatsheetOpen" - description: qsTr("Opens cheatsheet on press") - - onPressed: { - cheatsheetLoader.active = true; - } - } - - GlobalShortcut { - name: "cheatsheetClose" - description: qsTr("Closes cheatsheet on press") - - onPressed: { - cheatsheetLoader.active = false; - } - } - -} diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml deleted file mode 100644 index 230dfa4c..00000000 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ /dev/null @@ -1,164 +0,0 @@ -import QtQuick -import Quickshell -pragma Singleton -pragma ComponentBehavior: Bound - -Singleton { - property QtObject policies: QtObject { - property int ai: 1 // 0: No | 1: Yes | 2: Local - property int weeb: 1 // 0: No | 1: Open | 2: Closet - } - - property QtObject ai: QtObject { - property string systemPrompt: qsTr("Use casual tone. No user knowledge is to be assumed except basic Linux literacy. Be brief and concise: When explaining concepts, use bullet points (prefer minus sign (-) over asterisk (*)) and highlight keywords in bold to pinpoint the main concepts instead of long paragraphs. You are also encouraged to split your response with h2 headers, each header title beginning with an emoji, like `## 🐧 Linux`. When making changes to the user's config, you must get the config to know what values there are before setting.") - } - - property QtObject appearance: QtObject { - property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen - property bool transparency: false - property QtObject palette: QtObject { - property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot - } - } - - property QtObject audio: QtObject { // Values in % - property QtObject protection: QtObject { // Prevent sudden bangs - property bool enable: true - property real maxAllowedIncrease: 10 - property real maxAllowed: 90 // Realistically should already provide some protection when it's 99... - } - } - - property QtObject apps: QtObject { - property string bluetooth: "kcmshell6 kcm_bluetooth" - property string network: "plasmawindowed org.kde.plasma.networkmanagement" - property string networkEthernet: "kcmshell6 kcm_networkmanagement" - property string settings: "systemsettings" - property string taskManager: "plasma-systemmonitor --page-name Processes" - property string terminal: "kitty -1" // This is only for shell actions - } - - property QtObject background: QtObject { - property bool fixedClockPosition: false - property real clockX: -500 - property real clockY: -500 - } - - property QtObject bar: QtObject { - property bool bottom: false // Instead of top - property bool borderless: false // true for no grouping of items - property string topLeftIcon: "spark" // Options: distro, spark - property bool showBackground: true - property bool verbose: true - property QtObject resources: QtObject { - property bool alwaysShowSwap: true - property bool alwaysShowCpu: false - } - property list screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command - property QtObject utilButtons: QtObject { - property bool showScreenSnip: true - property bool showColorPicker: false - property bool showMicToggle: false - property bool showKeyboardToggle: true - } - property QtObject tray: QtObject { - property bool monochromeIcons: true - } - property QtObject workspaces: QtObject { - property int shown: 10 - property bool showAppIcons: true - property bool alwaysShowNumbers: false - property int showNumberDelay: 300 // milliseconds - } - } - - property QtObject battery: QtObject { - property int low: 20 - property int critical: 5 - property int suspend: 2 - } - - property QtObject dock: QtObject { - property real height: 60 - property real hoverRegionHeight: 3 - property bool pinnedOnStartup: false - property bool hoverToReveal: false // When false, only reveals on empty workspace - property list pinnedApps: [ // IDs of pinned entries - "org.kde.dolphin", - "kitty", - ] - } - - property QtObject language: QtObject { - property QtObject translator: QtObject { - property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google - property string targetLanguage: "auto" // Run `trans -list-all` for available languages - property string sourceLanguage: "auto" - } - } - - property QtObject networking: QtObject { - property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" - } - - property QtObject osd: QtObject { - property int timeout: 1000 - } - - property QtObject osk: QtObject { - property string layout: "qwerty_full" - property bool pinnedOnStartup: false - } - - property QtObject overview: QtObject { - property real scale: 0.18 // Relative to screen size - property real numOfRows: 2 - property real numOfCols: 5 - property bool showXwaylandIndicator: true - } - - property QtObject resources: QtObject { - property int updateInterval: 3000 - } - - property QtObject search: QtObject { - property int nonAppResultDelay: 30 // This prevents lagging when typing - property string engineBaseUrl: "https://www.google.com/search?q=" - property list excludedSites: [ "quora.com" ] - property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird. - property QtObject prefix: QtObject { - property string action: "/" - property string clipboard: ";" - property string emojis: ":" - } - } - - property QtObject sidebar: QtObject { - property QtObject translator: QtObject { - property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag. - } - property QtObject booru: QtObject { - property bool allowNsfw: false - property string defaultProvider: "yandere" - property int limit: 20 - property QtObject zerochan: QtObject { - property string username: "[unset]" - } - } - } - - property QtObject time: QtObject { - // https://doc.qt.io/qt-6/qtime.html#toString - property string format: "hh:mm" - property string dateFormat: "dddd, dd/MM" - } - - property QtObject windows: QtObject { - property bool showTitlebar: true // Client-side decoration for shell apps - } - - property QtObject hacks: QtObject { - property int arbitraryRaceConditionDelay: 20 // milliseconds - } - -} diff --git a/.config/quickshell/modules/common/PersistentStates.qml b/.config/quickshell/modules/common/PersistentStates.qml deleted file mode 100644 index 6887b479..00000000 --- a/.config/quickshell/modules/common/PersistentStates.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick -import Quickshell -pragma Singleton -pragma ComponentBehavior: Bound - -Singleton { - property QtObject ai: QtObject { - property string model - property real temperature: 0.5 - } - - property QtObject sidebar: QtObject { - property QtObject bottomGroup: QtObject { - property bool collapsed: false - } - } - - property QtObject booru: QtObject { - property bool allowNsfw: false - property string provider: "yandere" - } - -} diff --git a/.config/quickshell/modules/common/functions/color_utils.js b/.config/quickshell/modules/common/functions/color_utils.js deleted file mode 100644 index eb0fc0c2..00000000 --- a/.config/quickshell/modules/common/functions/color_utils.js +++ /dev/null @@ -1,109 +0,0 @@ -// This module provides high level utility functions for color manipulation. - -/** - * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take hue from. - * @returns {Qt.rgba} The resulting color. - */ -function colorWithHueOf(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 - var hue = c2.hsvHue; - var sat = c1.hsvSaturation; - var val = c1.hsvValue; - var alpha = c1.a; - - return Qt.hsva(hue, sat, val, alpha); -} - -/** - * Returns a color with the saturation of color2 and the hue/value/alpha of color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take saturation from. - * @returns {Qt.rgba} The resulting color. - */ -function colorWithSaturationOf(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - var hue = c1.hsvHue; - var sat = c2.hsvSaturation; - var val = c1.hsvValue; - var alpha = c1.a; - - return Qt.hsva(hue, sat, val, alpha); -} - -/** - * Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL). - * - * @param {string} color - The base color (any Qt.color-compatible string). - * @param {number} lightness - The lightness value to use (0-1). - * @returns {Qt.rgba} The resulting color. - */ -function colorWithLightness(color, lightness) { - var c = Qt.color(color); - return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a); -} - -/** - * Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL). - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The color to take lightness from. - * @returns {Qt.rgba} The resulting color. - */ -function colorWithLightnessOf(color1, color2) { - var c2 = Qt.color(color2); - return colorWithLightness(color1, c2.hslLightness); -} - -/** - * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. - * - * @param {string} color1 - The base color (any Qt.color-compatible string). - * @param {string} color2 - The accent color. - * @returns {Qt.rgba} The resulting color. - */ -function adaptToAccent(color1, color2) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - - var hue = c2.hslHue; - var sat = c2.hslSaturation; - var light = c1.hslLightness; - var alpha = c1.a; - - return Qt.hsla(hue, sat, light, alpha); -} - -/** - * Mixes two colors by a given percentage. - * - * @param {string} color1 - The first color (any Qt.color-compatible string). - * @param {string} color2 - The second color. - * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. - * @returns {Qt.rgba} The resulting mixed color. - */ -function mix(color1, color2, percentage = 0.5) { - var c1 = Qt.color(color1); - var c2 = Qt.color(color2); - return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); -} - -/** - * Transparentizes a color by a given percentage. - * - * @param {string} color - The color (any Qt.color-compatible string). - * @param {number} percentage - The amount to transparentize (0-1). - * @returns {Qt.rgba} The resulting color. - */ -function transparentize(color, percentage = 1) { - var c = Qt.color(color); - return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); -} diff --git a/.config/quickshell/modules/common/functions/file_utils.js b/.config/quickshell/modules/common/functions/file_utils.js deleted file mode 100644 index 758950de..00000000 --- a/.config/quickshell/modules/common/functions/file_utils.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Trims the File protocol off the input string - * @param {string} str - * @returns {string} - */ -function trimFileProtocol(str) { - return str.startsWith("file://") ? str.slice(7) : str; -} - diff --git a/.config/quickshell/modules/common/functions/object_utils.js b/.config/quickshell/modules/common/functions/object_utils.js deleted file mode 100644 index e65a4540..00000000 --- a/.config/quickshell/modules/common/functions/object_utils.js +++ /dev/null @@ -1,91 +0,0 @@ -function toPlainObject(qtObj) { - if (qtObj === null || typeof qtObj !== "object") return qtObj; - - // Handle true arrays - if (Array.isArray(qtObj)) { - return qtObj.map(item => toPlainObject(item)); - } - - // Handle array-like Qt objects (e.g., have length and numeric keys) - if ( - typeof qtObj.length === "number" && - qtObj.length > 0 && - Object.keys(qtObj).every( - key => !isNaN(key) || key === "length" - ) - ) { - let arr = []; - for (let i = 0; i < qtObj.length; i++) { - arr.push(toPlainObject(qtObj[i])); - } - return arr; - } - - const result = ({}); - for (let key in qtObj) { - if ( - typeof qtObj[key] !== "function" && - !key.startsWith("objectName") && - !key.startsWith("children") && - !key.startsWith("object") && - !key.startsWith("parent") && - !key.startsWith("metaObject") && - !key.startsWith("destroyed") && - !key.startsWith("reloadableId") - ) { - result[key] = toPlainObject(qtObj[key]); - } - } - // console.log(JSON.stringify(result)) - return result; -} - -function applyToQtObject(qtObj, jsonObj) { - // console.log("applyToQtObject", JSON.stringify(qtObj, null, 2), "<<", JSON.stringify(jsonObj, null, 2)); - if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return; - - // Detect array-like Qt objects - const isQtArrayLike = obj => { - return obj && typeof obj === "object" && - typeof obj.length === "number" && - obj.length > 0 && - Object.keys(obj).every(key => !isNaN(key) || key === "length"); - }; - - // If both are arrays or array-like, update in place or replace - if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) { - qtObj.length = 0; - for (let i = 0; i < jsonObj.length; i++) { - qtObj.push(jsonObj[i]); - } - return; - } - - // If target is array or array-like but source is not, clear - if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && !Array.isArray(jsonObj)) { - qtObj.length = 0; - return; - } - - // If source is array but target is not, assign directly if possible - if (!(Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) { - return jsonObj; - } - - for (let key in jsonObj) { - if (!qtObj.hasOwnProperty(key)) continue; - const value = qtObj[key]; - const jsonValue = jsonObj[key]; - // console.log("applying to qt obj key:", value, "jsonValue:", jsonValue); - if ((Array.isArray(value) || isQtArrayLike(value)) && Array.isArray(jsonValue)) { - value.length = 0; - for (let i = 0; i < jsonValue.length; i++) { - value.push(jsonValue[i]); - } - } else if (value && typeof value === "object" && !Array.isArray(value) && !isQtArrayLike(value)) { - applyToQtObject(value, jsonValue); - } else { - qtObj[key] = jsonValue; - } - } -} diff --git a/.config/quickshell/modules/common/functions/string_utils.js b/.config/quickshell/modules/common/functions/string_utils.js deleted file mode 100644 index c22671eb..00000000 --- a/.config/quickshell/modules/common/functions/string_utils.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Formats a string according to the args that are passed in - * @param { string } str - * @param {...any} args - * @returns - */ -function format(str, ...args) { - return str.replace(/{(\d+)}/g, (match, index) => - typeof args[index] !== 'undefined' ? args[index] : match - ); -} - -/** - * Returns the domain of the passed in url or null - * @param { string } url - * @returns { string| null } - */ -function getDomain(url) { - const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); - return match ? match[1] : null; -} - -/** - * Returns the base url of the passed in url or null - * @param { string } url - * @returns { string | null } - */ -function getBaseUrl(url) { - const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); - return match ? match[1] : null; -} - -/** - * Escapes single quotes in shell commands - * @param { string } str - * @returns { string } - */ -function shellSingleQuoteEscape(str) { - // escape single quotes - return String(str) - // .replace(/\\/g, '\\\\') - .replace(/'/g, "'\\''"); -} - -/** - * Splits markdown blocks into three different types: text, think, and code. - * @param { string } markdown - */ -function splitMarkdownBlocks(markdown) { - const regex = /```(\w+)?\n([\s\S]*?)```|([\s\S]*?)<\/think>/g; - /** - * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]} - */ - let result = []; - let lastIndex = 0; - let match; - while ((match = regex.exec(markdown)) !== null) { - if (match.index > lastIndex) { - const text = markdown.slice(lastIndex, match.index); - if (text.trim()) { - result.push({ type: "text", content: text }); - } - } - if (match[0].startsWith('```')) { - if (match[2] && match[2].trim()) { - result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true }); - } - } else if (match[0].startsWith('')) { - if (match[3] && match[3].trim()) { - result.push({ type: "think", content: match[3], completed: true }); - } - } - lastIndex = regex.lastIndex; - } - // Handle any remaining text after the last match - if (lastIndex < markdown.length) { - const text = markdown.slice(lastIndex); - // Check for unfinished block - const thinkStart = text.indexOf(''); - const codeStart = text.indexOf('```'); - if ( - thinkStart !== -1 && - (codeStart === -1 || thinkStart < codeStart) - ) { - const beforeThink = text.slice(0, thinkStart); - if (beforeThink.trim()) { - result.push({ type: "text", content: beforeThink }); - } - const thinkContent = text.slice(thinkStart + 7); - if (thinkContent.trim()) { - result.push({ type: "think", content: thinkContent, completed: false }); - } - } else if (codeStart !== -1) { - const beforeCode = text.slice(0, codeStart); - if (beforeCode.trim()) { - result.push({ type: "text", content: beforeCode }); - } - // Try to detect language after ``` - const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/); - let lang = ""; - let codeContentStart = codeStart + 3; - if (codeLangMatch) { - lang = codeLangMatch[1] || ""; - codeContentStart += codeLangMatch[0].length; - } else if (text[codeStart + 3] === '\n') { - codeContentStart += 1; - } - const codeContent = text.slice(codeContentStart); - if (codeContent.trim()) { - result.push({ type: "code", lang, content: codeContent, completed: false }); - } - } else if (text.trim()) { - result.push({ type: "text", content: text }); - } - } - // console.log(JSON.stringify(result, null, 2)); - return result; -} - -/** - * Returns the original string with backslashes escaped - * @param { string } str - * @returns { string } - */ -function escapeBackslashes(str) { - return str.replace(/\\/g, '\\\\'); -} - -/** - * Wraps words to supplied maximum length - * @param { string | null } str - * @param { number } maxLen - * @returns { string } - */ -function wordWrap(str, maxLen) { - if (!str) return ""; - let words = str.split(" "); - let lines = []; - let current = ""; - for (let i = 0; i < words.length; ++i) { - if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) { - if (current.length > 0) lines.push(current); - current = words[i]; - } else { - current += (current.length > 0 ? " " : "") + words[i]; - } - } - if (current.length > 0) lines.push(current); - return lines.join("\n"); -} - -function cleanMusicTitle(title) { - if (!title) return ""; - // Brackets - title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets - title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets - title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets - // Japenis brackets - title = title.replace(/^ *【[^】]*】/, "") // Touhou - title = title.replace(/^ *《[^》]*》/, "") // ?? - title = title.replace(/^ *「[^」]*」/, "") // OP/ED - title = title.replace(/^ *『[^』]*』/, "") // OP/ED - - return title; -} - -function friendlyTimeForSeconds(seconds) { - if (isNaN(seconds) || seconds < 0) return "0:00"; - seconds = Math.floor(seconds); - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = seconds % 60; - if (h > 0) { - return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; - } else { - return `${m}:${s.toString().padStart(2, '0')}`; - } -} - -function escapeHtml(str) { - if (typeof str !== 'string') return str; - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} diff --git a/.config/quickshell/modules/common/widgets/CustomIcon.qml b/.config/quickshell/modules/common/widgets/CustomIcon.qml deleted file mode 100644 index 8905b071..00000000 --- a/.config/quickshell/modules/common/widgets/CustomIcon.qml +++ /dev/null @@ -1,24 +0,0 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets - -Item { - id: root - - property string source: "" - property string iconFolder: "root:/assets/icons" // The folder to check first - width: 30 - height: 30 - - IconImage { - id: iconImage - anchors.fill: parent - source: { - if (iconFolder && iconFolder + "/" + root.source) { - return iconFolder + "/" + root.source - } - return root.source - } - implicitSize: root.height - } -} diff --git a/.config/quickshell/modules/common/widgets/NavRailButton.qml b/.config/quickshell/modules/common/widgets/NavRailButton.qml deleted file mode 100644 index 02c6bc4a..00000000 --- a/.config/quickshell/modules/common/widgets/NavRailButton.qml +++ /dev/null @@ -1,66 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/modules/common/functions/color_utils.js" as ColorUtils -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Io - -/** - * Old implementation. For future widgets use NavigationRailButton instead. - */ -Button { - id: button - - property bool toggled - property string buttonIcon - property string buttonText - property bool expanded: false - - property real baseSize: 56 - - Layout.alignment: Qt.AlignHCenter - implicitHeight: columnLayout.implicitHeight - implicitWidth: columnLayout.implicitWidth - - background: null - PointingHandInteraction {} - - // Real stuff - ColumnLayout { - id: columnLayout - spacing: 5 - Rectangle { - implicitWidth: 56 - implicitHeight: navRailButtonIcon.height + 2 * 2 - Layout.alignment: Qt.AlignHCenter - radius: Appearance.rounding.full - color: toggled ? - (button.down ? Appearance.colors.colSecondaryContainerActive : button.hovered ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer) : - (button.down ? Appearance.colors.colLayer1Active : button.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)) - - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - MaterialSymbol { - id: navRailButtonIcon - anchors.centerIn: parent - iconSize: Appearance.font.pixelSize.hugeass - fill: toggled ? 1 : 0 - text: buttonIcon - color: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1 - - Behavior on color { - animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) - } - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: buttonText - color: Appearance.colors.colOnLayer1 - } - } - -} diff --git a/.config/quickshell/modules/common/widgets/StyledSlider.qml b/.config/quickshell/modules/common/widgets/StyledSlider.qml deleted file mode 100644 index ca098003..00000000 --- a/.config/quickshell/modules/common/widgets/StyledSlider.qml +++ /dev/null @@ -1,113 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Widgets - -// Material 3 slider. See https://m3.material.io/components/sliders/overview -Slider { - id: root - property real scale: 0.85 - property real backgroundDotSize: 4 * scale - property real backgroundDotMargins: 4 * scale - // property real handleMargins: 0 * scale - property real handleMargins: (root.pressed ? 0 : 2) * scale - property real handleWidth: (root.pressed ? 3 : 5) * scale - property real handleHeight: 44 * scale - property real handleLimit: root.backgroundDotMargins - property real trackHeight: 30 * scale - property color highlightColor: Appearance.colors.colPrimary - property color trackColor: Appearance.colors.colSecondaryContainer - property color handleColor: Appearance.m3colors.m3onSecondaryContainer - property real trackRadius: Appearance.rounding.verysmall * scale - property real unsharpenRadius: Appearance.rounding.unsharpen - - property real limitedHandleRangeWidth: (root.availableWidth - handleWidth - root.handleLimit * 2) - property string tooltipContent: `${Math.round(value * 100)}%` - Layout.fillWidth: true - from: 0 - to: 1 - - Behavior on value { // This makes the adjusted value (like volume) shift smoothly - SmoothedAnimation { - velocity: Appearance.animation.elementMoveFast.velocity - } - } - - Behavior on handleMargins { - NumberAnimation { - duration: Appearance.animation.elementMoveFast.duration - easing.type: Appearance.animation.elementMoveFast.type - easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve - } - } - - MouseArea { - anchors.fill: parent - onPressed: (mouse) => mouse.accepted = false - cursorShape: root.pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor - } - - background: Item { - anchors.verticalCenter: parent.verticalCenter - implicitHeight: trackHeight - - // Fill left - Rectangle { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - width: root.handleLimit * 2 + root.visualPosition * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2) - height: trackHeight - color: root.highlightColor - topLeftRadius: root.trackRadius - bottomLeftRadius: root.trackRadius - topRightRadius: root.unsharpenRadius - bottomRightRadius: root.unsharpenRadius - } - - // Fill right - Rectangle { - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - width: root.handleLimit * 2 + (1 - root.visualPosition) * root.limitedHandleRangeWidth - (root.handleMargins + root.handleWidth / 2) - height: trackHeight - color: root.trackColor - topLeftRadius: root.unsharpenRadius - bottomLeftRadius: root.unsharpenRadius - topRightRadius: root.trackRadius - bottomRightRadius: root.trackRadius - } - - // Dot at the end - Rectangle { - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: root.backgroundDotMargins - width: root.backgroundDotSize - height: root.backgroundDotSize - radius: Appearance.rounding.full - color: root.handleColor - } - } - - handle: Rectangle { - id: handle - x: root.leftPadding + root.handleLimit + root.visualPosition * root.limitedHandleRangeWidth - y: root.topPadding + root.availableHeight / 2 - height / 2 - implicitWidth: root.handleWidth - implicitHeight: root.handleHeight - radius: Appearance.rounding.full - color: root.handleColor - - Behavior on implicitWidth { - animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) - } - - StyledToolTip { - extraVisibleCondition: root.pressed - content: root.tooltipContent - } - } -} \ No newline at end of file diff --git a/.config/quickshell/modules/dock/Dock.qml b/.config/quickshell/modules/dock/Dock.qml deleted file mode 100644 index 40370813..00000000 --- a/.config/quickshell/modules/dock/Dock.qml +++ /dev/null @@ -1,148 +0,0 @@ -import "root:/" -import "root:/services" -import "root:/modules/common" -import "root:/modules/common/widgets" -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell.Io -import Quickshell -import Quickshell.Widgets -import Quickshell.Wayland -import Quickshell.Hyprland - -Scope { // Scope - id: root - property bool pinned: ConfigOptions?.dock.pinnedOnStartup ?? false - - Variants { // For each monitor - model: Quickshell.screens - - LazyLoader { - id: dockLoader - required property var modelData - activeAsync: ConfigOptions?.dock.hoverToReveal || (!ToplevelManager.activeToplevel?.activated) - - component: PanelWindow { // Window - id: dockRoot - screen: dockLoader.modelData - - property bool reveal: root.pinned - || (ConfigOptions?.dock.hoverToReveal && dockMouseArea.containsMouse) - || dockApps.requestDockShow - || (!ToplevelManager.activeToplevel?.activated) - - anchors { - bottom: true - left: true - right: true - } - - exclusiveZone: root.pinned ? implicitHeight - - (Appearance.sizes.hyprlandGapsOut) - - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0 - - implicitWidth: dockBackground.implicitWidth - WlrLayershell.namespace: "quickshell:dock" - color: "transparent" - - implicitHeight: (ConfigOptions?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut - - mask: Region { - item: dockMouseArea - } - - MouseArea { - id: dockMouseArea - anchors.top: parent.top - height: parent.height - anchors.topMargin: dockRoot.reveal ? 0 : - ConfigOptions?.dock.hoverToReveal ? (dockRoot.implicitHeight - ConfigOptions.dock.hoverRegionHeight) : - (dockRoot.implicitHeight + 1) - - anchors.left: parent.left - anchors.right: parent.right - hoverEnabled: true - - Behavior on anchors.topMargin { - animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) - } - - Item { - id: dockHoverRegion - anchors.fill: parent - - Item { // Wrapper for the dock background - id: dockBackground - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - } - - implicitWidth: dockRow.implicitWidth + 5 * 2 - height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut - - StyledRectangularShadow { - target: dockVisualBackground - } - Rectangle { // The real rectangle that is visible - id: dockVisualBackground - property real margin: Appearance.sizes.elevationMargin - anchors.fill: parent - anchors.topMargin: Appearance.sizes.elevationMargin - anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut - color: Appearance.colors.colLayer0 - radius: Appearance.rounding.large - } - - RowLayout { - id: dockRow - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - spacing: 3 - property real padding: 5 - - VerticalButtonGroup { - Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work - GroupButton { // Pin button - baseWidth: 35 - baseHeight: 35 - clickedWidth: baseWidth - clickedHeight: baseHeight + 20 - buttonRadius: Appearance.rounding.normal - toggled: root.pinned - onClicked: root.pinned = !root.pinned - contentItem: MaterialSymbol { - text: "keep" - horizontalAlignment: Text.AlignHCenter - iconSize: Appearance.font.pixelSize.larger - color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0 - } - } - } - DockSeparator {} - DockApps { id: dockApps; } - DockSeparator {} - DockButton { - Layout.fillHeight: true - onClicked: Hyprland.dispatch("global quickshell:overviewToggle") - contentItem: MaterialSymbol { - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - font.pixelSize: parent.width / 2 - text: "apps" - color: Appearance.colors.colOnLayer0 - } - } - } - } - } - - } - } - } - } -} diff --git a/.config/quickshell/modules/screenCorners/ScreenCorners.qml b/.config/quickshell/modules/screenCorners/ScreenCorners.qml deleted file mode 100644 index 3988d73d..00000000 --- a/.config/quickshell/modules/screenCorners/ScreenCorners.qml +++ /dev/null @@ -1,87 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland - -Scope { - id: screenCorners - readonly property Toplevel activeWindow: ToplevelManager.activeToplevel - - Variants { - model: Quickshell.screens - - PanelWindow { - visible: (ConfigOptions.appearance.fakeScreenRounding === 1 - || (ConfigOptions.appearance.fakeScreenRounding === 2 - && !activeWindow?.fullscreen)) - - property var modelData - - screen: modelData - exclusionMode: ExclusionMode.Ignore - mask: Region { - item: null - } - HyprlandWindow.visibleMask: Region { - Region { - item: topLeftCorner - } - Region { - item: topRightCorner - } - Region { - item: bottomLeftCorner - } - Region { - item: bottomRightCorner - } - } - WlrLayershell.namespace: "quickshell:screenCorners" - WlrLayershell.layer: WlrLayer.Overlay - color: "transparent" - - anchors { - top: true - left: true - right: true - bottom: true - } - - RoundCorner { - id: topLeftCorner - anchors.top: parent.top - anchors.left: parent.left - size: Appearance.rounding.screenRounding - corner: cornerEnum.topLeft - } - RoundCorner { - id: topRightCorner - anchors.top: parent.top - anchors.right: parent.right - size: Appearance.rounding.screenRounding - corner: cornerEnum.topRight - } - RoundCorner { - id: bottomLeftCorner - anchors.bottom: parent.bottom - anchors.left: parent.left - size: Appearance.rounding.screenRounding - corner: cornerEnum.bottomLeft - } - RoundCorner { - id: bottomRightCorner - anchors.bottom: parent.bottom - anchors.right: parent.right - size: Appearance.rounding.screenRounding - corner: cornerEnum.bottomRight - } - - } - - } - -} diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml b/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml deleted file mode 100644 index 1cd56acc..00000000 --- a/.config/quickshell/modules/sidebarRight/quickToggles/GameMode.qml +++ /dev/null @@ -1,26 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "../" -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland - -QuickToggleButton { - property bool enabled: false - buttonIcon: "gamepad" - toggled: enabled - - onClicked: { - enabled = !enabled - if (enabled) { - // gameModeOn.running = true - Hyprland.dispatch(`exec hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`) - } else { - Hyprland.dispatch("exec hyprctl reload") - } - } - - StyledToolTip { - content: qsTr("Game mode") - } -} \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml b/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml deleted file mode 100644 index 72df3e1e..00000000 --- a/.config/quickshell/modules/sidebarRight/quickToggles/NightLight.qml +++ /dev/null @@ -1,42 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "../" -import Quickshell.Io -import Quickshell - -QuickToggleButton { - id: nightLightButton - property bool enabled: false - toggled: enabled - buttonIcon: "nightlight" - onClicked: { - nightLightButton.enabled = !nightLightButton.enabled - if (enabled) { - nightLightOn.startDetached() - } - else { - nightLightOff.startDetached() - } - } - Process { - id: nightLightOn - command: ["gammastep"] - } - Process { - id: nightLightOff - command: ["pkill", "gammastep"] - } - Process { - id: updateNightLightState - running: true - command: ["pidof", "gammastep"] - stdout: SplitParser { - onRead: (data) => { // if not empty then set toggled to true - nightLightButton.enabled = data.length > 0 - } - } - } - StyledToolTip { - content: qsTr("Night Light") - } -} diff --git a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml b/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml deleted file mode 100644 index c4600c7b..00000000 --- a/.config/quickshell/modules/sidebarRight/volumeMixer/VolumeMixerEntry.qml +++ /dev/null @@ -1,65 +0,0 @@ -import "root:/modules/common" -import "root:/modules/common/widgets" -import "root:/services" -import Qt5Compat.GraphicalEffects -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Pipewire - -Item { - id: root - required property PwNode node; - PwObjectTracker { objects: [ node ] } - - implicitHeight: rowLayout.implicitHeight - - RowLayout { - id: rowLayout - anchors.fill: parent - spacing: 10 - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - RowLayout { - StyledText { - Layout.fillWidth: true - font.pixelSize: Appearance.font.pixelSize.normal - elide: Text.ElideRight - text: { - // application.name -> description -> name - const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name); - const media = root.node.properties["media.name"]; - return media != undefined ? `${app} • ${media}` : app; - } - } - } - - RowLayout { - Image { - property real size: slider.trackHeight * 1.3 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - visible: source != "" - sourceSize.width: size - sourceSize.height: size - source: { - let icon; - icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]); - if (AppSearch.iconExists(icon)) return Quickshell.iconPath(icon, "image-missing"); - icon = AppSearch.guessIcon(root.node.properties["node.name"]); - return Quickshell.iconPath(icon, "image-missing"); - } - } - StyledSlider { - id: slider - value: root.node.audio.volume - onValueChanged: root.node.audio.volume = value - } - } - } - } -} \ No newline at end of file diff --git a/.config/quickshell/scripts/colors/random_konachan_wall.sh b/.config/quickshell/scripts/colors/random_konachan_wall.sh deleted file mode 100755 index 52853d09..00000000 --- a/.config/quickshell/scripts/colors/random_konachan_wall.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -mkdir -p ~/Pictures/Wallpapers -page=$((1 + RANDOM % 1000)); -response=$(curl "https://konachan.com/post.json?tags=rating%3Asafe&limit=1&page=$page") -link=$(echo "$response" | jq '.[0].file_url' -r); -ext=$(echo "$link" | awk -F. '{print $NF}') -downloadPath="$HOME/Pictures/Wallpapers/konachan_random_image.$ext" -curl "$link" -o "$downloadPath" -~/.config/quickshell/scripts/colors/switchwall.sh --image "$downloadPath" diff --git a/.config/quickshell/services/Battery.qml b/.config/quickshell/services/Battery.qml deleted file mode 100644 index 08bdee7c..00000000 --- a/.config/quickshell/services/Battery.qml +++ /dev/null @@ -1,30 +0,0 @@ -pragma Singleton - -import "root:/modules/common" -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import Quickshell.Services.UPower - -Singleton { - property bool available: UPower.displayDevice.isLaptopBattery - property var chargeState: UPower.displayDevice.state - property bool isCharging: chargeState == UPowerDeviceState.Charging - property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge - property real percentage: UPower.displayDevice.percentage - - property bool isLow: percentage <= ConfigOptions.battery.low / 100 - property bool isCritical: percentage <= ConfigOptions.battery.critical / 100 - property bool isSuspending: percentage <= ConfigOptions.battery.suspend / 100 - - property bool isLowAndNotCharging: isLow && !isCharging - property bool isCriticalAndNotCharging: isCritical && !isCharging - - onIsLowAndNotChargingChanged: { - if (available && isLowAndNotCharging) Hyprland.dispatch(`exec notify-send "Low battery" "Consider plugging in your device" -u critical -a "Shell"`) - } - - onIsCriticalAndNotChargingChanged: { - if (available && isCriticalAndNotCharging) Hyprland.dispatch(`exec notify-send "Critically low battery" "🙏 I beg for pleas charg\nAutomatic suspend triggers at ${ConfigOptions.battery.suspend}%" -u critical -a "Shell"`) - } -} diff --git a/.config/quickshell/services/ConfigLoader.qml b/.config/quickshell/services/ConfigLoader.qml deleted file mode 100644 index 347e8f40..00000000 --- a/.config/quickshell/services/ConfigLoader.qml +++ /dev/null @@ -1,138 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import "root:/modules/common" -import "root:/modules/common/functions/file_utils.js" as FileUtils -import "root:/modules/common/functions/string_utils.js" as StringUtils -import "root:/modules/common/functions/object_utils.js" as ObjectUtils -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import Qt.labs.platform - -/** - * Loads and manages the shell configuration file. - * The config file is by default at XDG_CONFIG_HOME/illogical-impulse/config.json. - * Automatically reloaded when the file changes. - */ -Singleton { - id: root - property string filePath: Directories.shellConfigPath - property bool firstLoad: true - property bool preventNextLoad: false - property var preventNextNotification: false - - function loadConfig() { - configFileView.reload() - } - - function applyConfig(fileContent) { - try { - if (fileContent.trim() === "") { - console.warn("[ConfigLoader] Config file is empty, skipping load."); - return; - } - const json = JSON.parse(fileContent); - - ObjectUtils.applyToQtObject(ConfigOptions, json); - if (root.firstLoad) { - root.firstLoad = false; - root.preventNextLoad = true; - root.saveConfig(); // Make sure new properties are added to the user's config file - } - } catch (e) { - console.error("[ConfigLoader] Error reading file:", e); - console.log("[ConfigLoader] File content was:", fileContent); - Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) - return; - - } - } - - function setLiveConfigValue(nestedKey, value) { - let keys = nestedKey.split("."); - let obj = ConfigOptions; - let parents = [obj]; - - // Traverse and collect parent objects - for (let i = 0; i < keys.length - 1; ++i) { - if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { - obj[keys[i]] = {}; - } - obj = obj[keys[i]]; - parents.push(obj); - } - - // Convert value to correct type using JSON.parse when safe - let convertedValue = value; - if (typeof value === "string") { - let trimmed = value.trim(); - if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) { - try { - convertedValue = JSON.parse(trimmed); - } catch (e) { - convertedValue = value; - } - } - } - - obj[keys[keys.length - 1]] = convertedValue; - } - - function saveConfig() { - const plainConfig = ObjectUtils.toPlainObject(ConfigOptions) - Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`) - } - - function setConfigValueAndSave(nestedKey, value, preventNextNotification = true) { - setLiveConfigValue(nestedKey, value); - root.preventNextNotification = preventNextNotification; - saveConfig(); - } - - Timer { - id: delayedFileRead - interval: ConfigOptions.hacks.arbitraryRaceConditionDelay - running: false - onTriggered: { - if (root.preventNextLoad) { - root.preventNextLoad = false; - return; - } - if (root.firstLoad) { - root.applyConfig(configFileView.text()) - } else { - root.applyConfig(configFileView.text()) - if (!root.preventNextNotification) { - // Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration reloaded")}" "${root.filePath}"`) - } else { - root.preventNextNotification = false; - } - } - } - } - - FileView { - id: configFileView - path: Qt.resolvedUrl(root.filePath) - watchChanges: true - onFileChanged: { - this.reload() - delayedFileRead.start() - } - onLoadedChanged: { - const fileContent = configFileView.text() - delayedFileRead.start() - } - onLoadFailed: (error) => { - if(error == FileViewError.FileNotFound) { - console.log("[ConfigLoader] File not found, creating new file.") - root.saveConfig() - Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`) - } else { - Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) - } - } - } -} diff --git a/.config/quickshell/services/HyprlandData.qml b/.config/quickshell/services/HyprlandData.qml deleted file mode 100644 index 2b88ad9c..00000000 --- a/.config/quickshell/services/HyprlandData.qml +++ /dev/null @@ -1,69 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Hyprland - -/** - * Provides access to some Hyprland data not available in Quickshell.Hyprland. - */ -Singleton { - id: root - property var windowList: [] - property var addresses: [] - property var windowByAddress: ({}) - property var monitors: [] - - function updateWindowList() { - getClients.running = true - getMonitors.running = true - } - - Component.onCompleted: { - updateWindowList() - } - - Connections { - target: Hyprland - - function onRawEvent(event) { - // Filter out redundant old v1 events for the same thing - if(event.name in [ - "activewindow", "focusedmon", "monitoradded", - "createworkspace", "destroyworkspace", "moveworkspace", - "activespecial", "movewindow", "windowtitle" - ]) return ; - updateWindowList() - } - } - - Process { - id: getClients - command: ["bash", "-c", "hyprctl clients -j | jq -c"] - stdout: SplitParser { - onRead: (data) => { - root.windowList = JSON.parse(data) - let tempWinByAddress = {} - for (var i = 0; i < root.windowList.length; ++i) { - var win = root.windowList[i] - tempWinByAddress[win.address] = win - } - root.windowByAddress = tempWinByAddress - root.addresses = root.windowList.map((win) => win.address) - } - } - } - Process { - id: getMonitors - command: ["bash", "-c", "hyprctl monitors -j | jq -c"] - stdout: SplitParser { - onRead: (data) => { - root.monitors = JSON.parse(data) - } - } - } -} - diff --git a/.config/quickshell/services/PersistentStateManager.qml b/.config/quickshell/services/PersistentStateManager.qml deleted file mode 100644 index c3d1536e..00000000 --- a/.config/quickshell/services/PersistentStateManager.qml +++ /dev/null @@ -1,105 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import "root:/modules/common" -import "root:/modules/common/functions/object_utils.js" as ObjectUtils -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -import Qt.labs.platform - -/** - * Manages persistent states across sessions. - * Run loadStates() once at startup to load the states, then use setState() and getState() to modify and access them. - */ -Singleton { - id: root - property string fileDir: Directories.state - property string fileName: "states.json" - property string filePath: `${root.fileDir}/${root.fileName}` - property bool allowWriteback: false - - function getState(nestedKey) { - let keys = nestedKey.split("."); - let obj = PersistentStates; - for (let i = 0; i < keys.length; ++i) { - if (obj[keys[i]] === undefined) { - console.error(`[PersistentStateManager] Key "${keys[i]}" not found in PersistentStates`); - return null; - } - obj = obj[keys[i]]; - } - return obj; - } - - function setState(nestedKey, value) { - if (!root.allowWriteback) return; - let keys = nestedKey.split("."); - let obj = PersistentStates; - let parents = [obj]; - - // Traverse and collect parent objects - for (let i = 0; i < keys.length - 1; ++i) { - if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { - obj[keys[i]] = {}; - } - obj = obj[keys[i]]; - parents.push(obj); - } - - // Set the value at the innermost key - obj[keys[keys.length - 1]] = value; - - saveStates() - } - - function loadStates() { - stateFileView.reload() - } - - function saveStates() { - const plainStates = ObjectUtils.toPlainObject(PersistentStates) - stateFileView.setText(JSON.stringify(plainStates, null, 2)) - } - - function applyStates(fileContent) { - try { - const json = JSON.parse(fileContent); - ObjectUtils.applyToQtObject(PersistentStates, json); - root.allowWriteback = true - } catch (e) { - console.error("[PersistentStateManager] Error reading file:", e); - return; - } - } - - Timer { - id: delayedFileRead - interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100 - repeat: false - running: false - onTriggered: { - root.applyStates(stateFileView.text()) - } - } - - FileView { - id: stateFileView - path: root.filePath - watchChanges: true - // onFileChanged: { - // console.log("[PersistentStateManager] File changed, reloading...") - // this.reload() - // delayedFileRead.start() - // } - onLoadedChanged: { - const fileContent = stateFileView.text() - root.applyStates(fileContent) - } - onLoadFailed: (error) => { - console.log("[PersistentStateManager] File not found, creating new file") - root.saveStates() - } - } -} diff --git a/.config/quickshell/settings.qml b/.config/quickshell/settings.qml deleted file mode 100644 index fd319344..00000000 --- a/.config/quickshell/settings.qml +++ /dev/null @@ -1,162 +0,0 @@ -//@ pragma UseQApplication -//@ pragma Env QS_NO_RELOAD_POPUP=1 -//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic - -// Adjust this to make the app smaller or larger -//@ pragma Env QT_SCALE_FACTOR=1 - -import Qt5Compat.GraphicalEffects -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Window -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -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/common/functions/file_utils.js" as FileUtils -import "root:/modules/common/functions/string_utils.js" as StringUtils - -ApplicationWindow { - id: root - property string firstRunFilePath: FileUtils.trimFileProtocol(`${Directories.state}/user/first_run.txt`) - property string firstRunFileContent: "This file is just here to confirm you've been greeted :>" - property real contentPadding: 8 - property bool showNextTime: false - property int currentPage: 0 - visible: true - onClosing: Qt.quit() - title: "illogical-impulse Settings" - - Component.onCompleted: { - MaterialThemeLoader.reapplyTheme() - ConfigLoader.loadConfig() - } - - minimumWidth: 600 - minimumHeight: 400 - width: 900 - height: 650 - color: Appearance.m3colors.m3background - - component Section: ColumnLayout { - id: sectionRoot - property string title - default property alias data: sectionContent.data - - Layout.fillWidth: true - spacing: 8 - StyledText { - text: sectionRoot.title - font.pixelSize: Appearance.font.pixelSize.larger - } - ColumnLayout { - id: sectionContent - spacing: 5 - } - } - - ColumnLayout { - anchors { - fill: parent - margins: contentPadding - } - - Item { // Titlebar - visible: ConfigOptions?.windows.showTitlebar - Layout.fillWidth: true - Layout.fillHeight: false - implicitHeight: Math.max(titleText.implicitHeight, windowControlsRow.implicitHeight) - StyledText { - id: titleText - anchors.centerIn: parent - color: Appearance.colors.colOnLayer0 - text: "Settings" - font.pixelSize: Appearance.font.pixelSize.title - font.family: Appearance.font.family.title - } - RowLayout { // Window controls row - id: windowControlsRow - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - RippleButton { - buttonRadius: Appearance.rounding.full - implicitWidth: 35 - implicitHeight: 35 - onClicked: root.close() - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - text: "close" - iconSize: 20 - } - } - } - } - - RowLayout { // Window content with navigation rail and content pane - Layout.fillWidth: true - Layout.fillHeight: true - spacing: contentPadding - NavigationRail { // Window content with navigation rail and content pane - id: navRail - Layout.fillHeight: true - Layout.margins: 5 - spacing: 10 - expanded: root.width > 900 - - NavigationRailExpandButton {} - - FloatingActionButton { - id: fab - iconText: "edit" - buttonText: "Edit config" - expanded: navRail.expanded - onClicked: { - Qt.openUrlExternally(`${Directories.config}/illogical-impulse/config.json`); - } - } - - ColumnLayout { - Layout.topMargin: 25 - spacing: 4 - - NavigationRailButton { - toggled: root.currentPage === 0 - onClicked: root.currentPage = 0; - expanded: navRail.expanded - buttonIcon: "tune" - buttonText: "General" - } - NavigationRailButton { - toggled: root.currentPage === 1 - onClicked: root.currentPage = 1; - expanded: navRail.expanded - buttonIcon: "dashboard" - buttonText: "Widgets" - } - NavigationRailButton { - toggled: root.currentPage === 2 - onClicked: root.currentPage = 2; - expanded: navRail.expanded - buttonIcon: "settings" - buttonText: "Services" - } - } - - Item { - Layout.fillHeight: true - } - } - Rectangle { // Content container - Layout.fillWidth: true - Layout.fillHeight: true - color: Appearance.m3colors.m3surfaceContainerLow - radius: Appearance.rounding.windowRounding - root.contentPadding - } - } - } -} diff --git a/.config/quickshell/translations/en_US.json b/.config/quickshell/translations/en_US.json new file mode 100644 index 00000000..fe8c6453 --- /dev/null +++ b/.config/quickshell/translations/en_US.json @@ -0,0 +1,314 @@ +{ + "Mo": "Mo/*keep*/", + "Tu": "Tu/*keep*/", + "We": "We/*keep*/", + "Th": "Th/*keep*/", + "Fr": "Fr/*keep*/", + "Sa": "Sa/*keep*/", + "Su": "Su/*keep*/", + "%1 characters": "%1 characters", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "No further instruction provided": "No further instruction provided", + "Action": "Action", + "Add": "Add", + "Add task": "Add task", + "All-rounder | Good quality, decent quantity": "All-rounder | Good quality, decent quantity", + "Allow NSFW": "Allow NSFW", + "Allow NSFW content": "Allow NSFW content", + "Anime": "Anime", + "Anime boorus": "Anime boorus", + "App": "App", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel", + "Bluetooth": "Bluetooth", + "Brightness": "Brightness", + "Cancel": "Cancel", + "Chain of Thought": "Chain of Thought", + "Cheat sheet": "Cheat sheet", + "Choose model": "Choose model", + "Clean stuff | Excellent quality, no NSFW": "Clean stuff | Excellent quality, no NSFW", + "Clear": "Clear", + "Clear chat history": "Clear chat history", + "Clear the current list of images": "Clear the current list of images", + "Close": "Close", + "Copy": "Copy", + "Copy code": "Copy code", + "Delete": "Delete", + "Desktop": "Desktop", + "Disable NSFW content": "Disable NSFW content", + "Done": "Done", + "Download": "Download", + "Edit": "Edit", + "Enter text to translate...": "Enter text to translate...", + "Finished tasks will go here": "Finished tasks will go here", + "For desktop wallpapers | Good quality": "For desktop wallpapers | Good quality", + "For storing API keys and other sensitive information": "For storing API keys and other sensitive information", + "Game mode": "Game mode", + "Get the next page of results": "Get the next page of results", + "Hibernate": "Hibernate", + "Input": "Input", + "Intelligence": "Intelligence", + "Interface": "Interface", + "Invalid arguments. Must provide `key` and `value`.": "Invalid arguments. Must provide `key` and `value`.", + "Jump to current month": "Jump to current month", + "Keep system awake": "Keep system awake", + "Large images | God tier quality, no NSFW.": "Large images | God tier quality, no NSFW.", + "Large language models": "Large language models", + "Launch": "Launch", + "Lock": "Lock", + "Logout": "Logout", + "Markdown test": "Markdown test", + "Math result": "Math result", + "Night Light": "Night Light", + "No audio source": "No audio source", + "No media": "No media", + "No notifications": "No notifications", + "Not visible to model": "Not visible to model", + "Nothing here!": "Nothing here!", + "Notifications": "Notifications", + "OK": "OK", + "Open file link": "Open file link", + "Output": "Output", + "Reboot": "Reboot", + "Reboot to firmware settings": "Reboot to firmware settings", + "Reload Hyprland & Quickshell": "Reload Hyprland & Quickshell", + "Run": "Run", + "Run command": "Run command", + "Save": "Save", + "Save to Downloads": "Save to Downloads", + "Search": "Search", + "Search the web": "Search the web", + "Search, calculate or run": "Search, calculate or run", + "Select Language": "Select Language", + "Session": "Session", + "Set API key": "Set API key", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.", + "Set the current API provider": "Set the current API provider", + "Shutdown": "Shutdown", + "Silent": "Silent", + "Sleep": "Sleep", + "System": "System", + "Task Manager": "Task Manager", + "Task description": "Task description", + "Temperature must be between 0 and 2": "Temperature must be between 0 and 2", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "The hentai one | Great quantity, a lot of NSFW, quality varies wildly", + "The popular one | Best quantity, but quality can vary wildly": "The popular one | Best quantity, but quality can vary wildly", + "Thinking": "Thinking", + "Translation goes here...": "Translation goes here...", + "Translator": "Translator", + "Unfinished": "Unfinished", + "Unknown": "Unknown", + "Unknown Album": "Unknown Album", + "Unknown Artist": "Unknown Artist", + "Unknown Title": "Unknown Title", + "View Markdown source": "View Markdown source", + "Volume": "Volume", + "Volume mixer": "Volume mixer", + "Waifus only | Excellent quality, limited quantity": "Waifus only | Excellent quality, limited quantity", + "Waiting for response...": "Waiting for response...", + "Workspace": "Workspace", + "Set with /mode PROVIDER": "Set with /mode PROVIDER", + "Invalid API provider. Supported: \n-": "Invalid API provider. Supported: \n-", + "Unknown command:": "Unknown command:", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window", + "The current API used. Endpoint:": "The current API used. Endpoint:", + "Provider set to": "Provider set to", + "Invalid model. Supported: \n```": "Invalid model. Supported: \n```", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number", + "Online | Google's model\nGives up-to-date information with search.": "Online | Google's model\nGives up-to-date information with search.", + "Switched to search mode. Continue with the user's request.": "Switched to search mode. Continue with the user's request.", + "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly", + "Settings": "Settings", + "Save chat": "Save chat", + "Load chat": "Load chat", + "or": "or", + "Set the system prompt for the model.": "Set the system prompt for the model.", + "To Do": "To Do", + "Calendar": "Calendar", + "Advanced": "Advanced", + "About": "About", + "Services": "Services", + "Style": "Style", + "Edit config": "Edit config", + "Colors & Wallpaper": "Colors & Wallpaper", + "Light": "Light", + "Dark": "Dark", + "Material palette": "Material palette", + "Fidelity": "Fidelity", + "Fruit Salad": "Fruit Salad", + "Alternatively use /dark, /light, /img in the launcher": "Alternatively use /dark, /light, /img in the launcher", + "Fake screen rounding": "Fake screen rounding", + "When not fullscreen": "When not fullscreen", + "Choose file": "Choose file", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers", + "Be patient...": "Be patient...", + "Decorations & Effects": "Decorations & Effects", + "Tonal Spot": "Tonal Spot", + "Shell windows": "Shell windows", + "Auto": "Auto", + "Wallpaper": "Wallpaper", + "Content": "Content", + "Title bar": "Title bar", + "Transparency": "Transparency", + "Expressive": "Expressive", + "Yes": "Yes", + "Enable": "Enable", + "Rainbow": "Rainbow", + "Might look ass. Unsupported.": "Might look ass. Unsupported.", + "Monochrome": "Monochrome", + "Random: Konachan": "Random: Konachan", + "Center title": "Center title", + "Neutral": "Neutral", + "Pick wallpaper image on your system": "Pick wallpaper image on your system", + "No": "No", + "AI": "AI", + "Local only": "Local only", + "Policies": "Policies", + "Weeb": "Weeb", + "Closet": "Closet", + "Bar style": "Bar style", + "Show next time": "Show next time", + "Usage": "Usage", + "Plain rectangle": "Plain rectangle", + "Useless buttons": "Useless buttons", + "GitHub": "GitHub", + "Style & wallpaper": "Style & wallpaper", + "Configuration": "Configuration", + "Change any time later with /dark, /light, /img in the launcher": "Change any time later with /dark, /light, /img in the launcher", + "Keybinds": "Keybinds", + "Float": "Float", + "Hug": "Hug", + "Yooooo hi there": "Yooooo hi there", + "illogical-impulse Welcome": "illogical-impulse Welcome", + "Info": "Info", + "Volume limit": "Volume limit", + "Prevents abrupt increments and restricts volume limit": "Prevents abrupt increments and restricts volume limit", + "Resources": "Resources", + "12h am/pm": "12h am/pm", + "Base URL": "Base URL", + "Audio": "Audio", + "Networking": "Networking", + "Format": "Format", + "Time": "Time", + "Battery": "Battery", + "Prefixes": "Prefixes", + "Emojis": "Emojis", + "Earbang protection": "Earbang protection", + "Automatically suspends the system when battery is low": "Automatically suspends the system when battery is low", + "Automatic suspend": "Automatic suspend", + "Suspend at": "Suspend at", + "Max allowed increase": "Max allowed increase", + "Web search": "Web search", + "Polling interval (ms)": "Polling interval (ms)", + "Clipboard": "Clipboard", + "Low warning": "Low warning", + "24h": "24h", + "Use Levenshtein distance-based algorithm instead of fuzzy": "Use Levenshtein distance-based algorithm instead of fuzzy", + "System prompt": "System prompt", + "12h AM/PM": "12h AM/PM", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)", + "Critical warning": "Critical warning", + "User agent (for services that require it)": "User agent (for services that require it)", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.", + "Note: turning off can hurt readability": "Note: turning off can hurt readability", + "Workspaces shown": "Workspaces shown", + "Dark/Light toggle": "Dark/Light toggle", + "Dock": "Dock", + "Weather": "Weather", + "Pinned on startup": "Pinned on startup", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience", + "Appearance": "Appearance", + "Always show numbers": "Always show numbers", + "Buttons": "Buttons", + "Keyboard toggle": "Keyboard toggle", + "Scale (%)": "Scale (%)", + "Overview": "Overview", + "Rows": "Rows", + "Borderless": "Borderless", + "Screenshot tool": "Screenshot tool", + "Number show delay when pressing Super (ms)": "Number show delay when pressing Super (ms)", + "Timeout (ms)": "Timeout (ms)", + "Show app icons": "Show app icons", + "Workspaces": "Workspaces", + "Columns": "Columns", + "On-screen display": "On-screen display", + "Screen snip": "Screen snip", + "Mic toggle": "Mic toggle", + "Hover to reveal": "Hover to reveal", + "Bar": "Bar", + "Show background": "Show background", + "Show regions of potential interest": "Show regions of potential interest", + "Color picker": "Color picker", + "Help & Support": "Help & Support", + "Discussions": "Discussions", + "Color generation": "Color generation", + "Dotfiles": "Dotfiles", + "Distro": "Distro", + "Privacy Policy": "Privacy Policy", + "Documentation": "Documentation", + "Shell & utilities theming must also be enabled": "Shell & utilities theming must also be enabled", + "illogical-impulse": "illogical-impulse", + "Donate": "Donate", + "Terminal": "Terminal", + "Shell & utilities": "Shell & utilities", + "Qt apps": "Qt apps", + "Report a Bug": "Report a Bug", + "Issues": "Issues", + "Drag or click a region • LMB: Copy • RMB: Edit": "Drag or click a region • LMB: Copy • RMB: Edit", + "Current model: %1\nSet it with %2model MODEL": "Current model: %1\nSet it with %2model MODEL", + "Message the model... \"%1\" for commands": "Message the model... \"%1\" for commands", + "No API key set for %1": "No API key set for %1", + "Loaded the following system prompt\n\n---\n\n%1": "Loaded the following system prompt\n\n---\n\n%1", + "%1 | Right-click to configure": "%1 | Right-click to configure", + "API key set for %1": "API key set for %1", + "Online via %1 | %2's model": "Online via %1 | %2's model", + "Current API endpoint: %1\nSet it with %2mode PROVIDER": "Current API endpoint: %1\nSet it with %2mode PROVIDER", + "Go to source (%1)": "Go to source (%1)", + "Temperature set to %1": "Temperature set to %1", + "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3", + "Enter tags, or \"%1\" for commands": "Enter tags, or \"%1\" for commands", + "%1 queries pending": "%1 queries pending", + "API key:\n\n```txt\n%1\n```": "API key:\n\n```txt\n%1\n```", + "Uptime: %1": "Uptime: %1", + "%1 Safe Storage": "%1 Safe Storage", + "%1 does not require an API key": "%1 does not require an API key", + "Temperature: %1": "Temperature: %1", + "Model set to %1": "Model set to %1", + "Page %1": "Page %1", + "Local Ollama model | %1": "Local Ollama model | %1", + "The current system prompt is\n\n---\n\n%1": "The current system prompt is\n\n---\n\n%1", + "Unknown function call: %1": "Unknown function call: %1", + "%1 notifications": "%1 notifications", + "Load chat from %1": "Load chat from %1", + "Load prompt from %1": "Load prompt from %1", + "Save chat to %1": "Save chat to %1", + "Weather Service": "Weather Service", + "Cannot find a GPS service. Using the fallback method instead.": "Cannot find a GPS service. Using the fallback method instead.", + "Critically low battery": "Critically low battery", + "Select output device": "Select output device", + "Code saved to file": "Code saved to file", + "Online models disallowed\n\nControlled by `policies.ai` config option": "Online models disallowed\n\nControlled by `policies.ai` config option", + "Scroll to change volume": "Scroll to change volume", + "Elements": "Elements", + "%1 • %2 tasks": "%1 • %2 tasks", + "Download complete": "Download complete", + "Please charge!\nAutomatic suspend triggers at %1": "Please charge!\nAutomatic suspend triggers at %1", + "Cloudflare WARP": "Cloudflare WARP", + "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", + "Scroll to change brightness": "Scroll to change brightness", + "Connection failed. Please inspect manually with the warp-cli command": "Connection failed. Please inspect manually with the warp-cli command", + "Select input device": "Select input device", + "Registration failed. Please inspect manually with the warp-cli command": "Registration failed. Please inspect manually with the warp-cli command", + "Consider plugging in your device": "Consider plugging in your device", + "Low battery": "Low battery", + "Saved to %1": "Saved to %1", + "Sunset": "Sunset", + "UV Index": "UV Index", + "Humidity": "Humidity", + "Wind": "Wind", + "Sunrise": "Sunrise", + "Pressure": "Pressure", + "Visibility": "Visibility", + "Precipitation": "Precipitation" +} \ No newline at end of file diff --git a/.config/quickshell/translations/tools/README.md b/.config/quickshell/translations/tools/README.md new file mode 100644 index 00000000..94c21363 --- /dev/null +++ b/.config/quickshell/translations/tools/README.md @@ -0,0 +1,285 @@ +# Translation Management Tool Suite + +This suite is used to manage project translation files, automatically extract translatable texts, compare differences between language files, and provide maintenance functions. + +## Tool Components + +### 1. `translation-manager.py` - Main Translation Manager +- Extract translatable texts +- Compare and update translation files +- Interactive addition/removal of translation keys + +### 2. `translation-cleaner.py` - Translation File Maintenance Tool +- Clean unused translation keys +- Synchronize key structure across different language files + +### 3. `manage-translations.sh` - Convenient Wrapper Script +- Provides a unified command-line interface +- Displays translation status +- Simplifies common operations + +## Quick Start + +### Using the Wrapper Script (Recommended) + +```bash +# Enter the tools directory +cd .config/quickshell/translations/tools + +# Show help +./manage-translations.sh --help + +# Show current translation status +./manage-translations.sh status + +# Extract translatable texts +./manage-translations.sh extract + +# Update all translation files +./manage-translations.sh update + +# Update a specific language +./manage-translations.sh update -l zh_CN + +# Clean unused keys +./manage-translations.sh clean + +# Synchronize keys across all language files +./manage-translations.sh sync +``` + +Or run from the project root: +```bash +# Run from the project root +.config/quickshell/translations/tools/manage-translations.sh status +.config/quickshell/translations/tools/manage-translations.sh update +``` + +## Detailed Usage + +### Translation Manager (`translation-manager.py`) + +Basic usage: +```bash +# Process all languages +./translation-manager.py + +# Specify a particular language +./translation-manager.py --language zh_CN + +# Extract translatable texts only +./translation-manager.py --extract-only + +# Show extracted texts +./translation-manager.py --extract-only --show-temp +``` + +Parameter description: +- `--translations-dir`, `-t`: Translation files directory (default: `.config/quickshell/translations`) +- `--source-dir`, `-s`: Source code directory (default: `.config/quickshell`) +- `--language`, `-l`: Specify the language code to process +- `--extract-only`, `-e`: Only extract translatable texts +- `--show-temp`: Show the content of the temporary extraction file + +### Translation Cleaner (`translation-cleaner.py`) + +```bash +# Clean unused translation keys +./translation-cleaner.py --clean + +# Synchronize translation keys (using en_US as the base) +./translation-cleaner.py --sync + +# Specify a different source language for syncing +./translation-cleaner.py --sync --source-lang zh_CN + +# Clean without creating backups +./translation-cleaner.py --clean --no-backup +``` + +## Workflow + +### Regular Translation Update Workflow + +1. **Check status**: + ```bash + ./manage-translations.sh status + ``` + +2. **Update translations**: + ```bash + ./manage-translations.sh update + ``` + +3. **Clean unused keys** (optional): + ```bash + ./manage-translations.sh clean + ``` + +### Adding a New Language + +1. **Create a new language file**: + ```bash + ./manage-translations.sh update -l new_lang + ``` + +2. **Synchronize key structure**: + ```bash + ./manage-translations.sh sync + ``` + +### Cleanup After Large Refactoring + +1. **Backup translation files**: + ```bash + cp -r .config/quickshell/translations .config/quickshell/translations.backup + ``` + +2. **Clean unused keys**: + ```bash + ./manage-translations.sh clean + ``` + +3. **Synchronize all languages**: + ```bash + ./manage-translations.sh sync + ``` + +## Supported Translatable Text Formats + +The tool recognizes the following formats for translatable texts: + +```qml +// Basic format +Translation.tr("Hello, world!") +Translation.tr('Hello, world!') +Translation.tr(`Hello, world!`) + +// With line breaks +Translation.tr("Line 1\nLine 2") + +// With escape characters +Translation.tr("Say \"Hello\"") + +// With parameter placeholders +Translation.tr("Hello, %1!").arg(name) +``` + +## Example Output + +### Status Display +``` +$ ./manage-translations.sh status +Analyzing translation status... +=== Current Project Status === +166 translatable texts extracted + +=== Translation File Status === + en_US: 470 keys + zh_CN: 470 keys +``` + +### Update Translations +``` +$ ./manage-translations.sh update -l zh_CN +Updating translation files... +================================================== +Processing language: zh_CN +================================================== +Analysis result: + Missing keys: 5 + Extra keys: 20 + +Found 5 missing translation keys: +1. "New feature text" +2. "Another new text" +... + +Add these 5 missing keys? (y/n): y +5 keys added + +Found 20 extra translation keys: +1. "Removed old text" -> "已删除的旧文本" +... + +Delete these 20 extra keys? (y/n): y +20 keys deleted + +Translation file saved +``` + +### Clean Unused Keys +``` +$ ./manage-translations.sh clean +Cleaning unused translation keys... +Processing language: zh_CN +Found 50 unused keys: + 1. "old_unused_text" + 2. "deprecated_message" + ... + +Delete these 50 unused keys? (y/n): y +50 keys deleted +Original key count: 470, after cleaning: 420 +``` + +## Advanced Features + +### Custom Directory Structure + +```bash +# Use custom directories +./translation-manager.py \ + --translations-dir /path/to/translations \ + --source-dir /path/to/source +``` + +### Ignore Mark Feature + +For dynamic resources or special texts that should not be automatically cleaned, you can add `/*keep*/` at the end of the translation value. The tool will automatically ignore these keys and will not delete them during cleaning or syncing. + +Example: +```json +{ + "dynamic_key": "Some dynamic value /*keep*/" +} +``` + +## Notes + +1. **Backup is important**: The tool automatically creates backups before cleaning, but it is recommended to manually back up important files + +2. **Text extraction limitations**: + - ~~Only supports static strings, not dynamically constructed strings~~ + - Dynamic resources (such as variable concatenation or runtime-generated text) cannot be automatically extracted. You need to manually add them to the translation file and use the `/*keep*/` mark for ignore management. + - Must use the `Translation.tr()` format + +3. **File encoding**: All files must use UTF-8 encoding + +4. **Key naming conventions**: It is recommended to use English for key names and avoid special characters + +## Troubleshooting + +### Common Issues + +**Q: Text does not appear after adding Translation.tr?** +A: You need to import the translation feature in your QML file using `import "root:/"`, otherwise the translation text will not be displayed correctly. + +**Q: The number of extracted texts does not match expectations?** +A: Check whether all translatable texts use the `Translation.tr()` format and ensure there are no dynamically constructed strings. + +**Q: Some translations are missing after syncing?** +A: Check whether the source language file contains all necessary keys, and consider using a different source language for syncing. + +**Q: The cleaning operation deleted needed keys?** +A: Restore from the automatically created backup file and check whether `Translation.tr()` is used correctly in the source code. + +### Restore Backup + +```bash +# Restore a single file +cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json + +# Restore all files +cp .config/quickshell/translations.backup/* .config/quickshell/translations/ +``` diff --git a/.config/quickshell/translations/tools/guide/translation-tools-guide-zh_CN.md b/.config/quickshell/translations/tools/guide/translation-tools-guide-zh_CN.md new file mode 100644 index 00000000..821533be --- /dev/null +++ b/.config/quickshell/translations/tools/guide/translation-tools-guide-zh_CN.md @@ -0,0 +1,286 @@ +# 翻译管理工具套件 + +这套工具用于管理项目的翻译文件,自动提取可翻译文本,比较不同语言文件之间的差异,并提供维护功能。 + +## 工具组成 + +### 1. `translation-manager.py` - 主要翻译管理器 +- 提取可翻译文本 +- 比较和更新翻译文件 +- 交互式添加/删除翻译键 + +### 2. `translation-cleaner.py` - 翻译文件维护工具 +- 清理不再使用的翻译键 +- 同步不同语言文件的键结构 + +### 3. `manage-translations.sh` - 便捷包装脚本 +- 提供统一的命令行界面 +- 显示翻译状态 +- 简化常用操作 + +## 快速开始 + +### 使用便捷脚本(推荐) + +```bash +# 进入工具目录 +cd .config/quickshell/translations/tools + +# 查看帮助 +./manage-translations.sh --help + +# 显示当前翻译状态 +./manage-translations.sh status + +# 提取可翻译文本 +./manage-translations.sh extract + +# 更新所有翻译文件 +./manage-translations.sh update + +# 更新特定语言 +./manage-translations.sh update -l zh_CN + +# 清理不再使用的键 +./manage-translations.sh clean + +# 同步所有语言文件的键 +./manage-translations.sh sync +``` + +或者从项目根目录运行: +```bash +# 从项目根目录运行 +.config/quickshell/translations/tools/manage-translations.sh status +.config/quickshell/translations/tools/manage-translations.sh update +``` + +## 详细使用说明 + +### 翻译管理器 (`translation-manager.py`) + +基本用法: +```bash +# 处理所有语言 +./translation-manager.py + +# 指定特定语言 +./translation-manager.py --language zh_CN + +# 仅提取可翻译文本 +./translation-manager.py --extract-only + +# 显示提取的文本 +./translation-manager.py --extract-only --show-temp +``` + +参数说明: +- `--translations-dir`, `-t`: 翻译文件目录(默认:`.config/quickshell/translations`) +- `--source-dir`, `-s`: 源代码目录(默认:`.config/quickshell`) +- `--language`, `-l`: 指定要处理的语言代码 +- `--extract-only`, `-e`: 仅提取可翻译文本 +- `--show-temp`: 显示临时提取文件的内容 + +### 翻译清理器 (`translation-cleaner.py`) + +```bash +# 清理不再使用的翻译键 +./translation-cleaner.py --clean + +# 同步翻译键(以 en_US 为基准) +./translation-cleaner.py --sync + +# 指定不同的源语言进行同步 +./translation-cleaner.py --sync --source-lang zh_CN + +# 清理时不创建备份 +./translation-cleaner.py --clean --no-backup +``` + +## 工作流程 + +### 日常翻译更新流程 + +1. **检查状态**: + ```bash + ./manage-translations.sh status + ``` + +2. **更新翻译**: + ```bash + ./manage-translations.sh update + ``` + +3. **清理无用键**(可选): + ```bash + ./manage-translations.sh clean + ``` + +### 新增语言流程 + +1. **创建新语言文件**: + ```bash + ./manage-translations.sh update -l new_lang + ``` + +2. **同步键结构**: + ```bash + ./manage-translations.sh sync + ``` + +### 大规模重构后的清理流程 + +1. **备份翻译文件**: + ```bash + cp -r .config/quickshell/translations .config/quickshell/translations.backup + ``` + +2. **清理无用键**: + ```bash + ./manage-translations.sh clean + ``` + +3. **同步所有语言**: + ```bash + ./manage-translations.sh sync + ``` + +## 支持的翻译文本格式 + +工具可以识别以下格式的可翻译文本: + +```qml +// 基本格式 +Translation.tr("Hello, world!") +Translation.tr('Hello, world!') +Translation.tr(`Hello, world!`) + +// 带换行符 +Translation.tr("Line 1\nLine 2") + +// 带转义字符 +Translation.tr("Say \"Hello\"") + +// 带参数占位符 +Translation.tr("Hello, %1!").arg(name) +``` + +## 示例输出 + +### 状态显示 +``` +$ ./manage-translations.sh status +正在分析翻译状态... +=== 当前项目状态 === +提取到 166 个可翻译文本 + +=== 翻译文件状态 === + en_US: 470 个键 + zh_CN: 470 个键 +``` + +### 更新翻译 +``` +$ ./manage-translations.sh update -l zh_CN +更新翻译文件... +================================================== +处理语言: zh_CN +================================================== +分析结果: + 缺少的键: 5 + 多余的键: 20 + +发现 5 个缺少的翻译键: +1. "New feature text" +2. "Another new text" +... + +是否添加这 5 个缺少的键? (y/n): y +已添加 5 个键 + +发现 20 个多余的翻译键: +1. "Removed old text" -> "已删除的旧文本" +... + +是否删除这 20 个多余的键? (y/n): y +已删除 20 个键 + +已保存翻译文件 +``` + +### 清理无用键 +``` +$ ./manage-translations.sh clean +清理不再使用的翻译键... +处理语言: zh_CN +发现 50 个不再使用的键: + 1. "old_unused_text" + 2. "deprecated_message" + ... + +是否删除这 50 个不再使用的键? (y/n): y +已删除 50 个键 +原始键数: 470, 清理后: 420 +``` + +## 高级功能 + +### 自定义目录结构 + +```bash +# 使用自定义目录 +./translation-manager.py \ + --translations-dir /path/to/translations \ + --source-dir /path/to/source +``` + + +## 注意事项 + +1. **备份重要**:在执行清理操作前,工具会自动创建备份,但建议手动备份重要文件 + +2. **文本提取限制**: + - ~~只支持静态字符串,不支持动态构建的字符串~~ + - 动态资源(如变量拼接、运行时生成的文本)无法自动提取,需要在翻译文件中手动添加,并使用 `/*keep*/` 标记进行忽略管理。 + - 必须使用 `Translation.tr()` 格式 +### 忽略标记功能 + +对于动态资源或特殊文本,如果不希望被自动清理,可在翻译值末尾添加 `/*keep*/`,工具会自动忽略这些键,不会在清理和同步时删除。 + +示例: +```json +{ + "dynamic_key": "Some dynamic value /*keep*/" +} +``` + +3. **文件编码**:所有文件必须使用 UTF-8 编码 + +4. **键名规范**:建议使用英文作为键名,避免使用特殊字符 + +## 故障排除 + +### 常见问题 + + +**Q: 添加了 Translation.tr 后文字不显示?** +A: 需要在 QML 文件中使用 `import "root:/"` 导入翻译功能,否则无法正常显示翻译文本。 + +**Q: 提取的文本数量与预期不符?** +A: 检查是否所有可翻译文本都使用了 `Translation.tr()` 格式,确保没有动态构建的字符串。 + +**Q: 同步后某些翻译丢失?** +A: 检查源语言文件是否包含所有必要的键,考虑使用不同的源语言进行同步。 + +**Q: 清理操作删除了需要的键?** +A: 从自动创建的备份文件中恢复,检查源代码中是否正确使用了 `Translation.tr()`。 + +### 恢复备份 + +```bash +# 恢复单个文件 +cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json + +# 恢复所有文件 +cp .config/quickshell/translations.backup/* .config/quickshell/translations/ +``` diff --git a/.config/quickshell/translations/tools/guide/translation-tools-guide.md b/.config/quickshell/translations/tools/guide/translation-tools-guide.md new file mode 100644 index 00000000..94c21363 --- /dev/null +++ b/.config/quickshell/translations/tools/guide/translation-tools-guide.md @@ -0,0 +1,285 @@ +# Translation Management Tool Suite + +This suite is used to manage project translation files, automatically extract translatable texts, compare differences between language files, and provide maintenance functions. + +## Tool Components + +### 1. `translation-manager.py` - Main Translation Manager +- Extract translatable texts +- Compare and update translation files +- Interactive addition/removal of translation keys + +### 2. `translation-cleaner.py` - Translation File Maintenance Tool +- Clean unused translation keys +- Synchronize key structure across different language files + +### 3. `manage-translations.sh` - Convenient Wrapper Script +- Provides a unified command-line interface +- Displays translation status +- Simplifies common operations + +## Quick Start + +### Using the Wrapper Script (Recommended) + +```bash +# Enter the tools directory +cd .config/quickshell/translations/tools + +# Show help +./manage-translations.sh --help + +# Show current translation status +./manage-translations.sh status + +# Extract translatable texts +./manage-translations.sh extract + +# Update all translation files +./manage-translations.sh update + +# Update a specific language +./manage-translations.sh update -l zh_CN + +# Clean unused keys +./manage-translations.sh clean + +# Synchronize keys across all language files +./manage-translations.sh sync +``` + +Or run from the project root: +```bash +# Run from the project root +.config/quickshell/translations/tools/manage-translations.sh status +.config/quickshell/translations/tools/manage-translations.sh update +``` + +## Detailed Usage + +### Translation Manager (`translation-manager.py`) + +Basic usage: +```bash +# Process all languages +./translation-manager.py + +# Specify a particular language +./translation-manager.py --language zh_CN + +# Extract translatable texts only +./translation-manager.py --extract-only + +# Show extracted texts +./translation-manager.py --extract-only --show-temp +``` + +Parameter description: +- `--translations-dir`, `-t`: Translation files directory (default: `.config/quickshell/translations`) +- `--source-dir`, `-s`: Source code directory (default: `.config/quickshell`) +- `--language`, `-l`: Specify the language code to process +- `--extract-only`, `-e`: Only extract translatable texts +- `--show-temp`: Show the content of the temporary extraction file + +### Translation Cleaner (`translation-cleaner.py`) + +```bash +# Clean unused translation keys +./translation-cleaner.py --clean + +# Synchronize translation keys (using en_US as the base) +./translation-cleaner.py --sync + +# Specify a different source language for syncing +./translation-cleaner.py --sync --source-lang zh_CN + +# Clean without creating backups +./translation-cleaner.py --clean --no-backup +``` + +## Workflow + +### Regular Translation Update Workflow + +1. **Check status**: + ```bash + ./manage-translations.sh status + ``` + +2. **Update translations**: + ```bash + ./manage-translations.sh update + ``` + +3. **Clean unused keys** (optional): + ```bash + ./manage-translations.sh clean + ``` + +### Adding a New Language + +1. **Create a new language file**: + ```bash + ./manage-translations.sh update -l new_lang + ``` + +2. **Synchronize key structure**: + ```bash + ./manage-translations.sh sync + ``` + +### Cleanup After Large Refactoring + +1. **Backup translation files**: + ```bash + cp -r .config/quickshell/translations .config/quickshell/translations.backup + ``` + +2. **Clean unused keys**: + ```bash + ./manage-translations.sh clean + ``` + +3. **Synchronize all languages**: + ```bash + ./manage-translations.sh sync + ``` + +## Supported Translatable Text Formats + +The tool recognizes the following formats for translatable texts: + +```qml +// Basic format +Translation.tr("Hello, world!") +Translation.tr('Hello, world!') +Translation.tr(`Hello, world!`) + +// With line breaks +Translation.tr("Line 1\nLine 2") + +// With escape characters +Translation.tr("Say \"Hello\"") + +// With parameter placeholders +Translation.tr("Hello, %1!").arg(name) +``` + +## Example Output + +### Status Display +``` +$ ./manage-translations.sh status +Analyzing translation status... +=== Current Project Status === +166 translatable texts extracted + +=== Translation File Status === + en_US: 470 keys + zh_CN: 470 keys +``` + +### Update Translations +``` +$ ./manage-translations.sh update -l zh_CN +Updating translation files... +================================================== +Processing language: zh_CN +================================================== +Analysis result: + Missing keys: 5 + Extra keys: 20 + +Found 5 missing translation keys: +1. "New feature text" +2. "Another new text" +... + +Add these 5 missing keys? (y/n): y +5 keys added + +Found 20 extra translation keys: +1. "Removed old text" -> "已删除的旧文本" +... + +Delete these 20 extra keys? (y/n): y +20 keys deleted + +Translation file saved +``` + +### Clean Unused Keys +``` +$ ./manage-translations.sh clean +Cleaning unused translation keys... +Processing language: zh_CN +Found 50 unused keys: + 1. "old_unused_text" + 2. "deprecated_message" + ... + +Delete these 50 unused keys? (y/n): y +50 keys deleted +Original key count: 470, after cleaning: 420 +``` + +## Advanced Features + +### Custom Directory Structure + +```bash +# Use custom directories +./translation-manager.py \ + --translations-dir /path/to/translations \ + --source-dir /path/to/source +``` + +### Ignore Mark Feature + +For dynamic resources or special texts that should not be automatically cleaned, you can add `/*keep*/` at the end of the translation value. The tool will automatically ignore these keys and will not delete them during cleaning or syncing. + +Example: +```json +{ + "dynamic_key": "Some dynamic value /*keep*/" +} +``` + +## Notes + +1. **Backup is important**: The tool automatically creates backups before cleaning, but it is recommended to manually back up important files + +2. **Text extraction limitations**: + - ~~Only supports static strings, not dynamically constructed strings~~ + - Dynamic resources (such as variable concatenation or runtime-generated text) cannot be automatically extracted. You need to manually add them to the translation file and use the `/*keep*/` mark for ignore management. + - Must use the `Translation.tr()` format + +3. **File encoding**: All files must use UTF-8 encoding + +4. **Key naming conventions**: It is recommended to use English for key names and avoid special characters + +## Troubleshooting + +### Common Issues + +**Q: Text does not appear after adding Translation.tr?** +A: You need to import the translation feature in your QML file using `import "root:/"`, otherwise the translation text will not be displayed correctly. + +**Q: The number of extracted texts does not match expectations?** +A: Check whether all translatable texts use the `Translation.tr()` format and ensure there are no dynamically constructed strings. + +**Q: Some translations are missing after syncing?** +A: Check whether the source language file contains all necessary keys, and consider using a different source language for syncing. + +**Q: The cleaning operation deleted needed keys?** +A: Restore from the automatically created backup file and check whether `Translation.tr()` is used correctly in the source code. + +### Restore Backup + +```bash +# Restore a single file +cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json + +# Restore all files +cp .config/quickshell/translations.backup/* .config/quickshell/translations/ +``` diff --git a/.config/quickshell/translations/tools/manage-translations.sh b/.config/quickshell/translations/tools/manage-translations.sh new file mode 100755 index 00000000..c20896a9 --- /dev/null +++ b/.config/quickshell/translations/tools/manage-translations.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# Translation management script - convenient wrapper + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TRANSLATIONS_DIR="$(dirname "$SCRIPT_DIR")" +SOURCE_DIR="$(dirname "$(dirname "$TRANSLATIONS_DIR")")" + +show_help() { + echo "Translation Management Tool - Convenient Wrapper" + echo "" + echo "Usage: $0 [options] " + echo "" + echo "Commands:" + echo " extract Extract translatable texts to temporary file" + echo " update Update translation files (add missing/remove extra keys)" + echo " clean Clean unused translation keys" + echo " sync Sync keys across all language files" + echo " status Show translation status" + echo "" + echo "Options:" + echo " -l, --lang LANG Specify language (e.g.: zh_CN)" + echo " -t, --trans-dir DIR Translation files directory (default: $TRANSLATIONS_DIR)" + echo " -s, --source-dir DIR Source code directory (default: $SOURCE_DIR)" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 extract # Extract translatable texts" + echo " $0 update -l zh_CN # Update Chinese translations" + echo " $0 update # Update all translations" + echo " $0 clean # Clean unused keys" + echo " $0 sync # Sync keys across all languages" + echo " $0 status # Show translation status" +} + +show_status() { + echo "Analyzing translation status..." + + # Extract current text count + echo "=== Current Project Status ===" + python3 "$SCRIPT_DIR/translation-manager.py" \ + --translations-dir "$TRANSLATIONS_DIR" \ + --source-dir "$SOURCE_DIR" \ + --extract-only | grep "Extracted" + + echo "" + echo "=== Translation File Status ===" + + if [ -d "$TRANSLATIONS_DIR" ]; then + for file in "$TRANSLATIONS_DIR"/*.json; do + if [ -f "$file" ]; then + lang=$(basename "$file" .json) + count=$(jq 'length' "$file" 2>/dev/null || echo "error") + echo " $lang: $count keys" + fi + done + else + echo " Translation directory does not exist: $TRANSLATIONS_DIR" + fi +} + +# Parse command line arguments +LANG_CODE="" +COMMAND="" + +while [[ $# -gt 0 ]]; do + case $1 in + -l|--lang) + LANG_CODE="$2" + shift 2 + ;; + -t|--trans-dir) + TRANSLATIONS_DIR="$2" + shift 2 + ;; + -s|--source-dir) + SOURCE_DIR="$2" + shift 2 + ;; + -h|--help) + show_help + exit 0 + ;; + extract|update|clean|sync|status) + if [ -n "$COMMAND" ]; then + echo "Error: Only one command can be specified" + exit 1 + fi + COMMAND="$1" + shift + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +if [ -z "$COMMAND" ]; then + echo "Error: A command must be specified" + show_help + exit 1 +fi + +# Check dependencies +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required" + exit 1 +fi + +if [ "$COMMAND" = "status" ] && ! command -v jq >/dev/null 2>&1; then + echo "Warning: jq is not installed, status display may be incomplete" +fi + +# Build base arguments +BASE_ARGS="--translations-dir $TRANSLATIONS_DIR --source-dir $SOURCE_DIR" + +case $COMMAND in + extract) + echo "Extracting translatable texts..." + python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS --extract-only --show-temp + ;; + update) + echo "Updating translation files..." + if [ -n "$LANG_CODE" ]; then + python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS --language "$LANG_CODE" + else + python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS + fi + ;; + clean) + echo "Cleaning unused translation keys..." + python3 "$SCRIPT_DIR/translation-cleaner.py" $BASE_ARGS --clean + ;; + sync) + echo "Syncing translation keys..." + python3 "$SCRIPT_DIR/translation-cleaner.py" $BASE_ARGS --sync + ;; + status) + show_status + ;; + *) + echo "Unknown command: $COMMAND" + show_help + exit 1 + ;; +esac diff --git a/.config/quickshell/translations/tools/translation-cleaner.py b/.config/quickshell/translations/tools/translation-cleaner.py new file mode 100755 index 00000000..b26e3fbd --- /dev/null +++ b/.config/quickshell/translations/tools/translation-cleaner.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Translation File Maintenance Helper +Used to clean and organize translation files, removing unused keys +""" + +import os +import sys +import json +import argparse +import importlib.util +from pathlib import Path +from typing import Dict, Set, List + +# Import from the same directory using importlib +current_dir = os.path.dirname(os.path.abspath(__file__)) +manager_path = os.path.join(current_dir, 'translation-manager.py') +spec = importlib.util.spec_from_file_location("translation_manager", manager_path) +translation_manager = importlib.util.module_from_spec(spec) +spec.loader.exec_module(translation_manager) +TranslationManager = translation_manager.TranslationManager + +def clean_translation_files(translations_dir: str, source_dir: str, backup: bool = True): + """Clean translation files by removing unused keys""" + print("Starting translation file cleanup...") + + # Create manager + manager = TranslationManager(translations_dir, source_dir) + + # Extract currently used texts + print("Extracting currently used translatable texts...") + current_texts = manager.extract_translatable_texts() + print(f"Extracted {len(current_texts)} currently used texts") + + # Get all language files + languages = manager.get_available_languages() + if not languages: + print("No translation files found") + return + + print(f"Found language files: {', '.join(languages)}") + + total_removed = 0 + + for lang in languages: + print(f"\nProcessing language: {lang}") + + # Load translation file + translations = manager.load_translation_file(lang) + original_count = len(translations) + + # Find unused keys, skip those whose value ends with /*keep*/ + unused_keys = set() + for k in translations.keys(): + v = translations[k] + if k not in current_texts: + if isinstance(v, str) and v.strip().endswith('/*keep*/'): + continue + unused_keys.add(k) + + if unused_keys: + print(f"Found {len(unused_keys)} unused keys:") + for i, key in enumerate(sorted(unused_keys)[:10], 1): # Only show first 10 + print(f" {i}. \"{key[:50]}{'...' if len(key) > 50 else ''}\"") + if len(unused_keys) > 10: + print(f" ... and {len(unused_keys) - 10} more keys") + + response = input(f"Delete these {len(unused_keys)} unused keys? (y/n): ") + if response.lower().strip() in ['y', 'yes']: + if backup: + # Create backup only when user confirms deletion + backup_file = Path(translations_dir) / f"{lang}.json.bak" + with open(backup_file, 'w', encoding='utf-8') as f: + json.dump(translations, f, ensure_ascii=False, indent=2) + print(f"Created backup: {backup_file}") + # Delete unused keys + for key in unused_keys: + del translations[key] + + # Save cleaned file + manager.save_translation_file(lang, translations) + removed_count = len(unused_keys) + total_removed += removed_count + print(f"Deleted {removed_count} keys") + else: + print("Skipped deletion") + else: + print("No unused keys found") + + new_count = len(translations) + print(f"Original key count: {original_count}, after cleanup: {new_count}") + + print(f"\nCleanup completed! Total deleted {total_removed} unused keys.") + +def sync_translations(translations_dir: str, source_lang: str = "en_US", target_langs: List[str] = None): + """Sync translation keys to ensure all language files have the same keys""" + print(f"Starting translation key sync using {source_lang} as reference...") + + translations_path = Path(translations_dir) + + # Load source language file + source_file = translations_path / f"{source_lang}.json" + if not source_file.exists(): + print(f"Error: Source language file does not exist: {source_file}") + return + + with open(source_file, 'r', encoding='utf-8') as f: + source_translations = json.load(f) + + source_keys = set(source_translations.keys()) + print(f"Source language {source_lang} has {len(source_keys)} keys") + + # Get target language list + if target_langs is None: + target_langs = [] + for file_path in translations_path.glob("*.json"): + lang_code = file_path.stem + if lang_code != source_lang: + target_langs.append(lang_code) + + if not target_langs: + print("No target language files found") + return + + print(f"Target languages: {', '.join(target_langs)}") + + for target_lang in target_langs: + print(f"\nSyncing language: {target_lang}") + + target_file = translations_path / f"{target_lang}.json" + if target_file.exists(): + with open(target_file, 'r', encoding='utf-8') as f: + target_translations = json.load(f) + else: + target_translations = {} + + target_keys = set(target_translations.keys()) + + # Find missing and extra keys + missing_keys = source_keys - target_keys + extra_keys = target_keys - source_keys + + print(f" Missing keys: {len(missing_keys)}") + print(f" Extra keys: {len(extra_keys)}") + + # Add missing keys + if missing_keys: + for key in missing_keys: + # Use source language value as placeholder by default + target_translations[key] = source_translations[key] + print(f" Added {len(missing_keys)} missing keys") + + # Ask whether to delete extra keys + if extra_keys: + response = input(f" Delete {len(extra_keys)} extra keys? (y/n): ") + if response.lower().strip() in ['y', 'yes']: + for key in extra_keys: + del target_translations[key] + print(f" Deleted {len(extra_keys)} extra keys") + + # Save file (ensure UTF-8, fix for special chars) + with open(target_file, 'w', encoding='utf-8', newline='') as f: + json.dump(target_translations, f, ensure_ascii=False, indent=2) + print(f" Saved: {target_file}") + +def main(): + parser = argparse.ArgumentParser(description="Translation File Maintenance Helper") + parser.add_argument("--translations-dir", "-t", + default=".config/quickshell/translations", + help="Translation files directory") + parser.add_argument("--source-dir", "-s", + default=".config/quickshell", + help="Source code directory") + parser.add_argument("--clean", "-c", action="store_true", + help="Clean unused translation keys") + parser.add_argument("--sync", action="store_true", + help="Sync translation keys") + parser.add_argument("--source-lang", default="en_US", + help="Source language for syncing (default: en_US)") + parser.add_argument("--no-backup", action="store_true", + help="Do not create backup files when cleaning") + + args = parser.parse_args() + + # Convert to absolute paths + translations_dir = os.path.abspath(args.translations_dir) + source_dir = os.path.abspath(args.source_dir) + + if args.clean: + clean_translation_files(translations_dir, source_dir, backup=not args.no_backup) + elif args.sync: + sync_translations(translations_dir, args.source_lang) + else: + print("Please specify an operation:") + print(" --clean: Clean unused translation keys") + print(" --sync: Sync translation keys") + +if __name__ == "__main__": + main() diff --git a/.config/quickshell/translations/tools/translation-manager.py b/.config/quickshell/translations/tools/translation-manager.py new file mode 100755 index 00000000..a3105446 --- /dev/null +++ b/.config/quickshell/translations/tools/translation-manager.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Translation File Management Script +Used to update and extract translatable texts, manage JSON translation file key comparison +""" + +import os +import json +import re +import sys +import argparse +from pathlib import Path +from typing import Dict, Set, List, Tuple +import tempfile +import subprocess + +class TranslationManager: + def __init__(self, translations_dir: str, source_dir: str): + self.translations_dir = Path(translations_dir) + self.source_dir = Path(source_dir) + self.temp_extracted_file = None + + # Ensure translation directory exists + self.translations_dir.mkdir(parents=True, exist_ok=True) + + def extract_translatable_texts(self) -> Set[str]: + """Extract translatable texts from source code""" + translatable_texts = set() + + # Search patterns: Translation.tr("text") or Translation.tr('text') + # Improved regex that handles nested quotes correctly + patterns = [ + r'Translation\.tr\s*\(\s*(["\'])(((?!\1)[^\\]|\\.)*)(\1)\s*\)', # Double or single quotes with escape support + r'Translation\.tr\s*\(\s*`([^`]*(?:\\.[^`]*)*?)`\s*\)', # Backticks (template strings) + ] + + # Search all .qml and .js files + file_extensions = ['*.qml', '*.js'] + + for ext in file_extensions: + for file_path in self.source_dir.rglob(ext): + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + for pattern in patterns: + matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL) + for match in matches: + # Handle different match group structures + if isinstance(match, tuple): + # For improved regex, text is in the second group + if len(match) >= 3: + text = match[1] # Second group is the text content + else: + text = match[0] if match else "" + else: + text = match + + try: + if '\\u' in text or '\\x' in text: + clean_text = bytes(text, "utf-8").decode("unicode_escape") + else: + clean_text = ( + text.replace('\\n', '\n') + .replace('\\t', '\t') + .replace('\\r', '\r') + .replace('\\"', '"') + .replace('\\\'', "'") + .replace('\\f', '\f') + .replace('\\b', '\b') + .replace('\\\\', '\\') + ) + except Exception: + clean_text = text + + # Clean text (remove extra whitespace) + clean_text = clean_text.strip() + if clean_text: + translatable_texts.add(clean_text) + + except (UnicodeDecodeError, IOError) as e: + print(f"Warning: Cannot read file {file_path}: {e}") + + return translatable_texts + + def create_temp_translation_file(self, texts: Set[str]) -> str: + """Create temporary JSON file containing extracted texts""" + temp_data = {} + for text in sorted(texts): + temp_data[text] = text # Key and value are the same, indicating untranslated + + # Create temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f: + json.dump(temp_data, f, ensure_ascii=False, indent=2) + self.temp_extracted_file = f.name + + return self.temp_extracted_file + + def load_translation_file(self, lang_code: str) -> Dict[str, str]: + """Load translation file for specified language""" + file_path = self.translations_dir / f"{lang_code}.json" + if file_path.exists(): + try: + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Warning: Cannot load translation file {file_path}: {e}") + return {} + return {} + + def save_translation_file(self, lang_code: str, translations: Dict[str, str]): + """Save translation file""" + file_path = self.translations_dir / f"{lang_code}.json" + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(translations, f, ensure_ascii=False, indent=2) + print(f"Translation file saved: {file_path}") + except IOError as e: + print(f"Error: Cannot save translation file {file_path}: {e}") + + def get_available_languages(self) -> List[str]: + """Get list of available languages""" + languages = [] + for file_path in self.translations_dir.glob("*.json"): + lang_code = file_path.stem + languages.append(lang_code) + return sorted(languages) + + def compare_translations(self, extracted_texts: Set[str], target_lang: str) -> Tuple[Set[str], Set[str]]: + """Compare extracted texts with existing translation file""" + existing_translations = self.load_translation_file(target_lang) + existing_keys = set(existing_translations.keys()) + + missing_keys = extracted_texts - existing_keys # Missing keys + extra_keys = existing_keys - extracted_texts # Extra keys + + return missing_keys, extra_keys + + def interactive_update(self, lang_code: str, missing_keys: Set[str], extra_keys: Set[str]): + """Interactively update translation file, create backup only if updating""" + translations = self.load_translation_file(lang_code) + modified = False + backup_created = False + + # Handle missing keys + if missing_keys: + print(f"\nFound {len(missing_keys)} missing translation keys:") + for i, key in enumerate(sorted(missing_keys), 1): + print(f"{i}. \"{key}\"") + + if self.ask_yes_no(f"\nAdd these {len(missing_keys)} missing keys?"): + if not backup_created: + backup_file = self.translations_dir / f"{lang_code}.json.bak" + with open(backup_file, 'w', encoding='utf-8') as f: + json.dump(translations, f, ensure_ascii=False, indent=2) + print(f"Created backup: {backup_file}") + backup_created = True + for key in missing_keys: + translations[key] = key # Default value is the key itself + modified = True + print(f"Added {len(missing_keys)} keys") + + # Handle extra keys + if extra_keys: + # Only show extra keys that are not marked with /*keep*/ + filtered_extra_keys = [key for key in extra_keys if not (isinstance(translations.get(key, ""), str) and translations.get(key, "").strip().endswith('/*keep*/'))] + if filtered_extra_keys: + print(f"\nFound {len(filtered_extra_keys)} extra translation keys:") + for i, key in enumerate(sorted(filtered_extra_keys), 1): + print(f"{i}. \"{key}\" -> \"{translations.get(key, '')}\"") + if self.ask_yes_no(f"\nDelete these {len(filtered_extra_keys)} extra keys?"): + if not backup_created: + backup_file = self.translations_dir / f"{lang_code}.json.bak" + with open(backup_file, 'w', encoding='utf-8') as f: + json.dump(translations, f, ensure_ascii=False, indent=2) + print(f"Created backup: {backup_file}") + backup_created = True + deleted_count = 0 + for key in filtered_extra_keys: + if key in translations: + del translations[key] + modified = True + deleted_count += 1 + print(f"Deleted {deleted_count} keys") + + # Save changes + if modified: + self.save_translation_file(lang_code, translations) + else: + print("No changes made") + + def ask_yes_no(self, question: str) -> bool: + """Ask user for confirmation""" + while True: + response = input(f"{question} (y/n): ").lower().strip() + if response in ['y', 'yes']: + return True + elif response in ['n', 'no']: + return False + else: + print("Please enter y/yes or n/no") + + def cleanup(self): + """Clean up temporary files""" + if self.temp_extracted_file and os.path.exists(self.temp_extracted_file): + os.unlink(self.temp_extracted_file) + +def main(): + parser = argparse.ArgumentParser(description="Translation file management tool") + parser.add_argument("--translations-dir", "-t", + default=".config/quickshell/translations", + help="Translation files directory (default: .config/quickshell/translations)") + parser.add_argument("--source-dir", "-s", + default=".config/quickshell", + help="Source code directory (default: .config/quickshell)") + parser.add_argument("--language", "-l", + help="Specify language code to process (e.g., zh_CN)") + parser.add_argument("--extract-only", "-e", action="store_true", + help="Only extract translatable texts to temporary file") + parser.add_argument("--show-temp", action="store_true", + help="Show temporary extracted file content") + + args = parser.parse_args() + + # Convert to absolute paths + translations_dir = os.path.abspath(args.translations_dir) + source_dir = os.path.abspath(args.source_dir) + + print(f"Translation directory: {translations_dir}") + print(f"Source code directory: {source_dir}") + + # Check if directories exist + if not os.path.exists(source_dir): + print(f"Error: Source code directory does not exist: {source_dir}") + sys.exit(1) + + # Create manager + manager = TranslationManager(translations_dir, source_dir) + + try: + # Extract translatable texts + print("\nExtracting translatable texts...") + extracted_texts = manager.extract_translatable_texts() + print(f"Extracted {len(extracted_texts)} translatable texts") + + # Create temporary file + temp_file = manager.create_temp_translation_file(extracted_texts) + print(f"Created temporary file: {temp_file}") + + if args.show_temp: + print("\nTemporary file contents:") + with open(temp_file, 'r', encoding='utf-8') as f: + print(f.read()) + + if args.extract_only: + print("Extract-only mode, program finished") + return + + # Get available languages + available_languages = manager.get_available_languages() + + if args.language: + target_languages = [args.language] + else: + print(f"\nAvailable languages: {', '.join(available_languages) if available_languages else 'None'}") + + if not available_languages: + print("No existing translation files found") + lang_input = input("Enter language code to create (e.g.: zh_CN): ").strip() + if lang_input: + target_languages = [lang_input] + else: + print("No language specified, program finished") + return + else: + print("Choose language to process:") + for i, lang in enumerate(available_languages, 1): + print(f"{i}. {lang}") + print("a. Process all languages") + + choice = input("Please choose (enter number, language code, or 'a'): ").strip() + + if choice.lower() == 'a': + target_languages = available_languages + elif choice.isdigit() and 1 <= int(choice) <= len(available_languages): + target_languages = [available_languages[int(choice) - 1]] + elif choice in available_languages: + target_languages = [choice] + else: + print("Invalid choice, program finished") + return + + # Process each language + for lang in target_languages: + print(f"\n{'='*50}") + print(f"Processing language: {lang}") + print('='*50) + + missing_keys, extra_keys = manager.compare_translations(extracted_texts, lang) + + if not missing_keys and not extra_keys: + print(f"Translation file for language {lang} is already up to date") + continue + + print(f"Analysis results:") + print(f" Missing keys: {len(missing_keys)}") + # Load translation file for current lang to get values + current_translations = manager.load_translation_file(lang) + filtered_extra_keys = [key for key in extra_keys if not (isinstance(current_translations.get(key, ""), str) and current_translations.get(key, "").strip().endswith('/*keep*/'))] + ignored_extra_keys = [key for key in extra_keys if (isinstance(current_translations.get(key, ""), str) and current_translations.get(key, "").strip().endswith('/*keep*/'))] + print(f" Extra keys: {len(filtered_extra_keys)}") + if ignored_extra_keys: + print(f" Ignored keys: {len(ignored_extra_keys)} (marked with /*keep*/)") + + if missing_keys or extra_keys: + manager.interactive_update(lang, missing_keys, extra_keys) + + finally: + # Clean up temporary files + manager.cleanup() + +if __name__ == "__main__": + main() diff --git a/.config/quickshell/translations/vi_VN.json b/.config/quickshell/translations/vi_VN.json new file mode 100644 index 00000000..9c2bc979 --- /dev/null +++ b/.config/quickshell/translations/vi_VN.json @@ -0,0 +1,335 @@ +{ + "Unknown function call: %1": "Hàm không xác định: %1", + "Show next time": "Hiển thị lần sau", + "Fidelity": "Khá giống gốc", + "Open file link": "Mở liên kết tệp", + "Interrupts possibility of overview being toggled on release.": "Ngăn mở overview khi nhả nút.", + "No audio source": "Không có nguồn âm thanh", + "Might look ass. Unsupported.": "Có thể rất tệ. Không được hỗ trợ.", + "Jump to current month": "Nhảy đến tháng hiện tại", + "Delete": "Xóa", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**Giá**: miễn phí. Dữ liệu được sử dụng cho mục đích huấn luyện. **Hướng dẫn**: Đăng nhập vào tài khoản Google, cho phép AI Studio tạo dự án Google Cloud gì đó, quay lại rồi ấn Get API key", + "Rainbow": "Cầu vồng", + "%1 does not require an API key": "%1 không cần API key", + "Choose model": "Chọn model", + "Prevents abrupt increments and restricts volume limit": "Chặn thay đổi đột ngột và giới hạn âm lượng", + "%1 characters": "%1 ký tự", + "Change any time later with /dark, /light, /img in the launcher": "Thay đổi bất cứ lúc nào sau này với /dark, /light, /img trong launcher", + "Tonal Spot": "Tonal Spot", + "Neutral": "Trung tính", + "To Do": "Cần làm", + "Auto": "Tự động", + "Polling interval (ms)": "Thời gian lặp lại (ms)", + "Closes on screen keyboard on press": "Đóng bàn phím trên màn hình khi ấn", + "Toggles right sidebar on press": "Mở/đóng sidebar phải khi ấn", + "Center title": "Căn giữa tiêu đề", + "Lock": "Khóa màn hình", + "Screen snip": "Chụp màn hình (chọn vùng)", + "User agent (for services that require it)": "User agent (nếu cần)", + "Report a Bug": "Báo lỗi", + "Shutdown": "Tắt máy", + "Keyboard toggle": "Mở/đóng bàn phím ảo", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "Cái nhiều hentai nhất | Số lượng rất tốt, rất nhiều NSFW, chất lượng có thể khác nhau nhiều", + "Download": "Tải xuống", + "Note: turning off can hurt readability": "Ghi chú: nếu tắt có thể khó đọc", + "Local Ollama model | %1": "Model Ollama trên máy | %1", + "Silent": "Im lặng", + "Columns": "Số cột", + "Set with /mode PROVIDER": "Set with /mode PROVIDER", + "Issues": "Các vấn đề", + "Policies": "Chính sách", + "Load chat from %1": "Tải trò chuyện từ %1", + "Unknown Album": "Album không xác định", + "Yes": "Có", + "Battery": "Pin", + "Material palette": "Kiểu material", + "Chain of Thought": "Dòng suy nghĩ", + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key.": "Cần cái này vì GlobalShortcut.onReleased cho một phím của Quickshell được kích hoạt kể cả khi ban ân phím khác trước khi thả phím đó.", + "Low warning": "Cảnh báo thấp", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Với Zerochan:\n- Hãy nhập tên một màu (bằng tiếng Anh)\n- Đặt username Zerochan trong tùy chọn `sidebar.booru.zerochan.username`. Bạn [có thể bị ban nếu không tuân thủ](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "Brightness": "Độ sáng", + "Yooooo hi there": "Yooooo chào bạn", + "Triggers volume OSD on press": "Hiển thị âm lượng khi ấn", + "Colors & Wallpaper": "Màu sắc & Hình nền", + "No media": "Không có media", + "Critical warning": "Cảnh báo rất thấp", + "Mic toggle": "Bật/tắt mic", + "12h AM/PM": "12h AM/PM", + "Large language models": "Mô hình ngôn ngữ lớn", + "Markdown test": "Test markdown", + "Temperature: %1": "Nhiệt độ: %1", + "Opens media controls on press": "Mở điều khiển media khi ấn", + "Edit": "Sửa", + "Closes overview": "Đóng overview", + "Waifus only | Excellent quality, limited quantity": "Chỉ waifus | Chất lượng xuất sắc, số lượng hạn chế", + "Cheat sheet": "Bảng tra cứu", + "Current model: %1\nSet it with %2model MODEL": "Model đang chọn: %1\nChọn với lệnh %2model MODEL", + "Provider set to": "Đã đặt nhà cung cấp thành", + "Clear": "Xóa hết", + "GitHub": "GitHub", + "App": "Ứng dụng", + "Title bar": "Thanh tiêu đề", + "Web search": "Tìm kiếm web", + "Invalid model. Supported: \n```": "Model không hợp lệ. Các lựa chọn: \n```", + "Calendar": "Lịch", + "Done": "Đã xong", + "Monochrome": "Đen trắng", + "Show regions of potential interest": "Hiển thị vùng thông minh", + "Dark/Light toggle": "Chuyển chế độ sáng/tối", + "Unknown command:": "Lệnh không xác định:", + "Allow NSFW content": "Cho phép nội dung NSFW", + "Closes cheatsheet on press": "Đóng bảng tra cứu khi ấn", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "Chỉnh giá trị nhiệt độ (sự ngẫu nhiên) của model. Giá trị 0-2 với Gemini, 0-1 với các model khác. Mặc định là 0.5.", + "Invalid API provider. Supported: \n-": "Nhà cung cấp API không hợp lệ. Các lựa chọn: \n-", + "Shell windows": "Cửa sổ của shell", + "Loaded the following system prompt\n\n---\n\n%1": "Đã tải chỉ dẫn hệ thống sau đây\n\n---\n\n%1", + "Clipboard": "Clipboard", + "Toggles left sidebar on press": "Mở/đóng sidebar trái khi ấn", + "For storing API keys and other sensitive information": "Để lưu trữ API key và các thông tin nhạy cảm khác", + "Wallpaper": "Hình nền", + "Decorations & Effects": "Trang trí & Hiệu ứng", + "AI": "AI", + "Large images | God tier quality, no NSFW.": "Ảnh kích thước lớn | Chất lượng cực tốt, không có NSFW.", + "When not fullscreen": "Khi không toàn màn hình", + "Resources": "Tài nguyên", + "Light": "Sáng", + "Weeb": "Wibu", + "Disable NSFW content": "Tắt nội dung NSFW", + "OK": "OK", + "Screenshot tool": "Công cụ chụp màn hình", + "Enable": "Bật", + "Select Language": "Chọn ngôn ngữ", + "System": "Hệ thống", + "Emojis": "Emoji", + "The current system prompt is\n\n---\n\n%1": "Chỉ dẫn hệ thống hiện tại như sau\n\n---\n\n%1", + "Closes right sidebar on press": "Đóng sidebar phải khi ấn", + "Translator": "Dịch", + "Sleep": "Ngủ", + "Action": "Hành động", + "Audio": "Âm thanh", + "Show background": "Hiện nền", + "All-rounder | Good quality, decent quantity": "Tốt đều | Chất lượng tốt, số lượng ổn", + "Documentation": "Tài liệu", + "Terminal": "Terminal", + "Distro": "Distro", + "Clear chat history": "Xóa lịch sử trò chuyện", + "Float": "Nổi", + "No further instruction provided": "No further instruction provided", + "Choose file": "Chọn tệp", + "Opens right sidebar on press": "Mở sidebar phải khi ấn", + "Set the system prompt for the model.": "Đặt chỉ dẫn hệ thống cho model.", + "Closes left sidebar on press": "Đóng sidebar trái khi ấn", + "Unknown Title": "Bài hát không rõ tên", + "Math result": "Kết quả phép tính", + "Logout": "Đăng xuất", + "Privacy Policy": "Chính sách quyền riêng tư", + "Style": "Phong cách", + "Borderless": "Không viền", + "Set API key": "Đặt API key", + "Clean stuff | Excellent quality, no NSFW": "Sạch sẽ | Chất lượng xuất sắc, không có NSFW", + "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "Thử nghiệm | Trực tuyến | Model của Google\nCó thể làm nhiều hơn một chút nhưng không tìm kiếm nhanh chóng", + "Toggles cheatsheet on press": "Mở/đóng bảng tra cứu khi ấn", + "Thinking": "Đang nghĩ", + "Earbang protection": "Bảo vệ tai", + "Advanced": "Nâng cao", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "Có thể tốt hơn nếu bạn gõ lệch phím nhiều,\nnhưng kết quả có thể hơi lạ và không hoạt động tốt với từ viết tắt\n(ví dụ tìm \"GIMP\" có thể không ra cái chương trình vẽ)", + "Shell & utilities theming must also be enabled": "Cần Shell & công cụ cũng bật", + "Desktop": "Màn hình chính", + "Anime": "Anime", + "Qt apps": "Các ứng dụng Qt", + "Style & wallpaper": "Phong cách & hình nền", + "Finished tasks will go here": "Việc đã xong sẽ hiện ở đây", + "Weather": "Thời tiết", + "Settings": "Cài đặt", + "Shell & utilities": "Shell & tiện ích", + "Toggles overview on release": "Mở/đóng overview khi nhả nút", + "Unfinished": "Chưa xong", + "Random: Konachan": "Ngẫu nhiên: Konachan", + "Opens left sidebar on press": "Mở sidebar trái khi ấn", + "Pick wallpaper image on your system": "Chọn hình nền trên máy", + "Volume": "Âm lượng", + "Add": "Thêm", + "Hibernate": "Ngủ đông", + "Run": "Chạy", + "Keep system awake": "Giữ hệ thống bật", + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.": "Để đảm bảo luôn hoạt động, dùng binditn = MODKEYS, catchall trong một submap luôn được kích hoạt bao trùm mọi thứ.", + "Plain rectangle": "Hình chữ nhật", + "%1 queries pending": "%1 lệnh gọi đang chờ", + "Temperature set to %1": "Nhiệt độ đã được đặt thành %1", + "Notifications": "Thông báo", + "System prompt": "Chỉ dẫn hệ thống", + "Hover to reveal": "Đặt chuột vào để hiện", + "No": "Không", + "Bar": "Bar", + "Search the web": "Tìm kiếm web", + "Page %1": "Trang %1", + "Reboot": "Khởi động lại", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "Các vùng có thể là hình ảnh hoặc phần của màn hình cớ vẻ được bao chứa.\nKhông luôn chính xác.\nSử dụng một thuật toán xử lý ảnh chạy trên máy, không dùng AI.", + "Show app icons": "Hiện biểu tượng ứng dụng", + "Closet": "Nghiện mà ngại", + "Set the current API provider": "Đặt nguồn cung cấp API", + "Cancel": "Hủy", + "Networking": "Mạng", + "Overview": "Overview", + "Search, calculate or run": "Tìm, tính hoặc chạy", + "Useless buttons": "Mấy nút vô dụng", + "Transparency": "Sự trong suốt", + "Temperature must be between 0 and 2": "Nhiệt độ phải trong khoảng từ 0 đến 2", + "Automatically suspends the system when battery is low": "Tự động ngủ khi pin thấp", + "Current API endpoint: %1\nSet it with %2mode PROVIDER": "Endpoint API hiện tại: %1\nĐặt với lệnh %2mode PROVIDER", + "Decrease brightness": "Giảm độ sáng", + "Services": "Các dịch vụ", + "Reload Hyprland & Quickshell": "Tải lại Hyprland & Quickshell", + "Automatic suspend": "Tự động ngủ", + "illogical-impulse Welcome": "illogical-impulse - Xin chào", + "Interface": "Giao diện", + "Load chat": "Tải cuộc trò chuyện", + "Opens session screen on press": "Mở màn hình session khi ấn", + "Number show delay when pressing Super (ms)": "Thời gian chờ hiện số khi nhấn Super (ms)", + "Clear the current list of images": "Xóa danh sách hình ảnh hiện tại", + "Fake screen rounding": "Giả bo tròn màn hình", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "Mẹo: Ẩn biểu tượng và luôn hiển thị số nếu\nmuốn giống trải nghiệm illogical-impulse gốc", + "Launch": "Chạy", + "%1 notifications": "%1 thông báo", + "Hides brightness OSD on press": "Ngừng hiển thị độ sáng khi nhấn", + "%1 | Right-click to configure": "%1 | Ấn chuột phải để chỉnh", + "Unknown Artist": "Nghệ sĩ không xác định", + "Appearance": "Giao diện", + "Task Manager": "Quản lí ứng dụng đang chạy", + "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "Để đặt API key, viết nó sau lệnh\n\nĐể xem lại, viết \"get\" sau lệnh
\n\n### Với %1:\n\n**Link**: %2\n\n%3", + "Hold to show workspace numbers, release to show icons": "Giữ để hiện số workspace, thả ra để hiện biểu tượng", + "Opens cheatsheet on press": "Mở bảng tra cứu khi ấn", + "Invalid arguments. Must provide `key` and `value`.": "Biến không hợp lệ. cần cả `key` và `value`.", + "About": "Giới thiệu", + "illogical-impulse": "illogical-impulse", + "Triggers brightness OSD on press": "Hiện độ sáng/âm lượng khi ấn", + "Help & Support": "Trợ giúp", + "Enter tags, or \"%1\" for commands": "Nhập tag hoặc \"%1\" để xem các lệnh", + "Format": "Định dạng", + "Content": "Giống gốc", + "Edit config": "Sửa config", + "Bluetooth": "Bluetooth", + "Be patient...": "Bình tĩnh...", + "Toggles session screen on press": "Mở/đóng màn hình session khi ấn", + "Discussions": "Thảo luận", + "Anime boorus": "Các booru anime", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "Quả này không được. Mẹo:\n- Kiểm tra tag và cài đặt NSFW\n- Nếu không nghĩ ra tag nào có thể nhập số trang", + "Task description": "Mô tả công việc", + "Max allowed increase": "Thay đổi tối đa", + "Rows": "Số hàng", + "Switched to search mode. Continue with the user's request.": "Đã chuyển sang chế độ tìm kiếm. Tiếp tục với yêu cầu của người dùng.", + "Use Levenshtein distance-based algorithm instead of fuzzy": "Sử dụng thuật toán dùng khoảng cách Levenshtein thay vì fuzzy", + "Copy": "Sao chép", + "12h am/pm": "12h am/pm", + "Unknown": "Không xác định", + "Hides volume OSD on press": "Ngừng hiển thị âm lượng khi nhấn", + "Waiting for response...": "Đang chờ phản hồi...", + "Workspace": "Workspace", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Hình nền Anime SFW ngẫu nhiên từ Konachan\nẢnh được lưu vào ~/Pictures/Wallpapers", + "Toggles on screen keyboard on press": "Mở/đóng bàn phím ảo khi ấn", + "Online via %1 | %2's model": "Trực tuyến qua %1 | Model của %2", + "Always show numbers": "Luôn hiện số", + "or": "hoặc", + "Drag or click a region • LMB: Copy • RMB: Edit": "Kéo thả hoặc chọn vùng • Chuột trái: Sao chép • Chuột phải: Chỉnh sửa", + "Local only": "Chỉ trên máy", + "Donate": "Ủng hộ", + "Online | Google's model\nGives up-to-date information with search.": "Trực tuyến | Model của Google\nCó thể tìm kiếm để cung cấp thông tin cập nhật.", + "Run command": "Chạy lệnh", + "Dotfiles": "Dotfiles", + "Volume limit": "Giới hạn âm lượng", + "On-screen display": "Âm lượng/độ sáng", + "Reboot to firmware settings": "Khởi động lại vào cài đặt firmware", + "Workspaces shown": "Số workspace hiển thị", + "Save": "Lưu", + "The popular one | Best quantity, but quality can vary wildly": "Phổ biến | Số lượng tốt nhất, nhưng chất lượng không biết đâu vào đâu", + "Save chat": "Lưu cuộc trò chuyện", + "Intelligence": "Trí tuệ", + "Translation goes here...": "Bản dịch sẽ hiện ở đây...", + "Toggle clipboard query on overview widget": "Mở/đóng tìm kiếm clipboard trên overview", + "Search": "Tìm kiếm", + "Timeout (ms)": "Thời gian chờ (ms)", + "24h": "24h", + "Color picker": "Chọn màu", + "Save to Downloads": "Lưu vào Downloads", + "No notifications": "Không có thông báo", + "Closes media controls on press": "Đóng điều khiển media khi nhấn", + "Game mode": "Chế độ game", + "Alternatively use /dark, /light, /img in the launcher": "Có thể dùng /dark, /light, /img trong launcher", + "Info": "Thông tin", + "Dock": "Dock", + "Pinned on startup": "Ghim khi khởi động", + "Suspend at": "Tạm dừng ở", + "Fruit Salad": "Salad hoa quả", + "API key:\n\n```txt\n%1\n```": "API key:\n\n```txt\n%1\n```", + "Increase brightness": "Tăng độ sáng", + "API key set for %1": "API key đã đặt cho %1", + "Not visible to model": "Không hiển thị cho model", + "Expressive": "Biểu cảm", + "Enter text to translate...": "Nhập văn bản để dịch...", + "Usage": "Cách dùng", + "Message the model... \"%1\" for commands": "Hỏi model... \"%1\" để xem lệnh", + "Keybinds": "Phím tắt", + "Model set to %1": "Đã đặt model thành %1", + "Scale (%)": "Tỉ lệ (%)", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "Gõ /key để bắt đầu dùng các model trực tuyến\nCtrl+O để mở rộng sidebar\nCtrl+P để nhấc sidebar thành cửa sổ", + "Toggle emoji query on overview widget": "Mở/đóng tìm kiếm emoji trên overview", + "Output": "Đầu ra", + "Uptime: %1": "Máy bật được %1", + "For desktop wallpapers | Good quality": "Cho hình nền máy tính | Chất lượng tốt", + "Nothing here!": "Không có gì ở đây!", + "Close": "Đóng", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "Phím mũi tên để chọn, Enter để xác nhận\nEsc hoặc ấn bất kỳ đâu để thoát", + "Copy code": "Sao chép code", + "Load prompt from %1": "Tải chỉ dẫn từ %1", + "Time": "Thời gian", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**Giá**: miễn phí. Chính sách sử dụng dữ liệu tùy thuộc vào cài đặt tài khoản OpenRouter của bạn.\n\n**Hướng dẫn**: Đăng nhập vào tài khoản OpenRouter, mở Keys ở menu góc trên bên phải, ấn Create API Key", + "Bar style": "Phong cách bar", + "Configuration": "Cài đặt", + "Prefixes": "Kí tự đầu", + "No API key set for %1": "Không có API key cho %1", + "Add task": "Thêm công việc", + "Volume mixer": "Trộn âm lượng", + "Toggles media controls on press": "Mở/đóng điều khiển media khi ấn", + "Go to source (%1)": "Đi đến nguồn (%1)", + "The current API used. Endpoint:": "API đang sử dụng. Endpoint:", + "View Markdown source": "Xem nguồn Markdown", + "Input": "Đầu vào", + "Opens on screen keyboard on press": "Mở bàn phím ảo khi ấn", + "Allow NSFW": "Cho phép NSFW", + "Session": "Session", + "Detach left sidebar into a window/Attach it back": "Nhấc sidebar trái thành cửa sổ/Đặt nó lại", + "Night Light": "Lọc ánh sáng xanh", + "Workspaces": "Các workspace", + "Toggles overview on press": "Mở/đóng overview khi ấn", + "Dark": "Tối", + "Base URL": "Base URL", + "Hug": "Ôm", + "Buttons": "Các nút", + "Get the next page of results": "Lấy trang kết quả tiếp theo", + "%1 Safe Storage": "Lưu trữ an toàn %1", + "Color generation": "Chỉnh màu", + "Select output device": "Chọn đầu ra", + "Select input device": "Chọn đầu vào", + "%1 • %2 tasks": "%1 • %2 việc cần làm", + "Online models disallowed\n\nControlled by `policies.ai` config option": "Model trực tuyến không được cho phép\n\nCài đặt bởi lựa chọn `policies.ai`", + "Download complete": "Đã tải xong", + "Code saved to file": "Code đã lưu vào file", + "Critically low battery": "Pin rất thấp", + "Scroll to change brightness": "Cuộn để thay đổi độ sáng", + "Cloudflare WARP": "Cloudflare WARP", + "Toggles bar on press": "Mở/đóng bar khi ấn", + "Saved to %1": "Đã lưu vào %1", + "Elements": "Nguyên tố", + "Save chat to %1": "Lưu chat vào %1", + "Connection failed. Please inspect manually with the warp-cli command": "Kết nối không thành công. Hãy xem lại với lệnh warp-cli", + "Weather Service": "Thời tiết", + "Registration failed. Please inspect manually with the warp-cli command": "Đăng ký không thành công. Hãy xem lại với lệnh warp-cli", + "Consider plugging in your device": "Hãy cắm nguồn thiết bị của bạn", + "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", + "Cannot find a GPS service. Using the fallback method instead.": "Không tìm thấy dịch vụ GPS. Đang sử dụng phương pháp dự phòng.", + "Opens bar on press": "Mở bar khi ấn", + "Low battery": "Pin yếu", + "Scroll to change volume": "Cuộn để thay đổi âm lượng", + "Please charge!\nAutomatic suspend triggers at %1": "Hãy sạc pin!\nHệ thống sẽ tự động ngủ khi pin xuống %1", + "Closes bar on press": "Đóng bar khi ấn" +} \ No newline at end of file diff --git a/.config/quickshell/translations/zh_CN.json b/.config/quickshell/translations/zh_CN.json new file mode 100644 index 00000000..34dc5415 --- /dev/null +++ b/.config/quickshell/translations/zh_CN.json @@ -0,0 +1,314 @@ +{ + "Mo": "一/*keep*/", + "Tu": "二/*keep*/", + "We": "三/*keep*/", + "Th": "四/*keep*/", + "Fr": "五/*keep*/", + "Sa": "六/*keep*/", + "Su": "日/*keep*/", + "%1 characters": "%1 个字符", + "**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key": "**价格**:免费。数据用于训练。\n\n**说明**:登录 Google 账户,允许 AI Studio 创建 Google Cloud 项目或其他要求,然后返回并点击获取 API 密钥", + "**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key": "**价格**:免费。数据使用政策取决于您的 OpenRouter 账户设置。\n\n**说明**:登录 OpenRouter 账户,在右上角菜单中选择 Keys,点击创建 API 密钥", + ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!": ". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!", + "No further instruction provided": "未提供进一步说明", + "API key set for %1": "已为 %1 设置 API 密钥", + "API key:\n\n```txt\n%1\n```": "API 密钥:\n\n```txt\n%1\n```", + "Action": "操作", + "Add": "添加", + "Add task": "添加任务", + "All-rounder | Good quality, decent quantity": "全能型 | 质量好,数量适中", + "Allow NSFW": "允许 NSFW", + "Allow NSFW content": "允许 NSFW 内容", + "Anime": "动漫", + "Anime boorus": "动漫图库", + "App": "应用", + "Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "方向键导航,回车选择\nEsc 或点击任意地方取消", + "Bluetooth": "蓝牙", + "Brightness": "亮度", + "Cancel": "取消", + "Chain of Thought": "思维链", + "Cheat sheet": "快捷键表", + "Choose model": "选择模型", + "Clean stuff | Excellent quality, no NSFW": "清洁内容 | 优秀质量,无 NSFW", + "Clear": "清除", + "Clear chat history": "清除聊天记录", + "Clear the current list of images": "清除当前图片列表", + "Close": "关闭", + "Copy": "复制", + "Copy code": "复制代码", + "Current API endpoint: %1\nSet it with %2mode PROVIDER": "当前 API 端点:%1\n使用 %2mode PROVIDER 设置", + "Delete": "删除", + "Desktop": "桌面", + "Disable NSFW content": "禁用 NSFW 内容", + "Done": "完成", + "Download": "下载", + "Edit": "编辑", + "Enter text to translate...": "输入要翻译的文本...", + "Finished tasks will go here": "已完成的任务将显示在这里", + "For desktop wallpapers | Good quality": "桌面壁纸专用 | 质量好", + "For storing API keys and other sensitive information": "用于存储 API 密钥和其他敏感信息", + "Game mode": "游戏模式", + "Get the next page of results": "获取下一页结果", + "Go to source (%1)": "转到源 (%1)", + "Hibernate": "休眠", + "Input": "输入", + "Intelligence": "智能体", + "Interface": "界面", + "Invalid arguments. Must provide `key` and `value`.": "参数无效。必须提供 `key` 和 `value`。", + "Jump to current month": "跳转到当前月份", + "Keep system awake": "保持系统唤醒", + "Large images | God tier quality, no NSFW.": "大尺寸图片 | 顶级质量,无 NSFW", + "Large language models": "大语言模型", + "Launch": "启动", + "Local Ollama model | %1": "本地 Ollama 模型 | %1", + "Lock": "锁定", + "Logout": "注销", + "Markdown test": "Markdown 测试", + "Math result": "数学结果", + "Night Light": "护眼模式", + "No API key set for %1": "未为 %1 设置 API 密钥", + "No audio source": "无音频源", + "No media": "无媒体", + "No notifications": "无通知", + "Not visible to model": "对模型不可见", + "Nothing here!": "这里什么都没有!", + "Notifications": "通知", + "OK": "确定", + "Open file link": "打开文件链接", + "Output": "输出", + "Page %1": "第 %1 页", + "Reboot": "重启", + "Reboot to firmware settings": "重启到固件设置", + "Reload Hyprland & Quickshell": "重新加载 Hyprland 和 Quickshell", + "Run": "运行", + "Run command": "运行命令", + "Save": "保存", + "Save to Downloads": "保存到下载文件夹", + "Search": "搜索", + "Search the web": "在网络上搜索", + "Search, calculate or run": "搜索、计算或运行", + "Select Language": "选择语言", + "Session": "会话", + "Set API key": "设置 API 密钥", + "Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "设置模型的温度(随机性)。Gemini 模型范围为 0 到 2,其他模型为 0 到 1。默认值为 0.5。", + "Set the current API provider": "设置当前 API 提供商", + "Shutdown": "关机", + "Silent": "静音", + "Sleep": "睡眠", + "System": "系统", + "Task Manager": "任务管理器", + "Task description": "任务描述", + "Temperature must be between 0 and 2": "温度必须在 0 到 2 之间", + "Temperature set to %1": "温度设置为 %1", + "Temperature: %1": "温度:%1", + "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向 | 数量巨大,大量 NSFW,质量参差不齐", + "The popular one | Best quantity, but quality can vary wildly": "最受欢迎 | 数量最多,但质量参差不齐", + "Thinking": "思考中", + "Translation goes here...": "翻译结果会显示在这里...", + "Translator": "翻译器", + "Unfinished": "未完成", + "Unknown": "未知", + "Unknown Album": "未知专辑", + "Unknown Artist": "未知艺术家", + "Unknown Title": "未知标题", + "Unknown function call: %1": "未知函数调用:%1", + "Uptime: %1": "运行时间:%1", + "View Markdown source": "查看 Markdown 源码", + "Volume": "音量", + "Volume mixer": "音量混合器", + "Waifus only | Excellent quality, limited quantity": "仅限角色 | 优秀质量,数量有限", + "Waiting for response...": "等待响应...", + "Workspace": "工作区", + "%1 Safe Storage": "%1 安全存储", + "%1 does not require an API key": "%1 不需要 API 密钥", + "%1 queries pending": "%1 个查询等待中", + "%1 | Right-click to configure": "%1 | 右键点击进行配置", + "Set with /mode PROVIDER": "使用 /mode PROVIDER 设置", + "Invalid API provider. Supported: \n-": "无效的 API 提供商。支持的:\n-", + "Unknown command:": "未知命令:", + "Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "输入 /key 开始使用在线模型\nCtrl+O 展开侧边栏\nCtrl+P 将侧边栏分离为窗口", + "The current API used. Endpoint:": "当前使用的 API。端点:", + "Provider set to": "提供商设置为", + "Invalid model. Supported: \n```": "无效模型。支持的:\n```", + "Switched to search mode. Continue with the user's request.": "已切换到搜索模式。继续处理用户请求。", + "Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "实验性 | 在线 | Google 模型\n功能更多但搜索速度较慢", + "To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command
\n\n### For %1:\n\n**Link**: %2\n\n%3": "要设置 API 密钥,请将其与命令一起传递\n\n要查看密钥,请将 \"get\" 与命令一起传递
\n\n### 对于 %1:\n\n**链接**:%2\n\n%3", + "Enter tags, or \"%1\" for commands": "输入标签,或 \"%1\" 查看命令", + "Online via %1 | %2's model": "通过 %1 在线 | %2 的模型", + "That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "没有找到结果。提示:\n- 检查您的标签和 NSFW 设置\n- 如果没有想到标签,请输入页码", + "Online | Google's model\nGives up-to-date information with search.": "在线 | Google 模型\n通过搜索提供最新信息。", + "Settings": "设置", + "Save chat": "保存对话", + "Load chat": "加载对话", + "or": "或", + "Set the system prompt for the model.": "为模型设置系统提示。", + "To Do": "待办", + "Calendar": "日历", + "Advanced": "高级", + "About": "关于", + "Services": "服务", + "Style": "样式", + "Edit config": "编辑配置", + "Colors & Wallpaper": "颜色和壁纸", + "Light": "浅色", + "Dark": "深色", + "Material palette": "颜色主题", + "Fidelity": "保真度", + "Fruit Salad": "水果沙拉", + "Alternatively use /dark, /light, /img in the launcher": "或者在启动器中使用 /dark、/light、/img", + "Fake screen rounding": "伪造屏幕圆角", + "When not fullscreen": "非全屏时", + "Choose file": "选择文件", + "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "随机 Konachan SFW 动漫壁纸\n图片保存到 ~/图片/Wallpapers", + "Be patient...": "请耐心等待...", + "Decorations & Effects": "装饰与特效", + "Tonal Spot": "色调点", + "Shell windows": "Shell 窗口", + "Auto": "自动", + "Wallpaper": "壁纸", + "Content": "内容", + "Title bar": "标题栏", + "Transparency": "透明度", + "Expressive": "表现力", + "Yes": "是", + "Enable": "启用", + "Rainbow": "彩虹", + "Might look ass. Unsupported.": "可能效果很差。不支持。", + "Monochrome": "单色", + "Random: Konachan": "随机:Konachan", + "Center title": "标题居中", + "Neutral": "中性", + "Pick wallpaper image on your system": "在系统中选择壁纸图片", + "No": "否", + "AI": "AI", + "Local only": "仅本地", + "Policies": "策略", + "Weeb": "二次元", + "Closet": "隐藏", + "Bar style": "条栏样式", + "Show next time": "下次显示", + "Usage": "用法", + "Plain rectangle": "纯矩形", + "Useless buttons": "无用按钮", + "GitHub": "GitHub", + "Style & wallpaper": "样式与壁纸", + "Configuration": "配置", + "Change any time later with /dark, /light, /img in the launcher": "之后可在启动器用 /dark、/light、/img 更改", + "Keybinds": "快捷键", + "Float": "浮动", + "Hug": "贴合", + "Yooooo hi there": "哟嗬,您好呀", + "illogical-impulse Welcome": "illogical-impulse 欢迎页", + "Info": "信息", + "Volume limit": "音量限制", + "Prevents abrupt increments and restricts volume limit": "防止骤增并限制音量", + "Resources": "资源", + "12h am/pm": "12小时 上午/下午", + "Base URL": "基础 URL", + "Audio": "声音", + "Networking": "网络", + "Format": "格式", + "Time": "时间", + "Battery": "电池", + "Prefixes": "前缀", + "Emojis": "表情符号", + "Earbang protection": "防爆音保护", + "Automatically suspends the system when battery is low": "电池电量低时自动挂起系统", + "Automatic suspend": "自动挂起", + "Suspend at": "挂起阈值", + "Max allowed increase": "最大允许增幅", + "Web search": "网页搜索", + "Polling interval (ms)": "轮询间隔(毫秒)", + "Clipboard": "剪贴板", + "Low warning": "低电量警告", + "24h": "24小时制", + "Use Levenshtein distance-based algorithm instead of fuzzy": "使用 Levenshtein 距离算法替代模糊匹配", + "System prompt": "系统提示词", + "12h AM/PM": "12小时 AM/PM", + "Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "如果你经常打错字可能更好用,但结果可能很奇怪,并且可能无法匹配缩写(如 \"GIMP\" 可能搜不到绘图程序)", + "Critical warning": "临界警告", + "User agent (for services that require it)": "用户代理(部分服务需要)", + "Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "这些区域可能是图片或屏幕中具有一定包容性的部分。\n可能并不总是准确。\n这是通过本地运行的图像处理算法实现的,没有使用 AI。", + "Note: turning off can hurt readability": "注意:关闭后可能影响可读性", + "Workspaces shown": "显示的工作区数", + "Dark/Light toggle": "深浅色切换", + "Dock": "停靠栏", + "Weather": "天气", + "Pinned on startup": "启动时固定", + "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "提示:隐藏图标并始终显示数字以获得经典体验", + "Appearance": "外观", + "Always show numbers": "总是显示数字", + "Buttons": "按钮", + "Keyboard toggle": "键盘切换", + "Scale (%)": "缩放比例(%)", + "Overview": "概览", + "Rows": "行数", + "Borderless": "无边框", + "Screenshot tool": "截图工具", + "Number show delay when pressing Super (ms)": "按下 Super 时数字显示延迟(ms)", + "Timeout (ms)": "超时时间(ms)", + "Show app icons": "显示应用图标", + "Workspaces": "工作区", + "Columns": "列数", + "On-screen display": "屏幕显示", + "Screen snip": "屏幕截图", + "Mic toggle": "麦克风切换", + "Hover to reveal": "悬停显示", + "Bar": "条栏", + "Show background": "显示背景", + "Show regions of potential interest": "显示可能感兴趣的区域", + "Color picker": "取色器", + "Help & Support": "帮助与支持", + "Discussions": "讨论区", + "Color generation": "配色生成", + "Dotfiles": "配置文件", + "Distro": "发行版", + "Privacy Policy": "隐私政策", + "Documentation": "文档", + "Shell & utilities theming must also be enabled": "必须同时启用 Shell 与工具主题", + "illogical-impulse": "illogical-impulse", + "Donate": "捐助", + "Terminal": "终端", + "Shell & utilities": "Shell 与工具", + "Qt apps": "Qt 应用", + "Report a Bug": "报告问题", + "Issues": "问题追踪", + "Drag or click a region • LMB: Copy • RMB: Edit": "拖动或点击一个区域 • 鼠标左键:复制 • 鼠标右键:编辑", + "Current model: %1\nSet it with %2model MODEL": "当前模型:%1\n使用 %2model MODEL 设置", + "Message the model... \"%1\" for commands": "与模型对话... \"%1\" 查看命令", + "The current system prompt is\n\n---\n\n%1": "当前系统提示词为\n\n---\n\n%1", + "Model set to %1": "模型已设置为 %1", + "Loaded the following system prompt\n\n---\n\n%1": "已加载以下系统提示词\n\n---\n\n%1", + "%1 notifications": "%1 条通知", + "Save chat to %1": "保存聊天记录到 %1", + "Load chat from %1": "从 %1 加载聊天记录", + "Load prompt from %1": "从 %1 加载提示词", + "Select output device": "选择输出设备", + "%1 • %2 tasks": "%1 • %2 个任务", + "Online models disallowed\n\nControlled by `policies.ai` config option": "禁止在线模型\n\n由 `policies.ai` 配置项控制", + "Select input device": "选择输入设备", + "Low battery": "电量低", + "Registration failed. Please inspect manually with the warp-cli command": "注册失败。请使用 warp-cli 命令手动检查", + "Code saved to file": "代码已保存到文件", + "Consider plugging in your device": "请考虑连接您的设备", + "Weather Service": "天气服务", + "Please charge!\nAutomatic suspend triggers at %1": "请充电!\n自动挂起将在 %1 时触发", + "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", + "Cloudflare WARP": "Cloudflare WARP", + "Download complete": "下载完成", + "Critically low battery": "电量极低", + "Scroll to change brightness": "滚动以调节亮度", + "Saved to %1": "已保存到 %1", + "Cannot find a GPS service. Using the fallback method instead.": "无法找到 GPS 服务。正在使用备用方法。", + "Elements": "元素", + "Scroll to change volume": "滚动以调节音量", + "Connection failed. Please inspect manually with the warp-cli command": "连接失败。请使用 warp-cli 命令手动检查", + "UV Index": "紫外线指数", + "Pressure": "气压", + "Visibility": "能见度", + "Sunrise": "日出", + "Sunset": "日落", + "Humidity": "湿度", + "Wind": "风", + "Precipitation": "降水量" +} diff --git a/.config/quickshell/welcome.qml b/.config/quickshell/welcome.qml deleted file mode 100644 index 75b283a4..00000000 --- a/.config/quickshell/welcome.qml +++ /dev/null @@ -1,561 +0,0 @@ -//@ pragma UseQApplication -//@ pragma Env QS_NO_RELOAD_POPUP=1 -//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic - -// Adjust this to make the app smaller or larger -//@ pragma Env QT_SCALE_FACTOR=1 - -import Qt5Compat.GraphicalEffects -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Window -import Quickshell -import Quickshell.Io -import Quickshell.Hyprland -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/common/functions/file_utils.js" as FileUtils -import "root:/modules/common/functions/string_utils.js" as StringUtils - -ApplicationWindow { - id: root - property string firstRunFilePath: FileUtils.trimFileProtocol(`${Directories.state}/user/first_run.txt`) - property string firstRunFileContent: "This file is just here to confirm you've been greeted :>" - property real contentPadding: 8 - property bool showNextTime: false - visible: true - onClosing: Qt.quit() - title: "illogical-impulse Welcome" - - Component.onCompleted: { - MaterialThemeLoader.reapplyTheme() - ConfigLoader.loadConfig() - } - - minimumWidth: 600 - minimumHeight: 400 - width: 800 - height: 650 - color: Appearance.m3colors.m3background - - Process { - id: konachanWallProc - property string status: "" - command: ["bash", "-c", FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/colors/random_konachan_wall.sh`)] - stdout: SplitParser { - onRead: data => { - console.log(`Konachan wall proc output: ${data}`); - konachanWallProc.status = data.trim(); - } - } - } - - component Section: ColumnLayout { - id: sectionRoot - property string title - default property alias data: sectionContent.data - - Layout.fillWidth: true - spacing: 8 - StyledText { - text: sectionRoot.title - font.pixelSize: Appearance.font.pixelSize.larger - } - ColumnLayout { - id: sectionContent - spacing: 5 - } - } - - component ButtonWithIcon: RippleButton { - id: buttonWithIconRoot - property string nerdIcon - property string iconText - property string mainText: "Button text" - property Component mainContentComponent: Component { - StyledText { - text: buttonWithIconRoot.mainText - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.colors.colOnSecondaryContainer - } - } - implicitHeight: 35 - horizontalPadding: 15 - buttonRadius: Appearance.rounding.small - colBackground: Appearance.colors.colLayer2 - - contentItem: RowLayout { - Item { - implicitWidth: Math.max(materialIconLoader.implicitWidth, nerdIconLoader.implicitWidth) - Loader { - id: materialIconLoader - anchors.centerIn: parent - active: !nerdIcon - sourceComponent: MaterialSymbol { - text: buttonWithIconRoot.iconText - iconSize: Appearance.font.pixelSize.larger - color: Appearance.colors.colOnSecondaryContainer - fill: 1 - } - } - Loader { - id: nerdIconLoader - anchors.centerIn: parent - active: nerdIcon - sourceComponent: StyledText { - text: buttonWithIconRoot.nerdIcon - font.pixelSize: Appearance.font.pixelSize.larger - font.family: Appearance.font.family.iconNerd - color: Appearance.colors.colOnSecondaryContainer - } - } - } - Loader { - sourceComponent: buttonWithIconRoot.mainContentComponent - Layout.alignment: Qt.AlignVCenter - } - } - } - - component LightDarkPrefButton: GroupButton { - id: lightDarkButtonRoot - required property bool dark - property color previewBg: dark ? ColorUtils.colorWithHueOf("#3f3838", Appearance.m3colors.m3primary) : - ColorUtils.colorWithHueOf("#F7F9FF", Appearance.m3colors.m3primary) - property color previewFg: dark ? Qt.lighter(previewBg, 2.2) : ColorUtils.mix(previewBg, "#292929", 0.85) - padding: 5 - Layout.fillWidth: true - colBackground: Appearance.colors.colLayer2 - toggled: Appearance.m3colors.darkmode === dark - onClicked: { - Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode ${dark ? "dark" : "light"} --noswitch`) - } - contentItem: Item { - anchors.centerIn: parent - implicitWidth: buttonContentLayout.implicitWidth - implicitHeight: buttonContentLayout.implicitHeight - ColumnLayout { - id: buttonContentLayout - anchors.centerIn: parent - Rectangle { - Layout.alignment: Qt.AlignHCenter - implicitWidth: 250 - implicitHeight: skeletonColumnLayout.implicitHeight + 10 * 2 - radius: lightDarkButtonRoot.buttonRadius - lightDarkButtonRoot.padding - color: lightDarkButtonRoot.previewBg - border { - width: 1 - color: Appearance.m3colors.m3outlineVariant - } - - // Some skeleton items - ColumnLayout { - id: skeletonColumnLayout - anchors.fill: parent - anchors.margins: 10 - spacing: 10 - RowLayout { - Rectangle { - radius: Appearance.rounding.full - color: lightDarkButtonRoot.previewFg - implicitWidth: 50 - implicitHeight: 50 - } - ColumnLayout { - spacing: 4 - Rectangle { - radius: Appearance.rounding.unsharpenmore - color: lightDarkButtonRoot.previewFg - Layout.fillWidth: true - implicitHeight: 22 - } - Rectangle { - radius: Appearance.rounding.unsharpenmore - color: lightDarkButtonRoot.previewFg - Layout.fillWidth: true - Layout.rightMargin: 45 - implicitHeight: 18 - } - } - } - StyledProgressBar { - Layout.topMargin: 5 - Layout.bottomMargin: 5 - Layout.fillWidth: true - value: 0.7 - sperm: true - animateSperm: lightDarkButtonRoot.toggled - highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg - trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5) - } - RowLayout { - spacing: 2 - Rectangle { - radius: Appearance.rounding.full - color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg - Layout.fillWidth: true - implicitHeight: 30 - MaterialSymbol { - visible: lightDarkButtonRoot.toggled - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - text: "check" - iconSize: 20 - color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : lightDarkButtonRoot.previewBg - } - } - Rectangle { - radius: Appearance.rounding.unsharpenmore - color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg - Layout.fillWidth: true - implicitHeight: 30 - } - Rectangle { - topLeftRadius: Appearance.rounding.unsharpenmore - bottomLeftRadius: Appearance.rounding.unsharpenmore - topRightRadius: Appearance.rounding.full - bottomRightRadius: Appearance.rounding.full - color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg - Layout.fillWidth: true - implicitHeight: 30 - } - } - } - } - StyledText { - Layout.fillWidth: true - text: dark ? "Dark" : "Light" - color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2 - horizontalAlignment: Text.AlignHCenter - } - } - } - } - - ColumnLayout { - anchors { - fill: parent - margins: contentPadding - } - - Item { // Titlebar - visible: ConfigOptions?.windows.showTitlebar - Layout.fillWidth: true - implicitHeight: Math.max(welcomeText.implicitHeight, windowControlsRow.implicitHeight) - StyledText { - id: welcomeText - anchors.centerIn: parent - color: Appearance.colors.colOnLayer0 - text: "Yooooo hi there" - font.pixelSize: Appearance.font.pixelSize.title - font.family: Appearance.font.family.title - } - RowLayout { // Window controls row - id: windowControlsRow - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - StyledText { - font.pixelSize: Appearance.font.pixelSize.smaller - text: "Show next time" - } - StyledSwitch { - id: showNextTimeSwitch - checked: root.showNextTime - scale: 0.6 - Layout.alignment: Qt.AlignVCenter - onCheckedChanged: { - if (checked) { - Hyprland.dispatch(`exec rm '${StringUtils.shellSingleQuoteEscape(root.firstRunFilePath)}'`) - } else { - console.log(`exec echo '${StringUtils.shellSingleQuoteEscape(root.firstRunFileContent)}' > '${StringUtils.shellSingleQuoteEscape(root.firstRunFilePath)}'`) - Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(root.firstRunFileContent)}' > '${StringUtils.shellSingleQuoteEscape(root.firstRunFilePath)}'`) - } - } - } - RippleButton { - buttonRadius: Appearance.rounding.full - implicitWidth: 35 - implicitHeight: 35 - onClicked: root.close() - contentItem: MaterialSymbol { - anchors.centerIn: parent - horizontalAlignment: Text.AlignHCenter - text: "close" - iconSize: 20 - } - } - } - } - Rectangle { // Content container - color: Appearance.m3colors.m3surfaceContainerLow - radius: Appearance.rounding.windowRounding - root.contentPadding - implicitHeight: contentColumn.implicitHeight - implicitWidth: contentColumn.implicitWidth - Layout.fillWidth: true - Layout.fillHeight: true - Flickable { - clip: true - anchors.fill: parent - contentHeight: contentColumn.implicitHeight - implicitWidth: contentColumn.implicitWidth - ColumnLayout { - id: contentColumn - anchors { - top: parent.top - bottom: parent.bottom - horizontalCenter: parent.horizontalCenter - margins: 10 - } - spacing: 20 - - Section { - title: "Style & wallpaper" - - ButtonGroup { - Layout.fillWidth: true - LightDarkPrefButton { - dark: false - } - LightDarkPrefButton { - dark: true - } - } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - ButtonWithIcon { - id: rndWallBtn - Layout.alignment: Qt.AlignHCenter - buttonRadius: Appearance.rounding.small - iconText: "wallpaper" - mainText: konachanWallProc.running ? "Be patient..." : "Random: Konachan" - onClicked: { - console.log(konachanWallProc.command.join(" ")) - konachanWallProc.running = true; - } - StyledToolTip { - content: "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers" - } - } - ButtonWithIcon { - iconText: "wallpaper" - StyledToolTip { - content: "Pick wallpaper image on your system" - } - onClicked: { - Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath}`) - } - mainContentComponent: Component { - RowLayout { - spacing: 10 - StyledText { - font.pixelSize: Appearance.font.pixelSize.small - text: "Choose file" - color: Appearance.colors.colOnSecondaryContainer - } - RowLayout { - spacing: 3 - KeyboardKey { - key: "Ctrl" - } - KeyboardKey { - key: "󰖳" - } - StyledText { - Layout.alignment: Qt.AlignVCenter - text: "+" - } - KeyboardKey { - key: "T" - } - } - } - } - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: "Change any time later with /dark, /light, /img in the launcher" - font.pixelSize: Appearance.font.pixelSize.smaller - color: Appearance.colors.colSubtext - } - } - - Section { - title: "Policies" - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 15 - ColumnLayout { // Weeb policy - StyledText { - text: "Weeb" - color: Appearance.colors.colSubtext - } - ButtonGroup { - id: weebPolicyBtnGroup - property int selectedPolicy: ConfigOptions.policies.weeb - spacing: 2 - SelectionGroupButton { - property int value: 0 - leftmost: true - buttonText: "No" - toggled: (weebPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.weeb", value); - } - } - SelectionGroupButton { - property int value: 1 - buttonText: "Yes" - toggled: (weebPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.weeb", value); - } - } - SelectionGroupButton { - property int value: 2 - rightmost: true - buttonText: "Closet" - toggled: (weebPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.weeb", value); - } - StyledToolTip { - content: "The Anime tab on the left sidebar would still\nbe available, but its tab button won't show" - } - } - } - } - ColumnLayout { // AI policy - StyledText { - text: "AI" - color: Appearance.colors.colSubtext - } - ButtonGroup { - id: aiPolicyBtnGroup - property int selectedPolicy: ConfigOptions.policies.ai - spacing: 2 - SelectionGroupButton { - property int value: 0 - leftmost: true - buttonText: "No" - toggled: (aiPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.ai", value); - } - } - SelectionGroupButton { - property int value: 1 - buttonText: "Yes" - toggled: (aiPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.ai", value); - } - } - SelectionGroupButton { - property int value: 2 - rightmost: true - buttonText: "Local only" - toggled: (aiPolicyBtnGroup.selectedPolicy === value) - onClicked: { - ConfigLoader.setConfigValueAndSave("policies.ai", value); - } - } - } - } - } - } - - Section { - title: "Info" - - Flow { - Layout.fillWidth: true - spacing: 10 - - ButtonWithIcon { - iconText: "keyboard_alt" - onClicked: { - Hyprland.dispatch("global quickshell:cheatsheetOpen") - } - mainContentComponent: Component { - RowLayout { - spacing: 10 - StyledText { - font.pixelSize: Appearance.font.pixelSize.small - text: "Keybinds" - color: Appearance.colors.colOnSecondaryContainer - } - RowLayout { - spacing: 3 - KeyboardKey { - key: "󰖳" - } - StyledText { - Layout.alignment: Qt.AlignVCenter - text: "+" - } - KeyboardKey { - key: "/" - } - } - } - } - } - - ButtonWithIcon { - iconText: "help" - mainText: "Usage" - onClicked: { - Qt.openUrlExternally("https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/02usage/") - } - } - ButtonWithIcon { - iconText: "construction" - mainText: "Configuration" - onClicked: { - Qt.openUrlExternally("https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/03config/") - } - } - } - } - - Section { - title: "Useless buttons" - - Flow { - Layout.fillWidth: true - spacing: 10 - - ButtonWithIcon { - nerdIcon: "󰊤" - mainText: "GitHub" - onClicked: { - Qt.openUrlExternally("https://github.com/end-4/dots-hyprland") - } - } - ButtonWithIcon { - iconText: "favorite" - mainText: "Funny number" - onClicked: { - Qt.openUrlExternally("https://github.com/sponsors/end-4") - } - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - - } - } - } - } -} diff --git a/.config/wlogout/layout b/.config/wlogout/layout index c31abd94..03d696b0 100644 --- a/.config/wlogout/layout +++ b/.config/wlogout/layout @@ -7,18 +7,18 @@ { "label" : "hibernate", "action" : "systemctl hibernate || loginctl hibernate", - "text" : "save", + "text" : "downloading", "keybind" : "h" } { "label" : "logout", - "action" : "pkill Hyprland || pkill sway || pkill niri || loginctl terminate-user $USER", + "action" : "hyprctl clients -j | jq -r '.[].pid' | xargs kill; pkill Hyprland || pkill sway || pkill niri || loginctl terminate-user $USER", "text" : "logout", "keybind" : "e" } { "label" : "shutdown", - "action" : "systemctl poweroff || loginctl poweroff", + "action" : "hyprctl clients -j | jq -r '.[].pid' | xargs kill; systemctl poweroff || loginctl poweroff", "text" : "power_settings_new", "keybind" : "s" } @@ -30,7 +30,7 @@ } { "label" : "reboot", - "action" : "systemctl reboot || loginctl reboot", + "action" : "hyprctl clients -j | jq -r '.[].pid' | xargs kill; systemctl reboot || loginctl reboot", "text" : "restart_alt", "keybind" : "r" } diff --git a/.github/assets/illogical-impulse.svg b/.github/assets/illogical-impulse.svg new file mode 100644 index 00000000..4d1362b0 --- /dev/null +++ b/.github/assets/illogical-impulse.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore index d3c0402e..e8c1afab 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ # Ignore Python cache files __pycache__/ *.py[cod] +.config/quickshell/ii/.qmlls.ini diff --git a/.local/share/icons/illogical-impulse.svg b/.local/share/icons/illogical-impulse.svg new file mode 100644 index 00000000..8b1b789c --- /dev/null +++ b/.local/share/icons/illogical-impulse.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.local/share/icons/quickshell.svg b/.local/share/icons/quickshell.svg new file mode 120000 index 00000000..8dc05d01 --- /dev/null +++ b/.local/share/icons/quickshell.svg @@ -0,0 +1 @@ +illogical-impulse.svg \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aaaa3de8..7af06aaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,35 @@ # Contributing -- Please understand that dotfiles are personal -- If you make new stuff, I'll probably nitpick as I want quality -- FFS pls make multiple PRs if you have many features 😭🙏 + +- I can accept features I do not personally want, but in that case I will ask you to make it configurable/optionally loaded +- If you want to add new stuff, it's a good idea to ask me first to not waste your work +- Please make multiple PRs if you have many features/fixes + +# Setting up + +Assumption: you have an Arch(-based) Linux system + +## Complete + +_might not be necessary depending on what you change, but this is recommended_ + +- [Install](https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/01setup/) the dotfiles (if you don't wanna replace your stuff completely, do it on a new user) +- Make changes, copy changes to a fork, PR + +## Partially working shell + +_most stuff in the shell will work but not everything_ + +- Install Hyprland and the development version of Quickshell (`yay -S hyprland quickshell-git`) +- Copy `.config/quickshell` folder to your home directory + +## Extra setup for Quickshell +- Quickshell-specific LSP setup: Run `touch ~/.config/quickshell/ii/.qmlls.ini` for proper LSP support +- Hint for VSCode: Get the official "Qt Qml" extension, go to its settings and change custom exe path to `/usr/bin/qmlls6` + +# Running + +- Launch Hyprland (not the "uwsm-managed" one) +- For the shell: + - Open `~/.config/quickshell/ii` in your code editor + - In a terminal run `pkill qs; qs -c ii` to start the shell in the terminal (for logs) + - Make edits in the opened folder. Changes are reloaded live. diff --git a/README.md b/README.md index e381b125..75f55661 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@
-![](https://img.shields.io/github/last-commit/end-4/dots-hyprland?&style=for-the-badge&color=FFB1C8&logoColor=D9E0EE&labelColor=292324) -![](https://img.shields.io/github/stars/end-4/dots-hyprland?style=for-the-badge&logo=andela&color=FFB686&logoColor=D9E0EE&labelColor=292324) -[![](https://img.shields.io/github/repo-size/end-4/dots-hyprland?color=CAC992&label=SIZE&logo=googledrive&style=for-the-badge&logoColor=D9E0EE&labelColor=292324)](https://github.com/end-4/hyprland) -![](https://img.shields.io/badge/issues-skill-green?style=for-the-badge&color=CCE8E9&logoColor=D9E0EE&labelColor=292324) - +![](https://img.shields.io/github/last-commit/end-4/dots-hyprland?&style=for-the-badge&color=8ad7eb&logo=git&logoColor=D9E0EE&labelColor=1E202B) +![](https://img.shields.io/github/stars/end-4/dots-hyprland?style=for-the-badge&logo=andela&color=86dbd7&logoColor=D9E0EE&labelColor=1E202B) +![](https://img.shields.io/github/repo-size/end-4/dots-hyprland?color=86dbce&label=SIZE&logo=protondrive&style=for-the-badge&logoColor=D9E0EE&labelColor=26230e) + Dynamic JSON Badge
@@ -18,7 +17,6 @@

-
Notable features @@ -28,25 +26,15 @@ - **Transparent installation**: Every command is shown before it's run
- Instructions + Installation instructions - - **Prerequisite**: Your system works. That's it. You don't have to reinstall your system! - - **Automatic**, but guided and transparent, installation for Arch(-based) Linux: - ```bash - bash <(curl -s "https://end-4.github.io/dots-hyprland-wiki/setup.sh") - ``` + - See the [Wiki](https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/01setup/) + - **Default keybinds**: Should be somewhat familiar to Windows or GNOME users. + - `Super`+`/` = keybind list + - `Super`+`Enter` = terminal + - If for whatever reason the keybind list widget does not work, here's an image: + image - If you are using fish shell (non-posix-compliant shell) then: - ```bash - bash -c "$(curl -s https://end-4.github.io/dots-hyprland-wiki/setup.sh)" - ``` - - - **Manual** installation, other distros and more: - - See the [Wiki](https://end-4.github.io/dots-hyprland-wiki/en/ii-qs/01setup/) - - - **Default keybinds**: Should be somewhat familiar if you've used Windows or GNOME. - - For a list, hit `Super`+`/` - - For a terminal, hit `Super`+`Enter`
@@ -65,30 +53,44 @@
- Logo ideas welcome - - - See [#1436](https://github.com/end-4/dots-hyprland/issues/1436) - + Discord + Server link | I hope this provides a friendlier environment for support without needing me to personally accept every friend request/DM. For real issues, prefer GitHub +
+

• screenshots •

-## illogical-impulseQuickshell + + + + + +
+ illogical-impulse logo + + latest and only style that I actively use. Other past styles are still there for your viewing pleasure and not actual use, but code is still available, see below. +
-| AI | Common widgets | + +### illogical-impulseQuickshell + +Widget system: Quickshell | Support: Yes + +[Showcase video](https://www.youtube.com/watch?v=RPwovTInagE) + +| AI, settings app | Some widgets | |:---|:---------------| -| ![image](https://github.com/user-attachments/assets/08d26785-b54d-4ad1-875b-bb08cc6757f5) | ![image](https://github.com/user-attachments/assets/4fcd63d9-0943-4b21-8737-4bed97b71961) | +| ![image](https://github.com/user-attachments/assets/4d45e860-ae60-418a-a564-309b4939f5c4) | ![image](https://github.com/user-attachments/assets/4fcd63d9-0943-4b21-8737-4bed97b71961) | | Window management | Weeb power | | ![image](https://github.com/user-attachments/assets/86cc511b-0d33-4c78-bcc0-3037d02a17da) | ![image](https://github.com/user-attachments/assets/292259fc-57d3-4663-a583-2ce2faad13fb) | -By the way... -- The funny notification positions are mimicking Android 16's dragging behavior -- The clock on the wallpaper is automatically placed at the "least busy" region of the image +### illogical-impulseAGS (Deprecated) -## illogical-impulseAGS (Deprecated) +Widget system: AGS | Support: Limited, no new features | AI | Common widgets | |:---|:---------------| @@ -96,28 +98,39 @@ By the way... | Window management | Weeb power | | ![image](https://github.com/user-attachments/assets/02983b9b-79ba-4c25-8717-90bef2357ae5) | ![image](https://github.com/user-attachments/assets/bbb332ec-962a-4e88-a95b-486d0bd8ce76) | -## Unsupported stuff +### Very old stuff -- The pics are here mainly for your viewing pleasure -- The files are still available, feel free to grab them from the [`archive`](https://github.com/end-4/dots-hyprland/tree/archive) branch if you're willing to see some spaghetti and troubleshoot +- Source code not likely to work but still available in the [`archive`](https://github.com/end-4/dots-hyprland/tree/archive) branch. Extremely spaghetti. - Click image for a presentation video -### [m3ww](https://github.com/end-4/dots-hyprland/tree/archive) +#### m3ww + + Widget system: EWW | Support: No, dead + Material Eww! -### [NovelKnock](https://github.com/end-4/dots-hyprland/tree/archive) +#### NovelKnock + + Widget system: EWW | Support: No, dead + Desktop Preview -### [Hybrid](https://github.com/end-4/dots-hyprland/tree/archive) +#### Hybrid + + Widget system: EWW | Support: No, dead + click the circles! -### [Windoes](https://github.com/end-4/dots-hyprland/tree/archive) +#### Windoes + + Widget system: EWW | Support: No, dead + Desktop Preview @@ -135,6 +148,8 @@ By the way... - EWW: [fufexan's config](https://github.com/fufexan/dotfiles) (he thanks more people there btw) - AI bots for providing useful examples + [@tokyobot](https://github.com/tokyob0t) for saying hi +

• stonks •

diff --git a/arch-packages/illogical-impulse-backlight/PKGBUILD b/arch-packages/illogical-impulse-backlight/PKGBUILD index 2525be9d..451c7e5d 100644 --- a/arch-packages/illogical-impulse-backlight/PKGBUILD +++ b/arch-packages/illogical-impulse-backlight/PKGBUILD @@ -5,7 +5,6 @@ pkgdesc='Illogical Impulse Backlight Dependencies' arch=(any) license=(None) depends=( - gammastep geoclue brightnessctl ddcutil diff --git a/arch-packages/illogical-impulse-fonts-themes/PKGBUILD b/arch-packages/illogical-impulse-fonts-themes/PKGBUILD index 12a7209b..fd2ee207 100644 --- a/arch-packages/illogical-impulse-fonts-themes/PKGBUILD +++ b/arch-packages/illogical-impulse-fonts-themes/PKGBUILD @@ -6,13 +6,16 @@ arch=(any) license=(None) depends=( adw-gtk-theme-git + breeze breeze-plus + darkly-bin eza fish fontconfig kde-material-you-colors kitty matugen-bin + otf-space-grotesk starship ttf-gabarito-git ttf-jetbrains-mono-nerd diff --git a/arch-packages/illogical-impulse-kde/PKGBUILD b/arch-packages/illogical-impulse-kde/PKGBUILD index b8b91c3f..99eec565 100644 --- a/arch-packages/illogical-impulse-kde/PKGBUILD +++ b/arch-packages/illogical-impulse-kde/PKGBUILD @@ -10,5 +10,6 @@ depends=( networkmanager plasma-nm polkit-kde-agent + dolphin systemsettings ) diff --git a/arch-packages/illogical-impulse-portal/PKGBUILD b/arch-packages/illogical-impulse-portal/PKGBUILD index dbc96e2e..4394d848 100644 --- a/arch-packages/illogical-impulse-portal/PKGBUILD +++ b/arch-packages/illogical-impulse-portal/PKGBUILD @@ -7,6 +7,7 @@ license=(None) depends=( xdg-desktop-portal xdg-desktop-portal-kde + xdg-desktop-portal-gtk xdg-desktop-portal-hyprland ) diff --git a/arch-packages/illogical-impulse-screencapture/PKGBUILD b/arch-packages/illogical-impulse-screencapture/PKGBUILD index 8feb1c0d..cde216bf 100644 --- a/arch-packages/illogical-impulse-screencapture/PKGBUILD +++ b/arch-packages/illogical-impulse-screencapture/PKGBUILD @@ -5,11 +5,10 @@ pkgdesc='Illogical Impulse Screenshot and Recording Dependencies' arch=(any) license=(None) depends=( - swappy - wf-recorder hyprshot + slurp + swappy tesseract tesseract-data-eng - slurp + wf-recorder ) - diff --git a/arch-packages/illogical-impulse-toolkit/PKGBUILD b/arch-packages/illogical-impulse-toolkit/PKGBUILD index 6f12584a..636709f2 100644 --- a/arch-packages/illogical-impulse-toolkit/PKGBUILD +++ b/arch-packages/illogical-impulse-toolkit/PKGBUILD @@ -7,6 +7,7 @@ license=(None) depends=( kdialog qt6-5compat + qt6-avif-image-plugin qt6-base qt6-declarative qt6-imageformats diff --git a/arch-packages/illogical-impulse-widgets/PKGBUILD b/arch-packages/illogical-impulse-widgets/PKGBUILD index 868bea9e..9aa690dc 100644 --- a/arch-packages/illogical-impulse-widgets/PKGBUILD +++ b/arch-packages/illogical-impulse-widgets/PKGBUILD @@ -12,8 +12,7 @@ depends=( hyprlock hyprpicker nm-connection-editor - quickshell - swww + quickshell-git translate-shell wlogout ) diff --git a/install.sh b/install.sh index e22f912b..ae9891e9 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,7 @@ source ./scriptdata/installers source ./scriptdata/options ##################################################################################### -if ! command -v pacman >/dev/null 2>&1; then +if ! command -v pacman >/dev/null 2>&1; then printf "\e[31m[$0]: pacman not found, it seems that the system is not ArchLinux or Arch-based distros. Aborting...\e[0m\n" exit 1 fi @@ -23,7 +23,7 @@ startask () { printf 'This script 1. only works for ArchLinux and Arch-based distros.\n' printf ' 2. does not handle system-level/hardware stuff like Nvidia drivers\n' printf "\e[31m" - + printf "Would you like to create a backup for \"$XDG_CONFIG_HOME\" and \"$HOME/.local/\" folders?\n[y/N]: " read -p " " backup_confirm case $backup_confirm in @@ -34,7 +34,7 @@ startask () { echo "Skipping backup..." ;; esac - + printf '\n' printf 'Do you want to confirm every time before a command executes?\n' @@ -144,7 +144,6 @@ esac v sudo usermod -aG video,i2c,input "$(whoami)" v bash -c "echo i2c-dev | sudo tee /etc/modules-load.d/i2c-dev.conf" -v sudo pacman -S archlinux-xdg-menu && XDG_MENU_PREFIX=arch- kbuildsycoca6; sudo ln -s /etc/xdg/menus/plasma-applications.menu /etc/xdg/menus/applications.menu v systemctl --user enable ydotool --now v sudo systemctl enable bluetooth --now v gsettings set org.gnome.desktop.interface font-name 'Rubik 11' @@ -233,6 +232,7 @@ esac # some foldes (eg. .local/bin) should be processed separately to avoid `--delete' for rsync, # since the files here come from different places, not only about one program. # v rsync -av ".local/bin/" "$XDG_BIN_HOME" # No longer needed since scripts are no longer in ~/.local/bin +v rsync -av ".local/share/icons/" "${XDG_DATA_HOME:-$HOME/.local/share}"/icons/ # Prevent hyprland from not fully loaded sleep 1 diff --git a/scriptdata/previous_dependencies.conf b/scriptdata/previous_dependencies.conf index 90ae9e75..4f32959e 100644 --- a/scriptdata/previous_dependencies.conf +++ b/scriptdata/previous_dependencies.conf @@ -1,6 +1,7 @@ ### This file contains a list of dependencies which were previously explicitly installed that are now installed using meta packages ### Must be one package per line as it needs to be compared against the explicitly installed list from pacman illogical-impulse-ags +archlinux-xdg-menu axel bc coreutils