Merge branch 'master' into master
This commit is contained in:
@@ -321,6 +321,7 @@ in
|
|||||||
bar.notifications.scrollUp = mkStrOption "";
|
bar.notifications.scrollUp = mkStrOption "";
|
||||||
bar.notifications.show_total = mkBoolOption false;
|
bar.notifications.show_total = mkBoolOption false;
|
||||||
bar.scrollSpeed = mkIntOption 5;
|
bar.scrollSpeed = mkIntOption 5;
|
||||||
|
bar.systray.ignore = mkStrListOption [];
|
||||||
bar.volume.label = mkBoolOption true;
|
bar.volume.label = mkBoolOption true;
|
||||||
bar.volume.middleClick = mkStrOption "";
|
bar.volume.middleClick = mkStrOption "";
|
||||||
bar.volume.rightClick = mkStrOption "";
|
bar.volume.rightClick = mkStrOption "";
|
||||||
@@ -406,6 +407,7 @@ in
|
|||||||
menus.dashboard.powermenu.reboot = mkStrOption "systemctl reboot";
|
menus.dashboard.powermenu.reboot = mkStrOption "systemctl reboot";
|
||||||
menus.dashboard.powermenu.shutdown = mkStrOption "systemctl poweroff";
|
menus.dashboard.powermenu.shutdown = mkStrOption "systemctl poweroff";
|
||||||
menus.dashboard.powermenu.sleep = mkStrOption "systemctl suspend";
|
menus.dashboard.powermenu.sleep = mkStrOption "systemctl suspend";
|
||||||
|
menus.dashboard.recording.path = mkStrOption "$HOME/Videos/Screencasts";
|
||||||
menus.dashboard.shortcuts.enabled = mkBoolOption true;
|
menus.dashboard.shortcuts.enabled = mkBoolOption true;
|
||||||
menus.dashboard.shortcuts.left.shortcut1.command = mkStrOption "microsoft-edge-stable";
|
menus.dashboard.shortcuts.left.shortcut1.command = mkStrOption "microsoft-edge-stable";
|
||||||
menus.dashboard.shortcuts.left.shortcut1.icon = mkStrOption "";
|
menus.dashboard.shortcuts.left.shortcut1.icon = mkStrOption "";
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -24,8 +24,13 @@
|
|||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../../../../usr/share/astal/gjs": {
|
||||||
|
"name": "astal",
|
||||||
|
"license": "LGPL-2.1"
|
||||||
|
},
|
||||||
"../../../../usr/share/astal/gjs": {
|
"../../../../usr/share/astal/gjs": {
|
||||||
"name": "astal",
|
"name": "astal",
|
||||||
|
"extraneous": true,
|
||||||
"license": "LGPL-2.1"
|
"license": "LGPL-2.1"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
@@ -601,7 +606,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/astal": {
|
"node_modules/astal": {
|
||||||
"resolved": "../../../../usr/share/astal/gjs",
|
"resolved": "../../../../../usr/share/astal/gjs",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
|
|||||||
@@ -1,49 +1,94 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
check_arch_updates() {
|
has_param() {
|
||||||
official_updates=0
|
local term="$1"
|
||||||
aur_updates=0
|
shift
|
||||||
if command -v paru &> /dev/null; then
|
for arg; do
|
||||||
aur_helper="paru"
|
if [[ $arg == "$term" ]]; then
|
||||||
else
|
return 0
|
||||||
aur_helper="yay"
|
fi
|
||||||
fi
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$1" = "-y" ]; then
|
wait_for_process_to_finish() {
|
||||||
aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l)
|
local process_name="$1"
|
||||||
elif [ "$1" = "-p" ]; then
|
while pgrep -a "$process_name" >/dev/null; do
|
||||||
official_updates=$(checkupdates 2>/dev/null | wc -l)
|
sleep 0.1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_arch_updates() {
|
||||||
|
if command -v paru &> /dev/null; then
|
||||||
|
aur_helper="paru"
|
||||||
else
|
else
|
||||||
official_updates=$(checkupdates 2>/dev/null | wc -l)
|
aur_helper="yay"
|
||||||
aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l)
|
fi
|
||||||
|
|
||||||
|
if has_param "-tooltip" "$@"; then
|
||||||
|
command=" | head -n 50"
|
||||||
|
official_updates=""
|
||||||
|
aur_updates=""
|
||||||
|
wait_for_process_to_finish "checkupdates"
|
||||||
|
else
|
||||||
|
command="2>/dev/null | wc -l"
|
||||||
|
official_updates=0
|
||||||
|
aur_updates=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
aur_command="$aur_helper -Qum $command"
|
||||||
|
official_command="checkupdates $command"
|
||||||
|
|
||||||
|
if has_param "-y" "$@"; then
|
||||||
|
aur_updates=$(eval "$aur_command")
|
||||||
|
elif has_param "-p" "$@"; then
|
||||||
|
official_updates=$(eval "$official_command")
|
||||||
|
else
|
||||||
|
aur_updates=$(eval "$aur_command")
|
||||||
|
official_updates=$(eval "$official_command")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_param "-tooltip" "$@"; then
|
||||||
|
if [ "$official_updates" ];then
|
||||||
|
echo "pacman:"
|
||||||
|
echo "$official_updates"
|
||||||
|
fi
|
||||||
|
if [ "$official_updates" ] && [ "$aur_updates" ];then
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
if [ "$aur_updates" ];then
|
||||||
|
echo "AUR:"
|
||||||
|
echo "$aur_updates"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
total_updates=$((official_updates + aur_updates))
|
||||||
|
echo $total_updates
|
||||||
fi
|
fi
|
||||||
|
|
||||||
total_updates=$((official_updates + aur_updates))
|
|
||||||
|
|
||||||
echo $total_updates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_ubuntu_updates() {
|
check_ubuntu_updates() {
|
||||||
result=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst)
|
result=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst)
|
||||||
echo "$result"
|
echo "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_fedora_updates() {
|
check_fedora_updates() {
|
||||||
result=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l)
|
result=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l)
|
||||||
echo "$result"
|
echo "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-arch)
|
-arch)
|
||||||
check_arch_updates "$2"
|
check_arch_updates "$2" "$3"
|
||||||
;;
|
;;
|
||||||
-ubuntu)
|
-ubuntu)
|
||||||
check_ubuntu_updates
|
check_ubuntu_updates
|
||||||
;;
|
;;
|
||||||
-fedora)
|
-fedora)
|
||||||
check_fedora_updates
|
check_fedora_updates
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "0"
|
echo "0"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Requires wf-recorder: https://github.com/ammen99/wf-recorder
|
# Requires wf-recorder: https://github.com/ammen99/wf-recorder
|
||||||
|
|
||||||
outputDir="$HOME/Videos/Screencasts"
|
# Get the default audio sink
|
||||||
defaultSink=$(pactl get-default-sink)
|
defaultSink=$(pactl get-default-sink)
|
||||||
WF_RECORDER_OPTS="--audio=$defaultSink.monitor -c libx264rgb"
|
WF_RECORDER_OPTS="--audio=$defaultSink.monitor -c libx264rgb"
|
||||||
|
outputFile=""
|
||||||
|
outputDir=""
|
||||||
|
|
||||||
|
# Function to check if recording is active
|
||||||
checkRecording() {
|
checkRecording() {
|
||||||
if pgrep -f "wf-recorder" >/dev/null; then
|
pgrep -f "wf-recorder" >/dev/null
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to start screen recording
|
||||||
startRecording() {
|
startRecording() {
|
||||||
if checkRecording; then
|
if checkRecording; then
|
||||||
echo "A recording is already in progress."
|
echo "A recording is already in progress."
|
||||||
@@ -21,50 +21,115 @@ startRecording() {
|
|||||||
|
|
||||||
target="$2"
|
target="$2"
|
||||||
|
|
||||||
outputFile="recording_$(date +%Y-%m-%d_%H-%M-%S)"
|
|
||||||
outputPath="$outputDir/${outputFile}.mp4"
|
|
||||||
mkdir -p "$outputDir"
|
|
||||||
|
|
||||||
if [ "$target" == "screen" ]; then
|
if [ "$target" == "screen" ]; then
|
||||||
monitor_info=$(hyprctl -j monitors | jq -r ".[] | select(.name == \"$3\")")
|
monitor_name="$3"
|
||||||
w=$(echo $monitor_info | jq -r '.width')
|
outputDir="$4"
|
||||||
h=$(echo $monitor_info | jq -r '.height')
|
|
||||||
scale=$(echo $monitor_info | jq -r '.scale')
|
|
||||||
scaled_width=$(awk "BEGIN {print $w / $scale}")
|
|
||||||
scaled_height=$(awk "BEGIN {print $h / $scale}")
|
|
||||||
x=$(echo $monitor_info | jq -r '.x')
|
|
||||||
y=$(echo $monitor_info | jq -r '.y')
|
|
||||||
wf-recorder $WF_RECORDER_OPTS --geometry "${x},${y} ${scaled_width}x${scaled_height}" --file "$outputPath" &
|
|
||||||
elif [ "$target" == "region" ]; then
|
elif [ "$target" == "region" ]; then
|
||||||
wf-recorder $WF_RECORDER_OPTS --geometry "$(slurp)" --file "$outputPath" &
|
outputDir="$3"
|
||||||
else
|
else
|
||||||
echo "Usage: $0 start {region|screen [screen_name]}"
|
echo "Usage: $0 start {screen <monitor_name> | region} <output_directory>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
disown "$(jobs -p | tail -n 1)"
|
|
||||||
|
|
||||||
echo "Recording started. Output will be saved to $outputPath"
|
# Set a default output directory if not provided
|
||||||
|
outputDir="${outputDir:-$HOME/Videos}"
|
||||||
|
|
||||||
|
# Expand ~ to $HOME if present in outputDir
|
||||||
|
outputDir="${outputDir/#\~/$HOME}"
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
if [ ! -d "$outputDir" ]; then
|
||||||
|
echo "Error: Output directory '$outputDir' does not exist."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate output filename and path
|
||||||
|
outputFile="recording_$(date +%Y-%m-%d_%H-%M-%S).mp4"
|
||||||
|
outputPath="$outputDir/$outputFile"
|
||||||
|
|
||||||
|
echo "Target: $target"
|
||||||
|
echo "Monitor: ${monitor_name:-N/A}"
|
||||||
|
echo "Output dir: $outputDir"
|
||||||
|
echo "Output file: $outputPath"
|
||||||
|
|
||||||
|
# Start screen recording
|
||||||
|
if [ "$target" == "screen" ]; then
|
||||||
|
if [ -z "$monitor_name" ]; then
|
||||||
|
echo "Error: Monitor name is required for screen recording."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
monitor_info=$(hyprctl -j monitors | jq -r ".[] | select(.name == \"$monitor_name\")")
|
||||||
|
if [ -z "$monitor_info" ]; then
|
||||||
|
echo "Error: Monitor '$monitor_name' not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
w=$(echo "$monitor_info" | jq -r '.width')
|
||||||
|
h=$(echo "$monitor_info" | jq -r '.height')
|
||||||
|
scale=$(echo "$monitor_info" | jq -r '.scale')
|
||||||
|
x=$(echo "$monitor_info" | jq -r '.x')
|
||||||
|
y=$(echo "$monitor_info" | jq -r '.y')
|
||||||
|
|
||||||
|
transform=$(echo "$monitor_info" | jq -r '.transform')
|
||||||
|
rotation_filter=""
|
||||||
|
|
||||||
|
if [ "$transform" -eq 1 ] || [ "$transform" -eq 3 ]; then
|
||||||
|
scaled_width=$(awk "BEGIN { print $h / $scale }")
|
||||||
|
scaled_height=$(awk "BEGIN { print $w / $scale }")
|
||||||
|
else
|
||||||
|
scaled_width=$(awk "BEGIN { print $w / $scale }")
|
||||||
|
scaled_height=$(awk "BEGIN { print $h / $scale }")
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$transform" in
|
||||||
|
1)
|
||||||
|
rotation_filter="-F transpose=1"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
rotation_filter="-F transpose=2"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
wf-recorder $WF_RECORDER_OPTS $rotation_filter --geometry "${x},${y} ${scaled_width}x${scaled_height}" --file "$outputPath" &
|
||||||
|
elif [ "$target" == "region" ]; then
|
||||||
|
wf-recorder $WF_RECORDER_OPTS --geometry "$(slurp)" --file "$outputPath" &
|
||||||
|
fi
|
||||||
|
|
||||||
|
disown "$(jobs -p | tail -n 1)"
|
||||||
|
echo "Recording started. Saving to $outputPath"
|
||||||
|
echo "$outputPath" >/tmp/last_recording_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to stop screen recording
|
||||||
stopRecording() {
|
stopRecording() {
|
||||||
if ! checkRecording; then
|
if ! checkRecording; then
|
||||||
echo "No recording is in progress."
|
echo "No recording in progress."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pkill -SIGINT -f wf-recorder
|
pkill -SIGINT -f wf-recorder
|
||||||
|
sleep 1 # Allow wf-recorder time to terminate before proceeding
|
||||||
|
|
||||||
recentFile=$(ls -t "$outputDir"/recording_*.mp4 | head -n 1)
|
outputPath=$(cat /tmp/last_recording_path 2>/dev/null)
|
||||||
|
|
||||||
notify-send "Recording stopped" "Your recording has been saved." \
|
if [ -z "$outputPath" ] || [ ! -f "$outputPath" ]; then
|
||||||
|
notify-send "Recording stopped" "No recent recording found." \
|
||||||
|
-i video-x-generic \
|
||||||
|
-a "Screen Recorder" \
|
||||||
|
-t 10000
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify-send "Recording stopped" "Saved to: $outputPath" \
|
||||||
-i video-x-generic \
|
-i video-x-generic \
|
||||||
-a "Screen Recorder" \
|
-a "Screen Recorder" \
|
||||||
-t 10000 \
|
-t 10000 \
|
||||||
-u normal \
|
--action="scriptAction:-xdg-open $(dirname "$outputPath")=Open Directory" \
|
||||||
--action="scriptAction:-xdg-open $outputDir=Directory" \
|
--action="scriptAction:-xdg-open $outputPath=Play"
|
||||||
--action="scriptAction:-xdg-open $recentFile=Play"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Handle script arguments
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
startRecording "$@"
|
startRecording "$@"
|
||||||
@@ -80,7 +145,7 @@ status)
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {start [screen screen_name|region]|stop|status}"
|
echo "Usage: $0 {start [screen <monitor_name> | region] <output_directory> | stop | status}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import { App, Gtk } from 'astal/gtk3';
|
|||||||
|
|
||||||
import Astal from 'gi://Astal?version=3.0';
|
import Astal from 'gi://Astal?version=3.0';
|
||||||
import { bind, Variable } from 'astal';
|
import { bind, Variable } from 'astal';
|
||||||
import { gdkMonitorIdToHyprlandId, getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||||
|
import { GdkMonitorMapper } from './utils/GdkMonitorMapper';
|
||||||
|
|
||||||
const { layouts } = options.bar;
|
const { layouts } = options.bar;
|
||||||
const { location } = options.theme.bar;
|
const { location } = options.theme.bar;
|
||||||
@@ -68,107 +69,110 @@ const widget = {
|
|||||||
cava: (): JSX.Element => WidgetContainer(Cava()),
|
cava: (): JSX.Element => WidgetContainer(Cava()),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Bar = (() => {
|
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||||
const usedHyprlandMonitors = new Set<number>();
|
|
||||||
|
|
||||||
return (monitor: number): JSX.Element => {
|
export const Bar = (monitor: number): JSX.Element => {
|
||||||
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
|
const hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor);
|
||||||
|
|
||||||
const computeVisibility = bind(layouts).as(() => {
|
const computeVisibility = bind(layouts).as(() => {
|
||||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||||
return !isLayoutEmpty(foundLayout);
|
return !isLayoutEmpty(foundLayout);
|
||||||
});
|
});
|
||||||
|
|
||||||
const computeAnchor = bind(location).as((loc) => {
|
const computeClassName = bind(layouts).as(() => {
|
||||||
if (loc === 'bottom') {
|
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||||
return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
return !isLayoutEmpty(foundLayout) ? `bar` : '';
|
||||||
}
|
});
|
||||||
|
|
||||||
return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
const computeAnchor = bind(location).as((loc) => {
|
||||||
});
|
if (loc === 'bottom') {
|
||||||
|
return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => {
|
return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||||
if (tear && barLayer === 'overlay') {
|
});
|
||||||
return Astal.Layer.TOP;
|
|
||||||
}
|
|
||||||
const layerMap = {
|
|
||||||
overlay: Astal.Layer.OVERLAY,
|
|
||||||
top: Astal.Layer.TOP,
|
|
||||||
bottom: Astal.Layer.BOTTOM,
|
|
||||||
background: Astal.Layer.BACKGROUND,
|
|
||||||
};
|
|
||||||
|
|
||||||
return layerMap[barLayer];
|
const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => {
|
||||||
});
|
if (tear && barLayer === 'overlay') {
|
||||||
|
return Astal.Layer.TOP;
|
||||||
|
}
|
||||||
|
const layerMap = {
|
||||||
|
overlay: Astal.Layer.OVERLAY,
|
||||||
|
top: Astal.Layer.TOP,
|
||||||
|
bottom: Astal.Layer.BOTTOM,
|
||||||
|
background: Astal.Layer.BACKGROUND,
|
||||||
|
};
|
||||||
|
|
||||||
const computeBorderLocation = bind(borderLocation).as((brdrLcn) =>
|
return layerMap[barLayer];
|
||||||
brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel',
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
const computeBorderLocation = bind(borderLocation).as((brdrLcn) =>
|
||||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel',
|
||||||
|
);
|
||||||
|
|
||||||
return foundLayout.left
|
const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||||
.filter((mod) => Object.keys(widget).includes(mod))
|
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||||
.map((w) => widget[w](hyprlandMonitor));
|
|
||||||
});
|
|
||||||
const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
|
||||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
|
||||||
|
|
||||||
return foundLayout.middle
|
return foundLayout.left
|
||||||
.filter((mod) => Object.keys(widget).includes(mod))
|
.filter((mod) => Object.keys(widget).includes(mod))
|
||||||
.map((w) => widget[w](hyprlandMonitor));
|
.map((w) => widget[w](hyprlandMonitor));
|
||||||
});
|
});
|
||||||
const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||||
|
|
||||||
return foundLayout.right
|
return foundLayout.middle
|
||||||
.filter((mod) => Object.keys(widget).includes(mod))
|
.filter((mod) => Object.keys(widget).includes(mod))
|
||||||
.map((w) => widget[w](hyprlandMonitor));
|
.map((w) => widget[w](hyprlandMonitor));
|
||||||
});
|
});
|
||||||
|
const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||||
|
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||||
|
|
||||||
return (
|
return foundLayout.right
|
||||||
<window
|
.filter((mod) => Object.keys(widget).includes(mod))
|
||||||
inhibit={bind(idleInhibit)}
|
.map((w) => widget[w](hyprlandMonitor));
|
||||||
name={`bar-${hyprlandMonitor}`}
|
});
|
||||||
namespace={`bar-${hyprlandMonitor}`}
|
|
||||||
className={'bar'}
|
return (
|
||||||
application={App}
|
<window
|
||||||
monitor={monitor}
|
inhibit={bind(idleInhibit)}
|
||||||
visible={computeVisibility}
|
name={`bar-${hyprlandMonitor}`}
|
||||||
anchor={computeAnchor}
|
namespace={`bar-${hyprlandMonitor}`}
|
||||||
layer={computeLayer()}
|
className={computeClassName}
|
||||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
application={App}
|
||||||
onDestroy={() => {
|
monitor={monitor}
|
||||||
computeLayer.drop();
|
visible={computeVisibility}
|
||||||
leftBinding.drop();
|
anchor={computeAnchor}
|
||||||
middleBinding.drop();
|
layer={computeLayer()}
|
||||||
rightBinding.drop();
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
}}
|
onDestroy={() => {
|
||||||
>
|
computeLayer.drop();
|
||||||
<box className={'bar-panel-container'}>
|
leftBinding.drop();
|
||||||
<centerbox
|
middleBinding.drop();
|
||||||
css={'padding: 1px;'}
|
rightBinding.drop();
|
||||||
hexpand
|
}}
|
||||||
className={computeBorderLocation}
|
>
|
||||||
startWidget={
|
<box className={'bar-panel-container'}>
|
||||||
<box className={'box-left'} hexpand>
|
<centerbox
|
||||||
{leftBinding()}
|
css={'padding: 1px;'}
|
||||||
</box>
|
hexpand
|
||||||
}
|
className={computeBorderLocation}
|
||||||
centerWidget={
|
startWidget={
|
||||||
<box className={'box-center'} halign={Gtk.Align.CENTER}>
|
<box className={'box-left'} hexpand>
|
||||||
{middleBinding()}
|
{leftBinding()}
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
endWidget={
|
centerWidget={
|
||||||
<box className={'box-right'} halign={Gtk.Align.END}>
|
<box className={'box-center'} halign={Gtk.Align.CENTER}>
|
||||||
{rightBinding()}
|
{middleBinding()}
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
/>
|
endWidget={
|
||||||
</box>
|
<box className={'box-right'} halign={Gtk.Align.END}>
|
||||||
</window>
|
{rightBinding()}
|
||||||
);
|
</box>
|
||||||
};
|
}
|
||||||
})();
|
/>
|
||||||
|
</box>
|
||||||
|
</window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Astal } from 'astal/gtk3';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
updateCommand,
|
updateCommand,
|
||||||
|
updateTooltipCommand,
|
||||||
|
extendedTooltip,
|
||||||
label,
|
label,
|
||||||
padZero,
|
padZero,
|
||||||
autoHide,
|
autoHide,
|
||||||
@@ -21,6 +23,7 @@ const {
|
|||||||
} = options.bar.customModules.updates;
|
} = options.bar.customModules.updates;
|
||||||
|
|
||||||
const pendingUpdates: Variable<string> = Variable('0');
|
const pendingUpdates: Variable<string> = Variable('0');
|
||||||
|
const pendingUpdatesTooltip: Variable<string> = Variable('');
|
||||||
const postInputUpdater = Variable(true);
|
const postInputUpdater = Variable(true);
|
||||||
const isVis = Variable(!autoHide.get());
|
const isVis = Variable(!autoHide.get());
|
||||||
|
|
||||||
@@ -29,15 +32,31 @@ const processUpdateCount = (updateCount: string): string => {
|
|||||||
return `${updateCount.padStart(2, '0')}`;
|
return `${updateCount.padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processUpdateTooltip = (updateTooltip: string, updateCount: Variable<string>): string => {
|
||||||
|
const defaultTooltip = updateCount.get() + ' updates available';
|
||||||
|
if (!extendedTooltip.get()) return defaultTooltip;
|
||||||
|
return defaultTooltip + '\n\n' + updateTooltip;
|
||||||
|
};
|
||||||
|
|
||||||
const updatesPoller = new BashPoller<string, []>(
|
const updatesPoller = new BashPoller<string, []>(
|
||||||
pendingUpdates,
|
pendingUpdates,
|
||||||
[bind(padZero), bind(postInputUpdater)],
|
[bind(padZero), bind(postInputUpdater), bind(updateCommand)],
|
||||||
bind(pollingInterval),
|
bind(pollingInterval),
|
||||||
updateCommand.get(),
|
updateCommand.get(),
|
||||||
processUpdateCount,
|
processUpdateCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tooltipPoller = new BashPoller<string, [Variable<string>]>(
|
||||||
|
pendingUpdatesTooltip,
|
||||||
|
[bind(extendedTooltip), bind(postInputUpdater), bind(updateTooltipCommand)],
|
||||||
|
bind(pollingInterval),
|
||||||
|
updateTooltipCommand.get(),
|
||||||
|
processUpdateTooltip,
|
||||||
|
pendingUpdates,
|
||||||
|
);
|
||||||
|
|
||||||
updatesPoller.initialize('updates');
|
updatesPoller.initialize('updates');
|
||||||
|
tooltipPoller.initialize('updates');
|
||||||
|
|
||||||
Variable.derive([bind(autoHide)], (autoHideModule) => {
|
Variable.derive([bind(autoHide)], (autoHideModule) => {
|
||||||
isVis.set(!autoHideModule || (autoHideModule && parseFloat(pendingUpdates.get()) > 0));
|
isVis.set(!autoHideModule || (autoHideModule && parseFloat(pendingUpdates.get()) > 0));
|
||||||
@@ -54,7 +73,7 @@ const updatesIcon = Variable.derive(
|
|||||||
export const Updates = (): BarBoxChild => {
|
export const Updates = (): BarBoxChild => {
|
||||||
const updatesModule = Module({
|
const updatesModule = Module({
|
||||||
textIcon: updatesIcon(),
|
textIcon: updatesIcon(),
|
||||||
tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`),
|
tooltipText: bind(pendingUpdatesTooltip),
|
||||||
boxClass: 'updates',
|
boxClass: 'updates',
|
||||||
isVis: isVis,
|
isVis: isVis,
|
||||||
label: bind(pendingUpdates),
|
label: bind(pendingUpdates),
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const getAppIcon = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findIconForClient = (clientClass: string, clientTitle: string): string | undefined => {
|
const findIconForClient = (clientClass: string, clientTitle: string): string | undefined => {
|
||||||
const appIconMap = { ...defaultApplicationIconMap, ...userDefinedIconMap };
|
const appIconMap = { ...userDefinedIconMap, ...defaultApplicationIconMap };
|
||||||
|
|
||||||
const iconEntry = Object.entries(appIconMap).find(([matcher]) => {
|
const iconEntry = Object.entries(appIconMap).find(([matcher]) => {
|
||||||
if (matcher.startsWith('class:')) {
|
if (matcher.startsWith('class:')) {
|
||||||
|
|||||||
@@ -237,6 +237,17 @@ export const CustomModuleSettings = (): JSX.Element => {
|
|||||||
title="Check Updates Command"
|
title="Check Updates Command"
|
||||||
type="string"
|
type="string"
|
||||||
/>
|
/>
|
||||||
|
<Option
|
||||||
|
opt={options.bar.customModules.updates.updateTooltipCommand}
|
||||||
|
title="Check Updates Tooltip Command"
|
||||||
|
type="string"
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
opt={options.bar.customModules.updates.extendedTooltip}
|
||||||
|
title="Show Extended Tooltip"
|
||||||
|
subtitle="Lists packages with updates. Arch only."
|
||||||
|
type="boolean"
|
||||||
|
/>
|
||||||
<Option
|
<Option
|
||||||
opt={options.bar.customModules.updates.icon.pending}
|
opt={options.bar.customModules.updates.icon.pending}
|
||||||
title="Updates Available Icon"
|
title="Updates Available Icon"
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export const Module = ({
|
|||||||
const getIconWidget = (useTxtIcn: boolean): JSX.Element | undefined => {
|
const getIconWidget = (useTxtIcn: boolean): JSX.Element | undefined => {
|
||||||
let iconWidget: JSX.Element | undefined;
|
let iconWidget: JSX.Element | undefined;
|
||||||
|
|
||||||
if (icon !== undefined && !useTxtIcn) {
|
if (icon !== undefined && icon.get() != '' && !useTxtIcn) {
|
||||||
iconWidget = <icon className={`txt-icon bar-button-icon module-icon ${boxClass}`} icon={icon} />;
|
iconWidget = <icon className={`txt-icon bar-button-icon module-icon ${boxClass}`} icon={icon} />;
|
||||||
} else if (textIcon !== undefined) {
|
} else if (textIcon !== undefined && textIcon.get() != '') {
|
||||||
iconWidget = <label className={`txt-icon bar-button-icon module-icon ${boxClass}`} label={textIcon} />;
|
iconWidget = <label className={`txt-icon bar-button-icon module-icon ${boxClass}`} label={textIcon} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
265
src/components/bar/utils/GdkMonitorMapper.ts
Normal file
265
src/components/bar/utils/GdkMonitorMapper.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import { Gdk } from 'astal/gtk3';
|
||||||
|
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||||
|
|
||||||
|
const hyprlandService = AstalHyprland.get_default();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MonitorMapper class encapsulates the conversion logic between GDK and Hyprland monitor IDs.
|
||||||
|
* It maintains internal state for monitors that have already been used so that duplicate assignments are avoided.
|
||||||
|
*/
|
||||||
|
export class GdkMonitorMapper {
|
||||||
|
private usedGdkMonitors: Set<number>;
|
||||||
|
private usedHyprlandMonitors: Set<number>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.usedGdkMonitors = new Set();
|
||||||
|
this.usedHyprlandMonitors = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the internal state for both GDK and Hyprland monitor mappings.
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this.usedGdkMonitors.clear();
|
||||||
|
this.usedHyprlandMonitors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a GDK monitor id to the corresponding Hyprland monitor id.
|
||||||
|
*
|
||||||
|
* @param monitor The GDK monitor id.
|
||||||
|
* @returns The corresponding Hyprland monitor id.
|
||||||
|
*/
|
||||||
|
public mapGdkToHyprland(monitor: number): number {
|
||||||
|
const gdkMonitors = this._getGdkMonitors();
|
||||||
|
|
||||||
|
if (Object.keys(gdkMonitors).length === 0) {
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdkMonitor = gdkMonitors[monitor];
|
||||||
|
const hyprlandMonitors = hyprlandService.get_monitors();
|
||||||
|
|
||||||
|
return this._matchMonitor(
|
||||||
|
hyprlandMonitors,
|
||||||
|
gdkMonitor,
|
||||||
|
monitor,
|
||||||
|
this.usedHyprlandMonitors,
|
||||||
|
(mon) => mon.id,
|
||||||
|
(mon, gdkMon) => this._matchMonitorKey(mon, gdkMon),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Hyprland monitor id to the corresponding GDK monitor id.
|
||||||
|
*
|
||||||
|
* @param monitor The Hyprland monitor id.
|
||||||
|
* @returns The corresponding GDK monitor id.
|
||||||
|
*/
|
||||||
|
public mapHyprlandToGdk(monitor: number): number {
|
||||||
|
const gdkMonitors = this._getGdkMonitors();
|
||||||
|
const gdkCandidates = Object.entries(gdkMonitors).map(([monitorId, monitorMetadata]) => ({
|
||||||
|
id: Number(monitorId),
|
||||||
|
monitor: monitorMetadata,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (gdkCandidates.length === 0) {
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hyprlandMonitors = hyprlandService.get_monitors();
|
||||||
|
const foundHyprlandMonitor = hyprlandMonitors.find((mon) => mon.id === monitor) || hyprlandMonitors[0];
|
||||||
|
|
||||||
|
return this._matchMonitor(
|
||||||
|
gdkCandidates,
|
||||||
|
foundHyprlandMonitor,
|
||||||
|
monitor,
|
||||||
|
this.usedGdkMonitors,
|
||||||
|
(candidate) => candidate.id,
|
||||||
|
(candidate, hyprlandMonitor) => this._matchMonitorKey(hyprlandMonitor, candidate.monitor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic helper that finds the best matching candidate monitor based on:
|
||||||
|
* 1. A direct match (candidate matches the source and has the same id as the target).
|
||||||
|
* 2. A relaxed match (candidate matches the source, regardless of id).
|
||||||
|
* 3. A fallback match (first candidate that hasn’t been used).
|
||||||
|
*
|
||||||
|
* @param candidates Array of candidate monitors.
|
||||||
|
* @param source The source monitor object to match against.
|
||||||
|
* @param target The desired monitor id.
|
||||||
|
* @param usedMonitors A Set of already used candidate ids.
|
||||||
|
* @param getId Function to extract the id from a candidate.
|
||||||
|
* @param compare Function that determines if a candidate matches the source.
|
||||||
|
* @returns The chosen monitor id.
|
||||||
|
*/
|
||||||
|
private _matchMonitor<T, U>(
|
||||||
|
candidates: T[],
|
||||||
|
source: U,
|
||||||
|
target: number,
|
||||||
|
usedMonitors: Set<number>,
|
||||||
|
getId: (candidate: T) => number,
|
||||||
|
compare: (candidate: T, source: U) => boolean,
|
||||||
|
): number {
|
||||||
|
// Direct match: candidate matches the source and has the same id as the target.
|
||||||
|
const directMatch = candidates.find(
|
||||||
|
(candidate) =>
|
||||||
|
compare(candidate, source) && !usedMonitors.has(getId(candidate)) && getId(candidate) === target,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (directMatch !== undefined) {
|
||||||
|
usedMonitors.add(getId(directMatch));
|
||||||
|
return getId(directMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relaxed match: candidate matches the source regardless of id.
|
||||||
|
const relaxedMatch = candidates.find(
|
||||||
|
(candidate) => compare(candidate, source) && !usedMonitors.has(getId(candidate)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relaxedMatch !== undefined) {
|
||||||
|
usedMonitors.add(getId(relaxedMatch));
|
||||||
|
return getId(relaxedMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use the first candidate that hasn't been used.
|
||||||
|
const fallback = candidates.find((candidate) => !usedMonitors.has(getId(candidate)));
|
||||||
|
|
||||||
|
if (fallback !== undefined) {
|
||||||
|
usedMonitors.add(getId(fallback));
|
||||||
|
return getId(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a last resort, iterate over candidates.
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const candidateId = getId(candidate);
|
||||||
|
if (!usedMonitors.has(candidateId)) {
|
||||||
|
usedMonitors.add(candidateId);
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Returning original monitor index as a last resort: ${target}`);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a Hyprland monitor matches a GDK monitor by comparing their keys
|
||||||
|
*
|
||||||
|
* @param hyprlandMonitor - Hyprland monitor object
|
||||||
|
* @param gdkMonitor - GDK monitor object
|
||||||
|
* @returns boolean indicating if the monitors match
|
||||||
|
*/
|
||||||
|
private _matchMonitorKey(hyprlandMonitor: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
||||||
|
const isRotated90 = hyprlandMonitor.transform % 2 !== 0;
|
||||||
|
const gdkScaleFactor = Math.ceil(hyprlandMonitor.scale);
|
||||||
|
|
||||||
|
const scaleFactorWidth = Math.trunc(hyprlandMonitor.width / gdkScaleFactor);
|
||||||
|
const scaleFactorHeight = Math.trunc(hyprlandMonitor.height / gdkScaleFactor);
|
||||||
|
const gdkScaleFactorKey = `${hyprlandMonitor.model}_${scaleFactorWidth}x${scaleFactorHeight}_${gdkScaleFactor}`;
|
||||||
|
|
||||||
|
const transWidth = isRotated90 ? hyprlandMonitor.height : hyprlandMonitor.width;
|
||||||
|
const transHeight = isRotated90 ? hyprlandMonitor.width : hyprlandMonitor.height;
|
||||||
|
const scaleWidth = Math.trunc(transWidth / hyprlandMonitor.scale);
|
||||||
|
const scaleHeight = Math.trunc(transHeight / hyprlandMonitor.scale);
|
||||||
|
const hyprlandScaleFactorKey = `${hyprlandMonitor.model}_${scaleWidth}x${scaleHeight}_${gdkScaleFactor}`;
|
||||||
|
|
||||||
|
const keyMatch = gdkMonitor.key === gdkScaleFactorKey || gdkMonitor.key === hyprlandScaleFactorKey;
|
||||||
|
|
||||||
|
this._logMonitorInfo(
|
||||||
|
gdkMonitor,
|
||||||
|
hyprlandMonitor,
|
||||||
|
isRotated90,
|
||||||
|
gdkScaleFactor,
|
||||||
|
gdkScaleFactorKey,
|
||||||
|
hyprlandScaleFactorKey,
|
||||||
|
keyMatch,
|
||||||
|
);
|
||||||
|
|
||||||
|
return keyMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all GDK monitors from the default display
|
||||||
|
*
|
||||||
|
* @returns Object containing GDK monitor information indexed by monitor ID
|
||||||
|
*/
|
||||||
|
private _getGdkMonitors(): GdkMonitors {
|
||||||
|
const display = Gdk.Display.get_default();
|
||||||
|
if (display === null) {
|
||||||
|
console.error('Failed to get Gdk display.');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const numGdkMonitors = display.get_n_monitors();
|
||||||
|
const gdkMonitors: GdkMonitors = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < numGdkMonitors; i++) {
|
||||||
|
const curMonitor = display.get_monitor(i);
|
||||||
|
if (curMonitor === null) {
|
||||||
|
console.warn(`Monitor at index ${i} is null.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = curMonitor.get_model() || '';
|
||||||
|
const geometry = curMonitor.get_geometry();
|
||||||
|
const scaleFactor = curMonitor.get_scale_factor();
|
||||||
|
|
||||||
|
// GDK3 only supports integer scale factors
|
||||||
|
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
||||||
|
gdkMonitors[i] = { key, model, used: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return gdkMonitors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs detailed monitor information for debugging purposes
|
||||||
|
* @param gdkMonitor - GDK monitor object
|
||||||
|
* @param hyprlandMonitor - Hyprland monitor information
|
||||||
|
* @param isRotated90 - Whether the monitor is rotated 90 degrees
|
||||||
|
* @param gdkScaleFactor - The GDK monitor's scale factor
|
||||||
|
* @param gdkScaleFactorKey - Key used for scale factor matching
|
||||||
|
* @param hyprlandScaleFactorKey - Key used for general scale matching
|
||||||
|
* @param keyMatch - Whether the monitor keys match
|
||||||
|
*/
|
||||||
|
private _logMonitorInfo(
|
||||||
|
gdkMonitor: GdkMonitor,
|
||||||
|
hyprlandMonitor: AstalHyprland.Monitor,
|
||||||
|
isRotated90: boolean,
|
||||||
|
gdkScaleFactor: number,
|
||||||
|
gdkScaleFactorKey: string,
|
||||||
|
hyprlandScaleFactorKey: string,
|
||||||
|
keyMatch: boolean,
|
||||||
|
): void {
|
||||||
|
console.debug('=== Monitor Matching Debug Info ===');
|
||||||
|
console.debug('GDK Monitor');
|
||||||
|
console.debug(` Key: ${gdkMonitor.key}`);
|
||||||
|
console.debug('Hyprland Monitor');
|
||||||
|
console.debug(` ID: ${hyprlandMonitor.id}`);
|
||||||
|
console.debug(` Model: ${hyprlandMonitor.model}`);
|
||||||
|
console.debug(` Resolution: ${hyprlandMonitor.width}x${hyprlandMonitor.height}`);
|
||||||
|
console.debug(` Scale: ${hyprlandMonitor.scale}`);
|
||||||
|
console.debug(` Transform: ${hyprlandMonitor.transform}`);
|
||||||
|
console.debug('Calculated Values');
|
||||||
|
console.debug(` Rotation: ${isRotated90 ? '90°' : '0°'}`);
|
||||||
|
console.debug(` GDK Scale Factor: ${gdkScaleFactor}`);
|
||||||
|
console.debug('Calculated Keys');
|
||||||
|
console.debug(` GDK Scale Factor Key: ${gdkScaleFactorKey}`);
|
||||||
|
console.debug(` Hyprland Scale Factor Key: ${hyprlandScaleFactorKey}`);
|
||||||
|
console.debug('Match Result');
|
||||||
|
console.debug(` ${keyMatch ? '✅ Monitors Match' : '❌ No Match'}`);
|
||||||
|
console.debug('===============================\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GdkMonitor = {
|
||||||
|
key: string;
|
||||||
|
model: string;
|
||||||
|
used: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GdkMonitors = {
|
||||||
|
[key: string]: GdkMonitor;
|
||||||
|
};
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
import { Gdk } from 'astal/gtk3';
|
|
||||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
|
||||||
import { BarLayout, BarLayouts } from 'src/lib/types/options';
|
import { BarLayout, BarLayouts } from 'src/lib/types/options';
|
||||||
|
|
||||||
const hyprlandService = AstalHyprland.get_default();
|
/**
|
||||||
|
* Returns the bar layout configuration for a specific monitor
|
||||||
type GdkMonitor = {
|
*
|
||||||
key: string;
|
* @param monitor - Monitor ID number
|
||||||
model: string;
|
* @param layouts - Object containing layout configurations for different monitors
|
||||||
used: boolean;
|
* @returns BarLayout configuration for the specified monitor, falling back to default if not found
|
||||||
};
|
*/
|
||||||
|
|
||||||
type GdkMonitors = {
|
|
||||||
[key: string]: GdkMonitor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
|
export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
|
||||||
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
|
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
|
||||||
const wildcard = Object.keys(layouts).find((key) => key === '*');
|
const wildcard = Object.keys(layouts).find((key) => key === '*');
|
||||||
@@ -33,6 +26,12 @@ export const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLa
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a bar layout configuration is empty
|
||||||
|
*
|
||||||
|
* @param layout - Bar layout configuration to check
|
||||||
|
* @returns boolean indicating if all sections of the layout are empty
|
||||||
|
*/
|
||||||
export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||||
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
|
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
|
||||||
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
|
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
|
||||||
@@ -40,169 +39,3 @@ export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
|||||||
|
|
||||||
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getGdkMonitors(): GdkMonitors {
|
|
||||||
const display = Gdk.Display.get_default();
|
|
||||||
|
|
||||||
if (display === null) {
|
|
||||||
console.error('Failed to get Gdk display.');
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const numGdkMonitors = display.get_n_monitors();
|
|
||||||
const gdkMonitors: GdkMonitors = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < numGdkMonitors; i++) {
|
|
||||||
const curMonitor = display.get_monitor(i);
|
|
||||||
|
|
||||||
if (curMonitor === null) {
|
|
||||||
console.warn(`Monitor at index ${i} is null.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = curMonitor.get_model() || '';
|
|
||||||
const geometry = curMonitor.get_geometry();
|
|
||||||
const scaleFactor = curMonitor.get_scale_factor();
|
|
||||||
|
|
||||||
// We can only use the scaleFactor for a scale variable in the key
|
|
||||||
// GDK3 doesn't support the fractional "scale" attribute (available in GDK4)
|
|
||||||
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
|
||||||
gdkMonitors[i] = { key, model, used: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return gdkMonitors;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function matchMonitorKey(hypMon: AstalHyprland.Monitor, gdkMonitor: GdkMonitor): boolean {
|
|
||||||
const isRotated90 = hypMon.transform % 2 !== 0;
|
|
||||||
|
|
||||||
// Needed for the key regardless of scaling below because GDK3 only has the scale factor for the key
|
|
||||||
const gdkScaleFactor = Math.ceil(hypMon.scale);
|
|
||||||
|
|
||||||
// When gdk is scaled with the scale factor, the hyprland width/height will be the same as the base monitor resolution
|
|
||||||
// The GDK width/height will NOT flip regardless of transformation (e.g. 90 degrees will NOT swap the GDK width/height)
|
|
||||||
const scaleFactorWidth = Math.trunc(hypMon.width / gdkScaleFactor);
|
|
||||||
const scaleFactorHeight = Math.trunc(hypMon.height / gdkScaleFactor);
|
|
||||||
const scaleFactorKey = `${hypMon.model}_${scaleFactorWidth}x${scaleFactorHeight}_${gdkScaleFactor}`;
|
|
||||||
|
|
||||||
// When gdk geometry is scaled with the fractional scale, we need to scale the hyprland geometry to match it
|
|
||||||
// However a 90 degree transformation WILL flip the GDK width/height
|
|
||||||
const transWidth = isRotated90 ? hypMon.height : hypMon.width;
|
|
||||||
const transHeight = isRotated90 ? hypMon.width : hypMon.height;
|
|
||||||
const scaleWidth = Math.trunc(transWidth / hypMon.scale);
|
|
||||||
const scaleHeight = Math.trunc(transHeight / hypMon.scale);
|
|
||||||
const scaleKey = `${hypMon.model}_${scaleWidth}x${scaleHeight}_${gdkScaleFactor}`;
|
|
||||||
|
|
||||||
// In GDK3 the GdkMonitor geometry can change depending on how the compositor handles scaling surface framebuffers
|
|
||||||
// We try to match against two different possibilities:
|
|
||||||
// 1) The geometry is scaled by the correct fractional scale
|
|
||||||
// 2) The geometry is scaled by the scaleFactor (the fractional scale rounded up)
|
|
||||||
const keyMatch = gdkMonitor.key === scaleFactorKey || gdkMonitor.key === scaleKey;
|
|
||||||
|
|
||||||
// Monitor matching debug logging, use if your workspaces are appearing on the wrong screen
|
|
||||||
// To use, kill any running HyprPanel instances and then start a terminal, then run:
|
|
||||||
// G_MESSAGES_DEBUG=all hyprpanel | grep "hyprpanel-DEBUG"
|
|
||||||
// Create an issue in HyprPanel github and post these logs
|
|
||||||
console.debug('Attempting gdk key match');
|
|
||||||
console.debug(`GDK key: ${gdkMonitor.key}`);
|
|
||||||
console.debug(`HypMon.width: ${hypMon.width}`);
|
|
||||||
console.debug(`HypMon.height: ${hypMon.height}`);
|
|
||||||
console.debug(`HypMon.scale: ${hypMon.scale}`);
|
|
||||||
console.debug(`HypMon.transform: ${hypMon.transform}`);
|
|
||||||
console.debug(`isRotated90: ${isRotated90}`);
|
|
||||||
console.debug(`scaleFactor: ${gdkScaleFactor}`);
|
|
||||||
console.debug(`scaleFactorKey: ${scaleFactorKey}`);
|
|
||||||
console.debug(`scaleKey: ${scaleKey}`);
|
|
||||||
console.debug(`match?: ${keyMatch}`);
|
|
||||||
|
|
||||||
return keyMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: Some more funky stuff being done by GDK.
|
|
||||||
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
|
|
||||||
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
|
|
||||||
*
|
|
||||||
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
|
|
||||||
* the id 0 will ALWAYS be DP-1.
|
|
||||||
*
|
|
||||||
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
|
|
||||||
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
|
|
||||||
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
|
|
||||||
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
|
|
||||||
* is at id 2.
|
|
||||||
*
|
|
||||||
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
|
|
||||||
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
|
|
||||||
* is being determined as by Hyprland so the bars show up on the right monitors.
|
|
||||||
*
|
|
||||||
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
|
|
||||||
* in the case that there are multiple models in the same resolution with the same scale. We find the
|
|
||||||
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
|
|
||||||
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
|
|
||||||
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
|
|
||||||
*
|
|
||||||
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
|
|
||||||
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
|
|
||||||
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
|
|
||||||
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
|
|
||||||
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
|
|
||||||
*
|
|
||||||
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
|
|
||||||
* an existing one or shutting it off.
|
|
||||||
*
|
|
||||||
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
|
|
||||||
*
|
|
||||||
* Fun stuff really... :facepalm:
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
|
|
||||||
const gdkMonitors = getGdkMonitors();
|
|
||||||
|
|
||||||
if (Object.keys(gdkMonitors).length === 0) {
|
|
||||||
return monitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the GDK monitor for the given monitor index
|
|
||||||
const gdkMonitor = gdkMonitors[monitor];
|
|
||||||
|
|
||||||
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
|
|
||||||
const directMatch = hyprlandService.get_monitors().find((hypMon) => {
|
|
||||||
return matchMonitorKey(hypMon, gdkMonitor) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (directMatch) {
|
|
||||||
usedHyprlandMonitors.add(directMatch.id);
|
|
||||||
return directMatch.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: Relaxed matching without considering the monitor index
|
|
||||||
const hyprlandMonitor = hyprlandService.get_monitors().find((hypMon) => {
|
|
||||||
return matchMonitorKey(hypMon, gdkMonitor) && !usedHyprlandMonitors.has(hypMon.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hyprlandMonitor) {
|
|
||||||
usedHyprlandMonitors.add(hyprlandMonitor.id);
|
|
||||||
return hyprlandMonitor.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Find the first available monitor ID that hasn't been used
|
|
||||||
const fallbackMonitor = hyprlandService.get_monitors().find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
|
|
||||||
|
|
||||||
if (fallbackMonitor) {
|
|
||||||
usedHyprlandMonitors.add(fallbackMonitor.id);
|
|
||||||
return fallbackMonitor.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we return a valid monitor ID that actually exists
|
|
||||||
for (let i = 0; i < hyprlandService.get_monitors().length; i++) {
|
|
||||||
if (!usedHyprlandMonitors.has(i)) {
|
|
||||||
usedHyprlandMonitors.add(i);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// As a last resort, return the original monitor index if no unique monitor can be found
|
|
||||||
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
|
|
||||||
return monitor;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { bind, execAsync, Variable } from 'astal';
|
import { bind, Variable } from 'astal';
|
||||||
import { App, Gdk, Gtk } from 'astal/gtk3';
|
import { App, Gdk, Gtk } from 'astal/gtk3';
|
||||||
import Menu from 'src/components/shared/Menu';
|
import Menu from 'src/components/shared/Menu';
|
||||||
import MenuItem from 'src/components/shared/MenuItem';
|
import MenuItem from 'src/components/shared/MenuItem';
|
||||||
import { isRecording } from '../helpers';
|
import { isRecording, getRecordingPath, executeCommand } from '../helpers';
|
||||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||||
|
|
||||||
const hyprlandService = AstalHyprland.get_default();
|
const hyprlandService = AstalHyprland.get_default();
|
||||||
@@ -10,44 +10,41 @@ const hyprlandService = AstalHyprland.get_default();
|
|||||||
const MonitorListDropdown = (): JSX.Element => {
|
const MonitorListDropdown = (): JSX.Element => {
|
||||||
const monitorList: Variable<AstalHyprland.Monitor[]> = Variable([]);
|
const monitorList: Variable<AstalHyprland.Monitor[]> = Variable([]);
|
||||||
|
|
||||||
const monitorBinding = Variable.derive([bind(hyprlandService, 'monitors')], () =>
|
const monitorBinding = Variable.derive([bind(hyprlandService, 'monitors')], () => {
|
||||||
monitorList.set(hyprlandService.get_monitors()),
|
monitorList.set(hyprlandService.get_monitors());
|
||||||
);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu className={'dropdown recording'} halign={Gtk.Align.FILL} onDestroy={() => monitorBinding.drop()} hexpand>
|
<Menu className={'dropdown recording'} halign={Gtk.Align.FILL} onDestroy={() => monitorBinding.drop()} hexpand>
|
||||||
{bind(monitorList).as((monitors) => {
|
{bind(monitorList).as((monitors) =>
|
||||||
return monitors.map((monitor) => (
|
monitors.map((monitor) => {
|
||||||
<MenuItem
|
const sanitizedPath = getRecordingPath().replace(/"/g, '\\"');
|
||||||
label={`Display ${monitor.name}`}
|
|
||||||
onButtonPressEvent={(_, event) => {
|
|
||||||
const buttonClicked = event.get_button()[1];
|
|
||||||
|
|
||||||
if (buttonClicked !== Gdk.BUTTON_PRIMARY) {
|
return (
|
||||||
return;
|
<MenuItem
|
||||||
}
|
label={`Display ${monitor.name}`}
|
||||||
|
onButtonPressEvent={(_, event) => {
|
||||||
|
if (event.get_button()[1] !== Gdk.BUTTON_PRIMARY) return;
|
||||||
|
|
||||||
App.get_window('dashboardmenu')?.set_visible(false);
|
App.get_window('dashboardmenu')?.set_visible(false);
|
||||||
|
|
||||||
execAsync(`${SRC_DIR}/scripts/screen_record.sh start screen ${monitor.name}`).catch((err) =>
|
const command = `${SRC_DIR}/scripts/screen_record.sh start screen "${monitor.name}" "${sanitizedPath}"`;
|
||||||
console.error(err),
|
executeCommand(command);
|
||||||
);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
);
|
||||||
));
|
}),
|
||||||
})}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Region"
|
label="Region"
|
||||||
onButtonPressEvent={(_, event) => {
|
onButtonPressEvent={(_, event) => {
|
||||||
const buttonClicked = event.get_button()[1];
|
if (event.get_button()[1] !== Gdk.BUTTON_PRIMARY) return;
|
||||||
|
|
||||||
if (buttonClicked !== Gdk.BUTTON_PRIMARY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.get_window('dashboardmenu')?.set_visible(false);
|
App.get_window('dashboardmenu')?.set_visible(false);
|
||||||
|
|
||||||
execAsync(`${SRC_DIR}/scripts/screen_record.sh start region`).catch((err) => console.error(err));
|
const sanitizedPath = getRecordingPath().replace(/"/g, '\\"');
|
||||||
|
const command = `${SRC_DIR}/scripts/screen_record.sh start region "${sanitizedPath}"`;
|
||||||
|
executeCommand(command);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -58,7 +55,7 @@ export const RecordingButton = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`dashboard-button record ${isRecording.get() ? 'active' : ''}`}
|
className={`dashboard-button record ${isRecording.get() ? 'active' : ''}`}
|
||||||
tooltipText={'Record Screen'}
|
tooltipText="Record Screen"
|
||||||
vexpand
|
vexpand
|
||||||
onButtonPressEvent={(_, event) => {
|
onButtonPressEvent={(_, event) => {
|
||||||
const buttonClicked = event.get_button()[1];
|
const buttonClicked = event.get_button()[1];
|
||||||
@@ -67,9 +64,12 @@ export const RecordingButton = (): JSX.Element => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sanitizedPath = getRecordingPath().replace(/"/g, '\\"');
|
||||||
|
|
||||||
if (isRecording.get() === true) {
|
if (isRecording.get() === true) {
|
||||||
App.get_window('dashboardmenu')?.set_visible(false);
|
App.get_window('dashboardmenu')?.set_visible(false);
|
||||||
return execAsync(`${SRC_DIR}/scripts/screen_record.sh stop`).catch((err) => console.error(err));
|
const command = `${SRC_DIR}/scripts/screen_record.sh stop "${sanitizedPath}"`;
|
||||||
|
executeCommand(command);
|
||||||
} else {
|
} else {
|
||||||
const monitorDropdownList = MonitorListDropdown() as Gtk.Menu;
|
const monitorDropdownList = MonitorListDropdown() as Gtk.Menu;
|
||||||
monitorDropdownList.popup_at_pointer(event);
|
monitorDropdownList.popup_at_pointer(event);
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ import options from 'src/options';
|
|||||||
|
|
||||||
const { left } = options.menus.dashboard.shortcuts;
|
const { left } = options.menus.dashboard.shortcuts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the latest recording path from options.
|
||||||
|
*
|
||||||
|
* @returns The configured recording path.
|
||||||
|
*/
|
||||||
|
export const getRecordingPath = (): string => options.menus.dashboard.recording.path.get();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a shell command asynchronously with proper error handling.
|
||||||
|
*
|
||||||
|
* @param command The command to execute.
|
||||||
|
*/
|
||||||
|
export const executeCommand = async (command: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await execAsync(`/bin/bash -c '${command}'`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Command failed:', command);
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the recorder status based on the command output.
|
* Handles the recorder status based on the command output.
|
||||||
*
|
*
|
||||||
@@ -16,10 +37,7 @@ const { left } = options.menus.dashboard.shortcuts;
|
|||||||
* @returns True if the command output is 'recording', false otherwise.
|
* @returns True if the command output is 'recording', false otherwise.
|
||||||
*/
|
*/
|
||||||
export const handleRecorder = (commandOutput: string): boolean => {
|
export const handleRecorder = (commandOutput: string): boolean => {
|
||||||
if (commandOutput === 'recording') {
|
return commandOutput === 'recording';
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,9 +53,7 @@ export const handleClick = (action: string, tOut: number = 0): void => {
|
|||||||
|
|
||||||
timeout(tOut, () => {
|
timeout(tOut, () => {
|
||||||
execAsync(`bash -c "${action}"`)
|
execAsync(`bash -c "${action}"`)
|
||||||
.then((res) => {
|
.then((res) => res)
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -45,10 +61,7 @@ export const handleClick = (action: string, tOut: number = 0): void => {
|
|||||||
/**
|
/**
|
||||||
* Checks if a shortcut has a command.
|
* Checks if a shortcut has a command.
|
||||||
*
|
*
|
||||||
* This function determines if the provided shortcut has a command defined.
|
|
||||||
*
|
|
||||||
* @param shortCut The shortcut to check.
|
* @param shortCut The shortcut to check.
|
||||||
*
|
|
||||||
* @returns True if the shortcut has a command, false otherwise.
|
* @returns True if the shortcut has a command, false otherwise.
|
||||||
*/
|
*/
|
||||||
export const hasCommand = (shortCut: ShortcutVariable): boolean => {
|
export const hasCommand = (shortCut: ShortcutVariable): boolean => {
|
||||||
@@ -58,7 +71,7 @@ export const hasCommand = (shortCut: ShortcutVariable): boolean => {
|
|||||||
/**
|
/**
|
||||||
* A variable indicating whether the left card is hidden.
|
* A variable indicating whether the left card is hidden.
|
||||||
*
|
*
|
||||||
* This variable is set to true if none of the left shortcuts have commands defined.
|
* This is set to true if none of the left shortcuts have commands.
|
||||||
*/
|
*/
|
||||||
export const leftCardHidden = Variable(
|
export const leftCardHidden = Variable(
|
||||||
!(
|
!(
|
||||||
@@ -82,7 +95,8 @@ export const isRecording = Variable(false);
|
|||||||
/**
|
/**
|
||||||
* A poller for checking the recording status.
|
* A poller for checking the recording status.
|
||||||
*
|
*
|
||||||
* This poller periodically checks the recording status by executing a bash command and updates the `isRecording` variable.
|
* This poller periodically checks the recording status by executing a bash command
|
||||||
|
* and updates the `isRecording` variable accordingly.
|
||||||
*/
|
*/
|
||||||
export const recordingPoller = new BashPoller<boolean, []>(
|
export const recordingPoller = new BashPoller<boolean, []>(
|
||||||
isRecording,
|
isRecording,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default (): JSX.Element => {
|
|||||||
curPage.drop();
|
curPage.drop();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box className={'notification-menu-content'} css={'padding: 1px; margin: -1px;'} hexpand vexpand>
|
<box className={'notification-menu-content'} hexpand vexpand>
|
||||||
<box className={'notification-card-container menu'} hexpand vexpand vertical>
|
<box className={'notification-card-container menu'} hexpand vexpand vertical>
|
||||||
<Controls />
|
<Controls />
|
||||||
<NotificationsContainer curPage={curPage} />
|
<NotificationsContainer curPage={curPage} />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Astal } from 'astal/gtk3';
|
|||||||
import { NotificationCard } from './Notification.js';
|
import { NotificationCard } from './Notification.js';
|
||||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||||
|
import { GdkMonitorMapper } from '../bar/utils/GdkMonitorMapper';
|
||||||
|
|
||||||
const hyprlandService = AstalHyprland.get_default();
|
const hyprlandService = AstalHyprland.get_default();
|
||||||
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
const { position, monitor, active_monitor, showActionsOnHover, displayedTotal } = options.notifications;
|
||||||
@@ -19,15 +20,22 @@ trackPopupNotifications(popupNotifications);
|
|||||||
trackAutoTimeout();
|
trackAutoTimeout();
|
||||||
|
|
||||||
export default (): JSX.Element => {
|
export default (): JSX.Element => {
|
||||||
|
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||||
|
|
||||||
const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
|
const windowLayer = bind(tear).as((tear) => (tear ? Astal.Layer.TOP : Astal.Layer.OVERLAY));
|
||||||
const windowAnchor = bind(position).as(getPosition);
|
const windowAnchor = bind(position).as(getPosition);
|
||||||
const windowMonitor = Variable.derive(
|
const windowMonitor = Variable.derive(
|
||||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||||
(focusedMonitor, monitor, activeMonitor) => {
|
(focusedMonitor, monitor, activeMonitor) => {
|
||||||
|
gdkMonitorMapper.reset();
|
||||||
|
|
||||||
if (activeMonitor === true) {
|
if (activeMonitor === true) {
|
||||||
return focusedMonitor.id;
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(focusedMonitor.id);
|
||||||
|
return gdkMonitor;
|
||||||
}
|
}
|
||||||
return monitor;
|
|
||||||
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(monitor);
|
||||||
|
return gdkMonitor;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
|||||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||||
import options from 'src/options';
|
import options from 'src/options';
|
||||||
import Brightness from 'src/services/Brightness';
|
import Brightness from 'src/services/Brightness';
|
||||||
|
import { GdkMonitorMapper } from '../bar/utils/GdkMonitorMapper';
|
||||||
|
|
||||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||||
const audioService = wireplumber.audio;
|
const audioService = wireplumber.audio;
|
||||||
@@ -59,14 +60,20 @@ export const handleReveal = (self: Widget.Revealer): void => {
|
|||||||
* @returns A Variable<number> representing the monitor index for the OSD.
|
* @returns A Variable<number> representing the monitor index for the OSD.
|
||||||
*/
|
*/
|
||||||
export const getOsdMonitor = (): Variable<number> => {
|
export const getOsdMonitor = (): Variable<number> => {
|
||||||
|
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||||
|
|
||||||
return Variable.derive(
|
return Variable.derive(
|
||||||
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
[bind(hyprlandService, 'focusedMonitor'), bind(monitor), bind(active_monitor)],
|
||||||
(currentMonitor, defaultMonitor, followMonitor) => {
|
(currentMonitor, defaultMonitor, followMonitor) => {
|
||||||
|
gdkMonitorMapper.reset();
|
||||||
|
|
||||||
if (followMonitor === true) {
|
if (followMonitor === true) {
|
||||||
return currentMonitor.id;
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(currentMonitor.id);
|
||||||
|
return gdkMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultMonitor;
|
const gdkMonitor = gdkMonitorMapper.mapHyprlandToGdk(defaultMonitor);
|
||||||
|
return gdkMonitor;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const DashboardMenuSettings = (): JSX.Element => {
|
|||||||
<Option opt={options.menus.dashboard.powermenu.logout} title="Logout Command" type="string" />
|
<Option opt={options.menus.dashboard.powermenu.logout} title="Logout Command" type="string" />
|
||||||
<Option opt={options.menus.dashboard.powermenu.sleep} title="Sleep Command" type="string" />
|
<Option opt={options.menus.dashboard.powermenu.sleep} title="Sleep Command" type="string" />
|
||||||
|
|
||||||
|
<Header title="Recording" />
|
||||||
|
<Option opt={options.menus.dashboard.recording.path} title="Recording Path" type="string" />
|
||||||
|
|
||||||
<Header title="Controls" />
|
<Header title="Controls" />
|
||||||
<Option opt={options.menus.dashboard.controls.enabled} title="Enabled" type="boolean" />
|
<Option opt={options.menus.dashboard.controls.enabled} title="Enabled" type="boolean" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import FontButton from 'src/components/shared/FontButton';
|
import FontButton from 'src/components/shared/FontButton';
|
||||||
import { Opt } from 'src/lib/option';
|
import { Opt } from 'src/lib/option';
|
||||||
import { styleToString } from './utils';
|
import { FontStyle, styleToString } from './utils';
|
||||||
|
|
||||||
export const FontInputter = <T extends string | number | boolean | object>({
|
export const FontInputter = <T extends string | number | boolean | object>({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
@@ -38,6 +38,6 @@ export const FontInputter = <T extends string | number | boolean | object>({
|
|||||||
|
|
||||||
interface FontInputterProps<T> {
|
interface FontInputterProps<T> {
|
||||||
fontFamily: Opt<T>;
|
fontFamily: Opt<T>;
|
||||||
fontStyle?: Opt<string>;
|
fontStyle?: Opt<FontStyle>;
|
||||||
fontLabel?: Opt<string>;
|
fontLabel?: Opt<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/lib/types/options.d.ts
vendored
3
src/lib/types/options.d.ts
vendored
@@ -3,6 +3,7 @@ import { Variable } from 'types/variable';
|
|||||||
import { defaultColorMap } from './defaults/options';
|
import { defaultColorMap } from './defaults/options';
|
||||||
import { Astal } from 'astal/gtk3';
|
import { Astal } from 'astal/gtk3';
|
||||||
import { dropdownMenuList } from '../constants/options';
|
import { dropdownMenuList } from '../constants/options';
|
||||||
|
import { FontStyle } from 'src/components/settings/shared/inputs/font/utils';
|
||||||
|
|
||||||
export type MkOptionsResult = {
|
export type MkOptionsResult = {
|
||||||
array: () => Opt[];
|
array: () => Opt[];
|
||||||
@@ -104,7 +105,7 @@ export interface RowProps<T> {
|
|||||||
subtitleLink?: string;
|
subtitleLink?: string;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
increment?: number;
|
increment?: number;
|
||||||
fontStyle?: Opt<string>;
|
fontStyle?: Opt<FontStyle>;
|
||||||
fontLabel?: Opt<string>;
|
fontLabel?: Opt<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1142,6 +1142,8 @@ const options = mkOptions({
|
|||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
updateCommand: opt(`${SRC_DIR}/scripts/checkUpdates.sh -arch`),
|
updateCommand: opt(`${SRC_DIR}/scripts/checkUpdates.sh -arch`),
|
||||||
|
updateTooltipCommand: opt(`${SRC_DIR}/scripts/checkUpdates.sh -arch -tooltip`),
|
||||||
|
extendedTooltip: opt(false),
|
||||||
label: opt(true),
|
label: opt(true),
|
||||||
padZero: opt(true),
|
padZero: opt(true),
|
||||||
autoHide: opt(false),
|
autoHide: opt(false),
|
||||||
@@ -1280,6 +1282,9 @@ const options = mkOptions({
|
|||||||
interval: opt(2000),
|
interval: opt(2000),
|
||||||
enable_gpu: opt(false),
|
enable_gpu: opt(false),
|
||||||
},
|
},
|
||||||
|
recording: {
|
||||||
|
path: opt('$HOME/Videos/Screencasts'),
|
||||||
|
},
|
||||||
controls: {
|
controls: {
|
||||||
enabled: opt(true),
|
enabled: opt(true),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,9 +36,7 @@
|
|||||||
min-height: 0em;
|
min-height: 0em;
|
||||||
font-size: $font-size * $bar-menus-menu-notifications-scaling * 0.01;
|
font-size: $font-size * $bar-menus-menu-notifications-scaling * 0.01;
|
||||||
border: 0.15em solid $notification-border;
|
border: 0.15em solid $notification-border;
|
||||||
border-radius: 0em;
|
border-radius: $notification-border_radius;
|
||||||
border-bottom-left-radius: $notification-border_radius;
|
|
||||||
border-top-left-radius: $notification-border_radius;
|
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +206,7 @@
|
|||||||
|
|
||||||
.notification-menu-pager {
|
.notification-menu-pager {
|
||||||
background: $bar-menus-menu-notifications-pager-background;
|
background: $bar-menus-menu-notifications-pager-background;
|
||||||
|
margin-bottom: 0.1em;
|
||||||
border-radius: $bar-menus-border-radius;
|
border-radius: $bar-menus-border-radius;
|
||||||
border-top-left-radius: 0em;
|
border-top-left-radius: 0em;
|
||||||
border-top-right-radius: 0em;
|
border-top-right-radius: 0em;
|
||||||
|
|||||||
Reference in New Issue
Block a user