diff --git a/nix/module.nix b/nix/module.nix index a26fbfd..48182a7 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -321,6 +321,7 @@ in bar.notifications.scrollUp = mkStrOption ""; bar.notifications.show_total = mkBoolOption false; bar.scrollSpeed = mkIntOption 5; + bar.systray.ignore = mkStrListOption []; bar.volume.label = mkBoolOption true; bar.volume.middleClick = mkStrOption ""; bar.volume.rightClick = mkStrOption ""; @@ -406,6 +407,7 @@ in menus.dashboard.powermenu.reboot = mkStrOption "systemctl reboot"; menus.dashboard.powermenu.shutdown = mkStrOption "systemctl poweroff"; menus.dashboard.powermenu.sleep = mkStrOption "systemctl suspend"; + menus.dashboard.recording.path = mkStrOption "$HOME/Videos/Screencasts"; menus.dashboard.shortcuts.enabled = mkBoolOption true; menus.dashboard.shortcuts.left.shortcut1.command = mkStrOption "microsoft-edge-stable"; menus.dashboard.shortcuts.left.shortcut1.icon = mkStrOption "󰇩"; diff --git a/package-lock.json b/package-lock.json index fc70eb6..7d53acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,13 @@ "typescript": "^5.6.2" } }, + "../../../../../usr/share/astal/gjs": { + "name": "astal", + "license": "LGPL-2.1" + }, "../../../../usr/share/astal/gjs": { "name": "astal", + "extraneous": true, "license": "LGPL-2.1" }, "node_modules/@eslint-community/eslint-utils": { @@ -601,7 +606,7 @@ } }, "node_modules/astal": { - "resolved": "../../../../usr/share/astal/gjs", + "resolved": "../../../../../usr/share/astal/gjs", "link": true }, "node_modules/available-typed-arrays": { diff --git a/scripts/checkUpdates.sh b/scripts/checkUpdates.sh index ab36d3c..cff43db 100755 --- a/scripts/checkUpdates.sh +++ b/scripts/checkUpdates.sh @@ -1,49 +1,94 @@ #!/bin/bash -check_arch_updates() { - official_updates=0 - aur_updates=0 - if command -v paru &> /dev/null; then - aur_helper="paru" - else - aur_helper="yay" - fi +has_param() { + local term="$1" + shift + for arg; do + if [[ $arg == "$term" ]]; then + return 0 + fi + done + return 1 +} - if [ "$1" = "-y" ]; then - aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) - elif [ "$1" = "-p" ]; then - official_updates=$(checkupdates 2>/dev/null | wc -l) +wait_for_process_to_finish() { + local process_name="$1" + while pgrep -a "$process_name" >/dev/null; do + sleep 0.1 + done +} + +check_arch_updates() { + if command -v paru &> /dev/null; then + aur_helper="paru" else - official_updates=$(checkupdates 2>/dev/null | wc -l) - aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) + aur_helper="yay" + 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 - total_updates=$((official_updates + aur_updates)) - echo $total_updates } check_ubuntu_updates() { - result=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst) - echo "$result" + result=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst) + echo "$result" } check_fedora_updates() { - result=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l) - echo "$result" + result=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l) + echo "$result" } case "$1" in --arch) - check_arch_updates "$2" - ;; --ubuntu) - check_ubuntu_updates - ;; --fedora) - check_fedora_updates - ;; -*) - echo "0" - ;; + -arch) + check_arch_updates "$2" "$3" + ;; + -ubuntu) + check_ubuntu_updates + ;; + -fedora) + check_fedora_updates + ;; + *) + echo "0" + ;; esac diff --git a/scripts/screen_record.sh b/scripts/screen_record.sh index f203689..af09575 100755 --- a/scripts/screen_record.sh +++ b/scripts/screen_record.sh @@ -1,18 +1,18 @@ #!/usr/bin/env bash # Requires wf-recorder: https://github.com/ammen99/wf-recorder -outputDir="$HOME/Videos/Screencasts" +# Get the default audio sink defaultSink=$(pactl get-default-sink) WF_RECORDER_OPTS="--audio=$defaultSink.monitor -c libx264rgb" +outputFile="" +outputDir="" +# Function to check if recording is active checkRecording() { - if pgrep -f "wf-recorder" >/dev/null; then - return 0 - else - return 1 - fi + pgrep -f "wf-recorder" >/dev/null } +# Function to start screen recording startRecording() { if checkRecording; then echo "A recording is already in progress." @@ -21,50 +21,115 @@ startRecording() { target="$2" - outputFile="recording_$(date +%Y-%m-%d_%H-%M-%S)" - outputPath="$outputDir/${outputFile}.mp4" - mkdir -p "$outputDir" - if [ "$target" == "screen" ]; then - monitor_info=$(hyprctl -j monitors | jq -r ".[] | select(.name == \"$3\")") - w=$(echo $monitor_info | jq -r '.width') - 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" & + monitor_name="$3" + outputDir="$4" elif [ "$target" == "region" ]; then - wf-recorder $WF_RECORDER_OPTS --geometry "$(slurp)" --file "$outputPath" & + outputDir="$3" else - echo "Usage: $0 start {region|screen [screen_name]}" + echo "Usage: $0 start {screen | region} " exit 1 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() { if ! checkRecording; then - echo "No recording is in progress." + echo "No recording in progress." exit 1 fi 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 \ -a "Screen Recorder" \ -t 10000 \ - -u normal \ - --action="scriptAction:-xdg-open $outputDir=Directory" \ - --action="scriptAction:-xdg-open $recentFile=Play" + --action="scriptAction:-xdg-open $(dirname "$outputPath")=Open Directory" \ + --action="scriptAction:-xdg-open $outputPath=Play" } +# Handle script arguments case "$1" in start) startRecording "$@" @@ -80,7 +145,7 @@ status) fi ;; *) - echo "Usage: $0 {start [screen screen_name|region]|stop|status}" + echo "Usage: $0 {start [screen | region] | stop | status}" exit 1 ;; esac diff --git a/src/components/bar/index.tsx b/src/components/bar/index.tsx index 115fd43..12f79b2 100644 --- a/src/components/bar/index.tsx +++ b/src/components/bar/index.tsx @@ -34,7 +34,8 @@ import { App, Gtk } from 'astal/gtk3'; import Astal from 'gi://Astal?version=3.0'; 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 { location } = options.theme.bar; @@ -68,107 +69,110 @@ const widget = { cava: (): JSX.Element => WidgetContainer(Cava()), }; -export const Bar = (() => { - const usedHyprlandMonitors = new Set(); +const gdkMonitorMapper = new GdkMonitorMapper(); - return (monitor: number): JSX.Element => { - const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors); +export const Bar = (monitor: number): JSX.Element => { + const hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor); - const computeVisibility = bind(layouts).as(() => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); - return !isLayoutEmpty(foundLayout); - }); + const computeVisibility = bind(layouts).as(() => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); + return !isLayoutEmpty(foundLayout); + }); - const computeAnchor = bind(location).as((loc) => { - if (loc === 'bottom') { - return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; - } + const computeClassName = bind(layouts).as(() => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); + 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) => { - 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 Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + }); - 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) => - brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel', - ); + return layerMap[barLayer]; + }); - const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + const computeBorderLocation = bind(borderLocation).as((brdrLcn) => + brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel', + ); - return foundLayout.left - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); - const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return foundLayout.middle - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); - const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { - const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + return foundLayout.left + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return foundLayout.right - .filter((mod) => Object.keys(widget).includes(mod)) - .map((w) => widget[w](hyprlandMonitor)); - }); + return foundLayout.middle + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); - return ( - { - computeLayer.drop(); - leftBinding.drop(); - middleBinding.drop(); - rightBinding.drop(); - }} - > - - - {leftBinding()} - - } - centerWidget={ - - {middleBinding()} - - } - endWidget={ - - {rightBinding()} - - } - /> - - - ); - }; -})(); + return foundLayout.right + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + + return ( + { + computeLayer.drop(); + leftBinding.drop(); + middleBinding.drop(); + rightBinding.drop(); + }} + > + + + {leftBinding()} + + } + centerWidget={ + + {middleBinding()} + + } + endWidget={ + + {rightBinding()} + + } + /> + + + ); +}; diff --git a/src/components/bar/modules/updates/index.tsx b/src/components/bar/modules/updates/index.tsx index c2460bf..a367150 100644 --- a/src/components/bar/modules/updates/index.tsx +++ b/src/components/bar/modules/updates/index.tsx @@ -8,6 +8,8 @@ import { Astal } from 'astal/gtk3'; const { updateCommand, + updateTooltipCommand, + extendedTooltip, label, padZero, autoHide, @@ -21,6 +23,7 @@ const { } = options.bar.customModules.updates; const pendingUpdates: Variable = Variable('0'); +const pendingUpdatesTooltip: Variable = Variable(''); const postInputUpdater = Variable(true); const isVis = Variable(!autoHide.get()); @@ -29,15 +32,31 @@ const processUpdateCount = (updateCount: string): string => { return `${updateCount.padStart(2, '0')}`; }; +const processUpdateTooltip = (updateTooltip: string, updateCount: Variable): string => { + const defaultTooltip = updateCount.get() + ' updates available'; + if (!extendedTooltip.get()) return defaultTooltip; + return defaultTooltip + '\n\n' + updateTooltip; +}; + const updatesPoller = new BashPoller( pendingUpdates, - [bind(padZero), bind(postInputUpdater)], + [bind(padZero), bind(postInputUpdater), bind(updateCommand)], bind(pollingInterval), updateCommand.get(), processUpdateCount, ); +const tooltipPoller = new BashPoller]>( + pendingUpdatesTooltip, + [bind(extendedTooltip), bind(postInputUpdater), bind(updateTooltipCommand)], + bind(pollingInterval), + updateTooltipCommand.get(), + processUpdateTooltip, + pendingUpdates, +); + updatesPoller.initialize('updates'); +tooltipPoller.initialize('updates'); Variable.derive([bind(autoHide)], (autoHideModule) => { isVis.set(!autoHideModule || (autoHideModule && parseFloat(pendingUpdates.get()) > 0)); @@ -54,7 +73,7 @@ const updatesIcon = Variable.derive( export const Updates = (): BarBoxChild => { const updatesModule = Module({ textIcon: updatesIcon(), - tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`), + tooltipText: bind(pendingUpdatesTooltip), boxClass: 'updates', isVis: isVis, label: bind(pendingUpdates), diff --git a/src/components/bar/modules/workspaces/helpers/utils.ts b/src/components/bar/modules/workspaces/helpers/utils.ts index d861198..4e80535 100644 --- a/src/components/bar/modules/workspaces/helpers/utils.ts +++ b/src/components/bar/modules/workspaces/helpers/utils.ts @@ -130,7 +130,7 @@ export const getAppIcon = ( } const findIconForClient = (clientClass: string, clientTitle: string): string | undefined => { - const appIconMap = { ...defaultApplicationIconMap, ...userDefinedIconMap }; + const appIconMap = { ...userDefinedIconMap, ...defaultApplicationIconMap }; const iconEntry = Object.entries(appIconMap).find(([matcher]) => { if (matcher.startsWith('class:')) { diff --git a/src/components/bar/settings/config.tsx b/src/components/bar/settings/config.tsx index 363255a..70e2802 100644 --- a/src/components/bar/settings/config.tsx +++ b/src/components/bar/settings/config.tsx @@ -237,6 +237,17 @@ export const CustomModuleSettings = (): JSX.Element => { title="Check Updates Command" type="string" /> +