pax_global_header00006660000000000000000000000064151221714700014512gustar00rootroot0000000000000052 comment=4f0f67bb424e2a1b4852738ffc13194651185f56 pipemixer-0.4.0/000077500000000000000000000000001512217147000135155ustar00rootroot00000000000000pipemixer-0.4.0/.gitignore000066400000000000000000000000311512217147000154770ustar00rootroot00000000000000*.log .cache/ src/debug/ pipemixer-0.4.0/COPYING000066400000000000000000001045151512217147000145560ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . pipemixer-0.4.0/README.md000066400000000000000000000023221512217147000147730ustar00rootroot00000000000000# pipemixer This is a TUI volume control application for [pipewire] built with [ncurses]. Heavily inspired by [pulsemixer] and [pwvucontrol]. ![Screenshot](screenshot.png) ## Building ``` git clone https://github.com/heather7283/pipemixer cd pipemixer meson setup build meson compile -C build ``` ## Running ``` pipemixer -h ``` To debug: ``` pipemixer -l trace -L 4 4>pipemixer.log ``` With valgrind: ``` valgrind --leak-check=full --show-leak-kinds=all --track-fds=yes --log-fd=5 -- pipemixer -l trace -L 4 4>pipemixer.log 5>valgrind.log ``` ## Config pipemixer reads its config from `$XDG_CONFIG_HOME/pipemixer/pipemixer.ini`. See [example config](pipemixer.ini) and pipemixer.ini(5) for details. ## References - https://docs.pipewire.org - https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/src/tools - https://github.com/saivert/pwvucontrol - https://github.com/quickshell-mirror/quickshell/tree/master/src/services/pipewire - https://invisible-island.net/ncurses - https://tldp.org/HOWTO/NCURSES-Programming-HOWTO [pipewire]: https://pipewire.org/ [pulsemixer]: https://github.com/GeorgeFilipkin/pulsemixer [pwvucontrol]: https://github.com/saivert/pwvucontrol [ncurses]: https://invisible-island.net/ncurses pipemixer-0.4.0/com.github.pipemixer.desktop000066400000000000000000000003001512217147000211410ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=pipemixer GenericName=PipeWire Volume Control Exec=pipemixer Categories=Audio;AudioVideo Keywords=PipeWire;Volume;Audio;Mixer;Control;Sound Terminal=true pipemixer-0.4.0/completion/000077500000000000000000000000001512217147000156665ustar00rootroot00000000000000pipemixer-0.4.0/completion/_pipemixer000066400000000000000000000010361512217147000177520ustar00rootroot00000000000000#compdef pipemixer local -a arguments arguments=( '(-c --config)'{-c,--config=}'[path to configuration file]: :_files' '(-l --loglevel)'{-l,--loglevel=}'[set loglevel]: :((TRACE DEBUG INFO WARN ERROR QUIET))' '(-L --log-fd)'{-L,--log-fd=}'[write log to this fd]:file descriptor:_file_descriptors' '(-C --color)'{-C,--color}'[force logging with colors]' '(- :)'{-h,--help}'[print this help message and exit]' '(- :)'{-V,--version}'[print version information]' ) _arguments "${arguments[@]}" pipemixer-0.4.0/lib/000077500000000000000000000000001512217147000142635ustar00rootroot00000000000000pipemixer-0.4.0/lib/pollen/000077500000000000000000000000001512217147000155545ustar00rootroot00000000000000pipemixer-0.4.0/lib/pollen/LICENSE000066400000000000000000000020551512217147000165630ustar00rootroot00000000000000MIT License Copyright (c) 2025 heather7283 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pipemixer-0.4.0/lib/pollen/pollen.h000066400000000000000000001156141512217147000172260ustar00rootroot00000000000000/* * pollen version 3.1.0 * latest version is available at: https://github.com/heather7283/pollen * * This is a single-header library that provides simple event loop abstraction built on epoll. * To use this library, do this in one C file: * #define POLLEN_IMPLEMENTATION * #include "pollen.h" * * COMPILE-TIME TUNABLES: * POLLEN_EPOLL_MAX_EVENTS - Maximum amount of events processed during one loop iteration. * Default: #define POLLEN_EPOLL_MAX_EVENTS 32 * * POLLEN_CALLOC(n, size) - calloc()-like function that will be used to allocate memory. * Default: #define POLLEN_CALLOC(n, size) calloc(n, size) * POLLEN_FREE(ptr) - free()-like function that will be used to free memory. * Default: #define POLLEN_FREE(ptr) free(ptr) * * Following macros will, if defined, be used for logging. * They must expand to printf()-like function, for example: * #define POLLEN_LOG_DEBUG(fmt, ...) fprintf(stderr, "event loop: " fmt "\n", ##__VA_ARGS__) * POLLEN_LOG_DEBUG(fmt, ...) * POLLEN_LOG_INFO(fmt, ...) * POLLEN_LOG_WARN(fmt, ...) * POLLEN_LOG_ERR(fmt, ...) */ /* ONLY UNCOMMENT THIS TO GET SYNTAX HIGHLIGHTING, DONT FORGET TO COMMENT IT BACK #define POLLEN_IMPLEMENTATION //*/ #ifndef POLLEN_H #define POLLEN_H #if !defined(POLLEN_EPOLL_MAX_EVENTS) #define POLLEN_EPOLL_MAX_EVENTS 32 #endif #if !defined(POLLEN_CALLOC) || !defined(POLLEN_FREE) #include #endif #if !defined(POLLEN_CALLOC) #define POLLEN_CALLOC(n, size) calloc(n, size) #endif #if !defined(POLLEN_FREE) #define POLLEN_FREE(ptr) free(ptr) #endif #if !defined(POLLEN_LOG_DEBUG) #define POLLEN_LOG_DEBUG(...) (void)(#__VA_ARGS__) #endif #if !defined(POLLEN_LOG_INFO) #define POLLEN_LOG_INFO(...) (void)(#__VA_ARGS__) #endif #if !defined(POLLEN_LOG_WARN) #define POLLEN_LOG_WARN(...) (void)(#__VA_ARGS__) #endif #if !defined(POLLEN_LOG_ERR) #define POLLEN_LOG_ERR(...) (void)(#__VA_ARGS__) #endif #include #include #include #include #include /* I suck at naming and it took me too long to find a suitable name for this */ struct pollen_event_source; /* Ugly but ensures backwards compatibility */ #define pollen_callback pollen_event_source typedef int (*pollen_fd_callback_fn)(struct pollen_event_source *event_source, int fd, uint32_t events, void *data); typedef int (*pollen_idle_callback_fn)(struct pollen_event_source *event_source, void *data); typedef int (*pollen_signal_callback_fn)(struct pollen_event_source *event_source, int signum, void *data); typedef int (*pollen_timer_callback_fn)(struct pollen_event_source *event_source, void *data); typedef int (*pollen_efd_callback_fn)(struct pollen_event_source *event_source, uint64_t val, void *data); /* Creates a new pollen_loop instance. Returns NULL and sets errno on failure. */ struct pollen_loop *pollen_loop_create(void); /* Frees all resources associated with the loop. Passing NULL is a harmless no-op. */ void pollen_loop_cleanup(struct pollen_loop *loop); /* * Adds fd to epoll interest list. * Argument events directly corresponts to epoll_event.events field, see epoll_ctl(2). * If autoclose is true, the fd will be closed when pollen_event_source_remove is called. * * Returns NULL and sets errno on failure. */ struct pollen_event_source *pollen_loop_add_fd(struct pollen_loop *loop, int fd, uint32_t events, bool autoclose, pollen_fd_callback_fn callback_fn, void *data); /* * Modifies fd event source by calling epoll_ctl(2) with EPOLL_CTL_MOD. * Argument new_events directly corresponds to epoll_event.events field. * * Sets errno and returns false on failure, true on success. */ bool pollen_fd_modify_events(struct pollen_event_source *event_source, uint32_t new_events); /* * Adds a callback that will run unconditionally on every event loop iteration, * after all other callback types were processed. * Callbacks with higher priority will run before callbacks with lower priority. * If two callbacks have equal priority, the order is undefined. * * Returns NULL and sets errno on failure. */ struct pollen_event_source *pollen_loop_add_idle(struct pollen_loop *loop, int priority, pollen_idle_callback_fn callback, void *data); /* * Adds a callback that will run when signal is caught. * This function tries to preserve original sigmask if it fails. * * Returns NULL and sets errno on failure. */ struct pollen_event_source *pollen_loop_add_signal(struct pollen_loop *loop, int signal, pollen_signal_callback_fn callback, void *data); /* * Adds a timerfd-based timer callback. * Arm/disarm the timer with pollen_timer_arm/disarm functions. * See timerfd_create(2) for description of clockid argument. * * Returns NULL and sets errno on failure. */ struct pollen_event_source *pollen_loop_add_timer(struct pollen_loop *loop, int clockid, pollen_timer_callback_fn callback, void *data); /* * Arms the timer to expire once after initial timespec, * and then repeatedly every periodic timespec. * If absolute is true, initial is an absolute value instead of relative. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_arm(struct pollen_event_source *event_source, bool absolute, struct timespec initial, struct timespec periodic); /* * Arms the timer to expire once after initial_s seconds, * and then repeatedly every periodic_s seconds. * If absolute is true, initial_s is an absolute value instead of relative. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_arm_s(struct pollen_event_source *event_source, bool absolute, unsigned long initial_s, unsigned long periodic_s); /* * Arms the timer to expire once after initial_ms milliseconds, * and then repeatedly every periodic_ms milliseconds. * If absolute is true, initial_ms is an absolute value instead of relative. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_arm_ms(struct pollen_event_source *event_source, bool absolute, unsigned long initial_ms, unsigned long periodic_ms); /* * Arms the timer to expire once after initial_us microseconds, * and then repeatedly every periodic_us microseconds. * If absolute is true, initial_us is an absolute value instead of relative. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_arm_us(struct pollen_event_source *event_source, bool absolute, unsigned long initial_us, unsigned long periodic_us); /* * Arms the timer to expire once after initial_ns nanoseconds, * and then repeatedly every periodic_ns nanoseconds. * If absolute is true, initial_ns is an absolute value instead of relative. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_arm_ns(struct pollen_event_source *event_source, bool absolute, unsigned long initial_ns, unsigned long periodic_ns); /* * Disarms the timer. * * Sets errno and returns false on failre, true on success. */ bool pollen_timer_disarm(struct pollen_event_source *event_source); /* * This is a convenience wrapper around eventfd(2). * Use pollen_efd_trigger() to increment the efd and cause the callback to run. * The efd will be automatically reset before running the callback. * * Returns NULL and sets errno on failure. */ struct pollen_event_source *pollen_loop_add_efd(struct pollen_loop *loop, pollen_efd_callback_fn callback, void *data); /* * Increment efd by 1, causing the callback to run on the next loop iteration. * Callback must have been created by a call to pollen_loop_add_efd(). * * Returns true on success, false on failure and sets errno. */ bool pollen_efd_trigger(struct pollen_event_source *event_source); /* * Increment efd by n, causing the callback to run on the next loop iteration. * Callback must have been created by a call to pollen_loop_add_efd(). * * Returns true on success, false on failure and sets errno. */ bool pollen_efd_inc(struct pollen_event_source *event_source, uint64_t n); /* * Remove an event source from event loop. * * For fd event sources, this function will close the fd if autoclose=true. * For signal event sources, this function will unblock the signal. * * Passing NULL is a harmless no-op. */ void pollen_event_source_remove(struct pollen_event_source *event_source); /* for backwards compatibility */ #define pollen_loop_remove_callback pollen_event_source_remove /* Get pollen_loop instance associated with this pollen_event_source. */ struct pollen_loop *pollen_event_source_get_loop(struct pollen_event_source *callback); /* for backwards compatibility */ #define pollen_callback_get_loop pollen_event_source_get_loop /* * Run the event loop. This function blocks until event loop exits. * This function returns 0 if no errors occured. * If any of the callbacks return negative value, * the event loop with be stopped and this value returned. */ int pollen_loop_run(struct pollen_loop *loop); /* * Quit the event loop. * Argument retcode specifies the value that will be returned by pollen_loop_run. */ void pollen_loop_quit(struct pollen_loop *loop, int retcode); #endif /* #ifndef POLLEN_H */ /* * ============================================================================ * IMPLEMENTATION * ============================================================================ */ #ifdef POLLEN_IMPLEMENTATION #include #include #include #include #include #include #include #include #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L #define POLLEN_TYPEOF(expr) typeof(expr) #else #define POLLEN_TYPEOF(expr) __typeof__(expr) #endif #define POLLEN_CONTAINER_OF(ptr, sample, member) \ (POLLEN_TYPEOF(sample))((char *)(ptr) - offsetof(POLLEN_TYPEOF(*sample), member)) /* * Linked list. * In the head, next points to the first list elem, prev points to the last. * In the list element, next points to the next elem, prev points to the previous elem. * In the last element, next points to the head. In the first element, prev points to the head. * If the list is empty, next and prev point to the head itself. */ struct pollen_ll { struct pollen_ll *next; struct pollen_ll *prev; }; static inline void pollen_ll_init(struct pollen_ll *head) { head->next = head; head->prev = head; } static inline bool pollen_ll_is_empty(struct pollen_ll *head) { return head->next == head && head->prev == head; } /* Inserts new after elem. */ static inline void pollen_ll_insert(struct pollen_ll *elem, struct pollen_ll *new) { elem->next->prev = new; new->next = elem->next; elem->next = new; new->prev = elem; } static inline void pollen_ll_remove(struct pollen_ll *elem) { elem->prev->next = elem->next; elem->next->prev = elem->prev; } #define POLLEN_LL_FOR_EACH_REVERSE(var, head, member) \ for (var = POLLEN_CONTAINER_OF((head)->prev, var, member); \ &var->member != (head); \ var = POLLEN_CONTAINER_OF(var->member.prev, var, member)) #define POLLEN_LL_FOR_EACH_SAFE(var, tmp, head, member) \ for (var = POLLEN_CONTAINER_OF((head)->next, var, member), \ tmp = POLLEN_CONTAINER_OF((var)->member.next, tmp, member); \ &var->member != (head); \ var = tmp, \ tmp = POLLEN_CONTAINER_OF(var->member.next, tmp, member)) enum pollen_event_source_type { POLLEN_EVENT_SOURCE_TYPE_FD, POLLEN_EVENT_SOURCE_TYPE_IDLE, POLLEN_EVENT_SOURCE_TYPE_SIGNAL, POLLEN_EVENT_SOURCE_TYPE_TIMER, POLLEN_EVENT_SOURCE_TYPE_EFD, }; struct pollen_event_source { struct pollen_loop *loop; enum pollen_event_source_type type; union { struct { int fd; pollen_fd_callback_fn callback; bool autoclose; } fd; struct { int priority; pollen_idle_callback_fn callback; } idle; struct { int sig; pollen_signal_callback_fn callback; } signal; struct { int fd; pollen_timer_callback_fn callback; } timer; struct { int efd; pollen_efd_callback_fn callback; } efd; } as; void *data; struct pollen_ll link; }; struct pollen_loop { bool should_quit; int retcode; int epoll_fd; /* Cannot do signal_sources[SIGRTMAX] here * because SIGRTMAX is not a compile-time constant. * TODO: this is cringe. Use something like a hashmap? */ struct pollen_event_source **signal_sources; int signal_fd; sigset_t sigset; struct pollen_ll sources; struct pollen_ll idle_sources; }; /* a hack to hook signal handling into the loop */ static int pollen_internal_signal_handler(struct pollen_event_source *event_source, int fd, uint32_t events, void *data) { struct pollen_loop *loop = data; /* TODO: figure out why does this always only read only one siginfo */ int ret; struct signalfd_siginfo siginfo; while ((ret = read(loop->signal_fd, &siginfo, sizeof(siginfo))) == sizeof(siginfo)) { int signal = siginfo.ssi_signo; POLLEN_LOG_DEBUG("received signal %d via signalfd", signal); struct pollen_event_source *signal_callback = loop->signal_sources[signal]; if (signal_callback != NULL) { return signal_callback->as.signal.callback(signal_callback, signal, signal_callback->data); } else { POLLEN_LOG_ERR("signal %d received via signalfd has no callbacks installed", signal); return -1; } } if (ret >= 0) { POLLEN_LOG_ERR("read incorrect amount of bytes from signalfd"); return -1; } else /* ret < 0 */ { if (errno == EAGAIN || errno == EWOULDBLOCK) { /* no more signalds to handle. exit. */ POLLEN_LOG_DEBUG("no more signals to handle"); return 0; } else { POLLEN_LOG_ERR("failed to read siginfo from signalfd: %s", strerror(errno)); return -1; } } } static int pollen_internal_setup_signalfd(struct pollen_loop *loop) { int save_errno = 0; POLLEN_LOG_DEBUG("setting up signalfd"); loop->signal_sources = POLLEN_CALLOC(SIGRTMAX + 1, sizeof(loop->signal_sources[0])); if (loop->signal_sources == NULL) { POLLEN_LOG_ERR("failed to allocate memory for signal sources array: %s", strerror(errno)); goto err; } sigemptyset(&loop->sigset); loop->signal_fd = signalfd(-1, &loop->sigset, SFD_NONBLOCK | SFD_CLOEXEC); if (loop->signal_fd < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to create signalfd: %s", strerror(errno)); goto err; } if (pollen_loop_add_fd(loop, loop->signal_fd, EPOLLIN, false, pollen_internal_signal_handler, loop) == NULL) { save_errno = errno; goto err; } return 0; err: free(loop->signal_sources); errno = save_errno; return -1; } struct pollen_loop *pollen_loop_create(void) { POLLEN_LOG_INFO("creating event loop"); int save_errno = 0; struct pollen_loop *loop = POLLEN_CALLOC(1, sizeof(*loop)); if (loop == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event loop: %s", strerror(errno)); goto err; } pollen_ll_init(&loop->sources); pollen_ll_init(&loop->idle_sources); loop->epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (loop->epoll_fd < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to create epoll: %s", strerror(errno)); goto err; } /* signalfd will be set up when first signal callback is added */ loop->signal_fd = -1; return loop; err: POLLEN_FREE(loop); errno = save_errno; return NULL; } void pollen_loop_cleanup(struct pollen_loop *loop) { if (loop == NULL) { return; } POLLEN_LOG_INFO("cleaning up event loop"); struct pollen_event_source *source, *source_tmp; POLLEN_LL_FOR_EACH_SAFE(source, source_tmp, &loop->sources, link) { pollen_event_source_remove(source); } POLLEN_LL_FOR_EACH_SAFE(source, source_tmp, &loop->idle_sources, link) { pollen_event_source_remove(source); } if (loop->signal_fd > 0) { close(loop->signal_fd); } free(loop->signal_sources); close(loop->epoll_fd); POLLEN_FREE(loop); } struct pollen_event_source *pollen_loop_add_fd(struct pollen_loop *loop, int fd, uint32_t events, bool autoclose, pollen_fd_callback_fn callback, void *data) { struct pollen_event_source *new_source = NULL; int save_errno = 0; POLLEN_LOG_INFO("adding fd source to event loop, fd %d, events %X", fd, events); new_source = POLLEN_CALLOC(1, sizeof(*new_source)); if (new_source == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event source: %s", strerror(errno)); goto err; } new_source->loop = loop; new_source->type = POLLEN_EVENT_SOURCE_TYPE_FD; new_source->as.fd.fd = fd; new_source->as.fd.callback = callback; new_source->as.fd.autoclose = autoclose; new_source->data = data; struct epoll_event epoll_event; epoll_event.events = events; epoll_event.data.ptr = new_source; if (epoll_ctl(loop->epoll_fd, EPOLL_CTL_ADD, fd, &epoll_event) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to add fd %d to epoll: %s", fd, strerror(errno)); goto err; } pollen_ll_insert(&loop->sources, &new_source->link); return new_source; err: POLLEN_FREE(new_source); errno = save_errno; return NULL; } bool pollen_fd_modify_events(struct pollen_event_source *source, uint32_t new_events) { int save_errno; if (source->type != POLLEN_EVENT_SOURCE_TYPE_FD) { POLLEN_LOG_ERR("passed non-fd type source to pollen_fd_modify_events"); save_errno = EINVAL; goto err; } POLLEN_LOG_DEBUG("modifying events for fd %d, new_events: %d", source->as.fd.fd, new_events); struct epoll_event ev; ev.data.ptr = source; ev.events = new_events; if (epoll_ctl(source->loop->epoll_fd, EPOLL_CTL_MOD, source->as.fd.fd, &ev) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to modify events for fd %d: %s", source->as.fd.fd, strerror(errno)); goto err; } return true; err: errno = save_errno; return false; } struct pollen_event_source *pollen_loop_add_idle(struct pollen_loop *loop, int priority, pollen_idle_callback_fn callback, void *data) { struct pollen_event_source *new_source = NULL; int save_errno = 0; POLLEN_LOG_INFO("adding idle source with prio %d to event loop", priority); new_source = POLLEN_CALLOC(1, sizeof(*new_source)); if (new_source == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event source: %s", strerror(errno)); goto err; } new_source->loop = loop; new_source->type = POLLEN_EVENT_SOURCE_TYPE_IDLE; new_source->as.idle.priority = priority; new_source->as.idle.callback = callback; new_source->data = data; if (pollen_ll_is_empty(&loop->idle_sources)) { pollen_ll_insert(&loop->idle_sources, &new_source->link); } else { struct pollen_event_source *elem; bool found = false; POLLEN_LL_FOR_EACH_REVERSE(elem, &loop->idle_sources, link) { /* |6| * |9| |8|\/|4| |2| * <----------------- * iterate from the end and find the first callback with higher prio */ if (elem->as.idle.priority > priority) { found = true; pollen_ll_insert(&elem->link, &new_source->link); break; } } if (!found) { pollen_ll_insert(&loop->idle_sources, &new_source->link); } } return new_source; err: POLLEN_FREE(new_source); errno = save_errno; return NULL; } struct pollen_event_source *pollen_loop_add_signal(struct pollen_loop *loop, int signal, pollen_signal_callback_fn callback, void *data) { struct pollen_event_source *new_source = NULL; int save_errno = 0; bool sigset_saved = false; sigset_t save_global_sigset; sigset_t save_loop_sigset = loop->sigset; bool need_reset_handler = false; POLLEN_LOG_INFO("adding signal source for signal %d", signal); if (loop->signal_fd < 0 && pollen_internal_setup_signalfd(loop) < 0) { goto err; } if (sigprocmask(SIG_BLOCK /* ignored */, NULL, &save_global_sigset) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to save original sigmask: %s", strerror(errno)); goto err; } sigset_saved = true; new_source = POLLEN_CALLOC(1, sizeof(*new_source)); if (new_source == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event source: %s", strerror(errno)); goto err; } new_source->loop = loop; new_source->type = POLLEN_EVENT_SOURCE_TYPE_SIGNAL; new_source->as.signal.sig = signal; new_source->as.signal.callback = callback; new_source->data = data; /* first, create empty sigset and add our desired signal there. */ sigset_t set; sigemptyset(&set); if (sigaddset(&set, signal) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to add signal %d to sigset: %s", signal, strerror(errno)); goto err; } /* block the desired signal globally. */ if (sigprocmask(SIG_BLOCK, &set, NULL) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to block signal %d: %s", signal, strerror(errno)); goto err; } /* on success, add the same signal to loop's sigset. */ if (sigaddset(&loop->sigset, signal) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to add signal %d to loop sigset: %s", signal, strerror(errno)); goto err; } /* check if handler for this signal already exists */ if (loop->signal_sources[signal] != NULL) { POLLEN_LOG_ERR("source for signal %d already exists", signal); save_errno = EEXIST; goto err; } loop->signal_sources[signal] = new_source; need_reset_handler = true; /* change signalfd mask to report newly added signal */ int ret = signalfd(loop->signal_fd, &loop->sigset, 0); if (ret < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to change signalfd sigmask: %s", strerror(errno)); goto err; } pollen_ll_insert(&loop->sources, &new_source->link); return new_source; err: /* restore original sigmask on failure. important! */ if (sigset_saved) { if (sigprocmask(SIG_SETMASK, &save_global_sigset, NULL) < 0) { POLLEN_LOG_WARN("failed to restore original signal mask! %s", strerror(errno)); } } loop->sigset = save_loop_sigset; if (need_reset_handler) { loop->signal_sources[signal] = NULL; } POLLEN_FREE(new_source); errno = save_errno; return NULL; } struct pollen_event_source *pollen_loop_add_timer(struct pollen_loop *loop, int clockid, pollen_timer_callback_fn callback, void *data) { struct pollen_event_source *new_source = NULL; int save_errno = 0; int tfd = -1; POLLEN_LOG_INFO("adding timer source to event loop, clockid %d", clockid); tfd = timerfd_create(clockid, TFD_NONBLOCK | TFD_CLOEXEC); if (tfd < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to create timerfd: %s", strerror(errno)); goto err; } new_source = POLLEN_CALLOC(1, sizeof(*new_source)); if (new_source == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event source: %s", strerror(errno)); goto err; } new_source->loop = loop; new_source->type = POLLEN_EVENT_SOURCE_TYPE_TIMER; new_source->as.timer.fd = tfd; new_source->as.timer.callback = callback; new_source->data = data; struct epoll_event epoll_event; epoll_event.events = EPOLLIN; epoll_event.data.ptr = new_source; if (epoll_ctl(loop->epoll_fd, EPOLL_CTL_ADD, tfd, &epoll_event) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to add fd %d to epoll: %s", tfd, strerror(errno)); goto err; } pollen_ll_insert(&loop->sources, &new_source->link); return new_source; err: if (tfd > 0) { close(tfd); } POLLEN_FREE(new_source); errno = save_errno; return NULL; } bool pollen_timer_arm(struct pollen_event_source *source, bool absolute, struct timespec initial, struct timespec periodic) { int save_errno = 0; if (source->type != POLLEN_EVENT_SOURCE_TYPE_TIMER) { POLLEN_LOG_ERR("passed non-timer type source to pollen_timer_arm"); save_errno = EINVAL; goto err; } POLLEN_LOG_DEBUG("arming timerfd %d for (%li s %li ns) initial, (%li s %li ns) periodic", source->as.timer.fd, initial.tv_sec, initial.tv_nsec, periodic.tv_sec, periodic.tv_nsec); const struct itimerspec itimerspec = { .it_value = initial, .it_interval = periodic, }; const int flags = absolute ? TFD_TIMER_ABSTIME : 0; if (timerfd_settime(source->as.timer.fd, flags, &itimerspec, NULL) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to arm timer: %s", strerror(errno)); goto err; } return true; err: errno = save_errno; return false; } bool pollen_timer_arm_s(struct pollen_event_source *source, bool absolute, unsigned long initial_s, unsigned long periodic_s) { const struct timespec initial = { .tv_sec = initial_s }; const struct timespec periodic = { .tv_sec = periodic_s }; return pollen_timer_arm(source, absolute, initial, periodic); } bool pollen_timer_arm_ms(struct pollen_event_source *source, bool absolute, unsigned long initial_ms, unsigned long periodic_ms) { const struct timespec initial = { .tv_sec = initial_ms / 1000, .tv_nsec = (initial_ms % 1000) * 1000000, }; const struct timespec periodic = { .tv_sec = periodic_ms / 1000, .tv_nsec = (periodic_ms % 1000) * 1000000, }; return pollen_timer_arm(source, absolute, initial, periodic); } bool pollen_timer_arm_us(struct pollen_event_source *source, bool absolute, unsigned long initial_us, unsigned long periodic_us) { const struct timespec initial = { .tv_sec = initial_us / 1000000, .tv_nsec = (initial_us % 1000000) * 1000, }; const struct timespec periodic = { .tv_sec = periodic_us / 1000000, .tv_nsec = (periodic_us % 1000000) * 1000, }; return pollen_timer_arm(source, absolute, initial, periodic); } bool pollen_timer_arm_ns(struct pollen_event_source *source, bool absolute, unsigned long initial_ns, unsigned long periodic_ns) { const struct timespec initial = { .tv_sec = initial_ns / 1000000000, .tv_nsec = initial_ns % 1000000000, }; const struct timespec periodic = { .tv_sec = periodic_ns / 1000000000, .tv_nsec = periodic_ns % 1000000000, }; return pollen_timer_arm(source, absolute, initial, periodic); } bool pollen_timer_disarm(struct pollen_event_source *source) { int save_errno = 0; if (source->type != POLLEN_EVENT_SOURCE_TYPE_TIMER) { POLLEN_LOG_ERR("passed non-timer type source to pollen_timer_disarm"); save_errno = EINVAL; goto err; } POLLEN_LOG_DEBUG("disarming timerfd %d", source->as.timer.fd); struct itimerspec itimerspec; itimerspec.it_value.tv_sec = 0; itimerspec.it_value.tv_nsec = 0; itimerspec.it_interval.tv_sec = 0; itimerspec.it_interval.tv_nsec = 0; if (timerfd_settime(source->as.timer.fd, 0, &itimerspec, NULL) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to disarm timer: %s", strerror(errno)); goto err; } return true; err: errno = save_errno; return false; } struct pollen_event_source *pollen_loop_add_efd(struct pollen_loop *loop, pollen_efd_callback_fn callback, void *data) { struct pollen_event_source *new_source = NULL; int save_errno = 0; POLLEN_LOG_INFO("adding efd source to event loop"); int efd = eventfd(0, EFD_CLOEXEC); if (efd < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to create eventfd: %s", strerror(errno)); goto err; } new_source = POLLEN_CALLOC(1, sizeof(*new_source)); if (new_source == NULL) { save_errno = errno; POLLEN_LOG_ERR("failed to allocate memory for event source: %s", strerror(errno)); goto err; } new_source->loop = loop; new_source->type = POLLEN_EVENT_SOURCE_TYPE_EFD; new_source->as.efd.efd = efd; new_source->as.efd.callback = callback; new_source->data = data; struct epoll_event epoll_event; epoll_event.events = EPOLLIN; epoll_event.data.ptr = new_source; if (epoll_ctl(loop->epoll_fd, EPOLL_CTL_ADD, efd, &epoll_event) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to add efd %d to epoll: %s", efd, strerror(errno)); goto err; } pollen_ll_insert(&loop->sources, &new_source->link); return new_source; err: POLLEN_FREE(new_source); errno = save_errno; return NULL; } bool pollen_efd_inc(struct pollen_event_source *source, uint64_t n) { int save_errno; if (source->type != POLLEN_EVENT_SOURCE_TYPE_EFD) { POLLEN_LOG_ERR("passed non-efd type source to pollen_efd_trigger"); save_errno = EINVAL; goto err; } if (write(source->as.efd.efd, &n, sizeof(n)) < 0) { save_errno = errno; POLLEN_LOG_ERR("failed to write %lu to efd %d: %s", n, source->as.efd.efd, strerror(errno)); goto err; } return true; err: errno = save_errno; return false; } bool pollen_efd_trigger(struct pollen_event_source *source) { return pollen_efd_inc(source, 1); } void pollen_event_source_remove(struct pollen_event_source *source) { if (source == NULL) { return; } switch (source->type) { case POLLEN_EVENT_SOURCE_TYPE_FD: { int fd = source->as.fd.fd; POLLEN_LOG_INFO("removing fd source for fd %d from event loop", fd); if (epoll_ctl(source->loop->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) { POLLEN_LOG_WARN("failed to remove fd %d from epoll: %s", fd, strerror(errno)); } if (source->as.fd.autoclose) { POLLEN_LOG_INFO("closing fd %d", fd); if (close(fd) < 0) { POLLEN_LOG_WARN("closing fd %d failed: %s (was it closed somewhere else?)", fd, strerror(errno)); }; } break; } case POLLEN_EVENT_SOURCE_TYPE_IDLE: { POLLEN_LOG_INFO("removing idle source with prio %d from event loop", source->as.idle.priority); break; } case POLLEN_EVENT_SOURCE_TYPE_SIGNAL: { int signal = source->as.signal.sig; struct pollen_loop *loop = source->loop; POLLEN_LOG_INFO("removing signal source for signal %d from event loop", signal); sigdelset(&loop->sigset, signal); int ret = signalfd(loop->signal_fd, &loop->sigset, 0); if (ret < 0) { POLLEN_LOG_WARN("failed to remove signal %d from signalfd: %s (THIS IS VERY BAD)", signal, strerror(errno)); } sigset_t set; sigemptyset(&set); sigaddset(&set, signal); if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) { POLLEN_LOG_WARN("failed to unblock signal %d: %s (program might misbehave)", signal, strerror(errno)); }; loop->signal_sources[signal] = NULL; break; } case POLLEN_EVENT_SOURCE_TYPE_TIMER: { int tfd = source->as.timer.fd; POLLEN_LOG_INFO("removing timer source with tfd %d for from event loop", tfd); if (epoll_ctl(source->loop->epoll_fd, EPOLL_CTL_DEL, tfd, NULL) < 0) { POLLEN_LOG_WARN("failed to remove tfd %d from epoll: %s", tfd, strerror(errno)); } if (close(tfd) < 0) { POLLEN_LOG_WARN("closing tfd %d failed: %s", tfd, strerror(errno)); }; break; } case POLLEN_EVENT_SOURCE_TYPE_EFD: { int efd = source->as.efd.efd; POLLEN_LOG_INFO("removing efd source for efd %d from event loop", efd); if (epoll_ctl(source->loop->epoll_fd, EPOLL_CTL_DEL, efd, NULL) < 0) { POLLEN_LOG_WARN("failed to remove efd %d from epoll: %s", efd, strerror(errno)); } if (close(efd) < 0) { POLLEN_LOG_WARN("closing efd %d failed: %s", efd, strerror(errno)); }; break; } } pollen_ll_remove(&source->link); POLLEN_FREE(source); } struct pollen_loop *pollen_event_source_get_loop(struct pollen_event_source *source) { return source->loop; } int pollen_loop_run(struct pollen_loop *loop) { POLLEN_LOG_INFO("running event loop"); int ret = 0; int number_fds = -1; static struct epoll_event events[POLLEN_EPOLL_MAX_EVENTS]; loop->should_quit = false; while (!loop->should_quit) { do { number_fds = epoll_wait(loop->epoll_fd, events, POLLEN_EPOLL_MAX_EVENTS, -1); } while (number_fds == -1 && errno == EINTR); if (number_fds == -1) { ret = errno; POLLEN_LOG_ERR("epoll_wait error (%s)", strerror(errno)); loop->retcode = -ret; goto out; } POLLEN_LOG_DEBUG("received events on %d fds", number_fds); for (int n = 0; n < number_fds; n++) { struct pollen_event_source *source = events[n].data.ptr; switch (source->type) { case POLLEN_EVENT_SOURCE_TYPE_FD: POLLEN_LOG_DEBUG("running callback for fd %d", source->as.fd.fd); ret = source->as.fd.callback(source, source->as.fd.fd, events[n].events, source->data); break; case POLLEN_EVENT_SOURCE_TYPE_TIMER: POLLEN_LOG_DEBUG("running callback for timer on tfd %d", source->as.timer.fd); /* drain the timer fd */ uint64_t dummy; while ((ret = read(source->as.timer.fd, &dummy, sizeof(dummy))) > 0) { /* no-op */ } if (ret < 0 && errno != EAGAIN) { POLLEN_LOG_ERR("failed to read from timerfd %d: %s", source->as.timer.fd, strerror(errno)); loop->retcode = ret; goto out; } ret = source->as.timer.callback(source, source->data); break; case POLLEN_EVENT_SOURCE_TYPE_EFD: POLLEN_LOG_DEBUG("running callback for efd %d", source->as.efd.efd); uint64_t efd_val; if (read(source->as.efd.efd, &efd_val, sizeof(efd_val)) < 0) { POLLEN_LOG_ERR("failed to read from efd %d: %s", source->as.efd.efd, strerror(errno)); loop->retcode = -1; goto out; } ret = source->as.efd.callback(source, efd_val, source->data); break; default: POLLEN_LOG_ERR("got invalid callback type from epoll"); loop->retcode = -1; goto out; } if (ret < 0) { POLLEN_LOG_ERR("callback returned %d, quitting", ret); loop->retcode = ret; goto out; } } /* dispatch idle callbacks */ struct pollen_event_source *source, *source_tmp; POLLEN_LL_FOR_EACH_SAFE(source, source_tmp, &loop->idle_sources, link) { POLLEN_LOG_DEBUG("running idle callback with prio %d", source->as.idle.priority); ret = source->as.idle.callback(source, source->data); if (ret < 0) { POLLEN_LOG_ERR("callback returned %d, quitting", ret); loop->retcode = ret; goto out; } } } out: return loop->retcode; } void pollen_loop_quit(struct pollen_loop *loop, int retcode) { POLLEN_LOG_INFO("quitting pollen loop"); loop->should_quit = true; loop->retcode = retcode; } #endif /* #ifndef POLLEN_IMPLEMENTATION */ /* * pollen is licensed under the standard MIT license: * * MIT License * * Copyright (c) 2025 heather7283 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ pipemixer-0.4.0/man/000077500000000000000000000000001512217147000142705ustar00rootroot00000000000000pipemixer-0.4.0/man/pipemixer.1000066400000000000000000000035571512217147000163660ustar00rootroot00000000000000.TH pipemixer 1 "December 2025" "0.4.0" "User Commands" .SH NAME pipemixer \- audio mixer for pipewire .SH SYNOPSIS .B pipemixer [\fIOPTIONS\fR] .SH DESCRIPTION pipemixer is a terminal-based audio mixer for the pipewire audio system. .SH OPTIONS .TP .B \-c, \-\-config Path to configuration file, see \fBpipemixer.ini\fR(5) for details. .TP .B \-l, \-\-loglevel Set log level. Accepted values: DEBUG, INFO, WARN, ERROR, QUIET. .TP .B \-L, \-\-log-fd Write log output to the specified file descriptor (must be open for writing). .TP .B \-C, \-\-color Force colored log output. .TP .B \-h, \-\-help Display help message and exit. .SH CONTROLS By default, pipemixer uses the following keybinds: .TP .B j, Down arrow Move down the node list. .TP .B k, Up arrow Move up the node list. .TP .B t, Tab Switch to the next tab. .TP .B T, Shift+Tab Switch to the previous tab. .TP .B 1, 2, 3, 4 Switch to the 1st, 2nd, 3rd, or 4th tab. .TP .B m Toggle mute for the selected node. .TP .B l, Right arrow Increase volume. .TP .B h, Left arrow Decrease volume. .TP .B Space Toggle channel lock for selected node (affects whether volume changes apply to all channels). .TP .B D Set focused source or sink as default. .TP .B p Enter route selection mode on nodes that support it. .TP .B P Enter profile selection mode. .TP .B Enter Confirm selection. .TP .B Escape Cancel selection. .TP .B q Quit the application. .SH BUGS Please report bugs to https://github.com/heather7283/pipemixer/issues .PP When reporting bugs: .PD 0 .IP \(bu 4 Provide backtrace with symbols if you experience a crash. .IP \(bu 4 Describe steps necessary to reproduce the issue. .IP \(bu 4 Attach output of pw-dump (note that it will contain currently playing media). .PD .SH SEE ALSO pulsemixer: https://github.com/GeorgeFilipkin/pulsemixer .br pwvucontrol: https://github.com/saivert/pwvucontrol .PP .BR pipemixer.ini (5), .BR pw-cli (1) pipemixer-0.4.0/man/pipemixer.ini.5000066400000000000000000000067371512217147000171530ustar00rootroot00000000000000.TH pipemixer.ini 5 "December 2025" "0.4.0" "File Formats" .SH NAME pipemixer.ini \- configuration file for \fBpipemixer\fR(1) .SH DESCRIPTION The pipemixer.ini file configures the behavior and appearance of pipemixer. .PP pipemixer will look for a configuration file in the following locations: .IP \(bu 4 \fB$XDG_CONFIG_HOME/pipemixer/pipemixer.ini\fR .IP \(bu 4 \fB$HOME/.config/pipemixer/pipemixer.ini\fR, if XDG_CONFIG_HOME is unset. .SH SECTION: main General options. .PP .B volume-step .RS 4 Volume change step in percentage (1 means 1%). Default: 1. .RE .PP .B volume-min, volume-max .RS 4 Max and min volume that can be set with pipemixer. Default: 0, 150. .RE .PP .B wraparound .RS 4 Whether scrolling through nodes will continue at the top after reaching the bottom and vice versa. Default: false. .RE .PP .B tab-order .RS 4 Order of tabs in the UI. A comma-separated string of tab names. Default: playback,recording,output-devices,input-devices,cards .RE .PP .B default-tab .RS 4 Specifies which tab will be active when pipemixer starts. Default: playback .RE .SH SECTION: binds Configure keybindings. Format is action=key. Each action can be bound to multiple keys. Key can be specified in several ways: .IP \(bu 4 All printable ASCII/Unicode characters. .br Example: tab-next=t .IP \(bu 4 Special keys: up, down, left, right, enter, tab, backtab, space, backspace, escape. .br Example: tab-prev=backtab .IP \(bu 4 Arbitrary keycodes with code:N, where N is a decimal number. If N is prefixed with 0x, it is interpreted as hexadecimal instead. .br Example: quit=code:185 .PP .B focus-down, focus-up .RS 4 Navigate node list. .RE .PP .B focus-first, focus-last .RS 4 Focus first or last node. .RE .PP .B volume-up, volume-down .RS 4 Adjust volume. .RE .PP .B tab-next, tab-prev .RS 4 Cycle forward/backward through tabs. .RE .PP .B tab-playback, tab-recording, tab-input-devices, tab-output-devices .RS 4 Switch to specific tab. .RE .PP .B tab-N .RS 4 Switch to Nth tab (1-based). .RE .PP .B mute-toggle, mute-enable, mute-disable .RS 4 Set mute state for selected node. .RE .PP .B channel-lock-toggle, channel-lock-enable, channel-lock-disable .RS 4 Set channel lock for selected node. .RE .PP .B volume-set-N .RS 4 Set volume of selected node to N%. .RE .PP .B set-default .RS 4 Set focused source or sink as default. .RE .PP .B select-route .RS 4 Enter route selection menu on nodes that support it. .RE .PP .B select-profile .RS 4 Enter profile selection menu. .RE .PP .B confirm-selection, cancel-selection .RS 4 Self-explanatory. Applies to menu selection mode. .RE .PP .B quit .RS 4 Exit application. .RE .PP .B unbind .RS 4 Unbind a key from any action. .RE .SH SECTION: interface Configure various UI elements. .PP .B bar-empty-char, bar-full-char .RS 4 Characters for drawing volume bars. .RE .PP .B routes-separator .RS 4 String that separates routes on nodes that have them. .RE .PP .B profiles-separator .RS 4 String that separates profiles on devices. .RE .PP .B border-left, border-right, border-top, border-bottom, .br .B border-top-left, border-top-right, border-bottom-left, border-bottom-right .RS 4 Characters that make up border around each node. .RE .PP .B volume-frame-top-left, volume-frame-top-right, .br .B volume-frame-bottom-left, volume-frame-bottom-right, .br .B volume-frame-center-left, volume-frame-center-right, .br .B volume-frame-mono-left, volume-frame-mono-right, .br .B volume-frame-focus .RS 4 Characters that make up frame around per-channel volume bars. .RE .SH SEE ALSO .BR pipemixer (1) pipemixer-0.4.0/meson.build000066400000000000000000000040601512217147000156570ustar00rootroot00000000000000project('pipemixer', 'c', version: '0.4.0', license: 'GPL-3.0-or-later', default_options: ['warning_level=3']) add_project_arguments('-Wno-pedantic', language: 'c') add_project_arguments('-Wno-unused-value', language: 'c') add_project_arguments('-Wno-unused-parameter', language: 'c') git = find_program('git', required: false) if git.found() git_tag = run_command(git, 'describe', '--tags', check: false).stdout().strip() if git_tag != '' add_project_arguments('-DPIPEMIXER_GIT_TAG="@0@"'.format(git_tag), language: 'c') endif git_branch = run_command(git, 'branch', '--show-current', check: false).stdout().strip() if git_branch != '' add_project_arguments('-DPIPEMIXER_GIT_BRANCH="@0@"'.format(git_branch), language: 'c') endif endif add_project_arguments('-DPIPEMIXER_VERSION="@0@"'.format(meson.project_version()), language: 'c') pipewire_dep = dependency('libpipewire-0.3') ncursesw_dep = dependency('ncursesw') inih_dep = dependency('inih') cc = meson.get_compiler('c') m_dep = cc.find_library('m') pipemixer_sources = [ 'src/pipemixer.c', 'src/tui.c', 'src/menu.c', 'src/log.c', 'src/xmalloc.c', 'src/utils.c', 'src/config.c', 'src/signals.c', 'src/eventloop.c', 'src/collections/vec.c', 'src/collections/map.c', 'src/pw/common.c', 'src/pw/device.c', 'src/pw/node.c', 'src/pw/roundtrip.c', 'src/pw/events.c', 'src/pw/default.c', ] include_dirs = [ 'src' ] executable('pipemixer', pipemixer_sources, include_directories: include_dirs, dependencies: [ncursesw_dep, pipewire_dep, m_dep, inih_dep], install: true) install_data( 'com.github.pipemixer.desktop', install_dir: join_paths(get_option('datadir'), 'applications'), ) install_data( 'man/pipemixer.1', install_dir: join_paths(get_option('mandir'), 'man1'), ) install_data( 'man/pipemixer.ini.5', install_dir: join_paths(get_option('mandir'), 'man5'), ) if get_option('shell-completions') zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') install_data('completion/_pipemixer', install_dir: zsh_install_dir) endif pipemixer-0.4.0/meson.options000066400000000000000000000001451512217147000162530ustar00rootroot00000000000000option('shell-completions', type: 'boolean', value: true, description: 'Install shell completions') pipemixer-0.4.0/pipemixer.ini000066400000000000000000000036021512217147000162210ustar00rootroot00000000000000[main] ; in percents (1 means 1%) volume-step=1 volume-min=0 volume-max=150 tab-order=playback,recording,output-devices,input-devices,cards default-tab=playback [interface] ; defaults: ; border-left=│ ; border-right=│ ; border-top=─ ; border-bottom=─ ; border-top-left=┌ ; border-top-right=┐ ; border-bottom-left=└ ; border-bottom-right=┘ ; volume-frame-top-left=┌ ; volume-frame-top-right=┐ ; volume-frame-bottom-left=└ ; volume-frame-bottom-right=┘ ; volume-frame-center-left=├ ; volume-frame-center-right=┤ ; volume-frame-mono-left=╶ ; volume-frame-mono-right=╴ ; volume-frame-focus=─ ; bar-empty-char=- ; bar-full-char=# ; example: use rounded borders border-top-left=╭ border-top-right=╮ border-bottom-left=╰ border-bottom-right=╯ volume-frame-top-left=╭ volume-frame-top-right=╮ volume-frame-bottom-left=╰ volume-frame-bottom-right=╯ [binds] ; keybinds in the form action=key ; supported keys are: ; all printable ASCII/Unicode: a, b, c, 1, 2, 3, а, б, в, etc. ; some special characters: enter/tab/backtab/space/backspace/up/down/right/left ; arbitrary keycodes: code:1234 where 1234 is decimal representation of keycode ; ref: https://invisible-island.net/ncurses/ada/terminal_interface-curses_constants__ads.htm ; default binds: focus-down=j focus-down=down focus-up=k focus-up=up focus-first=g focus-last=G volume-up=l volume-up=right volume-down=h volume-down=left tab-next=t tab-next=tab tab-prev=T tab-prev=backtab tab-playback=1 tab-recording=2 tab-input-devices=3 tab-output-devices=4 ; mute-{enable,disable} are also valid mute-toggle=m ; channel-lock-{enable,disable} are also valid channel-lock-toggle=space set-default=D select-route=p select-profile=P confirm-selection=enter cancel-selection=escape quit=q ; additional keybinds you can configure: ; volume-set-N - set volume to N% ; use unbind=key to unbind a key from any action pipemixer-0.4.0/screenshot.png000066400000000000000000000755431512217147000164160ustar00rootroot00000000000000PNG  IHDRZR IDATxy|Gv96M} }TEkj'mmkmZC+xH9p$\f%ds?f;3,7>  CD )] kQEQY7+5:WT5ʢ33c/T?S:&^xcfդj9!:taNZ*d^̲btv47l[3$,\,\,-vlQ1ƞ_J_@-κ;ӥrqpcOE9j\ˍN&b֦nomu1/M c2_RPT) yVi(wFJDUbXT-MD$K<f+՗nZe+2N}|:;:zz㸞Qh4ۘVpx EHhVҌr%a}E"۬ΎWє0zWde;QU]3=%w.,!3f?3#!$dם)$@,v1^[Ͳ?_fp8fzU׫;@䨸`QYUs@s3v:}(oP6ڈ^M Db6J.L@U\c0 bOr1֪S>ͺnlD}mڀД0z6;H( |y\P.ٟ{9+x󃭪V͌T|\3bfzœFf,D,gM,}5./#|EIat}wR 1_$[Vuӗ2_$XMgGB/Lfs{[k[ٮ UE||.xy1:]Ue#.HZվ~"F`z R*y\^[[P(J LUp]ΦzG"GmCCRyYI#4,,_(8;Zur±:`+6nJmWKgVKhYu]. E&vsc4drfydwM9QM #p딄IDԡѱD0gJWU][ng;u9)1,D왝GDO:8}oY0hԌ=F[~`ʘ=+3ObÌFN b!"[hXlvu;~%IZ4]J__nnnaɠ9bxzy47KIB14bԱ^knjlnj4L+Ri]MUZl+==͍"H&jȄdnujUן(c6͠f[%;9~Zbzqf:bm'[hSTWTM `pE- sa;yԤpHJD?fsU-a~{dRqx_YeþsNO}cM;)f;g;j" TfOpp"ڶ[[lf|T㗿 2~C;V@ "Xc0LF"X5Dԥ^^J`OO/;٫++V+q8vOO閭z<-sMU%YG1Z[̗|?0(D &jUWx̘"rwK\unuww ]LD wZSSeۮ(*zͦi2R/FQ""_U}XZK   EA.viv6 g\d2T-L!=ÐY4GS4%0ȘJ" 6}w`?Y$8oDTV.p8K5:n:ɤbb!u_n;lM1eMK Rv|pn޾mTW؟@(2 zRtb6ߣD EF1cz]l6ve&f fQ="ؕ[f0 $V/0MG$t;`5RtK+k:VD*2 \F38$̠16_.4uӔ?Wd?>w_D29o~%.XqhV.MǓu:Bcl v.PPD"q`p8YvވK!"ժw]xc2"!BGSNHSV>_f˝h-f}=7 (j^AJ\87$#*,pO2bmcEӢ?xmas}=Ӧ{ݼ$yFptttt눨+4,\%|>m:ut{+}'Ej:;BO{{pm^a]]T&76Ա]OЈI:mL&pn?wP(~bǗz}7Kmmm2@@AuDho $Ltc|[,KhxS,Kuv+}|"&i:;d"yŌCOO]gm6{{zs|/o.PDf;K+3'2>>D|E,`9@$T*}y<^OO/foh tI$R+{F|єҔ0b<"˞s~ی&sR*} J<'9sAswH(7+hz{67cεc>_E5[JID?[㭐K&BiezDEGם+2[-33c#N3!wB Υjs9/oeWbfPxxxzDK[,}wL&m-͍̯Rc\.Ojio띨U,+m_b4vB&wj6_ `  VrerL.X,:B!+wgCѮV7+|&c? PFDbD&tPjD"ttZzb1rU57wv;Ѡ׻<-[@RlhNThvRMt-]O(;lڽY93&\ѡulv_ݡ=Vl)hx_mOԝWSG|"ztݻͭC{7d~_a<ܹre3V#8GV8_Ep=}fdߺ?Ou[8SU' .D W3 &M']Nܐ=KV=r."Εr?!5qRRl{2l#'Zo[6^;WGlvX7QFJcgJm"ʝ"zObl` r6m,nCF=cÞzhUaY#b~z"ɼ97-;]W& sV:DzɌunQmaIn=60[X9Kkضd۷=jq{[G.n WXZSXZCDq!a>.}.̛:)ĿqE}6ԝ>TovvV?3[,[wΛ99c1+ܽ=ݿqyPU*]ΜV,,#ؘ_5{lKfN ;=DvE|tCϼŬ^:ctH})S9ݱY.,yՏ5mJ7 nnwrS~v`1[,DdO<.eOg73,I[,ze6+3WooZU]m jfۗ*;t[wضTy_⽿6\RYbX.V5LOp8CfNVmÎPU]KU] Mytvu-+y*d s1=R09>yf4.QcscDt쪺-;?O-#"Ỗ(=횅9if;]$7ɘY<]cڈ3Q=绽.aN)kUb+GN7?SJD+NY̲K:t&s0mlDА3Lw#sC-9ܩ]ZCNV򹢪HHxX]$qܸ=Ū%uTZ5Vv{]ckHSx6ڙtjTgyf{lsj،1O]pws#ZZ{'BiVu4RdzQxyp;? Wp2~Iſ޻,,wSDrcB,}DTXZ# yNQL Ow+).6|bU~4Ջ j7C̘9+?h>i5/O\RQWRQae7|É>QɳE|7>*^p\ myRl1ן]wA<.56S1$Ddu9J sr0&5{93RzbBgw/KzD0C,6;)wxn{X6np,rUDdُ*JOpWk\9(hGtC;0!\z_bFG7h [vNaX[^ nٛTGFsb=<2YnhfE gcE2ϋ5uA"ADns2Fޡ]G̿ҚĘnVH""p8~͗n`FrrL5;|aA.>fi2߱gj٫K[kԵ j!_.}Lr|iɟo=="J ܰy^otlܼ/_m*Âd+p~Y67zF=Ug撊޹j˪/TLlV59YfSߎ}ySZ]'to{9N\Ts/\zRoPi坿q^oqۑʼn1a!Mdv;qt]̕!-|݉Cn'ʅYDTZQ_\^á3ӓm'ϕENNxgg۱#./o?[T7#uW42ScV/aXMfgGa:p&wz^uOqӢ;3Jy>W?uQֿY;u}fۛW`W-ʾgM~Ω݋'~+aGhyOVټy`$b"諽eUo%4[w ̅{V9to3U6dM+XJB|nNk\(!P-/]+guSnkG;>Q얗_7f$DlsjE1E9SR#d<"Eͷx7ik׾ Y]9lN]+sup+'OwwrzqȌoysqNݞNW"R[֯[wNO-{l))\Swyy6gq9xU?{-vi:nVw 7OqN+ 14TQM}KfyER IDATd%U5"]1| mo~u,jӛFY`\ zɵ2 \Kh!c` 2f6Ș cMo?{/dR綬_D$Nۺ6/lqn]e12Wv;XlY>'19YgLJNsMO{wnzw^zdi\O9'OND?F(T4G>eެ"b9O]3o +z"zkU -?[zeG=~yL.p9Lqv*8Yy)&*{E\.bǵNտ1BfbXl6"s6;P[k%4?{)qEQFe˿P :;zEӽǡV C/8~_*p~zγ<\Є=pX>D}0?juP˨nwh !c){\YblؑSn]nRwMo }%{un16ttj2+?-9\RZ'W -PR]fۉ 5u^cTڨ|E+iDhB :X|F0X,gmLv蟳g]Sz iF~8Qj^yg3%EcKfL/]/htl+2tU湿|HDcu((#"@IkwC E"5]!4QHwG%eD宨h9kg%ˉjCc>h|}bVӒ"ۙq/)͜=oyϳZlvs\f{IiAMHhޔ{&\f!Y/5% Q? ,6\Cdر;78Z5c4;bCE"h&`ߖ~%Vwjtqa:jXB ߢKFtZTIa\.Ic"[;<]&5:ӂqV3bɺ.MByF5޷<7ѯJ;] mtM%'!"z.} jOm]g[5?B] { L{g7G2 ;GKCrTSV.Z0+$G*1ˁ^Dm "CUraT"$]=~œGrcvZ܎czzMR7AcmEnzʋZ-ZՆDzMxNSiŪ:j#4Q.]((jk`DsKVkۮx|A3Z^;Vξsv{E$GN/1ܣaZbNVc'kUAD33ff&8v;]7Ukx͎\20φ"𒊥N&<YVAD~^7 yywT* .vv^k!%cBq3dl1A 3dl1A 3dl1A 3~M[֯6q(2!ep-(* 6Ș c` 2f6Ș c` 2f6Ș c` 2f6Ș c` 2f6Ș c`ﳾe cڗ}3>a>3dl1A 3dl1A 3dbyUH(xH24_C>Ǔ>or)}W{;_21r)5mu|Lu- 6Wۏp-xM-;1;,*(SxܨNu>zeMNtlYYyReRנ>tctؑP!wr9ffj> j0j0w\Q՘pť&NWmwʱr2ɎZ[-fnfAO& -f{vz/Q$9-,q_~`fuբ]ٸJ*eCw, [FyȺ%džϛgc^~#c Y8Z5L6JДҚ1/5E$-p<*xaNߨ%Ef6MlTœ#c:H433y%3uck{אǎO2?@&spdísMrp sˑSm7F^:Cm`. xl6;{<pp5c}y/(_l=ODdX+b -v\&]1ڂN-EeybIn7;X; mumS֧ |ϚpN_ Ρs B㢂_36 H1Q'l{rb_RPT) yG]>UXmPMH5JU]ҚذŹS?znIDMCy_[IrKfX,֍_%&!2f.CDo=H(&"f; dCXBqD={ H".M6l}[Oz\TVauH HDuDsWrTXRkm] !]_p|rb"L&˩:b2)8vЉB<w@z=DԬ`VUD$9W\EDaA>ø:c{${jYB!"8S׻:5D$ |bu*X?ђ _>潕wʍ X0Ow.MO m}^LrϪ9k͞<&sX0y_Xg$N4wFʤPw[φӥީ%:UKOR>Ʊb4YdNzWVyޞ)l|~Df1p靋oZ8]!w#ow~;h+31!&buF{o٣?fl7nsg=-3e.^/]j$"R]{<EDݷݱZP\@.!crz5v<{ff&dDG^7f&ņKDP"YbmpS[͋EBa%^>>U>g$3K5"Bhǿu#L(.2xrB(k8#w TQQYmeM3λXڱWۏQH2{j5DuiˍEe:y rnRަa4]ݪ6 %E{QeRxL)\8ʫUCND^sBD{͙ru d9rv|1/:q9%Ƅx:V\nMݞ<=%zNM"4x{R`p28@g_^PM9s&en"];KgvM`.k8dy3hӗ-#83t6O\\ʘmV;qm|ml$P}O:y9-|虷]9^s|7h9 WLDiIcRc[;1mɂӓ'IMF^?>wqi|=0- ^eK3󽹻L"]Ɂ\P `VWW<`r}VѰXD:=TtA`oC4@yjr3ݱ2X~M|8˱r\g +V[Rl(0b7O\쳑)qn" !".:gٍ`؈n;Y|BDBP@D=CMH7Q)ѥh>Qp]rX֩p>_,EBKEB%,F-Ν* <峧'QMH%^ z01EMDޢ/V"5-I**IQDTר&"XG_;], { D 3%nKD'+GXT~Ť]WX4hnh&?okrBW^:'?4#bj"(IC)qtY yԴ)1r7BJOsn:~aqnݫT;O:ϵ|WhuwЖ]rSM쭧\Z5_PlN86Z)_ˉ_r81;+eVfbx\F}\[8`֔)>}̖涣%Dt͹J/owSݦ&E%Ed%9 BQ=]rm]Dt|łrSrS}|gqr\s"r5ۋKS씧^Ŭ~#oChvڢٗ~y _mgFީ-Νf4UNǣc:xf¬x U5><**,pO2ti cuM]ZChk/<0+3q4]/Yqyj{,~YD"ҹ}>TT^Ӹȹe4[׽Qaif8[> Eٿ~XP\miW?XQfO%>aX:>A"Z{wyD ݆֎g1W?SaW}7"bF^,kQ?_pL+/~z?oxTֵҀVήLaeS?!2 yԎ *\V|i>QCD )-׭\ҀF>/O9򳿘̣_BtKD5z c0^W_<|DWsJ\QT0kM==eGfM;70n[چRw^zc6|/;3>w+"d Șn^!gȘ%ՎՆFfy oU͌ hqT.?m iyK~O3R5uZU;2f)*ݲؘuɀϟݥտ'n%Džц/+ "X$7}r|ϘצWFDj3!3[lecs;SNDfYAD2*mލ >2"w_:%*ΗgG[>%+ !o~1L 8C[XdO _|m‰h×{D,\>9>g O _kSH+#v`5ډH& 2f/ۙޞr"jVw\*м}dDduJT/eώ:|JVujC c{)[ p8#7/*8DD|>gut^H0N̴"$n""  d6fL:_1@ȘgԤp{Zy3Sb=Ҩ@"}+V6MB<;=FJMX(>olEr`MIyl>TŢ_h:kn;M!@%C˘k$iN3{."4Ӕˡ*eMlh !c@@/ꖝG%l/;ܕ0$b?^|WHK;UĄ.V6նw4yuV}i=W}MsW&kE:"ZIDu̜k"^J>F5JR(B r^F&2Cΐ1 EN:VDs?gfṵ5k}JFۻ6;oݷlK|_Iӄp$B4ZZHٖ҃k'˲n`tz\?7r͟=f?ϊ-Ɵ߹EDJ D"EDjJRVJEe%SHE$-єDt`ܶsI}ѤM(ИljYg_~/Ol+/X|l)wϝ]'HD}S*W,>mO{A]9D=1_d\ze?E%~;HI^Acȳ_um/>Wߗ>h^K vx7->iO32Dn],L"j{#6}ғ5-N6(C"3*;ww?؟Hr@ww=O @D}çӑXm{n'≤&BEUľ2R<#"jٔY߸a&D+q{K31Ac @34f9hrИ1Ac @34f97mܰď`rbafS04cV4f9hrИ1Ac @34f9hrИ1Ac @34f9hrCg7-8&];3CfAd @34f9hrИ1Ac @ΨNtyHJe4ƳF d{RV7$d216fem_@ fw5j>~-(* ǥZ%vw,+k֚}9'N]h4qKVk3.,8vBNu/ѤIa&cQ [k7AeuZ,-<p2_bu:˔*u<{"R@qe<!l9:]p(V^Y ZYT*zHT֩TKL SWvs"t:.̾ETPm7L6b$b灶htk0toϨNo`Y6f,٢VkQHh0<{DKl jMUuM$TRRq~'($"n<~( GoÔl($ǜ==3 3͖ʪbUFSyeJF"6ͥDGZTO0GDbQqyN0L_%AQՙia'߯T,+3LZ95fJR|^O ˪#pͮhzb1@/3+X-\6˖Sy0`kH&; "M1+:]Mm}Um}:,E%6Z( $u/OQoJɩ1+UJ"Jp, *ՑRۉqG9\sS΃5ڀ/}QP.N[H|,3Yc6[! j7Y9!ݑY:>;/.)B Ás.= 8t30Bye60EE%RJ=ݝBz˰leUu2h/l!"iG"b;1jB$K՚d2f#8kj^Xb``&Ltw)JgYx &S*GCg0R|*udRM"0#}!e㺻:1Djj=UUfm>"Jn"Rt@Z~DT0EdV[v5 Di6ccHv4NhW,TTVku!&agFq\"7[~l g**)^DTHfD6RY[Wϰ,0JR惏**dC!~LqJB` XbwH*;|nlBa0 HS)KՖlT*uVTTA#0E"aV(u:~ U9Jo2ʩ1 4)#oJqFYo0x<NB "c(1GPp,S(&90|4AjM2,- Bhx'14fQHד9Jhoz);*.q\`G;IJl&bh"( B%DRk$0+`p{[k"5xXTRK/dh|ZF"DGprcafSf}v]; 7ɫ`4=cBdF"bz;!BIv%noiVQ4;J18F 1 #N(F#hd,{XT*M̊[92m:,MfLwPHT$@v-"D*RrCK*J<8q'LR=MsWw5两R dB B:z%"DY՚jˮfǂho34fQHӽ=]Ї8m?iܣwEzn"iuD"D#F}==ä e*ҞrUQYm2i\:(gY/>EQ&Cg'fw: ÊH$fc!BH ~ 4r;d31LF\t CR 'ub(K;ڈ*R)"D(?z0 1Aj,b2ۛfFKDZN`mvCd29[jjCNgX.Wt/(4P(X2 "Z Iˈ!DU 7Fh2gV}bZřUgY9qL&#pWA{iy)2Qx,JDjU("DGp<(-wq\wW; 4%&@T(s1 W&yoˮQf30SM|{<<Ӹ$uPZβ_|.L4/mD6dVE"1"DRer1L Q>OyE`ec;Z\t CR 'ub(K;ڈ*R)"D(?z0 1PIDATnwt1fFKDZN`mvCd29[jjCNgX.W;/>+D<&0QС&!DU g |^LӍx jgVeDq\2D]βJ]~+Dy(*VH$b!I2d3 @34f9hrИ1Ac @34f9hrИ(4i41jYqLN @34f9hrИ1Ac @34f9hrИ1Ac @3d52CfAd @34f9hrИ1Ac @ΨsCmY}s<YRd;aEc6x^7f<YjTƳ5JУ3Gx"M?B,e۩;NclٲutMk.\`Nfol/g߲W߭.w\$'ݲw+u~yc`.jKW~}ȍ׬qeZzd÷,7?uG>}ƌW[VZ^z>^Ob?_ #Ƽzقj̶gn|/uISj˖ dkٍ5O>V. osv39A ٜcg ->Ih< G'z02_?XOWRo!xXrm۹#.;wwQo·_foՋB+oo>Wշ_⬒" 0^}k]}uU5xiDDƢ#߮:|Yd7PKey`D95f[< g[w%Q3/8J&S#㝭_pvc}ژo}իom/wUJ~NGO?f7po_Z]lf+1|M[A qwK˻uD?ldo}w=|zӔ+W-, "|ʛ9g^oQ|tz凡RDHOJ{K_yse:G2o3?|߽xɷwn8K񮇞xE~l5QNb6GUe%V7ndY;. c(/$"ِp,r,Y_ۚiF2U ?tK[>v9E\p6{}{o}Y ,-;l֕8#I xG}7T<~.;g7C2?켛t?_{ ׼gκeϽ~0 cLԺ{~~Ɣ^d0˦ i/ZuޙYv>RV͞:} (Ƭ`Y"J#λǻ|Dtz {+សmBxtr??22O=}@Dg GgA;>>mn_?<񺡶9StthӇD-ZwZ{O>-_8o0&_▏?JDGW_`+2g^{OQgmݏvwD(~U.ŷ:iLbY7>̖=9q/ȣ\1xWQbY/_y@D\r(|糖J-Z.YdV{~ibC(vʏ޺$]}Ѣ3 :"bs{\\ՊJc8z^L!t}sQݾC' qy=^!ٱGʛۿvZLijٰi`(y[Pu$c"qzCLO>M rzq/ȣ4fKx.%"efM~۞D{_ZKGL!NEͶcW:z`ɽ?q˿6oѵg/>cYS~$8()/<K32lL={ۻxo'p={(ϩ^ 'w?q |e(O$H:g[wK)WD`Jǡz=DTd1d7fAGDȟ ݐf ^٬Ʋlyʴp^iKwDT_oއ:9iǵpo`O[ ӛX37@^2q^u <__ r3uCnp/Z|hG,S_ty(}KΙ^+bg D]f Y%"a%^#Ec`QHJnإ3/rdko%ݏ>eHXL>_W׫V̦Q|q ϛˬU T06鴰cWso/*.2eVar>q\xNySH_-?/͞.eg].=ovCU/o^y׮Z+>o騩,]rV[4#,g}wyW7m>ٺ[oM3J?!ŷ?[K\=T۞yvj?Y.O .\0+ť< fU+ܾ"jij=w3%f"*DV/_@D{\},>]C$ιmί~tRwwStCOo替ٕ;[:*lKϞ·ebdF~~͕y̍^~޹gzǂKr/ȣ\=?N[;嶟\~fL[X]~fOqx얢>hD%KsחMT+ꫝ7lcϱk*:  &J-aղ,Mߺ3u# }Z*":u.G4"8g8XW˾%LyorQ04r%زu紺k/;6mX> %v%"[ QQ" ИF!On||U *խw?~3VgGO *V^4DAoTku*'BD%CQa#Qmԩ<"DYk_/=9KmAVwڊ2D(qb.HmYG:*C.KQCTNn 5lʬoܰvuu5+f]#Jׇow15ΘZwdz~Ë_{y] 2Zw04 ˤ)(R؎Pџ\a5iX)(oo"*X=(JҌYdY͒YM$Splq/z7Gѣ.Z*w^ݫT*n}kATWoryR}*Kn}DȦөqE" 3(I->ر'}ph kWWYFLf.rI`_E"BT)6k?ReM$/}H Ω03?>6nljY{4Y{+9-( ՕyՕ[:]Q{w)*.FT(Wِ<6 Yy)Sãc\r ] DcxlC|}ԥf*4/$,wl=QAV\͙*%˧Q$C鴸fsNDTG&-1| ='2RPI2Q~3]8?l%w=/u D^3N`[JDzާ{=l!*\=ИfdV[ZZlڜ)U6"*u龟| l޺?jO}S*Κ7ޖ%W, ;LD%vk!-#*Tq&z@'LD6#{vIˈ W3@̛0ovCfߒoΟp/]JD_=¥+ϾeS/ m&]EMQoYZgQiE]DTZ3 I O^1rИ1Ac @34f9h7lMk.\\颪:涟\qG㟇D_7}o~̃DQ̦ kWYWL^7^if?x[xQA#"tz\ѸH"Fy 0 EG>rRve~*-{||8EoجnOxJ5h"!*`DD,:`,۸) e* 4fQ=Rf50:<+^gqp_'JgIјei坿Q]/Q]J,wNHiD>ru5+O?e~x̲w8|syk]{yAZ]|5|KDh_tU3JEDYe҂;̞W S+Lӂ(etZ\n9V"*H1L A7я`֮1 8YzJ@HZkLJYa+]|;L *TpfrV">yg{Qᢼ$[7±Wu-O3e/*uiYLD(/-^+/wuּ陓=Drzk{uPQ8ƙ ]ݞp'\n3~ "BE 1_ykە.ttxdnext = (head)->prev = (head)) #define LIST_IS_EMPTY(head) ((head)->next == (head) && (head)->prev == (head)) #define LIST_IS_FIRST(head, elem) ((head)->next == (elem)) #define LIST_IS_LAST(head, elem) ((head)->prev == (elem)) #define LIST_FIRST(head) ((head)->next) #define LIST_LAST(head) ((head)->prev) #define LIST_GET(var, elem, member) ((var) = CONTAINER_OF(elem, var, member)) #define LIST_GET_FIRST(var, head, member) ((var) = CONTAINER_OF(LIST_FIRST(head), var, member)) #define LIST_GET_LAST(var, head, member) ((var) = CONTAINER_OF(LIST_LAST(head), var, member)) #define LIST_PREV(elem) ((elem)->prev) #define LIST_NEXT(elem) ((elem)->next) /* Inserts new after elem. */ #define LIST_INSERT(elem, new) \ do { \ (elem)->next->prev = (new); \ (new)->next = (elem)->next; \ (elem)->next = (new); \ (new)->prev = (elem); \ } while (0) #define LIST_REMOVE(elem) \ do { \ (elem)->prev->next = (elem)->next; \ (elem)->next->prev = (elem)->prev; \ } while (0) #define LIST_POP(var, elem, member) \ do { \ (var) = CONTAINER_OF(elem, var, member); \ (elem)->prev->next = (elem)->next; \ (elem)->next->prev = (elem)->prev; \ } while (0) #define LIST_FOR_EACH_AFTER_INTERNAL(var, head, elem, member, direction) \ for ( \ struct { struct list *cur, *direction; } iter = { \ .cur = (elem)->direction, .direction = (elem)->direction->direction \ }; \ \ ({ \ bool keep_going = true; \ if (iter.cur == (head)) { \ keep_going = false; \ } else { \ (var) = CONTAINER_OF(iter.cur, var, member); \ } \ keep_going; \ }); \ \ iter.cur = iter.direction, \ iter.direction = iter.direction->direction \ ) #define LIST_FOR_EACH_AFTER(var, head, elem, member) \ LIST_FOR_EACH_AFTER_INTERNAL(var, head, elem, member, next) #define LIST_FOR_EACH(var, head, member) \ LIST_FOR_EACH_AFTER(var, head, head, member) #define LIST_FOR_EACH_REVERSE_BEFORE(var, head, elem, member) \ LIST_FOR_EACH_AFTER_INTERNAL(var, head, elem, member, prev) #define LIST_FOR_EACH_REVERSE(var, head, member) \ LIST_FOR_EACH_REVERSE_BEFORE(var, head, head, member) #define LIST_SWAP_HEADS(head1, head2) \ do { \ if ((head1) == (head2)) { \ break; \ } \ \ bool empty1 = LIST_IS_EMPTY(head1); \ bool empty2 = LIST_IS_EMPTY(head2); \ if (!empty1 && !empty2) { \ struct list *h1_next = (head1)->next; \ struct list *h1_prev = (head1)->prev; \ struct list *h2_next = (head2)->next; \ struct list *h2_prev = (head2)->prev; \ \ (head1)->next = h2_next; \ (head1)->prev = h2_prev; \ h2_next->prev = (head1); \ h2_prev->next = (head1); \ \ (head2)->next = h1_next; \ (head2)->prev = h1_prev; \ h1_next->prev = (head2); \ h1_prev->next = (head2); \ } else if (empty1 && !empty2) { \ (head1)->next = (head2)->next; \ (head1)->prev = (head2)->prev; \ (head1)->next->prev = (head1); \ (head1)->prev->next = (head1); \ LIST_INIT(head2); \ } else if (!empty1 && empty2) { \ (head2)->next = (head1)->next; \ (head2)->prev = (head1)->prev; \ (head2)->next->prev = (head2); \ (head2)->prev->next = (head2); \ LIST_INIT(head1); \ } \ } while (0) pipemixer-0.4.0/src/collections/map.c000066400000000000000000000171711512217147000175520ustar00rootroot00000000000000#include #include #include #include #include "map.h" /* make sure this is 2^n where n > 0 */ #define INITIAL_BUCKET_COUNT 32 /* 0.75 */ #define REHASH_LOAD_THRESHOLD 75 __attribute__((unused)) static void print_info(struct map_generic *map) { uint64_t occupied_buckets = 0; int longest_chain = 0; for (uint64_t i = 0; i < map->n_buckets; i++) { struct map_entry_generic **const bucket = &map->buckets[i]; struct map_entry_generic *entry = *bucket; if (entry != NULL) { occupied_buckets += 1; } int chain = 0; while (entry != NULL) { chain += 1; entry = entry->next; } if (chain > longest_chain) { longest_chain = chain; } } fprintf(stderr, "MAP %lu buckets (%lu occupied, %.2f%%) %lu items (%.2f%% load) longest chain %d\n", map->n_buckets, occupied_buckets, (double)occupied_buckets / map->n_buckets * 100, map->n_entries, (double)map->n_entries / map->n_buckets * 100, longest_chain); } static inline uint64_t get_index(uint64_t key, uint64_t n_buckets) { return key & (n_buckets - 1); } static void map_init_generic(struct map_generic *map) { map->n_buckets = INITIAL_BUCKET_COUNT; map->buckets = calloc(map->n_buckets, sizeof(*map->buckets)); map->n_entries = 0; } static void map_rehash_generic(struct map_generic *map, size_t item_size) { const uint64_t old_n_buckets = map->n_buckets; struct map_entry_generic **const old_buckets = map->buckets; map->n_buckets *= 2; map->buckets = calloc(map->n_buckets, sizeof(*map->buckets)); for (uint64_t i = 0; i < old_n_buckets; i++) { struct map_entry_generic **const old_bucket = &old_buckets[i]; if (*old_bucket == NULL) { continue; } struct map_entry_generic *old_entry = *old_bucket; while (old_entry != NULL) { struct map_entry_generic *const next_old_entry = old_entry->next; const uint64_t new_index = get_index(old_entry->key, map->n_buckets); struct map_entry_generic **const new_bucket = &map->buckets[new_index]; struct map_entry_generic *new_entry; if (*new_bucket == NULL) { *new_bucket = malloc(sizeof(struct map_entry_generic) + item_size); new_entry = *new_bucket; } else { struct map_entry_generic *last; for (last = *new_bucket; last->next != NULL; last = last->next); last->next = malloc(sizeof(struct map_entry_generic) + item_size); new_entry = last->next; } new_entry->next = NULL; new_entry->key = old_entry->key; memcpy(&new_entry->data, &old_entry->data, item_size); free(old_entry); old_entry = next_old_entry; } } free(old_buckets); } void *map_insert_or_replace_generic(struct map_generic *map, uint64_t key, const void *item, size_t item_size, bool zero_init) { if (map->n_buckets == 0) { map_init_generic(map); } else if ((map->n_entries * 100 / map->n_buckets) >= REHASH_LOAD_THRESHOLD) { map_rehash_generic(map, item_size); } const uint64_t index = get_index(key, map->n_buckets); struct map_entry_generic **const bucket = &map->buckets[index]; struct map_entry_generic *new_entry; if (*bucket == NULL) { /* empty bucket */ *bucket = malloc(sizeof(struct map_entry_generic) + item_size); new_entry = *bucket; new_entry->next = NULL; map->n_entries += 1; } else { /* walk the linked list */ bool found = false; struct map_entry_generic *entry = *bucket; struct map_entry_generic *prev = NULL; while (entry != NULL) { if (entry->key == key) { found = true; break; } prev = entry; entry = entry->next; } if (found) { /* replace existing entry */ new_entry = entry; } else { /* insert at the end of linked list */ prev->next = malloc(sizeof(struct map_entry_generic) + item_size); new_entry = prev->next; new_entry->next = NULL; map->n_entries += 1; } } new_entry->key = key; if (item != NULL) { memcpy(&new_entry->data, item, item_size); } else if (zero_init) { memset(&new_entry->data, '\0', item_size); } return (void *)&new_entry->data; } void *map_get_generic(struct map_generic *map, uint64_t key) { if (map->n_entries == 0) { return NULL; } const uint64_t index = get_index(key, map->n_buckets); struct map_entry_generic **const bucket = &map->buckets[index]; if (*bucket == NULL) { return NULL; } bool found = false; struct map_entry_generic *entry = *bucket; while (entry != NULL) { if (entry->key == key) { found = true; break; } entry = entry->next; } if (!found) { return NULL; } return (void *)&entry->data; } void map_remove_generic(struct map_generic *map, uint64_t key) { if (map->n_entries == 0) { return; } const uint64_t index = get_index(key, map->n_buckets); struct map_entry_generic **const bucket = &map->buckets[index]; if (*bucket == NULL) { return; } bool found = false; struct map_entry_generic *entry = *bucket; struct map_entry_generic *prev = NULL; while (entry != NULL) { if (entry->key == key) { found = true; break; } prev = entry; entry = entry->next; } if (!found) { return; } if (prev == NULL) { *bucket = entry->next; } else { prev->next = entry->next; } free(entry); map->n_entries -= 1; } void map_free_generic(struct map_generic *map) { for (uint64_t i = 0; i < map->n_buckets; i++) { struct map_entry_generic **const bucket = &map->buckets[i]; struct map_entry_generic *entry = *bucket; while (entry != NULL) { struct map_entry_generic *const next = entry->next; free(entry); entry = next; } } free(map->buckets); map->buckets = NULL; map->n_buckets = 0; map->n_entries = 0; } struct map_iter_state map_iter_init(const struct map_generic *map, void **pitervar) { struct map_iter_state state = {0}; while (state.bucket < map->n_buckets) { if (map->buckets[state.bucket] != NULL) { break; } state.bucket += 1; } if (state.bucket < map->n_buckets) { state.entry = map->buckets[state.bucket]; state.next = state.entry->next; *pitervar = &state.entry->data; } return state; } bool map_iter_is_valid(const struct map_generic *map, struct map_iter_state *state) { return state->bucket < map->n_buckets; } void map_iter_next(const struct map_generic *map, struct map_iter_state *state, void **itervar) { if (state->next != NULL) { state->entry = state->next; state->next = state->entry->next; *itervar = &state->entry->data; } else { while (++state->bucket < map->n_buckets) { if (map->buckets[state->bucket] != NULL) { state->entry = map->buckets[state->bucket]; state->next = state->entry->next; *itervar = &state->entry->data; break; } } } } pipemixer-0.4.0/src/collections/map.h000066400000000000000000000056471512217147000175640ustar00rootroot00000000000000#pragma once #include #include #include struct map_entry_generic { uint64_t key; struct map_entry_generic *next; char data[]; }; struct map_generic { uint64_t n_entries, n_buckets; struct map_entry_generic **buckets; }; #define MAP(type) \ struct { \ size_t n_entries, n_buckets; \ struct { \ uint64_t key; \ struct map_entry_generic *next; \ type data; \ } **buckets; \ } #define MAP_SIZE(pmap) ((pmap)->n_entries) void *map_insert_or_replace_generic(struct map_generic *map, uint64_t key, const void *item, size_t item_size, bool zero_init); #define MAP_INSERT(pmap, key, pitem) \ map_insert_or_replace_generic((struct map_generic *)(pmap), (key), (pitem), \ sizeof(**(pmap)->buckets) - \ sizeof(struct map_entry_generic), \ false) #define MAP_EMPLACE(pmap, key) \ ((__typeof__(&(*(pmap)->buckets)->data)) \ map_insert_or_replace_generic((struct map_generic *)(pmap), (key), NULL, \ sizeof(**(pmap)->buckets) - \ sizeof(struct map_entry_generic), \ false)) #define MAP_EMPLACE_ZEROED(pmap, key) \ ((__typeof__(&(*(pmap)->buckets)->data)) \ map_insert_or_replace_generic((struct map_generic *)(pmap), (key), NULL, \ sizeof(**(pmap)->buckets) - \ sizeof(struct map_entry_generic), \ true)) void *map_get_generic(struct map_generic *map, uint64_t key); #define MAP_GET(pmap, key) \ ((__typeof__(&(*(pmap)->buckets)->data)) \ (map_get_generic((struct map_generic *)(pmap), (key)))) #define MAP_EXISTS(pmap, key) (MAP_GET(pmap, key) != NULL) void map_remove_generic(struct map_generic *map, uint64_t key); #define MAP_REMOVE(pmap, key) \ map_remove_generic((struct map_generic *)(pmap), (key)) void map_free_generic(struct map_generic *map); #define MAP_FREE(pmap) \ map_free_generic((struct map_generic *)(pmap)) struct map_iter_state { uint64_t bucket; struct map_entry_generic *entry; struct map_entry_generic *next; }; struct map_iter_state map_iter_init(const struct map_generic *map, void **itervar); bool map_iter_is_valid(const struct map_generic *map, struct map_iter_state *state); void map_iter_next(const struct map_generic *map, struct map_iter_state *state, void **itervar); #define MAP_FOREACH(pmap, pvar) \ for (struct map_iter_state iter_state = \ map_iter_init((struct map_generic *)(pmap), (void **)(pvar)); \ map_iter_is_valid((struct map_generic *)(pmap), &iter_state); \ map_iter_next((struct map_generic *)(pmap), &iter_state, (void **)(pvar))) pipemixer-0.4.0/src/collections/vec.c000066400000000000000000000070111512217147000175420ustar00rootroot00000000000000#include #include #include "collections/vec.h" #include "macros.h" #include "xmalloc.h" #define GROWTH_FACTOR 1.5 static void vec_ensure_capacity(struct vec_generic *vec, size_t elem_size, size_t cap) { if (cap > vec->capacity) { const size_t new_cap = MAX(cap, vec->capacity * GROWTH_FACTOR); vec->data = xreallocarray(vec->data, new_cap, elem_size); vec->capacity = new_cap; } } static size_t vec_bound_check(const struct vec_generic *vec, size_t index) { if (index >= vec->size) { fprintf(stderr, "Index %zu is out of bounds of vec of size %zu", index, vec->size); fflush(stderr); abort(); } return index; } size_t vec_normalise_index_generic(const struct vec_generic *vec, ptrdiff_t index) { if (index < 0) { return vec_bound_check(vec, vec->size - -index); } else { return vec_bound_check(vec, index); } } void *vec_insert_generic(struct vec_generic *vec, ptrdiff_t _index, const void *elems, size_t elem_size, size_t elem_count, bool zero_init) { size_t index = vec_normalise_index_generic(vec, _index); vec_ensure_capacity(vec, elem_size, vec->size + elem_count); /* shift existing elements to make space for new ones */ memmove((char *)vec->data + ((index + elem_count) * elem_size), (char *)vec->data + (index * elem_size), (vec->size - index) * elem_size); /* copy new elements to vec */ if (elems != NULL) { memcpy((char *)vec->data + (index * elem_size), elems, elem_size * elem_count); } else if (zero_init) { memset((char *)vec->data + (index * elem_size), '\0', elem_size * elem_count); } vec->size += elem_count; return (char *)vec->data + (index * elem_size); } void *vec_append_generic(struct vec_generic *vec, const void *elems, size_t elem_size, size_t elem_count, bool zero_init) { vec_ensure_capacity(vec, elem_size, vec->size + elem_count); /* append new elements to the end */ if (elems != NULL) { memcpy((char *)vec->data + (vec->size * elem_size), elems, elem_size * elem_count); } else if (zero_init) { memset((char *)vec->data + (vec->size * elem_size), '\0', elem_size * elem_count); } vec->size += elem_count; return (char *)vec->data + ((vec->size - elem_count) * elem_size); } void vec_erase_generic(struct vec_generic *vec, ptrdiff_t _index, size_t elem_size, size_t elem_count) { const size_t index = vec_normalise_index_generic(vec, _index); vec_bound_check(vec, index + elem_count - 1); memmove((char *)vec->data + (index * elem_size), (char *)vec->data + ((index + elem_count) * elem_size), (vec->size - index - elem_count) * elem_size); vec->size -= elem_count; } void *vec_at_generic(struct vec_generic *vec, ptrdiff_t _index, size_t elem_size) { size_t index = vec_normalise_index_generic(vec, _index); return (char *)vec->data + (index * elem_size); } void vec_clear_generic(struct vec_generic *vec) { vec->size = 0; } void vec_free_generic(struct vec_generic *vec) { vec->size = 0; vec->capacity = 0; free(vec->data); vec->data = NULL; } void vec_reserve_generic(struct vec_generic *vec, size_t elem_size, size_t elem_count) { vec_ensure_capacity(vec, elem_size, elem_count); } void vec_exchange_generic(struct vec_generic *v1, struct vec_generic *v2) { struct vec_generic tmp = *v1; *v1 = *v2; *v2 = tmp; } pipemixer-0.4.0/src/collections/vec.h000066400000000000000000000145521512217147000175570ustar00rootroot00000000000000#pragma once #include #include struct vec_generic { size_t size, capacity; void *data; }; #define VEC(type) \ struct { \ size_t size, capacity; \ type *data; \ } #define VEC_INITALISER {0} #define VEC_INIT(pvec) \ do { \ (pvec)->size = (pvec)->capacity = 0; \ (pvec)->data = NULL; \ } while (0) #define TYPECHECK_VEC(pvec) \ ({ \ (void)(pvec)->size; (void)(pvec)->capacity; (void)(pvec)->data; 1; \ }) #define TYPECHECK(a, b) \ ({ \ TYPEOF(a) dummy_a; \ TYPEOF(b) dummy_b; \ (void)(&dummy_a == &dummy_b); \ 1; \ }) #define VEC_SIZE(pvec) ((pvec)->size) #define VEC_DATA(pvec) ((pvec)->data) /* Accepts python-style array index and returns real index */ size_t vec_normalise_index_generic(const struct vec_generic *vec, ptrdiff_t index); #define VEC_NORMALISE_INDEX(pvec, index) \ (vec_normalise_index_generic((struct vec_generic *)(pvec), index)) /* * Insert elem_count elements, each of size elem_size, in vec at index. * If elems is not NULL, elements are initialised from elems. * If elems is NULL and zero_init is true, memory is zero-initialised. * If elems is NULL and zero_init is false, memory is NOT initialised. * Returns address of the first inserted element. * Supports python-style negative indexing. * Dumps core on OOB access. */ void *vec_insert_generic(struct vec_generic *vec, ptrdiff_t index, const void *elems, size_t elem_size, size_t elem_count, bool zero_init); #define VEC_INSERT_N(pvec, index, pelem, nelem) \ ({ \ TYPECHECK_VEC(pvec); \ TYPECHECK(*(pvec)->data, *(pelem)); \ vec_insert_generic((struct vec_generic *)(pvec), (index), \ (pelem), sizeof(*(pvec)->data), (nelem), false); \ }) #define VEC_INSERT(pvec, index, pelem) \ VEC_INSERT_N(pvec, index, pelem, 1) #define VEC_EMPLACE_INTERNAL_DO_NOT_USE(pvec, index, nelem, zeroed) \ ({ \ TYPECHECK_VEC(pvec); \ (TYPEOF((pvec)->data))vec_insert_generic((struct vec_generic *)(pvec), (index), \ NULL, sizeof(*(pvec)->data), (nelem), \ (zeroed)); \ }) #define VEC_EMPLACE_N(pvec, index, nelem) \ VEC_EMPLACE_INTERNAL_DO_NOT_USE(pvec, index, nelem, false) #define VEC_EMPLACE(pvec, index) \ VEC_EMPLACE_N(pvec, index, 1) #define VEC_EMPLACE_N_ZEROED(pvec, index, nelem) \ VEC_EMPLACE_INTERNAL_DO_NOT_USE(pvec, index, nelem, true) #define VEC_EMPLACE_ZEROED(pvec, index) \ VEC_EMPLACE_N_ZEROED(pvec, index, 1) /* * Appends elem_count elements, each of size elem_size, to the end of vec. * If elems is not NULL, elements are initialised from elems. * If elems is NULL and zero_init is true, memory is zero-initialised. * If elems is NULL and zero_init is false, memory is NOT initialised. * Returns address of the first appended element. */ void *vec_append_generic(struct vec_generic *vec, const void *elems, size_t elem_size, size_t elem_count, bool zero_init); #define VEC_APPEND_N(pvec, pelem, nelem) \ ({ \ TYPECHECK_VEC(pvec); \ TYPECHECK(*(pvec)->data, *(pelem)); \ vec_append_generic((struct vec_generic *)(pvec), (pelem), \ sizeof(*(pvec)->data), (nelem), false); \ }) #define VEC_APPEND(pvec, pelem) \ VEC_APPEND_N(pvec, pelem, 1) #define VEC_EMPLACE_BACK_INTERNAL_DO_NOT_USE(pvec, nelem, zeroed) \ ({ \ TYPECHECK_VEC(pvec); \ (TYPEOF((pvec)->data))vec_append_generic((struct vec_generic *)(pvec), NULL, \ sizeof(*(pvec)->data), (nelem), (zeroed)); \ }) #define VEC_EMPLACE_BACK_N(pvec, nelem) \ VEC_EMPLACE_BACK_INTERNAL_DO_NOT_USE(pvec, nelem, false) #define VEC_EMPLACE_BACK(pvec) \ VEC_EMPLACE_BACK_N(pvec, 1) #define VEC_EMPLACE_BACK_N_ZEROED(pvec, nelem) \ VEC_EMPLACE_BACK_INTERNAL_DO_NOT_USE(pvec, nelem, true) #define VEC_EMPLACE_BACK_ZEROED(pvec) \ VEC_EMPLACE_BACK_N_ZEROED(pvec, 1) /* * Removes elem_count elements, each of size elem_size, at index from vec. * Support python-style negative indexing. * Dumps core on OOB access. */ void vec_erase_generic(struct vec_generic *vec, ptrdiff_t index, size_t elem_size, size_t elem_count); #define VEC_ERASE_N(pvec, index, count) \ ({ \ TYPECHECK_VEC(pvec); \ vec_erase_generic((struct vec_generic *)(pvec), \ (index), sizeof(*(pvec)->data), (count)); \ }) #define VEC_ERASE(pvec, index) \ VEC_ERASE_N(pvec, index, 1) /* * Returns pointer to element of vec at index. * Supports python-style negative indexing. * Dumps core on OOB access. */ void *vec_at_generic(struct vec_generic *vec, ptrdiff_t index, size_t elem_size); #define VEC_AT(pvec, index) \ ({ \ TYPECHECK_VEC(pvec); \ (TYPEOF((pvec)->data))vec_at_generic((struct vec_generic *)(pvec), \ (index), sizeof(*(pvec)->data)); \ }) /* * Sets size to 0 but does not free memory. */ void vec_clear_generic(struct vec_generic *vec); #define VEC_CLEAR(pvec) \ ({ \ TYPECHECK_VEC(pvec); \ vec_clear_generic((struct vec_generic *)(pvec)); \ }) /* * Frees all memory and makes vec ready for reuse. */ void vec_free_generic(struct vec_generic *vec); #define VEC_FREE(pvec) \ ({ \ TYPECHECK_VEC(pvec); \ vec_free_generic((struct vec_generic *)(pvec)); \ }) /* * Reserves memory for elem_count elements, each of size elem_size. */ void vec_reserve_generic(struct vec_generic *vec, size_t elem_size, size_t elem_count); #define VEC_RESERVE(pvec, count) \ ({ \ TYPECHECK_VEC(pvec); \ vec_reserve_generic((struct vec_generic *)(pvec), sizeof(*(pvec)->data), (count)); \ }) /* * Swaps contents of v1 and v2 */ void vec_exchange_generic(struct vec_generic *v1, struct vec_generic *v2); #define VEC_EXCHANGE(pvec1, pvec2) \ ({ \ TYPECHECK((pvec1)->data, (pvec2)->data); \ vec_exchange_generic((struct vec_generic *)(pvec1), (struct vec_generic *)(pvec2)); \ }) #define VEC_FOREACH(pvec, iter) \ for (size_t iter = 0; iter < (pvec)->size; iter++) #define VEC_FOREACH_REVERSE(pvec, iter) \ for (size_t iter = (pvec)->size; iter-- > 0; ) pipemixer-0.4.0/src/config.c000066400000000000000000000364701512217147000157270ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "xmalloc.h" #include "tui.h" #include "config.h" #include "utils.h" #include "macros.h" #define ADD_BIND(keycode, function, data_type, data_value) \ do { \ struct tui_bind bind = { \ .func = function, \ .data.data_type = data_value, \ }; \ MAP_INSERT(&config.binds, keycode, &bind); \ } while (0) struct pipemixer_config config = { .volume_step = 0.01, .volume_min = 0.00, .volume_max = 1.50, .wraparound = false, .display_ids = false, .default_tab = PLAYBACK, .bar_full_char = L"#", .bar_empty_char = L"-", .volume_frame = { .tl = L"┌", .tr = L"┐", .bl = L"└", .br = L"┘", .cl = L"├", .cr = L"┤", .ml = L"╶", .mr = L"╴", .f = L"─", }, .borders = { .ls = L"│", .rs = L"│", .ts = L"─", .bs = L"─", .tl = L"┌", .tr = L"┐", .bl = L"└", .br = L"┘", }, .routes_separator = ", ", .profiles_separator = ", ", }; static const char *get_default_config_path(void) { static char path[PATH_MAX]; const char *home = getenv("HOME"); const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); if (xdg_config_home != NULL) { snprintf(path, sizeof(path), "%s/pipemixer/pipemixer.ini", xdg_config_home); return path; } else if (home != NULL) { snprintf(path, sizeof(path), "%s/.config/pipemixer/pipemixer.ini", home); return path; } else { /* config will be parsed before initalising curses so printfing here is fine */ fprintf(stderr, "config: HOME and XDG_CONFIG_HOME are unset, cannot determine config path\n"); return NULL; } } static bool get_first_wchar(const char *str, wchar_t *res) { size_t len = strlen(str); wchar_t res_tmp; mbtowc(NULL, NULL, 0); /* reset mbtowc state */ if (mbtowc(&res_tmp, str, len) < 1) { return false; } *res = res_tmp; return true; } static bool get_percentage(const char *str, float *res) { unsigned long tmp; if (!str_to_ulong(str, &tmp)) { return false; } *res = (float)tmp * 0.01; return true; } static bool get_bool(const char *str, bool *res) { if (STREQ(str, "1") || STRCASEEQ(str, "yes") || STRCASEEQ(str, "true")) { *res = true; return true; } else if (STREQ(str, "0") || STRCASEEQ(str, "no") || STRCASEEQ(str, "false")) { *res = false; return true; } else { return false; } } static enum tui_tab_type tui_tab_from_name(const char *name) { if (STRCASEEQ(name, "playback")) return PLAYBACK; else if (STRCASEEQ(name, "recording")) return RECORDING; else if (STRCASEEQ(name, "input-devices")) return INPUT_DEVICES; else if (STRCASEEQ(name, "output-devices")) return OUTPUT_DEVICES; else if (STRCASEEQ(name, "cards")) return CARDS; else return TUI_TAB_INVALID; } static bool get_tab_order(const char *_str) { char *str = xstrdup(_str); bool ret = true; int map_index_to_enum[TUI_TAB_COUNT]; int map_enum_to_index[TUI_TAB_COUNT]; bool seen[TUI_TAB_COUNT]; memset(seen, false, sizeof(seen)); int index = 0; for (char *tok = strtok(str, ","); tok != NULL; tok = strtok(NULL, ",")) { const enum tui_tab_type tab = tui_tab_from_name(tok); if (tab == TUI_TAB_INVALID) { ret = false; goto out; } seen[tab] = true; map_index_to_enum[index] = tab; map_enum_to_index[tab] = index; index += 1; } for (int i = 0; i < TUI_TAB_COUNT; i++) { if (!seen[i]) { ret = false; goto out; } } memcpy(&config.tab_map_index_to_enum, map_index_to_enum, sizeof(config.tab_map_index_to_enum)); memcpy(&config.tab_map_enum_to_index, map_enum_to_index, sizeof(config.tab_map_enum_to_index)); out: free(str); return ret; } static int key_value_handler(void *data, const char *s, const char *k, const char *v) { #define CONFIG_LOG(fmt, ...) \ fprintf(stderr, "config: (%s::%s) "fmt"\n", s, k, ##__VA_ARGS__) #define CONFIG_GET_WCHAR(dst) \ if (!get_first_wchar(v, dst)) CONFIG_LOG("invalid or incomplete multibyte sequence") #define CONFIG_GET_PERCENTAGE(dst) \ if (!get_percentage(v, dst)) CONFIG_LOG("invalid percentage value") #define CONFIG_GET_BOOL(dst) \ if (!get_bool(v, dst)) CONFIG_LOG("invalid boolean: %s", v) if (STREQ(s, "main")) { if (STREQ(k, "volume-step")) { CONFIG_GET_PERCENTAGE(&config.volume_step); } else if (STREQ(k, "volume-min")) { CONFIG_GET_PERCENTAGE(&config.volume_min); } else if (STREQ(k, "volume-max")) { CONFIG_GET_PERCENTAGE(&config.volume_max); } else if (STREQ(k, "wraparound")) { CONFIG_GET_BOOL(&config.wraparound); } else if (STREQ(k, "display-ids")) { CONFIG_GET_BOOL(&config.display_ids); } else if (STREQ(k, "tab-order")) { if (!get_tab_order(v)) CONFIG_LOG("invalid tab order string: %s", v); } else if (STREQ(k, "default-tab")) { enum tui_tab_type tab = tui_tab_from_name(v); if (tab == TUI_TAB_INVALID) { CONFIG_LOG("invalid tab name: %s", v); } else { config.default_tab = tab; } } else { CONFIG_LOG("unknown key %s in section %s", k, s); } } else if (STREQ(s, "interface")) { if (STREQ(k, "routes-separator")) { config.routes_separator = xstrdup(v); } else if (STREQ(k, "profiles-separator")) { config.profiles_separator = xstrdup(v); } else if (STREQ(k, "border-left")) { CONFIG_GET_WCHAR(&config.borders.ls[0]); } else if (STREQ(k, "border-right")) { CONFIG_GET_WCHAR(&config.borders.rs[0]); } else if (STREQ(k, "border-top")) { CONFIG_GET_WCHAR(&config.borders.ts[0]); } else if (STREQ(k, "border-bottom")) { CONFIG_GET_WCHAR(&config.borders.bs[0]); } else if (STREQ(k, "border-top-left")) { CONFIG_GET_WCHAR(&config.borders.tl[0]); } else if (STREQ(k, "border-top-right")) { CONFIG_GET_WCHAR(&config.borders.tr[0]); } else if (STREQ(k, "border-bottom-left")) { CONFIG_GET_WCHAR(&config.borders.bl[0]); } else if (STREQ(k, "border-bottom-right")) { CONFIG_GET_WCHAR(&config.borders.br[0]); } else if (STREQ(k, "volume-frame-center-left")) { CONFIG_GET_WCHAR(&config.volume_frame.cl[0]); } else if (STREQ(k, "volume-frame-center-right")) { CONFIG_GET_WCHAR(&config.volume_frame.cr[0]); } else if (STREQ(k, "volume-frame-top-left")) { CONFIG_GET_WCHAR(&config.volume_frame.tl[0]); } else if (STREQ(k, "volume-frame-top-right")) { CONFIG_GET_WCHAR(&config.volume_frame.tr[0]); } else if (STREQ(k, "volume-frame-bottom-left")) { CONFIG_GET_WCHAR(&config.volume_frame.bl[0]); } else if (STREQ(k, "volume-frame-bottom-right")) { CONFIG_GET_WCHAR(&config.volume_frame.br[0]); } else if (STREQ(k, "volume-frame-mono-left")) { CONFIG_GET_WCHAR(&config.volume_frame.ml[0]); } else if (STREQ(k, "volume-frame-mono-right")) { CONFIG_GET_WCHAR(&config.volume_frame.mr[0]); } else if (STREQ(k, "volume-frame-focus")) { CONFIG_GET_WCHAR(&config.volume_frame.f[0]); } else if (STREQ(k, "bar-full-char")) { CONFIG_GET_WCHAR(&config.bar_full_char[0]); } else if (STREQ(k, "bar-empty-char")) { CONFIG_GET_WCHAR(&config.bar_empty_char[0]); } else { CONFIG_LOG("unknown key %s in section %s", k, s); } } else if (STREQ(s, "binds")) { wint_t keycode; if (!key_code_from_key_name(v, &keycode)) { CONFIG_LOG("invalid keycode: %s", v); } else { const char *prefix = NULL; if (prefix = "focus-", STRSTARTSWITH(k, prefix)) { if (STREQ(k + strlen(prefix), "up")) { ADD_BIND(keycode, tui_bind_change_focus, direction, UP); } else if (STREQ(k + strlen(prefix), "down")) { ADD_BIND(keycode, tui_bind_change_focus, direction, DOWN); } else if (STREQ(k + strlen(prefix), "first")) { ADD_BIND(keycode, tui_bind_focus_first, nothing, NOTHING); } else if (STREQ(k + strlen(prefix), "last")) { ADD_BIND(keycode, tui_bind_focus_last, nothing, NOTHING); } else { CONFIG_LOG("unknown action: %s", k); } } else if (prefix = "volume-set-", STRSTARTSWITH(k, prefix)) { const char *vol_str = k + strlen(prefix); unsigned long vol; if (!str_to_ulong(vol_str, &vol)) { CONFIG_LOG("%s is not a valid integer", vol_str); } else { ADD_BIND(keycode, tui_bind_set_volume, volume, (float)vol * 0.01); } } else if (prefix = "volume-", STRSTARTSWITH(k, prefix)) { if (STREQ(k + strlen(prefix), "up")) { ADD_BIND(keycode, tui_bind_change_volume, direction, UP); } else if (STREQ(k + strlen(prefix), "down")) { ADD_BIND(keycode, tui_bind_change_volume, direction, DOWN); } else { CONFIG_LOG("unknown action: %s", k); } } else if (prefix = "mute-", STRSTARTSWITH(k, prefix)) { if (STREQ(k + strlen(prefix), "enable")) { ADD_BIND(keycode, tui_bind_change_mute, change_mode, ENABLE); } else if (STREQ(k + strlen(prefix), "disable")) { ADD_BIND(keycode, tui_bind_change_mute, change_mode, DISABLE); } else if (STREQ(k + strlen(prefix), "toggle")) { ADD_BIND(keycode, tui_bind_change_mute, change_mode, TOGGLE); } else { CONFIG_LOG("unknown action: %s", k); } } else if (prefix = "channel-lock-", STRSTARTSWITH(k, prefix)) { if (STREQ(k + strlen(prefix), "enable")) { ADD_BIND(keycode, tui_bind_change_channel_lock, change_mode, ENABLE); } else if (STREQ(k + strlen(prefix), "disable")) { ADD_BIND(keycode, tui_bind_change_channel_lock, change_mode, DISABLE); } else if (STREQ(k + strlen(prefix), "toggle")) { ADD_BIND(keycode, tui_bind_change_channel_lock, change_mode, TOGGLE); } else { CONFIG_LOG("unknown action: %s", k); } } else if (prefix = "tab-", STRSTARTSWITH(k, prefix)) { if (STREQ(k + strlen(prefix), "next")) { ADD_BIND(keycode, tui_bind_change_tab, direction, UP); } else if (STREQ(k + strlen(prefix), "prev")) { ADD_BIND(keycode, tui_bind_change_tab, direction, DOWN); } else if (STREQ(k + strlen(prefix), "playback")) { ADD_BIND(keycode, tui_bind_set_tab, tab, PLAYBACK); } else if (STREQ(k + strlen(prefix), "recording")) { ADD_BIND(keycode, tui_bind_set_tab, tab, RECORDING); } else if (STREQ(k + strlen(prefix), "input-devices")) { ADD_BIND(keycode, tui_bind_set_tab, tab, INPUT_DEVICES); } else if (STREQ(k + strlen(prefix), "output-devices")) { ADD_BIND(keycode, tui_bind_set_tab, tab, OUTPUT_DEVICES); } else { uint32_t index; if (str_to_u32(k + strlen(prefix), &index) && index >= 1 && index <= TUI_TAB_COUNT) { ADD_BIND(keycode, tui_bind_set_tab_index, index, index - 1); } else { CONFIG_LOG("unknown action: %s", k); } } } else if (STREQ(k, "set-default")) { ADD_BIND(keycode, tui_bind_set_default, nothing, NOTHING); } else if (STREQ(k, "select-route")) { ADD_BIND(keycode, tui_bind_select_route, nothing, NOTHING); } else if (STREQ(k, "select-profile")) { ADD_BIND(keycode, tui_bind_select_profile, nothing, NOTHING); } else if (STREQ(k, "confirm-selection")) { ADD_BIND(keycode, tui_bind_confirm_selection, nothing, NOTHING); } else if (STREQ(k, "cancel-selection")) { ADD_BIND(keycode, tui_bind_cancel_selection, nothing, NOTHING); } else if (STREQ(k, "quit-or-cancel-selection")) { ADD_BIND(keycode, tui_bind_quit_or_cancel_selection, nothing, NOTHING); } else if (STREQ(k, "quit")) { ADD_BIND(keycode, tui_bind_quit, nothing, NOTHING); } else if (STREQ(k, "unbind")) { MAP_REMOVE(&config.binds, keycode); } else { CONFIG_LOG("unknown action: %s", k); } } } else { CONFIG_LOG("unknown section %s", s); } return 0; #undef CONFIG_GET_BOOL #undef CONFIG_GET_PERCENTAGE #undef CONFIG_GET_WCHAR #undef CONFIG_LOG } static void parse_config(const char *config) { ini_parse_string(config, key_value_handler, NULL); } static void add_default_config(void) { static const char default_config[] = "[main]\n" "tab-order=playback,recording,output-devices,input-devices,cards\n" "[binds]\n" "focus-down=j\n" "focus-down=down\n" "focus-up=k\n" "focus-up=up\n" "focus-first=g\n" "focus-last=G\n" "volume-up=l\n" "volume-up=right\n" "volume-down=h\n" "volume-down=left\n" "tab-next=t\n" "tab-next=tab\n" "tab-prev=T\n" "tab-prev=backtab\n" "tab-1=1\n" "tab-2=2\n" "tab-3=3\n" "tab-4=4\n" "tab-5=5\n" "mute-toggle=m\n" "channel-lock-toggle=space\n" "select-route=p\n" "select-profile=P\n" "set-default=D\n" "confirm-selection=enter\n" "quit-or-cancel-selection=escape\n" "quit=q\n" ; parse_config(default_config); } void load_config(const char *config_path) { int fd = -1; char *config_str = NULL; add_default_config(); if (config_path == NULL) { config_path = get_default_config_path(); } if (config_path != NULL) { fd = open(config_path, O_RDONLY); if (fd < 0) { fprintf(stderr, "config: failed to open %s: %s\n", config_path, strerror(errno)); goto out; } config_str = read_string_from_fd(fd, NULL); if (config_str == NULL) { fprintf(stderr, "config: failed to read config file: %s\n", strerror(errno)); goto out; } parse_config(config_str); } out: if (fd > 0) { close(fd); } if (config_str != NULL) { free(config_str); } } pipemixer-0.4.0/src/config.h000066400000000000000000000022121512217147000157170ustar00rootroot00000000000000#pragma once #include #include "collections/map.h" #include "tui.h" struct pipemixer_config { float volume_step; float volume_min, volume_max; /* value of 60 means use 60% of available screen width */ int volume_bar_width_percentage; /* upper limit of volume bar */ float volume_display_max; bool wraparound; bool display_ids; enum tui_tab_type default_tab; wchar_t bar_full_char[2], bar_empty_char[2]; struct { wchar_t tl[2], tr[2], bl[2], br[2], cl[2], cr[2], ml[2], mr[2], f[2]; } volume_frame; struct { /* see curs_border(3x) */ wchar_t ls[2], rs[2], ts[2], bs[2], tl[2], tr[2], bl[2], br[2]; } borders; char *routes_separator; char *profiles_separator; /* tab_map_index_to_enum[i] returns enum tui_tab, i is tab position in the ui */ enum tui_tab_type tab_map_index_to_enum[TUI_TAB_COUNT]; /* tab_map_enum_to_index[enum tui_tab] returns i, i is tab position in the ui */ int tab_map_enum_to_index[TUI_TAB_COUNT]; MAP(struct tui_bind) binds; }; extern struct pipemixer_config config; void load_config(const char *config_path); pipemixer-0.4.0/src/eventloop.c000066400000000000000000000010411512217147000164570ustar00rootroot00000000000000#include #include "xmalloc.h" #define POLLEN_CALLOC(n, size) xcalloc(n, size) #define POLLEN_FREE(ptr) free(ptr) #include "log.h" //#define POLLEN_LOG_DEBUG(fmt, ...) DEBUG("event loop: " fmt, ##__VA_ARGS__) #define POLLEN_LOG_INFO(fmt, ...) INFO("event loop: " fmt, ##__VA_ARGS__) #define POLLEN_LOG_WARN(fmt, ...) WARN("event loop: " fmt, ##__VA_ARGS__) #define POLLEN_LOG_ERR(fmt, ...) ERROR("event loop: " fmt, ##__VA_ARGS__) #define POLLEN_IMPLEMENTATION #include "lib/pollen/pollen.h" struct pollen_loop *event_loop = NULL; pipemixer-0.4.0/src/eventloop.h000066400000000000000000000001261512217147000164670ustar00rootroot00000000000000#pragma once #include "lib/pollen/pollen.h" extern struct pollen_loop *event_loop; pipemixer-0.4.0/src/log.c000066400000000000000000000051371512217147000152370ustar00rootroot00000000000000#include #include #include #include #include "log.h" #define LOG_ANSI_COLORS_ERROR "\033[31m" #define LOG_ANSI_COLORS_WARN "\033[33m" #define LOG_ANSI_COLORS_DEBUG "\033[2m" #define LOG_ANSI_COLORS_RESET "\033[0m" struct log_config { FILE *stream; enum log_loglevel loglevel; bool colors; }; static struct log_config log_config = { .stream = NULL, .loglevel = LOG_INFO, .colors = false, }; enum log_loglevel log_str_to_loglevel(const char *str) { if (strcasecmp(str, "trace") == 0) { return LOG_TRACE; } else if (strcasecmp(str, "debug") == 0) { return LOG_DEBUG; } else if (strcasecmp(str, "info") == 0) { return LOG_INFO; } else if (strcasecmp(str, "warn") == 0) { return LOG_WARN; } else if (strcasecmp(str, "error") == 0) { return LOG_ERROR; } else if (strcasecmp(str, "quiet") == 0) { return LOG_QUIET; } else { return LOG_INVALID; } } void log_init(FILE *stream, enum log_loglevel level, bool force_colors) { log_config.stream = stream; log_config.loglevel = level; log_config.colors = force_colors ? true : isatty(fileno(stream)); } void log_print(enum log_loglevel level, char *message, ...) { if (log_config.stream == NULL) { return; } if (level > log_config.loglevel) { return; } char level_char = '?'; switch (level) { case LOG_ERROR: if (log_config.colors) { fprintf(log_config.stream, LOG_ANSI_COLORS_ERROR); } level_char = 'E'; break; case LOG_WARN: if (log_config.colors) { fprintf(log_config.stream, LOG_ANSI_COLORS_WARN); } level_char = 'W'; break; case LOG_INFO: /* no special color here... */ level_char = 'I'; break; case LOG_DEBUG: if (log_config.colors) { fprintf(log_config.stream, LOG_ANSI_COLORS_DEBUG); } level_char = 'D'; break; case LOG_TRACE: if (log_config.colors) { fprintf(log_config.stream, LOG_ANSI_COLORS_DEBUG); } level_char = 'T'; break; default: fprintf(stderr, "logger error: unknown loglevel %d\n", level); abort(); } fprintf(log_config.stream, "%c ", level_char); va_list args; va_start(args, message); vfprintf(log_config.stream, message, args); va_end(args); if (log_config.colors) { fprintf(log_config.stream, LOG_ANSI_COLORS_RESET); } fprintf(log_config.stream, "\n"); fflush(log_config.stream); } pipemixer-0.4.0/src/log.h000066400000000000000000000027141512217147000152420ustar00rootroot00000000000000#pragma once #include #include #define PRINTF(fmt_index, first_arg_index) \ __attribute__((format(printf, fmt_index, first_arg_index))) enum log_loglevel { LOG_INVALID, LOG_QUIET, LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG, LOG_TRACE, }; enum log_loglevel log_str_to_loglevel(const char *str); void log_init(FILE *stream, enum log_loglevel level, bool force_colors); PRINTF(2, 3) void log_print(enum log_loglevel level, char *msg, ...); /* ncurses has a trace macro so make this one uppercase, TODO: make others uppercase too */ #define TRACE(fmt, ...) \ do { \ log_print(LOG_TRACE, \ "%s:%-3d " fmt, \ __FILE__, __LINE__, ##__VA_ARGS__); \ } while (0) #define DEBUG(fmt, ...) \ do { \ log_print(LOG_DEBUG, \ "%s:%-3d " fmt, \ __FILE__, __LINE__, ##__VA_ARGS__); \ } while (0) #define INFO(fmt, ...) \ do { \ log_print(LOG_INFO, \ "%s:%-3d " fmt, \ __FILE__, __LINE__, ##__VA_ARGS__); \ } while (0) #define WARN(fmt, ...) \ do { \ log_print(LOG_WARN, \ "%s:%-3d " fmt, \ __FILE__, __LINE__, ##__VA_ARGS__); \ } while (0) #define ERROR(fmt, ...) \ do { \ log_print(LOG_ERROR, \ "%s:%-3d " fmt, \ __FILE__, __LINE__, ##__VA_ARGS__); \ } while (0) #undef PRINTF pipemixer-0.4.0/src/macros.h000066400000000000000000000021441512217147000157420ustar00rootroot00000000000000#pragma once #define STREQ(a, b) (strcmp((a), (b)) == 0) #define STRCASEEQ(a, b) (strcasecmp((a), (b)) == 0) #define STRSTARTSWITH(a, b) (strncmp((a), (b), strlen(b)) == 0) #define BYTE_BINARY_FORMAT "0b%c%c%c%c%c%c%c%c" #define BYTE_BINARY_ARGS(byte) \ ((byte) & (1 << 7) ? '1' : '0'), \ ((byte) & (1 << 6) ? '1' : '0'), \ ((byte) & (1 << 5) ? '1' : '0'), \ ((byte) & (1 << 4) ? '1' : '0'), \ ((byte) & (1 << 3) ? '1' : '0'), \ ((byte) & (1 << 2) ? '1' : '0'), \ ((byte) & (1 << 1) ? '1' : '0'), \ ((byte) & (1 << 0) ? '1' : '0') #define MAX(a, b) (((a) > (b)) ? (a) : (b)) #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #define TYPEOF(x) __typeof__(x) #define CONTAINER_OF(member_ptr, container_ptr, member_name) \ ((TYPEOF(container_ptr))((char *)(member_ptr) - offsetof(TYPEOF(*container_ptr), member_name))) #define SIZEOF_ARRAY(arr) (sizeof(arr) / sizeof((arr)[0])) #define SWAP(a, b) \ do { \ TYPEOF(a) tmp = (a); \ (a) = (b); \ (b) = tmp; \ } while (0) #define _3(a, b) a##b #define _2(a, b) _3(a, b) #define _ _2(_dummy_param_, __COUNTER__) pipemixer-0.4.0/src/menu.c000066400000000000000000000047121512217147000154200ustar00rootroot00000000000000#include "menu.h" #include "xmalloc.h" #include "config.h" #include "log.h" void tui_menu_resize(struct tui_menu *const menu, int term_width, int term_height) { menu->x = 1; menu->y = 2; menu->w = term_width - 2; menu->h = term_height - 4; if (menu->win == NULL) { menu->win = newwin(menu->h, menu->w, menu->y, menu->x); } else { wresize(menu->win, menu->h, menu->w); } } struct tui_menu *tui_menu_create(unsigned int n_items) { struct tui_menu *menu = xzalloc(sizeof(*menu) + (sizeof(*menu->items) * n_items)); menu->n_items = n_items; return menu; } void tui_menu_free(struct tui_menu *menu) { for (unsigned int i = 0; i < menu->n_items; i++) { free(menu->items[i].str); } free(menu->header); free(menu); } bool tui_menu_change_focus(struct tui_menu *const menu, int direction) { bool change = false; if (direction < 0) { if (menu->selected > 0) { menu->selected -= 1; change = true; } else if (config.wraparound) { menu->selected = menu->n_items - 1; change = true; } } else { if (menu->selected < menu->n_items - 1) { menu->selected += 1; change = true; } else if (config.wraparound) { menu->selected = 0; change = true; } } return change; } void tui_menu_draw(const struct tui_menu *const menu) { TRACE("tui_draw_menu: %dx%d at %dx%d", menu->w, menu->h, menu->x, menu->y); WINDOW *win = menu->win; werase(win); /* box */ wmove(win, 0, 0); waddwstr(win, config.borders.tl); for (int x = 1; x < menu->w - 1; x++) { waddwstr(win, config.borders.ts); } waddwstr(win, config.borders.tr); wmove(win, menu->h - 1, 0); waddwstr(win, config.borders.bl); for (int x = 1; x < menu->w - 1; x++) { waddwstr(win, config.borders.bs); } waddwstr(win, config.borders.br); for (int y = 1; y < menu->h - 1; y++) { wmove(win, y, 0); waddwstr(win, config.borders.ls); wmove(win, y, menu->w - 1); waddwstr(win, config.borders.rs); } mvwaddnstr(win, 0, 1, menu->header, menu->w - 2); for (unsigned int i = 0; i < menu->n_items; i++) { if (i == menu->selected) { wattron(win, A_BOLD); } mvwaddnstr(win, 1 + i, 1, menu->items[i].str, menu->w - 2); wattroff(win, A_BOLD); } wnoutrefresh(win); } pipemixer-0.4.0/src/menu.h000066400000000000000000000015051512217147000154220ustar00rootroot00000000000000#pragma once #include struct tui_menu; struct tui_menu_item; typedef void (*tui_menu_callback_t)(struct tui_menu *menu, struct tui_menu_item *pick); struct tui_menu_item { char *str; union { void *ptr; uintptr_t uint; } data; }; struct tui_menu { WINDOW *win; int x, y, w, h; char *header; tui_menu_callback_t callback; union { void *ptr; uintptr_t uint; } data; unsigned int n_items; unsigned int selected; struct tui_menu_item items[]; }; struct tui_menu *tui_menu_create(unsigned int n_items); void tui_menu_resize(struct tui_menu *menu, int term_width, int term_height); void tui_menu_free(struct tui_menu *menu); void tui_menu_draw(const struct tui_menu *menu); bool tui_menu_change_focus(struct tui_menu *menu, int direction); pipemixer-0.4.0/src/pipemixer.c000066400000000000000000000114641512217147000164600ustar00rootroot00000000000000#include #include #include #include #include #include "log.h" #include "tui.h" #include "config.h" #include "utils.h" #include "eventloop.h" #include "pw/common.h" static void crash_handler(int sig) { /* restore terminal state before crashing */ endwin(); raise(sig); } static int sigint_sigterm_handler(struct pollen_event_source *_, int signal, void *_) { INFO("caught signal %d, stopping main loop", signal); pollen_loop_quit(event_loop, 0); return 0; } void print_help_and_exit(FILE *stream, int exit_status) { const char help_string[] = "pipemixer - pipewire volume control\n" "\n" "usage:\n" " pipemixer [OPTIONS]\n" "\n" "command line options:\n" " -c, --config path to configuration file\n" " -l, --loglevel one of TRACE, DEBUG, INFO, WARN, ERROR, QUIET\n" " -L, --log-fd write log to this fd (must be open for writing)\n" " -C, --color force logging with colors\n" " -V, --version print version information\n" " -h, --help print this help message and exit\n"; fputs(help_string, stream); exit(exit_status); } void print_version_and_exit(FILE *stream, int exit_status) { const char version_string[] = "pipemixer version " PIPEMIXER_VERSION #ifdef PIPEMIXER_GIT_TAG ", git tag " PIPEMIXER_GIT_TAG #endif #ifdef PIPEMIXER_GIT_BRANCH ", git branch " PIPEMIXER_GIT_BRANCH #endif "\n" ; fputs(version_string, stream); exit(exit_status); } int main(int argc, char **argv) { int retcode = 0; const char *config_path = NULL; FILE *log_stream = NULL; int log_fd = -1; enum log_loglevel loglevel = LOG_DEBUG; bool log_force_colors = false; static const char shortopts[] = "c:L:l:CVh"; static const struct option longopts[] = { { "config", required_argument, NULL, 'c' }, { "log-fd", required_argument, NULL, 'L' }, { "loglevel", required_argument, NULL, 'l' }, { "color", no_argument, NULL, 'C' }, { "version", no_argument, NULL, 'V' }, { "help", no_argument, NULL, 'h' }, { 0 } }; int c; while ((c = getopt_long(argc, argv, shortopts, longopts, NULL)) > 0) { switch (c) { case 'c': config_path = optarg; break; case 'L': if (!str_to_i32(optarg, &log_fd)) { fprintf(stderr, "failed to convert %s to integer\n", optarg); exit(1); } break; case 'l': loglevel = log_str_to_loglevel(optarg); if (loglevel == LOG_INVALID) { fprintf(stderr, "%s is not a valid loglevel\n", optarg); exit(1); } break; case 'C': log_force_colors = true; break; case 'V': print_version_and_exit(stdout, 0); break; case 'h': print_help_and_exit(stdout, 0); break; default: print_help_and_exit(stderr, 1); break; } } if (log_fd != -1) { log_stream = fdopen(log_fd, "w"); if (log_stream == NULL) { fprintf(stderr, "failed to fdopen() fd %d: %s\n", log_fd, strerror(errno)); exit(1); } log_init(log_stream, loglevel, log_force_colors); } /* needed for unicode support in ncurses and correct unicode handling in config */ setlocale(LC_ALL, ""); load_config(config_path); event_loop = pollen_loop_create(); if (event_loop == NULL) { fprintf(stderr, "pipemixer: failed to create event loop\n"); retcode = 1; goto cleanup; } signals_global_init(); if (pipewire_init() < 0) { fprintf(stderr, "pipemixer: failed to connect to pipewire\n"); retcode = 1; goto cleanup; } /* setup crash handler before initialising ncurses */ struct sigaction sa = { .sa_handler = crash_handler, .sa_flags = SA_NODEFER | SA_RESETHAND, }; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGABRT, &sa, NULL); sigaction(SIGFPE, &sa, NULL); sigaction(SIGILL, &sa, NULL); sigaction(SIGBUS, &sa, NULL); tui_init(); pollen_loop_add_signal(event_loop, SIGTERM, sigint_sigterm_handler, NULL); pollen_loop_add_signal(event_loop, SIGINT, sigint_sigterm_handler, NULL); retcode = pollen_loop_run(event_loop); cleanup: pollen_loop_cleanup(event_loop); pipewire_cleanup(); tui_cleanup(); if (log_stream != NULL) { fclose(log_stream); } /* see https://invisible-island.net/ncurses/man/curs_memleaks.3x.html */ exit_curses(retcode); } pipemixer-0.4.0/src/pw/000077500000000000000000000000001512217147000147325ustar00rootroot00000000000000pipemixer-0.4.0/src/pw/common.c000066400000000000000000000121151512217147000163660ustar00rootroot00000000000000#include #include "pw/common.h" #include "pw/node.h" #include "pw/device.h" #include "pw/default.h" #include "eventloop.h" #include "macros.h" #include "utils.h" #include "log.h" struct pw pw = {0}; static void on_registry_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { DEBUG("registry global: id %d, perms "PW_PERMISSION_FORMAT", ver %d, type %s", id, PW_PERMISSION_ARGS(permissions), version, type); uint32_t i = 0; const struct spa_dict_item *item; spa_dict_for_each(item, props) { TRACE("%c---%s: %s", (++i == props->n_items ? '\\' : '|'), item->key, item->value); } if (STREQ(type, PW_TYPE_INTERFACE_Node)) { const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); enum media_class media_class_value; if (media_class == NULL) { DEBUG("empty media.class, not binding"); return; } else if (STREQ(media_class, "Audio/Source")) { media_class_value = AUDIO_SOURCE; } else if (STREQ(media_class, "Audio/Sink")) { media_class_value = AUDIO_SINK; } else if (STREQ(media_class, "Stream/Input/Audio")) { media_class_value = STREAM_INPUT_AUDIO; } else if (STREQ(media_class, "Stream/Output/Audio")) { media_class_value = STREAM_OUTPUT_AUDIO; } else { DEBUG("not interested in media.class %s, not binding", media_class); return; } node_create(id, media_class_value); } else if (STREQ(type, PW_TYPE_INTERFACE_Device)) { const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); if (media_class == NULL) { DEBUG("empty media.class, not binding"); return; } else if (STREQ(media_class, "Audio/Device")) { /* no-op */ } else { DEBUG("not interested in media.class %s, not binding", media_class); return; } device_create(id); } else if (STREQ(type, PW_TYPE_INTERFACE_Metadata)) { const char *metadata_name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); if (!streq(metadata_name, "default")) { return; } struct default_metadata *md = &pw.default_metadata; INFO("get default metadata, id %d", id); if (md->id != 0) { WARN("got default metadata object again??? wtf"); return; } default_metadata_init(md, id); } } static void on_registry_global_remove(void *data, uint32_t id) { DEBUG("registry global remove: id %d", id); struct node *node = node_lookup(id); if (node != NULL) { node_destroy(node); return; } struct device *device = device_lookup(id); if (device != NULL) { device_destroy(device); } } static const struct pw_registry_events registry_events = { .version = PW_VERSION_REGISTRY_EVENTS, .global = on_registry_global, .global_remove = on_registry_global_remove, }; static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) { ERROR("core error %d on object %d: %d (%s)", seq, id, res, message); } static const struct pw_core_events core_events = { .version = PW_VERSION_CORE_EVENTS, .error = on_core_error, }; static int pipewire_fd_handler(struct pollen_event_source *callback, int fd, uint32_t events, void *data) { int res = pw_loop_iterate(pw.main_loop_loop, 0); if (res < 0 && res != -EINTR) { return res; } else { return 0; } } int pipewire_init(void) { pw_init(NULL, NULL); pw.main_loop = pw_main_loop_new(NULL /* properties */); pw.main_loop_loop = pw_main_loop_get_loop(pw.main_loop); pw.main_loop_loop_fd = pw_loop_get_fd(pw.main_loop_loop); pollen_loop_add_fd(event_loop, pw.main_loop_loop_fd, EPOLLIN, false, pipewire_fd_handler, NULL); pw.context = pw_context_new(pw.main_loop_loop, NULL, 0); if (pw.context == NULL) { ERROR("failed to create pw_context: %s", strerror(errno)); return -1; } pw.core = pw_context_connect(pw.context, NULL, 0); if (pw.core == NULL) { ERROR("failed to connect to pipewire: %s", strerror(errno)); return -1; } pw_core_add_listener(pw.core, &pw.core_listener, &core_events, NULL); pw.registry = pw_core_get_registry(pw.core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(pw.registry, &pw.registry_listener, ®istry_events, NULL); pw.emitter = signal_emitter_create(); return 0; } void pipewire_cleanup(void) { if (pw.registry != NULL) { pw_proxy_destroy((struct pw_proxy *)pw.registry); } if (pw.core != NULL) { pw_core_disconnect(pw.core); } if (pw.context != NULL) { pw_context_destroy(pw.context); } if (pw.main_loop != NULL) { pw_main_loop_destroy(pw.main_loop); } default_metadata_cleanup(&pw.default_metadata); pw_deinit(); } pipemixer-0.4.0/src/pw/common.h000066400000000000000000000021721512217147000163750ustar00rootroot00000000000000#ifndef SRC_PIPEWIRE_COMMON_H #define SRC_PIPEWIRE_COMMON_H #include #include #include "signals.h" /* for use is (node|device)_set_(volume|mute) functions, see node.h, device.h */ #define ALL_CHANNELS ((uint32_t)-1) enum media_class { MEDIA_CLASS_START, STREAM_OUTPUT_AUDIO, STREAM_INPUT_AUDIO, AUDIO_SOURCE, AUDIO_SINK, MEDIA_CLASS_END, }; struct pw { struct pw_main_loop *main_loop; struct pw_loop *main_loop_loop; int main_loop_loop_fd; struct pw_context *context; struct pw_core *core; struct spa_hook core_listener; struct pw_registry *registry; struct spa_hook registry_listener; struct default_metadata { uint32_t id; struct pw_metadata *pw_metadata; struct spa_hook listener; char *configured_audio_sink; char *audio_sink; char *configured_audio_source; char *audio_source; } default_metadata; struct signal_emitter *emitter; }; extern struct pw pw; int pipewire_init(void); void pipewire_cleanup(void); #endif /* #ifndef SRC_PIPEWIRE_COMMON_H */ pipemixer-0.4.0/src/pw/default.c000066400000000000000000000122561512217147000165300ustar00rootroot00000000000000#include #include #include "pw/common.h" #include "pw/default.h" #include "pw/node.h" #include "xmalloc.h" #include "utils.h" #include "log.h" static void on_default_changed(const char *value, enum media_class media_class) { /* FIXME: this is slow, but default nodes rarely change so it's probably fine */ struct node **pnode = NULL; MAP_FOREACH(&nodes, &pnode) { struct node *node = *pnode; if (node->media_class != media_class) { continue; } if (node->node_name == NULL) { continue; } /* * Yes, the way this is written allows for multiple nodes to be default * if they have equal names. This is not a pipemixer bug, this is due * to how wireplumber (an amazing piece of software) handles default * metadata. It expects it to contain a json { "name": "node_name" }, * from which it matches node by name and sets it as default. Yes, this * is very stupid, but who am I to question the infinite wisdom of fdo? * * Pavucontrol handles that by just displaying both nodes as default, * so let's do the same here to avoid complexities. */ if (streq(node->node_name, value)) { if (!node->is_default) { INFO("node %d is now default", node->id); node->is_default = true; signal_emit_bool(node->emitter, NODE_EVENT_DEFAULT, node->is_default); } } else if (node->is_default) { INFO("node %d is now NOT default", node->id); node->is_default = false; signal_emit_bool(node->emitter, NODE_EVENT_DEFAULT, node->is_default); } } } static bool get_name(const char *json, char **pname) { static char buf[4096]; /* thank you pipewire for this amazing json api */ int res = spa_json_str_object_find(json, strlen(json), "name", buf, sizeof(buf)); if (res != 1) { ERROR("failed to parse json: %s", json); return false; } if (*pname != NULL) { free(*pname); } *pname = xstrdup(buf); return true; } static int on_metadata_property(void *data, uint32_t idk, const char *key, const char *type, const char *val) { struct default_metadata *md = data; INFO("metadata property: %d \"%s\" = (%s)%s", idk, key, type, val); static const struct { const char *const name; const unsigned off; const enum media_class class; } props[] = { { "default.audio.source", offsetof(struct default_metadata, audio_source), AUDIO_SOURCE }, { "default.configured.audio.source", offsetof(struct default_metadata, configured_audio_source), AUDIO_SOURCE }, { "default.audio.sink", offsetof(struct default_metadata, audio_sink), AUDIO_SINK }, { "default.configured.audio.sink", offsetof(struct default_metadata, configured_audio_sink), AUDIO_SINK } }; for (unsigned i = 0; i < SIZEOF_ARRAY(props); i++) { if (streq(key, props[i].name)) { char **pname = (char **)((uintptr_t)md + props[i].off); if (get_name(val, pname)) { on_default_changed(*pname, props[i].class); } } } return 0; } static const struct pw_metadata_events metadata_events = { .version = PW_VERSION_METADATA_EVENTS, .property = on_metadata_property, }; void default_metadata_init(struct default_metadata *md, uint32_t id) { md->id = id; md->pw_metadata = pw_registry_bind(pw.registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); pw_metadata_add_listener(md->pw_metadata, &md->listener, &metadata_events, md); } void default_metadata_cleanup(struct default_metadata *md) { if (md->pw_metadata != NULL) { pw_proxy_destroy((struct pw_proxy *)md->pw_metadata); } } bool default_metadata_check_default(struct default_metadata *md, const char *name, enum media_class media_class) { switch (media_class) { case AUDIO_SINK: return streq(name, md->audio_sink) || streq(name, md->configured_audio_sink); case AUDIO_SOURCE: return streq(name, md->audio_source) || streq(name, md->configured_audio_source); default: return false; } } void default_metadata_set_default(struct default_metadata *md, const char *name, enum media_class media_class) { /* TODO: properly escape name? */ char *metadata = NULL; xasprintf(&metadata, "{ \"name\": \"%s\" }", name); const char *key; switch (media_class) { case AUDIO_SOURCE: key = "default.configured.audio.source"; break; case AUDIO_SINK: key = "default.configured.audio.sink"; break; default: /* should not be reached */ break; } pw_metadata_set_property(md->pw_metadata, 0, key, "Spa:String:JSON", metadata); free(metadata); } pipemixer-0.4.0/src/pw/default.h000066400000000000000000000007751512217147000165400ustar00rootroot00000000000000#pragma once #include #include #include "pw/common.h" void default_metadata_init(struct default_metadata *md, uint32_t id); void default_metadata_cleanup(struct default_metadata *md); bool default_metadata_check_default(struct default_metadata *md, const char *name, enum media_class media_class); void default_metadata_set_default(struct default_metadata *md, const char *name, enum media_class media_class); pipemixer-0.4.0/src/pw/device.c000066400000000000000000000415371512217147000163470ustar00rootroot00000000000000#include #include #include "pw/device.h" #include "pw/roundtrip.h" #include "pw/common.h" #include "pw/events.h" #include "collections/map.h" #include "log.h" #include "xmalloc.h" #include "macros.h" static MAP(struct device *) devices = {0}; struct device *device_lookup(uint32_t id) { struct device **dev = MAP_GET(&devices, id); if (dev == NULL) { WARN("device with id %u was not found", id); return NULL; } return *dev; } void device_set_props(const struct device *dev, const struct spa_pod *props, enum spa_direction direction, int32_t card_profile_device) { bool found = false; struct route *route = NULL; VEC_FOREACH(&dev->active_routes, i) { route = VEC_AT(&dev->active_routes, i); if (route->direction == direction && route->device == card_profile_device) { found = true; break; } } if (!found) { ERROR("could not set props on dev %d: route with device %d was not found", dev->id, card_profile_device); return; } uint8_t buffer[4096]; struct spa_pod_builder b; spa_pod_builder_init(&b, buffer, sizeof(buffer)); struct spa_pod* param = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route, SPA_PARAM_ROUTE_device, SPA_POD_Int(route->device), SPA_PARAM_ROUTE_index, SPA_POD_Int(route->index), SPA_PARAM_ROUTE_props, SPA_POD_PodObject(props), SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)); pw_device_set_param(dev->pw_device, SPA_PARAM_Route, 0, param); } void device_set_route(const struct device *dev, int32_t card_profile_device, int32_t index) { uint8_t buffer[1024]; struct spa_pod_builder b; spa_pod_builder_init(&b, buffer, sizeof(buffer)); struct spa_pod *route = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_Route, SPA_PARAM_ROUTE_device, SPA_POD_Int(card_profile_device), SPA_PARAM_ROUTE_index, SPA_POD_Int(index), SPA_PARAM_ROUTE_save, SPA_POD_Bool(true)); pw_device_set_param(dev->pw_device, SPA_PARAM_Route, 0, route); } void device_set_profile(const struct device *dev, int32_t index) { uint8_t buffer[1024]; struct spa_pod_builder b; spa_pod_builder_init(&b, buffer, sizeof(buffer)); struct spa_pod *profile = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, SPA_PARAM_PROFILE_index, SPA_POD_Int(index), SPA_PARAM_PROFILE_save, SPA_POD_Bool(true)); pw_device_set_param(dev->pw_device, SPA_PARAM_Profile, 0, profile); } const struct profile *device_get_active_profile(const struct device *dev) { return dev->active_profile; } size_t device_get_available_profiles(const struct device *dev, const struct profile **pprofiles) { *pprofiles = VEC_DATA(&dev->profiles); return VEC_SIZE(&dev->profiles); } static void profile_free_contents(struct profile *profile) { if (profile != NULL) { free(profile->name); free(profile->description); } } static void route_free_contents(struct route *route) { if (route != NULL) { free(route->description); free(route->name); VEC_FREE(&route->devices); VEC_FREE(&route->profiles); } } const struct pw_device_events device_events = { .version = PW_VERSION_DEVICE_EVENTS, .info = on_device_info, .param = on_device_param, }; void device_create(uint32_t id) { struct device *dev = xmalloc(sizeof(*dev)); *dev = (struct device){ .id = id, .new = true, .pw_device = pw_registry_bind(pw.registry, id, PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE, 0), .emitter = signal_emitter_create(), }; pw_device_add_listener(dev->pw_device, &dev->listener, &device_events, dev); MAP_INSERT(&devices, id, &dev); } void device_destroy(struct device *device) { signal_emit_u64(device->emitter, DEVICE_EVENT_REMOVE, device->id); pw_proxy_destroy((struct pw_proxy *)device->pw_device); free(device->description); VEC_FOREACH(&device->routes, i) { struct route *route = VEC_AT(&device->routes, i); route_free_contents(route); } VEC_FREE(&device->routes); VEC_FOREACH(&device->active_routes, i) { struct route *route = VEC_AT(&device->active_routes, i); route_free_contents(route); } VEC_FREE(&device->active_routes); VEC_FOREACH(&device->profiles, i) { struct profile *profile = VEC_AT(&device->profiles, i); profile_free_contents(profile); } VEC_FREE(&device->profiles); if (device->active_profile != NULL) { profile_free_contents(device->active_profile); free(device->active_profile); } /* staging */ VEC_FOREACH(&device->staging.routes, i) { struct route *route = VEC_AT(&device->staging.routes, i); route_free_contents(route); } VEC_FREE(&device->staging.routes); VEC_FOREACH(&device->staging.active_routes, i) { struct route *route = VEC_AT(&device->staging.active_routes, i); route_free_contents(route); } VEC_FREE(&device->staging.active_routes); VEC_FOREACH(&device->staging.profiles, i) { struct profile *profile = VEC_AT(&device->staging.profiles, i); profile_free_contents(profile); } VEC_FREE(&device->staging.profiles); if (device->staging.active_profile != NULL) { profile_free_contents(device->staging.active_profile); free(device->staging.active_profile); } MAP_REMOVE(&devices, device->id); signal_emitter_release(device->emitter); free(device); } void on_device_roundtrip_done(void *data) { struct device *dev = device_lookup((uintptr_t)data); if (dev == NULL) { WARN("roundtrip finished for device that does not exist!"); WARN("was it removed after roundtrip started?"); return; } if (dev->modified_params & ROUTE) { VEC_FOREACH(&dev->active_routes, i) { struct route *route = VEC_AT(&dev->active_routes, i); route_free_contents(route); } VEC_CLEAR(&dev->active_routes); VEC_EXCHANGE(&dev->active_routes, &dev->staging.active_routes); dev->modified_params &= ~ROUTE; } if (dev->modified_params & ENUM_ROUTE) { VEC_FOREACH(&dev->routes, i) { struct route *route = VEC_AT(&dev->routes, i); route_free_contents(route); } VEC_CLEAR(&dev->routes); VEC_EXCHANGE(&dev->routes, &dev->staging.routes); dev->modified_params &= ~ENUM_ROUTE; } if (dev->modified_params & ENUM_PROFILE) { VEC_FOREACH(&dev->profiles, i) { struct profile *profile = VEC_AT(&dev->profiles, i); profile_free_contents(profile); } VEC_CLEAR(&dev->profiles); VEC_EXCHANGE(&dev->profiles, &dev->staging.profiles); dev->modified_params &= ~ENUM_PROFILE; } if (dev->modified_params & PROFILE) { profile_free_contents(dev->active_profile); free(dev->active_profile); dev->active_profile = NULL; SWAP(dev->active_profile, dev->staging.active_profile); dev->modified_params &= ~PROFILE; } if (dev->new) { dev->new = false; signal_emit_u64(pw.emitter, PIPEWIRE_EVENT_DEVICE_ADDED, dev->id); } else { signal_emit_u64(dev->emitter, DEVICE_EVENT_CHANGE, dev->id); } } void on_device_info(void *data, const struct pw_device_info *info) { struct device *device = data; DEBUG("device info: id %d, %d params%s,%s change " BYTE_BINARY_FORMAT, info->id, info->n_params, info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS ? " C" : "", info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS ? " props," : "", BYTE_BINARY_ARGS(info->change_mask)); uint32_t i = 0; const struct spa_dict_item *item; spa_dict_for_each(item, info->props) { const char *k = item->key; const char *v = item->value; TRACE("%c---%s: %s", (++i == info->props->n_items ? '\\' : '|'), k, v); if (STREQ(k, PW_KEY_DEVICE_DESCRIPTION) && device->description == NULL) { device->description = xstrdup(v); } } if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) { for (i = 0; i < info->n_params; i++) { struct spa_param_info *param = &info->params[i]; if (param->id == SPA_PARAM_Route && param->flags & SPA_PARAM_INFO_READ) { pw_device_enum_params(device->pw_device, 0, param->id, 0, -1, NULL); device->modified_params |= ROUTE; } else if (param->id == SPA_PARAM_EnumRoute && param->flags & SPA_PARAM_INFO_READ) { pw_device_enum_params(device->pw_device, 0, param->id, 0, -1, NULL); device->modified_params |= ENUM_ROUTE; } else if (param->id == SPA_PARAM_Profile && param->flags & SPA_PARAM_INFO_READ) { pw_device_enum_params(device->pw_device, 0, param->id, 0, -1, NULL); device->modified_params |= PROFILE; } else if (param->id == SPA_PARAM_EnumProfile && param->flags & SPA_PARAM_INFO_READ) { pw_device_enum_params(device->pw_device, 0, param->id, 0, -1, NULL); device->modified_params |= ENUM_PROFILE; } } } if (device->modified_params) { roundtrip_async(pw.core, on_device_roundtrip_done, (void *)(uintptr_t)device->id); } } static void on_device_param_route(struct device *dev, const struct spa_pod *param) { const struct spa_pod_prop *name = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_name); const struct spa_pod_prop *index = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_index); const struct spa_pod_prop *device = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_device); const struct spa_pod_prop *profiles = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_profiles); const struct spa_pod_prop *direction = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_direction); const struct spa_pod_prop *description = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_description); if (index == NULL || device == NULL || direction == NULL || description == NULL || name == NULL) { WARN("Didn't find all required fields in Route"); return; } struct route *new_route = VEC_EMPLACE_BACK_ZEROED(&dev->staging.active_routes); spa_pod_get_int(&index->value, &new_route->index); spa_pod_get_int(&device->value, &new_route->device); spa_pod_get_id(&direction->value, &new_route->direction); const char *description_str = NULL; spa_pod_get_string(&description->value, &description_str); new_route->description = xstrdup(description_str); const char *name_str = NULL; spa_pod_get_string(&name->value, &name_str); new_route->name = xstrdup(name_str); struct spa_pod *iter; SPA_POD_ARRAY_FOREACH((const struct spa_pod_array *)&profiles->value, iter) { VEC_APPEND(&new_route->profiles, (int32_t *)iter); } DEBUG("New route (Route) on dev %d: %s device %d index %d dir %d", dev->id, new_route->description, new_route->device, new_route->index, new_route->direction); } static void on_device_param_enum_route(struct device *dev, const struct spa_pod *param) { const struct spa_pod_prop *name = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_name); const struct spa_pod_prop *index = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_index); const struct spa_pod_prop *devices = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_devices); const struct spa_pod_prop *profiles = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_profiles); const struct spa_pod_prop *direction = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_direction); const struct spa_pod_prop *description = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_description); if (index == NULL || direction == NULL || description == NULL || profiles == NULL || devices == NULL || name == NULL) { WARN("Didn't find all required fields in Route"); return; } struct route *new_route = VEC_EMPLACE_BACK_ZEROED(&dev->staging.routes); spa_pod_get_int(&index->value, &new_route->index); spa_pod_get_id(&direction->value, &new_route->direction); const char *description_str = NULL; spa_pod_get_string(&description->value, &description_str); new_route->description = xstrdup(description_str); const char *name_str = NULL; spa_pod_get_string(&name->value, &name_str); new_route->name = xstrdup(name_str); struct spa_pod *iter; SPA_POD_ARRAY_FOREACH((const struct spa_pod_array *)&devices->value, iter) { VEC_APPEND(&new_route->devices, (int32_t *)iter); } SPA_POD_ARRAY_FOREACH((const struct spa_pod_array *)&profiles->value, iter) { VEC_APPEND(&new_route->profiles, (int32_t *)iter); } DEBUG("New route (EnumRoute) on dev %d: %s index %d dir %d", dev->id, new_route->description, new_route->index, new_route->direction); } static void on_device_param_enum_profile(struct device *dev, const struct spa_pod *param) { const struct spa_pod_prop *index = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_index); const struct spa_pod_prop *description = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_description); const struct spa_pod_prop *name = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_name); if (index == NULL || description == NULL || name == NULL) { WARN("Didn't find index or name or description in Profile"); return; } struct profile *new_profile = VEC_EMPLACE_BACK_ZEROED(&dev->staging.profiles); spa_pod_get_int(&index->value, &new_profile->index); const char *description_str = NULL; spa_pod_get_string(&description->value, &description_str); new_profile->description = xstrdup(description_str); const char *name_str = NULL; spa_pod_get_string(&name->value, &name_str); new_profile->name = xstrdup(name_str); DEBUG("New profile (EnumProfile) on dev %d: %s (%s) index %d", dev->id, new_profile->description, new_profile->name, new_profile->index); } static void on_device_param_profile(struct device *dev, const struct spa_pod *param) { if (dev->staging.active_profile != NULL) { ERROR("Got Profile for dev %d, but active profile is already set to %d (%s), " "PLEASE REPORT THIS AS A BUG!!!", dev->id, dev->active_profile->index, dev->active_profile->name); return; } const struct spa_pod_prop *index = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_index); const struct spa_pod_prop *description = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_description); const struct spa_pod_prop *name = spa_pod_find_prop(param, NULL, SPA_PARAM_PROFILE_name); if (index == NULL || description == NULL || name == NULL) { WARN("Didn't find index or name or description in Profile"); return; } struct profile *new_profile = xcalloc(1, sizeof(*new_profile)); spa_pod_get_int(&index->value, &new_profile->index); const char *description_str = NULL; spa_pod_get_string(&description->value, &description_str); new_profile->description = xstrdup(description_str); const char *name_str = NULL; spa_pod_get_string(&name->value, &name_str); new_profile->name = xstrdup(name_str); dev->staging.active_profile = new_profile; DEBUG("New profile (Profile) on dev %d: %s (%s) index %d", dev->id, new_profile->description, new_profile->name, new_profile->index); } void on_device_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) { struct device *device = data; DEBUG("device %d param: id %d seq %d index %d next %d", device->id, id, seq, index, next); switch (id) { case SPA_PARAM_Route: on_device_param_route(device, param); break; case SPA_PARAM_EnumRoute: on_device_param_enum_route(device, param); break; case SPA_PARAM_Profile: on_device_param_profile(device, param); break; case SPA_PARAM_EnumProfile: on_device_param_enum_profile(device, param); break; } } void device_events_subscribe(struct device *device, struct signal_listener *listener, enum device_events events, signal_callback_t callback, void *callback_data) { signal_listener_subscribe(listener, device->emitter, events, callback, callback_data); } pipemixer-0.4.0/src/pw/device.h000066400000000000000000000046501512217147000163470ustar00rootroot00000000000000#ifndef SRC_PIPEWIRE_DEVICE_H #define SRC_PIPEWIRE_DEVICE_H #include #include #include "collections/vec.h" #include "signals.h" struct route { int32_t index; int32_t device; uint32_t direction; /* enum spa_direction */ VEC(int32_t) devices; VEC(int32_t) profiles; char *description; char *name; }; struct profile { int32_t index; char *description; char *name; }; enum device_modified_params { ROUTE = 1 << 0, ENUM_ROUTE = 1 << 1, PROFILE = 1 << 2, ENUM_PROFILE = 1 << 3, }; struct device { struct pw_device *pw_device; struct spa_hook listener; uint32_t id; char *description; bool new; enum device_modified_params modified_params; VEC(struct route) routes; VEC(struct route) active_routes; VEC(struct profile) profiles; /* FIXME: relies on the assumption that only one profile can be active at a time. */ struct profile *active_profile; /* needed to atomically update routes and profiles */ struct { VEC(struct route) routes; VEC(struct route) active_routes; VEC(struct profile) profiles; struct profile *active_profile; } staging; struct signal_emitter *emitter; }; struct device *device_lookup(uint32_t id); void device_create(uint32_t id); void device_destroy(struct device *device); void on_device_info(void *data, const struct pw_device_info *info); void on_device_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param); void device_set_props(const struct device *dev, const struct spa_pod *props, enum spa_direction direction, int32_t card_profile_device); void device_set_route(const struct device *dev, int32_t card_profile_device, int32_t index); void device_set_profile(const struct device *dev, int32_t index); const struct profile *device_get_active_profile(const struct device *dev); size_t device_get_available_profiles(const struct device *dev, const struct profile **profiles); enum device_events { DEVICE_EVENT_CHANGE = 1 << 0, DEVICE_EVENT_REMOVE = 1 << 1, DEVICE_EVENT_ANY = ~0, }; void device_events_subscribe(struct device *device, struct signal_listener *listener, enum device_events events, signal_callback_t callback, void *callback_data); #endif /* #ifndef SRC_PIPEWIRE_DEVICE_H */ pipemixer-0.4.0/src/pw/events.c000066400000000000000000000004541512217147000164050ustar00rootroot00000000000000#include "pw/events.h" #include "pw/common.h" void pipewire_events_subscribe(struct signal_listener *listener, uint64_t events, signal_callback_t callback, void *callback_data) { signal_listener_subscribe(listener, pw.emitter, events, callback, callback_data); } pipemixer-0.4.0/src/pw/events.h000066400000000000000000000006621512217147000164130ustar00rootroot00000000000000#ifndef SRC_PW_EVENTS_H #define SRC_PW_EVENTS_H #include "signals.h" enum pipewire_events { PIPEWIRE_EVENT_NODE_ADDED = 1 << 0, /* new node id as u64 */ PIPEWIRE_EVENT_DEVICE_ADDED = 1 << 1, /* new device id as u64 */ }; void pipewire_events_subscribe(struct signal_listener *listener, uint64_t events, signal_callback_t callback, void *callback_data); #endif /* #ifndef SRC_PW_EVENTS_H */ pipemixer-0.4.0/src/pw/node.c000066400000000000000000000324241512217147000160300ustar00rootroot00000000000000#include #include #include #include #include "pw/node.h" #include "pw/device.h" #include "pw/roundtrip.h" #include "pw/events.h" #include "pw/default.h" #include "collections/map.h" #include "log.h" #include "xmalloc.h" #include "macros.h" #include "config.h" #include "utils.h" /* * C is a great language and 2 anonymous struct that are completely identical to * each other are apparently incompatible types. * Use this hack to make the compiler shut up. */ __typeof__(nodes) nodes = {0}; static enum spa_direction media_class_to_direction(enum media_class class) { switch (class) { case STREAM_OUTPUT_AUDIO: case AUDIO_SINK: return SPA_DIRECTION_OUTPUT; case STREAM_INPUT_AUDIO: case AUDIO_SOURCE: return SPA_DIRECTION_INPUT; default: assert(0 && "Unexpected media_class value passed to media_class_to_direction"); } } struct node *node_lookup(uint32_t id) { struct node **node = MAP_GET(&nodes, id); if (node == NULL) { WARN("node with id %u was not found", id); return NULL; } return *node; } static void node_set_props(const struct node *node, const struct spa_pod *props) { if (node->device_id == 0) { pw_node_set_param(node->pw_node, SPA_PARAM_Props, 0, props); } else { struct device *dev = device_lookup(node->device_id); if (dev == NULL) { WARN("tried to set props of node %d with associated device, " "but no device was found", node->id); return; } enum spa_direction direction = media_class_to_direction(node->media_class); device_set_props(dev, props, direction, node->card_profile_device); } } void node_set_mute(const struct node *node, bool mute) { uint8_t buffer[1024]; struct spa_pod_builder b; spa_pod_builder_init(&b, buffer, sizeof(buffer)); struct spa_pod *props; props = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, SPA_PROP_mute, SPA_POD_Bool(mute)); node_set_props(node, props); } void node_change_volume(const struct node *node, bool absolute, float volume, uint32_t channel) { uint8_t buffer[4096]; struct spa_pod_builder b; spa_pod_builder_init(&b, buffer, sizeof(buffer)); float cubed_volumes[VEC_SIZE(&node->channels)]; for (uint32_t i = 0; i < VEC_SIZE(&node->channels); i++) { float new_volume; float old_volume = VEC_AT(&node->channels, i)->volume; if (channel == ALL_CHANNELS || i == channel) { if (absolute) { new_volume = volume; } else if (volume >= 0 /* positive delta */) { if (old_volume + volume > config.volume_max) { new_volume = old_volume; } else { new_volume = old_volume + volume; } } else /* volume < 0, negative delta */ { if (old_volume + volume < config.volume_min) { new_volume = old_volume; } else { new_volume = old_volume + volume; } } } else { new_volume = old_volume; } cubed_volumes[i] = new_volume * new_volume * new_volume; } struct spa_pod *props = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, SIZEOF_ARRAY(cubed_volumes), cubed_volumes)); node_set_props(node, props); } void node_set_route(const struct node *node, uint32_t route_index) { if (node->device_id == 0) { WARN("Tried to set route on a node that does not have a device"); } else { struct device *dev = device_lookup(node->device_id); if (dev == NULL) { ERROR("Tried to set route on a node but no device was found"); } else { device_set_route(dev, node->card_profile_device, route_index); } } } void node_set_default(const struct node *node) { switch (node->media_class) { case AUDIO_SINK: case AUDIO_SOURCE: default_metadata_set_default(&pw.default_metadata, node->node_name, node->media_class); break; default: WARN("node_set_default called on a node that's neither a sink nor a source"); } } size_t node_get_available_routes(const struct node *node, const struct route *const **proutes) { static VEC(const struct route *) routes = {0}; if (node->device_id == 0) { return 0; } const struct device *dev = device_lookup(node->device_id); if (dev == NULL) { return 0; } if (dev->active_profile == NULL) { ERROR("cannot get available routes for node %d with dev %d: no active profile on node", node->id, dev->id); return 0; } const enum spa_direction direction = media_class_to_direction(node->media_class); VEC_CLEAR(&routes); VEC_FOREACH(&dev->routes, i) { const struct route *route = VEC_AT(&dev->routes, i); if (route->direction != direction) { continue; } VEC_FOREACH(&route->profiles, j) { const int32_t profile = *VEC_AT(&route->profiles, j); if (profile == dev->active_profile->index) { VEC_APPEND(&routes, &route); } } } if (proutes != NULL) { *proutes = routes.data; } return VEC_SIZE(&routes); } const struct route *node_get_active_route(const struct node *node) { if (node->device_id == 0) { return NULL; } const struct device *dev = device_lookup(node->device_id); if (dev == NULL) { return NULL; } const enum spa_direction direction = media_class_to_direction(node->media_class); VEC_FOREACH(&dev->active_routes, i) { const struct route *route = VEC_AT(&dev->active_routes, i); if (route->device != node->card_profile_device || route->direction != direction) { continue; } VEC_FOREACH(&route->profiles, j) { const int32_t profile = *VEC_AT(&route->profiles, j); if (profile == dev->active_profile->index) { return route; } } } WARN("did not find active route for node %d", node->id); return NULL; } static void on_node_roundtrip_done(void *data) { /* node might get removed before roundtrip finishes, * so instead of passing node by ptr here pass its id * and look it up in the hashmap when roundtrip finishes */ struct node *node = node_lookup((uintptr_t)data); if (node == NULL) { WARN("roundtrip finished for node that does not exist!"); WARN("was it removed after roundtrip started?"); return; } if (node->new) { node->new = false; signal_emit_u64(pw.emitter, PIPEWIRE_EVENT_NODE_ADDED, node->id); node->is_default = default_metadata_check_default(&pw.default_metadata, node->node_name, node->media_class); if (node->is_default) { INFO("node %d is now default", node->id); } } else { signal_emit_u64(node->emitter, NODE_EVENT_CHANGE, node->changed); } } void on_node_info(void *data, const struct pw_node_info *info) { struct node *node = data; DEBUG("node info: id %d, op %d/%d%s, ip %d/%d%s, state %d%s, %d params%s,%s change " BYTE_BINARY_FORMAT, info->id, info->n_output_ports, info->max_output_ports, info->change_mask & PW_NODE_CHANGE_MASK_OUTPUT_PORTS ? " C" : "", info->n_input_ports, info->max_input_ports, info->change_mask & PW_NODE_CHANGE_MASK_INPUT_PORTS ? " C" : "", info->state, info->change_mask & PW_NODE_CHANGE_MASK_STATE ? " C" : "", info->n_params, info->change_mask & PW_NODE_CHANGE_MASK_PARAMS ? " C" : "", info->change_mask & PW_NODE_CHANGE_MASK_PROPS ? " props," : "", BYTE_BINARY_ARGS(info->change_mask)); /* reset changes */ node->changed = NODE_CHANGE_NOTHING; uint32_t i = 0; const struct spa_dict_item *item; spa_dict_for_each(item, info->props) { const char *k = item->key; const char *v = item->value; TRACE("%c---%s: %s", (++i == info->props->n_items ? '\\' : '|'), k, v); if (STREQ(k, PW_KEY_MEDIA_NAME)) { free(node->media_name); node->media_name = xstrdup(v); node->changed = NODE_CHANGE_INFO; } else if (STREQ(k, PW_KEY_NODE_NAME)) { free(node->node_name); node->node_name = xstrdup(v); node->changed = NODE_CHANGE_INFO; } else if (STREQ(k, PW_KEY_NODE_DESCRIPTION)) { free(node->node_description); node->node_description = xstrdup(v); node->changed = NODE_CHANGE_INFO; } else if (STREQ(k, PW_KEY_DEVICE_ID)) { str_to_u32(v, &node->device_id); } else if (STREQ(k, "card.profile.device")) { str_to_i32(v, &node->card_profile_device); } } bool needs_roundtrip = false; if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) { for (i = 0; i < info->n_params; i++) { struct spa_param_info *param = &info->params[i]; if (param->id == SPA_PARAM_Props && param->flags & SPA_PARAM_INFO_READ) { pw_node_enum_params(node->pw_node, 0, param->id, 0, -1, NULL); needs_roundtrip = true; } } } if (needs_roundtrip) { roundtrip_async(pw.core, on_node_roundtrip_done, (void *)(uintptr_t)node->id); } else { on_node_roundtrip_done((void *)(uintptr_t)node->id); } } void on_node_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) { struct node *node = data; DEBUG("node %d param: id %d seq %d index %d next %d", node->id, id, seq, index, next); const struct spa_pod_prop *volumes_prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelVolumes); const struct spa_pod_prop *channels_prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelMap); const struct spa_pod_prop *mute_prop = spa_pod_find_prop(param, NULL, SPA_PROP_mute); if (volumes_prop == NULL || channels_prop == NULL || mute_prop == NULL) { return; } const uint32_t old_channel_count = VEC_SIZE(&node->channels); VEC_CLEAR(&node->channels); const struct spa_pod_array *volumes_arr = (const struct spa_pod_array *)&volumes_prop->value; const struct spa_pod_array *channels_arr = (const struct spa_pod_array *)&channels_prop->value; const uint32_t volumes_child_size = volumes_arr->body.child.size; const uint32_t channels_child_size = channels_arr->body.child.size; const uint32_t n_channels = (volumes_arr->pod.size - 8) / volumes_child_size; /* cursed af */ for (uint32_t i = 0; i < n_channels; i++) { const float *volume = (void *)((uintptr_t)&volumes_arr->body + 8 + (volumes_child_size * i)); const enum spa_audio_channel *channel_enum = (void *)((uintptr_t)&channels_arr->body + 8 + (channels_child_size * i)); const struct node_channel c = { .volume = cbrtf(*volume), .name = channel_name_from_enum(*channel_enum), }; VEC_APPEND(&node->channels, &c); } node->changed |= NODE_CHANGE_VOLUME; const bool old_mute = node->mute; spa_pod_get_bool(&mute_prop->value, &node->mute); if (old_mute != node->mute) { node->changed |= NODE_CHANGE_MUTE; } if (old_channel_count != VEC_SIZE(&node->channels)) { node->changed |= NODE_CHANGE_CHANNEL_COUNT; } } static const struct pw_node_events node_events = { .version = PW_VERSION_NODE_EVENTS, .info = on_node_info, .param = on_node_param, }; void node_create(uint32_t id, enum media_class media_class) { struct node *node = xmalloc(sizeof(*node)); *node = (struct node){ .id = id, .new = true, .media_class = media_class, .pw_node = pw_registry_bind(pw.registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0), .emitter = signal_emitter_create(), }; pw_node_add_listener(node->pw_node, &node->listener, &node_events, node); MAP_INSERT(&nodes, id, &node); } void node_destroy(struct node *node) { signal_emit_u64(node->emitter, NODE_EVENT_REMOVE, node->id); pw_proxy_destroy((struct pw_proxy *)node->pw_node); free(node->media_name); free(node->node_name); free(node->node_description); VEC_FREE(&node->channels); MAP_REMOVE(&nodes, node->id); signal_emitter_release(node->emitter); free(node); } void node_events_subscribe(struct node *node, struct signal_listener *listener, enum node_events events, signal_callback_t callback, void *callback_data) { signal_listener_subscribe(listener, node->emitter, events, callback, callback_data); } pipemixer-0.4.0/src/pw/node.h000066400000000000000000000042101512217147000160250ustar00rootroot00000000000000#ifndef SRC_PIPEWIRE_NODE_H #define SRC_PIPEWIRE_NODE_H #include #include #include "pw/common.h" #include "pw/device.h" #include "collections/map.h" enum node_change_mask { NODE_CHANGE_NOTHING = 0, NODE_CHANGE_INFO = 1 << 0, NODE_CHANGE_MUTE = 1 << 1, NODE_CHANGE_VOLUME = 1 << 2, NODE_CHANGE_CHANNEL_COUNT = 1 << 3, NODE_CHANGE_EVERYTHING = ~0, }; struct node { struct pw_node *pw_node; struct spa_hook listener; uint32_t id; enum media_class media_class; char *media_name; char *node_name; char *node_description; bool mute; VEC(struct node_channel { const char *name; float volume; }) channels; bool is_default; uint32_t device_id; int32_t card_profile_device; bool new; enum node_change_mask changed; struct signal_emitter *emitter; }; extern MAP(struct node *) nodes; struct node *node_lookup(uint32_t id); void node_create(uint32_t id, enum media_class media_class); void node_destroy(struct node *node); void on_node_info(void *data, const struct pw_node_info *info); void on_node_param(void *data, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param); void node_set_mute(const struct node *node, bool mute); void node_change_volume(const struct node *node, bool absolute, float volume, uint32_t channel); void node_set_route(const struct node *node, uint32_t route_index); void node_set_default(const struct node *node); const struct route *node_get_active_route(const struct node *node); size_t node_get_available_routes(const struct node *node, const struct route *const **proutes); enum node_events { NODE_EVENT_CHANGE = 1 << 0, /* change mask as u64 */ NODE_EVENT_REMOVE = 1 << 1, /* node id as u64 */ NODE_EVENT_DEFAULT = 1 << 2, /* default state as bool */ NODE_EVENT_ANY = ~0, }; void node_events_subscribe(struct node *node, struct signal_listener *listener, enum node_events events, signal_callback_t callback, void *callback_data); #endif /* #ifndef SRC_PIPEWIRE_NODE_H */ pipemixer-0.4.0/src/pw/roundtrip.c000066400000000000000000000025321512217147000171260ustar00rootroot00000000000000#include "pw/roundtrip.h" #include "log.h" #include "xmalloc.h" #include "collections/list.h" struct roundtrip_async_data { roundtrip_async_callback_t callback; void *data; int seq; LIST_ENTRY link; }; static LIST_HEAD callbacks = LIST_INITIALISER(&callbacks); static void on_core_done(void *data, uint32_t id, int seq) { struct roundtrip_async_data *d; LIST_POP(d, LIST_LAST(&callbacks), link); if (d->seq != seq) { ERROR("roundtrip error: expected seq %d got %d", d->seq, seq); } else { TRACE("roundtrip finished with seq %d", seq); if (d->callback != NULL) { d->callback(d->data); } LIST_REMOVE(&d->link); free(d); } } static const struct pw_core_events core_events = { .done = on_core_done, }; void roundtrip_async(struct pw_core *core, roundtrip_async_callback_t callback, void *data) { static struct spa_hook listener; static bool initial_setup_done = false; if (!initial_setup_done) { initial_setup_done = true; pw_core_add_listener(core, &listener, &core_events, NULL); } struct roundtrip_async_data *d = xmalloc(sizeof(*d)); d->callback = callback; d->data = data; d->seq = pw_core_sync(core, PW_ID_CORE, 0); TRACE("roundtrip started with seq %d", d->seq); LIST_INSERT(&callbacks, &d->link); } pipemixer-0.4.0/src/pw/roundtrip.h000066400000000000000000000004271512217147000171340ustar00rootroot00000000000000#ifndef SRC_PW_ROUNDTRIP_H #define SRC_PW_ROUNDTRIP_H #include typedef void (*roundtrip_async_callback_t)(void *data); void roundtrip_async(struct pw_core *core, roundtrip_async_callback_t callback, void *data); #endif /* #ifndef SRC_PW_ROUNDTRIP_H */ pipemixer-0.4.0/src/signals.c000066400000000000000000000110561512217147000161130ustar00rootroot00000000000000#include #include #include "signals.h" #include "eventloop.h" #include "macros.h" #include "xmalloc.h" #include "collections/vec.h" struct queued_event { uint64_t event; struct signal_data data; }; struct signal_emitter { LIST_HEAD listeners; VEC(struct queued_event) queued_events; bool added_to_pending; bool released; }; static struct pollen_event_source *efd_source = NULL; static VEC(struct signal_emitter *) pending_emitters = {0}; static void signal_emitter_dispatch_events(struct signal_emitter *emitter) { VEC_FOREACH(&emitter->queued_events, i) { const struct queued_event *event = &emitter->queued_events.data[i]; const struct signal_listener *listener; LIST_FOR_EACH(listener, &emitter->listeners, link) { if (event->event & listener->events) { listener->callback(event->event, &event->data, listener->callback_data); } } } VEC_CLEAR(&emitter->queued_events); } struct signal_emitter *signal_emitter_create(void) { struct signal_emitter *e = xzalloc(sizeof(*e)); LIST_INIT(&e->listeners); return e; } static void signal_emitter_free(struct signal_emitter *emitter) { VEC_FREE(&emitter->queued_events); free(emitter); } void signal_emitter_release(struct signal_emitter *emitter) { if (emitter->added_to_pending) { emitter->released = true; } else { signal_emitter_free(emitter); } } void signal_listener_subscribe(struct signal_listener *listener, struct signal_emitter *emitter, uint64_t events, signal_callback_t callback, void *callback_data) { *listener = (struct signal_listener){ .events = events, .callback = callback, .callback_data = callback_data }; LIST_INSERT(&emitter->listeners, &listener->link); } void signal_listener_unsubscribe(struct signal_listener *listener) { if (listener->link.next != NULL) { LIST_REMOVE(&listener->link); listener->link = (struct list){0}; } } static void signal_emit_internal(struct signal_emitter *emitter, uint64_t event, struct signal_data *data) { struct queued_event *ev = VEC_EMPLACE_BACK(&emitter->queued_events); *ev = (struct queued_event){ .event = event, .data = *data, }; if (!emitter->added_to_pending) { VEC_APPEND(&pending_emitters, &emitter); emitter->added_to_pending = true; if (VEC_SIZE(&pending_emitters) == 1) { pollen_efd_trigger(efd_source); } } } void signal_emit_ptr(struct signal_emitter *emitter, uint64_t event, void *ptr) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_PTR, .as.ptr = ptr, }; signal_emit_internal(emitter, event, &data); } void signal_emit_str(struct signal_emitter *emitter, uint64_t event, char *str) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_STR, .as.str = str, }; signal_emit_internal(emitter, event, &data); } void signal_emit_u64(struct signal_emitter *emitter, uint64_t event, uint64_t u64) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_U64, .as.u64 = u64, }; signal_emit_internal(emitter, event, &data); } void signal_emit_i64(struct signal_emitter *emitter, uint64_t event, int64_t i64) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_I64, .as.i64 = i64, }; signal_emit_internal(emitter, event, &data); } void signal_emit_f64(struct signal_emitter *emitter, uint64_t event, double f64) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_F64, .as.f64 = f64, }; signal_emit_internal(emitter, event, &data); } void signal_emit_bool(struct signal_emitter *emitter, uint64_t event, bool boolean) { struct signal_data data = { .type = SIGNAL_DATA_TYPE_BOOL, .as.boolean = boolean, }; signal_emit_internal(emitter, event, &data); } static int on_efd_triggered(struct pollen_event_source *_, uint64_t _, void *_) { VEC_FOREACH(&pending_emitters, i) { struct signal_emitter *emitter = pending_emitters.data[i]; signal_emitter_dispatch_events(emitter); emitter->added_to_pending = false; if (emitter->released) { signal_emitter_free(emitter); } } VEC_CLEAR(&pending_emitters); return 0; } bool signals_global_init(void) { efd_source = pollen_loop_add_efd(event_loop, on_efd_triggered, NULL); return efd_source != NULL; } pipemixer-0.4.0/src/signals.h000066400000000000000000000035531512217147000161230ustar00rootroot00000000000000#pragma once #include #include #include "collections/list.h" enum signal_data_type { SIGNAL_DATA_TYPE_PTR, SIGNAL_DATA_TYPE_STR, SIGNAL_DATA_TYPE_U64, SIGNAL_DATA_TYPE_I64, SIGNAL_DATA_TYPE_F64, SIGNAL_DATA_TYPE_BOOL, }; struct signal_data { enum signal_data_type type; union { void *ptr; char *str; uint64_t u64; int64_t i64; double f64; bool boolean; } as; }; typedef void (*signal_callback_t)(uint64_t event, const struct signal_data *data, void *userdata); struct signal_listener { /* bitmask */ uint64_t events; signal_callback_t callback; void *callback_data; LIST_ENTRY link; }; struct signal_emitter; /* * signals system uses one global eventfd that is created with this call */ bool signals_global_init(void); /* * to prevent events outliving their emitter and causing UAF, * make emitter a separate allocation with lazy free semantics */ struct signal_emitter *signal_emitter_create(void); void signal_emitter_release(struct signal_emitter *emitter); void signal_listener_subscribe(struct signal_listener *listener, struct signal_emitter *emitter, uint64_t events, signal_callback_t callback, void *callback_data); void signal_listener_unsubscribe(struct signal_listener *listener); void signal_emit_ptr(struct signal_emitter *emitter, uint64_t event, void *ptr); void signal_emit_str(struct signal_emitter *emitter, uint64_t event, char *str); void signal_emit_u64(struct signal_emitter *emitter, uint64_t event, uint64_t u64); void signal_emit_i64(struct signal_emitter *emitter, uint64_t event, int64_t i64); void signal_emit_f64(struct signal_emitter *emitter, uint64_t event, double f64); void signal_emit_bool(struct signal_emitter *emitter, uint64_t event, bool boolean); pipemixer-0.4.0/src/tui.c000066400000000000000000001257401512217147000152620ustar00rootroot00000000000000#include #include #include #include #include #include "tui.h" #include "macros.h" #include "log.h" #include "xmalloc.h" #include "utils.h" #include "config.h" #include "macros.h" #include "eventloop.h" #include "pw/events.h" #include "pw/node.h" #define FOR_EACH_TAB(var) for (int var = 0; var < TUI_TAB_COUNT; var++) enum color_pair { DEFAULT = 0, GREEN = 1, YELLOW = 2, RED = 3, }; struct tui tui = {0}; static enum tui_tab_type media_class_to_tui_tab(enum media_class class) { switch (class) { case STREAM_OUTPUT_AUDIO: return PLAYBACK; case STREAM_INPUT_AUDIO: return RECORDING; case AUDIO_SOURCE: return INPUT_DEVICES; case AUDIO_SINK: return OUTPUT_DEVICES; default: assert(0 && "Invalid media class passed to media_class_to_tui_tab"); } } static const char *tui_tab_name(enum tui_tab_type tab) { switch (tab) { case PLAYBACK: return "Playback"; case RECORDING: return "Recording"; case OUTPUT_DEVICES: return "Output Devices"; case INPUT_DEVICES: return "Input Devices"; case CARDS: return "Cards"; default: assert(0 && "Invalid tab type passed to tui_tab_name"); } } static void trigger_update(void) { if (!tui.efd_triggered) { if (!pollen_efd_trigger(tui.efd_source)) { ERROR("failed to trigger ui update: %m"); } else { tui.efd_triggered = true; } } } static void tui_tab_item_draw_node(const struct tui_tab_item *const item, enum tui_tab_item_draw_mask mask) { #define DRAW(element) if (mask & TUI_TAB_ITEM_DRAW_##element) const struct node *node = node_lookup(item->as.node.node_id); if (node == NULL) { WARN("tried to draw node %d but it does not exist", item->as.node.node_id); return; } const int usable_width = tui.term_width - 2; /* account for box borders */ const int two_thirds_usable_width = usable_width / 3 * 2; /* 5 for channel name, 1 space, 3 volume, 1 space, 4 more for decorations = 14 */ const int volume_bar_width_max = two_thirds_usable_width - 14; const int volume_bar_width = (volume_bar_width_max / 15) * 15; const int volume_area_width = volume_bar_width + 14; const int info_area_width = usable_width - volume_area_width - 1; /* leave a space */ const int info_area_start = 1; /* right after box border */ const int volume_area_start = info_area_start + info_area_width + 1; const int volume_bar_start = volume_area_start + 12; /* minus two decorations at the end */ const bool focused = item->focused; const bool muted = node->mute; TRACE("tui_draw_node: id %d mask "BYTE_BINARY_FORMAT, node->id, BYTE_BINARY_ARGS(mask)); WINDOW *const win = tui.pad_win; /* prevents leftover artifacts */ DRAW(BLANKS) { for (int i = 0; i < item->height; i++) { wmove(win, item->pos + i, 0); wclrtoeol(win); } } if (focused) { wattron(win, A_BOLD); } DRAW(DESCRIPTION) { wchar_t line[usable_width]; wchar_t *lineptr = line; const char *name_str = "NULL"; if (node->node_description != NULL) { name_str = node->node_description; } else if (node->node_name != NULL) { name_str = node->node_name; } if (node->is_default) { lineptr += swprintf(lineptr, usable_width - (lineptr - line), L"[*] "); } if (config.display_ids) { lineptr += swprintf(lineptr, usable_width - (lineptr - line), L"%d. ", node->id); } swprintf(lineptr, usable_width - (lineptr - line), L"%s%s%s%-*s", name_str, (node->media_name == NULL) ? "" : ": ", (node->media_name == NULL) ? "" : node->media_name, usable_width, ""); wcstrimcols(line, usable_width); mvwaddnwstr(win, item->pos + 1, info_area_start, line, usable_width); } DRAW(CHANNELS) { if (muted) { wattron(win, A_DIM); } for (uint32_t i = 0; i < VEC_SIZE(&node->channels); i++) { const int pos = item->pos + i + 2; const int vol_int = (int)roundf(VEC_AT(&node->channels, i)->volume * 100); mvwprintw(win, pos, volume_area_start, "%5s %-3d ", VEC_AT(&node->channels, i)->name, vol_int); /* draw volume bar */ int pair = DEFAULT; const int step = volume_bar_width / 3; const int thresh = vol_int * volume_bar_width / 150; for (int j = 0; j < volume_bar_width; j++) { cchar_t cc; if (j % step == 0 && !node->mute) { pair += 1; } setcchar(&cc, (j < thresh) ? config.bar_full_char : config.bar_empty_char, 0, pair, NULL); mvwadd_wch(win, pos, volume_bar_start + j, &cc); } } wattroff(win, A_DIM); } DRAW(DECORATIONS) { if (muted) { wattron(win, A_DIM); } for (uint32_t i = 0; i < VEC_SIZE(&node->channels); i++) { const int pos = item->pos + i + 2; const wchar_t *wchar_left, *wchar_right; cchar_t cchar_left, cchar_right; if (VEC_SIZE(&node->channels) == 1) { wchar_left = config.volume_frame.ml; wchar_right = config.volume_frame.mr; } else if (i == 0) { wchar_left = config.volume_frame.tl; wchar_right = config.volume_frame.tr; } else if (i == VEC_SIZE(&node->channels) - 1) { wchar_left = config.volume_frame.bl; wchar_right = config.volume_frame.br; } else { wchar_left = config.volume_frame.cl; wchar_right = config.volume_frame.cr; } setcchar(&cchar_left, wchar_left, 0, DEFAULT, NULL); setcchar(&cchar_right, wchar_right, 0, DEFAULT, NULL); mvwadd_wch(win, pos, volume_bar_start - 1, &cchar_left); mvwadd_wch(win, pos, volume_bar_start + volume_bar_width, &cchar_right); const wchar_t *wchar_focus; if (focused && (!item->as.node.unlocked_channels || item->as.node.focused_channel == i)) { wchar_focus = config.volume_frame.f; } else { wchar_focus = L" "; } cchar_t cchar_focus; setcchar(&cchar_focus, wchar_focus, 0, DEFAULT, NULL); mvwadd_wch(win, pos, volume_bar_start - 2, &cchar_focus); mvwadd_wch(win, pos, volume_bar_start + volume_bar_width + 1, &cchar_focus); } wattroff(win, A_DIM); } DRAW(ROUTES) { char buf[usable_width]; const int routes_line_pos = item->pos + item->height - 2; if (node->device_id != 0) { int written = 0, chars; chars = snprintf(buf, usable_width - written, "Routes: "); mvwaddnstr(win, routes_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); const struct route *active_route = node_get_active_route(node); const struct route *const *routes; const size_t nroutes = node_get_available_routes(node, &routes); if (active_route != NULL) { /* draw active route first */ chars = snprintf(buf, usable_width - written, "%s", active_route->description); mvwaddnstr(win, routes_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); wattron(win, A_DIM); for (size_t i = 0; i < nroutes; i++) { const struct route *route = routes[i]; if (active_route != NULL && route->index == active_route->index) { continue; } chars = snprintf(buf, usable_width - written, "%s%s", config.routes_separator, route->description); mvwaddnstr(win, routes_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } } else if (nroutes > 0) { wattron(win, A_DIM); for (size_t i = 0; i < nroutes; i++) { const struct route *route = routes[i]; if (i == 0) { chars = snprintf(buf, usable_width - written, "%s", route->description); } else { chars = snprintf(buf, usable_width - written, "%s%s", config.routes_separator, route->description); } mvwaddnstr(win, routes_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } } else { wattron(win, A_DIM); chars = snprintf(buf, usable_width - written, "(none)"); mvwaddnstr(win, routes_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } if (written >= usable_width) { cchar_t cc; setcchar(&cc, L"…", 0, DEFAULT, NULL); mvwadd_wch(win, routes_line_pos, usable_width, &cc); } else { mvwhline(win, routes_line_pos, written + 1, ' ', usable_width - written); } wattroff(win, A_DIM); } } DRAW(BORDERS) { /* box */ wmove(win, item->pos, 0); waddwstr(win, config.borders.tl); for (int x = 1; x < tui.term_width - 1; x++) { waddwstr(win, config.borders.ts); } waddwstr(win, config.borders.tr); wmove(win, item->pos + item->height - 1, 0); waddwstr(win, config.borders.bl); for (int x = 1; x < tui.term_width - 1; x++) { waddwstr(win, config.borders.bs); } waddwstr(win, config.borders.br); for (int y = 1; y < item->height - 1; y++) { wmove(win, item->pos + y, 0); waddwstr(win, config.borders.ls); } for (int y = 1; y < item->height - 1; y++) { wmove(win, item->pos + y, tui.term_width - 1); waddwstr(win, config.borders.ls); } } wattroff(win, A_BOLD); #undef DRAW } static void tui_tab_item_draw_device(const struct tui_tab_item *const item, enum tui_tab_item_draw_mask mask) { #define DRAW(element) if (mask & TUI_TAB_ITEM_DRAW_##element) const struct device *dev = device_lookup(item->as.device.device_id); if (dev == NULL) { WARN("tried to draw device %d but it does not exist", item->as.device.device_id); return; } const int usable_width = tui.term_width - 2; /* account for box borders */ const bool focused = item->focused; TRACE("tui_draw_device: id %d mask "BYTE_BINARY_FORMAT, dev->id, BYTE_BINARY_ARGS(mask)); WINDOW *const win = tui.pad_win; DRAW(BLANKS) { for (int i = 0; i < item->height; i++) { wmove(win, item->pos + i, 0); wclrtoeol(win); } } if (focused) { wattron(win, A_BOLD); } DRAW(DESCRIPTION) { wchar_t line[usable_width]; wchar_t *lineptr = line; if (config.display_ids) { lineptr += swprintf(lineptr, usable_width - (lineptr - line), L"%d. ", dev->id); } swprintf(lineptr, usable_width - (lineptr - line), L"%s", dev->description); mvwaddnwstr(win, item->pos + 1, 1, line, SIZEOF_ARRAY(line)); wclrtoeol(win); } DRAW(PROFILES) { /* draw profiles */ char buf[usable_width]; const int ports_line_pos = item->pos + item->height - 2; int written = 0, chars; chars = snprintf(buf, usable_width - written, "Profiles: "); mvwaddnstr(win, ports_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); const struct profile *active_profile = device_get_active_profile(dev); const struct profile *profiles; const size_t nprofiles = device_get_available_profiles(dev, &profiles); if (active_profile != NULL) { /* draw active profile first */ chars = snprintf(buf, usable_width - written, "%s", active_profile->description); mvwaddnstr(win, ports_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); wattron(win, A_DIM); for (size_t i = 0; i < nprofiles; i++) { const struct profile *const profile = &profiles[i]; if (active_profile != NULL && profile->index == active_profile->index) { continue; } chars = snprintf(buf, usable_width - written, "%s%s", config.profiles_separator, profile->description); mvwaddnstr(win, ports_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } } else if (nprofiles > 0) { wattron(win, A_DIM); for (size_t i = 0; i < nprofiles; i++) { const struct profile *const profile = &profiles[i]; if (i == 0) { chars = snprintf(buf, usable_width - written, "%s", profile->description); } else { chars = snprintf(buf, usable_width - written, "%s%s", config.profiles_separator, profile->description); } mvwaddnstr(win, ports_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } } else { wattron(win, A_DIM); chars = snprintf(buf, usable_width - written, "(none)"); mvwaddnstr(win, ports_line_pos, 1 + written, buf, usable_width - written); written = (written + chars > usable_width) ? usable_width : (written + chars); } if (written >= usable_width) { cchar_t cc; setcchar(&cc, L"…", 0, DEFAULT, NULL); mvwadd_wch(win, ports_line_pos, usable_width, &cc); } else { mvwhline(win, ports_line_pos, written + 1, ' ', usable_width - written); } wattroff(win, A_DIM); } DRAW(BORDERS) { wmove(win, item->pos, 0); waddwstr(win, config.borders.tl); for (int x = 1; x < tui.term_width - 1; x++) { waddwstr(win, config.borders.ts); } waddwstr(win, config.borders.tr); wmove(win, item->pos + item->height - 1, 0); waddwstr(win, config.borders.bl); for (int x = 1; x < tui.term_width - 1; x++) { waddwstr(win, config.borders.bs); } waddwstr(win, config.borders.br); for (int y = 1; y < item->height - 1; y++) { wmove(win, item->pos + y, 0); waddwstr(win, config.borders.ls); } for (int y = 1; y < item->height - 1; y++) { wmove(win, item->pos + y, tui.term_width - 1); waddwstr(win, config.borders.ls); } } wattroff(win, A_BOLD); #undef DRAW } static void tui_tab_item_draw(const struct tui_tab_item *const item, enum tui_tab_item_draw_mask mask) { if (item->tab_index != tui.tab_index) { return; } switch (item->type) { case TUI_TAB_ITEM_TYPE_NODE: tui_tab_item_draw_node(item, mask); break; case TUI_TAB_ITEM_TYPE_DEVICE: tui_tab_item_draw_device(item, mask); break; } } /* this only updates scroll pos and does not actually draw anything */ static void tui_tab_item_ensure_visible(const struct tui_tab_item *const item) { struct tui_tab *const tab = &tui.tabs[item->tab_index]; /* minus top bar */ const int visible_height = tui.term_height - 1; if (tab->scroll_pos > item->pos) { tab->scroll_pos = item->pos; } else if ((item->pos + item->height) > (tab->scroll_pos + visible_height)) { /* a + b = c + d <=> c = a + b - d */ tab->scroll_pos = (item->pos + item->height) - visible_height; } } static void tui_tab_item_focus(struct tui_tab_item *const item, bool draw, bool user) { struct tui_tab *const tab = &tui.tabs[item->tab_index]; if (user) { tab->user_changed_focus = true; } if (tab->focused == item) { return; } else if (tab->focused != NULL) { tab->focused->focused = false; if (draw) { tui_tab_item_draw(tab->focused, TUI_TAB_ITEM_DRAW_EVERYTHING); } } tab->focused = item; item->focused = true; if (draw) { tui_tab_item_draw(item, TUI_TAB_ITEM_DRAW_EVERYTHING); } tui_tab_item_ensure_visible(item); } static void tui_tab_item_unfocus(struct tui_tab_item *const item, bool draw) { struct tui_tab *const tab = &tui.tabs[item->tab_index]; if (tab->focused != item) { WARN("tui_tab_item_unfocus called on unfocused item"); return; } struct tui_tab_item *next = NULL; if (LIST_NEXT(&item->link) != &tab->items) { LIST_GET(next, LIST_NEXT(&item->link), link); } else if (LIST_PREV(&item->link) != &tab->items) { LIST_GET(next, LIST_PREV(&item->link), link); } if (next != NULL) { tui_tab_item_focus(next, draw, false); } else { tab->focused = NULL; } } void tui_bind_change_focus(union tui_bind_data data) { enum tui_direction direction = data.direction; if (tui.menu_active) { tui_menu_change_focus(tui.menu, (direction == UP) ? -1 : 1); return; } struct tui_tab *const tab = &tui.tabs[tui.tab_index]; struct tui_tab_item *const f = tab->focused; if (f == NULL) { return; } switch (direction) { case DOWN: if (f->type == TUI_TAB_ITEM_TYPE_NODE && f->as.node.unlocked_channels && f->as.node.focused_channel < f->as.node.n_channels - 1) { f->as.node.focused_channel += 1; tui_tab_item_draw(f, TUI_TAB_ITEM_DRAW_DECORATIONS); } else { struct tui_tab_item *next = NULL; if (!LIST_IS_LAST(&tab->items, &f->link)) { LIST_GET(next, LIST_NEXT(&f->link), link); } else if (config.wraparound) { LIST_GET_FIRST(next, &tab->items, link); } else { break; } if (next->type == TUI_TAB_ITEM_TYPE_NODE && next->as.node.unlocked_channels) { next->as.node.focused_channel = 0; } tui_tab_item_focus(next, true, true); } break; case UP: if (f->type == TUI_TAB_ITEM_TYPE_NODE && f->as.node.unlocked_channels && f->as.node.focused_channel > 0) { f->as.node.focused_channel -= 1; tui_tab_item_draw(f, TUI_TAB_ITEM_DRAW_DECORATIONS); } else { struct tui_tab_item *next = NULL; if (!LIST_IS_FIRST(&tab->items, &f->link)) { LIST_GET(next, LIST_PREV(&f->link), link); } else if (config.wraparound) { LIST_GET_LAST(next, &tab->items, link); } else { break; } if (next->type == TUI_TAB_ITEM_TYPE_NODE && next->as.node.unlocked_channels) { next->as.node.focused_channel = next->as.node.n_channels - 1; } tui_tab_item_focus(next, true, true); } break; } } static void redraw_current_tab(void) { const struct tui_tab *tab = &tui.tabs[tui.tab_index]; int bottom = 0; struct tui_tab_item *item; LIST_FOR_EACH(item, &tab->items, link) { tui_tab_item_draw(item, TUI_TAB_ITEM_DRAW_EVERYTHING); bottom += item->height; } if (wmove(tui.pad_win, bottom, 0) != OK) { WARN("wmove(tui.pad_win, %d, 0) failed!", bottom); } else { TRACE("wclrtobot(tui.pad_win) bottom %d", bottom); wclrtobot(tui.pad_win); } if (bottom == 0) { /* empty tab */ static const char empty[] = "Empty"; wattron(tui.pad_win, A_DIM); mvwaddstr(tui.pad_win, (tui.term_height - 1) / 2, (tui.term_width / 2) - (strlen(empty) / 2), empty); wattroff(tui.pad_win, A_DIM); } } static void redraw_status_bar(void) { wmove(tui.bar_win, 0, 0); FOR_EACH_TAB(tab_index) { if (tab_index != tui.tab_index) { wattron(tui.bar_win, A_DIM); } else { wattron(tui.bar_win, A_BOLD); } waddstr(tui.bar_win, tui_tab_name(config.tab_map_index_to_enum[tab_index])); if (tab_index != tui.tab_index) { wattroff(tui.bar_win, A_DIM); } else { wattroff(tui.bar_win, A_BOLD); } waddstr(tui.bar_win, " "); } wclrtoeol(tui.bar_win); } void tui_bind_focus_last(union tui_bind_data data) { struct tui_tab *const tab = &tui.tabs[tui.tab_index]; if (LIST_IS_EMPTY(&tab->items)) { return; } struct tui_tab_item *last; LIST_GET_LAST(last, &tab->items, link); tui_tab_item_focus(last, true, true); } void tui_bind_focus_first(union tui_bind_data data) { struct tui_tab *const tab = &tui.tabs[tui.tab_index]; if (LIST_IS_EMPTY(&tab->items)) { return; } struct tui_tab_item *first; LIST_GET_FIRST(first, &tab->items, link); tui_tab_item_focus(first, true, true); } void tui_bind_change_volume(union tui_bind_data data) { const enum tui_direction direction = data.direction; const struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } float delta = (direction == UP) ? config.volume_step : -config.volume_step; const struct node *const node = node_lookup(focused->as.node.node_id); if (focused->as.node.unlocked_channels) { node_change_volume(node, false, delta, focused->as.node.focused_channel); } else { node_change_volume(node, false, delta, ALL_CHANNELS); } } void tui_bind_set_volume(union tui_bind_data data) { const float vol = data.volume; const struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } const struct node *const node = node_lookup(focused->as.node.node_id); if (focused->as.node.unlocked_channels) { node_change_volume(node, true, vol, focused->as.node.focused_channel); } else { node_change_volume(node, true, vol, ALL_CHANNELS); } } void tui_bind_change_mute(union tui_bind_data data) { const enum tui_change_mode mode = data.change_mode; const struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } const struct node *const node = node_lookup(focused->as.node.node_id); switch (mode) { case ENABLE: node_set_mute(node, true); break; case DISABLE: node_set_mute(node, false); break; case TOGGLE: node_set_mute(node, !node->mute); break; } } void tui_bind_change_channel_lock(union tui_bind_data data) { enum tui_change_mode mode = data.change_mode; struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } bool change = false; switch (mode) { case ENABLE: if (!focused->as.node.unlocked_channels) { focused->as.node.unlocked_channels = true; change = true; } break; case DISABLE: if (focused->as.node.unlocked_channels) { focused->as.node.unlocked_channels = false; change = true; } break; case TOGGLE: focused->as.node.unlocked_channels = !focused->as.node.unlocked_channels; change = true; break; } if (change) { tui_tab_item_draw(focused, TUI_TAB_ITEM_DRAW_DECORATIONS); } } static void tui_set_tab_and_redraw(int new_tab_index) { if (tui.tab_index == new_tab_index) { return; } tui.tab_index = new_tab_index; redraw_current_tab(); redraw_status_bar(); TRACE("current tab is: index %d (enum %d)", tui.tab_index, config.tab_map_index_to_enum[tui.tab_index]); } void tui_bind_change_tab(union tui_bind_data data) { int new_tab_index; switch (data.direction) { case UP: if (tui.tab_index == TUI_TAB_COUNT - 1) { new_tab_index = 0; } else { new_tab_index = tui.tab_index + 1; } break; case DOWN: if (tui.tab_index == 0) { new_tab_index = TUI_TAB_COUNT - 1; } else { new_tab_index = tui.tab_index - 1; } break; } tui_bind_set_tab_index((union tui_bind_data){ .index = new_tab_index }); } void tui_bind_set_tab(union tui_bind_data data) { tui_bind_set_tab_index((union tui_bind_data){ .index = config.tab_map_enum_to_index[data.tab] }); } void tui_bind_set_tab_index(union tui_bind_data data) { const int index = data.index; if (tui.menu_active) { return; } tui_set_tab_and_redraw(index); } void tui_bind_set_default(union tui_bind_data data) { struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } const struct node *const node = node_lookup(focused->as.node.node_id); node_set_default(node); } static void on_profile_selection_done(struct tui_menu *menu, struct tui_menu_item *pick) { const uint32_t device_id = menu->data.uint; const uint32_t profile_id = pick->data.uint; TRACE("on_profile_selection_done: device_id %d route_id %d", device_id, profile_id); struct device *device = device_lookup(device_id); if (device == NULL) { WARN("on_profile_selection_done: device with id %d does not exist", device_id); } device_set_profile(device, profile_id); tui_menu_free(menu); tui.menu_active = false; redraw_current_tab(); } void tui_bind_select_profile(union tui_bind_data data) { struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_DEVICE || tui.menu_active) { return; } const struct device *const device = device_lookup(focused->as.device.device_id); const struct profile *active_profile = device_get_active_profile(device); const struct profile *profiles; const size_t nprofiles = device_get_available_profiles(device, &profiles); if (nprofiles < 2) { return; } tui.menu = tui_menu_create(nprofiles); tui.menu->callback = on_profile_selection_done; tui.menu->data.uint = device->id; tui_menu_resize(tui.menu, tui.term_width, tui.term_height); xasprintf(&tui.menu->header, "Select profile for %s", device->description); for (size_t i = 0; i < nprofiles; i++) { const struct profile *const profile = &profiles[i]; struct tui_menu_item *const item = &tui.menu->items[i]; xasprintf(&item->str, "%d. %s (%s)", profile->index, profile->description, profile->name); item->data.uint = profile->index; if (active_profile != NULL && profile->index == active_profile->index) { tui.menu->selected = i; } } tui.menu_active = true; } static void on_route_selection_done(struct tui_menu *menu, struct tui_menu_item *pick) { const uint32_t node_id = menu->data.uint; const uint32_t route_id = pick->data.uint; TRACE("on_route_selection_done: node_id %d route_id %d", node_id, route_id); struct node *node = node_lookup(node_id); if (node == NULL) { WARN("on_route_selection_done: node with id %d does not exist", node_id); } node_set_route(node, route_id); tui_menu_free(menu); tui.menu_active = false; redraw_current_tab(); } void tui_bind_select_route(union tui_bind_data data) { struct tui_tab_item *const focused = tui.tabs[tui.tab_index].focused; if (focused == NULL || focused->type != TUI_TAB_ITEM_TYPE_NODE || tui.menu_active) { return; } const struct node *const node = node_lookup(focused->as.node.node_id); const struct route *active_route = node_get_active_route(node); const struct route *const *routes; const size_t nroutes = node_get_available_routes(node, &routes); if (nroutes < 2) { return; } tui.menu = tui_menu_create(nroutes); tui.menu->callback = on_route_selection_done; tui.menu->data.uint = node->id; tui_menu_resize(tui.menu, tui.term_width, tui.term_height); xasprintf(&tui.menu->header, "Select route for %s", node->node_name); for (size_t i = 0; i < nroutes; i++) { const struct route *route = routes[i]; struct tui_menu_item *item = &tui.menu->items[i]; xasprintf(&item->str, "%d. %s (%s)", route->index, route->description, route->name); item->data.uint = route->index; if (active_route != NULL && route->index == active_route->index) { tui.menu->selected = i; } } tui.menu_active = true; } void tui_bind_cancel_selection(union tui_bind_data data) { if (!tui.menu_active) { return; } tui_menu_free(tui.menu); tui.menu_active = false; redraw_current_tab(); } void tui_bind_confirm_selection(union tui_bind_data data) { if (!tui.menu_active) { return; } struct tui_menu *menu = tui.menu; menu->callback(menu, &menu->items[menu->selected]); } void tui_bind_quit_or_cancel_selection(union tui_bind_data data) { if (tui.menu_active) { tui_bind_cancel_selection(data); } else { tui_bind_quit(data); } } void tui_bind_quit(union tui_bind_data data) { pollen_loop_quit(event_loop, 0); } static WINDOW *tui_resize_pad(WINDOW *pad, int y, int x, bool keep_contents) { TRACE("tui_resize_pad: y %d x %d", y, x); WINDOW *new_pad = newpad(y, x); /* TODO: not the best place to put those functions */ nodelay(new_pad, TRUE); /* getch() will fail instead of blocking waiting for input */ keypad(new_pad, TRUE); if (keep_contents && pad != NULL) { copywin(pad, new_pad, 0, 0, 0, 0, y, x, FALSE); delwin(pad); } return new_pad; } /* 0 to leave dimension as is */ enum tui_set_pad_size_policy { EXACTLY, AT_LEAST }; static void tui_set_pad_size(enum tui_set_pad_size_policy y_policy, int y, enum tui_set_pad_size_policy x_policy, int x, bool keep_contents) { TRACE("tui_set_pad_size: y %s %d x %s %d", y_policy == EXACTLY ? "exactly" : "at least", y, x_policy == EXACTLY ? "exactly" : "at least", x); if (tui.pad_win == NULL) { tui.pad_win = tui_resize_pad(tui.pad_win, y, x, keep_contents); } else { bool need_resize = false; int new_y, new_x; int max_y = getmaxy(tui.pad_win); int max_x = getmaxx(tui.pad_win); switch (y_policy) { case EXACTLY: if (y != max_y) { need_resize = true; } new_y = y; break; case AT_LEAST: if (y > max_y) { need_resize = true; } new_y = MAX(max_y, y); break; } switch (x_policy) { case EXACTLY: if (x != max_x) { need_resize = true; } new_x = x; break; case AT_LEAST: if (x > max_x) { need_resize = true; } new_x = MAX(max_x, x); break; } if (need_resize) { tui.pad_win = tui_resize_pad(tui.pad_win, new_y, new_x, keep_contents); } } } /* Change size (height) of item to (new_height), * while also adjusting positions of other items in the same tab as needed. * DOES NOT DRAW ANYTHING BY ITSELF */ static void tui_tab_item_resize(struct tui_tab_item *item, int new_height) { const int diff = new_height - item->height; if (diff == 0) { return; } const struct tui_tab *const tab = &tui.tabs[item->tab_index]; struct tui_tab_item *last; LIST_GET_LAST(last, &tab->items, link); tui_set_pad_size(AT_LEAST, last->pos + last->height + diff, AT_LEAST, tui.term_width, true); TRACE("tui_tab_item_resize: resizing item %p from %d to %d", (void *)item, item->height, item->height + diff); item->height += diff; struct tui_tab_item *next; LIST_FOR_EACH_AFTER(next, &tab->items, &item->link, link) { TRACE("tui_tab_item_resize: shifting item %p from %d to %d", (void *)next, next->pos, next->pos + diff); next->pos += diff; } } /* EVENTS FOR NODE ITEMS */ static void on_node_remove(struct tui_tab_item *item) { TRACE("tui_on_node_removed: id %d", item->type == TUI_TAB_ITEM_TYPE_NODE ? item->as.node.node_id : item->as.device.device_id); signal_listener_unsubscribe(&item->node_listener); signal_listener_unsubscribe(&item->device_listener); tui_tab_item_resize(item, 0); tui_tab_item_unfocus(item, false); LIST_REMOVE(&item->link); if (item->tab_index == tui.tab_index) { redraw_current_tab(); } free(item); } static void on_node_change(struct tui_tab_item *item, const struct node *node, enum node_change_mask change) { TRACE("tui_on_node_changed: id %d", node->id); if (change & NODE_CHANGE_CHANNEL_COUNT) { int new_item_height = VEC_SIZE(&node->channels) + 3; if (node->device_id != 0) { new_item_height += 1; } tui_tab_item_resize(item, new_item_height); if (item->tab_index == tui.tab_index) { redraw_current_tab(); } } else { enum tui_tab_item_draw_mask mask = 0; if (change & NODE_CHANGE_INFO) { mask |= TUI_TAB_ITEM_DRAW_DESCRIPTION; } if (change & NODE_CHANGE_MUTE) { mask |= TUI_TAB_ITEM_DRAW_CHANNELS; mask |= TUI_TAB_ITEM_DRAW_DECORATIONS; } if (change & NODE_CHANGE_VOLUME) { mask |= TUI_TAB_ITEM_DRAW_CHANNELS; } tui_tab_item_draw(item, mask); } } static void on_device_change_for_node(uint64_t events, const struct signal_data *data, void *userdata) { struct tui_tab_item *const item = userdata; tui_tab_item_draw(item, TUI_TAB_ITEM_DRAW_ROUTES); } static void on_node_events(uint64_t events, const struct signal_data *data, void *userdata) { struct tui_tab_item *const item = userdata; switch ((enum node_events)events) { case NODE_EVENT_CHANGE: { const uint64_t change = data->as.u64; const struct node *const node = node_lookup(item->as.node.node_id); if (node != NULL) { on_node_change(item, node, change); } break; } case NODE_EVENT_DEFAULT: { tui_tab_item_draw(item, TUI_TAB_ITEM_DRAW_DESCRIPTION); break; } case NODE_EVENT_REMOVE: { on_node_remove(item); break; } default: break; } trigger_update(); } static void on_node_added(struct node *node) { TRACE("tui_on_node_added: id %d", node->id); const int tab_index = config.tab_map_enum_to_index[media_class_to_tui_tab(node->media_class)]; struct tui_tab_item *new_item = xmalloc(sizeof(*new_item)); *new_item = (struct tui_tab_item){ .tab_index = tab_index, .type = TUI_TAB_ITEM_TYPE_NODE, .as.node = { .node_id = node->id, .n_channels = VEC_SIZE(&node->channels), }, }; node_events_subscribe(node, &new_item->node_listener, (enum node_events)-1, on_node_events, new_item); if (node->device_id != 0) { struct device *device = device_lookup(node->device_id); device_events_subscribe(device, &new_item->device_listener, DEVICE_EVENT_CHANGE, on_device_change_for_node, new_item); } int new_item_height = new_item->as.node.n_channels + 3; if (node->device_id != 0) { new_item_height += 1; } LIST_INSERT(&tui.tabs[tab_index].items, &new_item->link); tui_tab_item_resize(new_item, new_item_height); if (tui.tabs[tab_index].focused == NULL || !tui.tabs[tab_index].user_changed_focus) { tui_tab_item_focus(new_item, false, false); } redraw_current_tab(); } /* EVENTS FOR DEVICE ITEMS */ static void on_device_remove(struct tui_tab_item *item) { TRACE("tui_on_device_removed: id %d", item->as.device.device_id); signal_listener_unsubscribe(&item->device_listener); tui_tab_item_resize(item, 0); tui_tab_item_unfocus(item, false); LIST_REMOVE(&item->link); if (item->tab_index == tui.tab_index) { redraw_current_tab(); } free(item); } static void on_device_events_for_device(uint64_t events, const struct signal_data *data, void *userdata) { struct tui_tab_item *const item = userdata; switch ((enum device_events)events) { case DEVICE_EVENT_CHANGE: { tui_tab_item_draw(item, TUI_TAB_ITEM_DRAW_DESCRIPTION); break; } case DEVICE_EVENT_REMOVE: { on_device_remove(item); break; } default: break; } trigger_update(); } static void on_device_added(struct device *dev) { TRACE("tui_on_device_added: id %d", dev->id); const int tab_index = config.tab_map_enum_to_index[CARDS]; struct tui_tab_item *new_item = xmalloc(sizeof(*new_item)); *new_item = (struct tui_tab_item){ .tab_index = tab_index, .type = TUI_TAB_ITEM_TYPE_DEVICE, .as.device = { .device_id = dev->id, }, }; device_events_subscribe(dev, &new_item->device_listener, (enum device_events)-1, on_device_events_for_device, new_item); const int new_item_height = 4; LIST_INSERT(&tui.tabs[tab_index].items, &new_item->link); tui_tab_item_resize(new_item, new_item_height); if (tui.tabs[tab_index].focused == NULL || !tui.tabs[tab_index].user_changed_focus) { tui_tab_item_focus(new_item, false, false); } redraw_current_tab(); } static void on_pipewire_object_added(uint64_t event, const struct signal_data *data, void *_) { switch ((enum pipewire_events)event) { case PIPEWIRE_EVENT_NODE_ADDED: { struct node *node = node_lookup(data->as.u64); if (node != NULL) { on_node_added(node); } break; } case PIPEWIRE_EVENT_DEVICE_ADDED: { struct device *device = device_lookup(data->as.u64); if (device != NULL) { on_device_added(device); } break; } default: break; } trigger_update(); } static int on_sigwinch(struct pollen_event_source *_, int _, void *_) { struct winsize winsize; if (ioctl(0 /* stdin */, TIOCGWINSZ, &winsize) < 0) { ERROR("failed to get new window size: %s", strerror(errno)); return -1; } resize_term(winsize.ws_row, winsize.ws_col); tui.term_height = getmaxy(stdscr); tui.term_width = getmaxx(stdscr); DEBUG("new window dimensions %d lines %d columns", tui.term_height, tui.term_width); tui_set_pad_size(AT_LEAST, tui.term_height, EXACTLY, tui.term_width, false); if (tui.tabs[tui.tab_index].focused != NULL) { tui_tab_item_ensure_visible(tui.tabs[tui.tab_index].focused); } if (tui.bar_win != NULL) { delwin(tui.bar_win); } tui.bar_win = newwin(1, tui.term_width, 0, 0); redraw_current_tab(); redraw_status_bar(); if (tui.menu_active) { tui_menu_resize(tui.menu, tui.term_width, tui.term_height); } trigger_update(); return 0; } static int on_stdin_ready(struct pollen_event_source *_, int _, uint32_t _, void *_) { wint_t ch; while (errno = 0, wget_wch(tui.pad_win, &ch) != ERR || errno == EINTR) { struct tui_bind *bind = MAP_GET(&config.binds, ch); if (bind == NULL) { DEBUG("unhandled key %s (%d)", key_name_from_key_code(ch), ch); } else { bind->func(bind->data); trigger_update(); } } return 0; } /* * Trying to optimize updates is brain damage and I don't wanna deal with it. * Instead just update after any event that might or might not cause a draw * and let ncurses figure out the rest, it's good at damage tracking */ static int on_update_triggered(struct pollen_event_source *_, uint64_t _, void *_) { tui.efd_triggered = false; pnoutrefresh(tui.pad_win, tui.tabs[tui.tab_index].scroll_pos, 0, 1, 0, tui.term_height - 1, tui.term_width - 1); wnoutrefresh(tui.bar_win); if (tui.menu_active) { tui_menu_draw(tui.menu); } doupdate(); return 0; } int tui_init(void) { initscr(); refresh(); /* https://stackoverflow.com/a/22121866 */ cbreak(); noecho(); curs_set(0); ESCDELAY = 50 /* ms */; start_color(); use_default_colors(); init_pair(GREEN, COLOR_GREEN, -1); init_pair(YELLOW, COLOR_YELLOW, -1); init_pair(RED, COLOR_RED, -1); FOR_EACH_TAB(tab) { LIST_INIT(&tui.tabs[tab].items); } pollen_loop_add_fd(event_loop, 0 /* stdin */, EPOLLIN, false, on_stdin_ready, NULL); pollen_loop_add_signal(event_loop, SIGWINCH, on_sigwinch, NULL); tui.efd_source = pollen_loop_add_efd(event_loop, on_update_triggered, NULL); tui.efd_triggered = false; pipewire_events_subscribe(&tui.pipewire_listener, (uint64_t)-1, on_pipewire_object_added, NULL); tui.tab_index = config.tab_map_enum_to_index[config.default_tab]; /* send SIGWINCH to self to pick up initial terminal size */ raise(SIGWINCH); return 0; } int tui_cleanup(void) { if (tui.bar_win != NULL) { delwin(tui.bar_win); } if (tui.pad_win != NULL) { delwin(tui.pad_win); } endwin(); return 0; } pipemixer-0.4.0/src/tui.h000066400000000000000000000061031512217147000152560ustar00rootroot00000000000000#pragma once #include #include "collections/list.h" #include "signals.h" #include "menu.h" enum tui_tab_type { PLAYBACK, RECORDING, INPUT_DEVICES, OUTPUT_DEVICES, CARDS, TUI_TAB_COUNT, TUI_TAB_INVALID, }; struct tui_tab { LIST_HEAD items; struct tui_tab_item *focused; int scroll_pos; bool user_changed_focus; }; struct tui { int term_height, term_width; WINDOW *bar_win; WINDOW *pad_win; bool menu_active; struct tui_menu *menu; int tab_index; struct tui_tab tabs[TUI_TAB_COUNT]; bool efd_triggered; struct pollen_event_source *efd_source; struct signal_listener pipewire_listener; }; enum tui_tab_item_draw_mask { TUI_TAB_ITEM_DRAW_NOTHING = 0, TUI_TAB_ITEM_DRAW_EVERYTHING = ~0, TUI_TAB_ITEM_DRAW_DESCRIPTION = 1 << 0, TUI_TAB_ITEM_DRAW_DECORATIONS = 1 << 1, TUI_TAB_ITEM_DRAW_CHANNELS = 1 << 2, TUI_TAB_ITEM_DRAW_ROUTES = 1 << 3, TUI_TAB_ITEM_DRAW_PROFILES = 1 << 4, TUI_TAB_ITEM_DRAW_BORDERS = 1 << 5, TUI_TAB_ITEM_DRAW_BLANKS = 1 << 6, }; enum tui_tab_item_type { TUI_TAB_ITEM_TYPE_NODE, TUI_TAB_ITEM_TYPE_DEVICE, }; struct tui_tab_item { int pos, height; bool focused; enum tui_tab_item_type type; union { struct { uint32_t node_id; uint32_t n_channels; bool unlocked_channels; uint32_t focused_channel; } node; struct { uint32_t device_id; } device; } as; struct signal_listener device_listener; struct signal_listener node_listener; int tab_index; LIST_ENTRY link; }; extern struct tui tui; int tui_init(void); int tui_cleanup(void); /* binds */ union tui_bind_data; typedef void (*tui_bind_func_t)(union tui_bind_data data); enum tui_direction { UP, DOWN }; void tui_bind_change_focus(union tui_bind_data data); void tui_bind_change_volume(union tui_bind_data data); void tui_bind_change_tab(union tui_bind_data data); void tui_bind_set_volume(union tui_bind_data data); void tui_bind_set_tab(union tui_bind_data data); void tui_bind_set_tab_index(union tui_bind_data data); enum tui_change_mode { ENABLE, DISABLE, TOGGLE }; void tui_bind_change_mute(union tui_bind_data data); void tui_bind_change_channel_lock(union tui_bind_data data); enum tui_nothing { NOTHING }; void tui_bind_focus_first(union tui_bind_data data); void tui_bind_focus_last(union tui_bind_data data); void tui_bind_confirm_selection(union tui_bind_data data); void tui_bind_cancel_selection(union tui_bind_data data); void tui_bind_quit_or_cancel_selection(union tui_bind_data data); void tui_bind_quit(union tui_bind_data data); void tui_bind_set_default(union tui_bind_data data); void tui_bind_select_route(union tui_bind_data data); void tui_bind_select_profile(union tui_bind_data data); union tui_bind_data { enum tui_direction direction; enum tui_change_mode change_mode; enum tui_tab_type tab; enum tui_nothing nothing; float volume; int index; }; struct tui_bind { union tui_bind_data data; tui_bind_func_t func; }; pipemixer-0.4.0/src/utils.c000066400000000000000000000245021512217147000156130ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "utils.h" #include "macros.h" #include "xmalloc.h" const char *channel_name_from_enum(enum spa_audio_channel chan) { switch (chan) { case SPA_AUDIO_CHANNEL_UNKNOWN: return "UNK"; case SPA_AUDIO_CHANNEL_NA: return "NA"; case SPA_AUDIO_CHANNEL_MONO: return "MONO"; case SPA_AUDIO_CHANNEL_FL: return "FL"; case SPA_AUDIO_CHANNEL_FR: return "FR"; case SPA_AUDIO_CHANNEL_FC: return "FC"; case SPA_AUDIO_CHANNEL_LFE: return "LFE"; case SPA_AUDIO_CHANNEL_SL: return "SL"; case SPA_AUDIO_CHANNEL_SR: return "SR"; case SPA_AUDIO_CHANNEL_FLC: return "FLC"; case SPA_AUDIO_CHANNEL_FRC: return "FRC"; case SPA_AUDIO_CHANNEL_RC: return "RC"; case SPA_AUDIO_CHANNEL_RL: return "RL"; case SPA_AUDIO_CHANNEL_RR: return "RR"; case SPA_AUDIO_CHANNEL_TC: return "TC"; case SPA_AUDIO_CHANNEL_TFL: return "TFL"; case SPA_AUDIO_CHANNEL_TFC: return "TFC"; case SPA_AUDIO_CHANNEL_TFR: return "TFR"; case SPA_AUDIO_CHANNEL_TRL: return "TRL"; case SPA_AUDIO_CHANNEL_TRC: return "TRC"; case SPA_AUDIO_CHANNEL_TRR: return "TRR"; case SPA_AUDIO_CHANNEL_RLC: return "RLC"; case SPA_AUDIO_CHANNEL_RRC: return "RRC"; case SPA_AUDIO_CHANNEL_FLW: return "FLW"; case SPA_AUDIO_CHANNEL_FRW: return "FRW"; case SPA_AUDIO_CHANNEL_LFE2: return "LFE2"; case SPA_AUDIO_CHANNEL_FLH: return "FLH"; case SPA_AUDIO_CHANNEL_FCH: return "FCH"; case SPA_AUDIO_CHANNEL_FRH: return "FRH"; case SPA_AUDIO_CHANNEL_TFLC: return "TFLC"; case SPA_AUDIO_CHANNEL_TFRC: return "TFRC"; case SPA_AUDIO_CHANNEL_TSL: return "TSL"; case SPA_AUDIO_CHANNEL_TSR: return "TSR"; case SPA_AUDIO_CHANNEL_LLFE: return "LLFR"; case SPA_AUDIO_CHANNEL_RLFE: return "RLFE"; case SPA_AUDIO_CHANNEL_BC: return "BC"; case SPA_AUDIO_CHANNEL_BLC: return "BLC"; case SPA_AUDIO_CHANNEL_BRC: return "BRC"; case SPA_AUDIO_CHANNEL_AUX0: return "AUX0"; case SPA_AUDIO_CHANNEL_AUX1: return "AUX1"; case SPA_AUDIO_CHANNEL_AUX2: return "AUX2"; case SPA_AUDIO_CHANNEL_AUX3: return "AUX3"; case SPA_AUDIO_CHANNEL_AUX4: return "AUX4"; case SPA_AUDIO_CHANNEL_AUX5: return "AUX5"; case SPA_AUDIO_CHANNEL_AUX6: return "AUX6"; case SPA_AUDIO_CHANNEL_AUX7: return "AUX7"; case SPA_AUDIO_CHANNEL_AUX8: return "AUX8"; case SPA_AUDIO_CHANNEL_AUX9: return "AUX9"; case SPA_AUDIO_CHANNEL_AUX10: return "AUX10"; case SPA_AUDIO_CHANNEL_AUX11: return "AUX11"; case SPA_AUDIO_CHANNEL_AUX12: return "AUX12"; case SPA_AUDIO_CHANNEL_AUX13: return "AUX13"; case SPA_AUDIO_CHANNEL_AUX14: return "AUX14"; case SPA_AUDIO_CHANNEL_AUX15: return "AUX15"; case SPA_AUDIO_CHANNEL_AUX16: return "AUX16"; case SPA_AUDIO_CHANNEL_AUX17: return "AUX17"; case SPA_AUDIO_CHANNEL_AUX18: return "AUX18"; case SPA_AUDIO_CHANNEL_AUX19: return "AUX19"; case SPA_AUDIO_CHANNEL_AUX20: return "AUX20"; case SPA_AUDIO_CHANNEL_AUX21: return "AUX21"; case SPA_AUDIO_CHANNEL_AUX22: return "AUX22"; case SPA_AUDIO_CHANNEL_AUX23: return "AUX23"; case SPA_AUDIO_CHANNEL_AUX24: return "AUX24"; case SPA_AUDIO_CHANNEL_AUX25: return "AUX25"; case SPA_AUDIO_CHANNEL_AUX26: return "AUX26"; case SPA_AUDIO_CHANNEL_AUX27: return "AUX27"; case SPA_AUDIO_CHANNEL_AUX28: return "AUX28"; case SPA_AUDIO_CHANNEL_AUX29: return "AUX29"; case SPA_AUDIO_CHANNEL_AUX30: return "AUX30"; case SPA_AUDIO_CHANNEL_AUX31: return "AUX31"; case SPA_AUDIO_CHANNEL_AUX32: return "AUX32"; case SPA_AUDIO_CHANNEL_AUX33: return "AUX33"; case SPA_AUDIO_CHANNEL_AUX34: return "AUX34"; case SPA_AUDIO_CHANNEL_AUX35: return "AUX35"; case SPA_AUDIO_CHANNEL_AUX36: return "AUX36"; case SPA_AUDIO_CHANNEL_AUX37: return "AUX37"; case SPA_AUDIO_CHANNEL_AUX38: return "AUX38"; case SPA_AUDIO_CHANNEL_AUX39: return "AUX39"; case SPA_AUDIO_CHANNEL_AUX40: return "AUX40"; case SPA_AUDIO_CHANNEL_AUX41: return "AUX41"; case SPA_AUDIO_CHANNEL_AUX42: return "AUX42"; case SPA_AUDIO_CHANNEL_AUX43: return "AUX43"; case SPA_AUDIO_CHANNEL_AUX44: return "AUX44"; case SPA_AUDIO_CHANNEL_AUX45: return "AUX45"; case SPA_AUDIO_CHANNEL_AUX46: return "AUX46"; case SPA_AUDIO_CHANNEL_AUX47: return "AUX47"; case SPA_AUDIO_CHANNEL_AUX48: return "AUX48"; case SPA_AUDIO_CHANNEL_AUX49: return "AUX49"; case SPA_AUDIO_CHANNEL_AUX50: return "AUX50"; case SPA_AUDIO_CHANNEL_AUX51: return "AUX51"; case SPA_AUDIO_CHANNEL_AUX52: return "AUX52"; case SPA_AUDIO_CHANNEL_AUX53: return "AUX53"; case SPA_AUDIO_CHANNEL_AUX54: return "AUX54"; case SPA_AUDIO_CHANNEL_AUX55: return "AUX55"; case SPA_AUDIO_CHANNEL_AUX56: return "AUX56"; case SPA_AUDIO_CHANNEL_AUX57: return "AUX57"; case SPA_AUDIO_CHANNEL_AUX58: return "AUX58"; case SPA_AUDIO_CHANNEL_AUX59: return "AUX59"; case SPA_AUDIO_CHANNEL_AUX60: return "AUX60"; case SPA_AUDIO_CHANNEL_AUX61: return "AUX61"; case SPA_AUDIO_CHANNEL_AUX62: return "AUX62"; case SPA_AUDIO_CHANNEL_AUX63: return "AUX63"; default: return "?????"; } } const char *key_name_from_key_code(wint_t code) { static char codepoint[5]; if (iswgraph(code)) { snprintf(codepoint, sizeof(codepoint), "%lc", code); return codepoint; } switch (code) { case 0: return "NUL"; case 1: return "SOH"; case 2: return "STX"; case 3: return "ETX"; case 4: return "EOT"; case 5: return "ENQ"; case 6: return "ACK"; case 7: return "BEL"; case 8: return "BS"; case 9: return "HT"; case 10: return "LF"; case 11: return "VT"; case 12: return "FF"; case 13: return "CR"; case 14: return "SO"; case 15: return "SI"; case 16: return "DLE"; case 17: return "DC1"; case 18: return "DC2"; case 19: return "DC3"; case 20: return "DC4"; case 21: return "NAK"; case 22: return "SYN"; case 23: return "ETB"; case 24: return "CAN"; case 25: return "EM"; case 26: return "SUB"; case 27: return "ESC"; case 28: return "FS"; case 29: return "GS"; case 30: return "RS"; case 31: return "US"; case 32: return "SPACE"; /* Fallback */ default: return "?????"; } } /* TODO: this function is probably wrong in some way */ bool key_code_from_key_name(const char *name, wint_t *keycode) { if (name == NULL || name[0] == '\0') { return false; } if (name[1] == '\0' && isgraph(name[0])) { /* printable ascii */ *keycode = name[0]; return true; } else if (name[2] == '\0' || name[3] == '\0' || name[4] == '\0') { /* a single utf-8 char */ mbtowc(NULL, NULL, 0); /* reset state */ wchar_t res; int ret = mbtowc(&res, name, 4); if (ret > 0 && name[ret] == '\0' && iswgraph(res)) { *keycode = res; return true; } } if (STREQ(name, "up")) { *keycode = KEY_UP; return true; }; if (STREQ(name, "down")) { *keycode = KEY_DOWN; return true; }; if (STREQ(name, "left")) { *keycode = KEY_LEFT; return true; }; if (STREQ(name, "right")) { *keycode = KEY_RIGHT; return true; }; if (STREQ(name, "enter")) { *keycode = '\n'; return true; }; if (STREQ(name, "tab")) { *keycode = '\t'; return true; }; if (STREQ(name, "backtab")) { *keycode = KEY_BTAB; return true; }; if (STREQ(name, "space")) { *keycode = ' '; return true; }; if (STREQ(name, "backspace")) { *keycode = KEY_BACKSPACE; return true; }; if (STREQ(name, "escape")) { *keycode = '\e'; return true; }; const char *prefix; if (prefix = "code:", STRSTARTSWITH(name, prefix)) { const char *num = name + strlen(prefix); unsigned long code; if (str_to_ulong(num, &code) && code < INT_MAX) { *keycode = code; return true; } } return false; } bool str_to_long(const char *str, long *res) { char *endptr = NULL; int base = 10; if (str[0] == '0' && str[1] == 'x') { base = 16; } errno = 0; long res_tmp = strtol(str, &endptr, base); if (errno == 0 && *endptr == '\0') { *res = res_tmp; return true; } return false; } bool str_to_ulong(const char *str, unsigned long *res) { char *endptr = NULL; int base = 10; if (str[0] == '0' && str[1] == 'x') { base = 16; } errno = 0; unsigned long res_tmp = strtoul(str, &endptr, base); if (errno == 0 && *endptr == '\0') { *res = res_tmp; return true; } return false; } bool str_to_u32(const char *str, uint32_t *res) { unsigned long res_tmp; if (!str_to_ulong(str, &res_tmp) || res_tmp > UINT32_MAX) { return false; } else { *res = res_tmp; return true; } } bool str_to_i32(const char *str, int32_t *res) { long res_tmp; if (!str_to_long(str, &res_tmp) || res_tmp > INT32_MAX || res_tmp < INT32_MIN) { return false; } else { *res = res_tmp; return true; } } size_t wcstrimcols(wchar_t *str, size_t col) { size_t width = 0; size_t n_chars = 0; wchar_t *p = str; wchar_t c; while ((c = *p) != L'\0') { if ((width += wcwidth(c)) > col) { *p = L'\0'; break; } n_chars += 1; p += 1; } return n_chars; } char *read_string_from_fd(int fd, size_t *len) { const size_t chunk_size = 1024; size_t capacity = chunk_size; size_t length = 0; char *buffer = xmalloc(capacity + 1 /* terminator */); while (1) { if (length + chunk_size > capacity) { capacity *= 2; buffer = xrealloc(buffer, capacity + 1 /* terminator */); } ssize_t bytes_read = read(fd, buffer + length, chunk_size); if (bytes_read < 0) { goto err; } else if (bytes_read == 0) { /* EOF */ break; } else { length += bytes_read; } } buffer[length] = '\0'; if (len != NULL) { *len = length; } return buffer; err: if (len != NULL) { *len = 0; } return NULL; } bool streq(const char *a, const char *b) { if (a == NULL || b == NULL) { return false; } else { return strcmp(a, b) == 0; } } pipemixer-0.4.0/src/utils.h000066400000000000000000000014141512217147000156150ustar00rootroot00000000000000#pragma once #include #include #include #define CHANNEL_NAME_LENGTH_MAX 5 /* without null terminator */ const char *channel_name_from_enum(enum spa_audio_channel chan); const char *key_name_from_key_code(wint_t code); bool key_code_from_key_name(const char *name, wint_t *keycode); bool str_to_ulong(const char *str, unsigned long *res); bool str_to_long(const char *str, long *res); bool str_to_u32(const char *str, uint32_t *res); bool str_to_i32(const char *str, int32_t *res); /* modifies string in place! */ size_t wcstrimcols(wchar_t *str, size_t col); char *read_string_from_fd(int fd, size_t *len); /* compares two strings, safely handles NULL (NULL is distinct from NULL) */ bool streq(const char *a, const char *b); pipemixer-0.4.0/src/xmalloc.c000066400000000000000000000025341512217147000161130ustar00rootroot00000000000000#include #include #include #include #include "xmalloc.h" static void *check_alloc(void *const alloc) { if (alloc == NULL) { fprintf(stderr, "memory allocation failed, buy more ram lol\n"); fflush(stderr); abort(); } return alloc; } void *xmalloc(size_t size) { return check_alloc(malloc(size)); } void *xzalloc(size_t size) { return memset(check_alloc(malloc(size)), '\0', size); } void *xcalloc(size_t n, size_t size) { return check_alloc(calloc(n, size)); } void *xrealloc(void *ptr, size_t size) { return check_alloc(realloc(ptr, size)); } void *xreallocarray(void *ptr, size_t nmemb, size_t size) { return check_alloc(reallocarray(ptr, nmemb, size)); } char *xstrdup(const char *s) { return (s == NULL) ? NULL : check_alloc(strdup(s)); } static int xvasprintf(char **restrict strp, const char *restrict fmt, va_list ap) { va_list ap_copy; va_copy(ap_copy, ap); int length = vsnprintf(NULL, 0, fmt, ap_copy); va_end(ap_copy); if (length < 0) { return -1; } *strp = check_alloc(malloc(length + 1)); return vsnprintf(*strp, length + 1, fmt, ap); } int xasprintf(char **restrict strp, const char *restrict fmt, ...) { va_list args; va_start(args, fmt); int ret = xvasprintf(strp, fmt, args); va_end(args); return ret; } pipemixer-0.4.0/src/xmalloc.h000066400000000000000000000012321512217147000161120ustar00rootroot00000000000000#pragma once #include #include /* malloc, but aborts on alloc fail */ void *xmalloc(size_t size); /* xmalloc that also zero initialises memory */ void *xzalloc(size_t size); /* calloc, but aborts on alloc fail */ void *xcalloc(size_t n, size_t size); /* realloc, but aborts on alloc fail */ void *xrealloc(void *ptr, size_t size); /* reallocarray, but aborts on alloc fail */ void *xreallocarray(void *ptr, size_t nmemb, size_t size); /* strdup, but aborts on alloc fail and returns NULL when called on NULL */ char *xstrdup(const char *s); /* printf to mallocd string */ int xasprintf(char **restrict strp, const char *restrict fmt, ...);