mirror of
https://github.com/The-Repo-Club/DotFiles.git
synced 2024-11-25 00:38:20 -05:00
Just some configs
Signed-off-by: The-Repo-Club <wayne6324@gmail.com>
This commit is contained in:
parent
a35d05c61e
commit
0e57dc74fc
@ -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\y=#ffffff
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Lighting\Keys\z=#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\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\Name=Rainbow
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\0\Performance\AngleSnap=false
|
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)
|
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\y=#ff0000
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Lighting\Keys\z=#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\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\Name=Breathing
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\1\Performance\AngleSnap=false
|
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)
|
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\y=#aa00ff
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Lighting\Keys\z=#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\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\Name=Trippy
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\2\Performance\AngleSnap=false
|
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)
|
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\CurrentMode={51EB6E3A-27A0-4AD6-A35C-6B67E0329A3D}
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7B5B1E2E81-ED4F-4F79-9EB5-F8ACA67D1BF0%7D\HwModified=7ffd
|
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\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\%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\KeyMap=K68 GB
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Binding\UseRealNames=true
|
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\y=#ffffff
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Lighting\Keys\z=#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\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\Name=Rainbow
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\0\Performance\AngleSnap=false
|
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)
|
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\y=#ff0000
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Lighting\Keys\z=#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\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\Name=Breathing
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\1\Performance\AngleSnap=false
|
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)
|
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\y=#aa00ff
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Lighting\Keys\z=#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\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\Name=Trippy
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\2\Performance\AngleSnap=false
|
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)
|
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\CurrentMode={11C11AE3-3195-4DFC-B8AC-2FEA703414E5}
|
||||||
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\HwModified=40b6f054
|
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\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
|
0E029022AF4C18835CBDCC7EF5001BC3\%7BBA7FC152-2D51-4C26-A7A6-A036CC93D924%7D\Name=Demo
|
||||||
|
|
||||||
[Popups]
|
[Popups]
|
||||||
|
127
clifm/.config/clifm/colors/default.cfm
Normal file
127
clifm/.config/clifm/colors/default.cfm
Normal 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)
|
136
clifm/.config/clifm/keybindings.cfm
Normal file
136
clifm/.config/clifm/keybindings.cfm
Normal 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:
|
1
clifm/.config/clifm/profiles/default/.last
Normal file
1
clifm/.config/clifm/profiles/default/.last
Normal file
@ -0,0 +1 @@
|
|||||||
|
*0:/home/repo/Development/lovesay
|
36
clifm/.config/clifm/profiles/default/actions.cfm
Normal file
36
clifm/.config/clifm/profiles/default/actions.cfm
Normal 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
|
6
clifm/.config/clifm/profiles/default/bookmarks.cfm
Normal file
6
clifm/.config/clifm/profiles/default/bookmarks.cfm
Normal 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
|
273
clifm/.config/clifm/profiles/default/clifmrc
Normal file
273
clifm/.config/clifm/profiles/default/clifmrc
Normal 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
|
7
clifm/.config/clifm/profiles/default/dirhist.cfm
Normal file
7
clifm/.config/clifm/profiles/default/dirhist.cfm
Normal 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
|
2
clifm/.config/clifm/profiles/default/history.cfm
Normal file
2
clifm/.config/clifm/profiles/default/history.cfm
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
edit
|
||||||
|
echo $EDITOR
|
4
clifm/.config/clifm/profiles/default/jump.cfm
Normal file
4
clifm/.config/clifm/profiles/default/jump.cfm
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
2:1660715888:1660716582:/home/repo
|
||||||
|
3:1660716456:1660716585:/home/repo/Development
|
||||||
|
2:1660716460:1660716586:/home/repo/Development/lovesay
|
||||||
|
@3100
|
0
clifm/.config/clifm/profiles/default/log.cfm
Normal file
0
clifm/.config/clifm/profiles/default/log.cfm
Normal file
1
clifm/.config/clifm/profiles/default/messages.cfm
Normal file
1
clifm/.config/clifm/profiles/default/messages.cfm
Normal file
@ -0,0 +1 @@
|
|||||||
|
[2022-8-17T6:58:8] [1mNOTE[0m: 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
|
119
clifm/.config/clifm/profiles/default/mimelist.cfm
Normal file
119
clifm/.config/clifm/profiles/default/mimelist.cfm
Normal 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;
|
62
clifm/.config/clifm/profiles/default/nets.cfm
Normal file
62
clifm/.config/clifm/profiles/default/nets.cfm
Normal 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
|
9
clifm/.config/clifm/profiles/default/profile.cfm
Normal file
9
clifm/.config/clifm/profiles/default/profile.cfm
Normal 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
|
170
clifm/.config/clifm/prompts.cfm
Normal file
170
clifm/.config/clifm/prompts.cfm
Normal 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\]"
|
78
clifm/.config/clifm/readline.cfm
Normal file
78
clifm/.config/clifm/readline.cfm
Normal 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.
@ -1,30 +1,30 @@
|
|||||||
# Unlock user from passwords
|
# Unlock user from passwords
|
||||||
function ulock
|
function ulock
|
||||||
faillock --reset
|
command faillock --reset
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check ports for current user
|
# Check ports for current user
|
||||||
function ports
|
function ports
|
||||||
sudo netstat -tulanp
|
command sudo netstat -tulanp
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set permissions for user
|
# Set permissions for user
|
||||||
function setperm
|
function setperm
|
||||||
sudo chown dt:dt $argv
|
command sudo chown repo:repo $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
# Stow commands
|
# Stow commands
|
||||||
function stowadd
|
function stowadd
|
||||||
stow -St ~ $argv
|
command stow -St ~ $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
function stowremove
|
function stowremove
|
||||||
stow -Dt ~ $argv
|
command stow -Dt ~ $argv
|
||||||
end
|
end
|
||||||
|
|
||||||
# Clear command
|
# Clear command
|
||||||
function clear
|
function clear
|
||||||
command reset && fish
|
command reset && shellfetch
|
||||||
end
|
end
|
||||||
|
|
||||||
# free
|
# free
|
||||||
@ -39,5 +39,14 @@ end
|
|||||||
|
|
||||||
# grub update
|
# grub update
|
||||||
function update-grub
|
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
|
end
|
||||||
|
@ -98,7 +98,3 @@ end
|
|||||||
function gall
|
function gall
|
||||||
gpull && gadd . && gcommit -s && gpush
|
gpull && gadd . && gcommit -s && gpush
|
||||||
end
|
end
|
||||||
|
|
||||||
function config
|
|
||||||
git --git-dir=/mnt/500GB/.gitlabs/newDotFiles --work-tree=$HOME $argv
|
|
||||||
end
|
|
||||||
|
@ -17,34 +17,34 @@ function pacman
|
|||||||
command sudo pacman --color auto $argv
|
command sudo pacman --color auto $argv
|
||||||
else
|
else
|
||||||
if pacman -Qttdq
|
if pacman -Qttdq
|
||||||
pacman -Qttdq | pacman -Rns -
|
command sudo pacman -Qttdq | command sudo pacman -Rns -
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update Repo
|
# Update Repo
|
||||||
function update
|
function update
|
||||||
pacman -Syu
|
command pacman -Syu
|
||||||
end
|
end
|
||||||
|
|
||||||
function aurupdate
|
function aurupdate
|
||||||
auracle update -C ~/.cache/pkgs/
|
command auracle update -C ~/.cache/pkgs/
|
||||||
end
|
end
|
||||||
|
|
||||||
function upall
|
function upall
|
||||||
pacman -Fy && pacman -Syu --noconfirm && aurupdate
|
command pacman -Fy && pacman -Syu --noconfirm && aurupdate
|
||||||
end
|
end
|
||||||
|
|
||||||
#check aur and arch packages
|
#check aur and arch packages
|
||||||
function checkarch
|
function checkarch
|
||||||
pacman -Qqen >~/package_list.txt
|
command pacman -Qqen >~/package_list.txt
|
||||||
end
|
end
|
||||||
|
|
||||||
function checkaur
|
function checkaur
|
||||||
pacman -Qqem >~/package_list_aur.txt
|
command pacman -Qqem >~/package_list_aur.txt
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pacman unlock
|
# Pacman unlock
|
||||||
function unlock
|
function unlock
|
||||||
sudo rm /var/lib/pacman/db.lck
|
command sudo rm /var/lib/pacman/db.lck
|
||||||
end
|
end
|
||||||
|
3
fish/.config/fish/aliases/ssh.fish
Normal file
3
fish/.config/fish/aliases/ssh.fish
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
function chris
|
||||||
|
ssh linknsync@51.89.161.207
|
||||||
|
end
|
@ -1,5 +1,5 @@
|
|||||||
if test -f "$HOME/.confid/fish/fish.profile"
|
if test -f "$HOME/.config/fish/fish.profile"
|
||||||
source "$HOME/.confid/fish/fish.profile"
|
source "$HOME/.config/fish/fish.profile"
|
||||||
end
|
end
|
||||||
|
|
||||||
set PATH "$HOME/.local/bin:$PATH"
|
set PATH "$HOME/.local/bin:$PATH"
|
||||||
@ -35,7 +35,6 @@ if test -d "$HOME/.local/bin/clipmenu"
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fish_greeting
|
function fish_greeting
|
||||||
# bfetch --source ~/.config/bfetch/ascii.art --ascii_colors 7 1 2 3 5 8 --birthday 16/06
|
|
||||||
shellfetch
|
shellfetch
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -52,8 +51,8 @@ end
|
|||||||
bind \ec __history_previous_command
|
bind \ec __history_previous_command
|
||||||
bind \e\e __sudope
|
bind \e\e __sudope
|
||||||
|
|
||||||
if status is-login
|
# if status is-login
|
||||||
if test (tty) = /dev/tty1
|
# if test (tty) = /dev/tty1
|
||||||
exec tbsm
|
# exec tbsm
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
4
fish/.config/fish/functions/__sync_history.fish
Normal file
4
fish/.config/fish/functions/__sync_history.fish
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
function sync_history --on-event fish_preexec
|
||||||
|
history --save
|
||||||
|
history --merge
|
||||||
|
end
|
Binary file not shown.
85
wireplumber/.config/wireplumber/bluetooth.conf
Normal file
85
wireplumber/.config/wireplumber/bluetooth.conf
Normal 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 }
|
||||||
|
]
|
@ -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
|
@ -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
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
bluez_monitor.enable()
|
36
wireplumber/.config/wireplumber/common/00-functions.lua
Normal file
36
wireplumber/.config/wireplumber/common/00-functions.lua
Normal 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
|
74
wireplumber/.config/wireplumber/main.conf
Normal file
74
wireplumber/.config/wireplumber/main.conf
Normal 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 }
|
||||||
|
]
|
36
wireplumber/.config/wireplumber/main.lua.d/00-functions.lua
Normal file
36
wireplumber/.config/wireplumber/main.lua.d/00-functions.lua
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
146
wireplumber/.config/wireplumber/main.lua.d/50-alsa-config.lua
Normal file
146
wireplumber/.config/wireplumber/main.lua.d/50-alsa-config.lua
Normal 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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
23
wireplumber/.config/wireplumber/main.lua.d/90-enable-all.lua
Normal file
23
wireplumber/.config/wireplumber/main.lua.d/90-enable-all.lua
Normal 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")
|
73
wireplumber/.config/wireplumber/policy.conf
Normal file
73
wireplumber/.config/wireplumber/policy.conf
Normal 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 }
|
||||||
|
]
|
@ -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
|
@ -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
|
@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]]--
|
@ -0,0 +1 @@
|
|||||||
|
default_policy.enable()
|
@ -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()
|
134
wireplumber/.config/wireplumber/scripts/access/access-portal.lua
Normal file
134
wireplumber/.config/wireplumber/scripts/access/access-portal.lua
Normal 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()
|
129
wireplumber/.config/wireplumber/scripts/create-item.lua
Normal file
129
wireplumber/.config/wireplumber/scripts/create-item.lua
Normal 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()
|
93
wireplumber/.config/wireplumber/scripts/fallback-sink.lua
Normal file
93
wireplumber/.config/wireplumber/scripts/fallback-sink.lua
Normal 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()
|
74
wireplumber/.config/wireplumber/scripts/intended-roles.lua
Normal file
74
wireplumber/.config/wireplumber/scripts/intended-roles.lua
Normal 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()
|
@ -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
|
412
wireplumber/.config/wireplumber/scripts/monitors/alsa.lua
Normal file
412
wireplumber/.config/wireplumber/scripts/monitors/alsa.lua
Normal 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()
|
193
wireplumber/.config/wireplumber/scripts/monitors/bluez.lua
Normal file
193
wireplumber/.config/wireplumber/scripts/monitors/bluez.lua
Normal 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
|
169
wireplumber/.config/wireplumber/scripts/monitors/libcamera.lua
Normal file
169
wireplumber/.config/wireplumber/scripts/monitors/libcamera.lua
Normal 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
|
159
wireplumber/.config/wireplumber/scripts/monitors/v4l2.lua
Normal file
159
wireplumber/.config/wireplumber/scripts/monitors/v4l2.lua
Normal 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
|
398
wireplumber/.config/wireplumber/scripts/policy-bluetooth.lua
Normal file
398
wireplumber/.config/wireplumber/scripts/policy-bluetooth.lua
Normal 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()
|
@ -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()
|
477
wireplumber/.config/wireplumber/scripts/policy-device-routes.lua
Normal file
477
wireplumber/.config/wireplumber/scripts/policy-device-routes.lua
Normal 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()
|
@ -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()
|
@ -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()
|
@ -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()
|
981
wireplumber/.config/wireplumber/scripts/policy-node.lua
Normal file
981
wireplumber/.config/wireplumber/scripts/policy-node.lua
Normal 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()
|
462
wireplumber/.config/wireplumber/scripts/restore-stream.lua
Normal file
462
wireplumber/.config/wireplumber/scripts/restore-stream.lua
Normal 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()
|
36
wireplumber/.config/wireplumber/scripts/static-endpoints.lua
Normal file
36
wireplumber/.config/wireplumber/scripts/static-endpoints.lua
Normal 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
|
56
wireplumber/.config/wireplumber/scripts/suspend-node.lua
Normal file
56
wireplumber/.config/wireplumber/scripts/suspend-node.lua
Normal 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()
|
90
wireplumber/.config/wireplumber/wireplumber.conf
Normal file
90
wireplumber/.config/wireplumber/wireplumber.conf
Normal 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 }
|
||||||
|
]
|
@ -58,3 +58,4 @@ fi
|
|||||||
|
|
||||||
eval "$(starship init zsh)"
|
eval "$(starship init zsh)"
|
||||||
sf
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user