backup Arch and Niri configuration

This commit is contained in:
DucklingDev
2026-09-15 12:53:56 +08:00
commit 6aee617c20
34 changed files with 5948 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Secrets and machine-local state
*.key
*.pem
*.token
*.secret
.env
.env.*
# Editor and OS noise
.DS_Store
*.swp
+39
View File
@@ -0,0 +1,39 @@
# Arch + Niri dotfiles
这是 Arch Linux + Niri 桌面环境的可迁移备份。仓库包含自己维护的配置、Waybar 歌词脚本、壁纸和软件包清单;不会包含浏览器缓存、登录状态、SSH 私钥或其他凭据。
## 内容
- `config/`:Niri、Waybar、Mako、Fuzzel、Waypaper、Kitty、Fcitx5 和 btop 配置
- `resources/musicfox-waybar-lyrics/`:Waybar 歌词模块脚本
- `resources/wallpaper/`:壁纸
- `packages/official.txt`:官方仓库软件包
- `packages/aur.txt`:AUR 软件包
- `system/system-notes.txt`:需要按新电脑调整的系统信息
## 在新电脑恢复
```bash
sudo pacman -S --needed - < packages/official.txt
yay -S --needed - < packages/aur.txt
mkdir -p ~/.config
cp -a config/niri config/waybar config/mako config/fuzzel config/waypaper config/kitty config/fcitx5 config/btop ~/.config/
cp -a resources/musicfox-waybar-lyrics ~/Documents/code/
mkdir -p ~/Pictures
cp -a resources/wallpaper ~/Pictures/
```
随后检查并修改:
1. `config/niri/config.kdl` 中的显示器名称、分辨率和位置。
2. `config/waybar/config.jsonc` 中歌词脚本的绝对路径。
3. 当前电脑没有安装的可选命令,例如 `swaylock`、`pavucontrol` 和 Polkit GNOME 代理。
4. NVIDIA 驱动、输入法和系统级 Btrfs/zram 配置。
恢复后检查配置:
```bash
niri validate
```
+286
View File
@@ -0,0 +1,286 @@
#? Config file for btop v.1.4.7
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
color_theme = "Default"
#* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = true
#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
truecolor = true
#* Set to true to force tty mode regardless if a real tty has been detected or not.
#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
force_tty = false
#* Option to disable presets. Either the default preset, custom presets, or all presets.
#* "Off" All presets are enabled.
#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled.
disable_presets = "Off"
#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
#* Use whitespace " " as separator between different presets.
#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
vim_keys = false
#* Disable all mouse events.
disable_mouse = false
#* Rounded corners on boxes, is ignored if TTY mode is ON.
rounded_corners = true
#* Use terminal synchronized output sequences to reduce flickering on supported terminals.
terminal_sync = true
#* Default symbols to use for graph creation, "braille", "block" or "tty".
#* "braille" offers the highest resolution but might not be included in all fonts.
#* "block" has half the resolution of braille but uses more common characters.
#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
graph_symbol = "braille"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_cpu = "default"
# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
graph_symbol_gpu = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_mem = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_net = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_proc = "default"
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
shown_boxes = "cpu mem net proc"
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
update_ms = 2000
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
proc_sorting = "cpu lazy"
#* Reverse sorting order, True or False.
proc_reversed = false
#* Show processes as a tree.
proc_tree = false
#* Use the cpu graph colors in the process list.
proc_colors = true
#* Use a darkening gradient in the process list.
proc_gradient = true
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
proc_per_core = false
#* Show process memory as bytes instead of percent.
proc_mem_bytes = true
#* Show cpu graph for each process.
proc_cpu_graphs = true
#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
proc_info_smaps = false
#* Show proc box on left side of screen instead of right.
proc_left = false
#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
proc_filter_kernel = false
#* Should the process list follow the selected process when detailed view is open.
proc_follow_detailed = true
#* In tree-view, always accumulate child process resources in the parent process.
proc_aggregate = false
#* Should cpu and memory usage display be preserved for dead processes when paused.
keep_dead_proc_usage = false
#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_upper = "Auto"
#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
#* Select from a list of detected attributes from the options menu.
cpu_graph_lower = "Auto"
#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
show_gpu_info = "Auto"
#* Toggles if the lower CPU graph should be inverted.
cpu_invert_lower = true
#* Set to True to completely disable the lower CPU graph.
cpu_single_graph = false
#* Show cpu box at bottom of screen instead of top.
cpu_bottom = false
#* Shows the system uptime in the CPU box.
show_uptime = true
#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
show_cpu_watts = true
#* Show cpu temperature.
check_temp = true
#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
cpu_sensor = "Auto"
#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
show_coretemp = true
#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
#* Example: "4:0 5:1 6:3"
cpu_core_map = ""
#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
temp_scale = "celsius"
#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
base_10_sizes = false
#* Show CPU frequency.
show_cpu_freq = true
#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
freq_mode = "first"
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
clock_format = "%X"
#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
background_update = true
#* Custom cpu model name, empty string to disable.
custom_cpu_name = ""
#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
disks_filter = ""
#* Show graphs instead of meters for memory values.
mem_graphs = true
#* Show mem box below net box instead of above.
mem_below_net = false
#* Count ZFS ARC in cached and available memory.
zfs_arc_cached = true
#* If swap memory should be shown in memory box.
show_swap = true
#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
swap_disk = true
#* If mem box should be split to also show disks info.
show_disks = true
#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
only_physical = true
#* Read disks list from /etc/fstab. This also disables only_physical.
use_fstab = true
#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
zfs_hide_datasets = false
#* Set to true to show available disk space for privileged users.
disk_free_priv = false
#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
show_io_stat = true
#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
io_mode = false
#* Set to True to show combined read/write io graphs in io mode.
io_graph_combined = false
#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
#* Example: "/mnt/media:100 /:20 /boot:1".
io_graph_speeds = ""
#* Swap the positions of the upload and download speed graphs. When true, upload will be on top.
swap_upload_download = false
#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
net_download = 100
net_upload = 100
#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
net_auto = true
#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
net_sync = true
#* Starts with the Network Interface specified here.
net_iface = ""
#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
base_10_bitrate = "Auto"
#* Show battery stats in top right if battery is present.
show_battery = true
#* Which battery to use if multiple are present. "Auto" for auto detection.
selected_battery = "Auto"
#* Show power stats of battery next to charge indicator.
show_battery_watts = true
#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
log_level = "WARNING"
#* Automatically save current settings to config file on exit.
save_config_on_exit = true
#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
nvml_measure_pcie_speeds = true
#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
rsmi_measure_pcie_speeds = true
#* Horizontally mirror the GPU graph.
gpu_mirror_graph = true
#* Set which GPU vendors to show. Available values are "nvidia amd intel apple"
shown_gpus = "nvidia amd intel apple"
#* Custom gpu0 model name, empty string to disable.
custom_gpu_name0 = ""
#* Custom gpu1 model name, empty string to disable.
custom_gpu_name1 = ""
#* Custom gpu2 model name, empty string to disable.
custom_gpu_name2 = ""
#* Custom gpu3 model name, empty string to disable.
custom_gpu_name3 = ""
#* Custom gpu4 model name, empty string to disable.
custom_gpu_name4 = ""
#* Custom gpu5 model name, empty string to disable.
custom_gpu_name5 = ""
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
# Vertical Candidate List
Vertical Candidate List=False
# Use mouse wheel to go to prev or next page
WheelForPaging=True
# Font
Font="Sans 10"
# Menu Font
MenuFont="Sans 10"
# Tray Font
TrayFont="Sans Bold 10"
# Tray Label Outline Color
TrayOutlineColor=#000000
# Tray Label Text Color
TrayTextColor=#ffffff
# Prefer Text Icon
PreferTextIcon=False
# Show Layout Name In Icon
ShowLayoutNameInIcon=True
# Use input method language to display text
UseInputMethodLanguageToDisplayText=True
# Theme
Theme=catppuccin-mocha-mauve
# Dark Theme
DarkTheme=catppuccin-mocha-mauve
# Follow system light/dark color scheme
UseDarkTheme=False
# Follow system accent color if it is supported by theme and desktop
UseAccentColor=True
# Use Per Screen DPI on X11
PerScreenDPI=False
# Force font DPI on Wayland
ForceWaylandDPI=0
# Enable fractional scale under Wayland
EnableFractionalScale=True
+3
View File
@@ -0,0 +1,3 @@
# 隐藏通知
HiddenNotifications=
+83
View File
@@ -0,0 +1,83 @@
[Hotkey]
# Enumerate when holding modifier of Toggle key
EnumerateWithTriggerKeys=True
# Enumerate Input Method Forward
EnumerateForwardKeys=
# Enumerate Input Method Backward
EnumerateBackwardKeys=
# Skip first input method while enumerating
EnumerateSkipFirst=False
# Time limit in milliseconds for triggering modifier key shortcuts
ModifierOnlyKeyTimeout=250
[Hotkey/TriggerKeys]
0=Control+space
1=Zenkaku_Hankaku
2=Hangul
[Hotkey/ActivateKeys]
0=Hangul_Hanja
[Hotkey/DeactivateKeys]
0=Hangul_Romaja
[Hotkey/AltTriggerKeys]
0=Shift_L
[Hotkey/EnumerateGroupForwardKeys]
0=Super+space
[Hotkey/EnumerateGroupBackwardKeys]
0=Shift+Super+space
[Hotkey/PrevPage]
0=Up
[Hotkey/NextPage]
0=Down
[Hotkey/PrevCandidate]
0=Shift+Tab
[Hotkey/NextCandidate]
0=Tab
[Hotkey/TogglePreedit]
0=Control+Alt+P
[Behavior]
# Activate input method by default
ActiveByDefault=False
# Reset state on Focus In
resetStateWhenFocusIn=No
# Share Input State
ShareInputState=No
# Show preedit in application
PreeditEnabledByDefault=True
# Show Input Method Information when switch input method
ShowInputMethodInformation=True
# Show Input Method Information when changing focus
showInputMethodInformationWhenFocusIn=False
# Show compact input method information
CompactInputMethodInformation=True
# Show first input method information
ShowFirstInputMethodInformation=True
# Default Candidates per page
DefaultPageSize=5
# Override XKB Option
OverrideXkbOption=False
# Custom XKB Option
CustomXkbOption=
# Force Enabled Addons
EnabledAddons=
# Force Disabled Addons
DisabledAddons=
# Preload input method to be used by default
PreloadInputMethod=True
# Allow input method in the password field
AllowInputMethodForPassword=False
# Show preedit text when typing password
ShowPreeditForPassword=False
# Interval of saving user data in minutes
AutoSavePeriod=30
+23
View File
@@ -0,0 +1,23 @@
[Groups/0]
# Group Name
Name=Default
# Layout
Default Layout=us
# Default Input Method
DefaultIM=rime
[Groups/0/Items/0]
# Name
Name=keyboard-us
# Layout
Layout=
[Groups/0/Items/1]
# Name
Name=rime
# Layout
Layout=
[GroupOrder]
0=Default
+23
View File
@@ -0,0 +1,23 @@
[Groups/0]
# Group Name
Name=Default
# Layout
Default Layout=us
# Default Input Method
DefaultIM=rime
[Groups/0/Items/0]
# Name
Name=keyboard-us
# Layout
Layout=
[Groups/0/Items/1]
# Name
Name=rime
# Layout
Layout=
[GroupOrder]
0=Default
+36
View File
@@ -0,0 +1,36 @@
[main]
# 紧凑、清晰的应用启动器布局
font=sans-serif:size=13
use-bold=yes
prompt=“⌕ ”
width=27
lines=10
horizontal-pad=24
vertical-pad=14
inner-pad=8
image-size-ratio=0.55
match-counter=yes
[colors]
# 格式为 RRGGBBAA;背景 alpha=d6,约 84% 不透明
background=111827d6
text=e5e7ebff
prompt=7fc8ffff
placeholder=94a3b8ff
input=f8fafcff
match=7fc8ffff
# 选中项也保留轻微透明感
selection=2563a6b8
selection-text=ffffffff
selection-match=b9e4ffff
counter=94a3b8ff
border=7fc8ffb3
[border]
width=2
radius=14
[key-bindings]
# 整页浏览:Page Up / Page Down,或 Ctrl+方向键
prev-page=Page_Up KP_Page_Up Control+Up
next-page=Page_Down KP_Page_Down Control+Down
+2
View File
@@ -0,0 +1,2 @@
font_family WenQuanYi Micro Hei Mono
font_size 13
+40
View File
@@ -0,0 +1,40 @@
# Mako 通知守护进程配置
font=Sans 11
background-color=#202020F2
text-color=#F2F2F2FF
width=360
height=160
outer-margin=16
margin=8
padding=12
border-size=2
border-color=#7FC8FFFF
border-radius=8
progress-color=over #7FC8FFFF
icons=1
max-icon-size=48
icon-border-radius=8
markup=1
actions=1
max-visible=5
max-history=100
history=1
default-timeout=5000
anchor=top-right
layer=overlay
[urgency=low]
border-color=#505050FF
default-timeout=3000
[urgency=critical]
background-color=#3A1010F2
border-color=#FF6B6BFF
default-timeout=0
ignore-timeout=1
+725
View File
@@ -0,0 +1,725 @@
// ============================================================================
// Niri 主配置
// KDL 语法文档:https://kdl.dev
// Niri 配置文档:https://niri-wm.github.io/niri/Configuration:-Introduction
// ============================================================================
environment {
// 基础 locale 使用系统已经安装的英文环境
LANG "en_US.UTF-8"
// 软件界面优先显示简体中文,缺少翻译时回退英文
LANGUAGE "zh_CN:en_US"
// 避免继承的 LC_ALL 覆盖 LANG
LC_ALL null
// Fcitx5 compatibility for applications such as WeChat.
GTK_IM_MODULE "fcitx"
QT_IM_MODULE "fcitx"
XMODIFIERS "@im=fcitx"
}
// ----------------------------------------------------------------------------
// 1. 输入设备与手势
// ----------------------------------------------------------------------------
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// 关闭鼠标触碰屏幕角落时自动打开 Overview;Mod+O 快捷键不受影响。
gestures {
hot-corners {
off
}
}
// ----------------------------------------------------------------------------
// 2. 显示器
// 小米外接屏位于上方,笔记本内屏位于下方并水平居中。
// ----------------------------------------------------------------------------
output "eDP-1" {
mode "1920x1080@59.977"
scale 1
transform "normal"
position x=320 y=1440
}
output "HDMI-A-1" {
mode "2560x1440@59.951"
scale 1
transform "normal"
position x=0 y=0
}
// ----------------------------------------------------------------------------
// 3. 窗口布局与外观
// ----------------------------------------------------------------------------
layout {
// 让 Overview 的工作区底色透明,显示固定在 backdrop 中的 Awww 壁纸。
background-color "transparent"
// Set gaps around windows in logical pixels.
gaps 16
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
// You can change how the focus ring looks.
focus-ring {
// Uncomment this line to disable the focus ring.
// off
// How many logical pixels the ring extends out from the windows.
width 2
// Colors can be set in a variety of ways:
// - CSS named colors: "red"
// - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa"
// - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others.
// Color of the ring on the active monitor.
active-color "#7fc8ff"
// Color of the ring on inactive monitors.
//
// The focus ring only draws around the active window, so the only place
// where you can see its inactive-color is on other monitors.
inactive-color "#505050"
// You can also use gradients. They take precedence over solid colors.
// Gradients are rendered the same as CSS linear-gradient(angle, from, to).
// The angle is the same as in linear-gradient, and is optional,
// defaulting to 180 (top-to-bottom gradient).
// You can use any CSS linear-gradient tool on the web to set these up.
// Changing the color space is also supported, check the wiki for more info.
//
// active-gradient from="#80c8ff" to="#c7ff7f" angle=45
// You can also color the gradient relative to the entire view
// of the workspace, rather than relative to just the window itself.
// To do that, set relative-to="workspace-view".
//
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can also add a border. It's similar to the focus ring, but always visible.
border {
// The settings are the same as for the focus ring.
// If you enable the border, you probably want to disable the focus ring.
off
width 4
active-color "#ffc87f"
inactive-color "#505050"
// Color of the border around windows that request your attention.
urgent-color "#9b0000"
// Gradients can use a few different interpolation color spaces.
// For example, this is a pastel rainbow gradient via in="oklch longer hue".
//
// active-gradient from="#e5989b" to="#ffb4a2" angle=45 relative-to="workspace-view" in="oklch longer hue"
// inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view"
}
// You can enable drop shadows for windows.
shadow {
// Uncomment the next line to enable shadows.
// on
// By default, the shadow draws only around its window, and not behind it.
// Uncomment this setting to make the shadow draw behind its window.
//
// Note that niri has no way of knowing about the CSD window corner
// radius. It has to assume that windows have square corners, leading to
// shadow artifacts inside the CSD rounded corners. This setting fixes
// those artifacts.
//
// However, instead you may want to set prefer-no-csd and/or
// geometry-corner-radius. Then, niri will know the corner radius and
// draw the shadow correctly, without having to draw it behind the
// window. These will also remove client-side shadows if the window
// draws any.
//
// draw-behind-window true
// You can change how shadows look. The values below are in logical
// pixels and match the CSS box-shadow properties.
// Softness controls the shadow blur radius.
softness 30
// Spread expands the shadow.
spread 5
// Offset moves the shadow relative to the window.
offset x=0 y=5
// You can also change the shadow color and opacity.
color "#0007"
}
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// 抵消 10px 顶部外间距,保留约 6px 距离。
top -10
// bottom 64
}
}
// ----------------------------------------------------------------------------
// 4. 会话与自启动
// ----------------------------------------------------------------------------
// 状态栏
spawn-at-startup "waybar"
// Polkit 图形认证代理
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
// 桌面通知服务
spawn-at-startup "mako"
// 中文输入法
spawn-at-startup "fcitx5" "-d"
// Awww 壁纸守护进程,供 Waypaper 设置和切换壁纸。
spawn-at-startup "awww-daemon"
// 将 Awww 壁纸固定在 Overview 的背景层中。
layer-rule {
match namespace="^awww-daemon$"
place-within-backdrop true
}
// To run a shell command (with variables, pipes, etc.), use spawn-sh-at-startup:
// spawn-sh-at-startup "qs -c ~/source/qs/MyAwesomeShell"
hotkey-overlay {
// Uncomment this line to disable the "Important Hotkeys" pop-up at startup.
// skip-at-startup
}
// 尽可能移除应用自己的标题栏和客户端装饰;已运行的应用需重启。
prefer-no-csd
// ----------------------------------------------------------------------------
// 5. 截图与动画
// ----------------------------------------------------------------------------
// 截图文件名包含拍摄日期和时间。
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// You can also set this to null to disable saving screenshots to disk.
// screenshot-path null
// Animation settings.
// The wiki explains how to configure individual animations:
// https://niri-wm.github.io/niri/Configuration:-Animations
animations {
// Uncomment to turn off all animations.
// off
// Slow down all animations by this factor. Values below 1 speed them up instead.
// slowdown 3.0
}
// ----------------------------------------------------------------------------
// 6. 窗口规则
// ----------------------------------------------------------------------------
// 修复 WezTerm 初次打开时的尺寸问题。
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
// Firefox 画中画窗口默认浮动。
window-rule {
// This app-id regular expression will work for both:
// - host Firefox (app-id is "firefox")
// - Flatpak Firefox (app-id is "org.mozilla.firefox")
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
open-floating true
}
// Example: block out two password managers from screen capture.
// (This example rule is commented out with a "/-" in front.)
/-window-rule {
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
match app-id=r#"^org\.gnome\.World\.Secrets$"#
block-out-from "screen-capture"
// Use this instead if you want them visible on third-party screenshot tools.
// block-out-from "screencast"
}
// 全局使用 8px 圆角;焦点环只画在窗口外侧,不侵入透明背景。
window-rule {
geometry-corner-radius 8
clip-to-geometry true
draw-border-with-background false
}
// Kitty 使用 85% 不透明度(15% 透明)。
window-rule {
match app-id=r#"^kitty$"#
opacity 0.85
}
// Visual Studio Code 的实际 app-id 为 code,使用 85% 不透明度。
window-rule {
match app-id=r#"^code$"#
opacity 0.85
}
// 微信使用 85% 不透明度(15% 透明)。
window-rule {
match app-id=r#"(?i)^wechat$"#
opacity 0.85
}
// Telegram 使用 90% 不透明度(10% 透明)。
window-rule {
match app-id=r#"(?i)^org\.telegram\.desktop(\.desktop)?$"#
opacity 0.9
}
// 微信图片和视频查看窗口保持不透明,并默认悬浮。
window-rule {
match app-id=r#"(?i)^wechat$"# title=r#"^图片和视频$"#
opacity 1.0
open-floating true
}
// 微信朋友圈窗口保持完全不透明。
window-rule {
match app-id=r#"(?i)^wechat$"# title=r#"^朋友圈$"#
opacity 1.0
}
// Waypaper 默认以浮动窗口打开。
window-rule {
match app-id=r#"(?i)^waypaper$"#
open-floating true
}
// Nautilus 文件管理器默认以浮动窗口打开。
window-rule {
match app-id=r#"^org\.gnome\.Nautilus$"#
open-floating true
}
// Steam 好友列表默认浮动,不影响 Steam 主窗口。
window-rule {
match app-id=r#"^steam$"# title=r#"^Friends List$"#
open-floating true
}
// ----------------------------------------------------------------------------
// 7. 快捷键
// Mod 在正常 niri 会话中对应 Super 键。
// ----------------------------------------------------------------------------
binds {
// Keys consist of modifiers separated by + signs, followed by an XKB key name
// in the end. To find an XKB name for a particular key, you may use a program
// like wev.
//
// "Mod" is a special modifier equal to Super when running on a TTY, and to Alt
// when running as a winit window.
//
// Most actions that you can bind here can also be invoked programmatically with
// `niri msg action do-something`.
// --- 帮助与常用程序 -----------------------------------------------------
// Mod-Shift-/, which is usually the same as Mod-?,
// shows a list of important hotkeys.
Mod+Shift+Slash { show-hotkey-overlay; }
// Suggested binds for running programs: terminal, app launcher, screen locker.
Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "kitty"; }
Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; }
Mod+E hotkey-overlay-title="Open File Manager: Nautilus" { spawn "nautilus"; }
Mod+A repeat=false hotkey-overlay-title="Take a Screenshot with Mark Shot" { spawn "mark-shot"; }
Mod+Alt+W hotkey-overlay-title="Open Waypaper" { spawn "waypaper"; }
Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; }
// Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc.
// Note: the entire command goes as a single argument. It's passed verbatim to `sh -c`.
// For example, this is a standard bind to toggle the screen reader (orca).
Super+Alt+S allow-when-locked=true hotkey-overlay-title=null { spawn-sh "pkill orca || exec orca"; }
// --- 音量、媒体与亮度 ---------------------------------------------------
// Example volume keys mappings for PipeWire & WirePlumber.
// The allow-when-locked=true property makes them work even when the session is locked.
// Using spawn-sh allows to pass multiple arguments together with the command.
// "-l 1.0" limits the volume to 100%.
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1+ -l 1.0"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.1-"; }
XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; }
// Example media keys mapping using playerctl.
// This will work with any MPRIS-enabled media player.
XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; }
XF86AudioStop allow-when-locked=true { spawn-sh "playerctl stop"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; }
XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; }
// Example brightness key mappings for brightnessctl.
// You can use regular spawn with multiple arguments too (to avoid going through "sh"),
// but you need to manually put each argument in separate "" quotes.
XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "+10%"; }
XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "10%-"; }
// --- 窗口焦点与移动 -----------------------------------------------------
// Open/close the Overview: a zoomed-out view of workspaces and windows.
// You can also move the mouse into the top-left hot corner,
// or do a four-finger swipe up on a touchpad.
Mod+O repeat=false { toggle-overview; }
Mod+Q repeat=false { close-window; }
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
Mod+Ctrl+Left { move-column-left; }
Mod+Ctrl+Down { move-window-down; }
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Right { move-column-right; }
Mod+Ctrl+H { move-column-left; }
Mod+Ctrl+J { move-window-down; }
Mod+Ctrl+K { move-window-up; }
Mod+Ctrl+L { move-column-right; }
// Alternative commands that move across workspaces when reaching
// the first or last window in a column.
// Mod+J { focus-window-or-workspace-down; }
// Mod+K { focus-window-or-workspace-up; }
// Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
// Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// --- 显示器间切换与移动 -------------------------------------------------
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Down { focus-monitor-down; }
Mod+Shift+Up { focus-monitor-up; }
Mod+Shift+Right { focus-monitor-right; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
Mod+Shift+L { focus-monitor-right; }
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// Alternatively, there are commands to move just a single window:
// Mod+Shift+Ctrl+Left { move-window-to-monitor-left; }
// ...
// And you can also move a whole workspace to another monitor:
// Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; }
// ...
// --- 工作区切换与移动 ---------------------------------------------------
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+Page_Down { move-window-to-workspace-down; }
// ...
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// You can bind mouse wheel scroll ticks using the following syntax.
// These binds will change direction based on the natural-scroll setting.
//
// To avoid scrolling through workspaces really fast, you can use
// the cooldown-ms property. The bind will be rate-limited to this value.
// You can set a cooldown on any bind, but it's most useful for the wheel.
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
// Usually scrolling up and down with Shift in applications results in
// horizontal scrolling; these binds replicate that.
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// Similarly, you can bind touchpad scroll "ticks".
// Touchpad scrolling is continuous, so for these binds it is split into
// discrete intervals.
// These binds are also affected by touchpad's natural-scroll, so these
// example binds are "inverted", since we have natural-scroll enabled for
// touchpads by default.
// Mod+TouchpadScrollDown { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+"; }
// Mod+TouchpadScrollUp { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02-"; }
// You can refer to workspaces by index. However, keep in mind that
// niri is a dynamic workspace system, so these commands are kind of
// "best effort". Trying to refer to a workspace index bigger than
// the current workspace count will instead refer to the bottommost
// (empty) workspace.
//
// For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on
// will all refer to the 3rd workspace.
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+Ctrl+1 { move-column-to-workspace 1; }
Mod+Ctrl+2 { move-column-to-workspace 2; }
Mod+Ctrl+3 { move-column-to-workspace 3; }
Mod+Ctrl+4 { move-column-to-workspace 4; }
Mod+Ctrl+5 { move-column-to-workspace 5; }
Mod+Ctrl+6 { move-column-to-workspace 6; }
Mod+Ctrl+7 { move-column-to-workspace 7; }
Mod+Ctrl+8 { move-column-to-workspace 8; }
Mod+Ctrl+9 { move-column-to-workspace 9; }
// Alternatively, there are commands to move just a single window:
// Mod+Ctrl+1 { move-window-to-workspace 1; }
// Switches focus between the current and the previous workspace.
// Mod+Tab { focus-workspace-previous; }
// --- 列、尺寸与布局 -----------------------------------------------------
// The following binds move the focused window in and out of a column.
// If the window is alone, they will consume it into the nearby column to the side.
// If the window is already in a column, they will expel it out.
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
// Consume one window from the right to the bottom of the focused column.
Mod+Comma { consume-window-into-column; }
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// Cycle through widths set in preset-column-widths.
Mod+R { switch-preset-column-width; }
// Cycling through the presets in reverse order is also possible.
Mod+Shift+R { switch-preset-column-width-back; }
Mod+Ctrl+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
// While maximize-column leaves gaps and borders around the window,
// maximize-window-to-edges doesn't: the window expands to the edges of the screen.
// This bind corresponds to normal window maximizing,
// e.g. by double-clicking on the titlebar.
Mod+M { maximize-window-to-edges; }
// Expand the focused column to space not taken up by other fully visible columns.
// Makes the column "fill the rest of the space".
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
// Center all fully visible columns on screen.
Mod+Ctrl+C { center-visible-columns; }
// Finer width adjustments.
// This command can also:
// * set width in pixels: "1000"
// * adjust width in pixels: "-5" or "+5"
// * set width as a percentage of screen width: "25%"
// * adjust width as a percentage of screen width: "-10%" or "+10%"
// Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0,
// set-column-width "100" will make the column occupy 200 physical screen pixels.
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
// Finer height adjustments when in column with other windows.
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// Move the focused window between the floating and the tiling layout.
Mod+V { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
// Toggle tabbed column display mode.
// Windows in this column will appear as vertical tabs,
// rather than stacked on top of each other.
Mod+W { toggle-column-tabbed-display; }
// Actions to switch layouts.
// Note: if you uncomment these, make sure you do NOT have
// a matching layout switch hotkey configured in xkb options above.
// Having both at once on the same hotkey will break the switching,
// since it will switch twice upon pressing the hotkey (once by xkb, once by niri).
// Mod+Space { switch-layout "next"; }
// Mod+Shift+Space { switch-layout "prev"; }
// --- 截图 ---------------------------------------------------------------
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// --- 会话与系统控制 -----------------------------------------------------
// 重启 Waybar 状态栏。
Mod+F11 repeat=false hotkey-overlay-title="Restart Waybar" { spawn-sh "pkill waybar; exec waybar"; }
// Applications such as remote-desktop clients and software KVM switches may
// request that niri stops processing the keyboard shortcuts defined here
// so they may, for example, forward the key presses as-is to a remote machine.
// It's a good idea to bind an escape hatch to toggle the inhibitor,
// so a buggy application can't hold your session hostage.
//
// The allow-inhibiting=false property can be applied to other binds as well,
// which ensures niri always processes them, even when an inhibitor is active.
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// The quit action will show a confirmation dialog to avoid accidental exits.
Mod+Shift+E { quit; }
Ctrl+Alt+Delete { quit; }
// Powers off the monitors. To turn them back on, do any input like
// moving the mouse or pressing any other key.
Mod+Shift+P { power-off-monitors; }
}
+78
View File
@@ -0,0 +1,78 @@
// Waybar 基础配置(适用于 niri)
{
"layer": "top",
"position": "top",
"height": 32,
"spacing": 8,
"modules-left": [
"custom/arch-workspaces",
"custom/musicfox-lyrics"
],
"modules-center": [
"custom/center-clock"
],
"modules-right": [
"pulseaudio",
"network",
"cpu",
"memory",
"tray"
],
"custom/arch-workspaces": {
"exec": "printf ''",
"interval": 3600,
"tooltip": false
},
"custom/musicfox-lyrics": {
"exec": "/home/skyroy/Documents/code/musicfox-waybar-lyrics/musicfox_lyrics.py",
"return-type": "json",
"restart-interval": 5,
"escape": true,
"on-click": "playerctl --player=musicfox play-pause",
"on-scroll-up": "playerctl --player=musicfox next",
"on-scroll-down": "playerctl --player=musicfox previous",
"max-length": 70,
"tooltip": true
},
"custom/center-clock": {
"exec": "day=$(date +%u); case $day in 1) week=星期一;; 2) week=星期二;; 3) week=星期三;; 4) week=星期四;; 5) week=星期五;; 6) week=星期六;; 7) week=星期日;; esac; date \"+$week • %Y年%m月%d日 • %H:%M:%S\"",
"interval": 1,
"tooltip": false
},
"pulseaudio": {
"format": "{volume}% {icon}",
"format-muted": "静音",
"format-icons": {
"default": ["", "", ""]
},
"on-click": "pavucontrol"
},
"network": {
"format-wifi": "{essid} {signalStrength}% ",
"format-ethernet": "{ipaddr} 󰈀",
"format-disconnected": "未连接"
},
"cpu": {
"format": " {usage}%"
},
"memory": {
"format": " {percentage}%"
},
"clock": {
"format": "{:%Y-%m-%d %H:%M}",
"tooltip-format": "<big>{:%Y年%m月}</big>\n<tt><small>{calendar}</small></tt>"
},
"tray": {
"spacing": 8
}
}
+60
View File
@@ -0,0 +1,60 @@
/* Waybar 独立磨砂玻璃模块 */
* {
border: none;
border-radius: 0;
font-family: FontAwesome, sans-serif;
font-size: 13px;
min-height: 0;
}
window#waybar {
background: transparent;
color: #cdd6f4;
}
#custom-arch-workspaces,
#custom-musicfox-lyrics,
#custom-center-clock,
#pulseaudio,
#network,
#cpu,
#memory,
#clock,
#tray {
margin: 4px 3px;
padding: 0 12px;
border: none;
border-radius: 8px;
background: rgba(30, 30, 46, 0.55);
box-shadow: none;
}
#custom-arch-workspaces {
margin-left: 8px;
color: #89b4fa;
font-size: 16px;
}
#custom-musicfox-lyrics {
color: #cdd6f4;
}
#custom-musicfox-lyrics.hidden {
margin: 0;
padding: 0;
min-width: 0;
background: transparent;
}
#custom-center-clock {
color: #cdd6f4;
font-weight: bold;
}
#clock {
color: #89b4fa;
}
#tray {
margin-right: 8px;
}
+39
View File
@@ -0,0 +1,39 @@
[Settings]
language = en
backend = awww
folder = ~/Pictures/wallpaper
monitors = All
wallpaper = ~/Pictures/wallpaper/wallhaven-3q2zdd.jpg
show_path_in_tooltip = True
fill = fill
sort = name
color = #ffffff
subfolders = False
all_subfolders = False
show_hidden = False
show_gifs_only = False
zen_mode = True
post_command =
number_of_columns = 3
swww_transition_type = any
swww_transition_step = 63
swww_transition_angle = 0
swww_transition_duration = 2
swww_transition_fps = 60
mpvpaper_sound = False
mpvpaper_options =
use_xdg_state = False
stylesheet = /home/skyroy/.config/waypaper/style.css
keybindings = ~/.config/waypaper/keybindings.ini
wallpaperengine_folder = ~/.steam/root/steamapps/workshop/content/431960
linux_wallpaperengine_clamp = none
linux_wallpaperengine_volume = 15
linux_wallpaperengine_silent = False
linux_wallpaperengine_noautomute = False
linux_wallpaperengine_no_audio_processing = False
linux_wallpaperengine_fps = 30
linux_wallpaperengine_disable_particles = True
linux_wallpaperengine_disable_mouse = False
linux_wallpaperengine_disable_parallax = False
linux_wallpaperengine_no_fullscreen_pause = False
+100
View File
@@ -0,0 +1,100 @@
android-studio
android-tools
android-udev
archlinuxcn-keyring
awww
base
base-devel
blueman
bluez
bluez-utils
brightnessctl
btop
btrfs-progs
cc-switch-bin
clash-verge-rev
cloud-fs-bin
dbx
dida-appimage
dmidecode
docker-compose
efibootmgr
fcitx5
fcitx5-configtool
fcitx5-gtk
fcitx5-qt
fcitx5-rime
fd
feishu-bin
ffmpegthumbnailer
file-roller
firefox
fuse2
fuzzel
git
gnome-keyring
go-musicfox-bin
google-chrome
gst-libav
gst-plugins-base
gst-plugins-good
gvfs-smb
icoextract
intel-media-driver
intel-ucode
jdk17-openjdk
kitty
libnotify
libva-utils
linux
linux-firmware
linux-headers
mako
mark-shot
mesa-utils
nautilus-open-any-terminal
neovim
networkmanager
niri
notion-app-electron
noto-fonts-cjk
noto-fonts-emoji
noto-fonts-extra
nvidia-580xx-dkms
nvidia-580xx-utils
nvidia-prime
obsidian
onlyoffice-bin
otf-font-awesome
pipewire-alsa
pipewire-pulse
polkit-gnome
python-pillow
python-pip
qqmusic-bin
rime-ice-git
ripgrep
rsync
seahorse
sidra-bin
sudo
tailscale
telegram-desktop-bin
termark-bin
tradingview
ttf-nerd-fonts-symbols
unzip
vim
visual-studio-code-bin
vulkan-intel
waybar
waypaper
wechat-bin
wpa_supplicant
xdg-desktop-portal-gnome
xwayland-satellite
yay
yay-debug
yazi
zip
zram-generator
+27
View File
@@ -0,0 +1,27 @@
android-studio
archlinuxcn-keyring
cc-switch-bin
clash-verge-rev
cloud-fs-bin
dbx
dida-appimage
feishu-bin
go-musicfox-bin
google-chrome
mark-shot
nautilus-open-any-terminal
notion-app-electron
nvidia-580xx-dkms
nvidia-580xx-utils
onlyoffice-bin
qqmusic-bin
rime-ice-git
sidra-bin
telegram-desktop-bin
termark-bin
tradingview
visual-studio-code-bin
waypaper
wechat-bin
yay
yay-debug
+73
View File
@@ -0,0 +1,73 @@
android-tools
android-udev
awww
base
base-devel
blueman
bluez
bluez-utils
brightnessctl
btop
btrfs-progs
dmidecode
docker-compose
efibootmgr
fcitx5
fcitx5-configtool
fcitx5-gtk
fcitx5-qt
fcitx5-rime
fd
ffmpegthumbnailer
file-roller
firefox
fuse2
fuzzel
git
gnome-keyring
gst-libav
gst-plugins-base
gst-plugins-good
gvfs-smb
icoextract
intel-media-driver
intel-ucode
jdk17-openjdk
kitty
libnotify
libva-utils
linux
linux-firmware
linux-headers
mako
mesa-utils
neovim
networkmanager
niri
noto-fonts-cjk
noto-fonts-emoji
noto-fonts-extra
nvidia-prime
obsidian
otf-font-awesome
pipewire-alsa
pipewire-pulse
polkit-gnome
python-pillow
python-pip
ripgrep
rsync
seahorse
sudo
tailscale
ttf-nerd-fonts-symbols
unzip
vim
vulkan-intel
waybar
wpa_supplicant
xdg-desktop-portal-gnome
xwayland-satellite
yazi
zip
zram-generator
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.pytest_cache/
@@ -0,0 +1,78 @@
# Musicfox Waybar Lyrics
在 Waybar 中显示 go-musicfox 当前播放歌曲的网易云逐行同步歌词。
## 行为
- 只匹配名称包含 `musicfox` 的 MPRIS 播放器。
- musicfox 未运行、未播放或已经暂停时,模块自动隐藏。
- 播放时按 MPRIS 播放位置显示当前 LRC 歌词。
- 优先从 MPRIS URL/Track ID 提取网易云歌曲 ID,无法提取时按歌名、歌手和时长搜索。
- 歌词缓存在 `~/.cache/musicfox-waybar-lyrics/`,断网后已缓存歌曲仍可显示。
- 找不到同步歌词时显示当前歌曲名。
## 要求
- Python 3.11+
- Waybar
- playerctl
- 启用了 MPRIS 的 go-musicfox
先启动 musicfox 并播放歌曲,然后确认:
```bash
playerctl --list-all
playerctl --player=musicfox metadata
```
如果实际播放器名称不是 `musicfox`,可以运行:
```bash
./musicfox_lyrics.py --player-pattern 实际名称
```
## Waybar 配置
把 `custom/musicfox-lyrics` 加入 Waybar 的模块列表,例如放到中间:
```jsonc
"modules-center": [
"custom/musicfox-lyrics",
"custom/center-clock"
]
```
再把 [waybar-module.jsonc](./waybar-module.jsonc) 中的模块配置合并到 Waybar 顶层配置,把 [waybar-style.css](./waybar-style.css) 追加到 Waybar 样式文件。
你的配置文件通常位于:
```text
~/.config/waybar/config.jsonc
~/.config/waybar/style.css
```
重新加载 Waybar:
```bash
pkill -SIGUSR2 waybar
```
## 单独运行
```bash
./musicfox_lyrics.py
```
没有播放时会输出隐藏状态;播放后输出类似:
```json
{"text":"怀着冷却了的心窝漂远方","tooltip":"海阔天空 — Beyond\n怀着冷却了的心窝漂远方","class":"playing"}
```
## 测试
```bash
python -m unittest discover -s tests -v
```
网易云接口是网页内部接口,不提供长期稳定性保证。脚本已经包含缓存、超时和失败降级处理。
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""Stream synchronized go-musicfox lyrics to a Waybar custom module."""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
import time
import unicodedata
import urllib.parse
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Any
NETEASE_SEARCH_URL = "https://music.163.com/api/search/get/web"
NETEASE_LYRIC_URL = "https://music.163.com/api/song/lyric"
PLAYER_FIELDS_SEPARATOR = "\x1f"
PLAYER_FORMAT = PLAYER_FIELDS_SEPARATOR.join(
(
"{{status}}",
"{{position}}",
"{{xesam:title}}",
"{{xesam:artist}}",
"{{mpris:length}}",
"{{xesam:url}}",
"{{mpris:trackid}}",
)
)
TIMESTAMP_RE = re.compile(r"\[(\d{1,3}):(\d{1,2}(?:\.\d{1,3})?)\]")
SONG_ID_PATTERNS = (
re.compile(r"music\.163\.com/(?:#/)?song\?id=(\d+)", re.IGNORECASE),
re.compile(r"music\.163\.com/(?:#/)?song/(\d+)", re.IGNORECASE),
re.compile(r"(?:^|[/:])song[_/-]?(\d{5,})(?:$|[/?#])", re.IGNORECASE),
)
@dataclass(frozen=True)
class PlayerState:
player: str
status: str
position: float
title: str
artist: str
length: float
url: str
track_id: str
@property
def signature(self) -> tuple[str, str, int]:
return (self.title, self.artist, round(self.length))
@dataclass(frozen=True)
class LyricLine:
timestamp: float
text: str
def normalize_text(value: str) -> str:
normalized = unicodedata.normalize("NFKC", value).casefold()
return "".join(character for character in normalized if character.isalnum())
def parse_time_value(value: str) -> float:
try:
number = float(value)
except ValueError:
return 0.0
return number / 1_000_000 if number > 100_000 else number
def parse_lrc(raw_lrc: str) -> list[LyricLine]:
offset_seconds = 0.0
offset_match = re.search(r"\[offset:([+-]?\d+)\]", raw_lrc, re.IGNORECASE)
if offset_match:
offset_seconds = int(offset_match.group(1)) / 1000
parsed: list[LyricLine] = []
for raw_line in raw_lrc.splitlines():
timestamps = TIMESTAMP_RE.findall(raw_line)
if not timestamps:
continue
text = TIMESTAMP_RE.sub("", raw_line).strip()
if not text:
continue
for minutes, seconds in timestamps:
timestamp = max(0.0, int(minutes) * 60 + float(seconds) + offset_seconds)
parsed.append(LyricLine(timestamp=timestamp, text=text))
return sorted(parsed, key=lambda line: line.timestamp)
def lyric_at(lines: list[LyricLine], position: float) -> str:
current = ""
for line in lines:
if line.timestamp > position + 0.05:
break
current = line.text
return current
def extract_song_id(*values: str) -> int | None:
for value in values:
decoded = urllib.parse.unquote(value or "")
for pattern in SONG_ID_PATTERNS:
match = pattern.search(decoded)
if match:
return int(match.group(1))
return None
def candidate_score(candidate: dict[str, Any], state: PlayerState) -> float:
candidate_title = normalize_text(str(candidate.get("name", "")))
title = normalize_text(state.title)
artists = candidate.get("artists") or candidate.get("ar") or []
artist_names = [str(artist.get("name", "")) for artist in artists if isinstance(artist, dict)]
candidate_artist = normalize_text(" ".join(artist_names))
artist = normalize_text(state.artist)
score = 0.0
if candidate_title == title:
score += 100
elif title and (title in candidate_title or candidate_title in title):
score += 55
if artist and candidate_artist:
if artist in candidate_artist or candidate_artist in artist:
score += 60
elif any(normalize_text(name) in artist for name in artist_names):
score += 40
duration_ms = candidate.get("duration") or candidate.get("dt") or 0
if state.length > 0 and duration_ms:
difference = abs(float(duration_ms) / 1000 - state.length)
score += max(0.0, 45.0 - difference * 3)
return score
class MusicfoxClient:
def __init__(self, player_pattern: str = "musicfox") -> None:
self.player_pattern = player_pattern.casefold()
self.selected_player: str | None = None
def _run(self, arguments: list[str]) -> str:
completed = subprocess.run(
["playerctl", *arguments],
check=False,
capture_output=True,
text=True,
timeout=2,
)
if completed.returncode != 0:
return ""
return completed.stdout.strip()
def find_player(self) -> str | None:
players = self._run(["--list-all"]).splitlines()
matches = [player for player in players if self.player_pattern in player.casefold()]
self.selected_player = matches[0] if matches else None
return self.selected_player
def state(self) -> PlayerState | None:
player = self.selected_player or self.find_player()
if not player:
return None
output = self._run(["--player", player, "metadata", "--format", PLAYER_FORMAT])
fields = output.split(PLAYER_FIELDS_SEPARATOR)
if len(fields) != 7:
self.selected_player = None
return None
status, position, title, artist, length, url, track_id = fields
return PlayerState(
player=player,
status=status,
position=parse_time_value(position),
title=title.strip(),
artist=artist.strip(),
length=parse_time_value(length),
url=url.strip(),
track_id=track_id.strip(),
)
class NeteaseLyrics:
def __init__(self, cache_dir: Path, timeout: float = 8.0) -> None:
self.cache_dir = cache_dir
self.timeout = timeout
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _request_json(self, url: str, parameters: dict[str, str]) -> dict[str, Any]:
request_url = f"{url}?{urllib.parse.urlencode(parameters)}"
request = urllib.request.Request(
request_url,
headers={
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) musicfox-waybar-lyrics/0.1",
"Referer": "https://music.163.com/",
},
)
with urllib.request.urlopen(request, timeout=self.timeout) as response:
return json.load(response)
def search_song_id(self, state: PlayerState) -> int | None:
query = " ".join(part for part in (state.title, state.artist) if part).strip()
if not query:
return None
payload = self._request_json(
NETEASE_SEARCH_URL,
{"s": query, "type": "1", "offset": "0", "limit": "10", "total": "true"},
)
candidates = payload.get("result", {}).get("songs", [])
if not candidates:
return None
best = max(candidates, key=lambda candidate: candidate_score(candidate, state))
if candidate_score(best, state) < 80:
return None
song_id = best.get("id")
return int(song_id) if song_id is not None else None
def song_id(self, state: PlayerState) -> int | None:
return extract_song_id(state.url, state.track_id) or self.search_song_id(state)
def lyrics(self, song_id: int) -> list[LyricLine]:
cache_file = self.cache_dir / f"{song_id}.json"
if cache_file.exists():
try:
cached = json.loads(cache_file.read_text(encoding="utf-8"))
return parse_lrc(str(cached.get("lyric", "")))
except (OSError, json.JSONDecodeError):
pass
payload = self._request_json(
NETEASE_LYRIC_URL,
{"id": str(song_id), "lv": "-1", "kv": "-1", "tv": "-1"},
)
raw_lrc = str(payload.get("lrc", {}).get("lyric", ""))
temporary = cache_file.with_suffix(".tmp")
temporary.write_text(
json.dumps({"songId": song_id, "lyric": raw_lrc}, ensure_ascii=False),
encoding="utf-8",
)
temporary.replace(cache_file)
return parse_lrc(raw_lrc)
def waybar_payload(text: str, state: PlayerState | None = None) -> str:
if not text:
payload = {"text": "", "tooltip": "", "class": "hidden"}
else:
tooltip = text
if state:
tooltip = f"{state.title} — {state.artist}\n{text}".strip(" —\n")
payload = {"text": text, "tooltip": tooltip, "class": "playing"}
return json.dumps(payload, ensure_ascii=False)
def run(interval: float, player_pattern: str, cache_dir: Path) -> None:
client = MusicfoxClient(player_pattern)
provider = NeteaseLyrics(cache_dir)
track_signature: tuple[str, str, int] | None = None
lyrics: list[LyricLine] = []
last_output: str | None = None
next_player_scan = 0.0
while True:
try:
now = time.monotonic()
if not client.selected_player and now >= next_player_scan:
client.find_player()
next_player_scan = now + 2
state = client.state() if client.selected_player else None
output = waybar_payload("")
if state and state.status.casefold() == "playing" and state.title:
if state.signature != track_signature:
track_signature = state.signature
lyrics = []
try:
song_id = provider.song_id(state)
lyrics = provider.lyrics(song_id) if song_id else []
except (OSError, ValueError, urllib.error.URLError, TimeoutError):
lyrics = []
current_lyric = lyric_at(lyrics, state.position)
output = waybar_payload(current_lyric or state.title, state)
elif state is None:
track_signature = None
lyrics = []
if output != last_output:
print(output, flush=True)
last_output = output
except (OSError, subprocess.SubprocessError):
client.selected_player = None
hidden = waybar_payload("")
if hidden != last_output:
print(hidden, flush=True)
last_output = hidden
time.sleep(interval)
def main() -> None:
default_cache = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / (
"musicfox-waybar-lyrics"
)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--interval", type=float, default=0.5, help="Polling interval in seconds")
parser.add_argument(
"--player-pattern", default="musicfox", help="Substring used to select the MPRIS player"
)
parser.add_argument("--cache-dir", type=Path, default=default_cache)
arguments = parser.parse_args()
run(max(arguments.interval, 0.2), arguments.player_pattern, arguments.cache_dir)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(0)
@@ -0,0 +1,56 @@
import unittest
from musicfox_lyrics import (
LyricLine,
PlayerState,
candidate_score,
extract_song_id,
lyric_at,
parse_lrc,
)
class LyricsTest(unittest.TestCase):
def test_parses_and_sorts_lrc_with_offset(self) -> None:
lines = parse_lrc(
"[offset:500]\n[00:10.000]第二句\n[00:01.250][00:05.000]第一句\n[00:20.000]"
)
self.assertEqual(
lines,
[
LyricLine(1.75, "第一句"),
LyricLine(5.5, "第一句"),
LyricLine(10.5, "第二句"),
],
)
def test_selects_line_for_playback_position(self) -> None:
lines = [LyricLine(1, "一"), LyricLine(3, "二"), LyricLine(8, "三")]
self.assertEqual(lyric_at(lines, 0.5), "")
self.assertEqual(lyric_at(lines, 3.2), "二")
self.assertEqual(lyric_at(lines, 20), "三")
def test_extracts_netease_song_id(self) -> None:
self.assertEqual(extract_song_id("https://music.163.com/#/song?id=347230"), 347230)
self.assertEqual(extract_song_id("/org/mpris/MediaPlayer2/song_347230"), 347230)
self.assertIsNone(extract_song_id("https://example.com/track/347230"))
def test_song_match_prefers_title_artist_and_duration(self) -> None:
state = PlayerState("musicfox", "Playing", 10, "海阔天空", "Beyond", 326, "", "")
correct = {
"id": 347230,
"name": "海阔天空",
"artists": [{"name": "Beyond"}],
"duration": 326000,
}
wrong_version = {
"id": 1,
"name": "海阔天空",
"artists": [{"name": "Beyond"}],
"duration": 239000,
}
self.assertGreater(candidate_score(correct, state), candidate_score(wrong_version, state))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,14 @@
// Add custom/musicfox-lyrics to modules-left, modules-center, or modules-right.
{
"custom/musicfox-lyrics": {
"exec": "/home/skyroy/Documents/code/musicfox-waybar-lyrics/musicfox_lyrics.py",
"return-type": "json",
"restart-interval": 5,
"escape": true,
"on-click": "playerctl --player=musicfox play-pause",
"on-scroll-up": "playerctl --player=musicfox next",
"on-scroll-down": "playerctl --player=musicfox previous",
"max-length": 70,
"tooltip": true
}
}
@@ -0,0 +1,14 @@
#custom-musicfox-lyrics {
margin: 4px 3px;
padding: 0 14px;
border-radius: 8px;
background: rgba(30, 30, 46, 0.55);
color: #cdd6f4;
}
#custom-musicfox-lyrics.hidden {
margin: 0;
padding: 0;
min-width: 0;
background: transparent;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

+12
View File
@@ -0,0 +1,12 @@
当前机器记录(迁移时仅作参考,不要直接覆盖新机器):
- CPU: Intel Core i7-8565U
- GPU: Intel UHD 620 + NVIDIA MX150
- 根文件系统: Btrfs,子卷 @、@home、@pkg、@log
- EFI: /boot,systemd-boot + UKI
- zram: 4 GiB,zstd
- Niri 输出: HDMI-A-1 2560x1440 位于上方;eDP-1 1920x1080 位于下方
- 当前用户目录: /home/skyroy
注意:/etc/fstab 中的 UUID、Niri 输出名称、显示器位置、NVIDIA 驱动和 hostname 都是机器相关设置,迁移前必须重新确认。