diff --git a/.gitignore b/.gitignore index 9e298d0493..79d91282c1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ __pycache__/ # Emacs auto backup file *~ + +dots/.config/quickshell/ii/.agents/ +knowledge.md diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 8367692e33..1edb608f68 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -145,6 +145,12 @@ Singleton { } } + property JsonObject osk: JsonObject { + property int mode: 0 // 0=pinned, 1=floating, 2=fullwidth + property real floatX: 50 + property real floatY: 100 + } + property JsonObject timer: JsonObject { property JsonObject pomodoro: JsonObject { property bool running: false diff --git a/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml b/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml index 6d8c14598a..7e7d16f8d0 100644 --- a/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/background/widgets/AbstractBackgroundWidget.qml @@ -73,7 +73,8 @@ AbstractWidget { property int contentHeight: 300 property int horizontalPadding: 200 property int verticalPadding: 200 - command: [Quickshell.shellPath("scripts/images/least-busy-region-venv.sh") // Comments to force the formatter to break lines + command: [Quickshell.shellPath("scripts/run-in-venv.sh") // Comments to force the formatter to break lines + , "images/least_busy_region.py" // , "--screen-width", Math.round(root.scaledScreenWidth) // , "--screen-height", Math.round(root.scaledScreenHeight) // , "--width", contentWidth // diff --git a/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml b/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml index 3d98be7ca1..ae23e7804f 100644 --- a/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml +++ b/dots/.config/quickshell/ii/modules/ii/mediaControls/MediaControls.qml @@ -3,7 +3,6 @@ import qs import qs.services import qs.modules.common import qs.modules.common.widgets -import qs.modules.common.functions import QtQuick import QtQuick.Layouts import Quickshell @@ -18,12 +17,15 @@ Scope { readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property var realPlayers: MprisController.players readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers) + onMeaningfulPlayersChanged: { + if (meaningfulPlayers.length === 0 && GlobalStates.mediaControlsOpen) { + GlobalStates.mediaControlsOpen = false; + } + } readonly property real osdWidth: Appearance.sizes.osdWidth readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 - property list visualizerPoints: [] - function filterDuplicatePlayers(players) { let filtered = []; let used = new Set(); @@ -53,36 +55,13 @@ Scope { return filtered; } - Process { - id: cavaProc - running: mediaControlsLoader.active - onRunningChanged: { - if (!cavaProc.running) { - root.visualizerPoints = []; - } - } - command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`] - stdout: SplitParser { - onRead: data => { - // Parse `;`-separated values into the visualizerPoints array - let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p)); - root.visualizerPoints = points; - } - } - } - Loader { id: mediaControlsLoader - active: GlobalStates.mediaControlsOpen - onActiveChanged: { - if (!mediaControlsLoader.active && root.realPlayers.length === 0) { - GlobalStates.mediaControlsOpen = false; - } - } + active: true sourceComponent: PanelWindow { id: panelWindow - visible: true + visible: GlobalStates.mediaControlsOpen exclusionMode: ExclusionMode.Ignore exclusiveZone: 0 @@ -108,11 +87,12 @@ Scope { item: playerColumnLayout } - Component.onCompleted: { - GlobalFocusGrab.addDismissable(panelWindow); - } - Component.onDestruction: { - GlobalFocusGrab.removeDismissable(panelWindow); + onVisibleChanged: { + if (panelWindow.visible) { + GlobalFocusGrab.addDismissable(panelWindow); + } else { + GlobalFocusGrab.removeDismissable(panelWindow); + } } Connections { target: GlobalFocusGrab @@ -133,7 +113,6 @@ Scope { delegate: PlayerControl { required property MprisPlayer modelData player: modelData - visualizerPoints: root.visualizerPoints implicitWidth: root.widgetWidth implicitHeight: root.widgetHeight radius: root.popupRounding @@ -192,17 +171,17 @@ Scope { target: "mediaControls" function toggle(): void { - mediaControlsLoader.active = !mediaControlsLoader.active; - if (mediaControlsLoader.active) + GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen; + if (GlobalStates.mediaControlsOpen) Notifications.timeoutAll(); } function close(): void { - mediaControlsLoader.active = false; + GlobalStates.mediaControlsOpen = false; } function open(): void { - mediaControlsLoader.active = true; + GlobalStates.mediaControlsOpen = true; Notifications.timeoutAll(); } } diff --git a/dots/.config/quickshell/ii/modules/ii/mediaControls/PlayerControl.qml b/dots/.config/quickshell/ii/modules/ii/mediaControls/PlayerControl.qml index 7f0eabbca3..2fdea4a912 100644 --- a/dots/.config/quickshell/ii/modules/ii/mediaControls/PlayerControl.qml +++ b/dots/.config/quickshell/ii/modules/ii/mediaControls/PlayerControl.qml @@ -21,9 +21,6 @@ Item { // Player instance property string artFilePath: `${artDownloadLocation}/${artFileName}` 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 - property int visualizerSmoothing: 2 // Number of points to average for smoothing property real radius property string displayedArtFilePath: root.downloaded ? Qt.resolvedUrl(artFilePath) : "" @@ -134,16 +131,6 @@ Item { // Player instance } } - WaveVisualizer { - id: visualizerCanvas - anchors.fill: parent - live: root.player?.isPlaying - points: root.visualizerPoints - maxVisualizerValue: root.maxVisualizerValue - smoothing: root.visualizerSmoothing - color: blendedColors.colPrimary - } - RowLayout { anchors.fill: parent anchors.margins: 13 @@ -242,7 +229,7 @@ Item { // Player instance anchors.fill: parent active: root.player?.canSeek ?? false sourceComponent: StyledSlider { - configuration: StyledSlider.Configuration.Wavy + configuration: StyledSlider.Configuration.S highlightColor: blendedColors.colPrimary trackColor: blendedColors.colSecondaryContainer handleColor: blendedColors.colPrimary @@ -262,7 +249,7 @@ Item { // Player instance } active: !(root.player?.canSeek ?? false) sourceComponent: StyledProgressBar { - wavy: root.player?.isPlaying + wavy: false highlightColor: blendedColors.colPrimary trackColor: blendedColors.colSecondaryContainer value: root.player?.position / root.player?.length diff --git a/dots/.config/quickshell/ii/modules/ii/onScreenKeyboard/OnScreenKeyboard.qml b/dots/.config/quickshell/ii/modules/ii/onScreenKeyboard/OnScreenKeyboard.qml index 9dc1068d84..09cdccfb5f 100644 --- a/dots/.config/quickshell/ii/modules/ii/onScreenKeyboard/OnScreenKeyboard.qml +++ b/dots/.config/quickshell/ii/modules/ii/onScreenKeyboard/OnScreenKeyboard.qml @@ -12,9 +12,18 @@ import Quickshell.Hyprland Scope { // Scope id: root - property bool pinned: Config.options?.osk.pinnedOnStartup ?? false + // 0=floating (draggable, content width), 1=fullwidth (draggable, full width) + property int oskMode: Persistent.states.osk.mode + property real floatY: Persistent.states.osk.floatY + property real floatX: Persistent.states.osk.floatX + onOskModeChanged: { + if (root.oskMode === 1) { + root.floatX = 0 + Persistent.states.osk.floatX = 0 + } + } - component OskControlButton: GroupButton { // Pin button + component OskControlButton: GroupButton { // Control button baseWidth: 40 baseHeight: 40 clickedWidth: baseWidth @@ -30,21 +39,25 @@ Scope { // Scope Ydotool.releaseAllKeys(); } } - + sourceComponent: PanelWindow { // Window id: oskRoot visible: oskLoader.active && !GlobalStates.screenLocked anchors { - bottom: true + top: true left: true - right: true + right: root.oskMode === 1 // fullwidth stretches, floating is content-sized + } + margins { + top: root.floatY + left: root.oskMode === 0 ? root.floatX : 0 } function hide() { GlobalStates.oskOpen = false } - exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0 + exclusiveZone: 0 implicitWidth: oskBackground.width + Appearance.sizes.elevationMargin * 2 implicitHeight: oskBackground.height + Appearance.sizes.elevationMargin * 2 WlrLayershell.namespace: "quickshell:osk" @@ -57,6 +70,17 @@ Scope { // Scope item: oskBackground } + // Center keyboard when entering floating mode + Connections { + target: root + function onOskModeChanged() { + if (root.oskMode === 0 && oskRoot.screen) { + root.floatX = Math.max(0, (oskRoot.screen.width - oskBackground.implicitWidth) / 2) + Persistent.states.osk.floatX = root.floatX + } + } + } + // Make it usable with other panels Component.onCompleted: { GlobalFocusGrab.addPersistent(oskRoot); @@ -71,12 +95,16 @@ Scope { // Scope } Rectangle { id: oskBackground - anchors.centerIn: parent + // Always centered. Width switches between content-sized and parent-filling. + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: root.oskMode === 1 ? parent.width : implicitWidth color: Appearance.colors.colLayer0 radius: Appearance.rounding.windowRounding property real padding: 10 + property real dragHandleHeight: 24 implicitWidth: oskRowLayout.implicitWidth + padding * 2 - implicitHeight: oskRowLayout.implicitHeight + padding * 2 + implicitHeight: oskRowLayout.implicitHeight + dragHandleHeight + padding * 2 Keys.onPressed: (event) => { // Esc to close if (event.key === Qt.Key_Escape) { @@ -84,19 +112,75 @@ Scope { // Scope } } + // handle + Item { + id: dragHandle + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: oskBackground.dragHandleHeight + + Rectangle { + anchors.fill: parent + anchors.topMargin: 2 + radius: Appearance.rounding.verysmall + color: Appearance.colors.colLayer1 + + MaterialSymbol { + anchors.centerIn: parent + text: "drag_indicator" + iconSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnLayer2 + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.OpenHandCursor + property real lastX: 0 + property real lastY: 0 + + onPressed: (mouse) => { + lastX = mouse.x + lastY = mouse.y + } + onPositionChanged: (mouse) => { + if (pressed) { + if (root.oskMode === 0) { + root.floatX = Math.max(0, root.floatX + mouse.x - lastX) + } + root.floatY = Math.max(0, root.floatY + mouse.y - lastY) + lastX = mouse.x + lastY = mouse.y + } + } + onReleased: { + Persistent.states.osk.floatX = root.floatX + Persistent.states.osk.floatY = root.floatY + } + } + } + RowLayout { id: oskRowLayout - anchors.centerIn: parent + // Always centered. Width switches between content-sized and parent-filling. + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: oskBackground.dragHandleHeight / 2 + anchors.horizontalCenter: parent.horizontalCenter + width: root.oskMode === 1 ? parent.width - 2 * oskBackground.padding : implicitWidth spacing: 5 VerticalButtonGroup { - OskControlButton { // Pin button - toggled: root.pinned - downAction: () => root.pinned = !root.pinned + OskControlButton { // Mode cycle button + toggled: root.oskMode === 1 + downAction: () => { + root.oskMode = (root.oskMode + 1) % 2 + Persistent.states.osk.mode = root.oskMode + } contentItem: MaterialSymbol { - text: "keep" + text: ["open_in_full", "width_full"][root.oskMode] horizontalAlignment: Text.AlignHCenter iconSize: Appearance.font.pixelSize.larger - color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0 + color: root.oskMode === 0 ? Appearance.colors.colOnLayer0 : Appearance.m3colors.m3onPrimary } } OskControlButton { diff --git a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml index 19435a74c4..c30a104650 100644 --- a/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml +++ b/dots/.config/quickshell/ii/modules/ii/regionSelector/RegionSelection.qml @@ -220,8 +220,8 @@ PanelWindow { Process { id: imageDetectionProcess - command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh ` - + `--hyprctl ` + command: ["bash", "-c", `${Directories.scriptPath}/run-in-venv.sh ` + + `images/find_regions.py --hyprctl ` + `--image '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' ` + `--max-width ${Math.round(root.screen.width * root.falsePositivePreventionRatio)} ` + `--max-height ${Math.round(root.screen.height * root.falsePositivePreventionRatio)} `] diff --git a/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml index 175407ff31..0060036a8f 100644 --- a/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml +++ b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml @@ -22,7 +22,7 @@ Item { required property string screenshotPath readonly property string wikiLink: "https://ii.clsty.link/en/ii-qs/02usage/#setting-it-up" // TODO: write a page for this - readonly property string textColorDetectionScriptPath: Quickshell.shellPath("scripts/images/text-color-venv.sh") + readonly property string textColorDetectionScriptPath: Quickshell.shellPath("scripts/run-in-venv.sh") + " images/text_color.py" property bool loading: true property var visionParagraphs: [] diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml index e9c49dd4b2..0224b4f606 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/BottomWidgetGroup.qml @@ -17,6 +17,10 @@ Rectangle { property int selectedTab: Persistent.states.sidebar.bottomGroup.tab property int previousIndex: -1 property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed + readonly property int collapsedHeight: Math.max( + Appearance.font.pixelSize.larger + 24, + Appearance.font.pixelSize.large + 24 + ) property var tabs: [ { "type": "calendar", diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/CenterWidgetGroup.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/CenterWidgetGroup.qml index 4e7747eb62..229a3f49be 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/CenterWidgetGroup.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/CenterWidgetGroup.qml @@ -9,6 +9,7 @@ import QtQuick.Layouts Rectangle { id: root + clip: true radius: Appearance.rounding.normal color: Appearance.colors.colLayer1 diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/SidebarRightContent.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/SidebarRightContent.qml index 412912ed70..1ea46de3d8 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/SidebarRightContent.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/SidebarRightContent.qml @@ -96,17 +96,46 @@ Item { } } - CenterWidgetGroup { - Layout.alignment: Qt.AlignHCenter + Item { Layout.fillHeight: true Layout.fillWidth: true - } + clip: true - BottomWidgetGroup { - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: false - Layout.fillWidth: true - Layout.preferredHeight: implicitHeight + CenterWidgetGroup { + id: centerGroup + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: bottomGroup.top + anchors.bottomMargin: root.sidebarPadding + + opacity: bottomGroup.collapsed ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Appearance.animation.elementMove.duration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } + } + + BottomWidgetGroup { + id: bottomGroup + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: bottomGroup.collapsed ? bottomGroup.collapsedHeight : parent.height + + Behavior on height { + NumberAnimation { + duration: Appearance.animation.elementMove.duration + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } + } } } } diff --git a/dots/.config/quickshell/ii/modules/ii/sidebarRight/notifications/NotificationList.qml b/dots/.config/quickshell/ii/modules/ii/sidebarRight/notifications/NotificationList.qml index 99c1fb9a5d..c2d48bece0 100644 --- a/dots/.config/quickshell/ii/modules/ii/sidebarRight/notifications/NotificationList.qml +++ b/dots/.config/quickshell/ii/modules/ii/sidebarRight/notifications/NotificationList.qml @@ -68,4 +68,4 @@ Item { } } } -} \ No newline at end of file +} diff --git a/dots/.config/quickshell/ii/scripts/cava/raw_output_config.txt b/dots/.config/quickshell/ii/scripts/cava/raw_output_config.txt deleted file mode 100644 index 7760e4ea2d..0000000000 --- a/dots/.config/quickshell/ii/scripts/cava/raw_output_config.txt +++ /dev/null @@ -1,17 +0,0 @@ -[general] -mode = waves -framerate = 60 -autosens = 1 -bars = 50 - -[output] -method = raw -raw_target = /dev/stdout -data_format = ascii -channels = mono -mono_option = average - -[smoothing] -noise_reduction = 20 - - diff --git a/dots/.config/quickshell/ii/scripts/colors/applycolor.sh b/dots/.config/quickshell/ii/scripts/colors/applycolor.sh index a215c5abd7..77c1ac9bb8 100755 --- a/dots/.config/quickshell/ii/scripts/colors/applycolor.sh +++ b/dots/.config/quickshell/ii/scripts/colors/applycolor.sh @@ -16,16 +16,12 @@ if [ ! -d "$STATE_DIR"/user/generated ]; then fi cd "$CONFIG_DIR" || exit -colornames='' -colorstrings='' colorlist=() colorvalues=() - -colornames=$(cat $STATE_DIR/user/generated/material_colors.scss | cut -d: -f1) -colorstrings=$(cat $STATE_DIR/user/generated/material_colors.scss | cut -d: -f2 | cut -d ' ' -f2 | cut -d ";" -f1) -IFS=$'\n' -colorlist=($colornames) # Array of color names -colorvalues=($colorstrings) # Array of color values +while IFS=': ' read -r name value; do + colorlist+=("$name") + colorvalues+=("${value%;}") +done < "$STATE_DIR/user/generated/material_colors.scss" apply_kitty() { # Check if terminal escape sequence template exists @@ -42,9 +38,7 @@ apply_kitty() { done # Reload - if ! pgrep -f kitty >/dev/null; then - return - fi + pidof kitty || return kill -SIGUSR1 $(pidof kitty) } diff --git a/dots/.config/quickshell/ii/scripts/colors/generate_colors_material.py b/dots/.config/quickshell/ii/scripts/colors/generate_colors_material.py index db6b1664bb..a744188f45 100755 --- a/dots/.config/quickshell/ii/scripts/colors/generate_colors_material.py +++ b/dots/.config/quickshell/ii/scripts/colors/generate_colors_material.py @@ -1,7 +1,8 @@ -#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@"" +#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_\"\$0\"\_\"\$@\"" import argparse import math import json +import importlib from PIL import Image from materialyoucolor.quantize import QuantizeCelebi from materialyoucolor.score.score import Score @@ -30,8 +31,6 @@ rgba_to_hex = lambda rgba: "#{:02X}{:02X}{:02X}".format(rgba[0], rgba[1], rgba[2]) argb_to_hex = lambda argb: "#{:02X}{:02X}{:02X}".format(*map(round, rgba_from_argb(argb))) hex_to_argb = lambda hex_code: argb_from_rgb(int(hex_code[1:3], 16), int(hex_code[3:5], 16), int(hex_code[5:], 16)) -display_color = lambda rgba : "\x1B[38;2;{};{};{}m{}\x1B[0m".format(rgba[0], rgba[1], rgba[2], "\x1b[7m \x1b[7m") - def calculate_optimal_size (width: int, height: int, bitmap_size: int) -> (int, int): image_area = width * height; bitmap_area = bitmap_size ** 2 @@ -87,26 +86,35 @@ def boost_chroma_tone (argb: int, chroma: float = 1, tone: float = 1) -> int: argb = hex_to_argb(args.color) hct = Hct.from_int(argb) -if args.scheme == 'scheme-fruit-salad': - from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme -elif args.scheme == 'scheme-expressive': - from materialyoucolor.scheme.scheme_expressive import SchemeExpressive as Scheme -elif args.scheme == 'scheme-monochrome': - from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome as Scheme -elif args.scheme == 'scheme-rainbow': - from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow as Scheme -elif args.scheme == 'scheme-tonal-spot': - from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme -elif args.scheme == 'scheme-neutral': - from materialyoucolor.scheme.scheme_neutral import SchemeNeutral as Scheme -elif args.scheme == 'scheme-fidelity': - from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity as Scheme -elif args.scheme == 'scheme-content': - from materialyoucolor.scheme.scheme_content import SchemeContent as Scheme -elif args.scheme == 'scheme-vibrant': - from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant as Scheme -else: - from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme +# Map scheme names to module paths +_scheme_module_map = { + 'scheme-fruit-salad': 'materialyoucolor.scheme.scheme_fruit_salad', + 'scheme-expressive': 'materialyoucolor.scheme.scheme_expressive', + 'scheme-monochrome': 'materialyoucolor.scheme.scheme_monochrome', + 'scheme-rainbow': 'materialyoucolor.scheme.scheme_rainbow', + 'scheme-tonal-spot': 'materialyoucolor.scheme.scheme_tonal_spot', + 'scheme-neutral': 'materialyoucolor.scheme.scheme_neutral', + 'scheme-fidelity': 'materialyoucolor.scheme.scheme_fidelity', + 'scheme-content': 'materialyoucolor.scheme.scheme_content', + 'scheme-vibrant': 'materialyoucolor.scheme.scheme_vibrant', +} +_scheme_class_map = { + 'scheme-fruit-salad': 'SchemeFruitSalad', + 'scheme-expressive': 'SchemeExpressive', + 'scheme-monochrome': 'SchemeMonochrome', + 'scheme-rainbow': 'SchemeRainbow', + 'scheme-tonal-spot': 'SchemeTonalSpot', + 'scheme-neutral': 'SchemeNeutral', + 'scheme-fidelity': 'SchemeFidelity', + 'scheme-content': 'SchemeContent', + 'scheme-vibrant': 'SchemeVibrant', +} + +module_path = _scheme_module_map.get(args.scheme, _scheme_module_map['scheme-tonal-spot']) +class_name = _scheme_class_map.get(args.scheme, _scheme_class_map['scheme-tonal-spot']) +scheme_module = importlib.import_module(module_path) +Scheme = getattr(scheme_module, class_name) + # Generate scheme = Scheme(hct, darkmode, 0.0) @@ -151,31 +159,9 @@ def boost_chroma_tone (argb: int, chroma: float = 1, tone: float = 1) -> int: harmonized = boost_chroma_tone(harmonized, 1, 1 + (args.term_fg_boost * (1 if darkmode else -1))) term_colors[color] = argb_to_hex(harmonized) -if args.debug == False: - print(f"$darkmode: {darkmode};") - print(f"$transparent: {transparent};") - for color, code in material_colors.items(): - print(f"${color}: {code};") - for color, code in term_colors.items(): - print(f"${color}: {code};") -else: - if args.path is not None: - print('\n--------------Image properties-----------------') - print(f"Image size: {wsize} x {hsize}") - print(f"Resized image: {wsize_new} x {hsize_new}") - print('\n---------------Selected color------------------') - print(f"Dark mode: {darkmode}") - print(f"Scheme: {args.scheme}") - print(f"Accent color: {display_color(rgba_from_argb(argb))} {argb_to_hex(argb)}") - print(f"HCT: {hct.hue:.2f} {hct.chroma:.2f} {hct.tone:.2f}") - print('\n---------------Material colors-----------------') - for color, code in material_colors.items(): - rgba = rgba_from_argb(hex_to_argb(code)) - print(f"{color.ljust(32)} : {display_color(rgba)} {code}") - print('\n----------Harmonize terminal colors------------') - for color, code in term_colors.items(): - rgba = rgba_from_argb(hex_to_argb(code)) - code_source = term_source_colors[color] - rgba_source = rgba_from_argb(hex_to_argb(code_source)) - print(f"{color.ljust(6)} : {display_color(rgba_source)} {code_source} --> {display_color(rgba)} {code}") - print('-----------------------------------------------') +print(f"$darkmode: {darkmode};") +print(f"$transparent: {transparent};") +for color, code in material_colors.items(): + print(f"${color}: {code};") +for color, code in term_colors.items(): + print(f"${color}: {code};") diff --git a/dots/.config/quickshell/ii/scripts/colors/random/random_konachan_wall.sh b/dots/.config/quickshell/ii/scripts/colors/random/random_konachan_wall.sh index 5e92a5bf3c..7cb2307aa7 100755 --- a/dots/.config/quickshell/ii/scripts/colors/random/random_konachan_wall.sh +++ b/dots/.config/quickshell/ii/scripts/colors/random/random_konachan_wall.sh @@ -17,6 +17,9 @@ get_pictures_dir() { echo "$HOME/Pictures" } +# Guard: if sourced, only define functions and return +[[ "${BASH_SOURCE[0]}" != "${0}" ]] && return + QUICKSHELL_CONFIG_NAME="ii" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" diff --git a/dots/.config/quickshell/ii/scripts/colors/random/random_osu_wall.sh b/dots/.config/quickshell/ii/scripts/colors/random/random_osu_wall.sh index 54b3af6a0d..7f64f99e9c 100755 --- a/dots/.config/quickshell/ii/scripts/colors/random/random_osu_wall.sh +++ b/dots/.config/quickshell/ii/scripts/colors/random/random_osu_wall.sh @@ -1,21 +1,7 @@ #!/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" -} +# shellcheck source=random_konachan_wall.sh +. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/random_konachan_wall.sh" QUICKSHELL_CONFIG_NAME="ii" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" diff --git a/dots/.config/quickshell/ii/scripts/colors/switchwall.sh b/dots/.config/quickshell/ii/scripts/colors/switchwall.sh index 7c52083af1..1e0bfbf6f6 100755 --- a/dots/.config/quickshell/ii/scripts/colors/switchwall.sh +++ b/dots/.config/quickshell/ii/scripts/colors/switchwall.sh @@ -1,37 +1,22 @@ #!/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/$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="$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 - scheme-content|scheme-expressive|scheme-fidelity|scheme-fruit-salad|scheme-monochrome|scheme-neutral|scheme-rainbow|scheme-tonal-spot) - kde_scheme_variant="$type_flag" - ;; - *) - kde_scheme_variant="scheme-tonal-spot" # default - ;; + [ -f "$SHELL_CONFIG_FILE" ] && [ "$(jq -r '.appearance.wallpaperTheming.enableQtApps' "$SHELL_CONFIG_FILE")" = "false" ] && return + local variant="${type_flag:-scheme-tonal-spot}" + case "$variant" in + scheme-content|scheme-expressive|scheme-fidelity|scheme-fruit-salad|scheme-monochrome|scheme-neutral|scheme-rainbow|scheme-tonal-spot) ;; + *) variant="scheme-tonal-spot" ;; esac - "$XDG_CONFIG_HOME"/matugen/templates/kde/kde-material-you-colors-wrapper.sh --scheme-variant "$kde_scheme_variant" + "$XDG_CONFIG_HOME"/matugen/templates/kde/kde-material-you-colors-wrapper.sh --scheme-variant "$variant" } pre_process() { @@ -295,12 +280,10 @@ switch() { # Set harmony and related properties if [ -f "$SHELL_CONFIG_FILE" ]; then - harmony=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.harmony' "$SHELL_CONFIG_FILE") - harmonize_threshold=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.harmonizeThreshold' "$SHELL_CONFIG_FILE") - term_fg_boost=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.termFgBoost' "$SHELL_CONFIG_FILE") - [[ "$harmony" != "null" && -n "$harmony" ]] && generate_colors_material_args+=(--harmony "$harmony") - [[ "$harmonize_threshold" != "null" && -n "$harmonize_threshold" ]] && generate_colors_material_args+=(--harmonize_threshold "$harmonize_threshold") - [[ "$term_fg_boost" != "null" && -n "$term_fg_boost" ]] && generate_colors_material_args+=(--term_fg_boost "$term_fg_boost") + for prop in harmony harmonize_threshold term_fg_boost; do + val=$(jq -r ".appearance.wallpaperTheming.terminalGenerationProps.$prop" "$SHELL_CONFIG_FILE" 2>/dev/null) + [ "$val" != "null" ] && generate_colors_material_args+=(--"$prop" "$val") + done fi matugen "${matugen_args[@]}" diff --git a/dots/.config/quickshell/ii/scripts/images/find-regions-venv.sh b/dots/.config/quickshell/ii/scripts/images/find-regions-venv.sh deleted file mode 100755 index d31fe4a271..0000000000 --- a/dots/.config/quickshell/ii/scripts/images/find-regions-venv.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -"$SCRIPT_DIR/find_regions.py" "$@" -deactivate diff --git a/dots/.config/quickshell/ii/scripts/images/least-busy-region-venv.sh b/dots/.config/quickshell/ii/scripts/images/least-busy-region-venv.sh deleted file mode 100755 index 3a5e90dcb2..0000000000 --- a/dots/.config/quickshell/ii/scripts/images/least-busy-region-venv.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -"$SCRIPT_DIR/least_busy_region.py" "$@" -deactivate diff --git a/dots/.config/quickshell/ii/scripts/images/least_busy_region.py b/dots/.config/quickshell/ii/scripts/images/least_busy_region.py index b9e1c89630..c21a8cd969 100755 --- a/dots/.config/quickshell/ii/scripts/images/least_busy_region.py +++ b/dots/.config/quickshell/ii/scripts/images/least_busy_region.py @@ -8,6 +8,7 @@ import argparse import json + def center_crop(img, target_w, target_h): h, w = img.shape[:2] if w == target_w and h == target_h: @@ -18,39 +19,85 @@ 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", horizontal_padding=50, vertical_padding=50, busiest=False): + +def load_and_scale_image(image_path, screen_width=None, screen_height=None, screen_mode="fill", verbose=False): + """Load an image and scale/crop it to screen dimensions. + + Returns (img, orig_h, orig_w) where img is the (possibly scaled+cropped) grayscale image. + If screen dimensions are None, returns the original grayscale image unchanged. + """ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f"Image not found: {image_path}") orig_h, orig_w = img.shape - scale = 1.0 if screen_width is not None and screen_height is not None: scale_w = screen_width / orig_w scale_h = screen_height / orig_h - if screen_mode == "fill": - scale = max(scale_w, scale_h) - else: - scale = min(scale_w, scale_h) + scale = max(scale_w, scale_h) if screen_mode == "fill" else min(scale_w, scale_h) new_w = int(orig_w * scale) new_h = int(orig_h * scale) if verbose: print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})") img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) img = center_crop(img, screen_width, screen_height) - if verbose: - print(f"Cropped image to {screen_width}x{screen_height}") - else: - if verbose: - print(f"Using original image size: {orig_w}x{orig_h}") - arr = img.astype(np.float64) - h, w = arr.shape - # Validate & adjust stride - stride = max(1, int(stride) if stride else 1) - # Adjust region size if it does not fit given padding + if verbose: + print(f"Image size: {img.shape[1]}x{img.shape[0]}") + return img, orig_h, orig_w + + +def load_and_scale_image_color(image_path, screen_width=None, screen_height=None, screen_mode="fill"): + """Same as load_and_scale_image but returns BGR color image and (orig_h, orig_w).""" + img = cv2.imread(image_path) + if img is None: + raise FileNotFoundError(f"Image not found: {image_path}") + orig_h, orig_w = img.shape[:2] + if screen_width is not None and screen_height is not None: + scale_w = screen_width / orig_w + scale_h = screen_height / orig_h + scale = max(scale_w, scale_h) if screen_mode == "fill" else min(scale_w, scale_h) + new_w = int(orig_w * scale) + new_h = int(orig_h * scale) + img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) + img = center_crop(img, screen_width, screen_height) + return img, orig_h, orig_w + + +def clamp_padding(w, h, horizontal_padding, vertical_padding): + """Reduce padding so at least a 1x1 region fits.""" if horizontal_padding * 2 >= w or vertical_padding * 2 >= h: - # Reduce padding to fit at least a 1x1 region horizontal_padding = max(0, min(horizontal_padding, (w - 1) // 2)) vertical_padding = max(0, min(vertical_padding, (h - 1) // 2)) + return horizontal_padding, vertical_padding + + +def setup_integral(arr): + """Return (integral, integral_sq) for fast sliding-window variance computation.""" + integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:, 1:] + integral_sq = cv2.integral(arr ** 2, sdepth=cv2.CV_64F)[1:, 1:] + return integral, integral_sq + + +def region_sum(ii, x1, y1, x2, y2): + """Query sum over rectangle in integral image. Caller must ensure bounds.""" + total = ii[y2, x2] + if x1 > 0: + total -= ii[y2, x1 - 1] + if y1 > 0: + total -= ii[y1 - 1, x2] + if x1 > 0 and y1 > 0: + total += ii[y1 - 1, x1 - 1] + return total + + +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, busiest=False): + img, orig_h, orig_w = load_and_scale_image(image_path, screen_width, screen_height, screen_mode, verbose) + arr = img.astype(np.float64) + h, w = arr.shape + stride = max(1, int(stride) if stride else 1) + horizontal_padding, vertical_padding = clamp_padding(w, h, horizontal_padding, vertical_padding) + max_region_w = w - 2 * horizontal_padding max_region_h = h - 2 * vertical_padding if max_region_w <= 0 or max_region_h <= 0: @@ -63,24 +110,14 @@ def find_least_busy_region(image_path, region_width=300, region_height=200, scre if verbose: print(f"Requested region_height {region_height} too large; clamping to {max_region_h}") region_height = max_region_h - # Use OpenCV's integral for fast computation - integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:] - integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:] - def region_sum(ii, x1, y1, x2, y2): - # Assume bounds have been checked before calling - total = ii[y2, x2] - if x1 > 0: - total -= ii[y2, x1-1] - if y1 > 0: - total -= ii[y1-1, x2] - if x1 > 0 and y1 > 0: - total += ii[y1-1, x1-1] - return total + + integral, integral_sq = setup_integral(arr) + area = region_width * region_height min_var = None max_var = None min_coords = (horizontal_padding, vertical_padding) max_coords = (horizontal_padding, vertical_padding) - area = region_width * region_height + x_start = horizontal_padding y_start = vertical_padding x_end = w - region_width - horizontal_padding + 1 @@ -89,79 +126,48 @@ def region_sum(ii, x1, y1, x2, y2): x_end = x_start if y_end < y_start: y_end = y_start + for y in range(y_start, y_end + 1, stride): for x in range(x_start, x_end + 1, stride): x1, y1 = x, y x2, y2 = x + region_width - 1, y + region_height - 1 if x2 >= w or y2 >= h: - continue # Skip out-of-bounds window + continue s = region_sum(integral, x1, y1, x2, y2) s2 = region_sum(integral_sq, x1, y1, x2, y2) mean = s / area var = (s2 / area) - (mean ** 2) - if (min_var is None) or (var < min_var): + if min_var is None or var < min_var: min_var = var min_coords = (x, y) - if (max_var is None) or (var > max_var): + if max_var is None or var > max_var: max_var = var max_coords = (x, y) + if busiest: return max_coords, max_var else: 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, 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}") - orig_h, orig_w = img.shape - # ...existing scaling logic... - scale = 1.0 - if screen_width is not None and screen_height is not None: - scale_w = screen_width / orig_w - scale_h = screen_height / orig_h - if screen_mode == "fill": - scale = max(scale_w, scale_h) - else: - scale = min(scale_w, scale_h) - new_w = int(orig_w * scale) - new_h = int(orig_h * scale) - if verbose: - print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})") - img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) - img = center_crop(img, screen_width, screen_height) - if verbose: - print(f"Cropped image to {screen_width}x{screen_height}") - else: - if verbose: - print(f"Using original image size: {orig_w}x{orig_h}") + +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, orig_h, orig_w = load_and_scale_image(image_path, screen_width, screen_height, screen_mode, verbose) arr = img.astype(np.float64) h, w = arr.shape stride = max(1, int(stride) if stride else 1) threshold = max(0.0, float(threshold)) - # Adjust padding if image too small - if horizontal_padding * 2 >= w or vertical_padding * 2 >= h: - horizontal_padding = max(0, min(horizontal_padding, (w - 1) // 2)) - vertical_padding = max(0, min(vertical_padding, (h - 1) // 2)) - # Use OpenCV's integral for fast computation - integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:] - integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:] - def region_sum(ii, x1, y1, x2, y2): - total = ii[y2, x2] - if x1 > 0: - total -= ii[y2, x1-1] - if y1 > 0: - total -= ii[y1-1, x2] - if x1 > 0 and y1 > 0: - total += ii[y1-1, x1-1] - return total - min_size = 10 - # Determine maximum feasible size respecting padding + horizontal_padding, vertical_padding = clamp_padding(w, h, horizontal_padding, vertical_padding) + + integral, integral_sq = setup_integral(arr) + effective_w = w - 2 * horizontal_padding effective_h = h - 2 * vertical_padding if effective_w <= 0 or effective_h <= 0: return None, (0, 0), None - # Largest square-ish dimension given aspect ratio and effective space + + min_size = 10 if aspect_ratio >= 1.0: max_size = min(effective_h, int(effective_w / aspect_ratio)) else: @@ -169,6 +175,7 @@ def region_sum(ii, x1, y1, x2, y2): if max_size < min_size: min_size = 1 max_size = max(1, max_size) + best = None while min_size <= max_size: mid = (min_size + max_size) // 2 @@ -209,6 +216,7 @@ def region_sum(ii, x1, y1, x2, y2): min_size = mid + 1 else: max_size = mid - 1 + if best: x, y, region_w, region_h, var = best center_x = x + region_w // 2 @@ -217,96 +225,58 @@ def region_sum(ii, x1, y1, x2, y2): else: return None, (0, 0), None -def draw_region(image_path, coords, region_width=300, region_height=200, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"): - img = cv2.imread(image_path) - if img is None: - raise FileNotFoundError(f"Image not found: {image_path}") - orig_h, orig_w = img.shape[:2] - if screen_width is not None and screen_height is not None: - scale_w = screen_width / orig_w - scale_h = screen_height / orig_h - if screen_mode == "fill": - scale = max(scale_w, scale_h) - else: - scale = min(scale_w, scale_h) - new_w = int(orig_w * scale) - new_h = int(orig_h * scale) - img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) - img = center_crop(img, screen_width, screen_height) + +def draw_region(image_path, coords, region_width=300, region_height=200, output_path='output.png', + screen_width=None, screen_height=None, screen_mode="fill"): + img, orig_h, orig_w = load_and_scale_image_color(image_path, screen_width, screen_height, screen_mode) x, y = coords - cv2.rectangle(img, (x, y), (x+region_width-1, y+region_height-1), (0,0,255), 3) + cv2.rectangle(img, (x, y), (x + region_width - 1, y + region_height - 1), (0, 0, 255), 3) cv2.imwrite(output_path, img) - # print removed for quieter operation -def draw_largest_region(image_path, center, size, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"): - img = cv2.imread(image_path) - if img is None: - raise FileNotFoundError(f"Image not found: {image_path}") - orig_h, orig_w = img.shape[:2] - if screen_width is not None and screen_height is not None: - scale_w = screen_width / orig_w - scale_h = screen_height / orig_h - if screen_mode == "fill": - scale = max(scale_w, scale_h) - else: - scale = min(scale_w, scale_h) - new_w = int(orig_w * scale) - new_h = int(orig_h * scale) - img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) - img = center_crop(img, screen_width, screen_height) + +def draw_largest_region(image_path, center, size, output_path='output.png', + screen_width=None, screen_height=None, screen_mode="fill"): + img, orig_h, orig_w = load_and_scale_image_color(image_path, screen_width, screen_height, screen_mode) cx, cy = center region_w, region_h = size x1 = cx - region_w // 2 y1 = cy - region_h // 2 x2 = cx + region_w // 2 - 1 y2 = cy + region_h // 2 - 1 - cv2.rectangle(img, (x1, y1), (x2, y2), (255,0,0), 3) + cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 3) cv2.imwrite(output_path, img) - # print removed for quieter operation + def get_dominant_color(image_path, x, y, w, h, screen_width=None, screen_height=None, screen_mode="fill"): - img = cv2.imread(image_path) - if img is None: - raise FileNotFoundError(f"Image not found: {image_path}") - orig_h, orig_w = img.shape[:2] - if screen_width is not None and screen_height is not None: - scale_w = screen_width / orig_w - scale_h = screen_height / orig_h - if screen_mode == "fill": - scale = max(scale_w, scale_h) - else: - scale = min(scale_w, scale_h) - new_w = int(orig_w * scale) - new_h = int(orig_h * scale) - img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4) - img = center_crop(img, screen_width, screen_height) - # Ensure region is within bounds + img, orig_h, orig_w = load_and_scale_image_color(image_path, screen_width, screen_height, screen_mode) x = max(0, x) y = max(0, y) w = max(1, min(w, img.shape[1] - x)) h = max(1, min(h, img.shape[0] - y)) - region = img[y:y+h, x:x+w] + region = img[y:y + h, x:x + w] if region.size == 0 or region.shape[0] == 0 or region.shape[1] == 0: return [0, 0, 0] + region = region.reshape((-1, 3)) - # Filter out black pixels (optional, improves accuracy for some images) non_black = region[np.any(region > 10, axis=1)] if non_black.shape[0] == 0: non_black = region region = np.float32(non_black) if region.shape[0] < 3: return [int(x) for x in np.mean(region, axis=0)] - # K-means to find dominant color + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) K = min(3, region.shape[0]) _, labels, centers = cv2.kmeans(region, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) counts = np.bincount(labels.flatten()) dominant = centers[np.argmax(counts)] - # Reverse from BGR to RGB return [int(x) for x in reversed(dominant)] + def main(): - parser = argparse.ArgumentParser(description="Find least busy region in an image and output a JSON. Made for determining a suitable position for a wallpaper widget.") + parser = argparse.ArgumentParser( + description="Find least busy region in an image and output a JSON. " + "Made for determining a suitable position for a wallpaper widget.") parser.add_argument("image_path", help="Path to the input image") parser.add_argument("--width", type=int, default=300, help="Region width") parser.add_argument("--height", type=int, default=200, help="Region height") @@ -314,14 +284,21 @@ def main(): 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=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("--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("--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") - parser.add_argument("--busiest", action="store_true", help="Find the busiest region instead of the least busy") + 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("--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") + parser.add_argument("--busiest", action="store_true", + help="Find the busiest region instead of the least busy") args = parser.parse_args() if args.largest_region: @@ -339,8 +316,9 @@ def main(): ) if center: if args.visual_output: - draw_largest_region(args.image_path, center, size, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode) - # Extract dominant color + draw_largest_region(args.image_path, center, size, + screen_width=args.screen_width, screen_height=args.screen_height, + screen_mode=args.screen_mode) cx, cy = center region_w, region_h = size x1 = cx - region_w // 2 @@ -376,8 +354,8 @@ def main(): busiest=args.busiest ) 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) - # Output JSON with center point + 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) center_x = coords[0] + args.width // 2 center_y = coords[1] + args.height // 2 dominant_color = get_dominant_color( @@ -394,6 +372,6 @@ def main(): "dominant_color": dominant_color_hex })) + if __name__ == "__main__": main() - diff --git a/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh b/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh deleted file mode 100755 index f673bd6873..0000000000 --- a/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -"$SCRIPT_DIR/text_color.py" "$@" -deactivate diff --git a/dots/.config/quickshell/ii/scripts/run-in-venv.sh b/dots/.config/quickshell/ii/scripts/run-in-venv.sh new file mode 100755 index 0000000000..0521830352 --- /dev/null +++ b/dots/.config/quickshell/ii/scripts/run-in-venv.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Run a script inside the illogical-impulse virtual environment +# Usage: run-in-venv.sh [args...] +# script_relpath: path relative to the scripts/ directory (e.g. images/find_regions.py) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate +export GIO_USE_VFS=local +"$SCRIPT_DIR/$@" +EXIT_CODE=$? +deactivate +exit $EXIT_CODE diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh b/dots/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh index 3c46460143..824e1c3054 100755 --- a/dots/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh +++ b/dots/.config/quickshell/ii/scripts/thumbnails/generate-thumbnails-magick.sh @@ -29,18 +29,7 @@ md5() { } urlencode() { - # Percent-encode a string for use in a URI, but do not encode slashes - local str="$1" - local encoded="" - local c - for ((i=0; i<${#str}; i++)); do - c="${str:$i:1}" - case "$c" in - [a-zA-Z0-9.~_-]|/|'('|')'|'*') encoded+="$c" ;; - *) printf -v hex '%%%02X' "'${c}'"; encoded+="$hex" ;; - esac - done - echo "$encoded" + python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe='/:()*'))" "$1" } generate_thumbnail() { diff --git a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh b/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh deleted file mode 100755 index 9b0d923857..0000000000 --- a/dots/.config/quickshell/ii/scripts/thumbnails/thumbgen-venv.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate -GIO_USE_VFS=local "$SCRIPT_DIR/thumbgen.py" "$@" -THUMBGEN_EXIT_CODE=$? -deactivate - -exit $THUMBGEN_EXIT_CODE diff --git a/dots/.config/quickshell/ii/services/Wallpapers.qml b/dots/.config/quickshell/ii/services/Wallpapers.qml index a871187bf0..fb280ff6f5 100644 --- a/dots/.config/quickshell/ii/services/Wallpapers.qml +++ b/dots/.config/quickshell/ii/services/Wallpapers.qml @@ -15,7 +15,7 @@ pragma ComponentBehavior: Bound Singleton { id: root - property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/thumbgen-venv.sh` + property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/run-in-venv.sh thumbnails/thumbgen.py` property string generateThumbnailsMagickScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/generate-thumbnails-magick.sh` property alias directory: folderModel.folder readonly property string effectiveDirectory: FileUtils.trimFileProtocol(folderModel.folder.toString())