feat: 上传我的配置

This commit is contained in:
2026-02-06 17:17:48 +08:00
parent 5ae578a012
commit ac517ad717
126 changed files with 15159 additions and 0 deletions

105
.config/waybar/colors.css Executable file
View File

@@ -0,0 +1,105 @@
/*
* Css Colors
* Generated with Matugen
*/
@define-color background #15130b;
@define-color error #ffb4ab;
@define-color error_container #93000a;
@define-color inverse_on_surface #333027;
@define-color inverse_primary #6e5e0e;
@define-color inverse_surface #e9e2d4;
@define-color on_background #e9e2d4;
@define-color on_error #690005;
@define-color on_error_container #ffdad6;
@define-color on_primary #3a3000;
@define-color on_primary_container #f9e287;
@define-color on_primary_fixed #221b00;
@define-color on_primary_fixed_variant #534600;
@define-color on_secondary #373016;
@define-color on_secondary_container #eee2bc;
@define-color on_secondary_fixed #211b04;
@define-color on_secondary_fixed_variant #4e472a;
@define-color on_surface #e9e2d4;
@define-color on_surface_variant #cdc6b4;
@define-color on_tertiary #153722;
@define-color on_tertiary_container #c5ecce;
@define-color on_tertiary_fixed #00210f;
@define-color on_tertiary_fixed_variant #2c4e38;
@define-color outline #969080;
@define-color outline_variant #4b4739;
@define-color primary #dcc66e;
@define-color primary_container #534600;
@define-color primary_fixed #f9e287;
@define-color primary_fixed_dim #dcc66e;
@define-color scrim #000000;
@define-color secondary #d1c6a1;
@define-color secondary_container #4e472a;
@define-color secondary_fixed #eee2bc;
@define-color secondary_fixed_dim #d1c6a1;
@define-color shadow #000000;
@define-color source_color #dabc2c;
@define-color surface #15130b;
@define-color surface_bright #3c3930;
@define-color surface_container #221f17;
@define-color surface_container_high #2d2a21;
@define-color surface_container_highest #38352b;
@define-color surface_container_low #1e1b13;
@define-color surface_container_lowest #100e07;
@define-color surface_dim #15130b;
@define-color surface_tint #dcc66e;
@define-color surface_variant #4b4739;
@define-color tertiary #aad0b3;
@define-color tertiary_container #2c4e38;
@define-color tertiary_fixed #c5ecce;
@define-color tertiary_fixed_dim #aad0b3;

65
.config/waybar/config.jsonc Executable file
View File

@@ -0,0 +1,65 @@
{
//这个waybar因为大改过一次布局所以箭头的位置和名字已经完全对不上了从左到右看吧。
"include": [
"modules.jsonc",
"modules-dividers.jsonc"
],
//这一行layer top设置会让waybar显示在最上方
"layer": "top",
//"output": "DP-2",
"position": "top",
"fixed-center": true,
// "height": 30,
"reload_style_on_change": true,
"modules-left": [
// "ext/workspaces",
"niri/workspaces",
"custom/right_div#5",
"cffi/niri-taskbar",
"niri/window",
//"dwl/window",
// "hyprland/window",
"custom/right_div#6"
],
"modules-center": [
"custom/left_div#3",
"bluetooth",
"network",
"custom/wfrec",
// "custom/screenshot",
"custom/left_div#2",
// "idle_inhibitor",
"custom/colorpicker",
"power-profiles-daemon",
"custom/left_div#11",
"custom/left_div#1",
"custom/applauncher",
"custom/right_div#1",
"custom/right_div#11",
"clock",
"custom/right_div#2",
//"clock#date",
"custom/cava",
// "mpris",
"custom/right_div#3"
//"custom/right_div#4",
],
"modules-right": [
// "group/backlight",
"custom/left_div#7",
"custom/updates",
"tray",
// "custom/mako",
"custom/left_div#4",
//"backlight/slider",
//"backlight",
// "group/ddcutil",
"group/screenlight",
"privacy",
"group/audio",
"custom/left_div#8",
"battery",
"custom/left_div#5",
"group/powermenu"
]
}

BIN
.config/waybar/logo/bluetooth.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,88 @@
{
/*-------------------
left dividers
-------------------*/
"custom/left_div#1": {
"format": "",
"tooltip": false
},
"custom/left_div#11": {
"format": "",
"tooltip": false
},
"custom/left_div#2": {
"format": "",
"tooltip": false
},
"custom/left_div#3": {
"format": "",
"tooltip": false
},
"custom/left_div#4": {
"format": "",
"tooltip": false
},
"custom/left_div#5": {
"format": "",
"tooltip": false
},
"custom/left_div#6": {
"format": "",
"tooltip": false
},
"custom/left_div#7": {
"format": "",
"tooltip": false
},
"custom/left_div#8": {
"format": "",
"tooltip": false
},
"custom/left_div#9": {
"format": "",
"tooltip": false
},
"custom/left_inv#1": {
"format": "",
"tooltip": false
},
"custom/left_inv#2": {
"format": "",
"tooltip": false
},
/*--------------------
right dividers
--------------------*/
"custom/right_div#1": {
"format": "",
"tooltip": false
},
"custom/right_div#11": {
"format": "",
"tooltip": false
},
"custom/right_div#2": {
"format": "",
"tooltip": false
},
"custom/right_div#3": {
"format": "",
"tooltip": false
},
"custom/right_div#4": {
"format": "",
"tooltip": false
},
"custom/right_div#5": {
"format": "",
"tooltip": false
},
"custom/right_div#6": {
"format": "",
"tooltip": false
},
"custom/right_inv#1": {
"format": "",
"tooltip": false
}
}

422
.config/waybar/modules.jsonc Executable file
View File

@@ -0,0 +1,422 @@
{
//电池电量
"battery": {
"interval": 60,
"states": {
"critical": 20,
},
"format": "{icon} {capacity}%",
"format-charging": "󰂄 {capacity}% ",
"format-icons": [
"󰂎",
"󰁺",
"󰁻",
"󰁼",
"󰁽",
"󰁾",
"󰁿",
"󰂀",
"󰂂",
"󰁹"
],
"tooltip-format": "剩余电量:{capacity}%\n充 放 电 {power}\n可用时间{timeTo}\n---\n电池健康度{health}%",
},
//时钟
"clock": {
"interval": 60,
"format": "󰥔 {:%H:%M}",
"tooltip-format": "{:%Y/%m/%d %A}\n---\n左键打开时钟\n右键打开日历",
"max-length": 25,
"on-click": "gnome-clocks",
"on-click-right": "gnome-calendar",
},
"clock#date": {
// "interval":
"format": "󰸗 {:%m-%d}",
// "timezone":
// "timezones":
// "locale":
// "rotate":
// "on-click":
// "on-click-middle":
// "on-click-right":
// "on-scroll-up":
// "on-scroll-down":
// "smooth-scrolling-threshold":
// "tooltip":
"on-click": "gnome-calendar",
"tooltip-format": "{:%Y/%m/%d %A}",
},
//正在运行的软件
"tray": {
"icon-size": 22,
"spacing": 7,
// 自定义图标
//"icons": {
// "blueman": "/home/shorin/.config/waybar/logo/bluetooth.png",
//},
},
//工作区和窗口名字
"niri/workspaces": {
"format": "{icon}",
"format-icons": {
"active": "󰜋",
"default": "",
},
},
"niri/window": {
"format": "{}",
"rewrite": {
"(.*) - Mozilla Firefox": "🌎 $1",
"(.*) - zsh": "> [$1]",
},
},
//网络和蓝牙
"group/network-bluetooth": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false,
},
"modules": [
"network",
"bluetooth"
],
},
"network": {
"format-disconnected": "{icon}",
"format-wifi": "{icon}",
"format-ethernet": "{icon}",
"format-icons": {
"disconnected": "󰤮",
"wifi": [
"󰤯",
"󰤟",
"󰤢",
"󰤥",
"󰤨"
],
"ethernet": "󰈀",
},
//鼠标悬浮在wifi图标上会显示wifi名称、信号强度、ip地址、设备名
"tooltip-format-wifi": "{essid} ({signalStrength}%)\n{ifname} : {ipaddr}\n---\n左键打开面板\n右键打开高级网络配置工具",
"tooltip-format-ethernet": "{ifname} : {ipaddr}\n---\n左键打开面板\n右键打开高级网络配置工具",
"tooltip-format-disconnected": "网络未连接\n---\n左键打开面板\n右键打开高级网络配置工具",
"on-click": "kitty --class nmtui -e nmtui",
"on-click-right": "nm-connection-editor",
},
"bluetooth": {
//蓝牙关闭状态下的图标和鼠标悬浮时的提示
"format-disabled": "󰂲",
"tooltip-format-disabled": "左键:启用蓝牙",
"format": "󰂯",
"tooltip-format-on": "左键:禁用蓝牙\n右键打开面板",
//连接状态下的图标和提示
"format-connected": "󰂱",
"tooltip-format-connected": "󰂱 {:device_alias}\n---\n右键打开面板",
//点击功能
"on-click": "~/.config/waybar/scripts/toggle-bluetooth.sh",
"on-click-right": "kitty --class bluetui -e bluetui",
},
//左键循环切换性能模式
"power-profiles-daemon": {
"format": "{icon}",
"tooltip-format": "当前性能模式:{profile}\n---\n左键切换性能模式",
"tooltip": true,
"format-icons": {
"performance": "󱐋",
"balanced": "",
"power-saver": "",
},
},
//截图
"custom/screenshot": {
"format": "",
"tooltip-format": "左键:快速截图(仅保存到剪贴板)\n右键打开截图菜单\n中键打开长截图菜单",
"on-click": "~/.config/waybar/scripts/screenshot.sh",
"on-click-right": "~/.config/waybar/scripts/power-screenshot.sh",
"on-click-middle": "~/.config/waybar/scripts/longshot-sh/longshot.sh"
},
//wf-recorder
"custom/wfrec": {
"exec": "~/.config/waybar/scripts/wf-recorder.sh status-json",
"return-type": "json",
//"interval": 1, // 每秒刷新一次
"signal": 9, // 与脚本里的 WAYBAR_SIG 对应
"tooltip": true,
"on-click": "~/.config/waybar/scripts/wf-recorder.sh toggle", // 左键:开始/停止
"on-click-right": "~/.config/waybar/scripts/wf-recorder.sh stop"
},
//声音模块
"group/audio": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false,
},
"modules": [
"pulseaudio",
"pulseaudio/slider"
],
},
"pulseaudio": {
"tooltip-format": "左键:静音\n右键关闭麦克风\n中键打开面板",
"format": "{icon} {format_source} ",
"format-bluetooth": " {format_source} ",
"format-source": "{volume}%",
"format-source-muted": "",
"format-muted": " {format_source} ",
"format-icons": {
"headphone": "",
"bluetooth": "",
// "speaker":"󰓃"
"speaker": [
"",
"",
""
],
},
"on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle",
"on-click-middle": "pavucontrol --tab=3",
"on-scroll-up": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+",
"on-scroll-down": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-",
},
"pulseaudio/slider": {
"min": 0,
"max": 100,
"orientation": "horizontal",
},
//多媒体
"mpris": {
"format": "{player_icon}",
"format-paused": "{status_icon}",
"player-icons": {
"default": "▶",
},
"status-icons": {
"paused": "⏸",
},
"toooltip": "{dynamic}",
"enable-tooltip-len-limits": true,
},
//logo+程序启动器
"custom/applauncher": {
"tooltip-format": "左键:软件启动菜单\n右键切换壁纸",
"format": "󰣇",
"on-click": "fuzzel",
"on-click-right": "waypaper",
// "on-click-middle": "~/.config/scripts/niri_auto_blur_bg.sh || pkill -f niri_auto_blur_bg.sh"
},
//电源菜单
"group/powermenu": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false,
},
"modules": [
"custom/wlogout",
"custom/reboot",
"custom/logout",
"custom/lockscreen",
],
},
"custom/wlogout": {
"tooltip": false,
"format": "󰐥",
"on-click": "systemctl poweroff",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/lockscreen": {
"tooltip": false,
"format": "",
"on-click": "swaylock || hyprlock",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/reboot": {
"tooltip": false,
"format": "",
"on-click": "systemctl reboot",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/logout": {
"format": "󰈆",
"on-click": "niri msg action quit",
"tooltip": false,
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"group/screenlight": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false,
},
"modules": [
"backlight",
"backlight/slider",
],
},
//内屏亮度
"backlight/slider": {
"min": 5,
"max": 100,
"orientation": "horizontal",
},
"backlight": {
"tooltip": true,
"tooltip-format":"调节内屏亮度",
"format": "󰃠"
},
//调节外接屏幕亮度
"group/ddcutil": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false,
},
"modules": [
"custom/ddcutil-day",
"backlight/slider",
"backlight",
"custom/separator#1",
"custom/ddcutil-sleep",
"custom/ddcutil-night",
],
},
"custom/ddcutil-day": {
"tooltip-format": "左键100%外接屏幕亮度\n右键切换护眼模式",
"format": "󰃠",
"on-click": "ddcutil --display 1 setvcp 10 100 ",
"on-click-right": "~/.local/bin/toggle-wlsunset",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/ddcutil-night": {
"tooltip-format": "左键65%外接屏幕亮度",
"format": "󰃟",
"on-click": "ddcutil --display 1 setvcp 10 65 ",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/ddcutil-sleep": {
"tooltip-format": "左键5%外接屏幕亮度",
"format": "󰃞",
"on-click": "ddcutil --display 1 setvcp 10 5 ",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
//swaync通知模块
"custom/swaync": {
"tooltip": false,
"format": "{icon}",
"format-icons": {
"notification": "<span foreground='red'><sup></sup></span>",
"none": "",
"dnd-notification": "<span foreground='red'><sup></sup></span>",
"dnd-none": "",
"inhibited-notification": "<span foreground='red'><sup></sup></span>",
"inhibited-none": "",
"dnd-inhibited-notification": "<span foreground='red'><sup></sup></span>",
"dnd-inhibited-none": "",
},
"return-type": "json",
"exec-if": "which swaync-client",
"exec": "swaync-client -swb",
"on-click": "swaync-client -t -sw",
"on-click-right": "swaync-client -d -sw",
"escape": true,
},
"custom/mako": {
"format": "",
"on-click": "makoctl restore",
},
//gnome-control-center 
"custom/settings": {
"format": "",
"on-click": "env XDG_CURRENT_DESKTOP=GNOME gnome-control-center",
"tooltip-format": "Open control center",
},
//获取屏幕颜色colorpicker
"custom/colorpicker": {
"tooltip": true,
"format": "󱏜",
"on-click": "hyprpicker | wl-copy",
"tooltip-format":"左键:提取颜色",
},
//音频可视化
"custom/cava": {
"tooltip": false,
"format": "{}",
"exec": "~/.config/waybar/scripts/cava.sh",
},
//禁止熄屏
"idle_inhibitor": {
"format": "{icon}",
"format-icons": {
"activated": "",
"deactivated": "",
},
"tooltip-format-activated": "自动熄屏已禁止",
"tooltip-format-deactivated": "自动熄屏已开启",
"on-click-right": "~/.config/scripts/matugen-select-type.sh"
},
//分隔符󰇝 󱋱
"custom/separator#1": {
"format": "󱋱",
"tooltip": false,
},
//archupdater
"custom/updates": {
"format": "{}{icon}",
"return-type": "json",
"format-icons": {
"has-updates": "",
"updated": ""
},
"exec": "~/.config/waybar/scripts/check-updates.sh",
"interval": 3600,
"on-click": "kitty -e paru",
},
//隐私
"privacy": {
"icon-spacing": 10,
"icon-size": 16,
"transition-duration": 250,
"modules": [
{
"type": "screenshare",
"tooltip": true,
"tooltip-icon-size": 24,
},
],
},
"cffi/niri-taskbar": {
// module_path
"module_path": "/usr/lib/waybar/libniri_taskbar.so",
"apps": {
"signal": [
{
"match": "\\([0-9]+\\)$",
"class": "unread",
},
],
},
},
// dwl and mangowc
"ext/workspaces": {
"format": "{icon}",
//"format-icons": {
// "active": "󰜋",
// "default": ""
// },
"ignore-hidden": true,
"on-click": "activate",
"on-click-right": "deactivate",
"sort-by-id": true,
},
"dwl/window": {
"format": "[{layout}]{title}",
}
}

356
.config/waybar/modules.jsonc.bak Executable file
View File

@@ -0,0 +1,356 @@
{
// 电池电量
"battery": {
"interval": 60,
"states": {
"critical": 20
},
"format": "{icon} {capacity}%",
"format-charging": "󰂄 {capacity}% ",
"format-icons": [
"󰂎",
"󰁺",
"󰁻",
"󰁼",
"󰁽",
"󰁾",
"󰁿",
"󰂀",
"󰂂",
"󰁹"
],
"tooltip-format": "剩余电量:{capacity}%\n充 放 电 {power}\n可用时间{timeTo}\n---\n电池健康度{health}%"
},
// 时钟
"clock": {
"interval": 60,
"format": "󰥔 {:%H:%M}",
"tooltip-format": "{:%Y/%m/%d %A}\n---\n左键打开时钟\n右键打开日历",
"max-length": 25,
"on-click": "gnome-clocks",
"on-click-right": "gnome-calendar"
},
// 日期小部件(点击打开日历)
"clock#date": {
"format": "󰸗 {:%m-%d}",
"on-click": "gnome-calendar",
"tooltip-format": "{:%Y/%m/%d %A}"
},
// 系统托盘(蓝牙、输入法等图标)
"tray": {
"icon-size": 22,
"spacing": 7
},
// niri 工作区指示器
"niri/workspaces": {
"format": "{icon}",
"format-icons": {
"active": "󰜋",
"default": ""
}
},
// 当前窗口标题美化
"niri/window": {
"format": "{}",
"rewrite": {
"(.*) - Mozilla Firefox": "🌎 $1",
"(.*) - zsh": "> [$1]"
}
},
// 网络 & 蓝牙组合抽屉
"group/network-bluetooth": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false
},
"modules": [
"network",
"bluetooth"
]
},
// 网络状态
"network": {
"format-disconnected": "{icon}",
"format-wifi": "{icon}",
"format-ethernet": "{icon}",
"format-icons": {
"disconnected": "󰤮",
"wifi": [
"󰤯",
"󰤟",
"󰤢",
"󰤥",
"󰤨"
],
"ethernet": "󰈀"
},
"tooltip-format-wifi": "{essid} ({signalStrength}%)\n{ifname} : {ipaddr}\n---\n左键打开面板\n右键打开高级网络配置工具",
"tooltip-format-ethernet": "{ifname} : {ipaddr}\n---\n左键打开面板\n右键打开高级网络配置工具",
"tooltip-format-disconnected": "网络未连接\n---\n左键打开面板\n右键打开高级网络配置工具",
"on-click": "kitty --class nmtui -e nmtui",
"on-click-right": "nm-connection-editor"
},
// 蓝牙控制
"bluetooth": {
"format-disabled": "󰂲",
"tooltip-format-disabled": "左键:启用蓝牙",
"format": "󰂯",
"tooltip-format-on": "左键:禁用蓝牙\n右键打开面板",
"format-connected": "󰂱",
"tooltip-format-connected": "󰂱 {:device_alias}\n---\n右键打开面板",
"on-click": "~/.config/waybar/scripts/toggle-bluetooth.sh",
"on-click-right": "kitty --class bluetui -e bluetui"
},
// 性能模式切换(左键循环)
"power-profiles-daemon": {
"format": "{icon}",
"tooltip-format": "当前性能模式:{profile}\n---\n左键切换性能模式",
"tooltip": true,
"format-icons": {
"performance": "󱐋",
"balanced": "",
"power-saver": ""
}
},
// 音频组(图标 + 滑块)
"group/audio": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false
},
"modules": [
"pulseaudio",
"pulseaudio/slider"
]
},
// 音频控制(支持滚动调节麦克风音量)
"pulseaudio": {
"tooltip-format": "左键:静音\n右键关闭麦克风\n中键打开面板",
"format": "{icon} {format_source} ",
"format-bluetooth": " {format_source} ",
"format-source": "{volume}%",
"format-source-muted": "",
"format-muted": " {format_source} ",
"format-icons": {
"headphone": "",
"bluetooth": "",
"speaker": [
"",
"",
""
]
},
"on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
"on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle",
"on-click-middle": "pavucontrol --tab=3",
"on-scroll-up": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+",
"on-scroll-down": "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-"
},
// 音量滑块(横向)
"pulseaudio/slider": {
"min": 0,
"max": 100,
"orientation": "horizontal"
},
// 多媒体播放器控制mpris
"mpris": {
"format": "{player_icon}",
"format-paused": "{status_icon}",
"player-icons": {
"default": "▶"
},
"status-icons": {
"paused": "⏸"
},
"tooltip": "{dynamic}",
"enable-tooltip-len-limits": true
},
// 应用启动器 + 壁纸切换
"custom/applauncher": {
"tooltip-format": "左键:软件启动菜单\n右键切换壁纸",
"format": "󰣇",
"on-click": "fuzzel",
"on-click-right": "waypaper"
},
// 电源菜单组(登出/重启/锁屏)
"group/powermenu": {
"orientation": "inherit",
"drawer": {
"transition-left-to-right": false
},
"modules": [
"custom/wlogout",
"custom/reboot",
"custom/logout",
"custom/lockscreen"
]
},
"custom/wlogout": {
"tooltip": false,
"format": "󰐥",
"on-click": "wlogout",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/lockscreen": {
"tooltip": false,
"format": "",
"on-click": "swaylock || hyprlock",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/reboot": {
"tooltip": false,
"format": "",
"on-click": "systemctl reboot",
"on-scroll-up": "true",
"on-scroll-down": "true"
},
"custom/logout": {
"format": "󰈆",
"on-click": "niri msg action quit",
"tooltip": false,
"on-scroll-up": "true",
"on-scroll-down": "true"
},
// ✅ 内屏亮度控制模块(已优化,不依赖外屏)
// 使用 backlight + slider 组合,适配大多数笔记本/内置显示器
"backlight": {
"tooltip": true,
"tooltip-format": "调节内屏亮度",
"format": "{icon}",
"format-icons": [
"", // 0-10%
"", // 10-20%
"", // 20-30%
"", // 30-40%
"", // 40-50%
"", // 50-60%
"", // 60-70%
"", // 70-80%
"" // 80-100%
]
},
"backlight/slider": {
"min": 5, // 最低亮度设为5避免完全黑屏
"max": 100,
"orientation": "horizontal"
},
// 通知中心swaync
"custom/swaync": {
"tooltip": false,
"format": "{icon}",
"format-icons": {
"notification": "<span foreground='red'><sup></sup></span>",
"none": "",
"dnd-notification": "<span foreground='red'><sup></sup></span>",
"dnd-none": "",
"inhibited-notification": "<span foreground='red'><sup></sup></span>",
"inhibited-none": "",
"dnd-inhibited-notification": "<span foreground='red'><sup></sup></span>",
"dnd-inhibited-none": ""
},
"return-type": "json",
"exec-if": "which swaync-client",
"exec": "swaync-client -swb",
"on-click": "swaync-client -t -sw",
"on-click-right": "swaync-client -d -sw",
"escape": true
},
// 备用通知方案mako
"custom/mako": {
"format": "",
"on-click": "makoctl restore"
},
// 系统设置面板
"custom/settings": {
"format": "",
"on-click": "env XDG_CURRENT_DESKTOP=GNOME gnome-control-center",
"tooltip-format": "打开系统设置"
},
// 自动更新检查器Arch Linux
"custom/updates": {
"format": "{}{icon}",
"return-type": "json",
"format-icons": {
"has-updates": "",
"updated": ""
},
"exec": "~/.config/waybar/scripts/check-updates.sh",
"interval": 3600,
"on-click": "kitty -e sysup"
},
// 隐私指示器(如屏幕共享)
"privacy": {
"icon-spacing": 10,
"icon-size": 16,
"transition-duration": 250,
"modules": [
{
"type": "screenshare",
"tooltip": true,
"tooltip-icon-size": 24
}
]
},
// niri 任务栏扩展
"cffi/niri-taskbar": {
"module_path": "/usr/lib/waybar/libniri_taskbar.so",
"apps": {
"signal": [
{
"match": "\\([0-9]+\\)$",
"class": "unread"
}
]
}
},
// 兼容 dwl / mangowc 的工作区模块(备用)
"ext/workspaces": {
"format": "{icon}",
"ignore-hidden": true,
"on-click": "activate",
"on-click-right": "deactivate",
"sort-by-id": true
},
// dwl 窗口标题显示
"dwl/window": {
"format": "[{layout}]{title}"
},
// 分隔符
"custom/separator#1": {
"format": "󱋱",
"tooltip": false
}
}

65
.config/waybar/scripts/cava.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# 配置
CHARS="▁▂▃▄▅▆▇█"
BARS=10
CONF="/tmp/waybar_cava_config"
# 初始化
len=$((${#CHARS}-1))
idle_char="${CHARS:0:1}"
idle_output=$(printf "%0.s$idle_char" $(seq 1 $BARS))
# 生成 Cava 配置
cat > "$CONF" <<EOF
[general]
bars = $BARS
[input]
method = pulse
source = auto
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
ascii_max_range = $len
EOF
cleanup() {
trap - EXIT INT TERM
pkill -P $$ 2>/dev/null
echo "$idle_output"
exit 0
}
trap cleanup EXIT INT TERM
# 核心检测:是否存在未暂停的音频流
is_audio_active() {
pactl list sink-inputs 2>/dev/null | grep -q "Corked: no"
}
# 初始状态
echo "$idle_output"
while true; do
# 如果存在未静音的音频
if is_audio_active; then
if ! pgrep -P $$ -x cava >/dev/null; then
# 这里的 sed 字典是根据你的 CHARS 动态生成的
sed_dict="s/;//g;"
for ((i=0; i<=${len}; i++)); do
sed_dict="${sed_dict}s/$i/${CHARS:$i:1}/g;"
done
cava -p "$CONF" 2>/dev/null | sed -u "$sed_dict" &
fi
# 正在播放时,稍微降低检查频率减少 CPU 占用
sleep 1
else
if pgrep -P $$ -x cava >/dev/null; then
pkill -P $$ -x cava 2>/dev/null
wait 2>/dev/null
echo "$idle_output"
fi
# 没声音时,使用 subscribe 等待事件,被动唤醒,不产生任何循环开销
timeout 5s pactl subscribe 2>/dev/null | grep --line-buffered "sink-input" | head -n 1 >/dev/null
fi
done

View File

@@ -0,0 +1,60 @@
#!/bin/bash
# === 配置部分 ===
CACHE_FILE="$HOME/.cache/waybar-updates.json"
# === 函数定义 ===
generate_json() {
local updates=$1
local count
if [ -z "$updates" ]; then
count=0
else
count=$(echo "$updates" | wc -l)
fi
if [ "$count" -gt 0 ]; then
local tooltip=$(echo "$updates" | awk '{printf "%s\\n", $0}' | sed 's/"/\\"/g' | head -c -2)
printf '{"text": "%s", "alt": "has-updates", "tooltip": "%s"}\n' "$count" "$tooltip"
else
printf '{"text": "", "alt": "updated", "tooltip": "System is up to date"}\n'
fi
}
# === 主逻辑 ===
# 尝试获取更新
# 捕获输出
NEW_UPDATES=$(checkupdates 2>/dev/null)
STATUS=$?
# === 关键修正 ===
# checkupdates 退出代码说明:
# 0 = 有更新
# 2 = 无更新 (这是正常情况,不是错误!)
# 1 = 发生错误 (如网络断开、锁被占用)
if [ $STATUS -eq 0 ]; then
# --- 情况A发现更新 ---
OUTPUT=$(generate_json "$NEW_UPDATES")
echo "$OUTPUT" > "$CACHE_FILE"
echo "$OUTPUT"
elif [ $STATUS -eq 2 ]; then
# --- 情况B正常运行但没有更新 ---
# 必须清空缓存或者写入 0 状态,而不是读取旧缓存
OUTPUT=$(generate_json "")
echo "$OUTPUT" > "$CACHE_FILE"
echo "$OUTPUT"
else
# --- 情况C真的出错了 (Exit 1) ---
# 比如没网,或者 pacman 锁死
# 只有这种时候才读取旧缓存来保底
if [ -f "$CACHE_FILE" ]; then
cat "$CACHE_FILE"
else
printf '{"text": "?", "alt": "updated", "tooltip": "Check failed"}\n'
fi
fi

View File

@@ -0,0 +1,134 @@
#!/bin/bash
# =================语言=================
if env | grep -q "zh_CN"; then
STR_NEXT="📸 截取下一张 (仅需定高度)"
STR_FINISH="💾 完成并处理"
STR_ABORT="❌ 放弃"
STR_ERR="错误"
STR_SAVED="已保存"
else
STR_NEXT="📸 Capture Next (Height only)"
STR_FINISH="💾 Finish"
STR_ABORT="❌ Abort"
STR_ERR="Error"
STR_SAVED="Saved"
fi
# =================配置=================
CONFIG_DIR="$HOME/.cache/longshot-sh"
CONFIG_FILE="$CONFIG_DIR/mode"
SAVE_DIR="$HOME/Pictures/Screenshots/longshots"
TMP_DIR="/tmp/longshot_grim_$(date +%s)"
FILENAME="longshot_$(date +%Y%m%d_%H%M%S).png"
RESULT_PATH="$SAVE_DIR/$FILENAME"
TMP_STITCHED="$TMP_DIR/stitched.png"
mkdir -p "$SAVE_DIR" "$TMP_DIR"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT SIGINT SIGTERM
# 菜单工具
# [修改]: 将 fuzzel 宽度从 45 调整为 30使其更紧凑
CMD_FUZZEL="fuzzel -d --anchor=top --y-margin=20 --lines=3 --width=30"
CMD_WOFI="wofi --dmenu --lines 3"
CMD_ROFI="rofi -dmenu -l 3"
if command -v fuzzel &> /dev/null; then MENU_CMD="$CMD_FUZZEL"
elif command -v wofi &> /dev/null; then MENU_CMD="$CMD_WOFI"
elif command -v rofi &> /dev/null; then MENU_CMD="$CMD_ROFI"
else exit 1; fi
# [新增]: 动态计算宽度函数 (主要针对 wofi)
function get_dynamic_width() {
local text="$1"
# 获取最长行的长度
local max_len=$(echo -e "$text" | wc -L)
# 计算: 字符数 * 28px + 60px 边距 (可根据屏幕分辨率微调)
echo $(( max_len * 28 + 60 ))
}
# [修改]: 增加对 wofi 的动态宽度支持
function show_menu() {
local content="$1"
if [[ "$MENU_CMD" == *"wofi"* ]]; then
# 如果是 wofi计算宽度并附加参数
local width=$(get_dynamic_width "$content")
echo -e "$content" | $MENU_CMD --width "$width"
else
# 其他工具 (fuzzel/rofi) 保持原样
echo -e "$content" | $MENU_CMD
fi
}
# ======================================
# Step 1: 第一张截图 (直接开始,不询问)
# ======================================
# 用户在主菜单点击了 "选择区域",所以这里直接 Slurp
GEO_1=$(slurp)
if [ -z "$GEO_1" ]; then exit 0; fi
IFS=', x' read -r FIX_X FIX_Y FIX_W FIX_H <<< "$GEO_1"
grim -g "$GEO_1" "$TMP_DIR/001.png"
# ======================================
# Step 2: 循环截图
# ======================================
INDEX=2
DO_SAVE=false
while true; do
# 菜单提示下一张 (show_menu 会自动处理宽度)
ACTION=$(show_menu "$STR_NEXT\n$STR_FINISH\n$STR_ABORT")
case "$ACTION" in
*"📸"*)
sleep 0.2
GEO_NEXT=$(slurp)
if [ -z "$GEO_NEXT" ]; then continue; fi
# 锁定宽度和X轴只取新高度
IFS=', x' read -r _TX NEW_Y _TW NEW_H <<< "$GEO_NEXT"
FINAL_GEO="${FIX_X},${NEW_Y} ${FIX_W}x${NEW_H}"
IMG_NAME="$(printf "%03d" $INDEX).png"
grim -g "$FINAL_GEO" "$TMP_DIR/$IMG_NAME"
((INDEX++))
;;
*"💾"*) DO_SAVE=true; break ;;
*"❌"*) exit 0 ;;
*) break ;; # 意外退出
esac
done
# ======================================
# Step 3: 处理与自动动作
# ======================================
COUNT=$(ls "$TMP_DIR"/*.png 2>/dev/null | wc -l)
if [ "$COUNT" -gt 0 ] && [ "$DO_SAVE" = true ]; then
# 拼接
magick "$TMP_DIR"/*.png -append "$TMP_STITCHED"
mv "$TMP_STITCHED" "$RESULT_PATH"
# 复制到剪贴板
if command -v wl-copy &> /dev/null; then wl-copy < "$RESULT_PATH"; fi
# 读取配置执行动作
FINAL_MODE=$(cat "$CONFIG_FILE" 2>/dev/null || echo "PREVIEW")
case "$FINAL_MODE" in
"PREVIEW")
imv "$RESULT_PATH"
;;
"EDIT")
if command -v satty &> /dev/null; then satty -f "$RESULT_PATH"
else imv "$RESULT_PATH"; fi
;;
"SAVE")
notify-send -i "$RESULT_PATH" "Longshot" "$STR_SAVED: $(basename "$RESULT_PATH")"
;;
esac
fi

View File

@@ -0,0 +1,80 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_PYTHON="$SCRIPT_DIR/venv/bin/python"
PY_STITCH="$SCRIPT_DIR/stitch.py"
CONFIG_DIR="$HOME/.cache/longshot-sh"
CONFIG_FILE="$CONFIG_DIR/mode"
SAVE_DIR="$HOME/Pictures/Screenshots/longshots"
mkdir -p "$SAVE_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TMP_VIDEO="/tmp/longshot_${TIMESTAMP}.mp4"
OUTPUT_IMG="${SAVE_DIR}/longshot_${TIMESTAMP}.png"
# 语言
if env | grep -q "zh_CN"; then
TXT_REC="录制中"
TXT_MSG="正在拼接..."
TXT_SAVED="已保存并复制"
WIDTH=6
else
TXT_REC="Recording"
TXT_MSG="Stitching..."
TXT_SAVED="Saved"
WIDTH=10
fi
# 录制菜单
if command -v fuzzel &> /dev/null; then
MENU_REC_CMD="fuzzel -d --anchor top --y-margin 20 --width $WIDTH --lines 0"
elif command -v wofi &> /dev/null; then
MENU_REC_CMD="wofi -d -i -p Rec"
else
MENU_REC_CMD="rofi -dmenu -p Rec"
fi
# Step 1: 选区
GEOMETRY=$(slurp)
if [ -z "$GEOMETRY" ]; then exit 1; fi
# Step 2: 录制
wf-recorder -g "$GEOMETRY" -f "$TMP_VIDEO" \
-c libx264 -p crf=0 -p preset=ultrafast -p pixel_format=yuv420p \
&> /dev/null &
REC_PID=$!
sleep 0.5
if ! kill -0 $REC_PID 2>/dev/null; then
notify-send "Error" "wf-recorder failed"
exit 1
fi
# Step 3: 停止菜单
echo "Stop" | $MENU_REC_CMD -p "$TXT_REC" > /dev/null
kill -SIGINT $REC_PID
wait $REC_PID 2>/dev/null
# Step 4: 处理
if [ -f "$TMP_VIDEO" ]; then
notify-send -t 2000 "Longshot" "$TXT_MSG"
"$VENV_PYTHON" "$PY_STITCH" "$TMP_VIDEO" "$OUTPUT_IMG"
rm -f "$TMP_VIDEO"
if [ -f "$OUTPUT_IMG" ]; then
if command -v wl-copy &> /dev/null; then wl-copy < "$OUTPUT_IMG"; fi
# 自动执行动作
FINAL_MODE=$(cat "$CONFIG_FILE" 2>/dev/null || echo "PREVIEW")
case "$FINAL_MODE" in
"PREVIEW") imv "$OUTPUT_IMG" ;;
"EDIT") if command -v satty &> /dev/null; then satty -f "$OUTPUT_IMG"; else imv "$OUTPUT_IMG"; fi ;;
"SAVE") notify-send "Longshot" "$TXT_SAVED: $(basename "$OUTPUT_IMG")" ;;
esac
fi
else
notify-send "Error" "No video"
fi

View File

@@ -0,0 +1,224 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_PYTHON="$SCRIPT_DIR/venv/bin/python"
# =================配置区=================
CONFIG_DIR="$HOME/.cache/longshot-sh"
mkdir -p "$CONFIG_DIR"
FILE_MODE="$CONFIG_DIR/mode" # PREVIEW / EDIT / SAVE
FILE_BACKEND="$CONFIG_DIR/backend" # WF / GRIM
# 初始化默认值
[ ! -f "$FILE_MODE" ] && echo "PREVIEW" > "$FILE_MODE"
[ ! -f "$FILE_BACKEND" ] && echo "WF" > "$FILE_BACKEND"
# =================语言资源=================
if env | grep -q "zh_CN"; then
TXT_TITLE_WF="缓慢滚动,回车停止"
TXT_TITLE_GRIM="记住截图末尾位置"
TXT_START="📷 选择截图区域"
TXT_SETTING="⚙️ 设置"
TXT_EXIT="❌ 退出"
TXT_BACK="🔙 返回主菜单"
TXT_SW_BACKEND="📹 切换后端"
TXT_SW_ACTION="🛠 切换动作"
TXT_PROMPT_ACTION="请选择截图后的动作:"
TXT_ST_WF="流式录制 (wf-recorder)"
TXT_ST_GRIM="分段截图 (grim)"
TXT_ST_PRE="预览 (imv)"
TXT_ST_EDIT="编辑 (satty)"
TXT_ST_SAVE="仅保存"
# 初始化提示
TXT_MSG_INIT="首次运行,正在初始化环境..."
TXT_MSG_SETUP_DONE="环境初始化完成!"
TXT_ERR_SETUP="环境安装失败,请查看 /tmp/longshot_setup.log"
TXT_ERR_NO_SETUP="未找到 setup.sh 文件"
# 新增:依赖缺失提示
TXT_ERR_DEP_TITLE="缺少系统依赖"
TXT_ERR_DEP_MSG="请安装以下包:"
else
TXT_TITLE_WF="Scroll Slowly, Enter to Stop"
TXT_TITLE_GRIM="Remember End Position"
TXT_START="📷 Select Area"
TXT_SETTING="⚙️ Settings"
TXT_EXIT="❌ Exit"
TXT_BACK="🔙 Back"
TXT_SW_BACKEND="📹 Switch Backend"
TXT_SW_ACTION="🛠 Switch Action"
TXT_PROMPT_ACTION="Select action after capture:"
TXT_ST_WF="Stream (wf-recorder)"
TXT_ST_GRIM="Manual (grim)"
TXT_ST_PRE="Preview"
TXT_ST_EDIT="Edit"
TXT_ST_SAVE="Save Only"
# Init messages
TXT_MSG_INIT="First run, initializing environment..."
TXT_MSG_SETUP_DONE="Environment initialized!"
TXT_ERR_SETUP="Setup failed, check /tmp/longshot_setup.log"
TXT_ERR_NO_SETUP="setup.sh not found"
# New: Dependency error
TXT_ERR_DEP_TITLE="Missing Dependencies"
TXT_ERR_DEP_MSG="Please install:"
fi
# ================= 1. 系统依赖检测 (新增) =================
# 检测核心工具: wf-recorder, grim, slurp, imagemagick (magick)
REQUIRED_TOOLS=("wf-recorder" "grim" "slurp" "magick" "wl-copy")
MISSING_TOOLS=()
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &> /dev/null; then
MISSING_TOOLS+=("$tool")
fi
done
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
# 构建错误信息
MSG="${TXT_ERR_DEP_MSG} ${MISSING_TOOLS[*]}"
# 尝试发送通知
if command -v notify-send &> /dev/null; then
notify-send -u critical "$TXT_ERR_DEP_TITLE" "$MSG"
else
# 如果连 notify-send 都没有,这就很尴尬了,尝试用 echo 输出到 stderr
echo "$TXT_ERR_DEP_TITLE: $MSG" >&2
fi
exit 1
fi
# ================= 2. Python 环境自动检测与修复 =================
if [ ! -f "$VENV_PYTHON" ]; then
notify-send -t 5000 "Longshot" "$TXT_MSG_INIT"
if [ -f "$SCRIPT_DIR/setup.sh" ]; then
chmod +x "$SCRIPT_DIR/setup.sh"
"$SCRIPT_DIR/setup.sh" > /tmp/longshot_setup.log 2>&1
if [ ! -f "$VENV_PYTHON" ]; then
notify-send -u critical "Error" "$TXT_ERR_SETUP"
exit 1
else
notify-send -t 3000 "Longshot" "$TXT_MSG_SETUP_DONE"
fi
else
notify-send -u critical "Error" "$TXT_ERR_NO_SETUP"
exit 1
fi
fi
# =================菜单工具=================
if command -v fuzzel &> /dev/null; then
MENU_CMD="fuzzel -d --anchor top --y-margin 20 --width 35 --lines 4"
elif command -v wofi &> /dev/null; then
MENU_CMD="wofi -d -i -p Longshot"
else
MENU_CMD="rofi -dmenu"
fi
# =================主循环=================
while true; do
# 1. 读取当前配置
CUR_MODE=$(cat "$FILE_MODE")
CUR_BACKEND=$(cat "$FILE_BACKEND")
# 2. 动态生成 UI 文本
CURRENT_TITLE=""
if [ "$CUR_BACKEND" == "WF" ]; then
CURRENT_TITLE="$TXT_TITLE_WF"
else
CURRENT_TITLE="$TXT_TITLE_GRIM"
fi
LBL_MODE=""
case "$CUR_MODE" in
"PREVIEW") LBL_MODE="$TXT_ST_PRE" ;;
"EDIT") LBL_MODE="$TXT_ST_EDIT" ;;
"SAVE") LBL_MODE="$TXT_ST_SAVE" ;;
esac
LBL_BACKEND=""
case "$CUR_BACKEND" in
"WF") LBL_BACKEND="$TXT_ST_WF" ;;
"GRIM") LBL_BACKEND="$TXT_ST_GRIM" ;;
esac
# 3. 显示主菜单
OPTION_START="$TXT_START"
OPTION_SETTING="$TXT_SETTING [$LBL_BACKEND | $LBL_MODE]"
OPTION_EXIT="$TXT_EXIT"
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
CHOICE=$(echo -e "$OPTION_START\n$OPTION_SETTING\n$OPTION_EXIT" | $MENU_CMD -p "$CURRENT_TITLE")
else
CHOICE=$(echo -e "$OPTION_START\n$OPTION_SETTING\n$OPTION_EXIT" | $MENU_CMD)
fi
# 4. 处理选择
if [[ "$CHOICE" == *"$TXT_START"* ]]; then
# === 启动后端 ===
if [ "$CUR_BACKEND" == "WF" ]; then
exec "$SCRIPT_DIR/longshot-wf-recorder.sh"
else
exec "$SCRIPT_DIR/longshot-grim.sh"
fi
break
elif [[ "$CHOICE" == *"$TXT_SETTING"* ]]; then
# === 设置菜单循环 ===
while true; do
S_MODE=$(cat "$FILE_MODE")
S_BACK=$(cat "$FILE_BACKEND")
D_BACK=""; [ "$S_BACK" == "WF" ] && D_BACK="$TXT_ST_WF" || D_BACK="$TXT_ST_GRIM"
D_MODE="";
case "$S_MODE" in
"PREVIEW") D_MODE="$TXT_ST_PRE" ;;
"EDIT") D_MODE="$TXT_ST_EDIT" ;;
"SAVE") D_MODE="$TXT_ST_SAVE" ;;
esac
ITEM_BACKEND="$TXT_SW_BACKEND [$D_BACK]"
ITEM_ACTION="$TXT_SW_ACTION [$D_MODE]"
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
S_CHOICE=$(echo -e "$TXT_BACK\n$ITEM_BACKEND\n$ITEM_ACTION" | $MENU_CMD -p "$TXT_SETTING")
else
S_CHOICE=$(echo -e "$TXT_BACK\n$ITEM_BACKEND\n$ITEM_ACTION" | $MENU_CMD)
fi
if [[ "$S_CHOICE" == *"$TXT_BACK"* ]]; then
break
elif [[ "$S_CHOICE" == *"$TXT_SW_BACKEND"* ]]; then
if [ "$S_BACK" == "WF" ]; then echo "GRIM" > "$FILE_BACKEND"; else echo "WF" > "$FILE_BACKEND"; fi
elif [[ "$S_CHOICE" == *"$TXT_SW_ACTION"* ]]; then
if [[ "$MENU_CMD" == *"fuzzel"* ]] || [[ "$MENU_CMD" == *"rofi"* ]]; then
A_CHOICE=$(echo -e "$TXT_ST_PRE\n$TXT_ST_EDIT\n$TXT_ST_SAVE" | $MENU_CMD -p "$TXT_PROMPT_ACTION")
else
A_CHOICE=$(echo -e "$TXT_ST_PRE\n$TXT_ST_EDIT\n$TXT_ST_SAVE" | $MENU_CMD)
fi
if [[ "$A_CHOICE" == *"$TXT_ST_PRE"* ]]; then echo "PREVIEW" > "$FILE_MODE"; fi
if [[ "$A_CHOICE" == *"$TXT_ST_EDIT"* ]]; then echo "EDIT" > "$FILE_MODE"; fi
if [[ "$A_CHOICE" == *"$TXT_ST_SAVE"* ]]; then echo "SAVE" > "$FILE_MODE"; fi
else
exit 0
fi
done
else
exit 0
fi
done

View File

@@ -0,0 +1,42 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$SCRIPT_DIR/venv"
echo "🔧 初始化环境..."
# 1. 检查系统工具
# 涵盖了两个方案所需的所有工具
REQUIRED_TOOLS=("wf-recorder" "grim" "slurp" "wl-copy" "magick" "python3")
MISSING_TOOLS=()
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &> /dev/null; then
MISSING_TOOLS+=("$tool")
fi
done
if [ ${#MISSING_TOOLS[@]} -ne 0 ]; then
echo "⚠️ 警告: 缺少以下系统工具,请使用包管理器安装:"
echo " ${MISSING_TOOLS[*]}"
echo " (例如 Arch: sudo pacman -S wf-recorder grim slurp wl-clipboard imagemagick python)"
echo " (例如 Debian/Ubuntu: sudo apt install wf-recorder grim slurp wl-clipboard imagemagick python3-venv)"
fi
# 2. Python 虚拟环境
if [ ! -d "$VENV_DIR" ]; then
echo "📦 创建 Python 虚拟环境..."
python3 -m venv "$VENV_DIR"
fi
# 3. 安装 Python 依赖
echo "⬇️ 安装 Python 库..."
"$VENV_DIR/bin/pip" install --upgrade pip > /dev/null
"$VENV_DIR/bin/pip" install opencv-python numpy > /dev/null
# 4. 赋予执行权限
chmod +x "$SCRIPT_DIR/longshot.sh"
chmod +x "$SCRIPT_DIR/longshot-wf-recorder.sh"
chmod +x "$SCRIPT_DIR/longshot-grim.sh"
chmod +x "$SCRIPT_DIR/stitch.py"
echo "✅ 完成!请运行 ./longshot.sh"

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import cv2
import numpy as np
import sys
import os
def stitch_video(video_path, output_path):
if not os.path.exists(video_path):
return
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("❌ 无法打开视频")
return
frames = []
ret, prev_frame = cap.read()
if not ret: return
frames.append(prev_frame)
anchor_frame = prev_frame.copy()
# ==========================
# 核心参数 (手动滚动优化)
# ==========================
MIN_SCROLL = 2
MATCH_CONFIDENCE = 0.5
# 忽略上下边缘 (防止浏览器地址栏/状态栏干扰)
IGNORE_Y_TOP = 0.15
IGNORE_Y_BOTTOM = 0.15
IGNORE_X = 0.15
h, w, _ = anchor_frame.shape
# 有效特征区
x1 = int(w * IGNORE_X)
x2 = int(w * (1 - IGNORE_X))
y1 = int(h * IGNORE_Y_TOP)
template_h = int(h * 0.2)
print(f"⚡ 正在分析 (梯度匹配模式)...")
last_shift = 0
SEARCH_WINDOW = 50
while True:
ret, curr_frame = cap.read()
if not ret: break
# 1. 梯度处理 (解决白底黑字问题)
curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
anchor_gray = cv2.cvtColor(anchor_frame, cv2.COLOR_BGR2GRAY)
curr_grad = cv2.Sobel(curr_gray, cv2.CV_8U, 0, 1, ksize=3)
anchor_grad = cv2.Sobel(anchor_gray, cv2.CV_8U, 0, 1, ksize=3)
# 2. 提取模板
template = curr_grad[y1 : y1 + template_h, x1:x2]
roi = anchor_grad[y1:, x1:x2]
# 3. 匹配
res = cv2.matchTemplate(roi, template, cv2.TM_CCOEFF_NORMED)
# 4. 惯性约束
if last_shift > 0:
mask = np.zeros_like(res)
target_y = last_shift
y_min = max(0, target_y - SEARCH_WINDOW)
y_max = min(res.shape[0], target_y + SEARCH_WINDOW)
mask[y_min:y_max, :] = 1
res = np.multiply(res, mask)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
shift = max_loc[1]
# 5. 判定
if max_val > MATCH_CONFIDENCE and shift > MIN_SCROLL and shift < (roi.shape[0] - 5):
new_content_start_y = h - shift
if new_content_start_y < h:
new_part = curr_frame[new_content_start_y:, :, :]
frames.append(new_part)
anchor_frame = curr_frame.copy()
if last_shift == 0: last_shift = shift
else: last_shift = int(last_shift * 0.6 + shift * 0.4)
cap.release()
if len(frames) > 1:
try:
full_image = np.vstack(frames)
cv2.imwrite(output_path, full_image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
print(f"🎉 处理完成")
except Exception as e:
print(f"❌ 保存失败: {e}")
else:
print("⚠️ 未检测到滚动,保存第一帧")
cv2.imwrite(output_path, frames[0])
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python stitch.py <input_video> <output_image>")
else:
stitch_video(sys.argv[1], sys.argv[2])

View File

@@ -0,0 +1,195 @@
#!/bin/bash
# ==============================================================================
# 1. 本地化与文案配置
# ==============================================================================
# 默认英文
STR_PROMPT="Longshot> "
STR_START="⛶ Start Selection (Width as baseline)"
STR_CANCEL="❌ Cancel"
STR_NEXT="📸 Capture Next (Height only)"
STR_SAVE="💾 Save & Finish"
STR_EDIT="🎨 Edit & Finish"
STR_ABORT="❌ Abort"
STR_NOTIFY_TITLE="Longshot"
STR_NOTIFY_SAVED="Saved to"
STR_NOTIFY_COPIED="Copied to clipboard"
STR_ERR_DEP="Missing dependency"
STR_ERR_MENU="Menu tool not found"
STR_ERR_TITLE="Error"
# 中文检测
if env | grep -q "zh_CN"; then
STR_PROMPT="长截图> "
STR_START="⛶ 开始框选(该图宽视为基准)"
STR_CANCEL="❌ 取消"
STR_NEXT="📸 截取下一张(只需确定高度)"
STR_SAVE="💾 完成并保存"
STR_EDIT="🎨 完成并编辑"
STR_ABORT="❌ 放弃并退出"
STR_NOTIFY_TITLE="长截图完成"
STR_NOTIFY_SAVED="已保存至"
STR_NOTIFY_COPIED="并已复制到剪贴板"
STR_ERR_DEP="缺少核心依赖"
STR_ERR_MENU="未找到菜单工具 (fuzzel/rofi/wofi)"
STR_ERR_TITLE="错误"
fi
# ==============================================================================
# 2. 用户配置与安全初始化
# ==============================================================================
SAVE_DIR="$HOME/Pictures/Screenshots/longshots"
TMP_BASE_NAME="niri_longshot"
TMP_DIR="/tmp/${TMP_BASE_NAME}_$(date +%s)"
FILENAME="longshot_$(date +%Y%m%d_%H%M%S).png"
RESULT_PATH="$SAVE_DIR/$FILENAME"
TMP_STITCHED="$TMP_DIR/stitched_temp.png"
# --- [保险措施 1] 启动时清理陈旧垃圾 ---
# 查找 /tmp 下名字包含 niri_longshot 且修改时间超过 10 分钟的目录并删除
# 这可以防止因断电或 kill -9 导致的垃圾堆积,同时不影响刚启动的其他实例
find /tmp -maxdepth 1 -type d -name "${TMP_BASE_NAME}_*" -mmin +10 -exec rm -rf {} + 2>/dev/null
# 创建目录
mkdir -p "$SAVE_DIR"
mkdir -p "$TMP_DIR"
# --- [保险措施 2] 增强型 Trap ---
# 无论脚本是正常退出 (EXIT)、被 Ctrl+C (SIGINT)、还是被 kill (SIGTERM),都执行清理
# 这里的逻辑是:只要脚本进程结束,就删掉本次生成的 TMP_DIR
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT SIGINT SIGTERM SIGHUP
# ==============================================================================
# 3. 依赖与工具探测
# ==============================================================================
CMD_FUZZEL="fuzzel -d --anchor=top --y-margin=10 --lines=5 --width=45 --prompt=$STR_PROMPT"
CMD_ROFI="rofi -dmenu -i -p $STR_PROMPT -l 5"
CMD_WOFI="wofi --dmenu --lines 5 --prompt $STR_PROMPT"
REQUIRED_CMDS=("grim" "slurp" "magick" "notify-send")
for cmd in "${REQUIRED_CMDS[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
PKG_NAME="$cmd"
[[ "$cmd" == "magick" ]] && PKG_NAME="imagemagick"
notify-send -u critical "$STR_ERR_TITLE" "$STR_ERR_DEP: $cmd\nInstall: sudo pacman -S $PKG_NAME"
exit 1
fi
done
EDITOR_CMD=""
if command -v satty &> /dev/null; then EDITOR_CMD="satty --filename";
elif command -v swappy &> /dev/null; then EDITOR_CMD="swappy -f"; fi
MENU_CMD=""
if command -v fuzzel &> /dev/null; then MENU_CMD="$CMD_FUZZEL"
elif command -v rofi &> /dev/null; then MENU_CMD="$CMD_ROFI"
elif command -v wofi &> /dev/null; then MENU_CMD="$CMD_WOFI"
else
notify-send -u critical "$STR_ERR_TITLE" "$STR_ERR_MENU"
exit 1
fi
function show_menu() { echo -e "$1" | $MENU_CMD; }
# ==============================================================================
# 步骤 1: 第一张截图 (基准)
# ==============================================================================
SELECTION=$(show_menu "$STR_START\n$STR_CANCEL")
if [[ "$SELECTION" != "$STR_START" ]]; then exit 0; fi
sleep 0.2
GEO_1=$(slurp)
# 如果第一步被 Super+Q 杀掉 slurpGEO_1 为空,脚本会在此退出并触发 cleanup
if [ -z "$GEO_1" ]; then exit 0; fi
IFS=', x' read -r FIX_X FIX_Y FIX_W FIX_H <<< "$GEO_1"
grim -g "$GEO_1" "$TMP_DIR/001.png"
# ==============================================================================
# 步骤 2: 循环截图
# ==============================================================================
INDEX=2
SAVE_MODE=""
while true; do
MENU_OPTIONS="$STR_NEXT\n$STR_SAVE"
if [[ -n "$EDITOR_CMD" ]]; then MENU_OPTIONS="$MENU_OPTIONS\n$STR_EDIT"; fi
MENU_OPTIONS="$MENU_OPTIONS\n$STR_ABORT"
# 如果此时 Super+Q 杀掉了 FuzzelACTION 为空
ACTION=$(show_menu "$MENU_OPTIONS")
case "$ACTION" in
*"📸"*)
sleep 0.2
GEO_NEXT=$(slurp)
# 如果此时 Super+Q 杀掉 SlurpGEO_NEXT 为空,回到菜单
if [ -z "$GEO_NEXT" ]; then
continue
fi
IFS=', x' read -r _TEMP_X NEW_Y _TEMP_W NEW_H <<< "$GEO_NEXT"
FINAL_GEO="${FIX_X},${NEW_Y} ${FIX_W}x${NEW_H}"
IMG_NAME="$(printf "%03d" $INDEX).png"
grim -g "$FINAL_GEO" "$TMP_DIR/$IMG_NAME"
((INDEX++))
;;
*"💾"*)
SAVE_MODE="save"
break
;;
*"🎨"*)
SAVE_MODE="edit"
break
;;
*"❌"*)
exit 0
;;
*)
# Fuzzel 被 Super+Q 关闭ACTION 为空,进入这里
# 直接 Break 跳出循环,进入保存/拼接流程 (防止误操作导致丢失)
# 或者如果你想放弃,这里改成 exit 0
break
;;
esac
done
# ==============================================================================
# 步骤 3: 拼接与后续处理
# ==============================================================================
COUNT=$(ls "$TMP_DIR"/*.png 2>/dev/null | wc -l)
if [ "$COUNT" -gt 0 ]; then
magick "$TMP_DIR"/*.png -append "$TMP_STITCHED"
if [[ "$SAVE_MODE" == "edit" ]]; then
$EDITOR_CMD "$TMP_STITCHED"
fi
# 只要有保存意向 (SAVE_MODE不为空),或者是因为意外退出且至少有图
# 如果你是"意外退出菜单",默认是不保存的 (SAVE_MODE为空)
# 这里我们只在显式选择保存/编辑时才保存
if [[ -n "$SAVE_MODE" ]]; then
mv "$TMP_STITCHED" "$RESULT_PATH"
COPY_MSG=""
if command -v wl-copy &> /dev/null; then
wl-copy < "$RESULT_PATH"
COPY_MSG="$STR_NOTIFY_COPIED"
fi
notify-send -i "$RESULT_PATH" "$STR_NOTIFY_TITLE" "$STR_NOTIFY_SAVED $FILENAME\n$COPY_MSG"
fi
fi
# 脚本结束,触发 Trap 清理 TMP_DIR

View File

@@ -0,0 +1,653 @@
#!/usr/bin/env bash
set -euo pipefail
########################
# 配置区域(直接改这里)
########################
NIRI_CONFIG="$HOME/.config/niri/config.kdl" # niri 配置文件
SHOTEDITOR_DEFAULT="satty" # 默认截图编辑器swappy 或 satty
COPY_CMD="wl-copy" # 复制到剪贴板的命令
# 菜单程序,按你实际使用的启动器改
# wofi 示例: MENU_CMD='wofi -d'
# rofi 示例: MENU_CMD='rofi -dmenu'
MENU_CMD='fuzzel --dmenu'
# 图片目录
PICTURES_DIR="$(xdg-user-dir PICTURES 2>/dev/null || echo "$HOME/Pictures")"
SCREEN_DIR="$PICTURES_DIR/Screenshots"
########################
# 本地化(中/英)
########################
LOCALE="${LC_MESSAGES:-${LANG:-en}}"
if [[ "$LOCALE" == zh* ]]; then
# 通用
LABEL_CANCEL="取消"
LABEL_SETTINGS="设置"
LABEL_EDIT_YES="编辑"
LABEL_EDIT_NO="不编辑"
# Niri 模式
LABEL_NIRI_FULL="全屏"
LABEL_NIRI_WINDOW="窗口"
LABEL_NIRI_REGION="选取区域"
# Grim 模式
LABEL_GRIM_FULL="全屏"
LABEL_GRIM_REGION="选取区域"
# 设置菜单
LABEL_SETTINGS_EDITOR="截图工具"
LABEL_SETTINGS_BACKEND="后端模式"
LABEL_BACKEND_AUTO="自动(检测 Niri"
LABEL_BACKEND_GRIM="仅 Grim+slurp"
LABEL_BACK="返回"
# 编辑开关显示
LABEL_EDIT_STATE_ON="编辑:开启"
LABEL_EDIT_STATE_OFF="编辑:关闭"
# 提示文字
PROMPT_MAIN="请选择截图模式"
PROMPT_SETTINGS="设置 / 更改选项"
PROMPT_EDITOR="请选择截图编辑工具"
PROMPT_BACKEND="请选择后端模式"
else
LABEL_CANCEL="Cancel"
LABEL_SETTINGS="Settings"
LABEL_EDIT_YES="Edit"
LABEL_EDIT_NO="No edit"
LABEL_NIRI_FULL="Fullscreen"
LABEL_NIRI_WINDOW="Window"
LABEL_NIRI_REGION="Region"
LABEL_GRIM_FULL="Fullscreen"
LABEL_GRIM_REGION="Select area"
LABEL_SETTINGS_EDITOR="Screenshot tool"
LABEL_SETTINGS_BACKEND="Backend mode"
LABEL_BACKEND_AUTO="Auto (detect Niri)"
LABEL_BACKEND_GRIM="Grim+slurp only"
LABEL_BACK="Back"
LABEL_EDIT_STATE_ON="Edit: ON"
LABEL_EDIT_STATE_OFF="Edit: OFF"
PROMPT_MAIN="Choose screenshot mode"
PROMPT_SETTINGS="Settings / Options"
PROMPT_EDITOR="Choose screenshot editor"
PROMPT_BACKEND="Choose backend mode"
fi
########################
# 持久化配置路径 (已修改为 .cache 目录)
########################
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
# 旧路径: CONFIG_DIR="$HOME/.config/waybar/waybar-shot"
CONFIG_DIR="$XDG_CACHE_HOME/waybar-power-screenshot-sh"
BACKEND_FILE="$CONFIG_DIR/backend"
EDITOR_FILE="$CONFIG_DIR/editor"
EDIT_MODE_FILE="$CONFIG_DIR/edit_mode" # yes / no
# 确保新的缓存目录存在
mkdir -p "$CONFIG_DIR"
########################
# 通用工具函数
########################
menu() {
printf '%s\n' "$@" | eval "$MENU_CMD" 2>/dev/null || true
}
menu_prompt() {
local prompt="$1"
shift
local esc_prompt="${prompt//\"/\\\"}"
printf '%s\n' "$@" | eval "$MENU_CMD --prompt \"$esc_prompt\"" 2>/dev/null || true
}
load_backend_mode() {
local mode
if [[ -n "${SHOT_BACKEND:-}" ]]; then
mode="$SHOT_BACKEND"
elif [[ -f "$BACKEND_FILE" ]]; then
mode="$(<"$BACKEND_FILE")"
else
mode="auto"
fi
case "$mode" in
auto|grim|niri) ;;
*) mode="auto" ;;
esac
printf '%s\n' "$mode"
}
save_backend_mode() {
local mode="$1"
# 由于脚本开始时已创建,这里只需要保证文件写入成功
printf '%s\n' "$mode" >"$BACKEND_FILE"
}
load_editor() {
local ed
if [[ -n "${SHOTEDITOR:-}" ]]; then
ed="$SHOTEDITOR"
elif [[ -f "$EDITOR_FILE" ]]; then
ed="$(<"$EDITOR_FILE")"
else
ed="$SHOTEDITOR_DEFAULT"
fi
ed="${ed,,}"
case "$ed" in
swappy|satty) ;;
*) ed="$SHOTEDITOR_DEFAULT" ;;
esac
printf '%s\n' "$ed"
}
save_editor() {
local ed="$1"
# 由于脚本开始时已创建,这里只需要保证文件写入成功
printf '%s\n' "$ed" >"$EDITOR_FILE"
}
load_edit_mode() {
local v="yes"
if [[ -f "$EDIT_MODE_FILE" ]]; then
v="$(<"$EDIT_MODE_FILE")"
fi
case "$v" in
yes|no) ;;
*) v="yes" ;; # 默认:编辑开启
esac
printf '%s\n' "$v"
}
save_edit_mode() {
local v="$1"
# 由于脚本开始时已创建,这里只需要保证文件写入成功
printf '%s\n' "$v" >"$EDIT_MODE_FILE"
}
detect_backend() {
case "$BACKEND_MODE" in
niri) echo "niri" ;;
grim) echo "grim" ;;
auto|*)
if command -v niri >/dev/null 2>&1 && pgrep -x niri >/dev/null 2>&1; then
echo "niri"
else
echo "grim"
fi
;;
esac
}
choose_editor() {
local choice
choice="$(menu_prompt "$PROMPT_EDITOR" "swappy" "satty" "$LABEL_BACK")"
case "$choice" in
swappy|Swappy)
SHOTEDITOR="swappy"
save_editor "$SHOTEDITOR"
;;
satty|Satty)
SHOTEDITOR="satty"
save_editor "$SHOTEDITOR"
;;
*) : ;;
esac
}
choose_backend_mode() {
local choice
choice="$(menu_prompt "$PROMPT_BACKEND" "$LABEL_BACKEND_AUTO" "$LABEL_BACKEND_GRIM" "$LABEL_BACK")"
case "$choice" in
"$LABEL_BACKEND_AUTO")
BACKEND_MODE="auto"
save_backend_mode "$BACKEND_MODE"
;;
"$LABEL_BACKEND_GRIM")
BACKEND_MODE="grim"
save_backend_mode "$BACKEND_MODE"
;;
*) : ;;
esac
}
settings_menu() {
while :; do
local backend_desc editor_line backend_line choice
if [[ -n "${SHOT_BACKEND:-}" ]]; then
backend_desc="${BACKEND_MODE} (env)"
else
if [[ "$BACKEND_MODE" == "grim" ]]; then
backend_desc="$LABEL_BACKEND_GRIM"
elif [[ "$BACKEND_MODE" == "niri" ]]; then
backend_desc="niri"
else
backend_desc="$LABEL_BACKEND_AUTO"
fi
fi
editor_line="$LABEL_SETTINGS_EDITOR: $SHOTEDITOR"
backend_line="$LABEL_SETTINGS_BACKEND: $backend_desc"
choice="$(menu_prompt "$PROMPT_SETTINGS" "$editor_line" "$backend_line" "$LABEL_BACK")"
case "$choice" in
"$editor_line") choose_editor ;;
"$backend_line")
if [[ -n "${SHOT_BACKEND:-}" ]]; then
: # 环境变量强制时不改持久化
else
choose_backend_mode
fi
;;
*) return ;; # 返回上一层
esac
done
}
latest_in_dir() {
local dir="$1"
find "$dir" -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null \
| sort -n | tail -1 | cut -d' ' -f2-
}
########################
# 剪贴板相关Niri 编辑用)
########################
clip_hash() {
wl-paste -t image/png 2>/dev/null \
| sha1sum 2>/dev/null \
| cut -d' ' -f1 2>/dev/null \
|| echo ""
}
wait_clipboard_change() {
local old new i
old="$(clip_hash)"
for i in {1..200}; do # 最多等 ~10 秒
new="$(clip_hash)"
if [[ -n "$new" && "$new" != "$old" ]]; then
return 0
fi
sleep 0.05
done
return 1
}
########################
# Niri 相关
########################
get_niri_shot_dir() {
[[ -f "$NIRI_CONFIG" ]] || { echo "Config not found: $NIRI_CONFIG" >&2; return 1; }
local line tpl dir
line="$(
grep -E '^[[:space:]]*screenshot-path[[:space:]]' "$NIRI_CONFIG" \
| grep -v '^[[:space:]]*//' \
| tail -n 1 || true
)"
[[ -n "$line" ]] || { echo "No screenshot-path in config" >&2; return 1; }
tpl="$(sed -E 's/.*screenshot-path[[:space:]]+"([^"]+)".*/\1/' <<<"$line")"
[[ -n "$tpl" ]] || { echo "Failed to parse screenshot-path: $line" >&2; return 1; }
tpl="${tpl/#\~/$HOME}"
dir="${tpl%/*}"
printf '%s\n' "$dir"
}
# Grim 用:从文件编辑
edit_file_image() {
local src="$1"
local backend="$2" # "niri" 或 "grim"
local dir ts dst
if [[ "$backend" == "niri" ]]; then
dir="$NIRI_EDIT_DIR"
else
dir="$SCREEN_DIR"
fi
mkdir -p "$dir"
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
dst="$dir/$SHOTEDITOR-$ts.png"
case "$SHOTEDITOR" in
satty)
satty --filename "$src" --output-filename "$dst"
;;
swappy)
swappy -f "$src" -o "$dst"
;;
*)
echo "Unknown SHOTEDITOR: $SHOTEDITOR (use satty or swappy)" >&2
return 0
;;
esac
if [[ -f "$dst" ]]; then
if [[ "$backend" == "grim" && "$src" == /tmp/* ]]; then
rm -f "$src"
fi
"$COPY_CMD" < "$dst"
fi
}
# Niri 用:从剪贴板编辑
edit_from_clipboard() {
local backend="$1" # 目前只会传 "niri"
local dir ts dst
if [[ "$backend" == "niri" ]]; then
dir="$NIRI_EDIT_DIR"
else
dir="$SCREEN_DIR"
fi
mkdir -p "$dir"
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
dst="$dir/$SHOTEDITOR-$ts.png"
case "$SHOTEDITOR" in
satty)
wl-paste -t image/png 2>/dev/null | satty -f - --output-filename "$dst"
;;
swappy)
wl-paste -t image/png 2>/dev/null | swappy -f - -o "$dst"
;;
*)
echo "Unknown SHOTEDITOR: $SHOTEDITOR (use satty or swappy)" >&2
return 0
;;
esac
if [[ -f "$dst" ]]; then
"$COPY_CMD" < "$dst"
fi
}
niri_capture_and_maybe_edit() {
local mode="$1" # fullscreen / window / region
local need_edit="$2" # yes / no
local action
case "$mode" in
fullscreen) action="screenshot-screen" ;;
window) action="screenshot-window" ;;
region) action="screenshot" ;;
*) return 0 ;;
esac
# 不编辑:用目录里的最新文件判断 screenshot 完成
if [[ "$need_edit" != "yes" ]]; then
local before shot
before="$(latest_in_dir "$NIRI_SHOT_DIR" || true)"
niri msg action "$action"
while :; do
shot="$(latest_in_dir "$NIRI_SHOT_DIR" || true)"
if [[ -z "$before" && -n "$shot" ]] || \
[[ -n "$before" && -n "$shot" && "$shot" != "$before" ]]; then
break
fi
sleep 0.05
done
return 0
fi
# 编辑:基于剪贴板
niri msg action "$action"
if ! wait_clipboard_change; then
echo "等待剪贴板中的截图超时" >&2
return 0
fi
edit_from_clipboard "niri"
return 0
}
run_niri_flow() {
NIRI_SHOT_DIR="$(get_niri_shot_dir)" || return 0
NIRI_EDIT_DIR="$NIRI_SHOT_DIR/Edited"
mkdir -p "$NIRI_SHOT_DIR" "$NIRI_EDIT_DIR"
while :; do
local choice mode edit_mode edit_label
edit_mode="$(load_edit_mode)"
if [[ "$edit_mode" == "yes" ]]; then
edit_label="$LABEL_EDIT_STATE_ON"
else
edit_label="$LABEL_EDIT_STATE_OFF"
fi
choice="$(menu_prompt "$PROMPT_MAIN" \
"$LABEL_NIRI_FULL" \
"$LABEL_NIRI_WINDOW" \
"$LABEL_NIRI_REGION" \
"$edit_label" \
"$LABEL_SETTINGS" \
"$LABEL_CANCEL"
)"
[[ -z "$choice" || "$choice" == "$LABEL_CANCEL" ]] && return 2
case "$choice" in
"$LABEL_NIRI_FULL") mode="fullscreen" ;;
"$LABEL_NIRI_WINDOW") mode="window" ;;
"$LABEL_NIRI_REGION") mode="region" ;;
"$edit_label")
if [[ "$edit_mode" == "yes" ]]; then
save_edit_mode "no"
else
save_edit_mode "yes"
fi
continue
;;
"$LABEL_SETTINGS")
# 进入设置菜单(可以在里面改 editor / backend / edit-mode
settings_menu
# 立即重新加载持久化配置,使修改即时生效
SHOTEDITOR="$(load_editor)"
BACKEND_MODE="$(load_backend_mode)"
# 检查后端是否被改动;若改动则通知上层切换后端
NEW_BACKEND="$(detect_backend)"
if [[ "$NEW_BACKEND" != "niri" ]]; then
return 1
fi
continue
;;
*)
return 2
;;
esac
edit_mode="$(load_edit_mode)"
if [[ "$edit_mode" == "yes" ]]; then
niri_capture_and_maybe_edit "$mode" "yes"
else
niri_capture_and_maybe_edit "$mode" "no"
fi
# 截图完成后退出(主循环会根据返回码决定是否结束脚本)
return 0
done
}
########################
# Grim + slurp 相关
########################
grim_capture_and_maybe_edit() {
local mode="$1" # fullscreen / region
local need_edit="$2" # yes / no
mkdir -p "$SCREEN_DIR"
local ts shot geo
ts="$(date +'%Y-%m-%d_%H-%M-%S')"
if [[ "$need_edit" == "yes" ]]; then
# 编辑模式:原图在 /tmp用完删只保留编辑后的图
shot="/tmp/waybar-shot-$ts.png"
case "$mode" in
fullscreen)
grim "$shot"
;;
region)
geo="$(slurp 2>/dev/null)" || return 0
grim -g "$geo" "$shot"
;;
*)
return 0 ;;
esac
edit_file_image "$shot" "grim"
return 0
else
# 不编辑:原图保存到 Screenshots
shot="$SCREEN_DIR/Screenshot_$ts.png"
case "$mode" in
fullscreen)
grim "$shot"
;;
region)
geo="$(slurp 2>/dev/null)" || return 0
grim -g "$geo" "$shot"
;;
*)
return 0 ;;
esac
return 0
fi
}
run_grim_flow() {
mkdir -p "$SCREEN_DIR"
while :; do
local choice mode edit_mode edit_label
edit_mode="$(load_edit_mode)"
if [[ "$edit_mode" == "yes" ]]; then
edit_label="$LABEL_EDIT_STATE_ON"
else
edit_label="$LABEL_EDIT_STATE_OFF"
fi
choice="$(menu_prompt "$PROMPT_MAIN" \
"$LABEL_GRIM_FULL" \
"$LABEL_GRIM_REGION" \
"$edit_label" \
"$LABEL_SETTINGS" \
"$LABEL_CANCEL"
)"
[[ -z "$choice" || "$choice" == "$LABEL_CANCEL" ]] && return 2
case "$choice" in
"$LABEL_GRIM_FULL") mode="fullscreen" ;;
"$LABEL_GRIM_REGION") mode="region" ;;
"$edit_label")
if [[ "$edit_mode" == "yes" ]]; then
save_edit_mode "no"
else
save_edit_mode "yes"
fi
continue
;;
"$LABEL_SETTINGS")
settings_menu
# 立即重新加载持久化配置,使修改即时生效
SHOTEDITOR="$(load_editor)"
BACKEND_MODE="$(load_backend_mode)"
NEW_BACKEND="$(detect_backend)"
if [[ "$NEW_BACKEND" != "grim" ]]; then
return 1
fi
continue
;;
*)
return 2
;;
esac
edit_mode="$(load_edit_mode)"
if [[ "$edit_mode" == "yes" ]]; then
grim_capture_and_maybe_edit "$mode" "yes"
else
grim_capture_and_maybe_edit "$mode" "no"
fi
return 0
done
}
########################
# 入口(主循环)
########################
while :; do
BACKEND_MODE="$(load_backend_mode)"
SHOTEDITOR="$(load_editor)"
BACKEND="$(detect_backend)"
case "$BACKEND" in
niri)
rc=0
run_niri_flow || rc=$?
if [[ "$rc" -eq 0 ]]; then
exit 0
elif [[ "$rc" -eq 1 ]]; then
# 后端切换:继续主循环以根据新后端重试
continue
else
# 取消或其他:退出
exit 0
fi
;;
grim)
rc=0
run_grim_flow || rc=$?
if [[ "$rc" -eq 0 ]]; then
exit 0
elif [[ "$rc" -eq 1 ]]; then
continue
else
exit 0
fi
;;
*)
run_grim_flow
exit 0
;;
esac
done

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
grim -g "$(slurp)" - | wl-copy

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# 检查蓝牙的 rfkill 状态
# 'rfkill list bluetooth' 会输出蓝牙设备的信息
# 'grep -q "Soft blocked: yes"' 在输出中安静地 (-q) 查找 "Soft blocked: yes" 字符串
if rfkill list bluetooth | grep -q "Soft blocked: yes"; then
# 如果找到了 "Soft blocked: yes" (说明蓝牙被软屏蔽了)
# 则执行 unblock 命令来解锁
rfkill unblock bluetooth
# (可选) 发送一个通知,提供操作反馈
else
# 如果没有找到 "Soft blocked: yes" (说明蓝牙是开启的)
# 则执行 block 命令来屏蔽
rfkill block bluetooth
# (可选) 发送通知
fi

View File

@@ -0,0 +1,791 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# ================== Runtime state & Persistent Config ==================
APP="wf-recorder"
# --- 运行时状态 (应在每次会话结束时消失, 遵循 XDG_RUNTIME_DIR) ---
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$UID}"
STATE_DIR="$RUNTIME_DIR/wfrec"
PIDFILE="$STATE_DIR/pid"
STARTFILE="$STATE_DIR/start"
SAVEPATH_FILE="$STATE_DIR/save_path"
MODEFILE="$STATE_DIR/mode" # full/region -> tooltip
GIF_MARKER="$STATE_DIR/is_gif" # [NEW] 标记当前录制是否为 GIF 模式
TICKPIDFILE="$STATE_DIR/tickpid"
WAYBAR_PIDS_CACHE="$STATE_DIR/waybar.pids"
# --- 持久性配置 (缓存/设置, 存放在 .cache/ 下, 遵循 XDG_CACHE_HOME) ---
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
CONFIG_DIR="$XDG_CACHE_HOME/wf-recorder-sh" # 目标目录 .cache/wf-recorder-sh
CFG_CODEC="$CONFIG_DIR/codec"
CFG_FPS="$CONFIG_DIR/framerate"
CFG_AUDIO="$CONFIG_DIR/audio"
CFG_DRM="$CONFIG_DIR/drm_device"
CFG_EXT="$CONFIG_DIR/container_ext" # persisted file format (auto/mp4/mkv/webm)
# 创建所需的目录
mkdir -p "$STATE_DIR"
mkdir -p "$CONFIG_DIR"
# Hold chosen mode
MODE_DECIDED=""
IS_GIF_MODE="false" # [NEW] 临时变量
# ================== Tunables (ENV overridable) ==================
# defaults — 默认使用 CPU 编码 (libx264)
_DEFAULT_CODEC="libx264"
_DEFAULT_FRAMERATE=""
_DEFAULT_AUDIO="on"
_DEFAULT_SAVE_EXT="auto" # auto/mp4/mkv/webm
# --- [NEW] GIF 配置区域 ---
GIF_WIDTH=720
GIF_FPS=30
GIF_DITHER_MODE="bayer:bayer_scale=5"
GIF_STATS_MODE="diff"
# -------------------------
# load persisted settings if exist
codec_from_file=$(cat "$CFG_CODEC" 2>/dev/null || true)
fps_from_file=$(cat "$CFG_FPS" 2>/dev/null || true)
audio_from_file=$(cat "$CFG_AUDIO" 2>/dev/null || true)
drm_from_file=$(cat "$CFG_DRM" 2>/dev/null || true)
ext_from_file=$(cat "$CFG_EXT" 2>/dev/null || true)
# priority: ENV > persisted > default
CODEC="${CODEC:-${codec_from_file:-$_DEFAULT_CODEC}}"
FRAMERATE="${FRAMERATE:-${fps_from_file:-$_DEFAULT_FRAMERATE}}"
AUDIO="${AUDIO:-${audio_from_file:-$_DEFAULT_AUDIO}}"
DRM_DEVICE="${DRM_DEVICE:-${drm_from_file:-}}"
SAVE_EXT="${SAVE_EXT:-${ext_from_file:-$_DEFAULT_SAVE_EXT}}"
TITLE="${TITLE:-}"
SAVE_DIR_ENV="${SAVE_DIR:-}"
SAVE_SUBDIR_FS="${SAVE_SUBDIR_FS:-fullscreen}"
OUTPUT="${OUTPUT:-}" # e.g. eDP-1 / DP-2
OUTPUT_SELECT="${OUTPUT_SELECT:-auto}" # off|auto|menu
MENU_TITLE_OUTPUT="${MENU_TITLE_OUTPUT:-}"
MENU_BACKEND="${MENU_BACKEND:-auto}" # auto|fuzzel|wofi|rofi|bemenu|fzf|term
RECORD_MODE="${RECORD_MODE:-ask}" # ask|full|region
MODE_MENU_TITLE="${MODE_MENU_TITLE:-Select recording mode}"
REC_AREA="${REC_AREA:-}" # "x,y WIDTHxHEIGHT" (optional)
GEOM_IN_NAME="${GEOM_IN_NAME:-off}"
WAYBAR_POKE="${WAYBAR_POKE:-on}"
WAYBAR_SIG="${WAYBAR_SIG:-9}"
ICON_REC="${ICON_REC:-}"
ICON_IDLE="${ICON_IDLE:-}"
PKILL_AFTER_STOP="${PKILL_AFTER_STOP:-on}"
# DEBUG: 若设为 on则在前台运行 wf-recorder并把输出直接显示到终端仅终端不写文件
DEBUG="${DEBUG:-off}"
# ================== Utils ==================
has() { command -v "$1" >/dev/null 2>&1; }
lang_code() {
local l="${LC_MESSAGES:-${LANG:-en}}"
l="${l,,}"; l="${l%%.*}"; l="${l%%-*}"; l="${l%%_*}"
case "$l" in zh|zh-cn|zh-tw|zh-hk) echo zh ;; ja|jp) echo ja ;; *) echo en ;; esac
}
msg() {
local id="$1"; shift
case "$(lang_code)" in
zh)
case "$id" in
err_wf_not_found) printf "未找到 wf-recorder" ;;
err_need_slurp) printf "需要 slurp 以进行区域选择" ;;
err_need_ffmpeg) printf "GIF 转换需要 ffmpeg但未找到。" ;;
warn_drm_ignored) printf "警告DRM_DEVICE=%s 不存在或不可读,将忽略。" "$@" ;;
warn_invalid_fps) printf "警告FRAMERATE=\"%s\" 非法,已忽略。" "$@" ;;
warn_render_unreadable) printf "警告:无效的 render 节点:%s" "$@" ;;
cancel_no_mode) printf "已取消:未选择录制模式。" ;;
cancel_no_output) printf "已取消:未选择输出。" ;;
cancel_no_region) printf "已取消:未选择区域。" ;;
warn_multi_outputs_cancel) printf "检测到多个输出但未选择,已取消。" ;;
notif_started_full) printf "开始录制(全屏:%s→ %s" "$@" ;;
notif_started_region) printf "开始录制(区域)→ %s" "$@" ;;
notif_device_suffix) printf "(设备 %s" "$@" ;;
notif_saved) printf "已保存:%s" "$@" ;;
notif_stopped) printf "已停止录制。" ;;
notif_processing_gif) printf "正在转换为 GIF请稍候..." ;;
notif_gif_failed) printf "GIF 转换失败,保留原视频。" ;;
notif_copied) printf "文件已复制" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "选择录制模式" ;;
title_output) printf "选择输出" ;;
menu_fullscreen) printf "全屏" ;;
menu_region) printf "选择区域" ;;
menu_gif_region) printf "录制 GIF (区域)" ;;
# settings labels -> "标签:值"
title_settings) printf "设置..." ;;
menu_settings) printf "设置..." ;;
menu_set_codec) printf "编码格式:%s" "$@" ;;
menu_set_fps) printf "帧率:%s" "$@" ;;
menu_set_filefmt) printf "文件格式:%s" "$@" ;;
menu_toggle_audio) printf "音频:%s" "$@" ;;
menu_set_render) printf "渲染设备:%s" "$@" ;;
menu_back) printf "返回" ;;
fps_unlimited) printf "不限制" ;;
render_auto) printf "自动" ;;
ext_auto) printf "自动" ;;
title_select_codec) printf "选择编码格式" ;;
title_select_fps) printf "选择帧率" ;;
title_select_filefmt) printf "选择文件格式" ;;
title_select_render) printf "选择渲染设备(/dev/dri/renderD*" ;;
mode_full) printf "全屏" ;;
mode_region) printf "区域" ;;
prompt_enter_number) printf "输入编号:" ;;
menu_exit) printf "退出" ;;
*) printf "%s" "$id" ;;
esac
;;
ja)
case "$id" in
err_wf_not_found) printf "wf-recorder が見つかりません" ;;
err_need_slurp) printf "領域選択には slurp が必要です" ;;
err_need_ffmpeg) printf "GIF変換には ffmpeg が必要ですが、見つかりません。" ;;
warn_drm_ignored) printf "警告DRM_DEVICE=%s は無視されます。" "$@" ;;
warn_invalid_fps) printf "警告FRAMERATE=\"%s\" は不正です。" "$@" ;;
warn_render_unreadable) printf "警告:無効なレンダー ノード:%s" "$@" ;;
cancel_no_mode) printf "キャンセル:録画モード未選択。" ;;
cancel_no_output) printf "キャンセル:出力未選択。" ;;
cancel_no_region) printf "キャンセル:領域未選択。" ;;
warn_multi_outputs_cancel) printf "出力が複数ですが未選択のため中止。" ;;
notif_started_full) printf "録画開始(全画面:%s→ %s" "$@" ;;
notif_started_region) printf "録画開始(領域)→ %s" "$@" ;;
notif_device_suffix) printf "(デバイス %s" "$@" ;;
notif_saved) printf "保存しました:%s" "$@" ;;
notif_stopped) printf "録画を停止しました。" ;;
notif_processing_gif) printf "GIF に変換中、お待ちください..." ;;
notif_gif_failed) printf "GIF 変換に失敗しました。元の動画を保持します。" ;;
notif_copied) printf "ファイルをコピーしました" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "録画モードを選択" ;;
title_output) printf "出力を選択" ;;
menu_fullscreen) printf "全画面" ;;
menu_region) printf "領域選択" ;;
menu_gif_region) printf "GIF録画 (領域)" ;;
# settings labels -> "ラベル:値"(全角コロン)
title_settings) printf "設定..." ;;
menu_settings) printf "設定..." ;;
menu_set_codec) printf "コーデック:%s" "$@" ;;
menu_set_fps) printf "フレームレート:%s" "$@" ;;
menu_set_filefmt) printf "ファイル形式:%s" "$@" ;;
menu_toggle_audio) printf "音声:%s" "$@" ;;
menu_set_render) printf "レンダーデバイス:%s" "$@" ;;
menu_back) printf "戻る" ;;
fps_unlimited) printf "無制限" ;;
render_auto) printf "自動" ;;
ext_auto) printf "自動" ;;
title_select_codec) printf "コーデックを選択" ;;
title_select_fps) printf "フレームレートを選択" ;;
title_select_filefmt) printf "ファイル形式を選択" ;;
title_select_render) printf "レンダーデバイスを選択(/dev/dri/renderD*" ;;
mode_full) printf "全画面" ;;
mode_region) printf "領域" ;;
prompt_enter_number) printf "番号を入力:" ;;
menu_exit) printf "終了" ;;
*) printf "%s" "$id" ;;
esac
;;
*)
case "$id" in
err_wf_not_found) printf "wf-recorder not found" ;;
err_need_slurp) printf "slurp required for region selection" ;;
err_need_ffmpeg) printf "ffmpeg is required for GIF conversion but not found." ;;
warn_drm_ignored) printf "Warning: DRM_DEVICE=%s ignored." "$@" ;;
warn_invalid_fps) printf "Warning: invalid FRAMERATE=\"%s\"." "$@" ;;
warn_render_unreadable) printf "Warning: invalid render node: %s" "$@" ;;
cancel_no_mode) printf "Canceled: no recording mode selected." ;;
cancel_no_output) printf "Canceled: no output selected." ;;
cancel_no_region) printf "Canceled: no region selected." ;;
warn_multi_outputs_cancel) printf "Multiple outputs but none selected; canceled." ;;
notif_started_full) printf "Recording started (fullscreen: %s) → %s" "$@" ;;
notif_started_region) printf "Recording started (region) → %s" "$@" ;;
notif_device_suffix) printf " (device %s)" "$@" ;;
notif_saved) printf "Saved: %s" "$@" ;;
notif_stopped) printf "Recording stopped." ;;
notif_processing_gif) printf "Converting to GIF, please wait..." ;;
notif_gif_failed) printf "GIF conversion failed. Original video kept." ;;
notif_copied) printf "File copied" ;;
already_running) printf "already running" ;;
not_running) printf "not running" ;;
title_mode) printf "Select recording mode" ;;
title_output) printf "Select output" ;;
menu_fullscreen) printf "Fullscreen" ;;
menu_region) printf "Region" ;;
menu_gif_region) printf "Record GIF (Region)" ;;
# settings labels -> "Label: Value"
title_settings) printf "Settings..." ;;
menu_settings) printf "Settings..." ;;
menu_set_codec) printf "Codec: %s" "$@" ;;
menu_set_fps) printf "Framerate: %s" "$@" ;;
menu_set_filefmt) printf "File Format: %s" "$@" ;;
menu_toggle_audio) printf "Audio: %s" "$@" ;;
menu_set_render) printf "Render Device: %s" "$@" ;;
menu_back) printf "Back" ;;
fps_unlimited) printf "unlimited" ;;
render_auto) printf "Auto" ;;
ext_auto) printf "Auto" ;;
title_select_codec) printf "Select Codec" ;;
title_select_fps) printf "Select Framerate" ;;
title_select_filefmt) printf "Select File Format" ;;
title_select_render) printf "Select Render Device (/dev/dri/renderD*)" ;;
mode_full) printf "Fullscreen" ;;
mode_region) printf "Region" ;;
prompt_enter_number) printf "Enter number: " ;;
menu_exit) printf "Exit" ;;
*) printf "%s" "$id" ;;
esac
;;
esac
}
is_running() {
[[ -r "$PIDFILE" ]] || return 1
local pid; read -r pid <"$PIDFILE" 2>/dev/null || return 1
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
notify() { has notify-send && notify-send "wf-recorder" "$1" || true; }
signal_waybar() {
local pids
if [[ -r "$WAYBAR_PIDS_CACHE" ]]; then
pids="$(tr '\n' ' ' <"$WAYBAR_PIDS_CACHE")"
if [[ -n "$pids" ]]; then kill -RTMIN+"$WAYBAR_SIG" $pids 2>/dev/null && return 0; fi
fi
pids="$(pgrep -x -u "$UID" waybar 2>/dev/null | tr '\n' ' ')"
[[ -n "$pids" ]] && printf '%s\n' $pids >"$WAYBAR_PIDS_CACHE"
[[ -n "$pids" ]] && kill -RTMIN+"$WAYBAR_SIG" $pids 2>/dev/null || true
}
emit_waybar_signal() { [[ "${WAYBAR_POKE,,}" == "off" ]] && return 0; signal_waybar; }
start_tick() {
if [[ -f "$TICKPIDFILE" ]]; then
local tpid; read -r tpid <"$TICKPIDFILE" 2>/dev/null || true
[[ -n "$tpid" ]] && kill -TERM "$tpid" 2>/dev/null || true
rm -f "$TICKPIDFILE"
fi
(
while :; do
[[ -r "$PIDFILE" ]] || break
local p; read -r p <"$PIDFILE" 2>/dev/null || p=""
[[ -n "$p" ]] && kill -0 "$p" 2>/dev/null || break
signal_waybar
sleep 1
done
) & echo $! >"$TICKPIDFILE"
}
stop_tick() {
if [[ -f "$TICKPIDFILE" ]]; then
local tpid; read -r tpid <"$TICKPIDFILE" 2>/dev/null || true
[[ -n "$tpid" ]] && kill -TERM "$tpid" 2>/dev/null || true
rm -f "$TICKPIDFILE"
fi
}
get_save_dir() {
local videos
if has xdg-user-dir; then videos="$(xdg-user-dir VIDEOS 2>/dev/null || true)"; fi
videos="${videos:-"$HOME/Videos"}"
echo "${SAVE_DIR_ENV:-"$videos/wf-recorder"}"
}
# --- render device helpers ---
list_render_nodes() {
local d
for d in /dev/dri/renderD*; do
[[ -r "$d" ]] && printf '%s\n' "$d"
done 2>/dev/null || true
}
render_display() {
local cur="${1:-}"
if [[ -z "$cur" ]]; then
msg render_auto
else
printf "%s" "$cur"
fi
}
pick_render_device() {
local dev="${DRM_DEVICE:-}"
if [[ -n "$dev" && ! -r "$dev" ]]; then
printf '%s\n' "$(msg warn_render_unreadable "$dev")" >&2
dev=""
fi
echo -n "$dev"
}
# --- file format helpers ---
ext_for_codec(){ case "${1,,}" in
*h264*|*hevc*) echo mp4 ;;
*vp9*) echo webm ;;
*av1*) echo mkv ;;
*) echo mp4 ;;
esac; }
choose_ext(){
local e="${SAVE_EXT,,}"
if [[ -z "$e" || "$e" == "auto" ]]; then
ext_for_codec "$CODEC"
else
case "$e" in mp4|mkv|webm) echo "$e" ;; *) echo mp4 ;; esac
fi
}
# ================== Menus ==================
__norm() { printf '%s' "$1" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
_pick_menu_backend() {
local pref="${MENU_BACKEND,,}"
case "$pref" in fuzzel|wofi|rofi|bemenu|fzf|term) : ;; auto|"") pref="auto" ;; *) pref="auto" ;; esac
if [[ "$pref" != "auto" ]]; then
if has "$pref"; then echo "$pref"; else [[ -t 0 ]] && echo "term" || echo "none"; fi
return
fi
for b in fuzzel wofi rofi bemenu fzf; do has "$b" && { echo "$b"; return; }; done
[[ -t 0 ]] && echo "term" || echo "none"
}
menu_pick() { # $1:title; items...
local title="${1:-Select}"; shift
local items=("$@")
((${#items[@]})) || return 130
local backend; backend="$(_pick_menu_backend)"
local sel rc=130
case "$backend" in
fuzzel) set +e; sel="$(printf '%s\n' "${items[@]}" | fuzzel --dmenu -p "$title")"; rc=$?; set -e ;;
wofi) set +e; sel="$(printf '%s\n' "${items[@]}" | wofi --dmenu --prompt "$title")"; rc=$?; set -e ;;
rofi) set +e; sel="$(printf '%s\n' "${items[@]}" | rofi -dmenu -p "$title")"; rc=$?; set -e ;;
bemenu) set +e; sel="$(printf '%s\n' "${items[@]}" | bemenu -p "$title")"; rc=$?; set -e ;;
fzf) set +e; sel="$(printf '%s\n' "${items[@]}" | fzf --prompt "$title> ")"; rc=$?; set -e ;;
term)
echo "$title"
local i=1; for it in "${items[@]}"; do printf ' %d) %s\n' "$i" "$it"; ((i++)); done
printf "%s" "$(msg prompt_enter_number)"
local idx; set +e; read -r idx; rc=$?; set -e
if [[ $rc -eq 0 && -n "$idx" && "$idx" =~ ^[0-9]+$ ]]; then
if (( idx>=1 && idx<=${#items[@]} )); then sel="${items[$((idx-1))]}"; rc=0; fi
fi
;;
none) return 130 ;;
esac
[[ $rc -ne 0 || -z "${sel:-}" ]] && return 130
printf '%s' "$(__norm "$sel")"
}
# ---------- Outputs ----------
list_outputs() {
local raw
if raw="$(wf-recorder -L 2>/dev/null)"; then :; elif has wlr-randr; then raw="$(wlr-randr 2>/dev/null | awk '/^[^ ]/{print $1}')"; else raw=""; fi
awk 'BEGIN{RS="[ \t\r\n,]+"} /^[A-Za-z0-9_.:-]+$/ { if ($0 ~ /^(e?DP|HDMI|DVI|VGA|LVDS|Virtual|XWAYLAND)/) seen[$0]=1 } END{for(k in seen) print k}' <<<"$raw" | sort -u
}
decide_output() {
if [[ -n "$OUTPUT" ]]; then printf '%s' "$OUTPUT"; return 0; fi
local -a outs; mapfile -t outs < <(list_outputs || true)
local out_title; out_title="${MENU_TITLE_OUTPUT:-$(msg title_output)}"
if [[ "${OUTPUT_SELECT}" == "menu" ]] || { [[ "${OUTPUT_SELECT}" == "auto" ]] && ((${#outs[@]} > 1)); }; then
local pick; pick="$(menu_pick "$out_title" "${outs[@]}")" || return 130
printf '%s' "$pick"; return 0
fi
if ((${#outs[@]} == 1)); then printf '%s' "${outs[0]}"; else printf '%s\n' "$(msg warn_multi_outputs_cancel)" >&2; return 130; fi
}
# ---------- Settings ----------
choose_render_menu() {
local -a nodes
mapfile -t nodes < <(list_render_nodes | sort -V || true)
local auto_item; auto_item="$(msg render_auto)"
local pick
if ! pick="$(menu_pick "$(msg title_select_render)" "$auto_item" "${nodes[@]}")"; then
return 0
fi
if [[ "$pick" == "$auto_item" ]]; then
DRM_DEVICE=""
rm -f "$CFG_DRM"
return 0
fi
local sel="$pick"
if [[ -n "$sel" && -r "$sel" ]]; then
DRM_DEVICE="$sel"
printf '%s' "$DRM_DEVICE" >"$CFG_DRM"
else
printf '%s\n' "$(msg warn_render_unreadable "$sel")" >&2
fi
}
choose_filefmt_menu() {
local auto_item; auto_item="$(msg ext_auto)"
local pick
if ! pick="$(menu_pick "$(msg title_select_filefmt)" "$auto_item" "mp4" "mkv" "webm")"; then
return 0
fi
if [[ "$pick" == "$auto_item" ]]; then
SAVE_EXT="auto"
rm -f "$CFG_EXT"
else
case "$pick" in
mp4|mkv|webm) SAVE_EXT="$pick"; printf '%s' "$SAVE_EXT" >"$CFG_EXT" ;;
*) : ;;
esac
fi
}
show_settings_menu() {
while :; do
local fps_display="${FRAMERATE:-$(msg fps_unlimited)}"
local audio_display="${AUDIO}"
local render_display_now; render_display_now="$(render_display "$DRM_DEVICE")"
local ff_display; if [[ -z "$SAVE_EXT" || "${SAVE_EXT,,}" == "auto" ]]; then ff_display="$(msg ext_auto)"; else ff_display="$SAVE_EXT"; fi
# ORDER: Framerate → Audio → Codec → File Format → Render → Back
local pick; pick="$(menu_pick "$(msg title_settings)" \
"$(msg menu_set_fps "$fps_display")" \
"$(msg menu_toggle_audio "$audio_display")" \
"$(msg menu_set_codec "$CODEC")" \
"$(msg menu_set_filefmt "$ff_display")" \
"$(msg menu_set_render "$render_display_now")" \
"$(msg menu_back)")" || return 0
if [[ "$pick" == "$(msg menu_set_fps "$fps_display")" ]]; then
local newf; newf="$(menu_pick "$(msg title_select_fps)" "60" "30" "120" "144" "165" "240" "$(msg fps_unlimited)")" || continue
if [[ "$newf" == "$(msg fps_unlimited)" ]]; then
FRAMERATE=""; rm -f "$CFG_FPS"
else
if [[ "$newf" =~ ^[0-9]+$ && "$newf" -gt 0 ]]; then FRAMERATE="$newf"; printf '%s' "$FRAMERATE" >"$CFG_FPS"; fi
fi
elif [[ "$pick" == "$(msg menu_toggle_audio "$audio_display")" ]]; then
if [[ "$AUDIO" == "on" ]]; then AUDIO="off"; else AUDIO="on"; fi
printf '%s' "$AUDIO" >"$CFG_AUDIO"
elif [[ "$pick" == "$(msg menu_set_codec "$CODEC")" ]]; then
# 仅保留 CPU (libx264) 与所有常见 VAAPI 编码选项CPU 放在首位
local newc; newc="$(menu_pick "$(msg title_select_codec)" \
"libx264" "h264_vaapi" "hevc_vaapi" "av1_vaapi" "vp9_vaapi")" || continue
CODEC="$newc"; printf '%s' "$CODEC" >"$CFG_CODEC"
elif [[ "$pick" == "$(msg menu_set_filefmt "$ff_display")" ]]; then
choose_filefmt_menu
elif [[ "$pick" == "$(msg menu_set_render "$render_display_now")" ]]; then
choose_render_menu
elif [[ "$pick" == "$(msg menu_back)" ]]; then
return 0
fi
# loop to refresh values instantly
done
}
# ---------- Mode selection ----------
decide_mode() {
case "${RECORD_MODE,,}" in
full|fullscreen) MODE_DECIDED="full"; return 0 ;;
region|area) MODE_DECIDED="region"; return 0 ;;
*) ;;
esac
local L_FULL L_REGION L_GIF L_SETTINGS L_EXIT
case "$(lang_code)" in
zh) L_FULL="$(msg menu_fullscreen)"; L_REGION="$(msg menu_region)"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
ja) L_FULL="$(msg menu_fullscreen)"; L_REGION="$(msg menu_region)"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
*) L_FULL="Fullscreen"; L_REGION="Region"; L_GIF="$(msg menu_gif_region)"; L_SETTINGS="$(msg menu_settings)"; L_EXIT="$(msg menu_exit)";;
esac
local title; title="$(msg title_mode)"
while :; do
# ORDER: Fullscreen -> Region -> GIF -> Settings -> Exit
# [FIXED] 调整菜单顺序以匹配图片要求全屏在最前GIF在区域之后
local pick; pick="$(menu_pick "$title" "$L_FULL" "$L_REGION" "$L_GIF" "$L_SETTINGS" "$L_EXIT")" || return 130
if [[ "$pick" == "$L_FULL" ]]; then MODE_DECIDED="full"; return 0
elif [[ "$pick" == "$L_REGION" ]]; then MODE_DECIDED="region"; return 0
elif [[ "$pick" == "$L_GIF" ]]; then MODE_DECIDED="region"; IS_GIF_MODE="true"; return 0
elif [[ "$pick" == "$L_SETTINGS" ]]; then show_settings_menu; continue
elif [[ "$pick" == "$L_EXIT" ]]; then return 130
else return 130; fi
done
}
# ---------- Helpers ----------
geom_token() {
local g="$1"
awk 'NF==2{split($1,a,","); split($2,b,"x");
if(a[1]!=""){printf "%sx%s@%s,%s",b[1],b[2],a[1],a[2]}}' <<<"$g"
}
pretty_dur() {
local dur="${1:-0}"
[[ "$dur" =~ ^[0-9]+$ ]] || dur=0
if ((dur>=3600)); then printf "%d:%02d:%02d" $((dur/3600)) $(((dur%3600)/60)) $((dur%60))
else printf "%02d:%02d" $((dur/60)) $((dur%60)); fi
}
json_escape() { sed ':a;N;$!ba;s/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g'; }
# ================== Start / Stop ==================
start_rec() {
if is_running; then echo "$(msg already_running)"; exit 0; fi
has wf-recorder || { echo "$(msg err_wf_not_found)"; exit 1; }
MODE_DECIDED=""
IS_GIF_MODE="false"
if ! decide_mode; then
echo "$(msg cancel_no_mode)"; emit_waybar_signal; exit 130
fi
local mode="$MODE_DECIDED"
# [NEW] GIF 模式检查
if [[ "$IS_GIF_MODE" == "true" ]]; then
if ! has ffmpeg; then echo "$(msg err_need_ffmpeg)"; emit_waybar_signal; exit 1; fi
# GIF 模式强制使用 mp4 作为中间格式,因为 mp4 兼容性好且编码速度快
SAVE_EXT="mp4"
touch "$GIF_MARKER"
else
rm -f "$GIF_MARKER"
fi
local marker="" output="" GEOM="" gtok=""
local -a args
args=( -c "$CODEC" )
local ROOT_DIR TARGET_DIR
ROOT_DIR="$(get_save_dir)"
if [[ "$mode" == "full" ]]; then TARGET_DIR="$ROOT_DIR/${SAVE_SUBDIR_FS}"; else TARGET_DIR="$ROOT_DIR"; fi
mkdir -p "$TARGET_DIR"
if [[ "$mode" == "full" ]]; then
output="$(decide_output)" || { echo "$(msg cancel_no_output)"; emit_waybar_signal; exit 130; }
[[ -n "$output" ]] && args+=( -o "$output" )
marker="FS${output:+-$output}"
else
if [[ -n "$REC_AREA" ]]; then
GEOM="$REC_AREA"
else
has slurp || { echo "$(msg err_need_slurp)"; emit_waybar_signal; exit 1; }
set +e; GEOM="$(slurp)"; local rc=$?; set -e
if [[ $rc -ne 0 || -z "${GEOM// /}" ]]; then echo "$(msg cancel_no_region)"; emit_waybar_signal; exit 130; fi
fi
GEOM="$(echo -n "$GEOM" | tr -s '[:space:]' ' ')"
args+=( -g "$GEOM" )
if [[ "${GEOM_IN_NAME,,}" == "on" ]]; then gtok="$(geom_token "$GEOM")"; marker="REGION${gtok:+-$gtok}"; else marker="REGION"; fi
fi
local ts safe_title base SAVE_PATH ext
ts="$(date +'%Y-%m-%d-%H%M%S')"; safe_title="${TITLE// /_}"
base="$ts${safe_title:+-$safe_title}-${marker}"
ext="$(choose_ext)"
SAVE_PATH="$TARGET_DIR/$base.$ext"
args=( --file "$SAVE_PATH" "${args[@]}" )
# Render device
local dev; dev="$(pick_render_device)"; [[ -n "$dev" ]] && args+=( -d "$dev" )
# Audio
case "$AUDIO" in off|OFF|0|false) ;; on|ON|1|true|"") args+=( --audio ) ;; *) args+=( --audio="$AUDIO" ) ;; esac
# Framerate
if [[ -n "$FRAMERATE" ]]; then
if [[ "$FRAMERATE" =~ ^[0-9]+$ && "$FRAMERATE" -gt 0 ]]; then args+=( --framerate "$FRAMERATE" )
else printf '%s\n' "$(msg warn_invalid_fps "$FRAMERATE")" >&2; fi
fi
# Pixel format
if [[ "$CODEC" == *"_vaapi" ]]; then args+=( -F "scale_vaapi=format=nv12:out_range=full:out_color_primaries=bt709" )
else args+=( -F "format=yuv420p" ); fi
# === 不保存日志:仅在 DEBUG=on 时将 wf-recorder 输出到终端 ===
if [[ "${DEBUG,,}" == "on" ]]; then
echo "DEBUG=on: running wf-recorder in foreground"
echo "Command: wf-recorder ${args[*]}"
wf-recorder "${args[@]}" 2>&1 &
local pid=$!
echo "$pid" >"$PIDFILE"
date +%s >"$STARTFILE"
echo "$SAVE_PATH" >"$SAVEPATH_FILE"
echo "$mode" >"$MODEFILE"
local note; if [[ "$mode" == "full" ]]; then note="$(msg notif_started_full "$output" "$SAVE_PATH")"; else note="$(msg notif_started_region "$SAVE_PATH")"; fi
[[ -n "$dev" ]] && note+="$(msg notif_device_suffix "$dev")"
echo "$note";
emit_waybar_signal
start_tick
return 0
fi
# 非 DEBUG后台运行且不保存任何日志与原脚本行为相近
setsid nohup wf-recorder "${args[@]}" >/dev/null 2>&1 &
local pid=$!
echo "$pid" >"$PIDFILE"
date +%s >"$STARTFILE"
echo "$SAVE_PATH" >"$SAVEPATH_FILE"
echo "$mode" >"$MODEFILE"
local note; if [[ "$mode" == "full" ]]; then note="$(msg notif_started_full "$output" "$SAVE_PATH")"; else note="$(msg notif_started_region "$SAVE_PATH")"; fi
[[ -n "$dev" ]] && note+="$(msg notif_device_suffix "$dev")"
echo "$note";
emit_waybar_signal
start_tick
}
stop_rec() {
if ! is_running; then echo "$(msg not_running)"; emit_waybar_signal; exit 0; fi
local pid; read -r pid <"$PIDFILE"
kill -INT "$pid" 2>/dev/null || true
for _ in {1..40}; do sleep 0.1; is_running || break; done
is_running && kill -TERM "$pid" 2>/dev/null || true
sleep 0.2
is_running && kill -KILL "$pid" 2>/dev/null || true
# 停止后清理运行时状态文件
rm -f "$PIDFILE" "$MODEFILE"
stop_tick
local save_path=""; [[ -r "$SAVEPATH_FILE" ]] && read -r save_path <"$SAVEPATH_FILE"
# --- [NEW] GIF Conversion Logic ---
if [[ -f "$GIF_MARKER" ]]; then
rm -f "$GIF_MARKER"
if [[ -n "$save_path" && -f "$save_path" ]]; then
notify "$(msg notif_processing_gif)"
# [FIXED] 确保 GIF 目录存在: .../wf-recorder/gif/
local gif_dir="$(get_save_dir)/gif"
mkdir -p "$gif_dir"
local filename=$(basename "$save_path")
local gif_out="$gif_dir/${filename%.*}.gif"
# 使用您提供的滤镜字符串
local filters="fps=$GIF_FPS,scale=$GIF_WIDTH:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=$GIF_STATS_MODE[p];[s1][p]paletteuse=dither=$GIF_DITHER_MODE"
# 运行转换,如果成功则删除原文件
if ffmpeg -y -v error -i "$save_path" -vf "$filters" "$gif_out"; then
rm "$save_path"
save_path="$gif_out"
# 更新保存路径以便后续通知使用
echo "$save_path" > "$SAVEPATH_FILE"
else
notify "$(msg notif_gif_failed)"
fi
fi
fi
# -----------------------------------
if [[ -n "$save_path" && -f "$save_path" ]]; then
# 生成不带后缀的 latest例如.../latest
ln -sf "$(basename "$save_path")" "$(dirname "$save_path")/latest" || true
# --- [NEW] Auto Copy to Clipboard (as File Object) ---
local cp_note=""
if command -v wl-copy >/dev/null; then
# [CRITICAL FIX] 使用 text/uri-list MIME 类型,并添加 file:// 前缀
# 这会让剪贴板将其视为一个“文件”,允许在文件管理器或聊天软件中直接粘贴
echo "file://${save_path}" | wl-copy --type text/uri-list
cp_note=" $(msg notif_copied)"
fi
# ------------------------------------
local s; s="$(msg notif_saved "$save_path")${cp_note}"; echo "$s"; notify "$s"
else
local s; s="$(msg notif_stopped)"; echo "$s"; notify "$s"
fi
if [[ "${PKILL_AFTER_STOP,,}" != "off" ]]; then
for sig in INT TERM KILL; do
pgrep -x -u "$UID" "$APP" >/dev/null || break
pkill -"$sig" -x -u "$UID" "$APP" 2>/dev/null || true
sleep 0.1
done
fi
emit_waybar_signal
}
# ================== Waybar JSON/status ==================
tooltip_idle_text() {
case "$(lang_code)" in
zh) cat <<'EOF'
屏幕录制wf-recorder
左键:打开录制菜单
右键:强制关闭
EOF
;;
ja) cat <<'EOF'
画面録画wf-recorder
左クリック:録画メニューを開く
右クリック:強制停止
EOF
;;
*) cat <<'EOF'
Screen recording (wf-recorder)
Left click: open recording menu
Right click: force stop
EOF
;;
esac
}
tooltip_recording_text() { # $1 elapsed, $2 filepath, $3 mode: full|region
local t="$1" p="${2:-}" m="${3:-}"
local mode_label
case "$m" in full|fullscreen) mode_label="$(msg mode_full)";; region|area) mode_label="$(msg mode_region)";; *) mode_label="";; esac
case "$(lang_code)" in
zh) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "录制中(%s\n已用时%s\n文件%s\n" "$mode_label" "$t" "$p" || printf "录制中\n已用时%s\n文件%s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "录制中(%s\n已用时%s\n" "$mode_label" "$t" || printf "录制中\n已用时%s\n" "$t"; } ;;
ja) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "録画中(%s\n経過時間%s\nファイル%s\n" "$mode_label" "$t" "$p" || printf "録画中\n経過時間%s\nファイル%s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "録画中(%s\n経過時間%s\n" "$mode_label" "$t" || printf "録画中\n経過時間%s\n" "$t"; } ;;
*) [[ -n "$p" ]] && { [[ -n "$mode_label" ]] && printf "Recording (%s)\nElapsed: %s\nFile: %s\n" "$mode_label" "$t" "$p" || printf "Recording\nElapsed: %s\nFile: %s\n" "$t" "$p"; } || { [[ -n "$mode_label" ]] && printf "Recording (%s)\nElapsed: %s\n" "$mode_label" "$t" || printf "Recording\nElapsed: %s\n" "$t"; } ;;
esac
}
pretty_status_json() {
local text tooltip class alt
if is_running; then
local start=0; [[ -r "$STARTFILE" ]] && read -r start <"$STARTFILE" || true
[[ "$start" =~ ^[0-9]+$ ]] || start=0
local now dur; now="$(date +%s)"; dur=$((now - start)); (( dur < 0 )) && dur=0
local t; t="$(pretty_dur "$dur")"
local save_path=""; [[ -r "$SAVEPATH_FILE" ]] && read -r save_path <"$SAVEPATH_FILE" || true
local mode=""; [[ -r "$MODEFILE" ]] && read -r mode <"$MODEFILE" || true
text="$ICON_REC$t"
tooltip="$(tooltip_recording_text "$t" "$save_path" "$mode")"
class="recording"; alt="rec"
else
text="$ICON_IDLE"; tooltip="$(tooltip_idle_text)"; class="idle"; alt="idle"
fi
printf '{"text":"%s","tooltip":"%s","class":"%s","alt":"%s"}\n' \
"$(printf '%s' "$text" | json_escape)" \
"$(printf '%s' "$tooltip" | json_escape)" \
"$class" "$alt"
}
status_rec() {
local json="${1:-}"
if [[ "$json" == "--json" ]]; then
pretty_status_json
else
if is_running; then
local start=0; [[ -r "$STARTFILE" ]] && read -r start <"$STARTFILE" || true
[[ "$start" =~ ^[0-9]+$ ]] || start=0
local now dur; now="$(date +%s)"; dur=$((now - start)); (( dur < 0 )) && dur=0
printf "%s%s\n" "$ICON_REC" "$(pretty_dur "$dur")"
else
echo "$ICON_IDLE"
fi
fi
}
# ================== Main ==================
case "${1:-toggle}" in
start) start_rec ;;
stop) stop_rec ;;
status) status_rec ;;
status-json) status_rec --json ;;
waybar) status_rec --json ;;
is-active) if is_running; then exit 0; else exit 1; fi ;;
toggle) is_running && stop_rec || start_rec ;;
settings) show_settings_menu ;;
*) echo "Usage: $0 {start|stop|toggle|status|status-json|waybar|is-active|settings}"; exit 2 ;;
esac

494
.config/waybar/style.css Executable file
View File

@@ -0,0 +1,494 @@
@import "colors.css";
/* @import "/home/shorin/.cache/wal/colors-waybar.css"; */
/* 核心逻辑:全相对单位 (em) 改造
基准字体大小18px
如果需要整体放大缩小,只需修改下方的 18px 即可
*/
* {
border: none;
border-radius: 0;
font-family: "JetBrainsMono Nerd Font Propo","LXGW WenKai Screen","JetBrains Maple Mono";
/* font-family:"0xProto Nerd Font Propo"; */
font-size: 16.6px; /* 基准大小 */
opacity: 1;
}
window#waybar {
background: transparent;
color: @on_surface;
}
/* 鼠标悬浮信息提示 */
tooltip {
background: @secondary_container;
border: 0.17em solid @outline; /* 3px */
opacity: 1;
}
tooltip label {
color: @on_surface;
font-size: 0.89em; /* 16px */
}
/* 工作区 */
#workspaces button {
padding: 0px 0.56em; /* 10px */
background: @surface;
color: @tertiary_container;
}
/* #workspaces label {
font-size: 1.22em;
} */
#workspaces button:hover {
background: @on_tertiary;
}
#workspaces button.active{
color:@tertiary;
}
#custom-right_div.5 {
background: @surface_container_high;
color: @surface;
font-size: 1.39em; /* 25px */
padding: 0px;
}
/* waybar niri taskbar */
.niri-taskbar {
background: @surface_container_high;
padding: 0 0 0 0.28em; /* 5px */
}
.niri-taskbar button:hover {
background: @surface_container;
}
.niri-taskbar button.focused {
background: @surface;
}
.niri-taskbar button.urgent {
background-color: @tertiary;
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: steps(12);
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes blink {
to {
background-color: @primary;
color: @error;
}
}
/* 窗口名 */
#window {
padding: 0px 0.56em; /* 10px */
background-color: @surface_container_high;
color: @on_surface;
}
#window label {
font-size: 0.89em; /* 16px */
}
window#waybar.empty #window {
background-color: @surface_container_high;
}
#custom-right_div.6 {
color: @surface_container_high;
font-size: 1.39em; /* 25px */
padding: 0px;
}
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* 中间 */
#custom-left_div.3 {
color: @surface_container_high;
padding: 0px;
font-size: 1.39em; /* 25px */
}
#bluetooth {
padding: 0px 0.3em 0px 0.5em; /* 7px */
font-size: 1.25em; /* 20px */
}
#bluetooth.disabled{
padding: 0px 0.22em 0px 0.46em ;
}
#network {
padding: 0px 0.34em; /* 7px */
font-size: 1.2em; /* 22px */
}
#custom-settings {
padding: 0px 0.39em; /* 7px */
font-size: 1.06em; /* 19px */
}
#custom-screenshot {
padding: 0 0.4em; /* 7px */
font-size: 1.28em; /* 22px */
}
/* wf-recoder脚本 */
#custom-wfrec {
padding: 0 0.3em; /* 7px */
font-size: 1.1em;
}
#custom-wfrec.recording,
#custom-wfrec,
#bluetooth,
#network,
#custom-settings,
#custom-screenshot {
background-color: @surface_container_high;
color: @secondary;
}
/* 录制中 */
#custom-wfrec.recording {
color: @error;
}
/* #custom-wfrec,
#custom-screenshot{
color: @secondary;
} */
#custom-left_div.2 {
background-color: @surface_container_high;
color: @tertiary;
padding: 0px 0px;
font-size: 1.39em; /* 25px */
}
#power-profiles-daemon,
#custom-colorpicker,
#idle_inhibitor {
background-color: @tertiary;
color: @on_tertiary;
padding: 0px 0.36em; /* 6px */
}
#idle_inhibitor.activated{
padding: 0px 0.4em 0px 0.4em;
}
#power-profiles-daemon{
padding: 0px 0.4em 0em 0.36em;
}
#power-profiles-daemon.performance {
color: @on_error;
font-size: 1.28em; /* 23px */
padding: 0px 0.32em 0px 0.45em; /* 9px 8px */
}
#power-profiles-daemon.balanced {
color: @on_tertiary;
}
#power-profiles-daemon.power-saver {
color: #1aa052;
}
/* 菜单 */
#custom-left_div.11 {
background-color: @tertiary;
color: @surface_container
}
#custom-applauncher {
font-size: 1.39em; /* 25px */
padding: 0px 0.39em; /* 7px */
margin: 0px;
background-color: @primary;
color: @on_primary;
}
#custom-right_div.1,
#custom-left_div.1 {
background-color: @surface_container;
color: @primary;
}
#custom-left_div.1,
#custom-right_div.1 {
padding: 0px;
margin: 0px;
font-size: 1.39em; /* 25px */
}
#custom-left_div.11,
#custom-right_div.11 {
margin: 0px;
padding: 0px;
font-size: 1.39em; /* 25px */
}
#custom-right_div.11 {
background-color: @secondary;
color: @surface_container;
}
/*中间右边第二级*/
#clock {
padding: 0px 0.45em 0em 0.45em; /* 8px */
}
#clock {
background-color: @secondary;
color: @on_secondary;
}
#custom-right_div.2 {
background-color: @surface_container_high;
color: @secondary;
padding: 0px;
font-size: 1.39em; /* 25px */
}
#custom-cava {
background-color: @surface_container_high;
color: @primary;
padding: 0px 0.35em; /* 7px */
}
#mpris {
background-color: @surface_container_high;
color: @primary;
padding: 0px 0.39em 0px 0px; /* 7px 0 0 */
}
#custom-right_div.3 {
color: @surface_container_high;
padding: 0px 0.1em 0px 0px ;
font-size: 1.39em; /* 25px */
}
#custom-right_div.4 {
/* background-color: @surface_container_high; */
color: @on_secondary;
padding: 0px;
font-size: 1.39em; /* 25px */
}
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* —————————————————————————————————————————————————————————————————————————— */
/* 右侧 */
#custom-left_div.7 {
/* background-color: @surface_container_high; */
color: @surface_bright;
font-size: 1.39em; /* 25px */
padding: 0px;
}
#custom-updates {
border-radius: 0px;
padding: 0px 0.17em 0px 0.39em; /* 3px 7px */
background-color: @surface_bright;
color: @error;
}
#tray {
padding: 0px 0.39em 0px 0.39em; /* 7px */
font-size: 1.11em; /* 20px */
background-color: @surface_bright;
}
#custom-left_div.4 {
background-color: @surface_bright;
color: @surface_container_high;
padding: 0px;
font-size: 1.39em; /* 25px */
}
/* 亮度 */
#custom-ddcutil-day,
#custom-ddcutil-night,
#custom-ddcutil-sleep,
#custom-separator.1 {
background-color: @surface_container_high;
color: @tertiary;
padding: 0px 0.33em; /* 5px */
}
#backlight {
background-color: @surface_container_high;
color: @tertiary;
padding: 0px 0.28em 0px 0px; /* 5px */
}
#backlight-slider {
background-color: @surface_container_high;
padding: 0px 0.28em 0px 0px; /* 5px */
}
#backlight-slider slider {
min-height: 0px;
min-width: 0px;
opacity: 0;
background-image: none;
border: none;
box-shadow: none;
background: none;
}
#backlight-slider trough {
min-height: 0.56em; /* 10px */
min-width: 4.44em; /* 80px */
border-radius: 0.28em; /* 5px */
opacity: 0;
background-color: @background;
}
#backlight-slider highlight {
min-width: 0.56em; /* 10px */
border-radius: 0.28em; /* 5px */
background-color: @tertiary;
}
/* 音视频 */
#privacy {
padding: 0px 0.39em; /* 7px */
}
#privacy {
background-color: @surface_container_high;
color: @error;
}
#pulseaudio {
padding: 0px 0px 0px 0.28em; /* 5px */
}
#pulseaudio-slider {
padding: 0px 0px 0px 0.56em; /* 10px */
margin: 0px;
}
#pulseaudio-slider,
#pulseaudio {
background-color: @surface_container_high;
color: @tertiary;
}
#pulseaudio-slider slider {
min-height: 0px;
min-width: 0px;
opacity: 0;
background-image: none;
box-shadow: none;
background: none;
}
#pulseaudio-slider trough {
min-height: 0.56em; /* 10px */
min-width: 4.44em; /* 80px */
border-radius: 0.28em; /* 5px */
background-color: @surface;
}
#pulseaudio-slider highlight {
min-width: 0px;
border-radius: 0.28em; /* 5px */
background-color: @tertiary;
}
#custom-left_div.8 {
background-color: @surface_container_high;
color: @surface_container;
font-size: 1.39em; /* 25px */
padding: 0px;
}
/* 电池 */
#battery {
background-color: @surface_container;
color: @secondary;
padding: 0px 0.39em; /* 7px */
}
#battery.critical:not(.charging) {
background-color: @surface_container;
color: @error;
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: steps(12);
animation-iteration-count: infinite;
animation-direction: alternate;
padding: 0px 0.39em; /* 7px */
}
/* powermenu电源菜单 */
#custom-left_div.5 {
background-color: @surface_container;
color: @surface;
padding: 0px;
font-size: 1.39em; /* 25px */
}
#custom-wlogout {
padding: 0px 0.83em 0px 0.56em; /* 15px 10px */
font-size: 1.39em; /* 25px */
}
#custom-wlogout,
#custom-reboot,
#custom-lockscreen,
#custom-logout {
background-color: @surface;
color: @error;
padding: 0px 0.56em; /* 10px */
}
#clock.date {
padding: 0px 0.39em; /* 7px */
}
#custom-datelogo,
#clock.date {
background-color: @secondary_container;
color: @on_secondary_container;
}
#custom-swaync {
background-color: @surface_container_high;
color: @on_surface_container;
padding: 0px 0.83em; /* 15px */
}
#custom-mako {
background-color: @surface_container_high;
color: @on_surface_container;
padding: 0px 0.83em; /* 15px */
}
#custom-left_div.6 {
background-color: @surface;
color: @surface_container_high;
padding: 0px;
font-size: 1.39em; /* 25px */
}