Just some configs

Signed-off-by: The-Repo-Club <wayne6324@gmail.com>
This commit is contained in:
The-Repo-Club 2022-08-23 17:42:46 +01:00
parent a35d05c61e
commit 0e57dc74fc
No known key found for this signature in database
GPG Key ID: E30EC2FBFB05C44F
71 changed files with 7120 additions and 35 deletions

View File

@ -197,7 +197,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Lighting\Keys\y=#ffffff
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Lighting\Keys\z=#ffffff
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Modified=88e27f3d
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Modified=6a520b95
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Name=Rainbow
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Performance\DPI\0=@Point(400 400)
@ -425,7 +425,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Lighting\Keys\y=#ff0000
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Lighting\Keys\z=#ff0000
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Modified=92906afc
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Modified=1cffe5d4
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Name=Breathing
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Performance\DPI\0=@Point(400 400)
@ -675,7 +675,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Lighting\Keys\y=#aa00ff
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Lighting\Keys\z=#aa00ff
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Modified=21b24050
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Modified=34395641
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Name=Trippy
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Performance\DPI\0=@Point(400 400)
@ -733,7 +733,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\CurrentMode={51EB6E3A-27A0-4AD6-A35C-6B67E0329A3D}
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\HwModified=7ffd
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\ModeCount=3
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\Modified=b44e5399
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\Modified=b5bf4a67
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\Name=Multi
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Binding\KeyMap=K68 GB
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Binding\UseRealNames=true
@ -929,7 +929,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Lighting\Keys\y=#ffffff
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Lighting\Keys\z=#ffffff
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Modified=bf01e19d
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Modified=285eb9a1
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Name=Rainbow
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Performance\DPI\0=@Point(400 400)
@ -1157,7 +1157,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Lighting\Keys\y=#ff0000
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Lighting\Keys\z=#ff0000
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Modified=946354be
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Modified=14c31262
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Name=Breathing
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Performance\DPI\0=@Point(400 400)
@ -1407,7 +1407,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Lighting\Keys\y=#aa00ff
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Lighting\Keys\z=#aa00ff
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Lighting\UseRealNames=true
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Modified=ba0d6980
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Modified=d4f452e
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Name=Trippy
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Performance\AngleSnap=false
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Performance\DPI\0=@Point(400 400)
@ -1465,7 +1465,7 @@
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\CurrentMode={11C11AE3-3195-4DFC-B8AC-2FEA703414E5}
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\HwModified=40b6f054
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\ModeCount=3
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\Modified=8c0ddb8c
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\Modified=e5b492da
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\Name=Demo
[Popups]

View File

@ -0,0 +1,127 @@
# Theme file for CliFM
# Theme name: clifm
# Author: L. Abramovich
# License: GPL3
# FiletypeColors, InterfaceColors, and ExtColors use the same format used
# by the LS_COLORS environment variable. Thus, "di=01;34" means that (non-empty)
# directories will be printed in bold blue.
# Color codes are just traditional ANSI escape sequences less the escape char
# and the final 'm'.
# 4-bit, 8-bit (256 colors), and 24-bit (RGB/HEX) colors are supported.
# Example:
# 31 4-bit
# 38;5;160 8-bit
# 38;2;255;0;0 24-bit (RGB)
# #ff0000 24-bit (HEX)
#
# One attribute can be used for hex colors using a dash and an attribute
# number (RRGGBB-[1-9]), where 1-9 is:
#
# 1: Bold or increased intensity
# 2: Faint, decreased intensity or dim
# 3: Italic (Not widely supported)
# 4: Underline
# 5: Slow blink
# 6: Rapid blink
# 7: Reverse video or invert
# 8: Conceal or hide (Not widely supported)
# 9: Crossed-out or strike
#
# For example, to print bold red color, the hex code is #ff0000-1
# Definitions
# Define here up to 64 custom color variables. They can be used for:
# FiletypeColors
# InterfaceColors
# ExtColors
# DirIconColor
define D=0 # Default terminal foreground color
#define BD=1 # Bold (keep current color)
define BD=0;1 # Bold (reset foreground color)
define R=31 # Red
define BR=1;31 # Bold red
define DR=2;31 # Dimmed red
define UDR=4;2;31 # Underlined dimmed red
define UBR=4;1;31 # Underlined bold red
define G=32 # Green
define BG=1;32 # Bold green
define DG=2;32 # Dimmed green
define Y=33 # Yellow
define BY=1;33 # Bold yellow
define DY=2;33 # Dimmed yellow
define B=34 # Blue
define BB=1;34 # Bold blue
define DB=2;34 # Dimmed blue
define M=35 # Magenta
define BM=1;35 # Bold Magenta
define DM=2;35 # Dimmed magenta
define UM=4;35 # Underlined magenta
define C=36 # Cyan
define BC=1;36 # Bold cyan
define DC=2;36 # Dimmed cyan
define RC=7;36 # Reverse cyan
define UDC=4;2;36 # Underlined dimmed cyan
define BDC=1;2;36 # Bold dimmed cyan
define DW=2;37 # Dimmed white
# Foreground-background combinations
define URW=4;31;47 # Red foreground, white background
define UBW=4;34;47 # Blue foreground, white background
define WR=37;41 # White foreground, red background
# K stands for black (B is used for Blue)
define KY=30;43 # Black foreground, yellow background
define KR=30;41 # Black foreground, red background
define KG=30;42 # Black foreground, green background
# BG is already used for bold green
define BlGr=34;42 # Blue foreground, green background
define WB=37;44 # white foreground, blue background
# FiletypeColors defines the color used for file names when listing files,
# just as InterfaceColors defines colors for CliFM's interface.
# Consult the manpage for information about these codes.
FiletypeColors="bd=BY:ca=KR:cd=BD:di=BB:ed=DB:ee=G:ef=DY:ex=BG:fi=D:ln=BC:mh=RC:nd=UBR:ne=UDR:nf=UDR:no=URW:or=UDC:ow=BlGr:pi=M:sg=KY:so=BM:st=WB:su=WR:tw=KG:uf=UBW:"
InterfaceColors="bm=BG:dd=B:df=D:dg=Y:dl=DW:dn=DW:dr=Y:do=C:dp=M:dw=R:dxd=G:dxr=C:dz=G:el=C:em=BR:fc=DB:hb=C:hc=DR:hd=C:he=C:hn=M:hp=C:hq=Y:hr=R:hs=G:hv=G:li=BG:mi=BC:nm=BG:si=BB:sb=DY:sc=DR:sf=UDC:sh=DM:sp=DR:sx=DG:ti=BC:ts=UM:tt=BDC:tx=D:wc=BC:wm=BY:wp=DR:ws1=B:ws2=R:ws3=Y:ws4=G:ws5=C:ws6=C:ws7=C:ws8=C:xf=BR:xs=G:"
# Colors for specific file extensions
ExtColors="*.tar=BR:*.tgz=BR:*.taz=BR:*.lha=BR:*.lz4=BR:*.lzh=BR:*.lzma=BR:*.tlz=BR:*.txz=BR:*.tzo=BR:*.t7z=BR:*.zip=BR:*.z=BR:*.dz=BR:*.gz=BR:*.lrz=BR:*.lz=BR:*.lzo=BR:*.xz=BR:*.zst=BR:*.tzst=BR:*.bz2=BR:*.bz=BR:*.tbz=BR:*.tbz2=BR:*.tz=BR:*.deb=BR:*.rpm=BR:*.rar=BR:*.cpio=BR:*.7z=BR:*.rz=BR:*.cab=BR:*.jpg=BM:*.JPG=BM:*.jpeg=BM:*.mjpg=BM:*.mjpeg=BM:*.gif=BM:*.GIF=BM:*.bmp=BM:*.xbm=BM:*.xpm=BM:*.png=BM:*.PNG=BM:*.svg=BM:*.pcx=BM:*.mov=BM:*.mpg=BM:*.mpeg=BM:*.m2v=BM:*.mkv=BM:*.webm=BM:*.webp=BM:*.ogm=BM:*.mp4=BM:*.MP4=BM:*.m4v=BM:*.mp4v=BM:*.vob=BM:*.wmv=BM:*.flc=BM:*.avi=BM:*.flv=BM:*.m4a=C:*.mid=C:*.midi=C:*.mp3=C:*.MP3=C:*.ogg=C:*.wav=C:*.pdf=BR:*.PDF=BR:*.doc=M:*.docx=M:*.xls=M:*.xlsx=M:*.ppt=M:*.pptx=M:*.odt=M:*.ods=M:*.odp=M:*.cache=DW:*.tmp=DW:*.temp=DW:*.log=DW:*.bak=DW:*.bk=DW:*.in=DW:*.out=DW:*.part=DW:*.aux=DW:*.c=BD:*.c++=BD:*.h=BD:*.cc=BD:*.cpp=BD:*.h=BD:*.h++=BD:*.hh=BD:*.go=BD:*.java=BD:*.js=BD:*.lua=BD:*.rb=BD:*.rs=BD:"
# If icons are enabled, use this color for the directories icon
DirIconColor="Y"
# The prompt used by CliFM. Use the 'prompt' command to check for available
# prompts. Enter 'prompt --help' for more information
Prompt="clifm"
# Override prompt values
#Prompt=""
#Notifications=
#EnableWarningPrompt=
#WarningPrompt=""
# The string used to construct the line dividing the list of files and
# the prompt (Unicode is supported). Possible values:
# "0": Print just an empty line
# "C": C is a single char. This char is printed up to the end of the screen
# "CCC": 3 or more chars. Only these chars (no more) will be printed
# "": Print a special line drawn with box-drawing characters (not
# supported by all terminals/consoles)
# The color of this line is controlled by the 'dl' code in InterfaceColors
DividingLine="-"
# If FZF TAB completion mode is enabled, pass these options to fzf:
FzfTabOptions="--color='16,prompt:6,fg+:-1,pointer:4,hl:5,hl+:5,gutter:-1,marker:2' --marker='*' --bind tab:accept,right:accept,left:abort --inline-info --layout=reverse-list"
# Same options, but colorless
#FzfTabOptions="--color='bw' --marker='*' --bind tab:accept,right:accept,left:abort --inline-info --layout=reverse-list"
# For more information consult fzf(1)

View File

@ -0,0 +1,136 @@
# Keybindings file for CliFM
# Use the 'kbgen' plugin (compile it first: gcc -o kbgen kbgen.c) to
# find out the escape code for the key o key sequence you want. Use
# either octal, hexadecimal codes or symbols.
# Ex: For Alt-/ (in rxvt terminals) 'kbgen' will print the following
# lines:
# Hex | Oct | Symbol
# ---- | ---- | ------
# \x1b | \033 | ESC (\e)
# \x2f | \057 | /
# In this case, the keybinding, if using symbols, is: "\e/:function"
# In case you prefer the hex codes it would be: \x1b\x2f:function.
# GNU emacs escape sequences are also allowed (ex: "\M-a", Alt-a
# in most keyboards, or "\C-r" for Ctrl-r).
# Some codes, especially those involving keys like Ctrl or the arrow
# keys, vary depending on the terminal emulator and the system settings.
# These keybindings should be set up thus on a per terminal basis.
# You can also consult the terminfo database via the infocmp command.
# See terminfo(5) and infocmp(1).
# Alt-j
previous-dir:\M-j
# Shift-left (rxvt)
previous-dir2:\e[d
# Shift-left (xterm)
previous-dir3:\e[2D
# Shift-left (others)
previous-dir4:\e[1;2D
# Alt-k
next-dir:\M-k
# Shift-right (rxvt)
next-dir2:\e[c
# Shift-right (xterm)
next-dir3:\e[2C
# Shift-right (others)
next-dir4:\e[1;2C
first-dir:\C-\M-j
last-dir:\C-\M-k
# Alt-u
parent-dir:\M-u
# Shift-up (rxvt)
parent-dir2:\e[a
# Shift-up (xterm)
parent-dir3:\e[2A
# Shift-up (others)
parent-dir4:\e[1;2A
# Alt-e
home-dir:\M-e
# Home key (rxvt)
#home-dir2:\e[7~
# Home key (xterm)
#home-dir3:\e[H
# Home key (Emacs term)
#home-dir4:\e[1~
# Alt-r
root-dir:\M-r
# Alt-/ (rxvt)
root-dir2:\e/
#root-dir3:
pinned-dir:\M-p
workspace1:\M-1
workspace2:\M-2
workspace3:\M-3
workspace4:\M-4
# Help
# F1-3
show-manpage:\eOP
show-manpage2:\e[11~
show-cmds:\eOQ
show-cmds2:\e[12~
show-kbinds:\eOR
show-kbinds2:\e[13~
archive-sel:\C-\M-a
bookmark-sel:\C-\M-b
bookmarks:\M-b
clear-line:\M-c
clear-msgs:\M-t
create-file:\M-n
deselect-all:\M-d
export-sel:\C-\M-e
dirs-first:\M-g
lock:\M-o
mountpoints:\M-m
move-sel:\C-\M-n
new-instance:\C-x
next-profile:\C-\M-p
only-dirs:\M-,
open-sel:\C-\M-g
paste-sel:\C-\M-v
prepend-sudo:\M-v
previous-profile:\C-\M-o
rename-sel:\C-\M-r
remove-sel:\C-\M-d
refresh-screen:\C-r
selbox:\M-s
select-all:\M-a
show-dirhist:\M-h
sort-previous:\M-z
sort-next:\M-x
toggle-hidden:\M-i
toggle-hidden2:\M-.
toggle-light:\M-y
toggle-long:\M-l
toggle-max-name-len:\C-\M-l
toggle-disk-usage:\C-\M-i
trash-sel:\C-\M-t
untrash-all:\C-\M-u
# F6-12
open-mime:\e[17~
open-jump-db:\e[18~
edit-color-scheme:\e[19~
open-keybinds:\e[20~
open-config:\e[21~
open-bookmarks:\e[23~
quit:\e[24~
# Plugins
# 1) Make sure your plugin is in the plugins directory (or use any of the
# plugins in there)
# 2) Link pluginx to your plugin using the 'actions edit' command. Ex:
# "plugin1=myplugin.sh"
# 3) Set a keybinding here for pluginx. Ex: "plugin1:\M-7"
#plugin1:
#plugin2:
#plugin3:
#plugin4:

View File

@ -0,0 +1 @@
*0:/home/repo/Development/lovesay

View File

@ -0,0 +1,36 @@
######################
# CliFM actions file #
######################
# Define here your custom actions. Actions are custom command names
# bound to a executable file located either in DATADIR/clifm/plugins
# (usually /usr/share/clifm/plugins) or in $XDG_CONFIG_HOME/clifm/plugins.
# Actions can be executed directly from CliFM command line, as if they
# were any other command, and the associated file will be executed
# instead. All parameters passed to the action command will be passed
# to the corresponding plugin as well.
+=finder.sh
++=jumper.sh
-=fzfnav.sh
*=fzfsel.sh
**=fzfdesel.sh
//=rgfind.sh
_=fzcd.sh
bn=batch_create.sh
cr=cprm.sh
da=disk_analyzer.sh
dh=fzfdirhist.sh
dr=dragondrop.sh
fdups=fdups.sh
gg=pager.sh
h=fzfhist.sh
i=img_viewer.sh
ih=ihelp.sh
kbgen=kbgen
music=music_player.sh
ptot=pdf_viewer.sh
rrm=recur_rm.sh
update=update.sh
vid=vid_viewer.sh
wall=wallpaper_setter.sh

View File

@ -0,0 +1,6 @@
### This is the bookmarks file for clifm ###
# Empty and commented lines are ommited
# The bookmarks syntax is: [shortcut]name:path
# Example:
[c]clifm:/home/repo/.config//clifm/profiles/default

View File

@ -0,0 +1,273 @@
###########################################
# CLIFM #
# The command line file manager #
###########################################
# This is CliFM's main configuration file
# Commented and empty lines are ignored
# Color schemes (or just themes) are stored in the colors directory
# ($XDG_DATA_DIRS/clifm/colors, usually /usr/local/share/clifm/colors
# or /usr/share/clifm/colors). You can place your custom themes in
# $HOME/.config/clifm/colors
#
# Use the 'cs' command or the '--color-scheme' command line option to set
# a theme
#
# Run 'cs edit' to edit the current theme
#
# Each theme includes color definitions, just as definitions for the
# prompt, the warning prompt, the dividing line, and the FZF window
#
# Use TAB to list available themes: 'cs TAB'
#
# Visit https://github.com/leo-arch/clifm-colors to get some extra themes
ColorScheme=default
# The amount of files contained by a directory is informed next
# to the directory name. However, this feature might slow things down
# when, for example, listing files on a remote server. The files counter
# can be disabled here, via the --no-files-counter option, or using the
# 'fc' command while in the program itself.
FilesCounter=true
# How to list files: 0 = vertically (like ls(1) would), 1 = horizontally
ListingMode=0
# List files automatically after changing current directory
AutoLs=true
# If set to true, print a map of the current position in the directory
# history list, showing previous, current, and next entries
DirhistMap=false
# Use a regular expression to filter files from the files list.
# Example: "!.*~$" to exclude backup files (ending with ~), or "^\." to
# list only hidden files.
Filter=""
# Set the default copy command. Available options are: 0 = cp,
# 1 = advcp, 2 = wcp, 3 = rsync. 1-3 include a progress bar.
cpCmd=0
# Set the default move command. Available options are: 0 = mv,
# and 1 = advmv. 1 adds a progress bar to mv.
mvCmd=0
# TAB completion mode: either 'standard' or 'fzf'. Defaults to 'fzf' if
# the binary is found in PATH. Othwerwise, the standard mode is used
TabCompletionMode=
# MaxPath is only used for the /p option of the prompt: the current
# working directory will be abbreviated to its basename (everything after
# the last slash) whenever the current path is longer than MaxPath.
MaxPath=40
WelcomeMessage=true
# Print CliFM's logo screen at startup
SplashScreen=false
ShowHiddenFiles=false
# List files properties next to file names instead of just file names
LongViewMode=false
# Print files apparent size instead of actual device usage (Linux only)
ApparentSize=false
# If running in long view, print directories full size (including contents)
FullDirSize=false
# Keep a record of both external and internal commands able to modify the
# files system (e.g. 'r', 'c', 'm', and so on)
LogCmds=false
# Minimum length at which a file name can be trimmed in long view mode
# (including ELN length and spaces). When running in long mode, this
# setting overrides MaxFilenameLen.
MinFilenameTrim=20
# When a directory rank in the jump database is below MinJumpRank, it
# will be forgotten
MinJumpRank=10
# When the sum of all ranks in the jump database reaches MaxJumpTotalRank,
# all ranks will be reduced 10%, and those falling below MinJumpRank will
# be deleted
MaxJumpTotalRank=100000
# Should CliFM be allowed to run external, shell commands?
ExternalCommands=true
# Write the last visited directory to $XDG_CONFIG_HOME/clifm/.last to be
# later accessed by the corresponding shell function at program exit.
# To enable this feature consult the manpage.
CdOnQuit=false
# If set to true, a command name that is the name of a directory or a
# file is executed as if it were the argument to the the 'cd' or the
# 'open' commands respectivelly: 'cd DIR' works the same as just 'DIR'
# and 'open FILE' works the same as just 'FILE'.
Autocd=true
AutoOpen=true
# If set to true, enable auto-suggestions
AutoSuggestions=true
# The following checks will be performed in the order specified
# by SuggestionStrategy. Available checks:
# a = Aliases names\n\
# b = Bookmarks names\n\
# c = Possible completions\n\
# e = ELN's
# f = File names in current directory\n\
# h = Commands history\n\
# j = Jump database\n\
# Use a dash (-) to skip a check. Ex: 'ehfj-ac' to skip the bookmarks
# check
SuggestionStrategy=ehfjbac
# If set to true, suggest file names using the corresponding file type
# color (set via the color scheme file)
SuggestFiletypeColor=false
SyntaxHighlighting=true
# We have three search strategies: 0 = glob-only, 1 = regex-only,
# and 2 = glob-regex
#SearchStrategy=2
# If set to true, expand bookmark names into the corresponding bookmark
# path: if the bookmark is "name=/path", "name" will be interpreted
# as /path. TAB completion is also available for bookmark names.
ExpandBookmarks=false
# In light mode, extra file type checks (except those provided by
# the d_type field of the dirent structure (see readdir(3))
# are disabled to speed up the listing process. stat(3) and access(3)
# are not executed at all, so that we cannot know in advance if a file
# is readable by the current user, if it is executable, SUID, SGID, if a
# symlink is broken, and so on. The file extension check is ignored as
# well, so that the color per extension feature is disabled.
LightMode=false
# If running with colors, append directory indicator to directories. If
# running without colors (via the --no-colors option), append file type
# indicator at the end of file names:
# '/' for directories
# '@' for symbolic links
# '=' for sockets
# '|' for FIFO/pipes
# '*' for for executable files
# '?' for unknown file types
# Bear in mind that when running in light mode the check for executable
# files won't be performed, and thereby no indicator will be added to
# executable files.
Classify=true
# Should the Selection Box be shared among different profiles?
ShareSelbox=false
# Choose the resource opener to open files with their default associated
# application. If not set, 'lira', CLiFM's built-in opener, is used.
Opener=
# Only used when opening a directory via a new CliFM instance (with the
# 'x' command), this option specifies the command to be used to launch a
# terminal emulator to run CliFM on it.
TerminalCmd='xterm -e'
# Choose sorting method: 0 = none, 1 = name, 2 = size, 3 = atime
# 4 = btime (ctime if not available), 5 = ctime, 6 = mtime, 7 = version
# (name if note available) 8 = extension, 9 = inode, 10 = owner,
# 11 = group
Sort=1
# By default, CliFM sorts files from less to more (ex: from 'a' to 'z' if
# using the "name" method). To invert this ordering, set SortReverse to
# true (you can also use the --sort-reverse option or the 'st' command)
SortReverse=false
# Print a usage tip at startup
Tips=true
ListDirsFirst=true
# Enable case sensitive listing for files in the current directory
CaseSensitiveList=false
# Enable case sensitive lookup for the directory jumper function (via
# the 'j' command)
CaseSensitiveDirJump=false
# Enable case sensitive completion for file names
CaseSensitivePathComp=false
# Enable case sensitive search
CaseSensitiveSearch=false
Unicode=true
# Enable Mas, the files list pager (executed whenever the list of files
# does not fit in the screen)
Pager=false
# Maximum file name length for listed files. Names larger than
# MAXFILENAMELEN will be truncated at MAXFILENAMELEN using a tilde
# Set it to -1 (or empty) to remove this limit
# When running in long mode, this setting is overriden by MinFilenameTrim
MaxFilenameLen=20
MaxHistory=1000
MaxDirhist=100
MaxLog=500
DiskUsage=false
# If set to true, always print the list of selected files. Since this
# list could become quite extensive, you can limit the number of printed
# entries using the MaxPrintSelfiles option (-1 = no limit, 0 = auto
# (never print more than half terminal height), or any custom value)
PrintSelfiles=false
MaxPrintSelfiles=0
# If set to true, clear the screen before listing files
ClearScreen=true
# If not specified, StartingPath defaults to the current working
# directory.
StartingPath=
# If set to true, start CliFM in the last visited directory (and in the
# last used workspace). This option overrides StartingPath.
RestoreLastPath=true
# If set to true, the 'r' command executes 'trash' instead of 'rm' to
# prevent accidental deletions.
TrashAsRm=false
# Set readline editing mode: 0 for vi and 1 for emacs (default).
RlEditMode=1
# ALIASES
# Bind '?' to the interactive help plugin. Run 'actions' to print the # list of available plugins
#alias ?='ih'
# Bind 'b' to the directory history navigation plugin
#alias b='dh'
# Replace the standard deselect command (ds) by the fzfdesel plugin
#alias ds='**'
# PROMPT COMMANDS
# Write below the commands you want to be executed before each prompt. Ex:
#promptcmd /usr/local/share/clifm/plugins/git_status.sh
#promptcmd date | awk '{print $1", "$2,$3", "$4}'
# AUTOCOMMANDS
# Control CliFM's settings on a per directory basis. For more information
# consult the manpage
# Remote file systems are slow: let's speed this up by enabling the light
# mode and disabling the files counter
#autocmd /media/remotes/** lm=1,fc=0
# Just a friendly reminder
#autocmd ~/important !printf "Keep your fingers outta here!\n" && read -n1
# Plenty of images and vids? Launch the files previewer plugin
#autocmd ~/Downloads !/usr/local/share/clifm/plugins/fzfnav.sh

View File

@ -0,0 +1,7 @@
/home/repo
/home/repo/Development
/home/repo/Development/lovesay
/home/repo/Development
/home/repo
/home/repo/Development
/home/repo/Development/lovesay

View File

@ -0,0 +1,2 @@
edit
echo $EDITOR

View File

@ -0,0 +1,4 @@
2:1660715888:1660716582:/home/repo
3:1660716456:1660716585:/home/repo/Development
2:1660716460:1660716586:/home/repo/Development/lovesay
@3100

View File

@ -0,0 +1 @@
[2022-8-17T6:58:8] NOTE: clifm created a new MIME list file (~/.config//clifm/profiles/default/mimelist.cfm). It is recommended to edit this file (entering 'mm edit' or pressing F6) to add the programs you use and remove those you don't. This will make the process of opening files faster and smoother

View File

@ -0,0 +1,119 @@
###################################
# Configuration file for Lira #
# CliFM's resource opener #
###################################
# Commented and blank lines are omitted
# The below settings cover the most common filetypes
# It is recommended to edit this file placing your prefered applications
# at the beginning of the apps list to speed up the opening process
# The file is read top to bottom and left to right; the first existent
# application found will be used
# Applications defined here are NOT desktop files, but commands (arguments
# could be used as well). Write you own handmade scripts to open specific
# files if necessary. Ex: X:^text/.*:~/scripts/my_cool_script.sh
# Use 'X' to specify a GUI environment and '!X' for non-GUI environments,
# like the kernel built-in console or a remote SSH session.
# Use 'N' to match file names instead of MIME types.
# Regular expressions are allowed for both file types and file names.
# Use the %f placeholder to specify the position of the file name to be
# opened in the command. Example:
# 'mpv %f --terminal=no' -> 'mpv FILE --terminal=no'
# If %f is not specified, the file name will be added to the end of the
# command. Ex: 'mpv --terminal=no' -> 'mpv --terminal=no FILE'
# Running the opening application in the background:
# For GUI applications:
# APP %f &
# For terminal applications:
# TERM -e APP %f &
# Replace 'TERM' and 'APP' by the corresponding values. The -e option
# might vary depending on the terminal emulator used (TERM)
# To silence STDERR and/or STDOUT use !E and !O respectivelly (they could
# be used together). Examples:
# Silence STDERR only and run in the foreground:
# mpv %f !E
# Silence both (STDERR and STDOUT) and run in the background:
# mpv %f !EO &
# or
# mpv %f !E !O &
# Environment variables could be used as well. Example:
# X:text/plain=$TERM -e $EDITOR %f &;$VISUAL;nano;vi
###########################
# File names/extensions #
###########################
# Match a full file name
#X:N:some_filename=cmd
# Match all file names starting with 'str'
#X:N:^str.*=cmd
# Match files with extension 'ext'
#X:N:.*\.ext$=cmd
X:N:.*\.djvu$=djview;zathura;evince;atril
X:N:.*\.epub$=mupdf;zathura;ebook-viewer
X:N:.*\.mobi$=ebook-viewer
X:N:.*\.(cbr|cbz)$=zathura
X:N:(.*\.cfm$|clifmrc)=$EDITOR;$VISUAL;kak;micro;nvim;vim;vis;vi;mg;emacs;ed;nano;mili;leafpad;mousepad;featherpad;gedit;kate;pluma
!X:N:(.*\.cfm$|clifmrc)=$EDITOR;$VISUAL;kak;micro;nvim;vim;vis;vi;mg;emacs;ed;nano
##################
# MIME types #
##################
# Directories - only for the open-with command (ow) and the --open command
# line option
# In graphical environments directories will be opened in a new window
X:inode/directory=xterm -e clifm %f &;xterm -e vifm %f &;pcmanfm %f &;thunar %f &;xterm -e ncdu %f &
!X:inode/directory=vifm;ranger;nnn;ncdu
# Web content
X:^text/html$=$BROWSER;surf;vimprobable;vimprobable2;qutebrowser;dwb;jumanji;luakit;uzbl;uzbl-tabbed;uzbl-browser;uzbl-core;iceweasel;midori;opera;firefox;seamonkey;brave;chromium-browser;chromium;google-chrome;epiphany;konqueror;elinks;links2;links;lynx;w3m
!X:^text/html$=$BROWSER;elinks;links2;links;lynx;w3m
# Text
#X:^text/x-(c|shellscript|perl|script.python|makefile|fortran|java-source|javascript|pascal)$=geany
X:(^text/.*|application/json|inode/x-empty)=$EDITOR;$VISUAL;kak;micro;dte;nvim;vim;vis;vi;mg;emacs;ed;nano;mili;leafpad;mousepad;featherpad;nedit;kate;gedit;pluma;io.elementary.code;liri-text;xed;atom;nota;gobby;kwrite;xedit
!X:(^text/.*|application/json|inode/x-empty)=$EDITOR;$VISUAL;kak;micro;dte;nvim;vim;vis;vi;mg;emacs;ed;nano
# Office documents
X:^application/.*(open|office)document.*=libreoffice;soffice;ooffice
# Archives
# Note: 'ad' is CliFM's built-in archives utility (based on atool). Remove it if you
# prefer another application
X:^application/(zip|gzip|zstd|x-7z-compressed|x-xz|x-bzip*|x-tar|x-iso9660-image)=ad;xarchiver %f &;lxqt-archiver %f &;ark %f &
!X:^application/(zip|gzip|zstd|x-7z-compressed|x-xz|x-bzip*|x-tar|x-iso9660-image)=ad
# PDF
X:.*/pdf$=mupdf;sioyek;llpp;lpdf;zathura;mupdf-x11;apvlv;xpdf;evince;atril;okular;epdfview;qpdfview
# Images
X:^image/gif$=animate;pqiv;sxiv -a;nsxiv -a
X:^image/.*=fim;display;sxiv;nsxiv;pqiv;gpicview;qview;qimgv;inkscape;mirage;ristretto;eog;eom;xviewer;viewnior;nomacs;geeqie;gwenview;gthumb;gimp
!X:^image/*=fim;img2txt;cacaview;fbi;fbv
# Video and audio
X:^video/.*=ffplay;mplayer;mplayer2;mpv;vlc;gmplayer;smplayer;celluloid;qmplayer2;haruna;totem
X:^audio/.*=ffplay -nodisp -autoexit;mplayer;mplayer2;mpv;vlc;gmplayer;smplayer;totem
# Fonts
X:^font/.*=fontforge;fontpreview
# Torrent:
X:application/x-bittorrent=rtorrent;transimission-gtk;transmission-qt;deluge-gtk;ktorrent
# Fallback to another resource opener as last resource
.*=xdg-open;mimeo;mimeopen -n;whippet -m;open;linopen;

View File

@ -0,0 +1,62 @@
#####################################
# Remotes management file for CliFM #
#####################################
# Blank and commented lines are omitted
# The syntax is as follows:
# A name for this remote. It will be used by the 'net' command
# and will be available for TAB completion
# [work_smb]
# Comment=My work samba server
# Mountpoint=/home/user/.config/clifm/mounts/work_smb
# Use %m as a placeholder for Mountpoint
# MountCmd=mount.cifs //WORK_IP/shared %m -o OPTIONS
# UnmountCmd=umount %m
# Automatically mount this remote at startup
# AutoMount=true
# Automatically unmount this remote at exit
# AutoUnmount=true
# A few examples
# A. Samba share
#[samba_share]
#Comment=my samba share
#Mountpoint="~/.config/clifm/mounts/samba_share"
#MountCmd=sudo mount.cifs //192.168.0.26/resource_name %m -o mapchars,credentials=/etc/samba/credentials/samba_share
#UnmountCmd=sudo umount %m
#AutoUnmount=false
#AutoMount=false
# B. SSH file system (sshfs)
#[my_ssh]
#Comment=my ssh
#Mountpoint="/media/ssh"
#MountCmd=sshfs user@192.168.0.12: %m -C -p 22
#UnmountCmd=fusermount3 -u %m
#AutoUnmount=false
#AutoMount=false
# C. Mounting a local file system
#[local]
#Comment=Local filesystem
#Mountpoint="/media/extra"
#MountCmd=sudo mount -U 1232dsd761278... %m
#UnmountCmd=sudo umount %m
#AutoUnmount=false
#AutoMount=true
# D. Mounting a removable device
#[USB]
#Comment=My USB drive
#Mountpoint="/media/usb"
#MountCmd=sudo mount -o gid=1000,fmask=113,dmask=002 -U 5647-1... %m
#UnmountCmd=sudo umount %m
#AutoUnmount=true
#AutoMount=false

View File

@ -0,0 +1,9 @@
# This is CliFM's profile file
#
# Write here the commands you want to be executed at startup
# Ex:
#echo "CliFM, the command line file manager"; read -r
#
# Uncommented, non-empty lines are executed line by line. If you
# want a multi-line command, just write a script for it:
#sh /path/to/my/script.sh

View File

@ -0,0 +1,170 @@
# This file is part of CliFM
# Prompts for CliFM
# Do not edit this file directly: use the 'prompt' command instead
# The regular prompt (just as the warning one, a secondary prompt used
# to highlight invalid/non-existent command names) is built using command
# substitution ($(cmd)), string literals and/or one or more of the
# following escape sequences:
# The prompt line is build using command substitution ($(cmd)), string
# literals and/or the following escape sequences:
#
# \e: Escape character
# \u: The username
# \H: The full hostname
# \h: The hostname, up to the first dot (.)
# \s: The name of the shell (everything after the last slash) currently
# used by CliFM
# \S: Current workspace number (colored according to wsx code in the color
# scheme file)
# \l: Print an L if running in light mode
# \P: The current profile name
# \n: A newline character
# \r: A carriage return
# \a: A bell character
# \d: The date, in abbreviated form (ex: Tue May 26)
# \t: The time, in 24-hour HH:MM:SS format
# \T: The time, in 12-hour HH:MM:SS format
# \@: The time, in 12-hour am/pm format
# \A: The time, in 24-hour HH:MM format
# \w: The full current working directory, with $HOME abbreviated with a
# tilde
# \W: The basename of $PWD, with $HOME abbreviated with a tilde
# \p: A mix of the two above, it abbreviates the current working directory
# only if longer than PathMax (a value defined in the configuration
# file).
# \z: Exit code of the last executed command (printed in green in case of
# success and in bold red in case of error)
# \$: #, if the effective user ID is 0 (root), and $ otherwise
# \nnn: The character whose ASCII code is the octal value nnn
# \\: A literal backslash
# \[: Begin a sequence of non-printing characters. This is mostly used to
# add color to the prompt line (using full ANSI escape sequences)
# \]: End a sequence of non-printing characters
#
# The following files statistics escape sequences are available as well:
#
# \D: Amount of sub-directories in the current directory
# \R: Amount of regular files in the current directory
# \X: Amount of executable files in the current directory
# \.: Amount of hidden files in the current directory
# \U: Amount of SUID files in the current directory
# \G: Amount of SGID files in the current directory
# \F: Amount of FIFO/pipe files in the current directory
# \K: Amount of socket files in the current directory
# \B: Amount of block device files in the current directory
# \C: Amount of character device files in the current directory
# \x: Amount of files with capabilities in the current directory
# \L: Amount of symbolic links in the current directory
# \o: Amount of broken symbolic links in the current directory
# \M: Amount of multi-link files in the current directory
# \E: Amount of files with extended attributes in the current directory
# \O: Amount of other-writable files in the current directory
# \": Amount of files with the sticky bit set in the current directory
# \?: Amount of files of unknown file type in the current directory
# \!: Amount of unstatable files in the current directory
# Escape codes to control prompt notifications:
#
# \*: An asterisk + amount of selected files (e.g. *12)
# \%: 'T' + amount of trashed files (e.g. T3)
# \#: Print an 'R' if running as root
# \(: 'E' + amount of error messages (e.g. E2)
# \): 'W' + amount of warning messages (e.g. W2)
# \=: 'N' + amount of notice messages (e.g. N1)
#
# NOTE: Except in the case of \#, nothing is printed if the corresponding
# number is zero (no selected files, no trashed files, and so on)
# Unicode characters could be inserted by directly pasting the
# corresponding char, or by inserting its hex code:
# echo -ne "paste_your_char" | hexdump -C
# Set Notifications to false to prevent the automatic insertion of
# root, trash, messages (error, warning, and notice), and selected files
# indicators at the left of the prompt, in which case the prompt code
# should handle itself this data using the appropriate escape codes
# To permanetly set any of the below prompts edit your color scheme file
# (via the 'cs edit' command), set Prompt to either the prompt code or
# the prompt name you want (e.g. Prompt="classic"), and comment out the
# remaining prompt lines
#
# NOTE: Since the below prompts have been designed for CliFM's default
# color scheme, you might need to edit the one you choose manually to
# make it fit your current color scheme. For example, the last color
# used in the warning prompt should match the 'wp' color defined in your
# color scheme file
[clifm]
Notifications=true
RegularPrompt="\[\e[0m\][\[\e[0;36m\]\S\[\e[0m\]]\l \A \u:\H \[\e[0;36m\]\w\n\[\e[0m\]<\z\[\e[0m\]> \[\e[0;34m\]\$ \[\e[0m\]"
EnableWarningPrompt=true
WarningPrompt="\[\e[00;02;31m\](!) > "
#[clifm-colorless]
#Notifications=true
#RegularPrompt="\[\e[0m\][\S]\l \A \u:\H \w\n<\z\[\e[0m\]> \$ "
#EnableWarningPrompt=true
#WarningPrompt="(!) > "
[clifm-box-drawing]
# The box drawing set isn't supported by all terminals
Notifications=false
RegularPrompt="\[\e[0m\]\[\e[0;36m\]\[\e(0\]lq\[\e(B\]\[\e[0;31m\]\#\[\e[32m\]\*\[\e[36m\]\%\[\e[31m\]\(\[\e[33m\]\)\[\e[32m\]\=\[\e[0m\][\S\[\e[0m\]]\l \A \u:\H \[\e[0;36m\]\w\n\[\e[0;36m\]\[\e(0\]mq\[\e(B\]\[\e[0m\]<\z\[\e[0m\]> \[\e[0;34m\]\$ \[\e[0m\]"
EnableWarningPrompt=true
WarningPrompt="\[\e[0;36m\]\[\e(0\]mq\[\e(B\]\[\e[0m\]<\z\[\e[0m\]> \[\e[1;31m\]\! \[\e[00;02;31m\]"
[classic]
Notifications=true
RegularPrompt="\[\e[1;32m\][\u@\H] \[\e[1;34m\]\w \[\e[0m\]\$ "
EnableWarningPrompt=true
WarningPrompt="\[\e[1;32m\][\u@\H] \[\e[1;34m\]\w \[\e[1;31m\]! \[\e[00;02;31m\]"
[security-scanner]
# Print file statistics about the current directory (-:-:-:-) in this order:
# SUID, SGID, other-writable, and executable files
Notifications=true
RegularPrompt="\[\e[0m\][\[\e[0;36m\]\S\[\e[0m\]]\l \[\e[0m\]\[\e[1;31m\]\U\[\e[0m\]:\[\e[1;33m\]\G\[\e[0m\]:\[\e[1;34m\]\O\[\e[0m\]:\[\e[1;32m\]\X\[\e[0m\] \A \[\e[0;36m\]\w\n\[\e[0m\]<\z\[\e[0m\]> \[\e[0;34m\]\$ \[\e[0m\]"
EnableWarningPrompt=true
WarningPrompt="\[\e[00;02;31m\](!) > "
[curves]
Notifications=false
RegularPrompt="\[\e[00;01;32m\]╭─\[\e[0m\]\[\e[1;32m\]\*\[\e[1;36m\]\%\[\e[1;31m\]\(\[\e[1;33m\]\)\[\e[1;32m\]\=\[\e[0m\][\S\[\e[0m\]]\[\e[01;32m\]─\[\e[0m\](\u:\H)\[\e[01;32m\]─\[\e[0m\][\[\e[00;36m\]\w\[\e[0m\]]\n\[\e[01;32m\]╰─\[\e[1;0m\]<\z\[\e[0m\]> \[\e[34m\]λ\[\e[0m\] "
EnableWarningPrompt=true
WarningPrompt="\[\e[0m\]\[\e[01;32m\]╰─\[\e[1;0m\]<\z\[\e[0m\]> \[\e[0;31m\]λ\[\e[00;02;31m\] "
# The prompts below require a patched Nerdfont
[firestarter]
Notifications=false
RegularPrompt="\[\e[01;38;5;124m\]╭─\[\e[38;5;124m\]\[\e[37;48;5;124m\]\[\e[1;37m\]\#\[\e[32m\]\*\[\e[36m\]\%\[\e[37m\]\(\[\e[33m\]\)\[\e[32m\]\=\[\e[00;37;48;5;124m\][\S\[\e[37;48;5;124m\]] \[\e[0;48;5;124m\]\A \[\e[00;38;5;124;43m\]\[\e[00;30;43m\] \u:\H \[\e[00;33;48;5;124m\]\[\e[00;37;48;5;124m\] \w \[\e[00;38;5;124m\]\[\e[0m\]\n\[\e[01;38;5;124m\]╰─▶ \[\e[0m\]"
EnableWarningPrompt=true
WarningPrompt="\[\e[00;01;38;5;124m\]╰─\[\e[0;38;5;124m\]▶ \[\e[00;02;31m\]"
[cold-winter]
Notifications=false
RegularPrompt="\[\e[00;37;100m\]\[\e[1;31m\]\#\[\e[32m\]\*\[\e[36m\]\%\[\e[31m\]\(\[\e[33m\]\)\[\e[32m\]\=\[\e[0;37;100m\][\S\[\e[00;37;100m\]] \A \[\e[00;90;46m\] \[\e[0;30;46m\]\u:\H \[\e[0;36;100m\] \[\e[00;37;100m\]\w \[\e[00;90;40m\] \n \[\e[1;90m\]\[\e[0m\] "
EnableWarningPrompt=true
WarningPrompt=" \[\e[0m\]\[\e[1;2;31m\] \[\e[00;02;31m\]"
[spot]
Notifications=false
RegularPrompt="\[\e[00;38;5;0;48;5;53m\] \[\e[31m\]\#\[\e[32m\]\*\[\e[36m\]\%\[\e[31m\]\(\[\e[34m\]\)\[\e[32m\]\=\[\e[00;37;48;5;53m\][\S\[\e[37m\]] \[\e[38;5;53;48;5;178m\] \[\e[00;38;5;0;48;5;178m\]\A \u:\H \w \[\e[00;38;5;178;48;5;0m\]\[\e[0;40m\]\n\[\e[0;38;5;254;48;5;53m\] \$ \[\e[0;38;5;53;48;5;0m\] \[\e[0m\] "
EnableWarningPrompt=true
WarningPrompt="\n\[\e[0;37;48;5;124m\] \x \[\e[0;38;5;124;48;5;0m\] \[\e[00;02;31m\] "
[artic-particles]
Notifications=false
RegularPrompt="\[\e[00;37;48;5;18m\] \A \[\e[00;38;5;18;47m\] \u:\H \[\e[00;37;48;5;18m\] \w \[\e[00;38;5;18;40m\] \n\[\e[00;37;48;5;18m\] \$ \[\e[00;38;5;18;40m\] "
EnableWarningPrompt=true
WarningPrompt="\[\e[00;02;31;47m\] \$ \[\e[00;37;0m\] \[\e[00;02;31m\]"
[green-beret]
Notifications=false
RegularPrompt="╭─\[\e[0;38;5;239;48;5;0m\]\[\e[0;38;5;15;48;5;239m\]\[\e[31m\]\#\[\e[38;5;76m\]\*\[\e[36m\]\%\[\e[31m\]\(\[\e[33m\]\)\[\e[32m\]\=\[\e[38;5;15m\][\S\[\e[38;5;15m\]]  \A \[\e[0;38;5;239;48;5;70m\]\[\e[0;38;5;0;48;5;70m\] \w \[\e[0;38;5;70;48;5;0m\]\n\[\e[0;40m\]╰─\[\e[0;38;5;70;48;5;0m\]▶\[\e[0;40m\] "
EnableWarningPrompt=true
WarningPrompt="\[\e[0;40m\]╰─\[\e[0;38;5;9;48;5;0m\]▶ \[\e[00;02;31m\]"

View File

@ -0,0 +1,78 @@
# Readline keybindings for CliFM
# For the complete list of Readline options see:
# https://www.gnu.org/software/bash/manual/html_node/Readline-Init-File-Syntax.html#Readline-Init-File-Syntax
#$include /etc/inputrc
# Color files by types
set colored-stats on
# Append char to indicate type
set visible-stats on
# Mark symlinked directories
set mark-symlinked-directories on
# Color the common prefix
set colored-completion-prefix on
# Color the common prefix in menu-complete
set menu-complete-display-prefix on
# Disable paste protection
set enable-bracketed-paste on
set show-all-if-ambiguous on
set completion-ignore-case on
set meta-flag on
set input-meta on
set output-meta on
$if mode=emacs
# For linux console and RH/Debian xterm
"\e[5C": forward-word
"\e[5D": backward-word
"\e\e[C": forward-word
"\e\e[D": backward-word
"\e[1;5C": forward-word
"\e[1;5D": backward-word
# For rxvt
"\x1b\x4f\x64": backward-word
"\x1b\x4f\x63": forward-word
# A few keybinds to avoid conflicts with CliFM specific keybinds
"\C-d":
"\e\e":
"\C-r\C-r": re-read-init-file
"\C-p\C-p": exchange-point-and-mark
"\C-zA": do-lowercase-version
"\C-zB": do-lowercase-version
"\C-zC": do-lowercase-version
"\C-zD": do-lowercase-version
"\C-zE": do-lowercase-version
"\C-zF": do-lowercase-version
"\C-zG": do-lowercase-version
"\C-zH": do-lowercase-version
"\C-zI": do-lowercase-version
"\C-zJ": do-lowercase-version
"\C-zK": do-lowercase-version
"\C-zL": do-lowercase-version
"\C-zM": do-lowercase-version
"\C-zN": do-lowercase-version
"\C-zO": do-lowercase-version
"\C-zP": do-lowercase-version
"\C-zQ": do-lowercase-version
"\C-zR": do-lowercase-version
"\C-zS": do-lowercase-version
"\C-zT": do-lowercase-version
"\C-zU": do-lowercase-version
"\C-zV": do-lowercase-version
"\C-zW": do-lowercase-version
"\C-zX": do-lowercase-version
"\C-zY": do-lowercase-version
"\C-zZ": do-lowercase-version
# History completion based on prefix
#"\e[A": history-search-backward
#"\e[B": history-search-forward
$endif

Binary file not shown.

View File

@ -1,30 +1,30 @@
# Unlock user from passwords
function ulock
faillock --reset
command faillock --reset
end
# Check ports for current user
function ports
sudo netstat -tulanp
command sudo netstat -tulanp
end
# Set permissions for user
function setperm
sudo chown dt:dt $argv
command sudo chown repo:repo $argv
end
# Stow commands
function stowadd
stow -St ~ $argv
command stow -St ~ $argv
end
function stowremove
stow -Dt ~ $argv
command stow -Dt ~ $argv
end
# Clear command
function clear
command reset && fish
command reset && shellfetch
end
# free
@ -39,5 +39,14 @@ end
# grub update
function update-grub
sudo grub-mkconfig -o /boot/grub/grub.cfg
command sudo grub-mkconfig -o /boot/grub/grub.cfg
end
# add new fonts
function update-fc
command fc-cache -fv
end
function reload
source ~/.config/fish/config.fish
end

View File

@ -98,7 +98,3 @@ end
function gall
gpull && gadd . && gcommit -s && gpush
end
function config
git --git-dir=/mnt/500GB/.gitlabs/newDotFiles --work-tree=$HOME $argv
end

View File

@ -17,34 +17,34 @@ function pacman
command sudo pacman --color auto $argv
else
if pacman -Qttdq
pacman -Qttdq | pacman -Rns -
command sudo pacman -Qttdq | command sudo pacman -Rns -
end
end
end
# Update Repo
function update
pacman -Syu
command pacman -Syu
end
function aurupdate
auracle update -C ~/.cache/pkgs/
command auracle update -C ~/.cache/pkgs/
end
function upall
pacman -Fy && pacman -Syu --noconfirm && aurupdate
command pacman -Fy && pacman -Syu --noconfirm && aurupdate
end
#check aur and arch packages
function checkarch
pacman -Qqen >~/package_list.txt
command pacman -Qqen >~/package_list.txt
end
function checkaur
pacman -Qqem >~/package_list_aur.txt
command pacman -Qqem >~/package_list_aur.txt
end
# Pacman unlock
function unlock
sudo rm /var/lib/pacman/db.lck
command sudo rm /var/lib/pacman/db.lck
end

View File

@ -0,0 +1,3 @@
function chris
ssh linknsync@51.89.161.207
end

View File

@ -1,5 +1,5 @@
if test -f "$HOME/.confid/fish/fish.profile"
source "$HOME/.confid/fish/fish.profile"
if test -f "$HOME/.config/fish/fish.profile"
source "$HOME/.config/fish/fish.profile"
end
set PATH "$HOME/.local/bin:$PATH"
@ -35,7 +35,6 @@ if test -d "$HOME/.local/bin/clipmenu"
end
function fish_greeting
# bfetch --source ~/.config/bfetch/ascii.art --ascii_colors 7 1 2 3 5 8 --birthday 16/06
shellfetch
end
@ -52,8 +51,8 @@ end
bind \ec __history_previous_command
bind \e\e __sudope
if status is-login
if test (tty) = /dev/tty1
exec tbsm
end
end
# if status is-login
# if test (tty) = /dev/tty1
# exec tbsm
# end
# end

View File

@ -0,0 +1,4 @@
function sync_history --on-event fish_preexec
history --save
history --merge
end

View File

@ -0,0 +1,85 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
application.name = "WirePlumber Bluetooth"
log.level = 2
wireplumber.script-engine = lua-scripting
wireplumber.export-core = false
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.bluez5.* = bluez5/libspa-bluez5
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses RTKit to boost the data thread priority.
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = bluetooth.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,18 @@
bluez_monitor = {}
bluez_monitor.properties = {}
bluez_monitor.rules = {}
function bluez_monitor.enable()
if bluez_monitor.enabled == false then
return
end
load_monitor("bluez", {
properties = bluez_monitor.properties,
rules = bluez_monitor.rules,
})
if bluez_monitor.properties["with-logind"] then
load_optional_module("logind")
end
end

View File

@ -0,0 +1,116 @@
bluez_monitor.enabled = true
bluez_monitor.properties = {
-- These features do not work on all headsets, so they are enabled
-- by default based on the hardware database. They can also be
-- forced on/off for all devices by the following options:
--["bluez5.enable-sbc-xq"] = true,
--["bluez5.enable-msbc"] = true,
--["bluez5.enable-hw-volume"] = true,
-- See bluez-hardware.conf for the hardware database.
-- Enabled headset roles (default: [ hsp_hs hfp_ag ]), this
-- property only applies to native backend. Currently some headsets
-- (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag
-- enabled, disable either hsp_ag or hfp_ag to work around it.
--
-- Supported headset roles: hsp_hs (HSP Headset),
-- hsp_ag (HSP Audio Gateway),
-- hfp_hf (HFP Hands-Free),
-- hfp_ag (HFP Audio Gateway)
--["bluez5.headset-roles"] = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]",
-- Enabled A2DP codecs (default: all).
--["bluez5.codecs"] = "[ sbc sbc_xq aac ldac aptx aptx_hd aptx_ll aptx_ll_duplex faststream faststream_duplex ]",
-- HFP/HSP backend (default: native).
-- Available values: any, none, hsphfpd, ofono, native
--["bluez5.hfphsp-backend"] = "native",
-- Properties for the A2DP codec configuration
--["bluez5.default.rate"] = 48000,
--["bluez5.default.channels"] = 2,
-- Register dummy AVRCP player, required for AVRCP volume function.
-- Disable if you are running mpris-proxy or equivalent.
--["bluez5.dummy-avrcp-player"] = true,
-- Enable the logind module, which arbitrates which user will be allowed
-- to have bluetooth audio enabled at any given time (particularly useful
-- if you are using GDM as a display manager, as the gdm user also launches
-- pipewire and wireplumber).
-- This requires access to the D-Bus user session; disable if you are running
-- a system-wide instance of wireplumber.
["with-logind"] = true,
}
bluez_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "bluez_card.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- Auto-connect device profiles on start up or when only partial
-- profiles have connected. Disabled by default if the property
-- is not specified.
--["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink ]",
-- Hardware volume control (default: [ hfp_ag hsp_ag a2dp_source ])
--["bluez5.hw-volume"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
-- LDAC encoding quality
-- Available values: auto (Adaptive Bitrate, default)
-- hq (High Quality, 990/909kbps)
-- sq (Standard Quality, 660/606kbps)
-- mq (Mobile use Quality, 330/303kbps)
--["bluez5.a2dp.ldac.quality"] = "auto",
-- AAC variable bitrate mode
-- Available values: 0 (cbr, default), 1-5 (quality level)
--["bluez5.a2dp.aac.bitratemode"] = 0,
-- Profile connected first
-- Available values: a2dp-sink (default), headset-head-unit
--["device.profile"] = "a2dp-sink",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "bluez_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "bluez_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["resample.quality"] = 4,
--["channelmix.normalize"] = false,
--["channelmix.mix-lfe"] = false,
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
--["monitor.channel-volumes"] = false,
-- A2DP source role, "input" or "playback"
-- Defaults to "playback", playing stream to speakers
-- Set to "input" to use as an input for apps
--["bluez5.a2dp-source-role"] = "input",
},
},
}

View File

@ -0,0 +1 @@
bluez_monitor.enable()

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,74 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
#application.name = WirePlumber
log.level = 2
wireplumber.script-engine = lua-scripting
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.alsa.* = alsa/libspa-alsa
api.v4l2.* = v4l2/libspa-v4l2
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = main.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,19 @@
default_access = {}
default_access.properties = {}
default_access.rules = {}
function default_access.enable()
if default_access.enabled == false then
return
end
load_access("default", {
rules = default_access.rules
})
if default_access.properties["enable-flatpak-portal"] then
-- Enables portal permissions via org.freedesktop.impl.portal.PermissionStore
load_module("portal-permissionstore")
load_access("portal")
end
end

View File

@ -0,0 +1,29 @@
alsa_monitor = {}
alsa_monitor.properties = {}
alsa_monitor.rules = {}
function alsa_monitor.enable()
if alsa_monitor.enabled == false then
return
end
-- The "reserve-device" module needs to be loaded for reservation to work
if alsa_monitor.properties["alsa.reserve"] then
load_module("reserve-device")
end
load_monitor("alsa", {
properties = alsa_monitor.properties,
rules = alsa_monitor.rules,
})
if alsa_monitor.properties["alsa.midi"] then
load_monitor("alsa-midi", {
properties = alsa_monitor.properties,
})
-- The "file-monitor-api" module needs to be loaded for MIDI device monitoring
if alsa_monitor.properties["alsa.midi.monitoring"] then
load_module("file-monitor-api")
end
end
end

View File

@ -0,0 +1,14 @@
libcamera_monitor = {}
libcamera_monitor.properties = {}
libcamera_monitor.rules = {}
function libcamera_monitor.enable()
if libcamera_monitor.enabled == false then
return
end
load_monitor("libcamera", {
properties = libcamera_monitor.properties,
rules = libcamera_monitor.rules,
})
end

View File

@ -0,0 +1,14 @@
v4l2_monitor = {}
v4l2_monitor.properties = {}
v4l2_monitor.rules = {}
function v4l2_monitor.enable()
if v4l2_monitor.enabled == false then
return
end
load_monitor("v4l2", {
properties = v4l2_monitor.properties,
rules = v4l2_monitor.rules,
})
end

View File

@ -0,0 +1,62 @@
device_defaults = {}
device_defaults.enabled = true
device_defaults.properties = {
-- store preferences to the file system and restore them at startup;
-- when set to false, default nodes and routes are selected based on
-- their priorities and any runtime changes do not persist after restart
["use-persistent-storage"] = true,
-- the default volume to apply to ACP device nodes, in the linear scale
--["default-volume"] = 0.4,
-- Whether to auto-switch to echo cancel sink and source nodes or not
["auto-echo-cancel"] = true,
-- Sets the default echo-cancel-sink node name to automatically switch to
["echo-cancel-sink-name"] = "echo-cancel-sink",
-- Sets the default echo-cancel-source node name to automatically switch to
["echo-cancel-source-name"] = "echo-cancel-source",
}
-- Sets persistent device profiles that should never change when wireplumber is
-- running, even if a new profile with higher priority becomes available
device_defaults.persistent_profiles = {
{
matches = {
{
-- Matches all devices
{ "device.name", "matches", "*" },
},
},
profile_names = {
"off",
"pro-audio"
}
},
}
function device_defaults.enable()
if device_defaults.enabled == false then
return
end
-- Selects appropriate default nodes and enables saving and restoring them
load_module("default-nodes", device_defaults.properties)
-- Selects appropriate profile for devices
load_script("policy-device-profile.lua", {
persistent = device_defaults.persistent_profiles
})
-- Selects appropriate device routes ("ports" in pulseaudio terminology)
-- and enables saving and restoring them together with
-- their properties (per-route/port volume levels, channel maps, etc)
load_script("policy-device-routes.lua", device_defaults.properties)
if device_defaults.properties["use-persistent-storage"] then
-- Enables functionality to save and restore default device profiles
load_module("default-profile")
end
end

View File

@ -0,0 +1,37 @@
stream_defaults = {}
stream_defaults.enabled = true
stream_defaults.properties = {
-- whether to restore the last stream properties or not
["restore-props"] = true,
-- whether to restore the last stream target or not
["restore-target"] = true,
}
stream_defaults.rules = {
-- Rules to override settings per node
-- {
-- matches = {
-- {
-- { "application.name", "matches", "pw-play" },
-- },
-- },
-- apply_properties = {
-- ["state.restore-props"] = false,
-- ["state.restore-target"] = false,
-- },
-- },
}
function stream_defaults.enable()
if stream_defaults.enabled == false then
return
end
-- Save and restore stream-specific properties
load_script("restore-stream.lua", {
properties = stream_defaults.properties,
rules = stream_defaults.rules,
})
end

View File

@ -0,0 +1,146 @@
alsa_monitor.enabled = true
alsa_monitor.properties = {
-- Create a JACK device. This is not enabled by default because
-- it requires that the PipeWire JACK replacement libraries are
-- not used by the session manager, in order to be able to
-- connect to the real JACK server.
--["alsa.jack-device"] = false,
-- Reserve devices via org.freedesktop.ReserveDevice1 on D-Bus
-- Disable if you are running a system-wide instance, which
-- doesn't have access to the D-Bus user session
["alsa.reserve"] = true,
--["alsa.reserve.priority"] = -20,
--["alsa.reserve.application-name"] = "WirePlumber",
-- Enables MIDI functionality
["alsa.midi"] = true,
-- Enables monitoring of alsa MIDI devices
["alsa.midi.monitoring"] = true,
-- These properties override node defaults when running in a virtual machine.
-- The rules below still override those.
["vm.node.defaults"] = {
["api.alsa.period-size"] = 256,
["api.alsa.headroom"] = 8192,
},
}
alsa_monitor.rules = {
-- An array of matches/actions to evaluate.
--
-- If you want to disable some devices or nodes, you can apply properties per device as the following example.
-- The name can be found by running pw-cli ls Device, or pw-cli dump Device
--{
-- matches = {
-- {
-- { "device.name", "matches", "name_of_some_disabled_card" },
-- },
-- },
-- apply_properties = {
-- ["device.disabled"] = true,
-- },
--}
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "alsa_card.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- Use ALSA-Card-Profile devices. They use UCM or the profile
-- configuration to configure the device and mixer settings.
["api.alsa.use-acp"] = true,
-- Use UCM instead of profile when available. Can be
-- disabled to skip trying to use the UCM profile.
--["api.alsa.use-ucm"] = true,
-- Don't use the hardware mixer for volume control. It
-- will only use software volume. The mixer is still used
-- to mute unused paths based on the selected port.
--["api.alsa.soft-mixer"] = false,
-- Ignore decibel settings of the driver. Can be used to
-- work around buggy drivers that report wrong values.
--["api.alsa.ignore-dB"] = false,
-- The profile set to use for the device. Usually this is
-- "default.conf" but can be changed with a udev rule or here.
--["device.profile-set"] = "profileset-name",
-- The default active profile. Is by default set to "Off".
--["device.profile"] = "default profile name",
-- Automatically select the best profile. This is the
-- highest priority available profile. This is disabled
-- here and instead implemented in the session manager
-- where it can save and load previous preferences.
["api.acp.auto-profile"] = false,
-- Automatically switch to the highest priority available port.
-- This is disabled here and implemented in the session manager instead.
["api.acp.auto-port"] = false,
-- Other properties can be set here.
--["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "alsa_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "alsa_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["node.description"] = "My Node Description",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
--["monitor.channel-volumes"] = false
--["resample.quality"] = 4,
--["resample.disable"] = false,
--["channelmix.normalize"] = false,
--["channelmix.mix-lfe"] = false,
--["channelmix.upmix"] = true,
--["channelmix.upmix-method"] = "psd", -- "none" or "simple"
--["channelmix.lfe-cutoff"] = 150,
--["channelmix.fc-cutoff"] = 12000,
--["channelmix.rear-delay"] = 12.0,
--["channelmix.stereo-widen"] = 0.0,
--["channelmix.hilbert-taps"] = 0,
--["channelmix.disable"] = false,
--["dither.noise"] = 0,
--["audio.channels"] = 2,
--["audio.format"] = "S16LE",
--["audio.rate"] = 44100,
--["audio.allowed-rates"] = "32000,96000",
--["audio.position"] = "FL,FR",
--["api.alsa.period-size"] = 1024,
--["api.alsa.period-num"] = 2,
--["api.alsa.headroom"] = 0,
--["api.alsa.start-delay"] = 0,
--["api.alsa.disable-mmap"] = false,
--["api.alsa.disable-batch"] = false,
--["api.alsa.use-chmap"] = false,
--["api.alsa.multirate"] = true,
--["latency.internal.rate"] = 0
--["latency.internal.ns"] = 0
--["clock.name"] = "api.alsa.0"
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
},
},
}

View File

@ -0,0 +1,36 @@
default_access.enabled = true
default_access.properties = {
-- Enable the use of the flatpak portal integration.
-- Disable if you are running a system-wide instance, which
-- doesn't have access to the D-Bus user session
["enable-flatpak-portal"] = true,
}
default_access.rules = {
{
matches = {
{
{ "pipewire.access", "=", "flatpak" },
{ "media.category", "=", "Manager" },
},
},
default_permissions = "all",
},
{
matches = {
{
{ "pipewire.access", "=", "flatpak" },
},
},
default_permissions = "rx",
},
{
matches = {
{
{ "pipewire.access", "=", "restricted" },
},
},
default_permissions = "rx",
},
}

View File

@ -0,0 +1,38 @@
libcamera_monitor.enabled = true
libcamera_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "libcamera_device.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- ["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "libcamera_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "libcamera_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
},
},
}

View File

@ -0,0 +1,38 @@
v4l2_monitor.enabled = true
v4l2_monitor.rules = {
-- An array of matches/actions to evaluate.
{
-- Rules for matching a device or node. It is an array of
-- properties that all need to match the regexp. If any of the
-- matches work, the actions are executed for the object.
matches = {
{
-- This matches all cards.
{ "device.name", "matches", "v4l2_device.*" },
},
},
-- Apply properties on the matched object.
apply_properties = {
-- ["device.nick"] = "My Device",
},
},
{
matches = {
{
-- Matches all sources.
{ "node.name", "matches", "v4l2_input.*" },
},
{
-- Matches all sinks.
{ "node.name", "matches", "v4l2_output.*" },
},
},
apply_properties = {
--["node.nick"] = "My Node",
--["priority.driver"] = 100,
--["priority.session"] = 100,
--["node.pause-on-idle"] = false,
},
},
}

View File

@ -0,0 +1,23 @@
-- Provide the "default" pw_metadata, which stores
-- dynamic properties of pipewire objects in RAM
load_module("metadata")
-- Default client access policy
default_access.enable()
-- Load devices
alsa_monitor.enable()
v4l2_monitor.enable()
libcamera_monitor.enable()
-- Track/store/restore user choices about devices
device_defaults.enable()
-- Track/store/restore user choices about streams
stream_defaults.enable()
-- Link nodes by stream role and device intended role
load_script("intended-roles.lua")
-- Automatically suspends idle nodes after 3 seconds
load_script("suspend-node.lua")

View File

@ -0,0 +1,73 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
application.name = "WirePlumber Policy"
log.level = 2
wireplumber.script-engine = lua-scripting
wireplumber.export-core = false
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file
# Other components are loaded from there
{ name = policy.lua, type = config/lua }
]

View File

@ -0,0 +1,36 @@
components = {}
function load_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
end
end
function load_optional_module(m, a)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
end
end
function load_pw_module(m)
assert(type(m) == "string", "module name is mandatory, bail out");
if not components[m] then
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
end
end
function load_script(s, a)
if not components[s] then
components[s] = { s, type = "script/lua", args = a }
end
end
function load_monitor(s, a)
load_script("monitors/" .. s .. ".lua", a)
end
function load_access(s, a)
load_script("access/access-" .. s .. ".lua", a)
end

View File

@ -0,0 +1,76 @@
default_policy = {}
default_policy.enabled = true
default_policy.properties = {}
default_policy.endpoints = {}
default_policy.policy = {
["move"] = true, -- moves session items when metadata target.node changes
["follow"] = true, -- moves session items to the default device when it has changed
-- Whether to forward the ports format of filter stream nodes to their
-- associated filter device nodes. This is needed for application to stream
-- surround audio if echo-cancel is enabled.
["filter.forward-format"] = false,
-- Set to 'true' to disable channel splitting & merging on nodes and enable
-- passthrough of audio in the same format as the format of the device.
-- Note that this breaks JACK support; it is generally not recommended
["audio.no-dsp"] = false,
-- how much to lower the volume of lower priority streams when ducking
-- note that this is a linear volume modifier (not cubic as in pulseaudio)
["duck.level"] = 0.3,
}
bluetooth_policy = {}
bluetooth_policy.policy = {
-- Whether to store state on the filesystem.
["use-persistent-storage"] = true,
-- Whether to use headset profile in the presence of an input stream.
["media-role.use-headset-profile"] = true,
-- Application names correspond to application.name in stream properties.
-- Applications which do not set media.role but which should be considered
-- for role based profile switching can be specified here.
["media-role.applications"] = { "Firefox", "Chromium input", "Google Chrome input", "Brave input", "Microsoft Edge input", "Vivaldi input", "ZOOM VoiceEngine", "Telegram Desktop", "telegram-desktop", "linphone", "Mumble" },
}
function default_policy.enable()
if default_policy.enabled == false then
return
end
-- Session item factories, building blocks for the session management graph
-- Do not disable these unless you really know what you are doing
load_module("si-node")
load_module("si-audio-adapter")
load_module("si-standard-link")
load_module("si-audio-endpoint")
-- API to access default nodes from scripts
load_module("default-nodes-api")
-- API to access mixer controls, needed for volume ducking
load_module("mixer-api")
-- Create endpoints statically at startup
load_script("static-endpoints.lua", default_policy.endpoints)
-- Create items for nodes that appear in the graph
load_script("create-item.lua", default_policy.policy)
-- Link nodes to each other to make media flow in the graph
load_script("policy-node.lua", default_policy.policy)
-- Link client nodes with endpoints to make media flow in the graph
load_script("policy-endpoint-client.lua", default_policy.policy)
load_script("policy-endpoint-client-links.lua", default_policy.policy)
-- Link endpoints with device nodes to make media flow in the graph
load_script("policy-endpoint-device.lua", default_policy.policy)
-- Switch bluetooth profile based on media.role
load_script("policy-bluetooth.lua", bluetooth_policy.policy)
end

View File

@ -0,0 +1,95 @@
-- uncomment to enable role-based endpoints
-- this is not yet ready for desktop use
--
--[[
default_policy.policy.roles = {
["Capture"] = {
["alias"] = { "Multimedia", "Music", "Voice", "Capture" },
["priority"] = 25,
["action.default"] = "cork",
["action.capture"] = "mix",
["media.class"] = "Audio/Source",
},
["Multimedia"] = {
["alias"] = { "Movie", "Music", "Game" },
["priority"] = 25,
["action.default"] = "cork",
},
["Speech-Low"] = {
["priority"] = 30,
["action.default"] = "cork",
["action.Speech-Low"] = "mix",
},
["Custom-Low"] = {
["priority"] = 35,
["action.default"] = "cork",
["action.Custom-Low"] = "mix",
},
["Navigation"] = {
["priority"] = 50,
["action.default"] = "duck",
["action.Navigation"] = "mix",
},
["Speech-High"] = {
["priority"] = 60,
["action.default"] = "cork",
["action.Speech-High"] = "mix",
},
["Custom-High"] = {
["priority"] = 65,
["action.default"] = "cork",
["action.Custom-High"] = "mix",
},
["Communication"] = {
["priority"] = 75,
["action.default"] = "cork",
["action.Communication"] = "mix",
},
["Emergency"] = {
["alias"] = { "Alert" },
["priority"] = 99,
["action.default"] = "cork",
["action.Emergency"] = "mix",
},
}
default_policy.endpoints = {
["endpoint.capture"] = {
["media.class"] = "Audio/Source",
["role"] = "Capture",
},
["endpoint.multimedia"] = {
["media.class"] = "Audio/Sink",
["role"] = "Multimedia",
},
["endpoint.speech_low"] = {
["media.class"] = "Audio/Sink",
["role"] = "Speech-Low",
},
["endpoint.custom_low"] = {
["media.class"] = "Audio/Sink",
["role"] = "Custom-Low",
},
["endpoint.navigation"] = {
["media.class"] = "Audio/Sink",
["role"] = "Navigation",
},
["endpoint.speech_high"] = {
["media.class"] = "Audio/Sink",
["role"] = "Speech-High",
},
["endpoint.custom_high"] = {
["media.class"] = "Audio/Sink",
["role"] = "Custom-High",
},
["endpoint.communication"] = {
["media.class"] = "Audio/Sink",
["role"] = "Communication",
},
["endpoint.emergency"] = {
["media.class"] = "Audio/Sink",
["role"] = "Emergency",
},
}
]]--

View File

@ -0,0 +1 @@
default_policy.enable()

View File

@ -0,0 +1,53 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
function rulesGetDefaultPermissions(properties)
for _, r in ipairs(config.rules or {}) do
if r.default_permissions then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
return r.default_permissions
end
end
end
end
end
clients_om = ObjectManager {
Interest { type = "client" }
}
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
local properties = client["properties"]
local perms = rulesGetDefaultPermissions(properties)
if perms then
Log.info(client, "Granting permissions to client " .. id .. ": " .. perms)
client:update_permissions { ["any"] = perms }
end
end)
clients_om:activate()

View File

@ -0,0 +1,134 @@
MEDIA_ROLE_NONE = 0
MEDIA_ROLE_CAMERA = 1 << 0
function hasPermission (permissions, app_id, lookup)
if permissions then
for key, values in pairs(permissions) do
if key == app_id then
for _, v in pairs(values) do
if v == lookup then
return true
end
end
end
end
end
return false
end
function parseMediaRoles (media_roles_str)
local media_roles = MEDIA_ROLE_NONE
for role in media_roles_str:gmatch('[^,%s]+') do
if role == "Camera" then
media_roles = media_roles | MEDIA_ROLE_CAMERA
end
end
return media_roles
end
function setPermissions (client, allow_client, allow_nodes)
local client_id = client["bound-id"]
Log.info(client, "Granting ALL access to client " .. client_id)
-- Update permissions on client
client:update_permissions { [client_id] = allow_client and "all" or "-" }
-- Update permissions on camera source nodes
for node in nodes_om:iterate() do
local node_id = node["bound-id"]
client:update_permissions { [node_id] = allow_nodes and "all" or "-" }
end
end
function updateClientPermissions (client, permissions)
local client_id = client["bound-id"]
local str_prop = nil
local app_id = nil
local media_roles = nil
local allowed = false
-- Make sure the client is not the portal itself
str_prop = client.properties["pipewire.access.portal.is_portal"]
if str_prop == "yes" then
Log.info (client, "client is the portal itself")
return
end
-- Make sure the client has a portal app Id
str_prop = client.properties["pipewire.access.portal.app_id"]
if str_prop == nil then
Log.info (client, "Portal managed client did not set app_id")
return
end
if str_prop == "" then
Log.info (client, "Ignoring portal check for non-sandboxed client")
setPermissions (client, true, true)
return
end
app_id = str_prop
-- Make sure the client has portal media roles
str_prop = client.properties["pipewire.access.portal.media_roles"]
if str_prop == nil then
Log.info (client, "Portal managed client did not set media_roles")
return
end
media_roles = parseMediaRoles (str_prop)
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
Log.info (client, "Ignoring portal check for clients without camera role")
return
end
-- Update permissions
allowed = hasPermission (permissions, app_id, "yes")
Log.info (client, "setting permissions: " .. tostring(allowed))
setPermissions (client, allowed, allowed)
end
-- Create portal clients object manager
clients_om = ObjectManager {
Interest {
type = "client",
Constraint { "pipewire.access", "=", "portal" },
}
}
-- Set permissions to portal clients from the permission store if loaded
pps_plugin = Plugin.find("portal-permissionstore")
if pps_plugin then
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.role", "=", "Camera" },
Constraint { "media.class", "=", "Video/Source" },
}
}
nodes_om:activate()
clients_om:connect("object-added", function (om, client)
local new_perms = pps_plugin:call("lookup", "devices", "camera");
updateClientPermissions (client, new_perms)
end)
pps_plugin:connect("changed", function (p, table, id, deleted, permissions)
if table == "devices" or id == "camera" then
for app_id, _ in pairs(permissions) do
for client in clients_om:iterate {
Constraint { "pipewire.access.portal.app_id", "=", app_id }
} do
updateClientPermissions (client, permissions)
end
end
end
end)
else
-- Otherwise, just set all permissions to all portal clients
clients_om:connect("object-added", function (om, client)
local id = client["bound-id"]
Log.info(client, "Granting ALL access to client " .. id)
client:update_permissions { ["any"] = "all" }
end)
end
clients_om:activate()

View File

@ -0,0 +1,129 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
items = {}
function configProperties(node)
local np = node.properties
local properties = {
["item.node"] = node,
["item.plugged.usec"] = GLib.get_monotonic_time(),
["item.features.no-dsp"] = config["audio.no-dsp"],
["item.features.monitor"] = true,
["item.features.control-port"] = false,
["node.id"] = node["bound-id"],
["client.id"] = np["client.id"],
["object.path"] = np["object.path"],
["object.serial"] = np["object.serial"],
["target.object"] = np["target.object"],
["priority.session"] = np["priority.session"],
["device.id"] = np["device.id"],
["card.profile.device"] = np["card.profile.device"],
}
for k, v in pairs(np) do
if k:find("^node") or k:find("^stream") or k:find("^media") then
properties[k] = v
end
end
local media_class = properties["media.class"] or ""
if not properties["media.type"] then
for _, i in ipairs({ "Audio", "Video", "Midi" }) do
if media_class:find(i) then
properties["media.type"] = i
break
end
end
end
properties["item.node.type"] =
media_class:find("^Stream/") and "stream" or "device"
if media_class:find("Sink") or
media_class:find("Input") or
media_class:find("Duplex") then
properties["item.node.direction"] = "input"
elseif media_class:find("Source") or media_class:find("Output") then
properties["item.node.direction"] = "output"
end
return properties
end
function addItem (node, item_type)
local id = node["bound-id"]
local item
-- create item
item = SessionItem ( item_type )
items[id] = item
-- configure item
if not item:configure(configProperties(node)) then
Log.warning(item, "failed to configure item for node " .. tostring(id))
return
end
item:register ()
-- activate item
items[id]:activate (Features.ALL, function (item, e)
if e then
Log.message(item, "failed to activate item: " .. tostring(e));
if item then
item:remove ()
end
else
Log.info(item, "activated item for node " .. tostring(id))
-- Trigger object managers to update status
item:remove ()
if item["active-features"] ~= 0 then
item:register ()
end
end
end)
end
nodes_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
},
Interest {
type = "node",
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
},
Interest {
type = "node",
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
Constraint { "wireplumber.is-endpoint", "-", type = "pw" },
},
}
nodes_om:connect("object-added", function (om, node)
local media_class = node.properties['media.class']
if string.find (media_class, "Audio") then
addItem (node, "si-audio-adapter")
else
addItem (node, "si-node")
end
end)
nodes_om:connect("object-removed", function (om, node)
local id = node["bound-id"]
if items[id] then
items[id]:remove ()
items[id] = nil
end
end)
nodes_om:activate()

View File

@ -0,0 +1,93 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Frédéric Danis <frederic.danis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local sink_ids = {}
local fallback_node = nil
node_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
-- Do not consider endpoints created by WirePlumber
Constraint { "wireplumber.is-endpoint", "!", true, type = "pw" },
-- or the fallback sink itself
Constraint { "wireplumber.is-fallback", "!", true, type = "pw" },
}
}
function createFallbackSink()
if fallback_node then
return
end
Log.info("Create fallback sink")
local properties = {}
properties["node.name"] = "auto_null"
properties["node.description"] = "Dummy Output"
properties["audio.rate"] = 48000
properties["audio.channels"] = 2
properties["audio.position"] = "FL,FR"
properties["media.class"] = "Audio/Sink"
properties["factory.name"] = "support.null-audio-sink"
properties["node.virtual"] = "true"
properties["monitor.channel-volumes"] = "true"
properties["wireplumber.is-fallback"] = "true"
properties["priority.session"] = 500
fallback_node = LocalNode("adapter", properties)
fallback_node:activate(Feature.Proxy.BOUND)
end
function checkSinks()
local sink_ids_items = 0
for _ in pairs(sink_ids) do sink_ids_items = sink_ids_items + 1 end
if sink_ids_items > 0 then
if fallback_node then
Log.info("Remove fallback sink")
fallback_node = nil
end
elseif not fallback_node then
createFallbackSink()
end
end
function checkSinksAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
checkSinks()
timeout_source = nil
end)
end
node_om:connect("object-added", function (_, node)
Log.debug("object added: " .. node.properties["object.id"] .. " " ..
tostring(node.properties["node.name"]))
sink_ids[node.properties["object.id"]] = node.properties["node.name"]
checkSinksAfterTimeout()
end)
node_om:connect("object-removed", function (_, node)
Log.debug("object removed: " .. node.properties["object.id"] .. " " ..
tostring(node.properties["node.name"]))
sink_ids[node.properties["object.id"]] = nil
checkSinksAfterTimeout()
end)
node_om:activate()
checkSinksAfterTimeout()

View File

@ -0,0 +1,74 @@
-- WirePlumber
--
-- Copyright © 2021 Asymptotic
-- @author Arun Raghavan <arun@asymptotic.io>
--
-- SPDX-License-Identifier: MIT
--
-- Route streams of a given role (media.role property) to devices that are
-- intended for that role (device.intended-roles property)
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
devices_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.intended-roles", "is-present", type = "pw" },
}
}
streams_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/*/Audio", type = "pw-global" },
Constraint { "media.role", "is-present", type = "pw-global" }
}
}
local function routeUsingIntendedRole(stream, dev)
local stream_role = stream.properties["media.role"]
local is_input = stream.properties["media.class"]:find("Input") ~= nil
local is_source = dev.properties["media.class"]:find("Source") ~= nil
local dev_roles = dev.properties["device.intended-roles"]
-- Make sure the stream and device direction match
if is_input ~= is_source then
return
end
for role in dev_roles:gmatch("(%a+)") do
if role == stream_role then
Log.info(stream,
string.format("Routing stream '%s' (%d) with role '%s' to '%s' (%d)",
stream.properties["node.name"], stream["bound-id"], stream_role,
dev.properties["node.name"], dev["bound-id"])
)
local metadata = metadata_om:lookup()
metadata:set(stream["bound-id"], "target.node", "Spa:Id", dev["bound-id"])
end
end
end
streams_om:connect("object-added", function (streams_om, stream)
for dev in devices_om:iterate() do
routeUsingIntendedRole(stream, dev)
end
end)
devices_om:connect("object-added", function (devices_om, dev)
for stream in streams_om:iterate() do
routeUsingIntendedRole(stream, dev)
end
end)
metadata_om:activate()
devices_om:activate()
streams_om:activate()

View File

@ -0,0 +1,65 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.properties is not nil
config.properties = config.properties or {}
SND_PATH = "/dev/snd"
SEQ_NAME = "seq"
SND_SEQ_PATH = SND_PATH .. "/" .. SEQ_NAME
midi_node = nil
fm_plugin = nil
function CreateMidiNode ()
-- Midi properties
local props = {}
props["factory.name"] = "api.alsa.seq.bridge"
props["node.name"] = "Midi-Bridge"
-- create the midi node
local node = Node("spa-node-factory", props)
node:activate(Feature.Proxy.BOUND, function (n)
Log.info ("activated Midi bridge")
end)
return node;
end
if GLib.access (SND_SEQ_PATH, "rw") then
midi_node = CreateMidiNode ()
elseif config.properties["alsa.midi.monitoring"] then
fm_plugin = Plugin.find("file-monitor-api")
end
-- Only monitor the MIDI device if file does not exist and plugin API is loaded
if midi_node == nil and fm_plugin ~= nil then
-- listen for changed events
fm_plugin:connect ("changed", function (o, file, old, evtype)
-- files attributes changed
if evtype == "attribute-changed" then
if file ~= SND_SEQ_PATH then
return
end
if midi_node == nil and GLib.access (SND_SEQ_PATH, "rw") then
midi_node = CreateMidiNode ()
fm_plugin:call ("remove-watch", SND_PATH)
end
end
-- directory is going to be unmounted
if evtype == "pre-unmount" then
fm_plugin:call ("remove-watch", SND_PATH)
end
end)
-- add watch
fm_plugin:call ("add-watch", SND_PATH, "m")
end

View File

@ -0,0 +1,412 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.properties is not nil
config.properties = config.properties or {}
-- unique device/node name tables
device_names_table = nil
node_names_table = nil
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function nonempty(str)
return str ~= "" and str or nil
end
function createNode(parent, id, obj_type, factory, properties)
local dev_props = parent.properties
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- try to negotiate the max ammount of channels
if dev_props["api.alsa.use-acp"] ~= "true" then
properties["audio.channels"] = properties["audio.channels"] or "64"
end
local dev = properties["api.alsa.pcm.device"]
or properties["alsa.device"] or "0"
local subdev = properties["api.alsa.pcm.subdevice"]
or properties["alsa.subdevice"] or "0"
local stream = properties["api.alsa.pcm.stream"] or "unknown"
local profile = properties["device.profile.name"]
or (stream .. "." .. dev .. "." .. subdev)
local profile_desc = properties["device.profile.description"]
-- set priority
if not properties["priority.driver"] then
local priority = (dev == "0") and 1000 or 744
if stream == "capture" then
priority = priority + 1000
end
priority = priority - (tonumber(dev) * 16) - tonumber(subdev)
if profile:find("^analog%-") then
priority = priority + 9
elseif profile:find("^iec958%-") then
priority = priority + 8
end
properties["priority.driver"] = priority
properties["priority.session"] = priority
end
-- ensure the node has a media class
if not properties["media.class"] then
if stream == "capture" then
properties["media.class"] = "Audio/Source"
else
properties["media.class"] = "Audio/Sink"
end
end
-- ensure the node has a name
if not properties["node.name"] then
local name =
(stream == "capture" and "alsa_input" or "alsa_output")
.. "." ..
(dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
dev_props["device.name"] or
"unnamed-device")
.. "." ..
profile
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if node_names_table[properties["node.name"]] ~= true then
node_names_table[properties["node.name"]] = true
break
end
properties["node.name"] = name .. "." .. counter
end
end
-- and a nick
local nick = nonempty(properties["node.nick"])
or nonempty(properties["api.alsa.pcm.name"])
or nonempty(properties["alsa.name"])
or nonempty(profile_desc)
or dev_props["device.nick"]
if nick == "USB Audio" then
nick = dev_props["device.nick"]
end
-- also sanitize nick, replace ':' with ' '
properties["node.nick"] = nick:gsub("(:)", " ")
-- ensure the node has a description
if not properties["node.description"] then
local desc = nonempty(dev_props["device.description"]) or "unknown"
local name = nonempty(properties["api.alsa.pcm.name"]) or
nonempty(properties["api.alsa.pcm.id"]) or dev
if profile_desc then
desc = desc .. " " .. profile_desc
elseif subdev ~= "0" then
desc = desc .. " (" .. name .. " " .. subdev .. ")"
elseif dev ~= "0" then
desc = desc .. " (" .. name .. ")"
end
-- also sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
end
-- add api.alsa.card.* properties for rule matching purposes
for k, v in pairs(dev_props) do
if k:find("^api%.alsa%.card%..*") then
properties[k] = v
end
end
-- apply VM overrides
local vm_overrides = config.properties["vm.node.defaults"]
if nonempty(Core.get_vm_type()) and type(vm_overrides) == "table" then
for k, v in pairs(vm_overrides) do
properties[k] = v
end
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["node.disabled"] then
return
end
-- create the node
local node = Node("adapter", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, factory, properties)
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:connect("object-removed", function (parent, id)
local node = parent:get_managed_object(id)
node_names_table[node.properties["node.name"]] = nil
end)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
function prepareDevice(parent, id, obj_type, factory, properties)
-- ensure the device has an appropriate name
local name = "alsa_card." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if device_names_table[properties["device.name"]] ~= true then
device_names_table[properties["device.name"]] = true
break
end
properties["device.name"] = name .. "." .. counter
end
-- ensure the device has a description
if not properties["device.description"] then
local d = nil
local f = properties["device.form-factor"]
local c = properties["device.class"]
if f == "internal" then
d = I18n.gettext("Built-in Audio")
elseif c == "modem" then
d = I18n.gettext("Modem")
end
d = d or properties["device.product.name"]
or properties["api.alsa.card.name"]
or properties["alsa.card_name"]
or "Unknown device"
properties["device.description"] = d
end
-- ensure the device has a nick
properties["device.nick"] =
properties["device.nick"] or
properties["api.alsa.card.name"] or
properties["alsa.card_name"]
-- set the icon name
if not properties["device.icon-name"] then
local icon = nil
local icon_map = {
-- form factor -> icon
["microphone"] = "audio-input-microphone",
["webcam"] = "camera-web",
["handset"] = "phone",
["portable"] = "multimedia-player",
["tv"] = "video-display",
["headset"] = "audio-headset",
["headphone"] = "audio-headphones",
["speaker"] = "audio-speakers",
["hands-free"] = "audio-handsfree",
}
local f = properties["device.form-factor"]
local c = properties["device.class"]
local b = properties["device.bus"]
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
end
-- apply properties from config.rules
rulesApplyProperties(properties)
if properties["device.disabled"] then
return
end
-- override the device factory to use ACP
if properties["api.alsa.use-acp"] then
Log.info("Enabling the use of ACP on " .. properties["device.name"])
factory = "api.alsa.acp.device"
end
-- use device reservation, if available
if rd_plugin and properties["api.alsa.card"] then
local rd_name = "Audio" .. properties["api.alsa.card"]
local rd = rd_plugin:call("create-reservation",
rd_name,
config.properties["alsa.reserve.application-name"] or "WirePlumber",
properties["device.name"],
config.properties["alsa.reserve.priority"] or -20);
properties["api.dbus.ReserveDevice1"] = rd_name
-- unlike pipewire-media-session, this logic here keeps the device
-- acquired at all times and destroys it if someone else acquires
rd:connect("notify::state", function (rd, pspec)
local state = rd["state"]
if state == "acquired" then
-- create the device
createDevice(parent, id, factory, properties)
elseif state == "available" then
-- attempt to acquire again
rd:call("acquire")
elseif state == "busy" then
-- destroy the device
parent:store_managed_object(id, nil)
end
end)
rd:connect("release-requested", function (rd)
Log.info("release requested")
parent:store_managed_object(id, nil)
rd:call("release")
end)
if jack_device then
rd:connect("notify::owner-name-changed", function (rd, pspec)
if rd["state"] == "busy" and
rd["owner-application-name"] == "Jack audio server" then
-- TODO enable the jack device
else
-- TODO disable the jack device
end
end)
end
rd:call("acquire")
else
-- create the device
createDevice(parent, id, factory, properties)
end
end
function createMonitor ()
local m = SpaDevice("api.alsa.enum.udev", config.properties)
if m == nil then
Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\")"
.. "missing or broken. Sound Cards cannot be enumerated")
return nil
end
-- handle create-object to prepare device
m:connect("create-object", prepareDevice)
-- handle object-removed to destroy device reservations and recycle device name
m:connect("object-removed", function (parent, id)
local device = parent:get_managed_object(id)
if rd_plugin then
local rd_name = device.properties["api.dbus.ReserveDevice1"]
if rd_name then
rd_plugin:call("destroy-reservation", rd_name)
end
end
device_names_table[device.properties["device.name"]] = nil
for managed_node in device:iterate_managed_objects() do
node_names_table[managed_node.properties["node.name"]] = nil
end
end)
-- reset the name tables to make sure names are recycled
device_names_table = {}
node_names_table = {}
-- activate monitor
Log.info("Activating ALSA monitor")
m:activate(Feature.SpaDevice.ENABLED)
return m
end
-- create the JACK device (for PipeWire to act as client to a JACK server)
if config.properties["alsa.jack-device"] then
jack_device = Device("spa-device-factory", {
["factory.name"] = "api.jack.device",
["node.name"] = "JACK-Device",
})
jack_device:activate(Feature.Proxy.BOUND)
end
-- enable device reservation if requested
if config.properties["alsa.reserve"] then
rd_plugin = Plugin.find("reserve-device")
end
-- if the reserve-device plugin is enabled, at the point of script execution
-- it is expected to be connected. if it is not, assume the d-bus connection
-- has failed and continue without it
if rd_plugin and rd_plugin["state"] ~= "connected" then
Log.message("reserve-device plugin is not connected to D-Bus, "
.. "disabling device reservation")
rd_plugin = nil
end
-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
-- case D-Bus service is restarted
if rd_plugin then
local dbus = rd_plugin:call("get-dbus")
dbus:connect("notify::state", function (b, pspec)
local state = b["state"]
Log.info ("rd-plugin state changed to " .. state)
if state == "connected" then
Log.info ("Creating ALSA monitor")
monitor = createMonitor()
elseif state == "closed" then
Log.info ("Destroying ALSA monitor")
monitor = nil
end
end)
end
-- create the monitor
monitor = createMonitor()

View File

@ -0,0 +1,193 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node description
local desc =
dev_props["device.description"]
or dev_props["device.name"]
or dev_props["device.nick"]
or dev_props["device.alias"]
or "bluetooth-device"
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node name
local name =
((factory:find("sink") and "bluez_output") or
(factory:find("source") and "bluez_input" or factory)) .. "." ..
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
(properties["api.bluez5.profile"] or "unknown")
-- sanitize name
properties["node.name"] = name:gsub("([^%w_%-%.])", "_")
-- set priority
if not properties["priority.driver"] then
local priority = factory:find("source") and 2010 or 1010
properties["priority.driver"] = priority
properties["priority.session"] = priority
end
-- autoconnect if it's a stream
if properties["api.bluez5.profile"] == "headset-audio-gateway" or
factory:find("a2dp.source") then
properties["node.autoconnect"] = true
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node; bluez requires "local" nodes, i.e. ones that run in
-- the same process as the spa device, for several reasons
local node = LocalNode("adapter", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
local device = parent:get_managed_object(id)
if not device then
-- ensure a proper device name
local name =
(properties["device.name"] or
properties["api.bluez5.address"] or
properties["device.description"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
if not name:find("^bluez_card%.", 1) then
name = "bluez_card." .. name
end
properties["device.name"] = name
-- set the icon name
if not properties["device.icon-name"] then
local icon = nil
local icon_map = {
-- form factor -> icon
["microphone"] = "audio-input-microphone",
["webcam"] = "camera-web",
["handset"] = "phone",
["portable"] = "multimedia-player",
["tv"] = "video-display",
["headset"] = "audio-headset",
["headphone"] = "audio-headphones",
["speaker"] = "audio-speakers",
["hands-free"] = "audio-handsfree",
}
local f = properties["device.form-factor"]
local b = properties["device.bus"]
icon = icon_map[f] or "audio-card"
properties["device.icon-name"] = icon .. (b and ("-" .. b) or "")
end
-- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
properties["bluez5.profile"] = "off"
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the device
device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
return
end
end
Log.info(parent, string.format("%d, %s (%s): %s",
id, properties["device.description"],
properties["api.bluez5.address"], properties["api.bluez5.connection"]))
-- activate the device after the bluez profiles are connected
if properties["api.bluez5.connection"] == "connected" then
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
else
device:deactivate(Features.ALL)
end
end
function createMonitor()
local monitor_props = config.properties or {}
monitor_props["api.bluez5.connection-info"] = true
local monitor = SpaDevice("api.bluez5.enum.dbus", monitor_props)
if monitor then
monitor:connect("create-object", createDevice)
else
Log.message("PipeWire's BlueZ SPA missing or broken. Bluetooth not supported.")
return nil
end
monitor:activate(Feature.SpaDevice.ENABLED)
return monitor
end
logind_plugin = Plugin.find("logind")
if logind_plugin then
-- if logind support is enabled, activate
-- the monitor only when the seat is active
function startStopMonitor(seat_state)
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
if seat_state == "active" then
monitor = createMonitor()
elseif monitor then
monitor:deactivate(Feature.SpaDevice.ENABLED)
monitor = nil
end
end
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
startStopMonitor(logind_plugin:call("get-state"))
else
monitor = createMonitor()
end

View File

@ -0,0 +1,169 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function findDuplicate(parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object(i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
local location = properties["api.libcamera.location"]
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node name
local name =
(factory:find("sink") and "libcamera_output") or
(factory:find("source") and "libcamera_input" or factory)
.. "." ..
(dev_props["device.name"]:gsub("^libcamera_device%.(.+)", "%1") or
dev_props["device.name"] or
dev_props["device.nick"] or
dev_props["device.alias"] or
"libcamera-device")
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
properties["node.name"] = name .. "." .. counter
else
break
end
end
-- set the node description
local desc = dev_props["device.description"] or "libcamera-device"
if location == "front" then
desc = I18n.gettext("Built-in Front Camera")
elseif location == "back" then
desc = I18n.gettext("Built-in Back Camera")
end
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node nick
local nick = properties["node.nick"] or
dev_props["device.product.name"] or
dev_props["device.description"] or
dev_props["device.nick"]
properties["node.nick"] = nick:gsub("(:)", " ")
-- set priority
if not properties["priority.session"] then
local priority = 700
if location == "external" then
priority = priority + 150
elseif location == "front" then
priority = priority + 100
elseif location == "back" then
priority = priority + 50
end
properties["priority.session"] = priority
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node
local node = Node("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
-- ensure the device has an appropriate name
local name = "libcamera_device." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
properties["device.name"] = name .. "." .. counter
else
break
end
end
-- ensure the device has a description
properties["device.description"] =
properties["device.description"]
or properties["device.product.name"]
or "Unknown device"
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the device
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
monitor = SpaDevice("api.libcamera.enum.manager", config.properties or {})
if monitor then
monitor:connect("create-object", createDevice)
monitor:activate(Feature.SpaDevice.ENABLED)
else
Log.message("PipeWire's libcamera SPA missing or broken. libcamera not supported.")
end

View File

@ -0,0 +1,159 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
function findDuplicate(parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object(i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function createNode(parent, id, type, factory, properties)
local dev_props = parent.properties
-- set the device id and spa factory name; REQUIRED, do not change
properties["device.id"] = parent["bound-id"]
properties["factory.name"] = factory
-- set the default pause-on-idle setting
properties["node.pause-on-idle"] = false
-- set the node name
local name =
(factory:find("sink") and "v4l2_output") or
(factory:find("source") and "v4l2_input" or factory)
.. "." ..
(dev_props["device.name"]:gsub("^v4l2_device%.(.+)", "%1") or
dev_props["device.name"] or
dev_props["device.nick"] or
dev_props["device.alias"] or
"v4l2-device")
-- sanitize name
name = name:gsub("([^%w_%-%.])", "_")
properties["node.name"] = name
-- deduplicate nodes with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
properties["node.name"] = name .. "." .. counter
else
break
end
end
-- set the node description
local desc = dev_props["device.description"] or "v4l2-device"
desc = desc .. " (V4L2)"
-- sanitize description, replace ':' with ' '
properties["node.description"] = desc:gsub("(:)", " ")
-- set the node nick
local nick = properties["node.nick"] or
dev_props["device.product.name"] or
dev_props["api.v4l2.cap.card"] or
dev_props["device.description"] or
dev_props["device.nick"]
properties["node.nick"] = nick:gsub("(:)", " ")
-- set priority
if not properties["priority.session"] then
local path = properties["api.v4l2.path"] or "/dev/video100"
local dev = path:gsub("/dev/video(%d+)", "%1")
properties["priority.session"] = 1000 - (tonumber(dev) * 10)
end
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the node
local node = Node("spa-node-factory", properties)
node:activate(Feature.Proxy.BOUND)
parent:store_managed_object(id, node)
end
function createDevice(parent, id, type, factory, properties)
-- ensure the device has an appropriate name
local name = "v4l2_device." ..
(properties["device.name"] or
properties["device.bus-id"] or
properties["device.bus-path"] or
tostring(id)):gsub("([^%w_%-%.])", "_")
properties["device.name"] = name
-- deduplicate devices with the same name
for counter = 2, 99, 1 do
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
properties["device.name"] = name .. "." .. counter
else
break
end
end
-- ensure the device has a description
properties["device.description"] =
properties["device.description"]
or properties["device.product.name"]
or "Unknown device"
-- apply properties from config.rules
rulesApplyProperties(properties)
-- create the device
local device = SpaDevice(factory, properties)
if device then
device:connect("create-object", createNode)
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
parent:store_managed_object(id, device)
else
Log.warning ("Failed to create '" .. factory .. "' device")
end
end
monitor = SpaDevice("api.v4l2.enum.udev", config.properties or {})
if monitor then
monitor:connect("create-object", createDevice)
monitor:activate(Feature.SpaDevice.ENABLED)
else
Log.message("PipeWire's V4L SPA missing or broken. Video4Linux not supported.")
end

View File

@ -0,0 +1,398 @@
-- WirePlumber
--
-- Copyright © 2021 Asymptotic Inc.
-- @author Sanchayan Maity <sanchayan@asymptotic.io>
--
-- Based on bt-profile-switch.lua in tests/examples
-- Copyright © 2021 George Kiagiadakis
--
-- Based on bluez-autoswitch in media-session
-- Copyright © 2021 Pauli Virtanen
--
-- SPDX-License-Identifier: MIT
--
-- Checks for the existence of media.role and if present switches the bluetooth
-- profile accordingly. Also see bluez-autoswitch in media-session.
-- The intended logic of the script is as follows.
--
-- When a stream comes in, if it has a Communication or phone role in PulseAudio
-- speak in props, we switch to the highest priority profile that has an Input
-- route available. The reason for this is that we may have microphone enabled
-- non-HFP codecs eg. Faststream.
-- We track the incoming streams with Communication role or the applications
-- specified which do not set the media.role correctly perhaps.
-- When a stream goes away if the list with which we track the streams above
-- is empty, then we revert back to the old profile.
local config = ...
local use_persistent_storage = config["use-persistent-storage"] or false
local applications = {}
local use_headset_profile = config["media-role.use-headset-profile"] or false
local profile_restore_timeout_msec = 2000
local INVALID = -1
local timeout_source = nil
local restore_timeout_source = nil
local state = use_persistent_storage and State("policy-bluetooth") or nil
local headset_profiles = state and state:load() or {}
local last_profiles = {}
local active_streams = {}
local previous_streams = {}
for _, value in ipairs(config["media-role.applications"] or {}) do
applications[value] = true
end
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
devices_om = ObjectManager {
Interest {
type = "device",
Constraint { "device.api", "=", "bluez5" },
}
}
streams_om = ObjectManager {
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
-- Do not consider monitor streams
Constraint { "stream.monitor", "!", "true" }
}
}
local function parseParam(param_to_parse, id)
local param = param_to_parse:parse()
if param.pod_type == "Object" and param.object_id == id then
return param.properties
else
return nil
end
end
local function storeAfterTimeout()
if not use_persistent_storage then
return
end
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(headset_profiles)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
local function saveHeadsetProfile(device, profile_name)
local key = "saved-headset-profile:" .. device.properties["device.name"]
headset_profiles[key] = profile_name
storeAfterTimeout()
end
local function getSavedHeadsetProfile(device)
local key = "saved-headset-profile:" .. device.properties["device.name"]
return headset_profiles[key]
end
local function saveLastProfile(device, profile_name)
last_profiles[device.properties["device.name"]] = profile_name
end
local function getSavedLastProfile(device)
return last_profiles[device.properties["device.name"]]
end
local function isSwitched(device)
return getSavedLastProfile(device) ~= nil
end
local function isBluez5AudioSink(sink_name)
if sink_name and string.find(sink_name, "bluez_output.") ~= nil then
return true
end
return false
end
local function isBluez5DefaultAudioSink()
local metadata = metadata_om:lookup()
local default_audio_sink = metadata:find(0, "default.audio.sink")
return isBluez5AudioSink(default_audio_sink)
end
local function findProfile(device, index, name)
for p in device:iterate_params("EnumProfile") do
local profile = parseParam(p, "EnumProfile")
if not profile then
goto skip_enum_profile
end
Log.debug("Profile name: " .. profile.name .. ", priority: "
.. tostring(profile.priority) .. ", index: " .. tostring(profile.index))
if (index ~= nil and profile.index == index) or
(name ~= nil and profile.name == name) then
return profile.priority, profile.index, profile.name
end
::skip_enum_profile::
end
return INVALID, INVALID, nil
end
local function getCurrentProfile(device)
for p in device:iterate_params("Profile") do
local profile = parseParam(p, "Profile")
if profile then
return profile.name
end
end
return nil
end
local function highestPrioProfileWithInputRoute(device)
local profile_priority = INVALID
local profile_index = INVALID
local profile_name = nil
for p in device:iterate_params("EnumRoute") do
local route = parseParam(p, "EnumRoute")
-- Parse pod
if not route then
goto skip_enum_route
end
if route.direction ~= "Input" then
goto skip_enum_route
end
Log.debug("Route with index: " .. tostring(route.index) .. ", direction: "
.. route.direction .. ", name: " .. route.name .. ", description: "
.. route.description .. ", priority: " .. route.priority)
if route.profiles then
for _, v in pairs(route.profiles) do
local priority, index, name = findProfile(device, v)
if priority ~= INVALID then
if profile_priority < priority then
profile_priority = priority
profile_index = index
profile_name = name
end
end
end
end
::skip_enum_route::
end
return profile_priority, profile_index, profile_name
end
local function hasProfileInputRoute(device, profile_index)
for p in device:iterate_params("EnumRoute") do
local route = parseParam(p, "EnumRoute")
if route and route.direction == "Input" and route.profiles then
for _, v in pairs(route.profiles) do
if v == profile_index then
return true
end
end
end
end
return false
end
local function switchProfile()
local index
local name
if restore_timeout_source then
restore_timeout_source:destroy()
restore_timeout_source = nil
end
for device in devices_om:iterate() do
if isSwitched(device) then
goto skip_device
end
local cur_profile_name = getCurrentProfile(device)
saveLastProfile(device, cur_profile_name)
_, index, name = findProfile(device, nil, cur_profile_name)
if hasProfileInputRoute(device, index) then
Log.info("Current profile has input route, not switching")
goto skip_device
end
local saved_headset_profile = getSavedHeadsetProfile(device)
index = INVALID
if saved_headset_profile then
_, index, name = findProfile(device, nil, saved_headset_profile)
end
if index == INVALID then
_, index, name = highestPrioProfileWithInputRoute(device)
end
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
Log.info("Setting profile of '"
.. device.properties["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params("Profile", pod)
else
Log.warning("Got invalid index when switching profile")
end
::skip_device::
end
end
local function restoreProfile()
for device in devices_om:iterate() do
if isSwitched(device) then
local profile_name = getSavedLastProfile(device)
local cur_profile_name = getCurrentProfile(device)
saveLastProfile(device, nil)
if cur_profile_name then
Log.info("Setting saved headset profile to: " .. cur_profile_name)
saveHeadsetProfile(device, cur_profile_name)
end
if profile_name then
local _, index, name = findProfile(device, nil, profile_name)
if index ~= INVALID then
local pod = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = index
}
Log.info("Restoring profile of '"
.. device.properties["device.description"]
.. "' from: " .. cur_profile_name
.. " to: " .. name)
device:set_params("Profile", pod)
else
Log.warning("Failed to restore profile")
end
end
end
end
end
local function triggerRestoreProfile()
if restore_timeout_source then
return
end
if next(active_streams) ~= nil then
return
end
restore_timeout_source = Core.timeout_add(profile_restore_timeout_msec, function ()
restore_timeout_source = nil
restoreProfile()
end)
end
-- We consider a Stream of interest to have role Communication if it has
-- media.role set to Communication in props or it is in our list of
-- applications as these applications do not set media.role correctly or at
-- all.
local function checkStreamStatus(stream)
local app_name = stream.properties["application.name"]
local stream_role = stream.properties["media.role"]
if not (stream_role == "Communication" or applications[app_name]) then
return false
end
if not isBluez5DefaultAudioSink() then
return false
end
-- If a stream we previously saw stops running, we consider it
-- inactive, because some applications (Teams) just cork input
-- streams, but don't close them.
if previous_streams[stream["bound-id"]] and stream.state ~= "running" then
return false
end
return true
end
local function handleStream(stream)
if not use_headset_profile then
return
end
if checkStreamStatus(stream) then
active_streams[stream["bound-id"]] = true
previous_streams[stream["bound-id"]] = true
switchProfile()
else
active_streams[stream["bound-id"]] = nil
triggerRestoreProfile()
end
end
local function handleAllStreams()
for stream in streams_om:iterate {
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
Constraint { "stream.monitor", "!", "true" }
} do
handleStream(stream)
end
end
streams_om:connect("object-added", function (_, stream)
stream:connect("state-changed", function (stream, old_state, cur_state)
handleStream(stream)
end)
stream:connect("params-changed", handleStream)
handleStream(stream)
end)
streams_om:connect("object-removed", function (_, stream)
active_streams[stream["bound-id"]] = nil
previous_streams[stream["bound-id"]] = nil
triggerRestoreProfile()
end)
devices_om:connect("object-added", function (_, device)
-- Devices are unswitched initially
if isSwitched(device) then
saveLastProfile(device, nil)
end
handleAllStreams()
end)
metadata_om:connect("object-added", function (_, metadata)
metadata:connect("changed", function (m, subject, key, t, value)
if (use_headset_profile and subject == 0 and key == "default.audio.sink"
and isBluez5AudioSink(value)) then
-- If bluez sink is set as default, rescan for active input streams
handleAllStreams()
end
end)
end)
metadata_om:activate()
devices_om:activate()
streams_om:activate()

View File

@ -0,0 +1,211 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
local self = {}
self.config = ... or {}
self.config.persistent = self.config.persistent or {}
self.active_profiles = {}
self.best_profiles = {}
self.default_profile_plugin = Plugin.find("default-profile")
-- Preprocess persisten profiles and create Interest objects
for _, p in ipairs(self.config.persistent or {}) do
p.interests = {}
for _, i in ipairs(p.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(p.interests, interest)
end
p.matches = nil
end
-- Checks whether a device profile is persistent or not
function isProfilePersistent(device_props, profile_name)
for _, p in ipairs(self.config.persistent or {}) do
if p.profile_names then
for _, interest in ipairs(p.interests) do
if interest:matches(device_props) then
for _, pn in ipairs(p.profile_names) do
if pn == profile_name then
return true
end
end
end
end
end
end
return false
end
function parseParam(param, id)
local parsed = param:parse()
if parsed.pod_type == "Object" and parsed.object_id == id then
return parsed.properties
else
return nil
end
end
function setDeviceProfile (device, dev_id, dev_name, profile)
if self.active_profiles[dev_id] and
self.active_profiles[dev_id].index == profile.index then
Log.info ("Profile " .. profile.name .. " is already set in " .. dev_name)
return
end
local param = Pod.Object {
"Spa:Pod:Object:Param:Profile", "Profile",
index = profile.index,
}
Log.info ("Setting profile " .. profile.name .. " on " .. dev_name)
device:set_param("Profile", param)
end
function findDefaultProfile (device)
local def_name = nil
if self.default_profile_plugin ~= nil then
def_name = self.default_profile_plugin:call ("get-profile", device)
end
if def_name == nil then
return nil
end
for p in device:iterate_params("EnumProfile") do
local profile = parseParam(p, "EnumProfile")
if profile.name == def_name then
return profile
end
end
return nil
end
function findBestProfile (device)
local off_profile = nil
local best_profile = nil
local unk_profile = nil
for p in device:iterate_params("EnumProfile") do
profile = parseParam(p, "EnumProfile")
if profile and profile.name ~= "pro-audio" then
if profile.name == "off" then
off_profile = profile
elseif profile.available == "yes" then
if best_profile == nil or profile.priority > best_profile.priority then
best_profile = profile
end
elseif profile.available ~= "no" then
if unk_profile == nil or profile.priority > unk_profile.priority then
unk_profile = profile
end
end
end
end
if best_profile ~= nil then
return best_profile
elseif unk_profile ~= nil then
return unk_profile
elseif off_profile ~= nil then
return off_profile
end
return nil
end
function handleBestProfile (device, dev_id, dev_name)
-- Find best profile
local profile = findBestProfile (device)
if profile == nil then
Log.info ("Cannot find best profile for device " .. dev_name)
return false
end
-- Update if it has changed
if self.best_profiles[dev_id] == nil or
self.best_profiles[dev_id].index ~= profile.index then
self.best_profiles[dev_id] = profile
Log.info ("Best profile changed to " .. profile.name .. " in " .. dev_name)
return true
end
return false
end
function handleProfiles (device, new_device)
local dev_id = device["bound-id"]
local dev_name = device.properties["device.name"]
local def_profile = findDefaultProfile (device)
-- Do not do anything if active profile is both persistent and default
if not new_device and
self.active_profiles[dev_id] ~= nil and
isProfilePersistent (device.properties, self.active_profiles[dev_id].name) and
def_profile ~= nil and
self.active_profiles[dev_id].name == def_profile.name
then
local active_profile = self.active_profiles[dev_id].name
Log.info ("Device profile " .. active_profile .. " is persistent for " .. dev_name)
return
end
if def_profile ~= nil then
if def_profile.available == "no" then
Log.info ("Default profile " .. def_profile.name .. " unavailable for " .. dev_name)
else
Log.info ("Found default profile " .. def_profile.name .. " for " .. dev_name)
setDeviceProfile (device, dev_id, dev_name, def_profile)
return
end
else
Log.info ("Default profile not found for " .. dev_name)
end
-- Otherwise just set the best profile if changed
local best_changed = handleBestProfile (device, dev_id, dev_name)
local best_profile = self.best_profiles[dev_id]
if best_changed and best_profile ~= nil then
setDeviceProfile (device, dev_id, dev_name, best_profile)
elseif best_profile ~= nil then
Log.info ("Best profile " .. best_profile.name .. " did not change on " .. dev_name)
else
Log.info ("Best profile not found on " .. dev_name)
end
end
function onDeviceParamsChanged (device, param_name)
if param_name == "EnumProfile" then
handleProfiles (device, false)
end
end
self.om = ObjectManager {
Interest {
type = "device",
Constraint { "device.name", "is-present", type = "pw-global" },
}
}
self.om:connect("object-added", function (_, device)
device:connect ("params-changed", onDeviceParamsChanged)
handleProfiles (device, true)
end)
self.om:connect("object-removed", function (_, device)
local dev_id = device["bound-id"]
self.active_profiles[dev_id] = nil
self.best_profiles[dev_id] = nil
end)
self.om:activate()

View File

@ -0,0 +1,477 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
-- whether to store state on the file system
use_persistent_storage = config["use-persistent-storage"] or false
-- the default volume to apply
default_volume = tonumber(config["default-volume"] or 0.4)
-- table of device info
dev_infos = {}
-- the state storage
state = use_persistent_storage and State("default-routes") or nil
state_table = state and state:load() or {}
-- simple serializer {"foo", "bar"} -> "foo;bar;"
function serializeArray(a)
local str = ""
for _, v in ipairs(a) do
str = str .. tostring(v):gsub(";", "\\;") .. ";"
end
return str
end
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
function parseArray(str, convert_value)
local array = {}
local val = ""
local escaped = false
for i = 1, #str do
local c = str:sub(i,i)
if c == '\\' then
escaped = true
elseif c == ';' and not escaped then
val = convert_value and convert_value(val) or val
table.insert(array, val)
val = ""
else
val = val .. tostring(c)
escaped = false
end
end
return array
end
function arrayContains(a, value)
for _, v in ipairs(a) do
if v == value then
return true
end
end
return false
end
function parseParam(param, id)
local route = param:parse()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
else
return nil
end
end
function storeAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(state_table)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
function saveProfile(dev_info, profile_name)
if not use_persistent_storage then
return
end
local routes = {}
for idx, ri in pairs(dev_info.route_infos) do
if ri.save then
table.insert(routes, ri.name)
end
end
if #routes > 0 then
local key = dev_info.name .. ":profile:" .. profile_name
state_table[key] = serializeArray(routes)
storeAfterTimeout()
end
end
function saveRouteProps(dev_info, route)
if not use_persistent_storage or not route.props then
return
end
local props = route.props.properties
local key_base = dev_info.name .. ":" ..
route.direction:lower() .. ":" ..
route.name .. ":"
state_table[key_base .. "volume"] =
props.volume and tostring(props.volume) or nil
state_table[key_base .. "mute"] =
props.mute and tostring(props.mute) or nil
state_table[key_base .. "channelVolumes"] =
props.channelVolumes and serializeArray(props.channelVolumes) or nil
state_table[key_base .. "channelMap"] =
props.channelMap and serializeArray(props.channelMap) or nil
state_table[key_base .. "latencyOffsetNsec"] =
props.latencyOffsetNsec and tostring(props.latencyOffsetNsec) or nil
state_table[key_base .. "iec958Codecs"] =
props.iec958Codecs and serializeArray(props.iec958Codecs) or nil
storeAfterTimeout()
end
function restoreRoute(device, dev_info, device_id, route)
-- default props
local props = {
"Spa:Pod:Object:Param:Props", "Route",
channelVolumes = { default_volume },
mute = false,
}
-- restore props from persistent storage
if use_persistent_storage then
local key_base = dev_info.name .. ":" ..
route.direction:lower() .. ":" ..
route.name .. ":"
local str = state_table[key_base .. "volume"]
props.volume = str and tonumber(str) or props.volume
local str = state_table[key_base .. "mute"]
props.mute = str and (str == "true") or false
local str = state_table[key_base .. "channelVolumes"]
props.channelVolumes = str and parseArray(str, tonumber) or props.channelVolumes
local str = state_table[key_base .. "channelMap"]
props.channelMap = str and parseArray(str) or props.channelMap
local str = state_table[key_base .. "latencyOffsetNsec"]
props.latencyOffsetNsec = str and math.tointeger(str) or props.latencyOffsetNsec
local str = state_table[key_base .. "iec958Codecs"]
props.iec958Codecs = str and parseArray(str) or props.iec958Codecs
end
-- convert arrays to Spa Pod
if props.channelVolumes then
table.insert(props.channelVolumes, 1, "Spa:Float")
props.channelVolumes = Pod.Array(props.channelVolumes)
end
if props.channelMap then
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
props.channelMap = Pod.Array(props.channelMap)
end
if props.iec958Codecs then
table.insert(props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
props.iec958Codecs = Pod.Array(props.iec958Codecs)
end
-- construct Route param
local param = Pod.Object {
"Spa:Pod:Object:Param:Route", "Route",
index = route.index,
device = device_id,
props = Pod.Object(props),
save = route.save,
}
Log.debug(param, "setting route on " .. tostring(device))
device:set_param("Route", param)
route.prev_active = true
route.active = true
end
function findActiveDeviceIDs(profile)
-- parses the classes from the profile and returns the device IDs
----- sample structure, should return { 0, 8 } -----
-- classes:
-- 1: 2
-- 2:
-- 1: Audio/Source
-- 2: 1
-- 3: card.profile.devices
-- 4:
-- 1: 0
-- pod_type: Array
-- value_type: Spa:Int
-- pod_type: Struct
-- 3:
-- 1: Audio/Sink
-- 2: 1
-- 3: card.profile.devices
-- 4:
-- 1: 8
-- pod_type: Array
-- value_type: Spa:Int
-- pod_type: Struct
-- pod_type: Struct
local active_ids = {}
if type(profile.classes) == "table" and profile.classes.pod_type == "Struct" then
for _, p in ipairs(profile.classes) do
if type(p) == "table" and p.pod_type == "Struct" then
local i = 1
while true do
local k, v = p[i], p[i+1]
i = i + 2
if not k or not v then
break
end
if k == "card.profile.devices" and
type(v) == "table" and v.pod_type == "Array" then
for _, dev_id in ipairs(v) do
table.insert(active_ids, dev_id)
end
end
end
end
end
end
return active_ids
end
-- returns an array of the route names that were previously selected
-- for the given device and profile
function getStoredProfileRoutes(dev_name, profile_name)
local key = dev_name .. ":profile:" .. profile_name
local str = state_table[key]
return str and parseArray(str) or {}
end
-- find a route that was previously stored for a device_id
-- spr needs to be the array returned from getStoredProfileRoutes()
function findSavedRoute(dev_info, device_id, spr)
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) and arrayContains(spr, ri.name) then
return ri
end
end
return nil
end
-- find the best route for a given device_id, based on availability and priority
function findBestRoute(dev_info, device_id)
local best_avail = nil
local best_unk = nil
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) then
if ri.available == "yes" or ri.available == "unknown" then
if ri.direction == "Output" and ri.available ~= ri.prev_available then
best_avail = ri
ri.save = true
break
elseif ri.available == "yes" then
if (best_avail == nil or ri.priority > best_avail.priority) then
best_avail = ri
end
elseif best_unk == nil or ri.priority > best_unk.priority then
best_unk = ri
end
end
end
end
return best_avail or best_unk
end
function restoreProfileRoutes(device, dev_info, profile, profile_changed)
Log.info(device, "restore routes for profile " .. profile.name)
local active_ids = findActiveDeviceIDs(profile)
local spr = getStoredProfileRoutes(dev_info.name, profile.name)
for _, device_id in ipairs(active_ids) do
Log.info(device, "restoring device " .. device_id);
local route = nil
-- restore routes selection for the newly selected profile
-- don't bother if spr is empty, there is no point
if profile_changed and #spr > 0 then
route = findSavedRoute(dev_info, device_id, spr)
if route then
-- we found a saved route
if route.available == "no" then
Log.info(device, "saved route '" .. route.name .. "' not available")
-- not available, try to find next best
route = nil
else
Log.info(device, "found saved route: " .. route.name)
-- make sure we save it again
route.save = true
end
end
end
-- we could not find a saved route, try to find a new best
if not route then
route = findBestRoute(dev_info, device_id)
if not route then
Log.info(device, "can't find best route")
else
Log.info(device, "found best route: " .. route.name)
end
end
-- restore route
if route then
restoreRoute(device, dev_info, device_id, route)
end
end
end
function findRouteInfo(dev_info, route, return_new)
local ri = dev_info.route_infos[route.index]
if not ri and return_new then
ri = {
index = route.index,
name = route.name,
direction = route.direction,
devices = route.devices or {},
priority = route.priority or 0,
available = route.available or "unknown",
prev_available = route.available or "unknown",
active = false,
prev_active = false,
save = false,
}
end
return ri
end
function handleDevice(device)
local dev_info = dev_infos[device["bound-id"]]
local new_route_infos = {}
local avail_routes_changed = false
local profile = nil
-- get current profile
for p in device:iterate_params("Profile") do
profile = parseParam(p, "Profile")
end
-- look at all the routes and update/reset cached information
for p in device:iterate_params("EnumRoute") do
-- parse pod
local route = parseParam(p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = findRouteInfo(dev_info, route, true)
-- update properties
route_info.prev_available = route_info.available
if route_info.available ~= route.available then
Log.info(device, "route " .. route.name .. " available changed " ..
route_info.available .. " -> " .. route.available)
route_info.available = route.available
if profile and arrayContains(route.profiles, profile.index) then
avail_routes_changed = true
end
end
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos[route.index] = route_info
::skip_enum_route::
end
-- replace old route_infos to lose old routes
-- that no longer exist on the device
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params("Route") do
local route = parseParam(p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = findRouteInfo(dev_info, route, false)
if not route_info then
goto skip_route
end
-- update state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- make sure we save this as a preferred route
Log.info(device, "new active route found " .. route.name)
restoreRoute(device, dev_info, route.device, route_info)
elseif route.save then
-- just save route properties
Log.info(device, "storing route props for " .. route.name)
saveRouteProps(dev_info, route)
end
::skip_route::
end
-- restore routes for profile
if profile then
local profile_changed = (dev_info.active_profile ~= profile.index)
-- if the profile changed, restore routes for that profile
-- if any of the routes of the current profile changed in availability,
-- then try to select a new "best" route for each device and ignore
-- what was stored
if profile_changed or avail_routes_changed then
dev_info.active_profile = profile.index
restoreProfileRoutes(device, dev_info, profile, profile_changed)
end
saveProfile(dev_info, profile.name)
end
end
om = ObjectManager {
Interest {
type = "device",
Constraint { "device.name", "is-present", type = "pw-global" },
}
}
om:connect("objects-changed", function (om)
local new_dev_infos = {}
for device in om:iterate() do
local dev_info = dev_infos[device["bound-id"]]
-- new device appeared
if not dev_info then
dev_info = {
name = device.properties["device.name"],
active_profile = -1,
route_infos = {},
}
dev_infos[device["bound-id"]] = dev_info
device:connect("params-changed", handleDevice)
handleDevice(device)
end
new_dev_infos[device["bound-id"]] = dev_info
end
-- replace list to get rid of dev_info for devices that no longer exist
dev_infos = new_dev_infos
end)
om:activate()

View File

@ -0,0 +1,218 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local config = ... or {}
config.roles = config.roles or {}
config["duck.level"] = config["duck.level"] or 0.3
function findRole(role)
if role and not config.roles[role] then
for r, p in pairs(config.roles) do
if type(p.alias) == "table" then
for i = 1, #(p.alias), 1 do
if role == p.alias[i] then
return r
end
end
end
end
end
return role
end
function priorityForRole(role)
local r = role and config.roles[role] or nil
return r and r.priority or 0
end
function getAction(dominant_role, other_role)
-- default to "mix" if the role is not configured
if not dominant_role or not config.roles[dominant_role] then
return "mix"
end
local role_config = config.roles[dominant_role]
return role_config["action." .. other_role]
or role_config["action.default"]
or "mix"
end
function restoreVolume(role, media_class)
if not mixer_api then return end
local ep = endpoints_om:lookup {
Constraint { "media.role", "=", role, type = "pw" },
Constraint { "media.class", "=", media_class, type = "pw" },
}
if ep and ep.properties["node.id"] then
Log.debug(ep, "restore role " .. role)
mixer_api:call("set-volume", ep.properties["node.id"], {
monitorVolume = 1.0,
})
end
end
function duck(role, media_class)
if not mixer_api then return end
local ep = endpoints_om:lookup {
Constraint { "media.role", "=", role, type = "pw" },
Constraint { "media.class", "=", media_class, type = "pw" },
}
if ep and ep.properties["node.id"] then
Log.debug(ep, "duck role " .. role)
mixer_api:call("set-volume", ep.properties["node.id"], {
monitorVolume = config["duck.level"],
})
end
end
function getSuspendPlaybackMetadata ()
local suspend = false
local metadata = metadata_om:lookup()
if metadata then
local value = metadata:find(0, "suspend.playback")
if value then
suspend = value == "1" and true or false
end
end
return suspend
end
function rescan()
local links = {
["Audio/Source"] = {},
["Audio/Sink"] = {},
["Video/Source"] = {},
}
Log.info("Rescan endpoint links")
-- deactivate all links if suspend playback metadata is present
local suspend = getSuspendPlaybackMetadata()
for silink in silinks_om:iterate() do
if suspend then
silink:deactivate(Feature.SessionItem.ACTIVE)
end
end
-- gather info about links
for silink in silinks_om:iterate() do
local props = silink.properties
local role = props["media.role"]
local target_class = props["target.media.class"]
local plugged = props["item.plugged.usec"]
local active =
((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
if links[target_class] then
table.insert(links[target_class], {
silink = silink,
role = findRole(role),
active = active,
priority = priorityForRole(role),
plugged = plugged and tonumber(plugged) or 0
})
end
end
local function compareLinks(l1, l2)
return (l1.priority > l2.priority) or
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
end
for media_class, v in pairs(links) do
-- sort on priority and stream creation time
table.sort(v, compareLinks)
-- apply actions
local first_link = v[1]
if first_link then
for i = 2, #v, 1 do
local action = getAction(first_link.role, v[i].role)
if action == "cork" then
if v[i].active then
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
end
elseif action == "mix" then
if not v[i].active and not suspend then
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
restoreVolume(v[i].role, media_class)
elseif action == "duck" then
if not v[i].active and not suspend then
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
duck(v[i].role, media_class)
else
Log.warning("Unknown action: " .. action)
end
end
if not first_link.active and not suspend then
first_link.silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
end
restoreVolume(first_link.role, media_class)
end
end
end
pending_ops = 0
pending_rescan = false
function pendingOperation()
pending_ops = pending_ops + 1
return function()
pending_ops = pending_ops - 1
if pending_ops == 0 and pending_rescan then
pending_rescan = false
rescan()
end
end
end
function maybeRescan()
if pending_ops == 0 then
rescan()
else
pending_rescan = true
end
end
silinks_om = ObjectManager {
Interest {
type = "SiLink",
Constraint { "is.policy.endpoint.client.link", "=", true },
},
}
silinks_om:connect("objects-changed", maybeRescan)
silinks_om:activate()
-- enable ducking if mixer-api is loaded
mixer_api = Plugin.find("mixer-api")
if mixer_api then
endpoints_om = ObjectManager {
Interest { type = "endpoint" },
}
endpoints_om:activate()
end
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
metadata_om:connect("object-added", function (om, metadata)
metadata:connect("changed", function (m, subject, key, t, value)
if key == "suspend.playback" then
maybeRescan()
end
end)
end)
metadata_om:activate()

View File

@ -0,0 +1,259 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
config.roles = config.roles or {}
local self = {}
self.scanning = false
self.pending_rescan = false
function rescan ()
for si in linkables_om:iterate() do
handleLinkable (si)
end
end
function scheduleRescan ()
if self.scanning then
self.pending_rescan = true
return
end
self.scanning = true
rescan ()
self.scanning = false
if self.pending_rescan then
self.pending_rescan = false
Core.sync(function ()
scheduleRescan ()
end)
end
end
function findRole(role, tmc)
if role and not config.roles[role] then
-- find the role with matching alias
for r, p in pairs(config.roles) do
-- default media class can be overridden in the role config data
mc = p["media.class"] or "Audio/Sink"
if (type(p.alias) == "table" and tmc == mc) then
for i = 1, #(p.alias), 1 do
if role == p.alias[i] then
return r
end
end
end
end
-- otherwise get the lowest priority role
local lowest_priority_p = nil
local lowest_priority_r = nil
for r, p in pairs(config.roles) do
mc = p["media.class"] or "Audio/Sink"
if tmc == mc and (lowest_priority_p == nil or
p.priority < lowest_priority_p.priority) then
lowest_priority_p = p
lowest_priority_r = r
end
end
return lowest_priority_r
end
return role
end
function findTargetEndpoint (node, media_class, role)
local target_class_assoc = {
["Stream/Input/Audio"] = "Audio/Source",
["Stream/Output/Audio"] = "Audio/Sink",
["Stream/Input/Video"] = "Video/Source",
}
local media_role = nil
local highest_priority = -1
local target = nil
-- get target media class
local target_media_class = target_class_assoc[media_class]
if not target_media_class then
return nil
end
-- find highest priority endpoint by role
media_role = findRole(role, target_media_class)
for si_target_ep in endpoints_om:iterate {
Constraint { "role", "=", media_role, type = "pw-global" },
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
} do
local priority = tonumber(si_target_ep.properties["priority"])
if priority > highest_priority then
highest_priority = priority
target = si_target_ep
end
end
return target
end
function createLink (si, si_target_ep)
local out_item = nil
local in_item = nil
local si_props = si.properties
local target_ep_props = si_target_ep.properties
if si_props["item.node.direction"] == "output" then
-- playback
out_item = si
in_item = si_target_ep
else
-- capture
out_item = si_target_ep
in_item = si
end
Log.info (string.format("link %s <-> %s",
tostring(si_props["node.name"]),
tostring(target_ep_props["name"])))
-- create and configure link
local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["is.policy.endpoint.client.link"] = true,
["media.role"] = target_ep_props["role"],
["target.media.class"] = target_ep_props["media.class"],
["item.plugged.usec"] = si_props["item.plugged.usec"],
} then
Log.warning (si_link, "failed to configure si-standard-link")
return
end
-- register
si_link:register()
end
function checkLinkable (si)
-- only handle session items that has a node associated proxy
local node = si:get_associated_proxy ("node")
if not node or not node.properties then
return false
end
-- only handle stream session items
local media_class = node.properties["media.class"]
if not media_class or not string.find (media_class, "Stream") then
return false
end
-- Determine if we can handle item by this policy
if endpoints_om:get_n_objects () == 0 then
Log.debug (si, "item won't be handled by this policy")
return false
end
return true
end
function handleLinkable (si)
if not checkLinkable (si) then
return
end
local node = si:get_associated_proxy ("node")
local media_class = node.properties["media.class"] or ""
local media_role = node.properties["media.role"] or "Default"
Log.info (si, "handling item " .. tostring(node.properties["node.name"]) ..
" with role " .. media_role)
-- find proper target endpoint
local si_target_ep = findTargetEndpoint (node, media_class, media_role)
if not si_target_ep then
Log.info (si, "... target endpoint not found")
return
end
-- Check if item is linked to proper target, otherwise re-link
for link in links_om:iterate() do
local out_id = tonumber(link.properties["out.item.id"])
local in_id = tonumber(link.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
local is_out = out_id == si.id and true or false
for peer_ep in endpoints_om:iterate() do
if peer_ep.id == (is_out and in_id or out_id) then
if peer_ep.id == si_target_ep.id then
Log.info (si, "... already linked to proper target endpoint")
return
end
-- remove old link if active, otherwise schedule rescan
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
link:remove ()
Log.info (si, "... moving to new target")
else
scheduleRescan ()
Log.info (si, "... scheduled rescan")
return
end
end
end
end
end
-- create new link
createLink (si, si_target_ep)
end
function unhandleLinkable (si)
if not checkLinkable (si) then
return
end
local node = si:get_associated_proxy ("node")
Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
-- remove any links associated with this item
for silink in links_om:iterate() do
local out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
silink:remove ()
Log.info (silink, "... link removed")
end
end
end
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
linkables_om = ObjectManager { Interest { type = "SiLinkable",
-- only handle si-audio-adapter and si-node
Constraint {
"item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
Constraint {
"active-features", "!", 0, type = "gobject" },
}
}
links_om = ObjectManager { Interest { type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.endpoint.client.link", "=", true, type = "pw-global" },
} }
linkables_om:connect("objects-changed", function (om)
scheduleRescan ()
end)
linkables_om:connect("object-removed", function (om, si)
unhandleLinkable (si)
end)
endpoints_om:activate()
linkables_om:activate()
links_om:activate()

View File

@ -0,0 +1,234 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.move and config.follow are not nil
config.move = config.move or false
config.follow = config.follow or false
local self = {}
self.scanning = false
self.pending_rescan = false
function rescan ()
-- check endpoints and register new links
for si_ep in endpoints_om:iterate() do
handleEndpoint (si_ep)
end
end
function scheduleRescan ()
if self.scanning then
self.pending_rescan = true
return
end
self.scanning = true
rescan ()
self.scanning = false
if self.pending_rescan then
self.pending_rescan = false
Core.sync(function ()
scheduleRescan ()
end)
end
end
function findTargetByDefaultNode (target_media_class)
local def_id = default_nodes:call("get-default-node", target_media_class)
if def_id ~= Id.INVALID then
for si_target in linkables_om:iterate() do
local target_node = si_target:get_associated_proxy ("node")
if target_node["bound-id"] == def_id then
return si_target
end
end
end
return nil
end
function findTargetByFirstAvailable (target_media_class)
for si_target in linkables_om:iterate() do
local target_node = si_target:get_associated_proxy ("node")
if target_node.properties["media.class"] == target_media_class then
return si_target
end
end
return nil
end
function findUndefinedTarget (si_ep)
local media_class = si_ep.properties["media.class"]
local target_class_assoc = {
["Audio/Source"] = "Audio/Source",
["Audio/Sink"] = "Audio/Sink",
["Video/Source"] = "Video/Source",
}
local target_media_class = target_class_assoc[media_class]
if not target_media_class then
return nil
end
local si_target = findTargetByDefaultNode (target_media_class)
if not si_target then
si_target = findTargetByFirstAvailable (target_media_class)
end
return si_target
end
function createLink (si_ep, si_target)
local out_item = nil
local in_item = nil
local ep_props = si_ep.properties
local target_props = si_target.properties
if target_props["item.node.direction"] == "input" then
-- playback
out_item = si_ep
in_item = si_target
else
-- capture
in_item = si_ep
out_item = si_target
end
Log.info (string.format("link %s <-> %s",
ep_props["name"],
target_props["node.name"]))
-- create and configure link
local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["passive"] = true,
["is.policy.endpoint.device.link"] = true,
} then
Log.warning (si_link, "failed to configure si-standard-link")
return
end
-- register
si_link:register ()
-- activate
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
if e then
Log.warning (l, "failed to activate si-standard-link: " .. tostring(e))
l:remove ()
else
Log.info (l, "activated si-standard-link")
end
end)
end
function handleEndpoint (si_ep)
Log.info (si_ep, "handling endpoint " .. si_ep.properties["name"])
-- find proper target item
local si_target = findUndefinedTarget (si_ep)
if not si_target then
Log.info (si_ep, "... target item not found")
return
end
-- Check if item is linked to proper target, otherwise re-link
for link in links_om:iterate() do
local out_id = tonumber(link.properties["out.item.id"])
local in_id = tonumber(link.properties["in.item.id"])
if out_id == si_ep.id or in_id == si_ep.id then
local is_out = out_id == si_ep.id and true or false
for peer in linkables_om:iterate() do
if peer.id == (is_out and in_id or out_id) then
if peer.id == si_target.id then
Log.info (si_ep, "... already linked to proper target")
return
end
-- remove old link if active, otherwise schedule rescan
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
link:remove ()
Log.info (si_ep, "... moving to new target")
else
scheduleRescan ()
Log.info (si_ep, "... scheduled rescan")
return
end
end
end
end
end
-- create new link
createLink (si_ep, si_target)
end
function unhandleLinkable (si)
si_props = si.properties
Log.info (si, string.format("unhandling item: %s (%s)",
tostring(si_props["node.name"]), tostring(si_props["node.id"])))
-- remove any links associated with this item
for silink in links_om:iterate() do
local out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
silink:remove ()
Log.info (silink, "... link removed")
end
end
end
default_nodes = Plugin.find("default-nodes-api")
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
linkables_om = ObjectManager {
Interest {
type = "SiLinkable",
-- only handle device si-audio-adapter items
Constraint { "item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
Constraint { "item.node.type", "=", "device", type = "pw-global" },
Constraint { "active-features", "!", 0, type = "gobject" },
}
}
links_om = ObjectManager {
Interest {
type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.endpoint.device.link", "=", true, type = "pw-global" },
}
}
-- listen for default node changes if config.follow is enabled
if config.follow then
default_nodes:connect("changed", function (p)
scheduleRescan ()
end)
end
linkables_om:connect("objects-changed", function (om)
scheduleRescan ()
end)
endpoints_om:connect("object-added", function (om)
scheduleRescan ()
end)
linkables_om:connect("object-removed", function (om, si)
unhandleLinkable (si)
end)
endpoints_om:activate()
linkables_om:activate()
links_om:activate()

View File

@ -0,0 +1,981 @@
-- WirePlumber
--
-- Copyright © 2020 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
-- ensure config.move and config.follow are not nil
config.move = config.move or false
config.follow = config.follow or false
config.filter_forward_format = config["filter.forward-format"] or false
local self = {}
self.scanning = false
self.pending_rescan = false
self.events_skipped = false
self.pending_error_timer = nil
function rescan()
for si in linkables_om:iterate() do
handleLinkable (si)
end
end
function scheduleRescan ()
if self.scanning then
self.pending_rescan = true
return
end
self.scanning = true
rescan ()
self.scanning = false
if self.pending_rescan then
self.pending_rescan = false
Core.sync(function ()
scheduleRescan ()
end)
end
end
function parseBool(var)
return var and (var:lower() == "true" or var == "1")
end
function createLink (si, si_target, passthrough, exclusive)
local out_item = nil
local in_item = nil
local si_props = si.properties
local target_props = si_target.properties
local si_id = si.id
-- break rescan if tried more than 5 times with same target
if si_flags[si_id].failed_peer_id ~= nil and
si_flags[si_id].failed_peer_id == si_target.id and
si_flags[si_id].failed_count ~= nil and
si_flags[si_id].failed_count > 5 then
Log.warning (si, "tried to link on last rescan, not retrying")
return
end
if si_props["item.node.direction"] == "output" then
-- playback
out_item = si
in_item = si_target
else
-- capture
in_item = si
out_item = si_target
end
local passive = parseBool(si_props["node.passive"]) or
parseBool(target_props["node.passive"])
Log.info (
string.format("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s",
tostring(si_props["node.name"]),
tostring(target_props["node.name"]),
tostring(passive), tostring(passthrough), tostring(exclusive)))
-- create and configure link
local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["passive"] = passive,
["passthrough"] = passthrough,
["exclusive"] = exclusive,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["is.policy.item.link"] = true,
} then
Log.warning (si_link, "failed to configure si-standard-link")
return
end
-- register
si_flags[si_id].peer_id = si_target.id
si_flags[si_id].failed_peer_id = si_target.id
if si_flags[si_id].failed_count ~= nil then
si_flags[si_id].failed_count = si_flags[si_id].failed_count + 1
else
si_flags[si_id].failed_count = 1
end
si_link:register ()
-- activate
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
if e then
Log.info (l, "failed to activate si-standard-link: " .. tostring(e))
if si_flags[si_id] ~= nil then
si_flags[si_id].peer_id = nil
end
l:remove ()
else
if si_flags[si_id] ~= nil then
si_flags[si_id].failed_peer_id = nil
si_flags[si_id].failed_count = 0
end
Log.info (l, "activated si-standard-link")
end
end)
end
function isLinked(si_target)
local target_id = si_target.id
local linked = false
local exclusive = false
for l in links_om:iterate() do
local p = l.properties
local out_id = tonumber(p["out.item.id"])
local in_id = tonumber(p["in.item.id"])
linked = (out_id == target_id) or (in_id == target_id)
if linked then
exclusive = parseBool(p["exclusive"]) or parseBool(p["passthrough"])
break
end
end
return linked, exclusive
end
function canPassthrough (si, si_target)
-- both nodes must support encoded formats
if not parseBool(si.properties["item.node.supports-encoded-fmts"])
or not parseBool(si_target.properties["item.node.supports-encoded-fmts"]) then
return false
end
-- make sure that the nodes have at least one common non-raw format
local n1 = si:get_associated_proxy ("node")
local n2 = si_target:get_associated_proxy ("node")
for p1 in n1:iterate_params("EnumFormat") do
local p1p = p1:parse()
if p1p.properties.mediaSubtype ~= "raw" then
for p2 in n2:iterate_params("EnumFormat") do
if p1:filter(p2) then
return true
end
end
end
end
return false
end
function canLink (properties, si_target)
local target_properties = si_target.properties
-- nodes must have the same media type
if properties["media.type"] ~= target_properties["media.type"] then
return false
end
-- nodes must have opposite direction, or otherwise they must be both input
-- and the target must have a monitor (so the target will be used as a source)
local function isMonitor(properties)
return properties["item.node.direction"] == "input" and
parseBool(properties["item.features.monitor"]) and
not parseBool(properties["item.features.no-dsp"]) and
properties["item.factory.name"] == "si-audio-adapter"
end
if properties["item.node.direction"] == target_properties["item.node.direction"]
and not isMonitor(target_properties) then
return false
end
-- check link group
local function canLinkGroupCheck (link_group, si_target, hops)
local target_props = si_target.properties
local target_link_group = target_props["node.link-group"]
if hops == 8 then
return false
end
-- allow linking if target has no link-group property
if not target_link_group then
return true
end
-- do not allow linking if target has the same link-group
if link_group == target_link_group then
return false
end
-- make sure target is not linked with another node with same link group
-- start by locating other nodes in the target's link-group, in opposite direction
for n in linkables_om:iterate {
Constraint { "id", "!", si_target.id, type = "gobject" },
Constraint { "item.node.direction", "!", target_props["item.node.direction"] },
Constraint { "node.link-group", "=", target_link_group },
} do
-- iterate their peers and return false if one of them cannot link
for silink in links_om:iterate() do
local out_id = tonumber(silink.properties["out.item.id"])
local in_id = tonumber(silink.properties["in.item.id"])
if out_id == n.id or in_id == n.id then
local peer_id = (out_id == n.id) and in_id or out_id
local peer = linkables_om:lookup {
Constraint { "id", "=", peer_id, type = "gobject" },
}
if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
return false
end
end
end
end
return true
end
local link_group = properties["node.link-group"]
if link_group then
return canLinkGroupCheck (link_group, si_target, 0)
end
return true
end
function getTargetDirection(properties)
local target_direction = nil
if properties["item.node.direction"] == "output" or
(properties["item.node.direction"] == "input" and
parseBool(properties["stream.capture.sink"])) then
target_direction = "input"
else
target_direction = "output"
end
return target_direction
end
function getDefaultNode(properties, target_direction)
local target_media_class =
properties["media.type"] ..
(target_direction == "input" and "/Sink" or "/Source")
return default_nodes:call("get-default-node", target_media_class)
end
-- Try to locate a valid target node that was explicitly requsted by the
-- client(node.target) or by the user(target.node)
-- Use the target.node metadata, if config.move is enabled,
-- then use the node.target property that was set on the node
-- `properties` must be the properties dictionary of the session item
-- that is currently being handled
function findDefinedTarget (properties)
local metadata = config.move and metadata_om:lookup()
local target_direction = getTargetDirection(properties)
local target_key
local target_value
local node_defined = false
if properties["target.object"] ~= nil then
target_value = properties["target.object"]
target_key = "object.serial"
node_defined = true
elseif properties["node.target"] ~= nil then
target_value = properties["node.target"]
target_key = "node.id"
node_defined = true
end
if metadata then
local id = metadata:find(properties["node.id"], "target.object")
if id ~= nil then
target_value = id
target_key = "object.serial"
node_defined = false
else
id = metadata:find(properties["node.id"], "target.node")
if id ~= nil then
target_value = id
target_key = "node.id"
node_defined = false
end
end
end
if target_value == "-1" then
return nil, false, node_defined
end
if target_value and tonumber(target_value) then
local si_target = linkables_om:lookup {
Constraint { target_key, "=", target_value },
}
if si_target and canLink (properties, si_target) then
return si_target, true, node_defined
end
end
if target_value then
for si_target in linkables_om:iterate() do
local target_props = si_target.properties
if (target_props["node.name"] == target_value or
target_props["object.path"] == target_value) and
target_props["item.node.direction"] == target_direction and
canLink (properties, si_target) then
return si_target, true, node_defined
end
end
end
return nil, (target_value ~= nil), node_defined
end
function parseParam(param, id)
local route = param:parse()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
else
return nil
end
end
function arrayContains(a, value)
for _, v in ipairs(a) do
if v == value then
return true
end
end
return false
end
-- Does the target device have any active/available paths/routes to
-- the physical device(spkr/mic/cam)?
function haveAvailableRoutes (si_props)
local card_profile_device = si_props["card.profile.device"]
local device_id = si_props["device.id"]
local device = device_id and devices_om:lookup {
Constraint { "bound-id", "=", device_id, type = "gobject"},
}
if not card_profile_device or not device then
return true
end
local found = 0
local avail = 0
-- First check "SPA_PARAM_Route" if there are any active devices
-- in an active profile.
for p in device:iterate_params("Route") do
local route = parseParam(p, "Route")
if not route then
goto skip_route
end
if (route.device ~= tonumber(card_profile_device)) then
goto skip_route
end
if (route.available == "no") then
return false
end
do return true end
::skip_route::
end
-- Second check "SPA_PARAM_EnumRoute" if there is any route that
-- is available if not active.
for p in device:iterate_params("EnumRoute") do
local route = parseParam(p, "EnumRoute")
if not route then
goto skip_enum_route
end
if not arrayContains(route.devices, tonumber(card_profile_device)) then
goto skip_enum_route
end
found = found + 1;
if (route.available ~= "no") then
avail = avail +1
end
::skip_enum_route::
end
if found == 0 then
return true
end
if avail > 0 then
return true
end
return false
end
function findDefaultLinkable (si)
local si_props = si.properties
local target_direction = getTargetDirection(si_props)
local def_node_id = getDefaultNode(si_props, target_direction)
return linkables_om:lookup {
Constraint { "node.id", "=", tostring(def_node_id) }
}
end
function checkPassthroughCompatibility (si, si_target)
local si_must_passthrough = parseBool(si.properties["item.node.encoded-only"])
local si_target_must_passthrough = parseBool(si_target.properties["item.node.encoded-only"])
local can_passthrough = canPassthrough(si, si_target)
if (si_must_passthrough or si_target_must_passthrough)
and not can_passthrough then
return false, can_passthrough
end
return true, can_passthrough
end
function findBestLinkable (si)
local si_props = si.properties
local target_direction = getTargetDirection(si_props)
local target_picked = nil
local target_can_passthrough = false
local target_priority = 0
local target_plugged = 0
for si_target in linkables_om:iterate {
Constraint { "item.node.type", "=", "device" },
Constraint { "item.node.direction", "=", target_direction },
Constraint { "media.type", "=", si_props["media.type"] },
} do
local si_target_props = si_target.properties
local si_target_node_id = si_target_props["node.id"]
local priority = tonumber(si_target_props["priority.session"]) or 0
Log.debug(string.format("Looking at: %s (%s)",
tostring(si_target_props["node.name"]),
tostring(si_target_node_id)))
if not canLink (si_props, si_target) then
Log.debug("... cannot link, skip linkable")
goto skip_linkable
end
if not haveAvailableRoutes(si_target_props) then
Log.debug("... does not have routes, skip linkable")
goto skip_linkable
end
local passthrough_compatible, can_passthrough =
checkPassthroughCompatibility (si, si_target)
if not passthrough_compatible then
Log.debug("... passthrough is not compatible, skip linkable")
goto skip_linkable
end
local plugged = tonumber(si_target_props["item.plugged.usec"]) or 0
Log.debug("... priority:"..tostring(priority)..", plugged:"..tostring(plugged))
-- (target_picked == NULL) --> make sure atleast one target is picked.
-- (priority > target_priority) --> pick the highest priority linkable(node)
-- target.
-- (priority == target_priority and plugged > target_plugged) --> pick the
-- latest connected/plugged(in time) linkable(node) target.
if (target_picked == nil or
priority > target_priority or
(priority == target_priority and plugged > target_plugged)) then
Log.debug("... picked")
target_picked = si_target
target_can_passthrough = can_passthrough
target_priority = priority
target_plugged = plugged
end
::skip_linkable::
end
if target_picked then
Log.info(string.format("... best target picked: %s (%s), can_passthrough:%s",
tostring(target_picked.properties["node.name"]),
tostring(target_picked.properties["node.id"]),
tostring(target_can_passthrough)))
return target_picked, target_can_passthrough
else
return nil, nil
end
end
function findUndefinedTarget (si)
-- Just find the best linkable if default nodes module is not loaded
if default_nodes == nil then
return findBestLinkable (si)
end
-- Otherwise find the default linkable. If the default linkable is not
-- compatible, we find the best one instead. We return nil if the default
-- linkable does not exist.
local si_target = findDefaultLinkable (si)
if si_target then
local passthrough_compatible, can_passthrough =
checkPassthroughCompatibility (si, si_target)
if canLink (si.properties, si_target) and passthrough_compatible then
Log.info(string.format("... default target picked: %s (%s), can_passthrough:%s",
tostring(si_target.properties["node.name"]),
tostring(si_target.properties["node.id"]),
tostring(can_passthrough)))
return si_target, can_passthrough
else
return findBestLinkable (si)
end
end
return nil, nil
end
function lookupLink (si_id, si_target_id)
local link = links_om:lookup {
Constraint { "out.item.id", "=", si_id },
Constraint { "in.item.id", "=", si_target_id }
}
if not link then
link = links_om:lookup {
Constraint { "in.item.id", "=", si_id },
Constraint { "out.item.id", "=", si_target_id }
}
end
return link
end
function checkLinkable(si, handle_nonstreams)
-- only handle stream session items
local si_props = si.properties
if not si_props or (si_props["item.node.type"] ~= "stream"
and not handle_nonstreams) then
return false
end
-- Determine if we can handle item by this policy
if endpoints_om:get_n_objects () > 0 and
si_props["item.factory.name"] == "si-audio-adapter" then
return false
end
return true, si_props
end
si_flags = {}
function checkPending ()
local pending_linkables = pending_linkables_om:get_n_objects ()
-- We cannot process linkables if some of them are pending activation,
-- because linkables do not appear in the same order as nodes,
-- and we cannot resolve target node references until all linkables
-- have appeared.
if self.pending_error_timer then
self.pending_error_timer:destroy ()
self.pending_error_timer = nil
end
if pending_linkables ~= 0 then
-- Wait for linkables to get it sync
Log.debug(string.format("pending %d linkable not ready",
pending_linkables))
self.events_skipped = true
-- To make bugs in activation easier to debug, emit an error message
-- if they occur. policy-node should never be suspended for 20sec.
self.pending_error_timer = Core.timeout_add(20000, function()
self.pending_error_timer = nil
if pending_linkables ~= 0 then
Log.message(string.format("%d pending linkable(s) not activated in 20sec. "
.. "This should never happen.", pending_linkables))
end
end)
return true
elseif self.events_skipped then
Log.debug("pending linkables ready")
self.events_skipped = false
scheduleRescan ()
return true
end
return false
end
function checkFollowDefault (si, si_target, has_node_defined_target)
-- If it got linked to the default target that is defined by node
-- props but not metadata, start ignoring the node prop from now on.
-- This is what Pulseaudio does.
--
-- Pulseaudio skips here filter streams (i->origin_sink and
-- o->destination_source set in PA). Pipewire does not have a flag
-- explicitly for this, but we can use presence of node.link-group.
if not has_node_defined_target then
return
end
local si_props = si.properties
local target_props = si_target.properties
local reconnect = not parseBool(si_props["node.dont-reconnect"])
local is_filter = (si_props["node.link-group"] ~= nil)
if config.follow and default_nodes ~= nil and reconnect and not is_filter then
local def_id = getDefaultNode(si_props, getTargetDirection(si_props))
if target_props["node.id"] == tostring(def_id) then
local metadata = metadata_om:lookup()
-- Set target.node, for backward compatibility
metadata:set(tonumber(si_props["node.id"]), "target.node", "Spa:Id", "-1")
Log.info (si, "... set metadata to follow default")
end
end
end
function handleLinkable (si)
if checkPending () then
return
end
local valid, si_props = checkLinkable(si)
if not valid then
return
end
-- check if we need to link this node at all
local autoconnect = parseBool(si_props["node.autoconnect"])
if not autoconnect then
Log.debug (si, tostring(si_props["node.name"]) .. " does not need to be autoconnected")
return
end
Log.info (si, string.format("handling item: %s (%s)",
tostring(si_props["node.name"]), tostring(si_props["node.id"])))
ensureSiFlags(si)
-- get other important node properties
local reconnect = not parseBool(si_props["node.dont-reconnect"])
local exclusive = parseBool(si_props["node.exclusive"])
local si_must_passthrough = parseBool(si_props["item.node.encoded-only"])
-- find defined target
local si_target, has_defined_target, has_node_defined_target
= findDefinedTarget(si_props)
local can_passthrough = si_target and canPassthrough(si, si_target)
if si_target and si_must_passthrough and not can_passthrough then
si_target = nil
end
-- if the client has seen a target that we haven't yet prepared, schedule
-- a rescan one more time and hope for the best
local si_id = si.id
if has_defined_target
and not si_target
and not si_flags[si_id].was_handled
and not si_flags[si_id].done_waiting then
Log.info (si, "... waiting for target")
si_flags[si_id].done_waiting = true
scheduleRescan()
return
end
-- find fallback target
if not si_target and (reconnect or not has_defined_target) then
si_target, can_passthrough = findUndefinedTarget(si)
end
-- Check if item is linked to proper target, otherwise re-link
if si_flags[si_id].peer_id then
if si_target and si_flags[si_id].peer_id == si_target.id then
Log.debug (si, "... already linked to proper target")
-- Check this also here, in case in default targets changed
checkFollowDefault (si, si_target, has_node_defined_target)
return
end
local link = lookupLink (si_id, si_flags[si_id].peer_id)
if reconnect then
if link ~= nil then
-- remove old link
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) == 0) then
-- remove also not yet activated links: they might never become active,
-- and we should not loop waiting for them
Log.warning (link, "Link was not activated before removing")
end
si_flags[si_id].peer_id = nil
link:remove ()
Log.info (si, "... moving to new target")
end
else
if link ~= nil then
Log.info (si, "... dont-reconnect, not moving")
return
end
end
end
-- if the stream has dont-reconnect and was already linked before,
-- don't link it to a new target
if not reconnect and si_flags[si.id].was_handled then
si_target = nil
end
-- check target's availability
if si_target then
local target_is_linked, target_is_exclusive = isLinked(si_target)
if target_is_exclusive then
Log.info(si, "... target is linked exclusively")
si_target = nil
end
if target_is_linked then
if exclusive or si_must_passthrough then
Log.info(si, "... target is already linked, cannot link exclusively")
si_target = nil
else
-- disable passthrough, we can live without it
can_passthrough = false
end
end
end
if not si_target then
Log.info (si, "... target not found, reconnect:" .. tostring(reconnect))
local node = si:get_associated_proxy ("node")
if not reconnect then
Log.info (si, "... destroy node")
node:request_destroy()
elseif si_flags[si.id].was_handled then
Log.info (si, "... waiting reconnect")
return
end
local client_id = node.properties["client.id"]
if client_id then
local client = clients_om:lookup {
Constraint { "bound-id", "=", client_id, type = "gobject" }
}
if client then
client:send_error(node["bound-id"], -2, "no node available")
end
end
else
createLink (si, si_target, can_passthrough, exclusive)
si_flags[si.id].was_handled = true
checkFollowDefault (si, si_target, has_node_defined_target)
end
end
function unhandleLinkable (si)
local valid, si_props = checkLinkable(si, true)
if not valid then
return
end
Log.info (si, string.format("unhandling item: %s (%s)",
tostring(si_props["node.name"]), tostring(si_props["node.id"])))
-- remove any links associated with this item
for silink in links_om:iterate() do
local out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then
if out_id == si.id and
si_flags[in_id] and si_flags[in_id].peer_id == out_id then
si_flags[in_id].peer_id = nil
elseif in_id == si.id and
si_flags[out_id] and si_flags[out_id].peer_id == in_id then
si_flags[out_id].peer_id = nil
end
silink:remove ()
Log.info (silink, "... link removed")
end
end
si_flags[si.id] = nil
end
default_nodes = Plugin.find("default-nodes-api")
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } }
clients_om = ObjectManager { Interest { type = "client" } }
devices_om = ObjectManager { Interest { type = "device" } }
linkables_om = ObjectManager {
Interest {
type = "SiLinkable",
-- only handle si-audio-adapter and si-node
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "active-features", "!", 0, type = "gobject" },
}
}
pending_linkables_om = ObjectManager {
Interest {
type = "SiLinkable",
-- only handle si-audio-adapter and si-node
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "active-features", "=", 0, type = "gobject" },
}
}
links_om = ObjectManager {
Interest {
type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.item.link", "=", true },
}
}
-- listen for default node changes if config.follow is enabled
if config.follow and default_nodes ~= nil then
default_nodes:connect("changed", function ()
scheduleRescan ()
end)
end
-- listen for target.node metadata changes if config.move is enabled
if config.move then
metadata_om:connect("object-added", function (om, metadata)
metadata:connect("changed", function (m, subject, key, t, value)
if key == "target.node" or key == "target.object" then
scheduleRescan ()
end
end)
end)
end
function findAssociatedLinkGroupNode (si)
local si_props = si.properties
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
if link_group == nil then
return nil
end
-- get the associated media class
local assoc_direction = getTargetDirection(si_props)
local assoc_media_class =
si_props["media.type"] ..
(assoc_direction == "input" and "/Sink" or "/Source")
-- find the linkable with same link group and matching assoc media class
for assoc_si in linkables_om:iterate() do
local assoc_node = assoc_si:get_associated_proxy ("node")
local assoc_link_group = assoc_node.properties["node.link-group"]
if assoc_link_group == link_group and
assoc_media_class == assoc_node.properties["media.class"] then
return assoc_si
end
end
return nil
end
function onLinkGroupPortsStateChanged (si, old_state, new_state)
local new_str = tostring(new_state)
local si_props = si.properties
-- only handle items with configured ports state
if new_str ~= "configured" then
return
end
Log.info (si, "ports format changed on " .. si_props["node.name"])
-- find associated device
local si_device = findAssociatedLinkGroupNode (si)
if si_device ~= nil then
local device_node_name = si_device.properties["node.name"]
-- get the stream format
local f, m = si:get_ports_format()
-- unregister the device
Log.info (si_device, "unregistering " .. device_node_name)
si_device:remove()
-- set new format in the device
Log.info (si_device, "setting new format in " .. device_node_name)
si_device:set_ports_format(f, m, function (item, e)
if e ~= nil then
Log.warning (item, "failed to configure ports in " ..
device_node_name .. ": " .. e)
end
-- register back the device
Log.info (item, "registering " .. device_node_name)
item:register()
end)
end
end
function ensureSiFlags (si)
-- prepare flags table
if not si_flags[si.id] then
si_flags[si.id] = {}
end
end
function checkFiltersPortsState (si)
local si_props = si.properties
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
ensureSiFlags(si)
-- only listen for ports state changed on audio filter streams
if si_flags[si.id].ports_state_signal ~= true and
si_props["item.factory.name"] == "si-audio-adapter" and
si_props["item.node.type"] == "stream" and
link_group ~= nil then
si:connect("adapter-ports-state-changed", onLinkGroupPortsStateChanged)
si_flags[si.id].ports_state_signal = true
Log.info (si, "listening ports state changed on " .. si_props["node.name"])
end
end
linkables_om:connect("object-added", function (om, si)
local si_props = si.properties
-- Forward filters ports format to associated virtual devices if enabled
if config.filter_forward_format then
checkFiltersPortsState (si)
end
if si_props["item.node.type"] ~= "stream" then
scheduleRescan ()
else
handleLinkable (si)
end
end)
linkables_om:connect("object-removed", function (om, si)
unhandleLinkable (si)
if si.properties["item.node.type"] ~= "stream" then
scheduleRescan ()
end
end)
devices_om:connect("object-added", function (om, device)
device:connect("params-changed", function (d, param_name)
scheduleRescan ()
end)
end)
metadata_om:activate()
endpoints_om:activate()
clients_om:activate()
linkables_om:activate()
pending_linkables_om:activate()
links_om:activate()
devices_om:activate()

View File

@ -0,0 +1,462 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on restore-stream.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ... or {}
config.properties = config.properties or {}
config_restore_props = config.properties["restore-props"] or false
config_restore_target = config.properties["restore-target"] or false
-- preprocess rules and create Interest objects
for _, r in ipairs(config.rules or {}) do
r.interests = {}
for _, i in ipairs(r.matches) do
local interest_desc = { type = "properties" }
for _, c in ipairs(i) do
c.type = "pw"
table.insert(interest_desc, Constraint(c))
end
local interest = Interest(interest_desc)
table.insert(r.interests, interest)
end
r.matches = nil
end
-- applies properties from config.rules when asked to
function rulesApplyProperties(properties)
for _, r in ipairs(config.rules or {}) do
if r.apply_properties then
for _, interest in ipairs(r.interests) do
if interest:matches(properties) then
for k, v in pairs(r.apply_properties) do
properties[k] = v
end
end
end
end
end
end
-- the state storage
state = State("restore-stream")
state_table = state:load()
-- simple serializer {"foo", "bar"} -> "foo;bar;"
function serializeArray(a)
local str = ""
for _, v in ipairs(a) do
str = str .. tostring(v):gsub(";", "\\;") .. ";"
end
return str
end
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
function parseArray(str, convert_value, with_type)
local array = {}
local val = ""
local escaped = false
for i = 1, #str do
local c = str:sub(i,i)
if c == '\\' then
escaped = true
elseif c == ';' and not escaped then
val = convert_value and convert_value(val) or val
table.insert(array, val)
val = ""
else
val = val .. tostring(c)
escaped = false
end
end
if with_type then
array["pod_type"] = "Array"
end
return array
end
function parseParam(param, id)
local route = param:parse()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
else
return nil
end
end
function storeAfterTimeout()
if timeout_source then
timeout_source:destroy()
end
timeout_source = Core.timeout_add(1000, function ()
local saved, err = state:save(state_table)
if not saved then
Log.warning(err)
end
timeout_source = nil
end)
end
function findSuitableKey(properties)
local keys = {
"media.role",
"application.id",
"application.name",
"media.name",
"node.name",
}
local key = nil
for _, k in ipairs(keys) do
local p = properties[k]
if p then
key = string.format("%s:%s:%s",
properties["media.class"]:gsub("^Stream/", ""), k, p)
break
end
end
return key
end
function saveTarget(subject, target_key, type, value)
if target_key ~= "target.node" and target_key ~= "target.object" then
return
end
local node = streams_om:lookup {
Constraint { "bound-id", "=", subject, type = "gobject" }
}
if not node then
return
end
local stream_props = node.properties
rulesApplyProperties(stream_props)
if stream_props["state.restore-target"] == false then
return
end
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
local target_value = value
local target_name = nil
if not target_value then
local metadata = metadata_om:lookup()
if metadata then
target_value = metadata:find(node["bound-id"], target_key)
end
end
if target_value and target_value ~= "-1" then
local target_node
if target_key == "target.object" then
target_node = allnodes_om:lookup {
Constraint { "object.serial", "=", target_value, type = "pw-global" }
}
else
target_node = allnodes_om:lookup {
Constraint { "bound-id", "=", target_value, type = "gobject" }
}
end
if target_node then
target_name = target_node.properties["node.name"]
end
end
state_table[key_base .. ":target"] = target_name
Log.info(node, "saving stream target for " ..
tostring(stream_props["node.name"]) ..
" -> " .. tostring(target_name))
storeAfterTimeout()
end
function restoreTarget(node, target_name)
local target_node = allnodes_om:lookup {
Constraint { "node.name", "=", target_name, type = "pw" }
}
if target_node then
local metadata = metadata_om:lookup()
if metadata then
metadata:set(node["bound-id"], "target.node", "Spa:Id",
target_node["bound-id"])
end
end
end
function jsonTable(val, name)
local tmp = ""
local count = 0
if name then tmp = tmp .. string.format("%q", name) .. ": " end
if type(val) == "table" then
if val["pod_type"] == "Array" then
tmp = tmp .. "["
for _, v in ipairs(val) do
if count > 0 then tmp = tmp .. "," end
tmp = tmp .. jsonTable(v)
count = count + 1
end
tmp = tmp .. "]"
else
tmp = tmp .. "{"
for k, v in pairs(val) do
if count > 0 then tmp = tmp .. "," end
tmp = tmp .. jsonTable(v, k)
count = count + 1
end
tmp = tmp .. "}"
end
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[type:" .. type(val) .. "]\""
end
return tmp
end
function moveToMetadata(key_base, metadata)
local route_table = { }
local count = 0
key = "restore.stream." .. key_base
key = string.gsub(key, ":", ".", 1);
local str = state_table[key_base .. ":volume"]
if str then
route_table["volume"] = tonumber(str)
count = count + 1;
end
local str = state_table[key_base .. ":mute"]
if str then
route_table["mute"] = str == "true"
count = count + 1;
end
local str = state_table[key_base .. ":channelVolumes"]
if str then
route_table["volumes"] = parseArray(str, tonumber, true)
count = count + 1;
end
local str = state_table[key_base .. ":channelMap"]
if str then
route_table["channels"] = parseArray(str, nil, true)
count = count + 1;
end
if count > 0 then
metadata:set(0, key, "Spa:String:JSON", jsonTable(route_table));
end
end
function saveStream(node)
local stream_props = node.properties
rulesApplyProperties(stream_props)
if config_restore_props and stream_props["state.restore-props"] ~= false then
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
Log.info(node, "saving stream props for " ..
tostring(stream_props["node.name"]))
for p in node:iterate_params("Props") do
local props = parseParam(p, "Props")
if not props then
goto skip_prop
end
if props.volume then
state_table[key_base .. ":volume"] = tostring(props.volume)
end
if props.mute ~= nil then
state_table[key_base .. ":mute"] = tostring(props.mute)
end
if props.channelVolumes then
state_table[key_base .. ":channelVolumes"] = serializeArray(props.channelVolumes)
end
if props.channelMap then
state_table[key_base .. ":channelMap"] = serializeArray(props.channelMap)
end
::skip_prop::
end
storeAfterTimeout()
end
end
function restoreStream(node)
local stream_props = node.properties
rulesApplyProperties(stream_props)
local key_base = findSuitableKey(stream_props)
if not key_base then
return
end
if config_restore_props and stream_props["state.restore-props"] ~= false then
local needsRestore = false
local props = { "Spa:Pod:Object:Param:Props", "Props" }
local str = state_table[key_base .. ":volume"]
needsRestore = str and true or needsRestore
props.volume = str and tonumber(str) or nil
local str = state_table[key_base .. ":mute"]
needsRestore = str and true or needsRestore
props.mute = str and (str == "true") or nil
local str = state_table[key_base .. ":channelVolumes"]
needsRestore = str and true or needsRestore
props.channelVolumes = str and parseArray(str, tonumber) or nil
local str = state_table[key_base .. ":channelMap"]
needsRestore = str and true or needsRestore
props.channelMap = str and parseArray(str) or nil
-- convert arrays to Spa Pod
if props.channelVolumes then
table.insert(props.channelVolumes, 1, "Spa:Float")
props.channelVolumes = Pod.Array(props.channelVolumes)
end
if props.channelMap then
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
props.channelMap = Pod.Array(props.channelMap)
end
if needsRestore then
Log.info(node, "restore values from " .. key_base)
local param = Pod.Object(props)
Log.debug(param, "setting props on " .. tostring(node))
node:set_param("Props", param)
end
end
if config_restore_target and stream_props["state.restore-target"] ~= false then
local str = state_table[key_base .. ":target"]
if str then
restoreTarget(node, str)
end
end
end
if config_restore_target then
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
metadata_om:connect("object-added", function (om, metadata)
-- process existing metadata
for s, k, t, v in metadata:iterate(Id.ANY) do
saveTarget(s, k, t, v)
end
-- and watch for changes
metadata:connect("changed", function (m, subject, key, type, value)
saveTarget(subject, key, type, value)
end)
end)
metadata_om:activate()
end
function handleRouteSettings(subject, key, type, value)
if type ~= "Spa:String:JSON" then
return
end
if string.find(key, "^restore.stream.") == nil then
return
end
if value == nil then
return
end
local json = Json.Raw (value);
if json == nil or not json:is_object () then
return
end
local vparsed = json:parse()
local key_base = string.sub(key, string.len("restore.stream.") + 1)
local str;
key_base = string.gsub(key_base, "%.", ":", 1);
if vparsed.volume ~= nil then
state_table[key_base .. ":volume"] = tostring (vparsed.volume)
end
if vparsed.mute ~= nil then
state_table[key_base .. ":mute"] = tostring (vparsed.mute)
end
if vparsed.channels ~= nil then
state_table[key_base .. ":channelMap"] = serializeArray (vparsed.channels)
end
if vparsed.volumes ~= nil then
state_table[key_base .. ":channelVolumes"] = serializeArray (vparsed.volumes)
end
storeAfterTimeout()
end
rs_metadata = ImplMetadata("route-settings")
rs_metadata:activate(Features.ALL, function (m, e)
if e then
Log.warning("failed to activate route-settings metadata: " .. tostring(e))
return
end
-- copy state into the metadata
moveToMetadata("Output/Audio:media.role:Notification", m)
-- watch for changes
m:connect("changed", function (m, subject, key, type, value)
handleRouteSettings(subject, key, type, value)
end)
end)
allnodes_om = ObjectManager { Interest { type = "node" } }
allnodes_om:activate()
streams_om = ObjectManager {
-- match stream nodes
Interest {
type = "node",
Constraint { "media.class", "matches", "Stream/*", type = "pw-global" },
},
-- and device nodes that are not associated with any routes
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.routes", "is-absent", type = "pw" },
},
Interest {
type = "node",
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
Constraint { "device.routes", "equals", "0", type = "pw" },
},
}
streams_om:connect("object-added", function (streams_om, node)
node:connect("params-changed", saveStream)
restoreStream(node)
end)
streams_om:activate()

View File

@ -0,0 +1,36 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local endpoints_config = ...
function createEndpoint (factory_name, properties)
-- create endpoint
local ep = SessionItem ( factory_name )
if not ep then
Log.warning (ep, "could not create endpoint of type " .. factory_name)
return
end
-- configure endpoint
if not ep:configure(properties) then
Log.warning(ep, "failed to configure endpoint " .. properties.name)
return
end
-- activate and register endpoint
ep:activate (Features.ALL, function (item)
item:register ()
Log.info(item, "registered endpoint " .. properties.name)
end)
end
for name, properties in pairs(endpoints_config) do
properties["name"] = name
createEndpoint ("si-audio-endpoint", properties)
end

View File

@ -0,0 +1,56 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
om = ObjectManager {
Interest { type = "node",
Constraint { "media.class", "matches", "Audio/*" }
},
Interest { type = "node",
Constraint { "media.class", "matches", "Video/*" }
},
}
sources = {}
om:connect("object-added", function (om, node)
node:connect("state-changed", function (node, old_state, cur_state)
-- Always clear the current source if any
local id = node["bound-id"]
if sources[id] then
sources[id]:destroy()
sources[id] = nil
end
-- Add a timeout source if idle for at least 5 seconds
if cur_state == "idle" or cur_state == "error" then
-- honor "session.suspend-timeout-seconds" if specified
local timeout =
tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
if timeout == 0 then
return
end
-- add idle timeout; multiply by 1000, timeout_add() expects ms
sources[id] = Core.timeout_add(timeout * 1000, function()
-- Suspend the node
Log.info(node, "was idle for a while; suspending ...")
node:send_command("Suspend")
-- Unref the source
sources[id] = nil
-- false (== G_SOURCE_REMOVE) destroys the source so that this
-- function does not get fired again after 5 seconds
return false
end)
end
end)
end)
om:activate()

View File

@ -0,0 +1,90 @@
# WirePlumber daemon context configuration #
context.properties = {
## Properties to configure the PipeWire context and some modules
#application.name = WirePlumber
log.level = 2
wireplumber.script-engine = lua-scripting
#wireplumber.export-core = true
#mem.mlock-all = false
#support.dbus = true
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
api.alsa.* = alsa/libspa-alsa
api.bluez5.* = bluez5/libspa-bluez5
api.v4l2.* = v4l2/libspa-v4l2
api.libcamera.* = libcamera/libspa-libcamera
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# PipeWire modules to load.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses RTKit to boost the data thread priority.
{ name = libpipewire-module-rt
args = {
nice.level = -11
#rt.prio = 88
#rt.time.soft = -1
#rt.time.hard = -1
}
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
wireplumber.components = [
#{ name = <component-name>, type = <component-type> }
#
# WirePlumber components to load
#
# The lua scripting engine
{ name = libwireplumber-module-lua-scripting, type = module }
# The lua configuration file(s)
# Other components are loaded from there
{ name = main.lua, type = config/lua }
{ name = policy.lua, type = config/lua }
{ name = bluetooth.lua, type = config/lua }
]

View File

@ -58,3 +58,4 @@ fi
eval "$(starship init zsh)"
sf
if [ -e /home/repo/.nix-profile/etc/profile.d/nix.sh ]; then . /home/repo/.nix-profile/etc/profile.d/nix.sh; fi # added by Nix installer