main 039463c4a62a cached
66 files
216.9 KB
55.6k tokens
259 symbols
1 requests
Download .txt
Showing preview only (236K chars total). Download the full file or copy to clipboard to get everything.
Repository: aleho/gnome-shell-volume-mixer
Branch: main
Commit: 039463c4a62a
Files: 66
Total size: 216.9 KB

Directory structure:
gitextract_5h_ew3wj/

├── .editorconfig
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── workflows/
│       └── linting.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── tmux.conf
│   └── tools.sh
├── crowdin.yml
├── package.json
├── pubkey.asc
├── shell-volume-mixer@derhofbauer.at/
│   ├── extension.js
│   ├── lib/
│   │   ├── dbus/
│   │   │   └── dbus.js
│   │   ├── main.js
│   │   ├── menu/
│   │   │   ├── indicator.js
│   │   │   └── menu.js
│   │   ├── settings.js
│   │   ├── utils/
│   │   │   ├── cards.js
│   │   │   ├── eventBroker.js
│   │   │   ├── eventHandlerDelegate.js
│   │   │   ├── hotkeys.js
│   │   │   ├── log.js
│   │   │   ├── paHelper.js
│   │   │   ├── process.js
│   │   │   ├── string.js
│   │   │   └── utils.js
│   │   ├── volume/
│   │   │   ├── mixer.js
│   │   │   └── profiles.js
│   │   └── widget/
│   │       ├── floatingLabel.js
│   │       ├── menuItem.js
│   │       ├── panelButton.js
│   │       ├── percentageLabel.js
│   │       ├── slider.js
│   │       └── volume.js
│   ├── locale/
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   └── translations.pot
│   ├── metadata.json
│   ├── pautils/
│   │   ├── lib/
│   │   │   ├── __init__.py
│   │   │   ├── cards.py
│   │   │   ├── libpulse.py
│   │   │   ├── log.py
│   │   │   ├── pulseaudio.py
│   │   │   └── sinks.py
│   │   └── query.py
│   ├── prefs.js
│   ├── prefs.ui
│   ├── schemas/
│   │   ├── gschemas.compiled
│   │   └── org.gnome.shell.extensions.shell-volume-mixer.gschema.xml
│   └── stylesheet.css
└── styles.scss

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[Makefile]
indent_style = tab


================================================
FILE: .eslintrc.js
================================================
module.exports = {
    'env': {
        'es6': true,
    },
    'globals': {
        '_':               false,
        'ARGV':            false,
        'C_':              false,
        'Debugger':        false,
        'GjsFileImporter': false,
        'global':          false,
        'imports':         false,
        'InternalError':   false,
        'Iterator':        false,
        'log':             false,
        'logError':        false,
        'N_':              false,
        'ngettext':        false,
        'print':           false,
        'printerr':        false,
        'StopIteration':   false,
        'uneval':          false,
        'window':          false,
    },
    'extends': 'eslint:recommended',
    'parserOptions': {
        'ecmaVersion': 2018,
    },
    'rules': {
        'indent': [
            'error',
            4,
            { 'SwitchCase': 1 },
        ],
        'linebreak-style': [
            'error',
            'unix',
        ],
        'quotes': [
            'error',
            'single',
            { 'allowTemplateLiterals': true },
        ],
        'semi': [
            'error',
            'always',
        ],
        'no-unused-vars': [
            2,
            { 'vars': 'local', 'args': 'after-used' },
        ],
    },
};


================================================
FILE: .gitattributes
================================================
shell-volume-mixer@derhofbauer.at/schemas/gschemas.compiled binary


================================================
FILE: .github/workflows/linting.yml
================================================
name: Linting

on:
  push:
    branches: [ main ]
  pull_request:

jobs:

  checks:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js 14
        uses: actions/setup-node@v2
        with:
          node-version: '14'

      - name: Cache node modules
        id: node-cache
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: npm install
        if: steps.node-cache.outputs.cache-hit != 'true'
        run: npm install

      - name: Run ESLint
        run: npm run eslint


================================================
FILE: .gitignore
================================================
node_modules/
build/

*.swp
*.pyc
*.*~

shell-volume-mixer-*.zip


================================================
FILE: .gitmodules
================================================
[submodule "gnome-shell-sass"]
	path = gnome-shell-sass
	url = https://gitlab.gnome.org/GNOME/gnome-shell-sass.git


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  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
this service 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 make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  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.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
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
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the 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 a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE 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.

                     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
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    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 2 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, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision 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, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This 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.


================================================
FILE: Makefile
================================================
VERSION = 42.0
EXTENSION = shell-volume-mixer@derhofbauer.at

SRCDIR = $(EXTENSION)
BUILDDIR = build/
PACKAGE = shell-volume-mixer-$(VERSION).zip

FILES = LICENSE README.md

LOCALE_DIR = $(SRCDIR)/locale
LOCALES_SRC = $(foreach dir,$(LOCALE_DIR),$(wildcard $(dir)/*/*/*.po))
LOCALES = $(patsubst %.po,%.mo,$(LOCALES_SRC))

SOURCES = \
	pautils/lib/*.py \
	pautils/query.py \
	lib/** \
	*.js \
	prefs.ui \
	stylesheet.css \
	$(LOCALES:$(SRCDIR)/%=%) \
	$(GSCHEMA) $(SCHEMA_COMP)

I18N = \
	*.js \
	lib/**/*.js \
	prefs.ui

SCHEMA_COMP = schemas/gschemas.compiled
GSCHEMA = schemas/org.gnome.shell.extensions.shell-volume-mixer.gschema.xml

SRCFILES = $(addprefix $(SRCDIR)/, $(SOURCES) $(GSCHEMA) $(GSCHEMA_COMP))


dist: clean build check package
build: install-deps i18n stylesheet.css
package: $(PACKAGE)

prepare:
	mkdir -p $(BUILDDIR)

install-deps:
	npm install
	git submodule update --init

$(SRCDIR)/$(SCHEMA_COMP): $(SRCDIR)/$(GSCHEMA)
	glib-compile-schemas --targetdir=$(SRCDIR)/schemas $(SRCDIR)/schemas

$(PACKAGE): metadata.json $(SRCFILES) $(FILES)
	cd $(SRCDIR) && zip -r ../$(PACKAGE) $(SOURCES)
	zip $(PACKAGE) $(FILES)
	cd $(BUILDDIR) && zip ../$(PACKAGE) *

i18n: $(LOCALES_SRC)
	@xgettext \
		--keyword --keyword=__ \
		--omit-header \
		--default-domain=$(EXTENSION) \
		--from-code=UTF-8 \
		--output=$(LOCALE_DIR)/translations.pot \
		 $(wildcard $(addprefix $(SRCDIR)/, $(I18N)))

metadata.json: prepare
	cat $(addprefix $(SRCDIR)/, metadata.json) | grep -v '"version":' > $(BUILDDIR)/metadata.json

stylesheet.css:
	npm run build
	# remove harmful content produced by gnome-shell-sass
	sed -i '/\/\*\sGlobal\sValues\s\*\//,/\/\*\sGeneral\sTypography\s\*/d' $(SRCDIR)/stylesheet.css

check:
	npm run eslint

clean:
	@test ! -d "$(BUILDDIR)" || rm -rf $(BUILDDIR)
	@test ! -f "$(SRCDIR)/$(SCHEMA_COMP)" || rm $(SRCDIR)/$(SCHEMA_COMP)
	@test ! -f "$(PACKAGE)" || rm $(PACKAGE)


.PHONY: clean i18n


================================================
FILE: README.md
================================================
GNOME Shell Volume Mixer
========================

[![Linting](https://github.com/aleho/gnome-shell-volume-mixer/actions/workflows/linting.yml/badge.svg)](https://github.com/aleho/gnome-shell-volume-mixer/actions/workflows/linting.yml)

Shell Volume Mixer is an extension for GNOME Shell allowing separate
configuration of PulseAudio devices and output switches. It features a profile
switcher to quickly switch between pinned profiles and devices.

Middle mouse click on an indicator or a slider mutes the selected stream.

Indicators and streams are also scrollable,


<img src="/screenshot_1.png" alt="Outputs menu" width="40%"><img alt="Inputs menu" src="/screenshot_2.png" width="40%">


Requirements
------------

- PulseAudio (for retrieval of card details)
- gettext (for building of language files)
- nodejs / npm (styles and linting)
- glib2 bin (schema compilation)


Installation
------------

```
$ make
```

That's it. Add the resulting archive via GNOME Tweak Tool (extensions tab) or
copy it's content manually to
".~/.local/share/gnome-shell/extensions/shell-volume-mixer@derhofbauer.at".


Volume Steps
------------


GNOME Settings Daemon (GSD) hardcodes the step for each key press of volume keys
to 6% of maximum. While this might be OK for most people, some would prefer a
configurable setting. There's a bug in GNOME's tracker which, according to the
comments by developers, won't ever get fixed in a way that could allow
configurable volume
steps<sup>[[1]](https://bugzilla.gnome.org/show_bug.cgi?id=650371)</sup>.

Shell Volume Mixer tried to grab GSD's hotkeys to provide configurable steps
for sliders and media keys in the past, but at some point this stopped working.

GNOME's current solution to the problem is Shift + Key, i.e. hold down the shift
button to switch to a 2% step.


Acknowledgments
---------------

This is a fork of AdvancedVolumeMixer by [Harry Karvonen](https://github.com/Hatell)
(git://repo.or.cz/AdvancedVolumeMixer.git).
Many thanks go out to him for his initial work.


================================================
FILE: bin/tmux.conf
================================================
set -g mouse on


================================================
FILE: bin/tools.sh
================================================
#!/usr/bin/env bash
set -e


WINDOW_MODE=1280x800
X11=0
NAME=""
TMUX_SESSION="gsvm"


function print_help() {
    cat <<- EOT
		Shell Volume Mixer dev toolkit

		    *) test  Runs the extension in a nested session
		         --mode  Sets the nested session window size (default: 1280x800)
		         --x11   Runs a X11/xorg session (defaulting to wayland)

		    *) lg  Toggles Looking Glass via D-Bus

		    *) command  Executes a D-Bus call to debug the running extension
		                (use "help" or see D-Bus interface for methods)

		    *) add-sink  Adds a virtual sink via PulseAudio
		         --name  Virtual sink name

		    *) debug  Enables or disables debugging [true, false]
EOT
}

###

COMMAND=$1
if [[ -z $COMMAND ]]; then
    echo "Command required"
    echo ""
    print_help
    exit 1
fi

shift

OPTIONS=$(getopt -n "$0" -o h --long help,mode:,name:,x11 -- "$@")

if [[ $? -ne 0 ]]; then
    print_help
    exit 1
fi

eval set -- "$OPTIONS"

while true; do
    case $1 in
        --mode)
            WINDOW_MODE=$2
            shift
            ;;

        --name)
            NAME=$2
            shift
            ;;

        --x11)
            X11=1
            ;;


        -h|--help)
            print_help
            exit
            ;;

        --)
            shift
            break
            ;;
        *)
            print_help
            exit 1
            ;;
    esac

    shift
done


###


function add_virtual_sink() {
    local props
    if [[ -n $NAME ]]; then
        props="sink_properties=device.description=$NAME"
    fi

    local pipewire_opts="object.linger=1 media.class=Audio/Sink"

    pactl load-module module-null-sink sink_name=svm-${NAME:-virtual-sink} $pipewire_opts "$props" \
        || pactl load-module module-null-sink sink_name=svm-${NAME:-virtual-sink} "$props"
}

function toggle_looking_glass() {
    gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval 'Main.createLookingGlass() && Main.lookingGlass.toggle();'
}


function enable_debugging() {
    local enable="$1"

    if [[ $enable == 1 || $enable == true ]]; then
        echo "Enabling debug"
        enable="true"
    else
        echo "Disabling debug"
        enable="false"
    fi

    dconf write /org/gnome/shell/extensions/shell-volume-mixer/debug $enable
}

function has_tmux_session() {
    set +e
    if tmux has-session -t $TMUX_SESSION 2>/dev/null; then
        echo 1
    else
        echo 0
    fi
    set -e
}

function run_nested_session() {
    local manager=""
    if [[ $X11 == 1 ]]; then
        manager="--x11"
    fi

    if [[ $(has_tmux_session) == 1 ]]; then
        tmux kill-session -t $TMUX_SESSION
    fi

    dbus-run-session -- tmux -f bin/tmux.conf new-session -s $TMUX_SESSION "bin/tools.sh run-test-session --mode=${WINDOW_MODE} $manager"
}

function run_test_session() {
    local manager
    if [[ $X11 == 1 ]]; then
        manager="--x11"
    else
        manager="--wayland"
    fi

    set -x
    export MUTTER_DEBUG_NUM_DUMMY_MONITORS=1
    export MUTTER_DEBUG_DUMMY_MODE_SPECS="${WINDOW_MODE}"
    gnome-shell --nested $manager
}

function dbus_command() {
    local command="$1"
    local args="$2"

    if [[ -z $command ]]; then
        echo "Command needed"
        exit 1
    fi

    local command=(
        gdbus call
        --session
        --dest "org.gnome.Shell"
        --object-path "/at/derhofbauer/shell/VolumeMixer"
        --method "at.derhofbauer.shell.VolumeMixer.$command"
    )

    if [[ -n $args ]]; then
        command+=("$args")
    fi

    if [[ $(has_tmux_session) == 1 ]]; then
        local buffer="_cmd_output"

        if [[ -f $buffer ]]; then
            rm $buffer
        fi

        echo "Running command in test session"
        tmux new-window -n dbus-command -t $TMUX_SESSION: "${command[@]}" \; pipe-pane "cat > $buffer"

        sleep .3
        cat $buffer
        rm $buffer

    else
        "${command[@]}"
    fi
}


###


case $COMMAND in
    add-sink)
        add_virtual_sink
        ;;

    lg)
        toggle_looking_glass
        ;;

    test)
        run_nested_session
        ;;

    run-test-session)
        run_test_session
        ;;

    command)
        dbus_command "${@}"
        ;;

    debug)
        enable_debugging "$1"
        ;;

    *)
        print_help
        exit 1
        ;;
esac


================================================
FILE: crowdin.yml
================================================
files:
  - source: /shell-volume-mixer@derhofbauer.at/locale/translations.pot
    translation: /shell-volume-mixer@derhofbauer.at/locale/%osx_locale%/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po


================================================
FILE: package.json
================================================
{
  "license": "GPL-2.0-only",
  "repository": "https://github.com/aleho/gnome-shell-volume-mixer",
  "devDependencies": {
    "eslint": "^8.13.0",
    "sass": "^1.50.0"
  },
  "scripts": {
    "eslint": "eslint shell-volume-mixer@derhofbauer.at",
    "build": "sass --no-source-map styles.scss shell-volume-mixer@derhofbauer.at/stylesheet.css"
  }
}


================================================
FILE: pubkey.asc
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBFfd0QwBCADiW+vORNE/bGD7p8eUVIJ00REornZ4AfPMBb2gNN/AFG4MnN/c
uSgL1Tlbe7YuNaUMmtNWx/c41k7PYab8IciD/at0KBr/0hTRezk3a6aJlKfxibLF
hiQsCXkgg4SzTcovDnZSsNf3nlybDlY3824w1vNhyhvMYSG05aQkufeJadlN+66t
vgjfUeQHh2cVC3QalK19iq1h324DQsoYQfcj6xNMBHqIeWitqsM0yqxP6poxDeYT
MBBmXdnOMkJQMwNfo+2eMl9k35c0IhDoIvTYYbxqDedrQIe1Qyl1NYxEC7eo1Iaz
8KilmMSt/iLgky4WA/z32Wl44GAq3UwEUX7pABEBAAG0I0FsZXggSG9mYmF1ZXIg
PGFsZXhAZGVyaG9mYmF1ZXIuYXQ+iQE9BBMBCAAnBQJX3dEMAhsDBQkJZgGABQsJ
CAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEGrPIVMXiTpBOa4H/ihUAzQzgbpH1TAH
p2VZ1l5hgFgtYnQ3BvzISatrIG3NsQ9QxHxdZCoNXsIveTOPoEqlex3TQMVOY2uD
LOTYtk9Ul0cSYUU1VgFd9PVVQuThxLct7xTDzTpsp0oYdwlVc3Szg+ggAJE8Bu4p
Ghn9p4pcNgIVZVwpLCcP5S1NODcGpU0MChyTl+UrGmrVpjoHZix88/eewijbTOrs
opjxHIlaIxOMJJ4Fip4jLHkoYOL5rQCaLDgv+ZiAaWz3l8mjGsceG4aUt22ljY1l
vKwyfT5QgR5kJOG/D81yjZZ1AJMYmlnhEHLORtvFhsYKop/fsAK+AG5ArtLqtz+G
OR42dvu0KEFsZXhhbmRlciBIb2ZiYXVlciA8YWxleEBkZXJob2ZiYXVlci5hdD6J
AT0EEwEIACcFAlfd02wCGwMFCQlmAYAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AA
CgkQas8hUxeJOkFOfggAuAAWwXVfwMGkpxV7ujt51SOZf1WV5AJAxMumIeqsvj69
9LHphnpeO6fm0nRmYYoDTLM1gXMVXfRGu/ZFz2S0xTNJZOkiWaHT/+DpNvnFLLoc
5XMnZouNvrMJuu/DFH++BA7t3aXbJ6TE5J2gYF1xtI8UrAygnohQoZ3dtIlBy2Kf
ohZZcRiKYZRdWtl7ERpPJc19Pwe12Kza875YQHCKc2dkl0ya5AbDEHXfCzMGPNxF
RFgGcTTPt4nHsCCEE6kEPuRAfFsptMSCQKB1Z55sccdPdGyoKuy5yHwxU9L89+nj
lvzKd1swBJn2in7nPWWJcUa4AkwxmxzYvleqCMRJg7kBDQRX3dEMAQgAuV7wsfIQ
qjkxUvBIMVwocTmnUerOuwar4WfzJJx/oO49wd3jFto/WcJmJiJ67STM2Vi64iCa
8OzBudpYt7/9FZCFogJE2JUofjLFM/z9USgovOOVF7eeVggKchGRM7SeG3k/tK+s
bobt1F62Lk2WhYFbSDUr/WrtdnFNXSD4tCkpz1dBvd2codvTbCmuhl/vppJaRO8u
m3y9I11YXAL8yfiLYOGLyhBcOohWKxIZhBYEZhKdFUOjMnnYUOZmhRh6g7an/3FW
wVYvper6WMta0LdK8obTNC1koCaBQQIZISNwSF2/CIDiMwxL32txazZI7GqGHIwG
tm69OGqZ0FhraQARAQABiQElBBgBCAAPBQJX3dEMAhsMBQkJZgGAAAoJEGrPIVMX
iTpB0AoIAIUzzhKY53lrQzgQiyOQCkVYYyBY1kRKqSBYgWZbfItNTuYkpYSbB9tU
U7CtDLErS0K5xlrYMjQvDHEiskra4IGCvRDuNMu275VL7+hJHqjnXhoLWSzHXGrG
K4mZVX2QqD010KQZLC1aFHXnPM+boGVId/Lauu4i3RmUpZiLyvaodRaB0yfJm8WV
2sLbVEBBWYOWGGGt6OSuO/O6pyDpU6Grzy61X7donHMzkgfBPaqE13feVs3p+GwE
lfVc5Sr3v71RqctkHl1vLICjn8RAjb701B6ZQGVXH8XuqEZxEYuRdF32SP9G7XHF
6fZSkjd0dNecjp3b9tVRIxTnuEdkHIA=
=nkLe
-----END PGP PUBLIC KEY BLOCK-----


================================================
FILE: shell-volume-mixer@derhofbauer.at/extension.js
================================================
/**
 * Shell Volume Mixer
 *
 * Advanced mixer extension.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported init */

const ExtensionUtils = imports.misc.extensionUtils;
const Lib = ExtensionUtils.getCurrentExtension().imports.lib;

const { Extension } = Lib.main;


function init() {
    ExtensionUtils.initTranslations();

    return new Extension();
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/dbus/dbus.js
================================================
/**
 * Shell Volume Mixer
 *
 * D-Bus command module.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Dbus */

const { DBus, DBusExportedObject } = imports.gi.Gio;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;

const Log = Lib.utils.log;


const DBUS_INTERFACE =
'<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" \
  "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> \
<node name="/at/derhofbauer/shell/VolumeMixer"> \
    <interface name="at.derhofbauer.shell.VolumeMixer"> \
        <method name="debug"> \
            <arg name="what" type="s" direction="in" /> \
            <arg name="result" type="s" direction="out" /> \
        </method> \
        <method name="reload"/> \
        <method name="help"> \
            <arg name="result" type="s" direction="out" /> \
        </method> \
    </interface> \
</node>';


let instance;

/** @typedef {{
 *   reloadExtension: function,
 *   debugCards: {function():String},
 *   debugStreams: {function():String},
 *   debugEvents: {function():String},
 * }} CommandHandler
 */

/**
 * @property {CommandHandler} _handler
 */
var Dbus = class {
    /**
     * @param {CommandHandler} commandHandler
     * @returns {*}
     */
    constructor(commandHandler = {}) {
        if (instance) {
            return instance;
        }

        instance = this;

        this._handler = new Proxy(commandHandler, {
            get: function (target, prop) {
                if (prop in target && target[prop]) {
                    Log.info(`Got command "${prop}"`);

                    return Reflect.get(...arguments);
                }

                Log.info(`Unimplemented command "${prop}"`);
            }
        });
    }


    init() {
        this._dbus = DBusExportedObject.wrapJSObject(DBUS_INTERFACE, this);
        this._dbus.export(DBus.session, '/at/derhofbauer/shell/VolumeMixer');

        Log.info('D-Bus command interface enabled');
    }

    destroy() {
        if (this._dbus) {
            this._dbus.unexport();
            this._dbus = null;
            Log.info('D-Bus command interface disabled');
        }
    }

    //region dbus methods

    reload() {
        this._handler.reload();
    }

    /**
     * @param {string} what to debug
     * @return {string}
     */
    debug(what) {
        let result = '';

        switch (what) {
            case 'cards':
                result = this._handler.debugCards();
                break;

            case 'streams':
                result = this._handler.debugStreams();
                break;

            case 'events':
                result = this._handler.debugEvents();
                break;

            case '':
                Log.error(`D-Bus: command missing`);
                break;

            default:
                Log.error(`D-Bus: unknown command ${what}`);
        }

        return result;
    }

    help() {
        return 'Commands: debug (cards, streams, events), reload';
    }

    //endregion dbus methods
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/main.js
================================================
/**
 * Shell Volume Mixer
 *
 * Main extension setup.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Extension */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;

const { Dbus } = Lib.dbus.dbus;
const { EventBroker } = Lib.utils.eventBroker;
const { Mixer } = Lib.volume.mixer;
const { Indicator } = Lib.menu.indicator;
const { PanelButton } = Lib.widget.panelButton;
const { Settings, SETTING, cleanup: settingsCleanup } = Lib.settings;

const Log = Lib.utils.log;

const DEFAULT_INDICATOR_POS = 4;


let instance;

var Extension = class {
    constructor() {
        if (instance) {
            return instance;
        }

        instance = this;

        // save the original volume reference in aggregate menu.
        this._orgVolume = this._menu._volume;
    }

    /**
     * Settings instance.
     * @private
     */
    get _settings() {
        if (!this._settingsInstance) {
            this._settingsInstance = new Settings();
        }

        return this._settingsInstance;
    }

    /**
     * The shell aggregate menu instance.
     * @private
     */
    get _menu() {
        return Main.panel.statusArea.aggregateMenu;
    }

    enable() {
        this._events = new EventBroker();
        this._events.connect('extension-disable', () => {
            this.disable();
        });
        this._events.connect('extension-enable', () => {
            this.enable();
        });
        this._events.connect('extension-reload', () => {
            this._reloadExtension();
        });

        this._settings.connectChanged(() => {
            this._reloadExtension();
        });

        this._mixer = new Mixer();

        let position = this._settings.get_enum(SETTING.position);

        if (position === SETTING.position_at.menu) {
            this._replaceOriginal();
        } else {
            this._addPanelButton(position);
        }

        if (this._settings.get_boolean(SETTING.debug) === true) {
            this._enableDebugging();
        }
    }

    disable() {
        instance = null;

        this._menu._volume = this._orgVolume;
        this._showOriginal();

        if (this._events) {
            this._events.disconnectAll();
            this._events = null;
        }

        if (this._mixer) {
            this._mixer.destroy();
            this._mixer = null;
        }

        if (this._indicator) {
            this._menu._indicators.remove_actor(this._indicator);
            this._indicator.destroy();
            this._indicator = null;
        }

        if (this._panelButton) {
            this._panelButton.destroy();
            this._panelButton = null;
        }

        if (this._dbus) {
            this._dbus.destroy();
            this._dbus = null;
        }

        this._settingsInstance = null;
        settingsCleanup();
    }

    /**
     * Hides the original menu item and icon.
     * @private
     */
    _hideOriginal() {
        this._orgVolume._volumeMenu.actor.hide();
        this._orgVolume._primaryIndicator.hide();
        this._menu._indicators.remove_child(this._orgVolume);
    }

    /**
     * Restores the original menu item and icon.
     * @private
     */
    _showOriginal() {
        this._menu._indicators.insert_child_at_index(this._orgVolume, this._indicatorPos || DEFAULT_INDICATOR_POS);
        this._orgVolume._volumeMenu.actor.show();
        this._orgVolume._primaryIndicator.show();
    }

    /**
     * Replaces the current indicator and menu.
     * @private
     */
    _replaceOriginal() {
        this._indicator = new Indicator(this._mixer, {
            separator: false,
            showPercentageLabel: this._settings.get_boolean(SETTING.show_percentage_label),
            menuClass: 'svm-integrated-menu',
        });

        // get current indicator position
        this._indicatorPos = this._getCurrentIndicatorPosition();
        this._hideOriginal();

        // add our own indicator and menu
        this._menu._volume = this._indicator;
        this._menu._indicators.insert_child_at_index(this._indicator, this._indicatorPos);
        this._menu.menu.addMenuItem(this._indicator.menu, 0);

        this._menu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(), 1);

        // on disable/enable we won't get a stream-changed event, so trigger it here to be safe
        this._indicator.updateOutputIcon();
    }

    /**
     * Find the current volume icon's position.
     * @private
     */
    _getCurrentIndicatorPosition() {
        let indicators = this._menu._indicators.get_children();
        let indicatorPos = DEFAULT_INDICATOR_POS;

        for (let i = 0; i < indicators.length; i++) {
            if (this._orgVolume === indicators[i]) {
                indicatorPos = i;
                break;
            }
        }

        return indicatorPos;
    }

    /**
     * Inits this extension as stand-alone button.
     * @private
     */
    _addPanelButton(position) {
        if (this._settings.get_boolean(SETTING.remove_original)) {
            this._hideOriginal();
        }

        this._panelButton = new PanelButton(this._mixer, {
            showPercentageLabel: this._settings.get_boolean(SETTING.show_percentage_label),
        });

        if (position === SETTING.position_at.left) {
            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton, 999, 'left');
        } else if (position === SETTING.position_at.center) {
            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton, 999, 'center');
        } else {
            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton);
        }
    }

    /**
     * Reloads the extension be disabling / enabling it.
     * @private
     */
    _reloadExtension() {
        this.disable();
        this.enable();
    }

    /**
     * Enables the debugging and messages.
     *
     * @private
     */
    _enableDebugging() {
        Log.verbose = true;

        this._dbus = new Dbus({
            debugCards: () => {
                return this._emitDebugEvent('debug-cards');
            },

            debugStreams: () => {
                return this._emitDebugEvent('debug-streams');
            },

            debugEvents: () => {
                return this._emitDebugEvent('debug-events');
            },

            reload: () => {
                this._reloadExtension();
            },
        });

        this._dbus.init();
    }

    _emitDebugEvent(eventName) {
        this._events.emit(eventName, result => {
            Log.info(result);
        });

        return 'OK';
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/menu/indicator.js
================================================
/**
 * Shell Volume Mixer
 *
 * Customized indicator using Volume Menu.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Indicator */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const { GObject, Clutter } = imports.gi;
const PanelMenu = imports.ui.panelMenu;
const Volume = imports.ui.status.volume;

const { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;
const { Menu } = Lib.menu.menu;
const { PercentageLabel } = Lib.widget.percentageLabel;
const Utils = Lib.utils.utils;


const VolumeType = {
    OUTPUT: 0,
    INPUT: 1,
};


/**
 * Custom indicator with dropdown menu.
 * Copied from status/volume.js
 *
 * @copypaste from Volume.Indicator: Needs code to not initialize volume menu and all its events.
 */
var Indicator = GObject.registerClass(class Indicator extends PanelMenu.SystemIndicator
{
    /**
     * @param {Mixer} mixer
     * @param {Object} options
     * @private
     */
    _init(mixer, options = {}) {
        super._init();

        this._primaryIndicator = this._addIndicator();

        if (options.showPercentageLabel) {
            this._percentageLabel = new PercentageLabel(mixer);
            this.add_actor(this._percentageLabel);

            this._percentageLabel.reactive = true;
            this.connect(this._percentageLabel, 'scroll-event',
                (actor, event) => this._handleScrollEvent(VolumeType.OUTPUT, event));
            this.connect(this._percentageLabel, 'button-press-event',
                (actor, event) => this._handleButtonPress(VolumeType.OUTPUT, event));
        }

        this._inputIndicator = this._addIndicator();

        this._primaryIndicator.reactive = true;
        this._inputIndicator.reactive = true;

        this.connect(this._primaryIndicator, 'scroll-event',
            (actor, event) => this._handleScrollEvent(VolumeType.OUTPUT, event));
        this.connect(this._inputIndicator, 'scroll-event',
            (actor, event) => this._handleScrollEvent(VolumeType.INPUT, event));

        this.connect(this._primaryIndicator, 'button-press-event',
            (actor, event) => this._handleButtonPress(VolumeType.OUTPUT, event));
        this.connect(this._inputIndicator, 'button-press-event',
            (actor, event) => this._handleButtonPress(VolumeType.INPUT, event));

        this._control = mixer.control;
        this._volumeMenu = new Menu(mixer, options);
        this._volumeMenu.actor.add_style_class_name(options.menuClass);

        this.connect(this._volumeMenu, 'output-icon-changed', this.updateOutputIcon.bind(this));

        this._inputIndicator.visible = this._volumeMenu.getInputVisible();
        this.connect(this._volumeMenu, 'input-visible-changed', () => {
            this._inputIndicator.visible = this._volumeMenu.getInputVisible();
        });
        this.connect(this._volumeMenu, 'input-icon-changed', this.updateInputIcon.bind(this));
        // initial call to get an icon (especially for "show-always" setups)
        this.updateInputIcon();

        this.menu.addMenuItem(this._volumeMenu);
    }

    updateOutputIcon() {
        let icon = this._volumeMenu.getIcon(VolumeType.OUTPUT);

        if (icon) {
            this._primaryIndicator.icon_name = icon;
            this._primaryIndicator.visible = true;
        } else {
            this._primaryIndicator.visible = false;
        }
    }

    updateInputIcon() {
        let icon = this._volumeMenu.getIcon(VolumeType.INPUT);

        if (icon !== null) {
            this._inputIndicator.icon_name = icon;
        }
    }

    _handleScrollEvent(type, event) {
        return Volume.Indicator.prototype._handleScrollEvent.apply(this, [type, event]);
    }

    _handleButtonPress(type, event) {
        if (event.get_button() === 2) {
            if (type === VolumeType.OUTPUT) {
                this._volumeMenu._output.toggleMute();
            } else {
                this._volumeMenu._input.toggleMute();
            }

            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    destroy() {
        this.disconnectAll();

        if (this.menu) {
            this.menu.destroy();
            this.menu = null;
        }

        if (this._percentageLabel) {
            this._percentageLabel.destroy();
            this._percentageLabel = null;
        }
    }
});

Utils.mixin(Indicator, EventHandlerDelegate);


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/menu/menu.js
================================================
/**
 * Shell Volume Mixer
 *
 * Volume menu item implementation.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Menu */

const Gvc = imports.gi.Gvc;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const PopupMenu = imports.ui.popupMenu;
const Volume = imports.ui.status.volume;

const { EventBroker } = Lib.utils.eventBroker;
const { Settings, SETTING } = Lib.settings;
const Log = Lib.utils.log;
const Utils = Lib.utils.utils;
const {
    AggregatedInput,
    EventsSlider,
    InputSlider,
    InputStreamSlider,
    MasterSlider,
    OutputSlider,
} = Lib.widget.volume;


const VolumeType = {
    OUTPUT: 0,
    INPUT: 1,
};


/**
 * Extension of Volume.VolumeMenu without its constructor().
 */
class VolumeMenuExtension extends PopupMenu.PopupMenuSection {}
Utils.mixin(VolumeMenuExtension, Volume.VolumeMenu);


/** @typedef {{
 *   mixer: Mixer,
 *   detailed: Boolean,
 *   symbolicIcons: Boolean,
 *   stream: Gcv.MixerStream,
 * }} sliderOptions
 */


/**
 * Our own ui.status.VolumeMenu implementation.
 */
var Menu = class extends VolumeMenuExtension
{
    /**
     * @param {Mixer} mixer
     * @param {Object} options
     */
    constructor(mixer, options = {}) {
        super();

        this._events = new EventBroker();
        this._settings = new Settings();

        this.options = {
            detailed: this._settings.get_boolean(SETTING.show_detailed_sliders),
            systemSounds: this._settings.get_boolean(SETTING.show_system_sounds),
            virtualStreams: this._settings.get_boolean(SETTING.show_virtual_streams),
            symbolicIcons: this._settings.get_boolean(SETTING.use_symbolic_icons),
            alwaysShowInputStreams: this._settings.get_boolean(SETTING.always_show_input_streams)
        };

        this.actor.add_style_class_name('svm-menu');

        // master-menu items
        this._outputs = {};
        // input-menu items
        this._inputs = {};
        // menu items (all other menu items)
        this._items = {};

        this._mixer = mixer;
        this._control = mixer.control;

        this._init(mixer, options);

        this._events.connect('debug-streams', (event, callback) => {
            this._debugStreams(callback);
        });
    }

    _init(mixer, options) {
        const signals = {
            'state-changed': this._onControlStateChanged,
            'default-sink-changed': this._readOutput,
            'default-source-changed': this._readInput,
            'stream-added': this._streamAdded,
            'stream-removed': this._streamRemoved,
            'stream-changed': this._streamChanged
        };

        for (let name in signals) {
            try {
                mixer.connect(name, signals[name].bind(this));
            } catch (exception) {
                Log.info(`Could not connect to signal ${name} -`, exception);
            }
        }

        this._output = new MasterSlider(this._control, {
            mixer: mixer,
            detailed: this.options.detailed,
            symbolicIcons: this.options.symbolicIcons
        });

        this._output.connect('stream-updated', () => {
            this.emit('output-icon-changed');
        });


        this._inputMenu = new AggregatedInput(this._control, {
            mixer: mixer,
            detailed: this.options.detailed,
            symbolicIcons: this.options.symbolicIcons
        });


        this._input = new InputStreamSlider(this._control, {
            mixer: mixer,
            showAlways: this.options.alwaysShowInputStreams,
            detailed: this.options.detailed,
            symbolicIcons: this.options.symbolicIcons
        });

        this._input.item.connect('notify::visible', () => {
            this.emit('input-visible-changed');
        });
        this._input.connect('stream-updated', () => {
            this.emit('input-icon-changed');
        });


        this.addMenuItem(this._output.item, 0);
        this.addMenuItem(this._inputMenu.item, 2);
        this._inputMenu.setInputStream(this._input);

        if (options.separator) {
            this._addSeparator();
        }

        this._onControlStateChanged();
    }

    open(animate) {
        this._output.hideVolumeInfo();
        super.open(animate);
    }

    close(animate) {
        for (let id in this._outputs) {
            this._outputs[id].hideVolumeInfo();
        }

        for (let id in this._inputs) {
            this._inputs[id].hideVolumeInfo();
        }

        for (let id in this._items) {
            this._items[id].hideVolumeInfo();
        }

        this._output.hideVolumeInfo();

        super.close(animate);
    }

    _addSeparator() {
        if (this._separator) {
            this._separator.destroy();
        }

        this._separator = new PopupMenu.PopupSeparatorMenuItem();
        this.addMenuItem(this._separator, 3);
    }

    _addStream(control, stream) {
        if (stream instanceof Gvc.MixerSource
            || stream instanceof Gvc.MixerSourceOutput
        ) {
            return;
        }

        const isSystemSound = stream instanceof Gvc.MixerEventRole;
        const isInputStream = stream instanceof Gvc.MixerSinkInput;
        const isOutputStream = stream instanceof Gvc.MixerSink;

        if (stream.is_event_stream) {
            Log.info(`Skipping event stream (${stream.id})`);
            return;
        }

        if (stream.is_virtual && !this.options.virtualStreams) {
            Log.info(`Skipping virtual stream (${stream.id})`);
            return;
        }

        if (isSystemSound && !this.options.systemSounds) {
            Log.info(`Skipping system sound stream (${stream.id})`);
            return;
        }

        const options = {
            mixer: this._mixer,
            detailed: this.options.detailed,
            symbolicIcons: this.options.symbolicIcons,
            stream: stream
        };

        if (isSystemSound) {
            this._addSliderStream(stream, control, options);

        } else if (isInputStream) {
            this._addInputStream(stream, control, options);

        } else if (isOutputStream) {
            this._addOutputStream(stream, control, options);

        } else {
            Log.info(`Unhandled stream ${stream.id} (${stream.name} (${stream.constructor.name}))`);
        }
    }

    /**
     * Adds a stream to the multi-input menu.
     *
     * @param stream
     * @param control
     * @param {sliderOptions} options
     * @private
     */
    _addInputStream(stream, control, options) {
        if (stream.id in this._inputs) {
            Log.info(`Not adding already known input stream ${stream.id}:${stream.name}`);
            return;
        }

        Log.info(`Adding input stream ${stream.id}:${stream.name}`);

        let slider = new InputSlider(control, options);

        this._inputs[stream.id] = slider;
        this._inputMenu.addSlider(slider);
    }

    /**
     * Adds a stream to the master slider menu.
     *
     * @param stream
     * @param control
     * @param {sliderOptions} options
     * @private
     */
    _addOutputStream(stream, control, options) {
        if (stream.id in this._outputs) {
            Log.info(`Not adding already known output stream ${stream.id}:${stream.name}`);
            return;
        }

        Log.info(`Adding output stream ${stream.id}:${stream.name}`);

        let slider = new OutputSlider(control, options);

        this._outputs[stream.id] = slider;
        this._output.addOutputSlider(slider);
    }

    /**
     * Adds an additional slider below master and input slider.
     *
     * @param stream
     * @param control
     * @param {sliderOptions} options
     * @private
     */
    _addSliderStream(stream, control, options) {
        if (stream.id in this._items) {
            Log.info(`Not adding already known stream ${stream.id}:${stream.name}`);
            return;
        }

        Log.info(`Adding stream ${stream.id}:${stream.name}`);

        let slider = new EventsSlider(control, options);

        this._items[stream.id] = slider;
        this.addMenuItem(slider.item, 1);
    }

    _streamAdded(control, id) {
        let stream = control.lookup_stream_id(id);
        this._addStream(control, stream);
    }

    _streamRemoved(control, id) {
        if (id in this._items) {
            this._items[id].item.destroy();
            delete this._items[id];

        } else if (id in this._outputs) {
            this._outputs[id].item.destroy();
            delete this._outputs[id];

        } else if (id in this._inputs) {
            this._inputs[id].item.destroy();
            delete this._inputs[id];
            this._inputMenu.refresh();
        }
    }

    _streamChanged(control, id) {
        if (id in this._items) {
            this._items[id].refresh();

        } else if (id in this._outputs) {
            this._outputs[id].refresh();

        } else if (id in this._inputs) {
            this._inputs[id].refresh();
        }
    }

    _onControlStateChanged() {
        super._onControlStateChanged();

        if (this._control.get_state() !== Gvc.MixerControlState.READY) {
            return;
        }

        let streams = this._control.get_streams();
        for (let stream of streams) {
            this._addStream(this._control, stream);
        }
    }

    _readInput() {
        if (!this._input)  {
            return;
        }

        super._readInput();
    }

    _debugStreams(callback) {
        let dump = [];

        if (Object.keys(this._outputs).length) {
            dump.push('Output Streams:');
        }

        for (let id in this._outputs) {
            const stream = this._outputs[id].stream;
            dump.push(`  ${stream.id} (${stream.name}) (port=${stream.port}) ${stream.description}`);

            const ports = stream.get_ports();
            for (let p in ports) {
                const port = ports[p];
                dump.push(`    ${port.port}`);
            }
        }

        if (Object.keys(this._inputs).length) {
            dump.push('Input Streams:');
        }

        for (let id in this._inputs) {
            const stream = this._inputs[id].stream;
            dump.push(`  ${stream.id} (name=${stream.name}) (port=${stream.port}) ${stream.description}`);
        }

        callback(dump.join('\n'));
    }

    getInputVisible() {
        return this._input.isVisible();
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/settings.js
================================================
/**
 * Shell Volume Mixer
 *
 * Convenience class to wrap Gio.Settings.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Settings, cleanup */
/* exported SOUND_SETTINGS_SCHEMA, ALLOW_AMPLIFIED_VOLUME_KEY, SETTING */

const ExtensionUtils = imports.misc.extensionUtils;
const Extension = ExtensionUtils.getCurrentExtension();
const { Gio, GLib } = imports.gi;
const Lib = Extension.imports.lib;

const Log = Lib.utils.log;
const Utils = Lib.utils.utils;


const SETTINGS_SCHEMA = 'org.gnome.shell.extensions.shell-volume-mixer';
var SOUND_SETTINGS_SCHEMA = 'org.gnome.desktop.sound';
var ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent';

var SETTING = Object.freeze({
    position:                  'position',
    remove_original:           'remove-original',
    show_percentage_label:     'show-percentage-label',
    show_detailed_sliders:     'show-detailed-sliders',
    show_system_sounds:        'show-system-sounds',
    show_virtual_streams:      'show-virtual-streams',
    always_show_input_streams: 'always-show-input-streams',
    use_symbolic_icons:        'use-symbolic-icons',
    profile_switcher_hotkey:   'profile-switcher-hotkey',
    pinned_profiles:           'pinned-profiles',
    debug:                     'debug',

    position_at: {
        menu:   0,
        left:   1,
        center: 2,
        right:  3,
    },
});


const SIGNALS = {};
const GSETTINGS = {};



var Settings = class
{
    get settings() {
        if (!GSETTINGS[this.schema]) {
            GSETTINGS[this.schema] = ExtensionUtils.getSettings(this.schema);
        }

        return GSETTINGS[this.schema];
    }

    constructor(schema) {
        this.schema = schema || SETTINGS_SCHEMA;

        if (!SIGNALS[this.schema]) {
            SIGNALS[this.schema] = {};
        }

        this._signals = SIGNALS[this.schema];
    }

    /**
     * Registers a listener for a signal.
     *
     * @param {string} signal
     * @param {function()} callback
     * @param {boolean} allowMultiple Whether to error out if the setting has already been connected
     */
    connect(signal, callback, allowMultiple = false) {
        if (!allowMultiple && this._signals[signal]) {
            Log.error('Settings', 'connect', `Signal "${signal}" already bound for "${this.schema}"`);
            return false;
        }

        Log.info(`Connecting to settings change signal "${signal}"`);
        let id = this.settings.connect(signal, callback);
        this._signals[signal] = id;

        return id;
    }

    /**
     * Registers a listener to changed events.
     *
     * @param {function()} callback
     */
    connectChanged(callback) {
        this.connect('changed', callback);
    }

    /**
     * Disconnects all connected signals.
     */
    disconnectAll() {
        for (let signal in this._signals) {
            this.disconnect(this._signals[signal]);
        }
    }

    /**
     * Disconnects a signal by name.
     */
    disconnect(signal) {
        if (this._signals[signal]) {
            this.settings.disconnect(this._signals[signal]);
            delete this._signals[signal];
            return true;
        }

        return false;
    }

    /**
     * Disconnects a signal by id.
     */
    disconnectById(signalId) {
        for (let name in this._signals) {
            let id = this._signals[name];
            if (signalId === id) {
                this.settings.disconnect(id);
                delete this._signals[name];
                return true;
            }
        }

        return false;
    }


    /**
     * Retrieves the value of an 's' type key.
     */
    get_string(key) {
        return this.settings.get_string(key);
    }

    /**
     * Sets the value of an 's' type key.
     */
    set_string(key, value) {
        return this.settings.set_string(key, value);
    }

    /**
     * Retrieves the value of an 'i' type key.
     */
    get_int(key) {
        return this.settings.get_int(key);
    }

    /**
     * Sets the value of an 'i' type key.
     */
    set_int(key, value) {
        return this.settings.set_int(key, value);
    }

    /**
     * Retrieves the value of a 'b' type key.
     */
    get_boolean(key) {
        return this.settings.get_boolean(key);
    }

    /**
     * Sets the value of a 'b' type key.
     */
    set_boolean(key, value) {
        return this.settings.set_boolean(key, value);
    }

    /**
     * Retrieves the value of an enum key.
     */
    get_enum(key) {
        return this.settings.get_enum(key);
    }

    /**
     * Sets the value of an enum key.
     */
    set_enum(key, value) {
        return this.settings.set_enum(key, value);
    }

    /**
     * Retrieves the value of an array key.
     */
    get_array(key) {
        return this.settings.get_strv(key);
    }

    /**
     * Sets the value of an array key.
     */
    set_array(key, value) {
        return this.settings.set_strv(key, value);
    }
};

/**
 * Disconnects all signals of all schemas.
 * Used to make sure all there are no connected signals left.
 */
function cleanup() {
    for (let schema in SIGNALS) {
        if (!GSETTINGS[schema]) {
            continue;
        }

        for (let signal in SIGNALS[schema]) {
            GSETTINGS[schema].disconnect(SIGNALS[schema][signal]);
            delete SIGNALS[schema][signal];
        }
    }
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js
================================================
/**
 * Shell Volume Mixer
 *
 * PulseAudio card retrieval utilities.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Cards, STREAM_MATCHING */

const ExtensionUtils = imports.misc.extensionUtils;
const Lib = ExtensionUtils.getCurrentExtension().imports.lib;
const Main = imports.ui.main;
const { Gvc, GLib } = imports.gi;
const __ = ExtensionUtils.gettext;

const { EventBroker } = Lib.utils.eventBroker;
const { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;
const Log = Lib.utils.log;
const Utils = Lib.utils.utils;
const PaHelper = Lib.utils.paHelper;


var STREAM_MATCHING = Object.freeze({
    stream: 1,
    card:   2,
});

const NULL_CARD = 4294967295;

/** @typedef {{
 *   name: String,
 *   description: String,
 *   available: Boolean,
 * }} paProfile
 */

/** @typedef {{
 *   name: String,
 *   description: String,
 *   available: Boolean,
 *   direction: String,
 * }} paPort
 */

/** @typedef {{
 *   index: Number,
 *   alsaCard: Number,
 *   name: String,
 *   description: String,
 *   active_profile: String,
 *   profiles: Object.<string, paProfile>,
 *   ports: Object.<string, paPort>,
 *   fake: Boolean,
 *   card: Object<Gvc.MixerCard>,
 * }} paCard
 */

/**
 * @property {Object.<string, paCard>} _paCards
 * @mixes EventHandlerDelegate
 */
var Cards = class {
    /**
     * @param {Gvc.MixerControl} control
     */
    constructor(control) {
        this._events = new EventBroker();
        this._control = control;
        this.eventHandlerDelegate = control;

        this._initDone = new Promise(resolve => {
            this._initialized = resolve;
        });

        this.connect('state-changed', this._onStateChanged.bind(this), () => {
            return [this._control, this._control.get_state()];
        });

        this._events.connect('debug-cards', (event, callback) => {
            callback(Log.dump(this._paCards));
        });
    }

    /**
     * @returns {boolean}
     * @private
     */
    _controlIsReady() {
        return (this._control && this._control.get_state() === Gvc.MixerControlState.READY);
    }

    /**
     * Callback for state changes.
     */
    _onStateChanged(/* control, state */) {
        if (!this._controlIsReady()) {
            return;
        }

        // noinspection JSIgnoredPromiseFromCall
        this._init();
    }

    /**
     * @returns {Promise<void>}
     * @private
     */
    async _init() {
        try {
            await this._initCards();
            this._initialized();

        } catch (e) {
            Log.error('Cards', '_init', e);
            Main.notifyError('Volume Mixer', __('Querying PulseAudio sound cards failed, disabling extension'));
            this._events.emit('extension-disable');

            return;
        }

        this.connect('card-added', this._onCardAdded.bind(this));
        this.connect('card-removed', this._onCardRemoved.bind(this));
    }

    /**
     * Retrieves a list of all cards available, using our Python helper.
     * Tries to be error-resistant in case the helper cannot deliver.
     *
     * @returns {Promise<void>}
     */
    async _initCards() {
        this._paCards = {};
        this._cardNames = {};

        let cards;

        let retries = 3;
        do {
            cards = await this._getCardDetails();
        } while (!cards
            && (--retries) > 0
            && await new Promise(resolve => GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => resolve(true)))
        );

        if (!cards) {
            throw Error('Could not retrieve PA card details with Python helper script');
        }

        this._paCards = cards;
    }

    /**
     * @returns {?Object.<string, paCard>}
     * @private
     */
    async _getCardDetails() {
        const paCards = await PaHelper.getCards();

        if (!paCards || !Object.keys(paCards).length) {
            return null;
        }

        if (this._controlIsReady()) {
            for (let card of this._control.get_cards()) {
                if (card.index in paCards) {
                    this._addGvcCard(paCards[card.index], card);
                }
            }
        }

        return paCards;
    }

    /**
     * @param {paCard} paCard
     * @param {Gvc.MixerCard} card
     * @private
     */
    _addGvcCard(paCard, card) {
        if (!paCard) {
            Log.error('Cards', '_addGvcCard', 'No paCard passed');
            return;
        }

        if (!paCard.name) {
            Log.error('Cards', '_addGvcCard', 'Invalid paCard data, name missing');
            return;
        }

        Log.info('Card added', card.index, paCard.name);

        paCard.card = card;
        this._cardNames[paCard.name] = card.index;
    }

    /**
     * Signal for added cards.
     */
    async _onCardAdded(control, index) {
        // we're actually looking up card.index
        let card = control.lookup_card_id(index);
        let paCard = await this.get(index);

        if (!paCard || paCard.fake) {
            try {
                paCard = await PaHelper.getCardByIndex(index);
            } catch (e) {
                Log.error('Cards', '_onCardAdded', 'Calling Python helper failed');
            }

            if (!paCard) {
                Log.error('Cards', '_onCardAdded', 'GVC card not found through Python helper');

                // external script couldn't get card info, fake it
                paCard = {
                    // card name (human name) won't be useful, we'll set it anyway
                    name:     card.name,
                    index:    index,
                    profiles: [],
                    fake:     true
                };
            }

            this._paCards[index] = paCard;
        }

        this._addGvcCard(paCard, card);
    }

    /**
     * Signal for removed cards.
     */
    _onCardRemoved(control, index) {
        if (index in this._paCards) {
            const name = this._paCards[index].name;
            delete this._cardNames[name];
            delete this._paCards[index];
            Log.info('Card removed', index, name);

        } else {
            Log.info('Untracked card not removed', index);
        }
    }


    /**
     * Finds a card by card index.
     *
     * @param {number} index
     * @param {Boolean} forceRefresh
     * @returns {Promise<?paCard>}
     */
    async get(index, forceRefresh = false) {
        if (index === NULL_CARD) {
            return null;
        }

        await this._initDone;

        if (!forceRefresh) {
            if (index in this._paCards) {
                return this._paCards[index];
            }

            Log.info(`Card ${index} not found, querying...`);
        }

        const paCard = await PaHelper.getCardByIndex(index);

        if (!paCard) {
            return null;
        }

        if (this._controlIsReady()) {
            let card = this._control.lookup_card_id(paCard.index);
            this._addGvcCard(paCard, card);
        }

        return paCard;
    }

    /**
     * Finds a card by name.
     *
     * @param {string} name
     * @returns {Promise<?paCard>}
     */
    async getByName(name) {
        await this._initDone;

        const index = this._cardNames[name];
        if (!isNaN(index) && index >= 0) {
            return this.get(index);
        }

        return null;
    }

    /**
     * Tries to find out whether a certain stream matches profile for a card.
     *
     * @param {Gvc.MixerStream} stream
     * @param {paCard} paCard
     * @param {string} profileName
     * @returns {STREAM_MATCHING}
     */
    streamMatchesPaCard(stream, paCard, profileName) {
        const streamName = stream.name;
        const cardName = paCard.name;

        let [, streamAddr, streamIndex, streamProfile] = streamName.split('.');
        const [, cardAddr, cardIndex] = cardName.split('.');

        // try to fix stream names without index (cardName will not have an index either)
        if (streamIndex && !streamProfile && isNaN(streamIndex)) {
            streamProfile = streamIndex;
            streamIndex = undefined;
        }

        const profileParts = profileName.split(':');
        // remove direction
        profileParts.shift();
        const profile = profileParts.join(':');

        if (streamAddr !== cardAddr
            || streamIndex !== cardIndex
        ) {
            // cards don't match, certainly no hit
            return false;
        }

        if (streamProfile !== profile) {
            return STREAM_MATCHING.card;
        }

        return STREAM_MATCHING.stream;
    }

    /**
     * Cleanup.
     */
    destroy() {
        this.disconnectAll();
    }
};

Utils.mixin(Cards, EventHandlerDelegate);


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/eventBroker.js
================================================
/**
 * Shell Volume Mixer
 *
 * Global event broker singleton.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported EventBroker */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Signals = imports.signals;

const Log = Lib.utils.log;

let instance;

var EventBroker = class {
    constructor() {
        if (instance) {
            return instance;
        }

        instance = this;

        this.connect('debug-events', (event, callback) => {
            callback(Log.dump(this._signalConnections));
        });
    }
};

Signals.addSignalMethods(EventBroker.prototype);


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/eventHandlerDelegate.js
================================================
/**
 * Shell Volume Mixer
 *
 * Event handler mixin.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported EventHandlerDelegate */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Log = Lib.utils.log;


/**
 * @mixin
 */
var EventHandlerDelegate = class {
    /**
     * @returns {string[]}
     * @private
     */
    get _signals() {
        if (!this.__signals) {
            this.__signals = [];
        }

        return this.__signals;
    }

    /**
     * @param {Object} delegate
     */
    set eventHandlerDelegate(delegate) {
        this.__delegate = delegate;
    }

    get eventHandlerDelegate() {
        return ('__delegate' in this) ? this.__delegate : null;
    }

    /**
     * Connects to a signal.
     *
     * @param {Object} target
     * @param {string} signal
     * @param {function()} [callback]
     * @param {?function()} [initial] Function returning the initial value
     */
    connect(target, signal, callback, initial) {
        if (typeof target === 'string') {
            initial = callback;
            callback = signal;
            signal = target;
            target = this.eventHandlerDelegate;
        }

        if (!target) {
            Log.error('EventHandlerDelegate', 'connect', `Connect called before setting "eventHandlerDelegate" and without passing a target (${signal})`);
            return;
        }

        let id = target.connect(signal, callback);
        this._signals.push([target, id, signal]);

        if (typeof initial === 'function') {
            callback.apply(callback, initial());
        }
    }

    /**
     * Connects to a signal.
     *
     * @param {Object} target
     * @param {string} signal
     */
    disconnect(target, signal) {
        if (typeof target === 'string') {
            signal = target;
        }

        for (let index in this._signals) {
            const [target, id, name] = this._signals[index];
            if (target === target && signal === name) {
                target.disconnect(id);
                this._signals.splice(index, 1);
                return;
            }
        }

        Log.error('EventHandlerDelegate', 'disconnect', `Signal ${signal} not found on target`);
    }

    /**
     * Disconnects all locally used signals.
     */
    disconnectAll() {
        Log.info(`Disconnecting ${this._signals.length} signal(s)`);

        while (this._signals.length > 0) {
            const [target, id] = this._signals.pop();
            Log.info(`Disconnecting signal ${id} from ${target.constructor.name}`);

            target.disconnect(id);
        }
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/hotkeys.js
================================================
/**
 * Shell Volume Mixer
 *
 * Hotkeys.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Hotkeys  */

const { Meta, Shell } = imports.gi;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Main = imports.ui.main;

const Log = Lib.utils.log;

const BINDINGS = {};

var Hotkeys = class
{
    constructor(settings) {
        this._settings = settings;
        this._bindings = BINDINGS;
    }

    /**
     * Binds a hotkey using the local settings instance.
     *
     * @param {string} setting Settings key
     * @param {function()} callback
     */
    bind(setting, callback) {
        if (this._bindings[setting]) {
            Log.info(`Not binding hotkey for ${setting}, already bound`);
            return false;
        }

        const mode = Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW;
        const flags = Meta.KeyBindingFlags.IGNORE_AUTOREPEAT;
        const action = Main.wm.addKeybinding(setting, this._settings.settings, flags, mode, callback);

        if (action === Meta.KeyBindingAction.NONE) {
            Log.info(`Could not bind hotkey for ${setting}`);
        } else {
            Log.info(`Bound hotkey for ${setting}`);
            this._bindings[setting] = action;
        }
    }

    /**
     * Unbinds a hotkey.
     *
     * @param {string} setting Settings key
     */
    unbind(setting) {
        if (this._bindings[setting]) {
            Main.wm.removeKeybinding(setting);
            delete this._bindings[setting];
            Log.info(`Unbound hotkey for ${setting}`);
        }
    }

    /**
     * Unbinds all hotkeys.
     */
    unbindAll() {
        Log.info('Unbinding all hotkeys');
        for (let setting in this._bindings) {
            this.unbind(setting);
        }
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/log.js
================================================
/**
 * Shell Volume Mixer
 *
 * Logging.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported l, d, info, error, dump, verbose */

const Extension = imports.misc.extensionUtils.getCurrentExtension();
const Lib = Extension.imports.lib;

const LOG_PREAMBLE = Extension.metadata.uuid || 'Shell Volume Mixer';


var verbose = false;


/**
 * Helper to debug any variables(s) as string, using separators.
 *
 * @param {...string}
 */
function l() {
    log(Array.prototype.slice.call(arguments).join(' '));
}

/**
 * Logs verbose messages, if enabled.
 *
 * @param {...string}
 */
function info() {
    if (verbose) {
        log(`${LOG_PREAMBLE} | ${Array.prototype.slice.call(arguments).join(' ')}`);
    }
}

/**
 * Helper to debug any variable with pretty output.
 *
 * @param {*} object
 * @param {number} maxDepth
 */
function d(object, maxDepth = 1) {
    try {
        l(_dumpObject(object, maxDepth));
    } catch (e) {
        l(object);
    }
}

/**
 * Logs an error message.
 *
 * @param {string} module Module the error occurred in.
 * @param {string} context Optional.
 * @param {string|Error} error Error or error message (to construct Error from).
 */
function error(module, context, error) {
    if (module && !context && !error) {
        error = module;
        module = undefined;
        context = undefined;
    }
    if (context && !error) {
        error = context;
        context = undefined;
    }

    let output = LOG_PREAMBLE;
    if (module) {
        output += ` | ${module}.js`;
    }
    if (context) {
        output += ` | ${context}()`;
    }

    if (!(error instanceof Error)) {
        error = Error(error);
    }

    logError(error, output);
}

/**
 * Helper to dump any variable to a string.
 *
 * @param {*} object
 * @param {number} maxDepth
 */
function dump(object, maxDepth) {
    try {
        return _dumpObject(object, maxDepth);
    } catch (e) {
        return `Error dumping object: ${e}`;
    }
}


/**
 * Dumps any variable into a string that can be output through log().
 *
 * @param {*} object
 * @param {number} maxDepth
 * @param {number} currDepth
 * @returns {string}
 */
function _dumpObject(object, maxDepth = 8, currDepth = 0) {
    if (currDepth > maxDepth) {
        return '';
    }

    if (object === null) {
        return 'null';
    }

    if (object === undefined) {
        return 'undefined';
    }

    let dump = '';
    let indent = '';
    let stringMode = false;


    if (currDepth > 0) {
        indent = '\u00A0'.repeat(currDepth * 4);

    } else {
        let objectString = object.toString();
        dump += `${objectString}\n${'-'.repeat(objectString.length)}\n\n`;

        if (typeof object == 'string') {
            stringMode = true;
        }
    }

    let isFirst = true;

    for (let key in object) {
        const item = object[key];
        const type = typeof item;
        let typeInfo = type;

        if (stringMode) {
            let pos = parseInt(key);
            if (isNaN(pos)) {
                // don't debug string methods
                break;
            }
            typeInfo = object.charCodeAt(pos);
        }

        if (!isFirst) {
            dump += ',\n';
        }
        isFirst = false;

        dump += `${indent + key} => (${typeInfo})`;

        if (item === null) {
            dump += ' null';

        } else if (type ==='object' || type === 'function') {
            const isArray = Array.isArray(item);

            if (isArray) {
                dump += ' [';
            } else if (type === 'function') {
                dump += ' (';
            } else {
                // we're assuming toString() yields sane values
                let itemString = item.toString();
                if (itemString !== '[object Object]') {
                    dump += ` ${itemString}`;
                }
                dump += ' {';
            }

            let objDump = '';
            try {
                objDump = _dumpObject(item, maxDepth, currDepth + 1);
            } catch (e) {
                // object cannot be dumped, probably a non-null pointer
            }

            if (objDump.trim() !== '') {
                dump += `\n${objDump}\n${indent}`;
            }

            if (isArray) {
                dump += ']';
            } else if (type === 'function') {
                dump += ')';
            } else {
                dump += '}';
            }

        } else {
            dump += ` ${item}`;
        }
    }

    return dump;
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/paHelper.js
================================================
/**
 * Shell Volume Mixer
 *
 * PulseAudio helper.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported getCards, getCardByIndex */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Log = Lib.utils.log;
const Process = Lib.utils.process;
const Utils = Lib.utils.utils;

const PYTHON_HELPER_PATH = 'pautils/query.py';
const TYPE_CARDS = 'cards';

let PYTHON;

async function findPython() {
    if (PYTHON === undefined) {
        for (let python of ['python3', 'python']) {
            let ret;
            let stderr;

            try {
                [ret, , stderr] = await Process.execAsync(['/usr/bin/env', python]);

                if (ret === 0) {
                    PYTHON = python;
                    break;
                }

                Log.error('paHelper', 'findPython', `${python} not found: ${stderr} (${ret})`);

            } catch (e) {
                PYTHON = false;
                Log.error('paHelper', 'findPython', e);
            }
        }
    }

    return PYTHON;
}

/**
 * @param {string} type Type of data to query
 * @param {?number} [index=undefined]
 * @returns {Promise<?Object.<string, paCard>>} JSON object of the output
 */
async function execHelper(type, index = undefined) {
    const paUtilPath = Utils.getExtensionPath(PYTHON_HELPER_PATH);

    if (!paUtilPath) {
        Log.error('paHelper', 'execHelper', `Could not find PulseAudio utility in extension path ${PYTHON_HELPER_PATH}`);
        return null;
    }

    const python = await findPython();

    if (!python) {
        return null;
    }

    const args = ['/usr/bin/env', python, paUtilPath, type];

    if (!isNaN(index)) {
        args.push(index);
    }


    let ret;
    let stdout;
    let stderr;
    let pythonError;

    try {
        [ret, stdout, stderr] = await Process.execAsync(args);
    } catch (e) {
        pythonError = e;
    }

    if (pythonError) {
        Log.error('paHelper', 'execHelper', pythonError);
        if (stderr) {
            Log.error('paHelper', 'execHelper', `(${ret}) ${stderr}`);
        }
    }

    if (!stdout) {
        return null;
    }

    let data = null;
    try {
        data = JSON.parse(stdout);
    } catch (e) {
        Log.error('paHelper', 'execHelper', e);
        return null;
    }

    if (!data || typeof data !== 'object') {
        Log.error('paHelper', 'execHelper', 'Invalid response');
        return null;
    }

    if ('success' in data && data.success === false) {
        Log.error('paHelper', 'execHelper', `Error: ${data.error}`);
        return null;
    }

    return data;
}

/**
 * Calls the Python helper script to get details about all available cards and their profiles.
 *
 * @returns {Promise<?Object.<string, paCard>>} JSON object of the output
 */
async function getCards() {
    return await execHelper(TYPE_CARDS);
}

/**
 * Calls the Python helper script to get more details about a card and its profiles.
 *
 * @param {number} index
 * @returns {Promise<?paCard>} JSON object of the output
 */
async function getCardByIndex(index) {
    const data = await execHelper(TYPE_CARDS, index);

    if (data && data[index]) {
        return data[index];
    }

    return null;
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/process.js
================================================
/**
 * Shell Volume Mixer
 *
 * Process utility.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported execAsync */

const { Gio } = imports.gi;

/**
 * Executes an async command.
 *
 * @param {Array} command
 * @returns {Promise<[int, string, string]>|Promise<Error>}
 */
async function execAsync(command) {
    const process = new Gio.Subprocess({
        argv:  command.map(arg => arg.toString()),
        flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
    });

    process.init(null);

    return new Promise((resolve, reject) => {
        process.communicate_utf8_async(null, null, (process, result) => {
            try {
                const [success, stdout, stderr] = process.communicate_utf8_finish(result);
                const ret = process.get_exit_status();

                if (!success) {
                    reject(Error('Error spawning subprocess'));
                } else {
                    resolve([ret, stdout, stderr]);
                }

            } catch (e) {
                reject(e);
            }
        });
    });
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/string.js
================================================
/**
 * Shell Volume Mixer
 *
 * String utilities.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported repeat, versionGreaterOrEqual */


/**
 * Parses a version string and returns an array.
 *
 * @param {string} string
 * @returns {number[]}
 */
function parseVersionString(string) {
    let version = string.split('.', 3);

    for (let i = 0; i < 3; i++) {
        if (version[i]) {
            version[i] = parseInt(version[i]);
        } else {
            version[i] = 0;
        }
    }

    return version;
}

/**
 * Returns true if the current shell version is greater than the version string passed.
 *
 * @param {string} string Version string like 3.38.
 */
function versionGreaterOrEqual(string) {
    let current = parseVersionString(imports.misc.config.PACKAGE_VERSION);
    let version = parseVersionString(string);

    for (let i = 0; i < 3; i++) {
        if (current[i] < version[i]) {
            return false;
        }
    }

    return true;
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/utils/utils.js
================================================
/**
 * Shell Volume Mixer
 *
 * Utilities.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported getCards, getExtensionPath, mixin */

const Extension = imports.misc.extensionUtils.getCurrentExtension();


/**
 * Returns this extension's path, optionally querying a subdirectory or file.
 *
 * @param {string} [subpath] Subdirectory or -path to get.
 */
function getExtensionPath(subpath) {
    let dir = Extension.dir;

    if (subpath) {
        dir = dir.get_child(subpath);
    }

    if (!dir.query_exists(null)) {
        return null;
    }

    return dir.get_path();
}


/**
 * Allows a target object to receive all properties from a source.
 */
function mixin(target, source, keepExisting = false) {
    const sourceProps = Object.getOwnPropertyDescriptors(source.prototype);

    for (let name of Object.keys(sourceProps)) {
        if (keepExisting === true && Object.prototype.hasOwnProperty.call(target, name)) {
            continue;
        }

        Object.defineProperty(target.prototype, name, sourceProps[name]);
    }
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/volume/mixer.js
================================================
/**
 * Shell Volume Mixer
 *
 * Mixer class wrapping the mixer control.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Mixer */

const {Gio, Gvc} = imports.gi;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const Main = imports.ui.main;
const Volume = imports.ui.status.volume;

const { Cards, STREAM_MATCHING } = Lib.utils.cards;
const { EventBroker } = Lib.utils.eventBroker;
const { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;
const { Hotkeys } = Lib.utils.hotkeys;
const { Profiles } = Lib.volume.profiles;
const { Settings, SETTING } = Lib.settings;
const Log = Lib.utils.log;
const Utils = Lib.utils.utils;


/**
 * @mixes EventHandlerDelegate
 */
var Mixer = class
{
    constructor() {
        this._events = new EventBroker();
        this._control = Volume.getMixerControl();
        this.eventHandlerDelegate = this._control;

        this._settings = new Settings();
        this._hotkeys = new Hotkeys(this._settings);
        this._profiles = new Profiles(this._settings);
        this._cards = new Cards(this._control);

        this._state = null;
        this._defaultSink = null;
        this._pauseDefaultSinkEvent = false;

        this.connect(this._control, 'state-changed', this._onStateChanged.bind(this), () => {
            return [this._control, this._control.get_state()];
        });

        this.connect(this._control, 'default-sink-changed', this._onDefaultSinkChanged.bind(this));

        this._bindProfileHotkey();
    }

    /**
     * The current Gvc.MixerControl.
     *
     * @returns {Gvc.MixerControl}
     */
    get control() {
        return this._control;
    }

    /**
     * @returns {Cards}
     */
    get cards() {
        return this._cards;
    }

    /**
     * @returns {?Gvc.MixerSink}
     */
    get defaultSink() {
        return this._defaultSink;
    }

    /**
     * Returns the current volume in percent.
     *
     * @returns {?number}
     */
    getVolume() {
        if (!this._defaultSink) {
            return null;
        }

        if (this._defaultSink.is_muted) {
            return 0;
        }

        return Math.round(this._defaultSink.volume / this._control.get_vol_max_norm() * 100);
    }

    /**
     * Cleanup.
     */
    destroy() {
        this.disconnectAll();
        this._hotkeys.unbindAll();
        this._cards.destroy();
    }

    /**
     * Binds the hotkey for profile rotation.
     * @private
     */
    _bindProfileHotkey() {
        if (!this._profiles.count()) {
            Log.info('No profiles found, not enabling profile switching hotkey');
            return;
        }

        Log.info('Profiles found, enabling profile switching hotkey');
        this._hotkeys.bind(SETTING.profile_switcher_hotkey, this._switchProfile.bind(this));
    }

    /**
     * Disconnects volume update signals from the default sink.
     * @private
     */
    _disconnectSink() {
        this.disconnect(this._defaultSink, 'notify::is-muted');
        this.disconnect(this._defaultSink, 'notify::volume');
    }

    /**
     * Connects volume update signals from the default sink for notifications.
     * @private
     */
    _connectSink() {
        this.connect(this._defaultSink, 'notify::is-muted', this._onVolumeUpdate.bind(this));
        this.connect(this._defaultSink, 'notify::volume', this._onVolumeUpdate.bind(this));
    }

    /**
     * Emits a stream update event if volume changes.
     * @private
     */
    _onVolumeUpdate() {
        const percent = this.getVolume();

        if (percent !== null) {
            this._events.emit('volume-changed', percent);
        }
    }

    /**
     * Updates the default sink, trying to mark the currently active card.
     * @private
     */
    async _updateDefaultSink(stream) {
        if (this._defaultSink !== stream) {
            if (this._defaultSink) {
                this._disconnectSink();
            }

            this._defaultSink = stream;

            if (stream) {
                Log.info(`Updating default sink ${stream.id}:${stream.name}`);

                this._connectSink();
                this._onVolumeUpdate();

            } else {
                Log.info('Default sink updated to null, cannot update');
            }
        }

        // we might get a sink id without being able to lookup
        // ...or local code causing the event triggers another update
        if (stream && !this._pauseDefaultSinkEvent) {
            try {
                const paCard = await this._cards.get(stream.card_index);

                if (!paCard || !paCard.card) {
                    Log.info(`Default sink updated but PA/GVC card not found (${stream.card_index}/${stream.name})`);
                } else {
                    this._profiles.setCurrent(paCard);
                }

            } catch (e) {
                Log.error('Mixer', '_updateDefaultSink', e);
            }
        }

        this._pauseDefaultSinkEvent = false;
        this._events.emit('default-sink-updated', stream);
    }

    /**
     * Callback for state changes.
     * @private
     */
    _onStateChanged(control, state) {
        this._state = state;

        if (state !== Gvc.MixerControlState.READY) {
            return;
        }

        // noinspection JSIgnoredPromiseFromCall
        this._updateDefaultSink(this._control.get_default_sink());
    }

    /**
     * Callback for default sink changes.
     * @private
     */
    _onDefaultSinkChanged(control, id) {
        // noinspection JSIgnoredPromiseFromCall
        this._updateDefaultSink(control.lookup_stream_id(id));
    }


    /**
     * Hotkey handler to switch between profiles.
     * @private
     */
    async _switchProfile() {
        if (this._state !== Gvc.MixerControlState.READY) {
            return;
        }

        const next = this._profiles.next();

        if (!next) {
            return;
        }

        let paCard;
        try {
            // lookup card indirectly via name (indexes aren't UUIDs)
            paCard = await this._cards.getByName(next.card);
        } catch (e) {
            Log.error('Mixer', '_switchProfile', e);
        }

        if (!paCard || !paCard.card) {
            // we don't know this card, we won't be able to set its profile
            return;
        }

        // profile's changed, now get that new sink
        const sinks = this._control.get_sinks();
        let newSink = null;

        for (let sink of sinks) {
            let result = this._cards.streamMatchesPaCard(sink, paCard, next.profile);

            if (result === STREAM_MATCHING.stream) {
                newSink = sink;
                break;
            }

            if (result === STREAM_MATCHING.card || !newSink) {
                // maybe we can use this stream later, but we'll keep searching
                newSink = sink;
            }
        }

        paCard.card.set_profile(next.profile);

        if (newSink) {
            this._pauseDefaultSinkEvent = true;
            this._control.set_default_sink(newSink);
        }

        const paProfile = paCard.profiles[next.profile];
        this._showNotification(`${paCard.description || paCard.name}\n${paProfile.description || paProfile.name}`);
    }

    /**
     * Shows a notification window through Shell's OSD Window Manager.
     *
     * @param {string} text Text to display
     * @private
     */
    _showNotification(text) {
        let monitor = -1;
        let icon = Gio.Icon.new_for_string('audio-speakers-symbolic');
        Main.osdWindowManager.show(monitor, icon, text);
    }
};

Utils.mixin(Mixer, EventHandlerDelegate);


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/volume/profiles.js
================================================
/**
 * Shell Volume Mixer
 *
 * Card / profile settings cycling.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported Profiles */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const { SETTING } = Lib.settings;
const Log = Lib.utils.log;


/** @typedef {{
 *   card: String,
 *   profile: String,
 *   switcher: Boolean,
 *   show: Boolean,
 *   next: paCycledProfile,
 * }} paCycledProfile
 */

var Profiles = class {
    constructor(settings) {
        this._settings = settings;
        this._current = null;

        const data = this._settings.get_array(SETTING.pinned_profiles) || [];
        this._profiles = this._parseProfilesSetting(data);
    }

    /**
     * @returns {number}
     */
    count() {
        return this._profiles.length;
    }

    /**
     * @param {paCard} paCard
     */
    setCurrent(paCard) {
        if (!this.count()) {
            return;
        }

        let profile = paCard.card.profile;

        for (let entry of this._profiles) {
            if (entry.card === paCard.name && entry.profile === profile) {
                this._current = entry;
                break;
            }
        }
    }

    /**
     * @returns {?paCycledProfile}
     */
    next() {
        if (!this.count()) {
            return;
        }

        if (!this._current) {
            this._current = this._profiles[0];
        }

        this._current = this._current.next;

        return this._current;
    }

    /**
     * Reads all pinned profiles from settings.
     *
     * @param {string[]} data JSON formatted data
     * @private
     */
    _parseProfilesSetting(data) {
        const profiles = [];

        let count = 0;
        for (let entry of data) {
            let item = null;
            try {
                item = JSON.parse(entry);
            } catch (e) {
                Log.error('Profiles', '_parseProfilesSetting', e);
            }
            if (!item) {
                continue;
            }

            profiles.push(item);

            if (count > 0) {
                profiles[count - 1].next = item;
            }

            count++;
        }

        if (count > 0) {
            profiles[count - 1].next = profiles[0];
        }

        return profiles;
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/floatingLabel.js
================================================
/**
 * Shell Volume Mixer
 *
 * Floating label.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported FloatingLabel */

const Main = imports.ui.main;
const {Clutter, St} = imports.gi;



/**
 * A tooltip-like label to display the current value of a slider.
 *
 * Shamelessly stolen from gnome-shell/js/ui/dash.js.
 */
var FloatingLabel = class
{
    constructor() {
        this._label = new St.Label({ style_class: 'dash-label floating-label' });
        this.text = '100%';
        this._label.hide();
        Main.layoutManager.addChrome(this._label);
    }

    get text() {
        return this._label.get_text();
    }

    set text(text) {
        this._label.set_text(text);
    }

    get size() {
        return this._label.get_size();
    }

    show(x, y, animate) {
        this._label.opacity = 0;
        this._label.show();
        Main.uiGroup.set_child_above_sibling(this._label, null);

        const labelHeight = this._label.get_height();
        const labelWidth = this._label.get_width();

        const node = this._label.get_theme_node();
        const xOffset = node.get_length('-x-offset');

        let xPos;
        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
            xPos = x - labelWidth - xOffset;
        } else {
            xPos = x + labelWidth + xOffset;
        }

        const yPos = y - labelHeight;

        this._label.set_position(xPos, yPos);
        this._label.ease({
            opacity: 255,
            duration: animate !== false ? 150 : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    hide(animate) {
        this._label.ease({
            opacity: 0,
            duration: animate !== false ? 100 : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._label.hide(),
        });
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/menuItem.js
================================================
/**
 * Shell Volume Mixer
 *
 * Menu items.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported MasterMenuItem, SubMenuItem */

const { Clutter, GObject, St } = imports.gi;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const PopupMenu = imports.ui.popupMenu;

const Slider = Lib.widget.slider;
const Utils = Lib.utils.utils;

/**
 * @mixin
 */
class BaseMenuItem
{
    _makeOrnamentLabel() {
        return new St.Label({ style_class: 'popup-menu-ornament' });
    }

    /**
     * @returns {St.BoxLayout}
     */
    _makeItemLine() {
        return new St.BoxLayout({
            style_class: 'popup-menu-item svm-container-line',
            reactive:    true,
        });
    }

    _prepareMenuItem() {
        this.get_children().map(child => {
            this.remove_actor(child);
        });

        this.container = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            style_class: 'svm-menu-item-container',
        });

        this.add(this.container);

        if (!this.firstLine) {
            this.firstLine = this._makeItemLine();
            if (this._ornamentLabel) {
                this.firstLine.add_child(this._ornamentLabel);
            }
            this.container.add(this.firstLine);
        }

        if (!this.secondLine) {
            this.secondLine = this._makeItemLine();
            if (this._ornamentLabel) {
                this.secondLine.add_child(this._makeOrnamentLabel());
            }
            this.container.add(this.secondLine);
        }

        this.firstLine.add_style_class_name('line-1');
        this.secondLine.add_style_class_name('line-2');
    }
}


/**
 * Submenu item for the sink selection menu.
 *
 * @mixes BaseMenuItem
 */
var MasterMenuItem = GObject.registerClass(class MasterMenuItem extends PopupMenu.PopupSubMenuMenuItem
{
    _init() {
        super._init('', true);
        this._prepareMenuItem();

        this._slider = new Slider.VolumeSlider(0);

        this.firstLine.add_child(this.icon);
        this.firstLine.add_child(this.label);

        this.firstLine.add_child(new St.Bin({
            style_class: 'popup-menu-item-expander',
            x_expand: true,
        }));

        this.firstLine.add_child(this._triangleBin);

        this.secondLine.add_child(this._slider); // shell uses add_child here but that breaks layout?
        this.secondLine.add_style_class_name('svm-master-slider-line');

        this.label.add_style_class_name('svm-master-label');
        this.add_style_class_name('svm-master-slider svm-menu-item');
    }

    vfunc_button_release_event(event) {
        if (event.button === 2) {
            return Clutter.EVENT_STOP;
        }

        return super.vfunc_button_release_event(event);
    }

    /**
     * Change volume on left / right.
     */
    vfunc_key_press_event(event) {
        const symbol = event.keyval;

        if (symbol === Clutter.KEY_Right || symbol === Clutter.KEY_Left) {
            return this._slider.emit('key-press-event', event);
        }

        return super.vfunc_key_press_event(event);
    }

    addMenuItem(item) {
        const pos = (this.menu._getMenuItems().length || 0) - 1;

        this.menu.addMenuItem(item, pos < 0 ? 0 : pos);
    }
});

Utils.mixin(MasterMenuItem, BaseMenuItem, true);

/**
 * Sub menu item implementation for dropdown menus (via master slider menu or input menu).
 *
 * @mixes BaseMenuItem
 */
var SubMenuItem = GObject.registerClass(class SubMenuItem extends PopupMenu.PopupBaseMenuItem
{
    _init(params = {}) {
        super._init({
            ...params,
            activate: false,
        });

        this._prepareMenuItem();
    }

    addDetails(label) {
        const line = this._makeItemLine();

        line.add_child(label);
        this.container.insert_child_at_index(line, 1);
    }
});

Utils.mixin(SubMenuItem, BaseMenuItem, true);


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/panelButton.js
================================================
/**
 * Shell Volume Mixer
 *
 * Stand-alone menu panel button.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported PanelButton */

const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;
const { GObject, St } = imports.gi;
const PanelMenu = imports.ui.panelMenu;
const Volume = imports.ui.status.volume;

const { Indicator } = Lib.menu.indicator;

/**
 * Stand-alone panel menu button.
 */
var PanelButton = GObject.registerClass(class PanelButton extends PanelMenu.Button
{
    /**
     * @param {Mixer} mixer
     * @param {Object} options
     * @private
     */
    _init(mixer, options = {}) {
        super._init(0.0, 'ShellVolumeMixer', false);

        this._indicators = new St.BoxLayout({ style_class: 'panel-status-indicators-box' });
        this.add_child(this._indicators);

        this._volume = new Indicator(mixer, {
            ...options,
            separator: true,
            menuClass: 'svm-standalone-menu',
        });

        this._indicators.add_child(this._volume);
        this.menu.addMenuItem(this._volume.menu);
    }
});


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/percentageLabel.js
================================================
/**
 * Shell Volume Mixer
 *
 * Percentage label.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported PercentageLabel */

const { Clutter, GObject, St } = imports.gi;
const Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;

const { EventBroker } = Lib.utils.eventBroker;


var PercentageLabel = GObject.registerClass(class PercentageLabel extends St.Label {
    _init(mixer) {
        this._events = new EventBroker();

        super._init({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.add_style_class_name('percentage-label');

        this._events.connect('volume-changed', (event, volume) => {
            this._setText(volume);
        });

        // set initial value, if available
        this._setText(mixer.getVolume());
    }

    /**
     * @param {?number} percent
     * @private
     */
    _setText(percent) {
        if (percent === null) {
            this.text = '';
        } else {
            this.text = _('%d\u2009%%').format(percent);
        }
    }
});


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/slider.js
================================================
/**
 * Shell Volume Mixer
 *
 * Sliders.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported VolumeSlider */

const { Clutter, GObject } = imports.gi;
const Slider = imports.ui.slider;


/**
 * Custom Slider to allow for mute via middle button.
 */
var VolumeSlider = GObject.registerClass(class VolumeSlider extends Slider.Slider
{
    /**
     * Allow middle button event to bubble up for mute / unmute.
     */
    startDragging(event) {
        if (event.get_button() === 2) {
            return Clutter.EVENT_PROPAGATE;
        }
        return super.startDragging(event);
    }
});


================================================
FILE: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js
================================================
/**
 * Shell Volume Mixer
 *
 * Volume widgets.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported MasterSlider, AggregatedInput, OutputSlider, EventsSlider, InputSlider, InputStreamSlider, VolumeMenu */

const { Clutter, GLib, St } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const Lib = ExtensionUtils.getCurrentExtension().imports.lib;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Volume = imports.ui.status.volume;
const __ = ExtensionUtils.gettext;

const { EventBroker } = Lib.utils.eventBroker;
const { FloatingLabel } = Lib.widget.floatingLabel;
const Log = Lib.utils.log;
const MenuItem = Lib.widget.menuItem;
const Settings = Lib.settings;
const Slider = Lib.widget.slider;
const Utils = Lib.utils.utils;


/**
 * Extension of Volume.StreamSlider without its constructor().
 */
class StreamSliderExtension {}
Utils.mixin(StreamSliderExtension, Volume.StreamSlider);


/**
 * Extension of Volume.OutputStreamSlider without its constructor().
 */
class OutputStreamSliderExtension extends StreamSliderExtension {}
Utils.mixin(OutputStreamSliderExtension, Volume.OutputStreamSlider);


/**
 * Basic StreamSlider implementation for Input- and OutputStreams.
 *
 * We can extend (and monkey patch) Volume.OutputStreamSlider
 * (Volume.InputStreamSlider is meant for microphones only and Volume.StreamSlider is only basic).
 */
const StreamSlider = class extends OutputStreamSliderExtension
{
    /**
     * @param {Gvc.MixerControl} control
     * @param {sliderOptions} options
     * @private
     */
    constructor(control, options = {}) {
        super();

        this._isDestroyed = false;
        this._hasHeadphones = false;

        this.options = options;
        this._control = control;
        this._mixer = options.mixer;
        this._events = new EventBroker();

        this._init(options);

        return this;
    }

    /**
     * Init basically copied from Volume.StreamSlider (all init) and Volume.OutputStreamSlider (icons).
     *
     * @param {sliderOptions} options
     */
    _init(options) {
        if (!this.item) {
            this.item = new MenuItem.SubMenuItem();
        }

        if (this.icon) {
            // different widgets seem to use different naming
            this._icon = this.icon;
        }

        if (!this._icon) {
            this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
            this.item.firstLine.add_child(this._icon);
        }

        if (!this._label) {
            this._label = new St.Label({ text: '' });
            this.item.firstLine.add_child(this._label);
        }

        this.item.label_actor = this._label;

        if (!this._slider) {
            this._slider = new Slider.VolumeSlider(0);
            this.item.secondLine.add_child(this._slider);
        }

        this._volumeInfo = new FloatingLabel();


        this.item.connect('destroy', this._onDestroy.bind(this));

        if (this._onButtonPress) {
            this.item.connect('button-press-event', this._onButtonPress.bind(this));
        }

        if (this._onKeyPress) {
            this.item.connect('key-press-event', this._onKeyPress.bind(this));
        }

        if (this._slider.scroll) {
            this.item.connect('scroll-event', (slider, event) => {
                return this._slider.emit('scroll-event', event);
            });
        }

        this.item.connect('scroll-event', () => {
            this._showVolumeInfo();
        });


        this._inDrag = false;
        this._notifyVolumeChangeId = 0;

        this._soundSettings = new Settings.Settings(Settings.SOUND_SETTINGS_SCHEMA);
        this._soundSettings.connect(`changed::${Settings.ALLOW_AMPLIFIED_VOLUME_KEY}`, this._amplifySettingsChanged.bind(this), true);
        this._amplifySettingsChanged();

        this._sliderChangedId = this._slider.connect('notify::value', this._sliderChanged.bind(this));
        this._slider.connect('drag-begin', () => (this._inDrag = true));
        this._slider.connect('drag-end', () => {
            this._inDrag = false;
            this._notifyVolumeChange();
        });

        this.stream = options.stream || null;
        this._volumeCancellable = null;

        this._icons = [
            'audio-volume-muted-symbolic',
            'audio-volume-low-symbolic',
            'audio-volume-medium-symbolic',
            'audio-volume-high-symbolic',
            'audio-volume-overamplified-symbolic',
        ];
    }

    _onKeyPress(actor, event) {
        return this._slider.emit('key-press-event', event);
    }

    _onButtonPress(actor, event) {
        if (event.get_button() === 2) {
            this.toggleMute();

            return Clutter.EVENT_STOP;
        }

        return this._slider.startDragging(event);
    }

    refresh() {
        this._updateLabel();
        this._updateSliderIcon();
    }

    _updateSliderIcon() {
        if (this._stream && !this.options.symbolicIcons) {
            this._icon.gicon = this._stream.get_gicon();
        } else {
            super._updateSliderIcon();
        }

        this.emit('stream-updated');
    }

    _connectStream(stream) {
        super._connectStream(stream);
        this.refresh();
    }

    _updateLabel() {
        this._label.text = this._stream.name || this._stream.description || '';
    }

    _showVolumeInfo(position) {
        if (!this._stream || !this._volumeInfo) {
            return;
        }

        this._volumeInfo.text = Math.round(this._slider.value * 100) + '%';

        if (this._labelTimeoutId) {
            GLib.source_remove(this._labelTimeoutId);
            this._labelTimeoutId = undefined;
        }

        if (!this._infoShowing) {
            this._infoShowing = true;

            let x, y;
            if (position) {
                [x, y] = position;
                x     += 15;
                y     += 100;
            } else {
                [x, y] = this._slider.get_transformed_position();
                x = x + Math.floor(this._slider.get_width() / 2);
            }

            this._volumeInfo.show(x, y);
        }

        this._labelTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
            this._infoShowing = false;
            this._labelTimeoutId = undefined;
            this._volumeInfo.hide();
            return GLib.SOURCE_REMOVE;
        });
    }

    hideVolumeInfo() {
        if (this._labelTimeoutId) {
            GLib.source_remove(this._labelTimeoutId);
            this._labelTimeoutId = undefined;
        }

        this._infoShowing = false;
        this._volumeInfo.hide(false);
    }

    toggleMute() {
        if (this._stream) {
            this._stream.change_is_muted(!this._stream.is_muted);
        }
    }

    _onDestroy() {
        this._isDestroyed = true;

        // make sure we clean up all bindings
        if (this._stream) {
            this._disconnectStream(this._stream);
        }
    }
};



/**
 * Slider replacing the master volume slider.
 */
var MasterSlider = class extends StreamSlider
{
    _init(options) {
        this.item = new MenuItem.MasterMenuItem();
        this.item.menu.actor.add_style_class_name('svm-master-slider-menu');

        this._slider = this.item._slider;
        this._icon = this.item.icon;
        this._label = this.item.label;

        this._slider.accessible_name = _('Volume');

        super._init(options);
        this._addSettingsItem();
    }

    /**
     * Add settings shortcuts to the menu.
     *
     * @private
     */
    _addSettingsItem() {
        this.item.menu.addAction(_('Settings'), () => ExtensionUtils.openPrefs());
    }

    /**
     * @param {OutputSlider} slider
     */
    addOutputSlider(slider) {
        this.item.addMenuItem(slider.item);
    }

    /**
     * Override button click to allow for mute / unmute and menu to be opened.
     */
    _onButtonPress(actor, event) {
        if (event.get_button() === 2) {
            this.toggleMute();
        }

        return Clutter.EVENT_STOP;
    }

    _updateLabel() {
        this._label.text = this._stream.description;
    }

    scroll(event) {
        const eventResult = super.scroll(event);

        if (Main.panel.statusArea.aggregateMenu.menu.isOpen) {
            this._showVolumeInfo();
        }

        return eventResult;
    }
};


/**
 * Menu item for aggregated input streams.
 */
var AggregatedInput = class
{
    constructor() {
        this.item = new PopupMenu.PopupSubMenuMenuItem(__('Inputs'), true);
        this.item.icon.icon_name = 'applications-multimedia-symbolic';
        this.item.accessible_name = __('Inputs');

        this._inputStream = null;
    }

    setInputStream(inputSlider) {
        this._inputStream = inputSlider;
        this.addSlider(inputSlider, 0);
    }

    addSlider(slider, pos) {
        this.item.menu.addMenuItem(slider.item, pos || undefined);

        slider.connect('stream-updated', () => {
            this.refresh();
        });
    }

    refresh() {
        this.item.visible = (this.item.menu.numMenuItems > 1
            || (this.item.menu.numMenuItems > 0 && this._inputStream.isVisible())
        );
    }
};



/**
 * Slider for output sinks (e.g. alsa devices, different profiles).
 */
var OutputSlider = class extends StreamSlider
{
    _init(options) {
        // make details widget available before parent triggers accessing it
        if (options.detailed) {
            this._details = new St.Label({ text: '', style_class: 'svm-slider-details' });
        }

        super._init(options);

        if (options.detailed) {
            this.item.addDetails(this._details);
        }

        this._cards = options.mixer.cards;
        this._updateVisibility(false);

        this._events.connect('default-sink-updated', this._onDefaultSinkUpdated.bind(this));
    }

    _onButtonPress(actor, event) {
        if (event.get_button() === 1) {
            this._setAsDefault();
            return Clutter.EVENT_PROPAGATE;
        }

        return super._onButtonPress(actor, event);
    }

    _onKeyPress(actor, event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return) {
            this._setAsDefault();
            return Clutter.EVENT_STOP;
        }

        return super._onKeyPress(actor, event);
    }

    _updateLabel() {
        let text = this._stream.description;
        let description = this._stream.name;

        this._label.text = text;

        if (this.options.detailed && text !== description && description) {
            let parts = description.split('.');

            if (parts.length > 1) {
                if (parts[0] === 'alsa_output') {
                    // remove the common first (and uninteresting) part
                    parts.shift();
                }
                // the last segment of the path is the most interesting one
                description = parts.pop();
                description += ` | ${parts.join('.')}`;
            }

            this._details.text = description;
        }
    }

    _setAsDefault() {
        this._control.set_default_sink(this._stream);
    }

    _onDefaultSinkUpdated(/* stream */) {
        this._updateVisibility(false);
    }

    _updateVisibility(forceRefresh = true) {
        if (!this._shouldBeVisible()) {
            // set invisible immediately
            this.item.visible = false;

        } else {
            // check if port is available before setting visible
            (async () => {
                try {
                    const byPort = await this._shouldBeVisibleByPort(forceRefresh);

                    if (this._isDestroyed) {
                        // async race condition, we're already gone
                        return;
                    }

                    // This could be a race condition with the async code finishing after current conditions have changed.
                    // Therefore we have to check the sync path again.
                    this.item.visible = byPort && this._shouldBeVisible();

                } catch (e) {
                    Log.error('OutputSlider', '_updateVisibility', e);
                }
            })();
        }
    }

    _shouldBeVisible() {
        if (this.options.mixer.defaultSink === this._stream) {
            Log.info(`Hiding ${this._stream.id}:${this._stream.name}, it's the default sink`);

            return false;
        }

        return super._shouldBeVisible();
    }

    /**
     * @returns {Promise<boolean>}
     * @private
     */
    async _shouldBeVisibleByPort(forceRefresh = true) {
        if (!this._stream || ! this._cards) {
            return true;
        }

        if (!this._stream.card_index) {
            Log.error('OutputSlider', '_shouldBeVisibleByPort', 'Stream cannot be identified, no card index available');
            return true;
        }

        const card = await this._cards.get(this._stream.card_index, forceRefresh);

        if (!card) {
            Log.info(`Card ${this._stream.card_index} not found for stream ${this._stream.id}:${this._stream.name}`);
            return true;
        }

        const port = this._stream.port in card.ports ? card.ports[this._stream.port] : null;

        if (!port) {
            Log.error('OutputSlider', '_shouldBeVisibleByPort', `Port ${this._stream.port} not found for stream ${this._stream.id}:${this._stream.name}`);
            return true;
        }

        // null == cannot be disabled, true == available, false == not available
        if (port.available !== false) {
            return true;
        }

        Log.info(`Hiding ${this._stream.id}:${this._stream.name}, port "${this._stream.port}" not available`);

        return false;
    }
};


/**
 * Slider for system sounds.
 */
var EventsSlider = class extends StreamSlider
{
    /**
     * @param {sliderOptions} options
     * @private
     */
    _init(options) {
        super._init(options);

        this.item.add_style_class_name('events-stream-slider');
        this.item.secondLine.add_style_class_name('svm-events-slider-line');
    }

    _updateLabel() {
        this._label.text = this._stream.name;
    }
};


/**
 * Slider for input sinks (e.g. media players).
 */
var InputSlider = class extends StreamSlider
{
    _updateLabel() {
        let text = this._stream.name;
        let description = this._stream.description;

        if (description && text !== description) {
            if (text) {
                text = `${description} | ${text}`;
            } else {
                text = description;
            }
        }

        this._label.text = text || '[%s]'.format(__('unknown'));
    }
};


/**
 * Input stream slider (microphones, etc ?).
 */
var InputStreamSlider = class extends StreamSlider
{
    /**
     * @param {sliderOptions} options
     */
    _init(options) {
        super._init(options);

        this._showInput = false;

        this._slider.accessible_name = _('Microphone');
        this._streamAddedId = this._control.connect('stream-added', this._maybeShowInput.bind(this));
        this._streamRemovedId = this._control.connect('stream-removed', this._maybeShowInput.bind(this));

        this._icon.icon_name = 'audio-input-microphone-symbolic';
        this._icons = [
            'microphone-sensitivity-muted-symbolic',
            'microphone-sensitivity-low-symbolic',
            'microphone-sensitivity-medium-symbolic',
            'microphone-sensitivity-high-symbolic',
        ];
    }

    _connectStream(stream) {
        Volume.InputStreamSlider.prototype._connectStream.apply(this, [stream]);
        this.refresh();
    }

    _maybeShowInput() {
        if (this.options.showAlways === true) {
            this._showInput = true;
            this._updateVisibility();
        } else {
            // we extend from output stream slider impl, but this is an input stream slider
            Volume.InputStreamSlider.prototype._maybeShowInput.call(this);
        }
    }

    _shouldBeVisible() {
        return Volume.InputStreamSlider.prototype._shouldBeVisible.call(this);
    }

    isVisible() {
        return this._shouldBeVisible();
    }

    _updateLabel() {
        this._label.text = _('Microphone');
    }

    _updateSliderIcon() {
        if (this._stream && !this.options.symbolicIcons) {
            this._icon.gicon = this._stream.get_gicon();
        } else {
            this._icon.icon_name = 'audio-input-microphone-symbolic';
        }

        this.emit('stream-updated');
    }

    _onDestroy() {
        if (this._streamAddedId) {
            this._control.disconnect(this._streamAddedId);
        }

        if (this._streamRemovedId) {
            this._control.disconnect(this._streamRemovedId);
        }

        super._onDestroy();
    }
};


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/cs/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# Czech translation for GNOME Shell Volume Mixer.
# Copyright (C) 2017
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Daniel Rusek <mail@asciiwolf.com>
#
msgid ""
msgstr ""
"Project-Id-Version: GNOME Shell Volume Mixer\n"
"PO-Creation-Date: 2017-11-07 23:12+0100\n"
"PO-Revision-Date: 2021-12-08 11:37+0100\n"
"Last-Translator: Daniel Rusek <mail@asciiwolf.com>\n"
"Language-Team: \n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-KeywordsList: ;__\n"
"X-Generator: Poedit 3.0\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: locale\n"
"X-Poedit-SearchPathExcluded-1: pautils\n"
"X-Poedit-SearchPathExcluded-2: schemas\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Chyba při získávání detailů o kartě"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Pomocný skript nevrátil platná data o kartě."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Vstupy"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "neznámé"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Umístění směšovače hlasitosti"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Stavové menu"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Vlevo"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Uprostřed"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Vpravo"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Odstranit původní posuvníky"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Zobrazit detailní posuvníky"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Klávesová zkratka pro přepínač profilů"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(např. „<Super>p”)"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Použít symbolické ikony"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Zobrazit systémové zvuky"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Zobrazit virtuální proudy"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Vždy zobrazit vstupní proudy"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "Zobrazí posuvník i pokud žádná aplikace nenahrává zvuk"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Nastavení"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Zařízení"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Karta"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Profil"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Přepínač"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Zobrazit"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/de/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# German translation for GNOME Shell Volume Mixer.
# Copyright (C) 2014
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Jonatan Zedler <jonatan_zedler@gmx.de>, 2014.
# Alexander Hofbauer <alex@derhofbauer.at>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: GNOME Shell Volume Mixer\n"
"Report-Msgid-Bugs-To: \n"
"PO-Creation-Date: 2014-11-04 21:00+0100\n"
"PO-Revision-Date: 2021-12-08 11:35+0100\n"
"Last-Translator: Alexander Hofbauer <alex@derhofbauer.at>\n"
"Language-Team: German\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Fehler beim Abfragen der Karten"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Hilfsprogramm gab keine gültigen Daten zurück."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr "Abfrage der PulseAudio-Soundkarten fehlgeschlagen, Erweiterung deaktiviert"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Eingänge"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "unbekannt"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Position des Lautstärkereglers"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Statusmenü"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Links"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Zentriert"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Rechts"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Originalen Lautstärkeregler entfernen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Details an den Schiebereglern anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Hotkey für Profilumschalter"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(z.B. \"<Super>p\")"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Symbolische Icons verwenden"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Systemklänge anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Virtuelle Streams anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Eingänge immer anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr ""
"Zeigt Aufnahmeströme (Mikrofone) immer an, auch wenn keine Applikation "
"gerade aufnimmt"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr "Prozent anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr "Lautstärke des aktuellen Ausgangs in Prozent neben dem Icon anzeigen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Einstellungen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Geräte"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Karte"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Profil"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Umschalter"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Anzeigen"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/it/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Gianvito Cavasoli <gianvito@gmx.it>, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: GNOME Shell Volume Mixer\n"
"PO-Creation-Date: 2017-11-07 23:12+0100\n"
"PO-Revision-Date: 2021-12-08 11:37+0100\n"
"Last-Translator: Gianvito Cavasoli <gianvito@gmx.it>\n"
"Language-Team: \n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.0\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Errore nel recuperare le informazioni sulla scheda"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Lo script di aiuto non ha restituito dati della scheda validi"

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Sorgenti"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "sconosciuto"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Posizione mixer del volume"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Menù di stato"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Sinistra"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Centro"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Destra"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Rimuovere cursore di scorrimento originale"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Mostrare cursore di scorrimento dettagliato"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Scorciatoia selettore del profilo"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(esempio «<Super>p»)"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Usare le icone simboliche"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Mostra i suoni di sistema"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Mostrare i flussi virtuali"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Mostrare sempre i flussi di input"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "Mostra il cursore di scorrimento anche se nessuna applicazione sta registrando"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Impostazioni"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Dispositivi"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Scheda"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Profilo"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Attiva"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Visualizza"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/nl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Heimen Stoffels <vistausss@outlook.com>, 2020.
msgid ""
msgstr ""
"Project-Id-Version: \n"
"PO-Creation-Date: 2020-10-26 13:47+0100\n"
"PO-Revision-Date: 2021-12-08 11:37+0100\n"
"Last-Translator: Heimen Stoffels <vistausss@outlook.com>\n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Fout tijdens ophalen van kaartinformatie"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Het hulpscript koppelde geen geldige kaartinformatie terug."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""
"Het opvragen van PulseAudio-geluidskaarten is mislukt - de uitbreiding wordt uitgeschakeld"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Invoerbronnen"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "onbekend"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Positie van volumemixer"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Statusmenu"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Links"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Midden"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Rechts"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "GNOME's schuifbalk verbergen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Uitgebreide schuifbalken tonen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Sneltoets om te wisselen van profiel"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(bijv. '<Super>p')"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Symbolische pictogrammen gebruiken"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Systeemgeluiden tonen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Virtuele streams tonen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Altijd invoerstreams tonen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "Toon de schuifbalk ook als er geen opnames plaatsvinden"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr "Percentage tonen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr "Toon het volumepercentage van de huidige uitvoerbron op het pictogram"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Instellingen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Apparaten"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Kaart"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Profiel"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Wisselen"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Weergave"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/pl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# Polish translation for GNOME Shell Volume Mixer.
# Copyright (C) 2017
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Piotr Komur, pkomur@gmail.com, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: GNOME Shell Volume Mixer\n"
"Report-Msgid-Bugs-To: \n"
"PO-Creation-Date: 2017-11-07 23:12+0100\n"
"PO-Revision-Date: 2021-12-08 11:36+0100\n"
"Last-Translator: Piotr Komur <pkomur@gmail.com>\n"
"Language-Team: \n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"X-Poedit-SourceCharset: UTF-8\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Poedit-Basepath: .\n"
"X-Poedit-KeywordsList: _;title;label\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Błąd podczas pobierania danych karty"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Narzędzie nie zwróciło prawidłowych danych."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Wejścia"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "nieznany"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Położenie miksera"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Menu systemowe"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Z lewej"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Po środku"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Z prawej"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Ukryj systemowy regulator"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Pokaż szczegóły urządzenia"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Skrót klawiszowy przełączania profilów"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(np. \"<Super>p\")"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Pokaż ikony symboliczne"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Pokaż dźwięki systemowe"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Pokaż strumienie wirtualne"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Pokaż strumienie wejściowe"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "(Pokazuje regulatory, nawet przy braku aktywnych programów nagrywających)"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Ustawienia"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Urządzenia"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Karta"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Profil"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Przełącznik"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Wyświetlacz"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/pt_BR/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# Brazilian Portuguese translation for GNOME Shell Volume Mixer.
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Ricardo Silva Veloso <ricvelozo@gmail.com>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: GNOME Shell Volume Mixer\n"
"PO-Creation-Date: 2017-11-07 23:12+0100\n"
"PO-Revision-Date: 2021-12-08 11:37+0100\n"
"Last-Translator: Ricardo Silva Veloso <ricvelozo@gmail.com>\n"
"Language-Team: \n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-Basepath: ..\n"
"X-Poedit-KeywordsList: ;__\n"
"X-Generator: Poedit 3.0\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: locale\n"
"X-Poedit-SearchPathExcluded-1: pautils\n"
"X-Poedit-SearchPathExcluded-2: schemas\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Erro ao recuperar detalhes da placa"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "O script ajudante não retornou dados de placa válidos."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Entradas"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "desconhecido"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Posição do mixador de volume"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "Menu de Status"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Esquerda"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "Centro"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Direita"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Remover controle deslizante"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Exibir controles deslizantes detalhados"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Tecla de atalho para seletor de perfil"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(ex. \"<Super>p\")"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Usar ícones simbólicos"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Exibir sons do sistema"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Exibir fluxos virtuais"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Sempre exibir fluxos de entrada"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "Exibe o controle deslizante mesmo se nenhum aplicativo estiver gravando"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Configurações"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Dispositivos"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Placa"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Perfil"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Ativar"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Mostrar"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/ru/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po
================================================
# This file is distributed under the same license as the GNOME Shell Volume Mixer.
# Alexandr K <Alexandr1322@yandex.ua>, 2021.
msgid ""
msgstr ""
"Project-Id-Version: gnome-shell-volume-mixer\n"
"PO-Creation-Date: 2021-11-03 18:39+0100\n"
"PO-Revision-Date: 2021-12-08 11:37+0100\n"
"Last-Translator: Alexandr K <Alexandr1322@yandex.ua>\n"
"Language-Team: Alexandr K <Alexandr1322@yandex.ua>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"

#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr "Ошибка при получении сведений о карте"

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr "Вспомогательный скрипт не вернул действительные данные карты."

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr "Сбой запроса звуковых карт PulseAudio, отключение расширения"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr "Входы"

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr "неизвестно"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr "Позиция volume mixer"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr "В меню состояния"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr "Слева"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr "По центру"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr "Справо"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr "Скрыть оригинальный ползунок"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr "Показывать детали под ползунком"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr "Горячая клавиша для переключателя профилей"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr "(например, \"<Super>p\")"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr "Использовать символьные иконки"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr "Показывать системные звуки"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr "Показать действующие устройства"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr "Всегда показывать устройства ввода"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr "Показывает ползунок, даже если приложение не записывает"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr "Показать процент громкости"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr "Показывать ли процент от текущего выходного сигнала справа от значка"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr "Общие настройки"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr "Устройства вывода"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr "Карты"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr "Профиль"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr "Переключить"

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr "Показать"


================================================
FILE: shell-volume-mixer@derhofbauer.at/locale/translations.pot
================================================
#: shell-volume-mixer@derhofbauer.at/prefs.js:219
msgid "Error retrieving card details"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.js:220
msgid "Helper script did not return valid card data."
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116
msgid "Querying PulseAudio sound cards failed, disabling extension"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323
#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325
msgid "Inputs"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541
msgid "unknown"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:39
msgid "Position of volume mixer"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:53
msgid "Status Menu"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:54
msgid "Left"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:55
msgid "Center"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:56
msgid "Right"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:69
msgid "Remove original slider"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:81
msgid "Show detailed sliders"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:116
msgid "Hotkey for profile switcher"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:137
msgid "(e.g. \"<Super>p\")"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:149
msgid "Use symbolic icons"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:172
msgid "Show system sounds"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:195
msgid "Show virtual streams"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:218
msgid "Always show input streams"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:240
msgid "Shows the slider even if no application is recording"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:288
msgid "Show percentage"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:310
msgid "Whether to show percentage of current output right of the icon"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:340
msgid "Settings"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:372
#: shell-volume-mixer@derhofbauer.at/prefs.ui:502
msgid "Devices"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:413
msgid "Card"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:427
msgid "Profile"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:440
msgid "Switch"
msgstr ""

#: shell-volume-mixer@derhofbauer.at/prefs.ui:455
msgid "Display"
msgstr ""


================================================
FILE: shell-volume-mixer@derhofbauer.at/metadata.json
================================================
{
    "version": 9999,
    "uuid": "shell-volume-mixer@derhofbauer.at",
    "name": "Volume Mixer",
    "description": "Applet allowing separate configuration of PulseAudio mixers.\n\nShell Volume Mixer is an extension for GNOME Shell allowing separate configuration of PulseAudio devices and output switches. It features a profile switcher to quickly switch between pinned profiles and devices.\n\nMiddle mouse click on a slider mutes the selected stream.\n\nPlease file bugs and feature requests on the GitHub page.",
    "shell-version": [
        "42"
    ],
    "settings-schema": "org.gnome.shell.extensions.shell-volume-mixer",
    "gettext-domain": "gnome-shell-extensions-shell-volume-mixer",
    "url": "https://github.com/aleho/gnome-shell-volume-mixer"
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/__init__.py
================================================


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/cards.py
================================================
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>
#
# 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 <http://www.gnu.org/licenses/>.

from .pulseaudio import Pulseaudio
from . import log
from . import libpulse


class Cards(Pulseaudio):
    def build_callback(self):
        return libpulse.card_info_cb_t(self.pa_cb)

    def get_by_index(self, index, callback):
        return libpulse.context_get_card_info_by_index(self._context, index, callback, None)

    def get_by_name(self, name, callback):
        return libpulse.context_get_card_info_by_name(self._context, name, callback, None)

    def get_all(self, callback):
        return libpulse.context_get_card_info_list(self._context, callback, None)

    def cb_data(self, pa_card):
        card_name = pa_card.name.decode('utf8')

        try:
            alsa_card = libpulse.proplist_gets(pa_card.proplist, b'alsa.card')
            alsa_card = int(alsa_card.decode('utf8')) if alsa_card else None
        except Exception:
            log.debug('No property "alsa.card"')
            alsa_card = None

        try:
            description = libpulse.proplist_gets(pa_card.proplist, b'device.description')
            description = description.decode('utf8') if description else None
        except Exception:
            log.debug('No property "device.description"')
            description = None

        if not description:
            try:
                description = libpulse.proplist_gets(pa_card.proplist, b'alsa.card_name')
                description = description.decode('utf8') if description else None
            except Exception:
                log.debug('No property "alsa.card_name"')
                description = card_name

        card = {
            'index': pa_card.index,
            'alsaCard': alsa_card,
            'name': card_name,
            'description': description,
            'active_profile': None,
            'profiles': {
            },
            'ports': {
            },
        }

        if pa_card.active_profile and pa_card.active_profile[0]:
            ap = pa_card.active_profile[0]
            card['active_profile'] = ap.name.decode('utf8')

        for i in range(0, pa_card.n_profiles):
            if not pa_card.profiles2[i] or not pa_card.profiles2[i][0]:
                continue

            profile = pa_card.profiles2[i][0]
            name = profile.name.decode('utf8')

            card['profiles'][name] = {
                'name': name,
                'description': profile.description.decode('utf8'),
                'available': bool(profile.available),
            }

        for index in range(0, pa_card.n_ports):
            if not pa_card.ports[index] or not pa_card.ports[index][0]:
                continue

            port = pa_card.ports[index][0]
            name = port.name.decode('utf8')

            card['ports'][name] = {
                'name': name,
                'description': port.description.decode('utf8'),
                'direction': 'out' if port.direction == 1 else 'in',
                'available': True if port.available == 2 else (False if port.available == 1 else None),
            }

        return card


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/libpulse.py
================================================
# This library was generated through introspection of
#     /usr/include/pulse/introspect.h and
#     /usr/include/pulse/mainloop.h.
#     (E.g.:
#         python /usr/share/pyshared/ctypeslib/h2xml.py /usr/include/pulse/introspect.h -o pa.xml
#         python /usr/share/pyshared/ctypeslib/xml2py.py pa.xml -k f -l /usr/lib/libpulse.so -o pa.py
#     )
#
# License according to the header files listed above:
# GNU Lesser General Public License
#
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2014 Alexander Hofbauer <alex@derhofbauer.at>


from ctypes import *

try:
    lib = CDLL('libpulse.so.0')
except:
    lib = CDLL('libpulse.so')

STRING = c_char_p
WSTRING = c_wchar_p

class mainloop(Structure):
    pass

class mainloop_api(Structure):
    pass

class spawn_api(Structure):
    pass

class context(Structure):
    pass

class operation(Structure):
    pass

class proplist(Structure):
    pass

class card_profile_info(Structure):
    _fields_ = [
        ('name', STRING),
        ('description', STRING),
        ('n_sinks', c_uint32),
        ('n_sources', c_uint32),
        ('priority', c_uint32),
    ]

class card_profile_info2(Structure):
    _fields_ = [
        ('name', STRING),
        ('description', STRING),
        ('n_sinks', c_uint32),
        ('n_sources', c_uint32),
        ('priority', c_uint32),
        ('available', c_int),
    ]

class card_port_info(Structure):
    _fields_ = [
        ('name', STRING),
        ('description', STRING),
        ('priority', c_uint32),
        ('available', c_int),
        ('direction', c_int),
        ('n_profiles', c_uint32),
        ('profiles', POINTER(POINTER(card_profile_info))),
        ('proplist', POINTER(proplist)),
        ('latency_offset', c_int64),
        ('profiles2', POINTER(POINTER(card_profile_info2))),
    ]

class card_info(Structure):
    _fields_ = [
        ('index', c_uint32),
        ('name', STRING),
        ('owner_module', c_uint32),
        ('driver', STRING),
        ('n_profiles', c_uint32),
        ('profiles', POINTER(card_profile_info)),
        ('active_profile', POINTER(card_profile_info)),
        ('proplist', POINTER(proplist)),
        ('n_ports', c_uint32),
        ('ports', POINTER(POINTER(card_port_info))),
        ('profiles2', POINTER(POINTER(card_profile_info2))),
        ('active_profile2', POINTER(card_profile_info2)),
    ]

class sink_port_info(Structure):
    _fields_ = [
        ('name', STRING),
        ('description', STRING),
        ('priority', c_uint32),
        ('available', c_int),
        ('availability_group', STRING),
        ('type', c_uint32),
    ]

class format_info(Structure):
    _fields_ = [
        ('encoding', c_int),
        ('plist', POINTER(proplist)),
    ]

class sample_spec(Structure):
    _fields_ = [
        ('format', c_int),
        ('rate', c_uint32),
        ('channels', c_uint8),
    ]

class pa_channel_map(Structure):
    _fields_ = [
        ('channels', c_uint8),
        ('map', c_int * int(32)),
    ]

class pa_cvolume(Structure):
    _fields_ = [
        ('channels', c_uint8),
        ('values', c_uint32 * int(32)),
    ]

class sink_info(Structure):
    _fields_ = [
        ('name', STRING),
        ('index', c_uint32),
        ('description', STRING),
        ('sample_spec', sample_spec),
        ('channel_map', pa_channel_map),
        ('owner_module', c_uint32),
        ('volume', pa_cvolume),
        ('mute', c_int),
        ('monitor_source', c_uint32),
        ('monitor_source_name', STRING),
        ('latency', c_uint64),
        ('driver', STRING),
        ('flags', c_int),
        ('proplist', POINTER(proplist)),
        ('configured_latency', c_uint64),
        ('base_volume', c_uint32),
        ('state', c_int),
        ('n_volume_steps', c_uint32),
        ('card', c_uint32),
        ('n_ports', c_uint32),
        ('ports', POINTER(POINTER(sink_port_info))),
        ('active_port', POINTER(sink_port_info)),
        ('n_formats', c_uint8),
        ('formats', POINTER(POINTER(format_info))),
    ]

mainloop_new = lib.pa_mainloop_new
mainloop_new.restype = POINTER(mainloop)
mainloop_new.argtypes = []
mainloop_get_api = lib.pa_mainloop_get_api
mainloop_get_api.restype = POINTER(mainloop_api)
mainloop_get_api.argtypes = [POINTER(mainloop)]
mainloop_iterate = lib.pa_mainloop_iterate
mainloop_iterate.restype = c_int
mainloop_iterate.argtypes = [POINTER(mainloop), c_int, POINTER(c_int)]
mainloop_free = lib.pa_mainloop_free
mainloop_free.restype = None
mainloop_free.argtypes = [POINTER(mainloop)]

context_flags = c_int  # enum
context_flags_t = context_flags

context_state = c_int  # enum
context_state_t = context_state
# values for enumeration 'context_state'
CONTEXT_UNCONNECTED = 0
CONTEXT_CONNECTING = 1
CONTEXT_AUTHORIZING = 2
CONTEXT_SETTING_NAME = 3
CONTEXT_READY = 4
CONTEXT_FAILED = 5
CONTEXT_TERMINATED = 6

context_new = lib.pa_context_new
context_new.restype = POINTER(context)
context_new.argtypes = [POINTER(mainloop_api), STRING]
context_notify_cb_t = CFUNCTYPE(None, POINTER(context), c_void_p)
context_set_state_callback = lib.pa_context_set_state_callback
context_set_state_callback.restype = None
context_set_state_callback.argtypes = [POINTER(context), context_notify_cb_t, c_void_p]
context_connect = lib.pa_context_connect
context_connect.restype = c_int
context_connect.argtypes = [POINTER(context), STRING, context_flags_t, POINTER(spawn_api)]
context_disconnect = lib.pa_context_disconnect
context_disconnect.restype = None
context_disconnect.argtypes = [POINTER(context)]
context_unref = lib.pa_context_unref
context_unref.restype = None
context_unref.argtypes = [POINTER(context)]
context_get_state = lib.pa_context_get_state
context_get_state.restype = context_state_t
context_get_state.argtypes = [POINTER(context)]

operation_unref = lib.pa_operation_unref
operation_unref.restype = None
operation_unref.argtypes = [POINTER(operation)]

card_info_cb_t = CFUNCTYPE(None, POINTER(context), POINTER(card_info), c_int, c_void_p)
context_get_card_info_by_index = lib.pa_context_get_card_info_by_index
context_get_card_info_by_index.restype = POINTER(operation)
context_get_card_info_by_index.argtypes = [POINTER(context), c_uint32, card_info_cb_t, c_void_p]
context_get_card_info_by_name = lib.pa_context_get_card_info_by_name
context_get_card_info_by_name.restype = POINTER(operation)
context_get_card_info_by_name.argtypes = [POINTER(context), STRING, card_info_cb_t, c_void_p]
context_get_card_info_list = lib.pa_context_get_card_info_list
context_get_card_info_list.restype = POINTER(operation)
context_get_card_info_list.argtypes = [POINTER(context), card_info_cb_t, c_void_p]

sink_info_cb_t = CFUNCTYPE(None, POINTER(context), POINTER(sink_info), c_int, c_void_p)
context_get_sink_info_by_index = lib.pa_context_get_sink_info_by_index
context_get_sink_info_by_index.restype = POINTER(operation)
context_get_sink_info_by_index.argtypes = [POINTER(context), c_uint32, sink_info_cb_t, c_void_p]
context_get_sink_info_by_name = lib.pa_context_get_sink_info_by_name
context_get_sink_info_by_name.restype = POINTER(operation)
context_get_sink_info_by_name.argtypes = [POINTER(context), STRING, sink_info_cb_t, c_void_p]
context_get_sink_info_list = lib.pa_context_get_sink_info_list
context_get_sink_info_list.restype = POINTER(operation)
context_get_sink_info_list.argtypes = [POINTER(context), sink_info_cb_t, c_void_p]

proplist_gets = lib.pa_proplist_gets
proplist_gets.restype = STRING
proplist_gets.argtypes = [POINTER(proplist), STRING]

proplist_to_string = lib.pa_proplist_to_string
proplist_to_string.restype = STRING
proplist_to_string.argtypes = [POINTER(proplist)]

# this is a "magic" number (probably -1 at int32, but unsigned?) and tells us there's something fishy
NULL_ID = 4294967295


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/log.py
================================================
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>
#
# 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 <http://www.gnu.org/licenses/>.

import os
import sys

DEBUG = True if 'DEBUG' in os.environ else False


def debug(*msg):
    if DEBUG:
        print(*msg, file=sys.stderr)


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/pulseaudio.py
================================================
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>
#
# 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 <http://www.gnu.org/licenses/>.

import abc
from time import sleep

from . import log
from . import libpulse


class Pulseaudio:
    _pa_state = libpulse.CONTEXT_UNCONNECTED
    _op_done = False

    _iterations_max = 10000
    _iterations_intrv = 0.0001


    _data = {}
    _error = {
        'success': False,
        'error': None,
    }

    def __init__(self):
        self._pa_mainloop = libpulse.mainloop_new()
        self._pa_mainloop_api = libpulse.mainloop_get_api(self._pa_mainloop)

        self._context = libpulse.context_new(self._pa_mainloop_api, b'ShellVolumeMixer')
        self._context_notify_cb = libpulse.context_notify_cb_t(self.context_notify_cb)

        libpulse.context_set_state_callback(self._context, self._context_notify_cb, None)

    def __enter__(self):
        libpulse.context_connect(self._context, None, 0, None)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        libpulse.context_disconnect(self._context)
        libpulse.context_unref(self._context)
        libpulse.mainloop_free(self._pa_mainloop)

    def context_notify_cb(self, context, userdata):
        try:
            self._pa_state = libpulse.context_get_state(context)
        except Exception:
            self._pa_state = libpulse.CONTEXT_FAILED
            log.debug('Context failed')

    def get_info(self, index=None, name=None):
        log.debug('Querying details...')
        operation = None
        count = 0

        while self._pa_state != libpulse.CONTEXT_TERMINATED:
            if self._op_done:
                break

            if self._pa_state == libpulse.CONTEXT_FAILED:
                self._data = self._error
                self._data['error'] = 'context failed'
                break

            if count >= self._iterations_max:
                log.debug(f'Stopping iterations after {self._iterations_intrv * self._iterations_max}s (bug?)')
                break

            if count > 0 and count % (self._iterations_max / 2) == 0:
                log.debug(f'Delaying after {self._iterations_intrv * count}s (bug?)')
                sleep(2)

            if self._pa_state == libpulse.CONTEXT_READY and operation is None:
                callback = self.build_callback()

                if name:
                    log.debug('Requesting details for', name)
                    operation = self.get_by_name(name.encode('utf8'), callback)

                elif index and index >= 0:
                    log.debug('Requesting details for', index)
                    operation = self.get_by_index(index, callback)

                else:
                    log.debug('Requesting all available data')
                    operation = self.get_all(callback)

            libpulse.mainloop_iterate(self._pa_mainloop, 0, None)
            count += 1
            sleep(self._iterations_intrv)

        if operation:
            libpulse.operation_unref(operation)

        log.debug('Query done')

        return self._data

    def pa_cb(self, context, struct, eol, user_data):
        log.debug('In callback')

        if eol:
            self._op_done = True
            log.debug('All done')
            return

        if not struct or not struct[0]:
            log.debug('No data received for callback')
            return

        item = self.cb_data(struct[0])

        if item and 'index' in item:
            self._data[item['index']] = item

        log.debug('Callback done')

    @abc.abstractmethod
    def build_callback(self):
        return

    @abc.abstractmethod
    def get_by_index(self, index, callback):
        return

    @abc.abstractmethod
    def get_by_name(self, name, callback):
        return

    @abc.abstractmethod
    def get_all(self, callback):
        return

    @abc.abstractmethod
    def cb_data(self, data):
        return None


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/sinks.py
================================================
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>
#
# 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 <http://www.gnu.org/licenses/>.

from .pulseaudio import Pulseaudio
from . import log
from . import libpulse


class Sinks(Pulseaudio):
    def build_callback(self):
        return libpulse.sink_info_cb_t(self.pa_cb)

    def get_by_index(self, index, callback):
        return libpulse.context_get_sink_info_by_index(self._context, index, callback, None)

    def get_by_name(self, name, callback):
        return libpulse.context_get_sink_info_by_name(self._context, name, callback, None)

    def get_all(self, callback):
        return libpulse.context_get_sink_info_list(self._context, callback, None)

    def cb_data(self, pa_sink):
        sink_name = pa_sink.name.decode('utf8')
        description = pa_sink.description.decode('utf8')

        if not description:
            try:
                description = libpulse.proplist_gets(pa_sink.proplist, b'device.description')
                description = description.decode('utf8') if description else None
            except Exception:
                log.debug('No property "device.description"')
                description = sink_name

        try:
            alsa_card = libpulse.proplist_gets(pa_sink.proplist, b'alsa.card')
            alsa_card = int(alsa_card.decode('utf8')) if alsa_card else None
        except Exception:
            log.debug('No property "alsa.card"')
            alsa_card = None

        sink = {
            'index': pa_sink.index,
            'alsaCard': alsa_card,
            'name': sink_name,
            'description': description,
            'card': pa_sink.card if pa_sink.card != libpulse.NULL_ID else None,
            'active_port': None,
            'ports': {
            },
        }

        if pa_sink.active_port and pa_sink.active_port[0]:
            ap = pa_sink.active_port[0]
            sink['active_port'] = ap.name.decode('utf8')

        for index in range(0, pa_sink.n_ports):
            if not pa_sink.ports[index] or not pa_sink.ports[index][0]:
                continue

            port = pa_sink.ports[index][0]
            name = port.name.decode('utf8')

            sink['ports'][name] = {
                'name': name,
                'description': port.description.decode('utf8'),
                'type': port.type,
                'available': True if port.available == 2 else (False if port.available == 1 else None),
            }

        return sink


================================================
FILE: shell-volume-mixer@derhofbauer.at/pautils/query.py
================================================
#!/usr/bin/env python3
#
# Usage: query.py [cards|sinks] [index or name, omit for all data]
#
# Output is either a JSON object or an array of all data available, depending on
# whether an index / name was passed or no parameters at all.
#
# This file is part of GNOME Shell Volume Mixer
# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>
#
# 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 <http://www.gnu.org/licenses/>.

import json
import sys

from lib import log
from lib.cards import Cards
from lib.sinks import Sinks
from lib.libpulse import NULL_ID

if len(sys.argv) < 2:
    print('Need a type to query')
    sys.exit(1)

op_type = sys.argv[1]
index = name = None

if len(sys.argv) > 2:
    filter_arg = sys.argv[2]
    if filter_arg.isdigit():
        index = int(filter_arg)
    else:
        name = filter_arg


if index == NULL_ID:
    result = {}

elif op_type == 'cards':
    with Cards() as cards:
        result = cards.get_info(index=index, name=name)

elif op_type == 'sinks':
    with Sinks() as sinks:
        result = sinks.get_info(index=index, name=name)

else:
    print('Invalid type', op_type, 'requested')
    sys.exit(1)


print(json.dumps(result, indent=4 if log.DEBUG else None))


================================================
FILE: shell-volume-mixer@derhofbauer.at/prefs.js
================================================
/**
 * Shell Volume Mixer
 *
 * Preferences widget.
 *
 * @author Alexander Hofbauer <alex@derhofbauer.at>
 */

/* exported init, buildPrefsWidget */

const { Gtk, GObject } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const Lib = ExtensionUtils.getCurrentExtension().imports.lib;
const __ = ExtensionUtils.gettext;

const { Settings, SETTING } = Lib.settings;
const Log = Lib.utils.log;
const PaHelper = Lib.utils.paHelper;
const Utils = Lib.utils.utils;

let preferences;


const WIDGETS = {
    cmbPosition:              SETTING.position,
    swRemoveOriginal:         SETTING.remove_original,
    swShowPercentageLabel:    SETTING.show_percentage_label,
    swShowDetailedSliders:    SETTING.show_detailed_sliders,
    swShowSystemSounds:       SETTING.show_system_sounds,
    swShowVirtualStreams:     SETTING.show_virtual_streams,
    swAlwaysShowInputStreams: SETTING.always_show_input_streams,
    swUseSymbolicIcons:       SETTING.use_symbolic_icons,
    txtProfileSwitch:         null,
    treeDevices:              null,
    treePinned:               null,
    btnAddDevice:             null,
    btnRemoveDevice:          null,
};


const Preferences = GObject.registerClass({
    Implements: [ Gtk.BuilderScope ],
}, class Preferences extends GObject.Object
{
    vfunc_create_closure(builder, handlerName, flags, connectObject) {
        if (flags & Gtk.BuilderClosureFlags.SWAPPED)
            throw new Error('Unsupported template signal flag "swapped"');

        if (typeof this[handlerName] === 'undefined')
            throw new Error(`${handlerName} is undefined`);

        return this[handlerName].bind(connectObject || this);
    }

    _getId(object) {
        for (const [id, widget] of Object.entries(this._widgets)) {
            if (widget === object) {
                return id;
            }
        }

        return null;
    }

    _getSetting(object) {
        const id = this._getId(object);

        if (id) {
            return this._widgetSettings[id] || null;
        }

        return null;
    }

    toggleBoolean(object) {
        const setting = this._getSetting(object);

        if (!setting) {
            Log.error('Preferences', 'toggleBoolean', new Error(`BUG! No widget found for toggle handler (${this._getId(object)})`));
            return;
        }

        const active = object.get_active();
        this._settings.set_boolean(setting, active);
    }

    /**
     * Callback for menu position combobox.
     *
     * @param cmbPosition
     */
    onPositionChanged(cmbPosition) {
        let value = cmbPosition.get_active();

        this._settings.set_enum(SETTING.position, value);

        let checkbox = this._widgets.swRemoveOriginal;
        if (!checkbox) {
            return;
        }

        if (value === SETTING.position_at.menu) {
            checkbox.set_sensitive(false);
        } else {
            checkbox.set_sensitive(true);
        }
    }

    /**
     * Callback for hotkey button entry.
     *
     * @param widget
     */
    onProfileSwitchChanged(widget) {
        let entry = widget.get_text().trim();

        if (!entry) {
            this._settings.set_array(SETTING.profile_switcher_hotkey, []);
            return;
        }

        try {
            let [ok, key, mods] = Gtk.accelerator_parse(entry);

            if (!ok || key === 0 || mods === 0) {
                return;
            }

            let hotkey = Gtk.accelerator_name(key, mods);
            if (hotkey) {
                widget.set_text(hotkey);

                // Main.wm.addKeybinding expects an array
                this._settings.set_array(SETTING.profile_switcher_hotkey, [hotkey]);
            }

        } catch (e) {
            Log.error('Preferences', 'onProfileSwitchChanged', e);
        }
    }

    /**
     * Callback for add button.
     */
    onAddDevice(widget) {
        widget.set_sensitive(false);

        let [isSelected, store, iter] = this._deviceSelection.get_selected();

        if (!isSelected) {
            return;
        }


        let cardid = store.get_value(iter, 1);
        let profileid = store.get_value(iter, 2);

        let card = this._cards[cardid];
        let profile = this._cards[cardid].profiles[profileid];

        if (profile.pinned) {
            // safety check, we should never reach this anyway
            return;
        }

        profile.pinned = true;

        this._pinned.set(this._pinned.append(), [0, 1, 2, 3, 4, 5],
            [card.description, profile.description, true, true, cardid, profileid]);

        this._storePinned();
    }

    /**
     * Callback for remove button.
     */
    onRemoveDevice() {
        let [isSelected, store, iter] = this._pinnedSelection.get_selected();

        if (!isSelected) {
            return;
        }

        let cardid = store.get_value(iter, 4);
        let profileid = store.get_value(iter, 5);

        this._cards[cardid].profiles[profileid].pinned = false;

        store.remove(iter);
        this._storePinned();

        // now check for the currently selected entry in devices list
        [isSelected, store, iter] = this._deviceSelection.get_selected();

        if (!isSelected) {
            return;
        }

        let cardidSel = store.get_value(iter, 1);
        let profileidSel = store.get_value(iter, 2);

        if (cardid === cardidSel && profileid === profileidSel) {
            this._widgets.btnAddDevice.set_sensitive(true);
        }
    }

    /**
     * Callback for notebook tab selection.
     */
    onSwitchPage(page, pageNum) {
        if (this._hasCards || this._cardsWarningShown) {
            return;
        }

        if (pageNum === 1) {
            return;
        }

        this._showMessage(__('Error retrieving card details'),
            __('Helper script did not return valid card data.'));

        this._cardsWarningShown = true;
    }

    /**
     * Toggle event for quickswitch switches.
     */
    onQuickswitchToggled(widget, path) {
        let active = !widget.active;
        let [success, iter] = this._pinned.get_iter_from_string(path);
        if (!success) {
            return;
        }
        this._pinned.set_value(iter, 2, active);
        this._storePinned();
    }

    /**
     * Toggle event for display switches.
     */
    onDisplayToggled(widget, path) {
        let active = !widget.active;
        let [success, iter] = this._pinned.get_iter_from_string(path);
        if (!success) {
            return;
        }
        this._pinned.set_value(iter, 3, active);
        this._storePinned();
    }

    /**
     * Determines whether selection allows to enable the remove button.
     */
    onPinnedSelectionChanged(selection) {
        if (selection.count_selected_rows() > 0) {
            this._widgets.btnRemoveDevice.set_sensitive(true);
        } else {
            this._widgets.btnRemoveDevice.set_sensitive(false);
        }
    }

    /**
     * Selection event for devices, before selection is set.
     */
    onDeviceSelection(selection, model, path) {
        return path && path.get_depth() >= 2;
    }

    /**
     * Determines whether selection allows to enable the add button.
     */
    onDeviceSelectionChanged(selection) {
        this._widgets.btnAddDevice.set_sensitive(false);

        if (selection.count_selected_rows() <= 0) {
            return;
        }

        let [success, store, iter] = selection.get_selected();

        if (!success) {
            return;
        }

        let cardid = store.get_value(iter, 1);
        let profileid = store.get_value(iter, 2);

        if (this._cards[cardid].profiles[profileid].pinned) {
            // don't allow pinning of already pinned profiles
            return;
        }

        this._widgets.btnAddDevice.set_sensitive(true);
    }



    _init() {
        super._init();

        ExtensionUtils.initTranslations();

        this._widgets = {};
        this._widgetSettings = [];

        this._settings = new Settings();

        this._builder = new Gtk.Builder();
        this._builder.set_scope(this);
    }

    buildWidget() {
        this._builder.add_from_file(Utils.getExtensionPath('prefs.ui'));

        this._tabs = this._builder.get_object('tabs');

        for (const [id, setting] of Object.entries(WIDGETS)) {
            const widget = this._builder.get_object(id);

            this._widgets[id]        = widget;
            this._widgetSettings[id] = setting;

            if (!setting) {
                continue;
            }

            if (widget instanceof Gtk.ComboBox
                || widget instanceof Gtk.ComboBoxText
            ) {
                widget.set_active(this._settings.get_enum(setting));
            } else if (widget instanceof Gtk.Switch) {
                widget.set_active(this._settings.get_boolean(setting));
            }
        }

        this._widgets.txtProfileSwitch.set_text(this._settings.get_array(SETTING.profile_switcher_hotkey)[0] || '');

        this._deviceSelection = this._widgets.treeDevices.get_selection();
        this._deviceSelection.set_select_function(this.onDeviceSelection.bind(this));
        this._deviceSelection.connect('changed', this.onDeviceSelectionChanged.bind(this));

        this._pinnedSelection = this._widgets.treePinned.get_selection();
        this._pinnedSelection.connect('changed', this.onPinnedSelectionChanged.bind(this));

        this._devices = this._builder.get_object('storeDevices');
        this._pinned  = this._builder.get_object('storePinned');

        (async () => {
            await this._initCards();
            this._populatePinned();
        })();

        this.onPositionChanged(this._widgets.cmbPosition);

        return this._tabs;
    }


    /**
     * Initializes the content of the cards / profiles selection tree.
     */
    async _initCards() {
        this._cards = {};
        let cards = await PaHelper.getCards();

        let details = this._widgets.swShowDetailedSliders.active;

        for (let k in cards) {
            let card = cards[k];
            let row = this._devices.append(null);
            this._devices.set(row, [0, 1, 2], [card.description, '', '']);

            let profiles = {};

            for (let p in card.profiles) {
                let profile = card.profiles[p];

                if (profile.name === 'off' || profile.available === false) {
                    continue;
                }

                let invalid = false;

                let test = profile.name.split('+');
                for (let parts of test) {
                    let [part] = parts.split(':', 1);
                    // profiles containing 'input' won't be accepted by Gvc
                    if (part === 'input') {
                        invalid = true;
                        break;
                    }
                }

                if (invalid) {
                    continue;
                }

                let profiletext = profile.description;
                if (details) {
                    profiletext += `\n${profile.name}`;
                }

                this._devices.set(this._devices.append(row), [0, 1, 2],
                    [profiletext, card.name, profile.name]);

                profiles[profile.name] = {
                    description: profile.description,
                    pinned: false
                };
            }

            this._hasCards = true;

            this._cards[card.name] = {
                description: card.description,
                profiles: profiles
            };
        }

        this._widgets.treeDevices.expand_all();
    }

    /**
     * Updates the content of the selection list with all values from the
     * settings key.
     */
    _populatePinned() {
        let pinned = this._settings.get_array(SETTING.pinned_profiles);
        this._pinned.clear();

        for (let item of pinned) {
            if (!item) {
                continue;
            }
            let entry = null;
            try {
                entry = JSON.parse(item);
            } catch (e) {
                Log.error('Preferences', '_populatePinned', e);
            }
            if (!entry || !entry.card || !entry.profile) {
                continue;
            }

            let card = this._cards[entry.card];
            if (!card) {
                continue;
            }

            let profile = card.profiles[entry.profile];
            if (!profile) {
                continue;
            }

            profile.pinned = true;

            this._pinned.set(this._pinned.append(), [0, 1, 2, 3, 4, 5], [
                card.description, profile.description,
                entry.switcher, entry.show,
                entry.card, entry.profile
            ]);
        }
    }

    /**
     * Returns the entries in the selection list as array of strings.
     */
    _storePinned() {
        let values = [];
        let [success, iter] = this._pinned.get_iter_first();

        while (iter && success) {
            values.push(JSON.stringify({
                card: this._pinned.get_value(iter, 4),
                profile: this._pinned.get_value(iter, 5),
                switcher: this._pinned.get_value(iter, 2),
                show: this._pinned.get_value(iter, 3)
            }));
            success = this._pinned.iter_next(iter);
        }

        this._settings.set_array(SETTING.pinned_profiles, values);
    }


    /**
     * Shows a message dialog bound to the parent window.
     */
    _showMessage(title, text, type = 'WARNING') {
        let dialog = new Gtk.MessageDialog({
            text:           title,
            secondary_text: text,
            message_type:   Gtk.MessageType[type],
            buttons:        Gtk.ButtonsType.OK,
            transient_for:  this._tabs.get_root(),
            modal:          true,
        });

        dialog.connect('response', () => {
            dialog.destroy();
        });

        dialog.show();
    }
});


function init() {
    preferences = new Preferences();
}

function buildPrefsWidget() {
    return preferences.buildWidget();
}


================================================
FILE: shell-volume-mixer@derhofbauer.at/prefs.ui
================================================
<?xml version="1.0" encoding="UTF-8"?>
<interface domain="gnome-shell-extensions-shell-volume-mixer">
  <requires lib="gtk" version="4.0"/>
  <object class="GtkTreeStore" id="storeDevices">
    <columns>
      <column type="gchararray"/>
      <column type="gchararray"/>
      <column type="gchararray"/>
    </columns>
  </object>
  <object class="GtkListStore" id="storePinned">
    <columns>
      <column type="gchararray"/>
      <column type="gchararray"/>
      <column type="gboolean"/>
      <column type="gboolean"/>
      <column type="gchararray"/>
      <column type="gchararray"/>
    </columns>
  </object>
  <object class="GtkNotebook" id="tabs">
    <signal name="switch-page" handler="onSwitchPage" swapped="no"/>
    <child>
      <object class="GtkNotebookPage">
        <property name="child">
          <object class="GtkGrid" id="gridSettings">
            <property name="can_focus">1</property>
            <property name="margin-start">20</property>
            <property name="margin-end">20</property>
            <property name="margin_top">20</property>
            <property name="margin_bottom">20</property>
            <property name="row_spacing">10</property>
            <property name="column_spacing">20</property>
            <child>
              <object class="GtkLabel" id="lblPosition">
                <property name="can_focus">0</property>
                <property name="halign">end</property>
                <property name="valign">baseline</property>
                <property name="label" translatable="yes">Position of volume mixer</property>
                <layout>
                  <property name="column">0</property>
                  <property name="row">0</property>
                </layout>
              </object>
            </child>
            <child>
              <object class="GtkComboBoxText" id="cmbPosition">
                <property name="can_focus">0</property>
                <property name="valign">center</property>
                <property name="active">0</property>
                <signal name="changed" handler="onPositionChanged" swapped="no"/>
                <items>
                  <item id="0" translatable="yes">Status Menu</item>
                  <item id="1" translatable="yes">Left</item>
                  <item id="2" translatable="yes">Center</item>
                  <item id="3" translatable="yes">Right</item>
                </items>
                <layout>
                  <property name="column">1</property>
                  <property name="row">0</property>
                </layout>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="lblRemoveOriginal">
                <property name="can_focus">0</property>
                <property name="halign">end</property>
                <property name="valign">baseline</property>
                <property name="label" translatable="yes">Remove original slider</property>
                <layout>
                  <property name="column">0</property>
         
Download .txt
gitextract_5h_ew3wj/

├── .editorconfig
├── .eslintrc.js
├── .gitattributes
├── .github/
│   └── workflows/
│       └── linting.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── tmux.conf
│   └── tools.sh
├── crowdin.yml
├── package.json
├── pubkey.asc
├── shell-volume-mixer@derhofbauer.at/
│   ├── extension.js
│   ├── lib/
│   │   ├── dbus/
│   │   │   └── dbus.js
│   │   ├── main.js
│   │   ├── menu/
│   │   │   ├── indicator.js
│   │   │   └── menu.js
│   │   ├── settings.js
│   │   ├── utils/
│   │   │   ├── cards.js
│   │   │   ├── eventBroker.js
│   │   │   ├── eventHandlerDelegate.js
│   │   │   ├── hotkeys.js
│   │   │   ├── log.js
│   │   │   ├── paHelper.js
│   │   │   ├── process.js
│   │   │   ├── string.js
│   │   │   └── utils.js
│   │   ├── volume/
│   │   │   ├── mixer.js
│   │   │   └── profiles.js
│   │   └── widget/
│   │       ├── floatingLabel.js
│   │       ├── menuItem.js
│   │       ├── panelButton.js
│   │       ├── percentageLabel.js
│   │       ├── slider.js
│   │       └── volume.js
│   ├── locale/
│   │   ├── cs/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── it/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── nl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── pl/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── pt_BR/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   ├── ru/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── gnome-shell-extensions-shell-volume-mixer.mo
│   │   │       └── gnome-shell-extensions-shell-volume-mixer.po
│   │   └── translations.pot
│   ├── metadata.json
│   ├── pautils/
│   │   ├── lib/
│   │   │   ├── __init__.py
│   │   │   ├── cards.py
│   │   │   ├── libpulse.py
│   │   │   ├── log.py
│   │   │   ├── pulseaudio.py
│   │   │   └── sinks.py
│   │   └── query.py
│   ├── prefs.js
│   ├── prefs.ui
│   ├── schemas/
│   │   ├── gschemas.compiled
│   │   └── org.gnome.shell.extensions.shell-volume-mixer.gschema.xml
│   └── stylesheet.css
└── styles.scss
Download .txt
SYMBOL INDEX (259 symbols across 29 files)

FILE: shell-volume-mixer@derhofbauer.at/extension.js
  function init (line 17) | function init() {

FILE: shell-volume-mixer@derhofbauer.at/lib/dbus/dbus.js
  constant DBUS_INTERFACE (line 17) | const DBUS_INTERFACE =
  method constructor (line 52) | constructor(commandHandler = {}) {
  method init (line 73) | init() {
  method destroy (line 80) | destroy() {
  method reload (line 90) | reload() {
  method debug (line 98) | debug(what) {
  method help (line 125) | help() {

FILE: shell-volume-mixer@derhofbauer.at/lib/main.js
  constant DEFAULT_INDICATOR_POS (line 24) | const DEFAULT_INDICATOR_POS = 4;
  method constructor (line 30) | constructor() {
  method _settings (line 45) | get _settings() {
  method _menu (line 57) | get _menu() {
  method enable (line 61) | enable() {
  method disable (line 92) | disable() {
  method _hideOriginal (line 132) | _hideOriginal() {
  method _showOriginal (line 142) | _showOriginal() {
  method _replaceOriginal (line 152) | _replaceOriginal() {
  method _getCurrentIndicatorPosition (line 178) | _getCurrentIndicatorPosition() {
  method _addPanelButton (line 196) | _addPanelButton(position) {
  method _reloadExtension (line 218) | _reloadExtension() {
  method _enableDebugging (line 228) | _enableDebugging() {
  method _emitDebugEvent (line 252) | _emitDebugEvent(eventName) {

FILE: shell-volume-mixer@derhofbauer.at/lib/menu/indicator.js
  method _init (line 41) | _init(mixer, options = {}) {
  method updateOutputIcon (line 89) | updateOutputIcon() {
  method updateInputIcon (line 100) | updateInputIcon() {
  method _handleScrollEvent (line 108) | _handleScrollEvent(type, event) {
  method _handleButtonPress (line 112) | _handleButtonPress(type, event) {
  method destroy (line 126) | destroy() {

FILE: shell-volume-mixer@derhofbauer.at/lib/menu/menu.js
  class VolumeMenuExtension (line 39) | class VolumeMenuExtension extends PopupMenu.PopupMenuSection {}
  method constructor (line 61) | constructor(mixer, options = {}) {
  method _init (line 94) | _init(mixer, options) {
  method open (line 156) | open(animate) {
  method close (line 161) | close(animate) {
  method _addSeparator (line 179) | _addSeparator() {
  method _addStream (line 188) | _addStream(control, stream) {
  method _addInputStream (line 243) | _addInputStream(stream, control, options) {
  method _addOutputStream (line 265) | _addOutputStream(stream, control, options) {
  method _addSliderStream (line 287) | _addSliderStream(stream, control, options) {
  method _streamAdded (line 301) | _streamAdded(control, id) {
  method _streamRemoved (line 306) | _streamRemoved(control, id) {
  method _streamChanged (line 322) | _streamChanged(control, id) {
  method _onControlStateChanged (line 334) | _onControlStateChanged() {
  method _readInput (line 347) | _readInput() {
  method _debugStreams (line 355) | _debugStreams(callback) {
  method getInputVisible (line 385) | getInputVisible() {

FILE: shell-volume-mixer@derhofbauer.at/lib/settings.js
  constant SETTINGS_SCHEMA (line 21) | const SETTINGS_SCHEMA = 'org.gnome.shell.extensions.shell-volume-mixer';
  constant SIGNALS (line 47) | const SIGNALS = {};
  constant GSETTINGS (line 48) | const GSETTINGS = {};
  method settings (line 54) | get settings() {
  method constructor (line 62) | constructor(schema) {
  method connect (line 79) | connect(signal, callback, allowMultiple = false) {
  method connectChanged (line 97) | connectChanged(callback) {
  method disconnectAll (line 104) | disconnectAll() {
  method disconnect (line 113) | disconnect(signal) {
  method disconnectById (line 126) | disconnectById(signalId) {
  method get_string (line 143) | get_string(key) {
  method set_string (line 150) | set_string(key, value) {
  method get_int (line 157) | get_int(key) {
  method set_int (line 164) | set_int(key, value) {
  method get_boolean (line 171) | get_boolean(key) {
  method set_boolean (line 178) | set_boolean(key, value) {
  method get_enum (line 185) | get_enum(key) {
  method set_enum (line 192) | set_enum(key, value) {
  method get_array (line 199) | get_array(key) {
  method set_array (line 206) | set_array(key, value) {
  function cleanup (line 215) | function cleanup() {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js
  constant NULL_CARD (line 29) | const NULL_CARD = 4294967295;
  method constructor (line 67) | constructor(control) {
  method _controlIsReady (line 89) | _controlIsReady() {
  method _onStateChanged (line 96) | _onStateChanged(/* control, state */) {
  method _init (line 109) | async _init() {
  method _initCards (line 132) | async _initCards() {
  method _getCardDetails (line 157) | async _getCardDetails() {
  method _addGvcCard (line 180) | _addGvcCard(paCard, card) {
  method _onCardAdded (line 200) | async _onCardAdded(control, index) {
  method _onCardRemoved (line 234) | _onCardRemoved(control, index) {
  method get (line 254) | async get(index, forceRefresh = false) {
  method getByName (line 289) | async getByName(name) {
  method streamMatchesPaCard (line 308) | streamMatchesPaCard(stream, paCard, profileName) {
  method destroy (line 343) | destroy() {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/eventBroker.js
  method constructor (line 19) | constructor() {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/eventHandlerDelegate.js
  method _signals (line 23) | get _signals() {
  method eventHandlerDelegate (line 34) | set eventHandlerDelegate(delegate) {
  method eventHandlerDelegate (line 38) | get eventHandlerDelegate() {
  method connect (line 50) | connect(target, signal, callback, initial) {
  method disconnect (line 77) | disconnect(target, signal) {
  method disconnectAll (line 97) | disconnectAll() {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/hotkeys.js
  constant BINDINGS (line 17) | const BINDINGS = {};
  method constructor (line 21) | constructor(settings) {
  method bind (line 32) | bind(setting, callback) {
  method unbind (line 55) | unbind(setting) {
  method unbindAll (line 66) | unbindAll() {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/log.js
  constant LOG_PREAMBLE (line 14) | const LOG_PREAMBLE = Extension.metadata.uuid || 'Shell Volume Mixer';
  function l (line 25) | function l() {
  function info (line 34) | function info() {
  function d (line 46) | function d(object, maxDepth = 1) {
  function error (line 61) | function error(module, context, error) {
  function dump (line 93) | function dump(object, maxDepth) {
  function _dumpObject (line 110) | function _dumpObject(object, maxDepth = 8, currDepth = 0) {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/paHelper.js
  constant PYTHON_HELPER_PATH (line 16) | const PYTHON_HELPER_PATH = 'pautils/query.py';
  constant TYPE_CARDS (line 17) | const TYPE_CARDS = 'cards';
  constant PYTHON (line 19) | let PYTHON;
  function findPython (line 21) | async function findPython() {
  function execHelper (line 52) | async function execHelper(type, index = undefined) {
  function getCards (line 121) | async function getCards() {
  function getCardByIndex (line 131) | async function getCardByIndex(index) {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/process.js
  function execAsync (line 19) | async function execAsync(command) {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/string.js
  function parseVersionString (line 18) | function parseVersionString(string) {
  function versionGreaterOrEqual (line 37) | function versionGreaterOrEqual(string) {

FILE: shell-volume-mixer@derhofbauer.at/lib/utils/utils.js
  function getExtensionPath (line 19) | function getExtensionPath(subpath) {
  function mixin (line 37) | function mixin(target, source, keepExisting = false) {

FILE: shell-volume-mixer@derhofbauer.at/lib/volume/mixer.js
  method constructor (line 31) | constructor() {
  method control (line 59) | get control() {
  method cards (line 66) | get cards() {
  method defaultSink (line 73) | get defaultSink() {
  method getVolume (line 82) | getVolume() {
  method destroy (line 97) | destroy() {
  method _bindProfileHotkey (line 107) | _bindProfileHotkey() {
  method _disconnectSink (line 121) | _disconnectSink() {
  method _connectSink (line 130) | _connectSink() {
  method _onVolumeUpdate (line 139) | _onVolumeUpdate() {
  method _updateDefaultSink (line 151) | async _updateDefaultSink(stream) {
  method _onStateChanged (line 195) | _onStateChanged(control, state) {
  method _onDefaultSinkChanged (line 210) | _onDefaultSinkChanged(control, id) {
  method _switchProfile (line 220) | async _switchProfile() {
  method _showNotification (line 279) | _showNotification(text) {

FILE: shell-volume-mixer@derhofbauer.at/lib/volume/profiles.js
  method constructor (line 26) | constructor(settings) {
  method count (line 37) | count() {
  method setCurrent (line 44) | setCurrent(paCard) {
  method next (line 62) | next() {
  method _parseProfilesSetting (line 82) | _parseProfilesSetting(data) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/floatingLabel.js
  method constructor (line 23) | constructor() {
  method text (line 30) | get text() {
  method text (line 34) | set text(text) {
  method size (line 38) | get size() {
  method show (line 42) | show(x, y, animate) {
  method hide (line 70) | hide(animate) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/menuItem.js
  class BaseMenuItem (line 21) | class BaseMenuItem
    method _makeOrnamentLabel (line 23) | _makeOrnamentLabel() {
    method _makeItemLine (line 30) | _makeItemLine() {
    method _prepareMenuItem (line 37) | _prepareMenuItem() {
  method _init (line 79) | _init() {
  method vfunc_button_release_event (line 102) | vfunc_button_release_event(event) {
  method vfunc_key_press_event (line 113) | vfunc_key_press_event(event) {
  method addMenuItem (line 123) | addMenuItem(item) {
  method _init (line 139) | _init(params = {}) {
  method addDetails (line 148) | addDetails(label) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/panelButton.js
  method _init (line 28) | _init(mixer, options = {}) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/percentageLabel.js
  method _init (line 18) | _init(mixer) {
  method _setText (line 40) | _setText(percent) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/slider.js
  method startDragging (line 23) | startDragging(event) {

FILE: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js
  class StreamSliderExtension (line 31) | class StreamSliderExtension {}
  class OutputStreamSliderExtension (line 38) | class OutputStreamSliderExtension extends StreamSliderExtension {}
  method constructor (line 55) | constructor(control, options = {}) {
  method _init (line 76) | _init(options) {
  method _onKeyPress (line 153) | _onKeyPress(actor, event) {
  method _onButtonPress (line 157) | _onButtonPress(actor, event) {
  method refresh (line 167) | refresh() {
  method _updateSliderIcon (line 172) | _updateSliderIcon() {
  method _connectStream (line 182) | _connectStream(stream) {
  method _updateLabel (line 187) | _updateLabel() {
  method _showVolumeInfo (line 191) | _showVolumeInfo(position) {
  method hideVolumeInfo (line 227) | hideVolumeInfo() {
  method toggleMute (line 237) | toggleMute() {
  method _onDestroy (line 243) | _onDestroy() {
  method _init (line 260) | _init(options) {
  method _addSettingsItem (line 279) | _addSettingsItem() {
  method addOutputSlider (line 286) | addOutputSlider(slider) {
  method _onButtonPress (line 293) | _onButtonPress(actor, event) {
  method _updateLabel (line 301) | _updateLabel() {
  method scroll (line 305) | scroll(event) {
  method constructor (line 322) | constructor() {
  method setInputStream (line 330) | setInputStream(inputSlider) {
  method addSlider (line 335) | addSlider(slider, pos) {
  method refresh (line 343) | refresh() {
  method _init (line 357) | _init(options) {
  method _onButtonPress (line 375) | _onButtonPress(actor, event) {
  method _onKeyPress (line 384) | _onKeyPress(actor, event) {
  method _updateLabel (line 394) | _updateLabel() {
  method _setAsDefault (line 417) | _setAsDefault() {
  method _onDefaultSinkUpdated (line 421) | _onDefaultSinkUpdated(/* stream */) {
  method _updateVisibility (line 425) | _updateVisibility(forceRefresh = true) {
  method _shouldBeVisible (line 452) | _shouldBeVisible() {
  method _shouldBeVisibleByPort (line 466) | async _shouldBeVisibleByPort(forceRefresh = true) {
  method _init (line 511) | _init(options) {
  method _updateLabel (line 518) | _updateLabel() {
  method _updateLabel (line 529) | _updateLabel() {
  method _init (line 554) | _init(options) {
  method _connectStream (line 572) | _connectStream(stream) {
  method _maybeShowInput (line 577) | _maybeShowInput() {
  method _shouldBeVisible (line 587) | _shouldBeVisible() {
  method isVisible (line 591) | isVisible() {
  method _updateLabel (line 595) | _updateLabel() {
  method _updateSliderIcon (line 599) | _updateSliderIcon() {
  method _onDestroy (line 609) | _onDestroy() {

FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/cards.py
  class Cards (line 22) | class Cards(Pulseaudio):
    method build_callback (line 23) | def build_callback(self):
    method get_by_index (line 26) | def get_by_index(self, index, callback):
    method get_by_name (line 29) | def get_by_name(self, name, callback):
    method get_all (line 32) | def get_all(self, callback):
    method cb_data (line 35) | def cb_data(self, pa_card):

FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/libpulse.py
  class mainloop (line 26) | class mainloop(Structure):
  class mainloop_api (line 29) | class mainloop_api(Structure):
  class spawn_api (line 32) | class spawn_api(Structure):
  class context (line 35) | class context(Structure):
  class operation (line 38) | class operation(Structure):
  class proplist (line 41) | class proplist(Structure):
  class card_profile_info (line 44) | class card_profile_info(Structure):
  class card_profile_info2 (line 53) | class card_profile_info2(Structure):
  class card_port_info (line 63) | class card_port_info(Structure):
  class card_info (line 77) | class card_info(Structure):
  class sink_port_info (line 93) | class sink_port_info(Structure):
  class format_info (line 103) | class format_info(Structure):
  class sample_spec (line 109) | class sample_spec(Structure):
  class pa_channel_map (line 116) | class pa_channel_map(Structure):
  class pa_cvolume (line 122) | class pa_cvolume(Structure):
  class sink_info (line 128) | class sink_info(Structure):

FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/log.py
  function debug (line 23) | def debug(*msg):

FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/pulseaudio.py
  class Pulseaudio (line 24) | class Pulseaudio:
    method __init__ (line 38) | def __init__(self):
    method __enter__ (line 47) | def __enter__(self):
    method __exit__ (line 51) | def __exit__(self, exc_type, exc_value, traceback):
    method context_notify_cb (line 56) | def context_notify_cb(self, context, userdata):
    method get_info (line 63) | def get_info(self, index=None, name=None):
    method pa_cb (line 111) | def pa_cb(self, context, struct, eol, user_data):
    method build_callback (line 131) | def build_callback(self):
    method get_by_index (line 135) | def get_by_index(self, index, callback):
    method get_by_name (line 139) | def get_by_name(self, name, callback):
    method get_all (line 143) | def get_all(self, callback):
    method cb_data (line 147) | def cb_data(self, data):

FILE: shell-volume-mixer@derhofbauer.at/pautils/lib/sinks.py
  class Sinks (line 22) | class Sinks(Pulseaudio):
    method build_callback (line 23) | def build_callback(self):
    method get_by_index (line 26) | def get_by_index(self, index, callback):
    method get_by_name (line 29) | def get_by_name(self, name, callback):
    method get_all (line 32) | def get_all(self, callback):
    method cb_data (line 35) | def cb_data(self, pa_sink):

FILE: shell-volume-mixer@derhofbauer.at/prefs.js
  constant WIDGETS (line 24) | const WIDGETS = {
  method vfunc_create_closure (line 45) | vfunc_create_closure(builder, handlerName, flags, connectObject) {
  method _getId (line 55) | _getId(object) {
  method _getSetting (line 65) | _getSetting(object) {
  method toggleBoolean (line 75) | toggleBoolean(object) {
  method onPositionChanged (line 92) | onPositionChanged(cmbPosition) {
  method onProfileSwitchChanged (line 114) | onProfileSwitchChanged(widget) {
  method onAddDevice (line 145) | onAddDevice(widget) {
  method onRemoveDevice (line 177) | onRemoveDevice() {
  method onSwitchPage (line 210) | onSwitchPage(page, pageNum) {
  method onQuickswitchToggled (line 228) | onQuickswitchToggled(widget, path) {
  method onDisplayToggled (line 241) | onDisplayToggled(widget, path) {
  method onPinnedSelectionChanged (line 254) | onPinnedSelectionChanged(selection) {
  method onDeviceSelection (line 265) | onDeviceSelection(selection, model, path) {
  method onDeviceSelectionChanged (line 272) | onDeviceSelectionChanged(selection) {
  method _init (line 298) | _init() {
  method buildWidget (line 312) | buildWidget() {
  method _initCards (line 362) | async _initCards() {
  method _populatePinned (line 427) | _populatePinned() {
  method _storePinned (line 468) | _storePinned() {
  method _showMessage (line 489) | _showMessage(title, text, type = 'WARNING') {
  function init (line 508) | function init() {
  function buildPrefsWidget (line 512) | function buildPrefsWidget() {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (237K chars).
[
  {
    "path": ".editorconfig",
    "chars": 178,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1300,
    "preview": "module.exports = {\n    'env': {\n        'es6': true,\n    },\n    'globals': {\n        '_':               false,\n        '"
  },
  {
    "path": ".gitattributes",
    "chars": 67,
    "preview": "shell-volume-mixer@derhofbauer.at/schemas/gschemas.compiled binary\n"
  },
  {
    "path": ".github/workflows/linting.yml",
    "chars": 701,
    "preview": "name: Linting\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n\njobs:\n\n  checks:\n    runs-on: ubuntu-latest\n\n    step"
  },
  {
    "path": ".gitignore",
    "chars": 65,
    "preview": "node_modules/\nbuild/\n\n*.swp\n*.pyc\n*.*~\n\nshell-volume-mixer-*.zip\n"
  },
  {
    "path": ".gitmodules",
    "chars": 115,
    "preview": "[submodule \"gnome-shell-sass\"]\n\tpath = gnome-shell-sass\n\turl = https://gitlab.gnome.org/GNOME/gnome-shell-sass.git\n"
  },
  {
    "path": "LICENSE",
    "chars": 18092,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "Makefile",
    "chars": 1919,
    "preview": "VERSION = 42.0\nEXTENSION = shell-volume-mixer@derhofbauer.at\n\nSRCDIR = $(EXTENSION)\nBUILDDIR = build/\nPACKAGE = shell-vo"
  },
  {
    "path": "README.md",
    "chars": 2022,
    "preview": "GNOME Shell Volume Mixer\n========================\n\n[![Linting](https://github.com/aleho/gnome-shell-volume-mixer/actions"
  },
  {
    "path": "bin/tmux.conf",
    "chars": 16,
    "preview": "set -g mouse on\n"
  },
  {
    "path": "bin/tools.sh",
    "chars": 4380,
    "preview": "#!/usr/bin/env bash\nset -e\n\n\nWINDOW_MODE=1280x800\nX11=0\nNAME=\"\"\nTMUX_SESSION=\"gsvm\"\n\n\nfunction print_help() {\n    cat <<"
  },
  {
    "path": "crowdin.yml",
    "chars": 207,
    "preview": "files:\n  - source: /shell-volume-mixer@derhofbauer.at/locale/translations.pot\n    translation: /shell-volume-mixer@derho"
  },
  {
    "path": "package.json",
    "chars": 351,
    "preview": "{\n  \"license\": \"GPL-2.0-only\",\n  \"repository\": \"https://github.com/aleho/gnome-shell-volume-mixer\",\n  \"devDependencies\":"
  },
  {
    "path": "pubkey.asc",
    "chars": 2192,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFfd0QwBCADiW+vORNE/bGD7p8eUVIJ00REornZ4AfPMBb2gNN/AFG4MnN/c\nuSgL1Tlbe7YuNaUMm"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/extension.js",
    "chars": 376,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Advanced mixer extension.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/*"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/dbus/dbus.js",
    "chars": 3070,
    "preview": "/**\n * Shell Volume Mixer\n *\n * D-Bus command module.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exp"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/main.js",
    "chars": 6686,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Main extension setup.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exp"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/menu/indicator.js",
    "chars": 4412,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Customized indicator using Volume Menu.\n *\n * @author Alexander Hofbauer <alex@derhofbau"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/menu/menu.js",
    "chars": 10441,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Volume menu item implementation.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/settings.js",
    "chars": 5377,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Convenience class to wrap Gio.Settings.\n *\n * @author Alexander Hofbauer <alex@derhofbau"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/cards.js",
    "chars": 8707,
    "preview": "/**\n * Shell Volume Mixer\n *\n * PulseAudio card retrieval utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer."
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/eventBroker.js",
    "chars": 627,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Global event broker singleton.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n *"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/eventHandlerDelegate.js",
    "chars": 2641,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Event handler mixin.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* expo"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/hotkeys.js",
    "chars": 1793,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Hotkeys.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Hotkeys"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/log.js",
    "chars": 4509,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Logging.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported l, d, i"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/paHelper.js",
    "chars": 3229,
    "preview": "/**\n * Shell Volume Mixer\n *\n * PulseAudio helper.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* export"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/process.js",
    "chars": 1100,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Process utility.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/string.js",
    "chars": 986,
    "preview": "/**\n * Shell Volume Mixer\n *\n * String utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exporte"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/utils.js",
    "chars": 1059,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported getCa"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/volume/mixer.js",
    "chars": 7649,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Mixer class wrapping the mixer control.\n *\n * @author Alexander Hofbauer <alex@derhofbau"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/volume/profiles.js",
    "chars": 2287,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Card / profile settings cycling.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/floatingLabel.js",
    "chars": 1865,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Floating label.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported "
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/menuItem.js",
    "chars": 3925,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Menu items.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Mast"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/panelButton.js",
    "chars": 1096,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Stand-alone menu panel button.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n *"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/percentageLabel.js",
    "chars": 1076,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Percentage label.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exporte"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/slider.js",
    "chars": 608,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Sliders.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported VolumeS"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/volume.js",
    "chars": 16837,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Volume widgets.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported "
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/cs/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3889,
    "preview": "# Czech translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2017\n# This file is distributed under the same license"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/de/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3987,
    "preview": "# German translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2014\n# This file is distributed under the same licens"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/it/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3686,
    "preview": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Gianvito Cavasoli <gianvito@gmx.it>"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/nl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3769,
    "preview": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Heimen Stoffels <vistausss@outlook."
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/pl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3849,
    "preview": "# Polish translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2017\n# This file is distributed under the same licens"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/pt_BR/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3960,
    "preview": "# Brazilian Portuguese translation for GNOME Shell Volume Mixer.\n# This file is distributed under the same license as th"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/ru/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "chars": 3913,
    "preview": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Alexandr K <Alexandr1322@yandex.ua>"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/translations.pot",
    "chars": 2562,
    "preview": "#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"\"\n\n#: shell-volume-mixer"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/metadata.json",
    "chars": 767,
    "preview": "{\n    \"version\": 9999,\n    \"uuid\": \"shell-volume-mixer@derhofbauer.at\",\n    \"name\": \"Volume Mixer\",\n    \"description\": \""
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/cards.py",
    "chars": 3790,
    "preview": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This p"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/libpulse.py",
    "chars": 7772,
    "preview": "# This library was generated through introspection of\n#     /usr/include/pulse/introspect.h and\n#     /usr/include/pulse"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/log.py",
    "chars": 888,
    "preview": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This p"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/pulseaudio.py",
    "chars": 4552,
    "preview": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This p"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/sinks.py",
    "chars": 3104,
    "preview": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This p"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/query.py",
    "chars": 1773,
    "preview": "#!/usr/bin/env python3\n#\n# Usage: query.py [cards|sinks] [index or name, omit for all data]\n#\n# Output is either a JSON "
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/prefs.js",
    "chars": 14126,
    "preview": "/**\n * Shell Volume Mixer\n *\n * Preferences widget.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* expor"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/prefs.ui",
    "chars": 23189,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<interface domain=\"gnome-shell-extensions-shell-volume-mixer\">\n  <requires lib=\"g"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/schemas/org.gnome.shell.extensions.shell-volume-mixer.gschema.xml",
    "chars": 3491,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE schemalist SYSTEM \"/usr/share/glib-2.0/schemas/gschema.dtd\">\n<schemalis"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/stylesheet.css",
    "chars": 3679,
    "preview": "/* This stylesheet is generated, DO NOT EDIT */\n/* Copyright 2009, 2015 Red Hat, Inc.\n *\n * Portions adapted from Mx's d"
  },
  {
    "path": "styles.scss",
    "chars": 3414,
    "preview": "$variant: 'light';\n@import \"gnome-shell-sass/colors\";\n@import \"gnome-shell-sass/drawing\";\n@import \"gnome-shell-sass/comm"
  }
]

// ... and 8 more files (download for full content)

About this extraction

This page contains the full source code of the aleho/gnome-shell-volume-mixer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (216.9 KB), approximately 55.6k tokens, and a symbol index with 259 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!