[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n    'env': {\n        'es6': true,\n    },\n    'globals': {\n        '_':               false,\n        'ARGV':            false,\n        'C_':              false,\n        'Debugger':        false,\n        'GjsFileImporter': false,\n        'global':          false,\n        'imports':         false,\n        'InternalError':   false,\n        'Iterator':        false,\n        'log':             false,\n        'logError':        false,\n        'N_':              false,\n        'ngettext':        false,\n        'print':           false,\n        'printerr':        false,\n        'StopIteration':   false,\n        'uneval':          false,\n        'window':          false,\n    },\n    'extends': 'eslint:recommended',\n    'parserOptions': {\n        'ecmaVersion': 2018,\n    },\n    'rules': {\n        'indent': [\n            'error',\n            4,\n            { 'SwitchCase': 1 },\n        ],\n        'linebreak-style': [\n            'error',\n            'unix',\n        ],\n        'quotes': [\n            'error',\n            'single',\n            { 'allowTemplateLiterals': true },\n        ],\n        'semi': [\n            'error',\n            'always',\n        ],\n        'no-unused-vars': [\n            2,\n            { 'vars': 'local', 'args': 'after-used' },\n        ],\n    },\n};\n"
  },
  {
    "path": ".gitattributes",
    "content": "shell-volume-mixer@derhofbauer.at/schemas/gschemas.compiled binary\n"
  },
  {
    "path": ".github/workflows/linting.yml",
    "content": "name: Linting\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n\njobs:\n\n  checks:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Setup Node.js 14\n        uses: actions/setup-node@v2\n        with:\n          node-version: '14'\n\n      - name: Cache node modules\n        id: node-cache\n        uses: actions/cache@v2\n        with:\n          path: node_modules\n          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-node-\n\n      - name: npm install\n        if: steps.node-cache.outputs.cache-hit != 'true'\n        run: npm install\n\n      - name: Run ESLint\n        run: npm run eslint\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\nbuild/\n\n*.swp\n*.pyc\n*.*~\n\nshell-volume-mixer-*.zip\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"gnome-shell-sass\"]\n\tpath = gnome-shell-sass\n\turl = https://gitlab.gnome.org/GNOME/gnome-shell-sass.git\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "Makefile",
    "content": "VERSION = 42.0\nEXTENSION = shell-volume-mixer@derhofbauer.at\n\nSRCDIR = $(EXTENSION)\nBUILDDIR = build/\nPACKAGE = shell-volume-mixer-$(VERSION).zip\n\nFILES = LICENSE README.md\n\nLOCALE_DIR = $(SRCDIR)/locale\nLOCALES_SRC = $(foreach dir,$(LOCALE_DIR),$(wildcard $(dir)/*/*/*.po))\nLOCALES = $(patsubst %.po,%.mo,$(LOCALES_SRC))\n\nSOURCES = \\\n\tpautils/lib/*.py \\\n\tpautils/query.py \\\n\tlib/** \\\n\t*.js \\\n\tprefs.ui \\\n\tstylesheet.css \\\n\t$(LOCALES:$(SRCDIR)/%=%) \\\n\t$(GSCHEMA) $(SCHEMA_COMP)\n\nI18N = \\\n\t*.js \\\n\tlib/**/*.js \\\n\tprefs.ui\n\nSCHEMA_COMP = schemas/gschemas.compiled\nGSCHEMA = schemas/org.gnome.shell.extensions.shell-volume-mixer.gschema.xml\n\nSRCFILES = $(addprefix $(SRCDIR)/, $(SOURCES) $(GSCHEMA) $(GSCHEMA_COMP))\n\n\ndist: clean build check package\nbuild: install-deps i18n stylesheet.css\npackage: $(PACKAGE)\n\nprepare:\n\tmkdir -p $(BUILDDIR)\n\ninstall-deps:\n\tnpm install\n\tgit submodule update --init\n\n$(SRCDIR)/$(SCHEMA_COMP): $(SRCDIR)/$(GSCHEMA)\n\tglib-compile-schemas --targetdir=$(SRCDIR)/schemas $(SRCDIR)/schemas\n\n$(PACKAGE): metadata.json $(SRCFILES) $(FILES)\n\tcd $(SRCDIR) && zip -r ../$(PACKAGE) $(SOURCES)\n\tzip $(PACKAGE) $(FILES)\n\tcd $(BUILDDIR) && zip ../$(PACKAGE) *\n\ni18n: $(LOCALES_SRC)\n\t@xgettext \\\n\t\t--keyword --keyword=__ \\\n\t\t--omit-header \\\n\t\t--default-domain=$(EXTENSION) \\\n\t\t--from-code=UTF-8 \\\n\t\t--output=$(LOCALE_DIR)/translations.pot \\\n\t\t $(wildcard $(addprefix $(SRCDIR)/, $(I18N)))\n\nmetadata.json: prepare\n\tcat $(addprefix $(SRCDIR)/, metadata.json) | grep -v '\"version\":' > $(BUILDDIR)/metadata.json\n\nstylesheet.css:\n\tnpm run build\n\t# remove harmful content produced by gnome-shell-sass\n\tsed -i '/\\/\\*\\sGlobal\\sValues\\s\\*\\//,/\\/\\*\\sGeneral\\sTypography\\s\\*/d' $(SRCDIR)/stylesheet.css\n\ncheck:\n\tnpm run eslint\n\nclean:\n\t@test ! -d \"$(BUILDDIR)\" || rm -rf $(BUILDDIR)\n\t@test ! -f \"$(SRCDIR)/$(SCHEMA_COMP)\" || rm $(SRCDIR)/$(SCHEMA_COMP)\n\t@test ! -f \"$(PACKAGE)\" || rm $(PACKAGE)\n\n\n.PHONY: clean i18n\n"
  },
  {
    "path": "README.md",
    "content": "GNOME Shell Volume Mixer\n========================\n\n[![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)\n\nShell Volume Mixer is an extension for GNOME Shell allowing separate\nconfiguration of PulseAudio devices and output switches. It features a profile\nswitcher to quickly switch between pinned profiles and devices.\n\nMiddle mouse click on an indicator or a slider mutes the selected stream.\n\nIndicators and streams are also scrollable,\n\n\n<img src=\"/screenshot_1.png\" alt=\"Outputs menu\" width=\"40%\"><img alt=\"Inputs menu\" src=\"/screenshot_2.png\" width=\"40%\">\n\n\nRequirements\n------------\n\n- PulseAudio (for retrieval of card details)\n- gettext (for building of language files)\n- nodejs / npm (styles and linting)\n- glib2 bin (schema compilation)\n\n\nInstallation\n------------\n\n```\n$ make\n```\n\nThat's it. Add the resulting archive via GNOME Tweak Tool (extensions tab) or\ncopy it's content manually to\n\".~/.local/share/gnome-shell/extensions/shell-volume-mixer@derhofbauer.at\".\n\n\nVolume Steps\n------------\n\n\nGNOME Settings Daemon (GSD) hardcodes the step for each key press of volume keys\nto 6% of maximum. While this might be OK for most people, some would prefer a\nconfigurable setting. There's a bug in GNOME's tracker which, according to the\ncomments by developers, won't ever get fixed in a way that could allow\nconfigurable volume\nsteps<sup>[[1]](https://bugzilla.gnome.org/show_bug.cgi?id=650371)</sup>.\n\nShell Volume Mixer tried to grab GSD's hotkeys to provide configurable steps\nfor sliders and media keys in the past, but at some point this stopped working.\n\nGNOME's current solution to the problem is Shift + Key, i.e. hold down the shift\nbutton to switch to a 2% step.\n\n\nAcknowledgments\n---------------\n\nThis is a fork of AdvancedVolumeMixer by [Harry Karvonen](https://github.com/Hatell)\n(git://repo.or.cz/AdvancedVolumeMixer.git).\nMany thanks go out to him for his initial work.\n"
  },
  {
    "path": "bin/tmux.conf",
    "content": "set -g mouse on\n"
  },
  {
    "path": "bin/tools.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\n\nWINDOW_MODE=1280x800\nX11=0\nNAME=\"\"\nTMUX_SESSION=\"gsvm\"\n\n\nfunction print_help() {\n    cat <<- EOT\n\t\tShell Volume Mixer dev toolkit\n\n\t\t    *) test  Runs the extension in a nested session\n\t\t         --mode  Sets the nested session window size (default: 1280x800)\n\t\t         --x11   Runs a X11/xorg session (defaulting to wayland)\n\n\t\t    *) lg  Toggles Looking Glass via D-Bus\n\n\t\t    *) command  Executes a D-Bus call to debug the running extension\n\t\t                (use \"help\" or see D-Bus interface for methods)\n\n\t\t    *) add-sink  Adds a virtual sink via PulseAudio\n\t\t         --name  Virtual sink name\n\n\t\t    *) debug  Enables or disables debugging [true, false]\nEOT\n}\n\n###\n\nCOMMAND=$1\nif [[ -z $COMMAND ]]; then\n    echo \"Command required\"\n    echo \"\"\n    print_help\n    exit 1\nfi\n\nshift\n\nOPTIONS=$(getopt -n \"$0\" -o h --long help,mode:,name:,x11 -- \"$@\")\n\nif [[ $? -ne 0 ]]; then\n    print_help\n    exit 1\nfi\n\neval set -- \"$OPTIONS\"\n\nwhile true; do\n    case $1 in\n        --mode)\n            WINDOW_MODE=$2\n            shift\n            ;;\n\n        --name)\n            NAME=$2\n            shift\n            ;;\n\n        --x11)\n            X11=1\n            ;;\n\n\n        -h|--help)\n            print_help\n            exit\n            ;;\n\n        --)\n            shift\n            break\n            ;;\n        *)\n            print_help\n            exit 1\n            ;;\n    esac\n\n    shift\ndone\n\n\n###\n\n\nfunction add_virtual_sink() {\n    local props\n    if [[ -n $NAME ]]; then\n        props=\"sink_properties=device.description=$NAME\"\n    fi\n\n    local pipewire_opts=\"object.linger=1 media.class=Audio/Sink\"\n\n    pactl load-module module-null-sink sink_name=svm-${NAME:-virtual-sink} $pipewire_opts \"$props\" \\\n        || pactl load-module module-null-sink sink_name=svm-${NAME:-virtual-sink} \"$props\"\n}\n\nfunction toggle_looking_glass() {\n    gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval 'Main.createLookingGlass() && Main.lookingGlass.toggle();'\n}\n\n\nfunction enable_debugging() {\n    local enable=\"$1\"\n\n    if [[ $enable == 1 || $enable == true ]]; then\n        echo \"Enabling debug\"\n        enable=\"true\"\n    else\n        echo \"Disabling debug\"\n        enable=\"false\"\n    fi\n\n    dconf write /org/gnome/shell/extensions/shell-volume-mixer/debug $enable\n}\n\nfunction has_tmux_session() {\n    set +e\n    if tmux has-session -t $TMUX_SESSION 2>/dev/null; then\n        echo 1\n    else\n        echo 0\n    fi\n    set -e\n}\n\nfunction run_nested_session() {\n    local manager=\"\"\n    if [[ $X11 == 1 ]]; then\n        manager=\"--x11\"\n    fi\n\n    if [[ $(has_tmux_session) == 1 ]]; then\n        tmux kill-session -t $TMUX_SESSION\n    fi\n\n    dbus-run-session -- tmux -f bin/tmux.conf new-session -s $TMUX_SESSION \"bin/tools.sh run-test-session --mode=${WINDOW_MODE} $manager\"\n}\n\nfunction run_test_session() {\n    local manager\n    if [[ $X11 == 1 ]]; then\n        manager=\"--x11\"\n    else\n        manager=\"--wayland\"\n    fi\n\n    set -x\n    export MUTTER_DEBUG_NUM_DUMMY_MONITORS=1\n    export MUTTER_DEBUG_DUMMY_MODE_SPECS=\"${WINDOW_MODE}\"\n    gnome-shell --nested $manager\n}\n\nfunction dbus_command() {\n    local command=\"$1\"\n    local args=\"$2\"\n\n    if [[ -z $command ]]; then\n        echo \"Command needed\"\n        exit 1\n    fi\n\n    local command=(\n        gdbus call\n        --session\n        --dest \"org.gnome.Shell\"\n        --object-path \"/at/derhofbauer/shell/VolumeMixer\"\n        --method \"at.derhofbauer.shell.VolumeMixer.$command\"\n    )\n\n    if [[ -n $args ]]; then\n        command+=(\"$args\")\n    fi\n\n    if [[ $(has_tmux_session) == 1 ]]; then\n        local buffer=\"_cmd_output\"\n\n        if [[ -f $buffer ]]; then\n            rm $buffer\n        fi\n\n        echo \"Running command in test session\"\n        tmux new-window -n dbus-command -t $TMUX_SESSION: \"${command[@]}\" \\; pipe-pane \"cat > $buffer\"\n\n        sleep .3\n        cat $buffer\n        rm $buffer\n\n    else\n        \"${command[@]}\"\n    fi\n}\n\n\n###\n\n\ncase $COMMAND in\n    add-sink)\n        add_virtual_sink\n        ;;\n\n    lg)\n        toggle_looking_glass\n        ;;\n\n    test)\n        run_nested_session\n        ;;\n\n    run-test-session)\n        run_test_session\n        ;;\n\n    command)\n        dbus_command \"${@}\"\n        ;;\n\n    debug)\n        enable_debugging \"$1\"\n        ;;\n\n    *)\n        print_help\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\n  - source: /shell-volume-mixer@derhofbauer.at/locale/translations.pot\n    translation: /shell-volume-mixer@derhofbauer.at/locale/%osx_locale%/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"license\": \"GPL-2.0-only\",\n  \"repository\": \"https://github.com/aleho/gnome-shell-volume-mixer\",\n  \"devDependencies\": {\n    \"eslint\": \"^8.13.0\",\n    \"sass\": \"^1.50.0\"\n  },\n  \"scripts\": {\n    \"eslint\": \"eslint shell-volume-mixer@derhofbauer.at\",\n    \"build\": \"sass --no-source-map styles.scss shell-volume-mixer@derhofbauer.at/stylesheet.css\"\n  }\n}\n"
  },
  {
    "path": "pubkey.asc",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBFfd0QwBCADiW+vORNE/bGD7p8eUVIJ00REornZ4AfPMBb2gNN/AFG4MnN/c\nuSgL1Tlbe7YuNaUMmtNWx/c41k7PYab8IciD/at0KBr/0hTRezk3a6aJlKfxibLF\nhiQsCXkgg4SzTcovDnZSsNf3nlybDlY3824w1vNhyhvMYSG05aQkufeJadlN+66t\nvgjfUeQHh2cVC3QalK19iq1h324DQsoYQfcj6xNMBHqIeWitqsM0yqxP6poxDeYT\nMBBmXdnOMkJQMwNfo+2eMl9k35c0IhDoIvTYYbxqDedrQIe1Qyl1NYxEC7eo1Iaz\n8KilmMSt/iLgky4WA/z32Wl44GAq3UwEUX7pABEBAAG0I0FsZXggSG9mYmF1ZXIg\nPGFsZXhAZGVyaG9mYmF1ZXIuYXQ+iQE9BBMBCAAnBQJX3dEMAhsDBQkJZgGABQsJ\nCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEGrPIVMXiTpBOa4H/ihUAzQzgbpH1TAH\np2VZ1l5hgFgtYnQ3BvzISatrIG3NsQ9QxHxdZCoNXsIveTOPoEqlex3TQMVOY2uD\nLOTYtk9Ul0cSYUU1VgFd9PVVQuThxLct7xTDzTpsp0oYdwlVc3Szg+ggAJE8Bu4p\nGhn9p4pcNgIVZVwpLCcP5S1NODcGpU0MChyTl+UrGmrVpjoHZix88/eewijbTOrs\nopjxHIlaIxOMJJ4Fip4jLHkoYOL5rQCaLDgv+ZiAaWz3l8mjGsceG4aUt22ljY1l\nvKwyfT5QgR5kJOG/D81yjZZ1AJMYmlnhEHLORtvFhsYKop/fsAK+AG5ArtLqtz+G\nOR42dvu0KEFsZXhhbmRlciBIb2ZiYXVlciA8YWxleEBkZXJob2ZiYXVlci5hdD6J\nAT0EEwEIACcFAlfd02wCGwMFCQlmAYAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AA\nCgkQas8hUxeJOkFOfggAuAAWwXVfwMGkpxV7ujt51SOZf1WV5AJAxMumIeqsvj69\n9LHphnpeO6fm0nRmYYoDTLM1gXMVXfRGu/ZFz2S0xTNJZOkiWaHT/+DpNvnFLLoc\n5XMnZouNvrMJuu/DFH++BA7t3aXbJ6TE5J2gYF1xtI8UrAygnohQoZ3dtIlBy2Kf\nohZZcRiKYZRdWtl7ERpPJc19Pwe12Kza875YQHCKc2dkl0ya5AbDEHXfCzMGPNxF\nRFgGcTTPt4nHsCCEE6kEPuRAfFsptMSCQKB1Z55sccdPdGyoKuy5yHwxU9L89+nj\nlvzKd1swBJn2in7nPWWJcUa4AkwxmxzYvleqCMRJg7kBDQRX3dEMAQgAuV7wsfIQ\nqjkxUvBIMVwocTmnUerOuwar4WfzJJx/oO49wd3jFto/WcJmJiJ67STM2Vi64iCa\n8OzBudpYt7/9FZCFogJE2JUofjLFM/z9USgovOOVF7eeVggKchGRM7SeG3k/tK+s\nbobt1F62Lk2WhYFbSDUr/WrtdnFNXSD4tCkpz1dBvd2codvTbCmuhl/vppJaRO8u\nm3y9I11YXAL8yfiLYOGLyhBcOohWKxIZhBYEZhKdFUOjMnnYUOZmhRh6g7an/3FW\nwVYvper6WMta0LdK8obTNC1koCaBQQIZISNwSF2/CIDiMwxL32txazZI7GqGHIwG\ntm69OGqZ0FhraQARAQABiQElBBgBCAAPBQJX3dEMAhsMBQkJZgGAAAoJEGrPIVMX\niTpB0AoIAIUzzhKY53lrQzgQiyOQCkVYYyBY1kRKqSBYgWZbfItNTuYkpYSbB9tU\nU7CtDLErS0K5xlrYMjQvDHEiskra4IGCvRDuNMu275VL7+hJHqjnXhoLWSzHXGrG\nK4mZVX2QqD010KQZLC1aFHXnPM+boGVId/Lauu4i3RmUpZiLyvaodRaB0yfJm8WV\n2sLbVEBBWYOWGGGt6OSuO/O6pyDpU6Grzy61X7donHMzkgfBPaqE13feVs3p+GwE\nlfVc5Sr3v71RqctkHl1vLICjn8RAjb701B6ZQGVXH8XuqEZxEYuRdF32SP9G7XHF\n6fZSkjd0dNecjp3b9tVRIxTnuEdkHIA=\n=nkLe\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/extension.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Advanced mixer extension.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported init */\n\nconst ExtensionUtils = imports.misc.extensionUtils;\nconst Lib = ExtensionUtils.getCurrentExtension().imports.lib;\n\nconst { Extension } = Lib.main;\n\n\nfunction init() {\n    ExtensionUtils.initTranslations();\n\n    return new Extension();\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/dbus/dbus.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * D-Bus command module.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Dbus */\n\nconst { DBus, DBusExportedObject } = imports.gi.Gio;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\n\nconst Log = Lib.utils.log;\n\n\nconst DBUS_INTERFACE =\n'<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\" \\\n  \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\"> \\\n<node name=\"/at/derhofbauer/shell/VolumeMixer\"> \\\n    <interface name=\"at.derhofbauer.shell.VolumeMixer\"> \\\n        <method name=\"debug\"> \\\n            <arg name=\"what\" type=\"s\" direction=\"in\" /> \\\n            <arg name=\"result\" type=\"s\" direction=\"out\" /> \\\n        </method> \\\n        <method name=\"reload\"/> \\\n        <method name=\"help\"> \\\n            <arg name=\"result\" type=\"s\" direction=\"out\" /> \\\n        </method> \\\n    </interface> \\\n</node>';\n\n\nlet instance;\n\n/** @typedef {{\n *   reloadExtension: function,\n *   debugCards: {function():String},\n *   debugStreams: {function():String},\n *   debugEvents: {function():String},\n * }} CommandHandler\n */\n\n/**\n * @property {CommandHandler} _handler\n */\nvar Dbus = class {\n    /**\n     * @param {CommandHandler} commandHandler\n     * @returns {*}\n     */\n    constructor(commandHandler = {}) {\n        if (instance) {\n            return instance;\n        }\n\n        instance = this;\n\n        this._handler = new Proxy(commandHandler, {\n            get: function (target, prop) {\n                if (prop in target && target[prop]) {\n                    Log.info(`Got command \"${prop}\"`);\n\n                    return Reflect.get(...arguments);\n                }\n\n                Log.info(`Unimplemented command \"${prop}\"`);\n            }\n        });\n    }\n\n\n    init() {\n        this._dbus = DBusExportedObject.wrapJSObject(DBUS_INTERFACE, this);\n        this._dbus.export(DBus.session, '/at/derhofbauer/shell/VolumeMixer');\n\n        Log.info('D-Bus command interface enabled');\n    }\n\n    destroy() {\n        if (this._dbus) {\n            this._dbus.unexport();\n            this._dbus = null;\n            Log.info('D-Bus command interface disabled');\n        }\n    }\n\n    //region dbus methods\n\n    reload() {\n        this._handler.reload();\n    }\n\n    /**\n     * @param {string} what to debug\n     * @return {string}\n     */\n    debug(what) {\n        let result = '';\n\n        switch (what) {\n            case 'cards':\n                result = this._handler.debugCards();\n                break;\n\n            case 'streams':\n                result = this._handler.debugStreams();\n                break;\n\n            case 'events':\n                result = this._handler.debugEvents();\n                break;\n\n            case '':\n                Log.error(`D-Bus: command missing`);\n                break;\n\n            default:\n                Log.error(`D-Bus: unknown command ${what}`);\n        }\n\n        return result;\n    }\n\n    help() {\n        return 'Commands: debug (cards, streams, events), reload';\n    }\n\n    //endregion dbus methods\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/main.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Main extension setup.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Extension */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Main = imports.ui.main;\nconst PopupMenu = imports.ui.popupMenu;\n\nconst { Dbus } = Lib.dbus.dbus;\nconst { EventBroker } = Lib.utils.eventBroker;\nconst { Mixer } = Lib.volume.mixer;\nconst { Indicator } = Lib.menu.indicator;\nconst { PanelButton } = Lib.widget.panelButton;\nconst { Settings, SETTING, cleanup: settingsCleanup } = Lib.settings;\n\nconst Log = Lib.utils.log;\n\nconst DEFAULT_INDICATOR_POS = 4;\n\n\nlet instance;\n\nvar Extension = class {\n    constructor() {\n        if (instance) {\n            return instance;\n        }\n\n        instance = this;\n\n        // save the original volume reference in aggregate menu.\n        this._orgVolume = this._menu._volume;\n    }\n\n    /**\n     * Settings instance.\n     * @private\n     */\n    get _settings() {\n        if (!this._settingsInstance) {\n            this._settingsInstance = new Settings();\n        }\n\n        return this._settingsInstance;\n    }\n\n    /**\n     * The shell aggregate menu instance.\n     * @private\n     */\n    get _menu() {\n        return Main.panel.statusArea.aggregateMenu;\n    }\n\n    enable() {\n        this._events = new EventBroker();\n        this._events.connect('extension-disable', () => {\n            this.disable();\n        });\n        this._events.connect('extension-enable', () => {\n            this.enable();\n        });\n        this._events.connect('extension-reload', () => {\n            this._reloadExtension();\n        });\n\n        this._settings.connectChanged(() => {\n            this._reloadExtension();\n        });\n\n        this._mixer = new Mixer();\n\n        let position = this._settings.get_enum(SETTING.position);\n\n        if (position === SETTING.position_at.menu) {\n            this._replaceOriginal();\n        } else {\n            this._addPanelButton(position);\n        }\n\n        if (this._settings.get_boolean(SETTING.debug) === true) {\n            this._enableDebugging();\n        }\n    }\n\n    disable() {\n        instance = null;\n\n        this._menu._volume = this._orgVolume;\n        this._showOriginal();\n\n        if (this._events) {\n            this._events.disconnectAll();\n            this._events = null;\n        }\n\n        if (this._mixer) {\n            this._mixer.destroy();\n            this._mixer = null;\n        }\n\n        if (this._indicator) {\n            this._menu._indicators.remove_actor(this._indicator);\n            this._indicator.destroy();\n            this._indicator = null;\n        }\n\n        if (this._panelButton) {\n            this._panelButton.destroy();\n            this._panelButton = null;\n        }\n\n        if (this._dbus) {\n            this._dbus.destroy();\n            this._dbus = null;\n        }\n\n        this._settingsInstance = null;\n        settingsCleanup();\n    }\n\n    /**\n     * Hides the original menu item and icon.\n     * @private\n     */\n    _hideOriginal() {\n        this._orgVolume._volumeMenu.actor.hide();\n        this._orgVolume._primaryIndicator.hide();\n        this._menu._indicators.remove_child(this._orgVolume);\n    }\n\n    /**\n     * Restores the original menu item and icon.\n     * @private\n     */\n    _showOriginal() {\n        this._menu._indicators.insert_child_at_index(this._orgVolume, this._indicatorPos || DEFAULT_INDICATOR_POS);\n        this._orgVolume._volumeMenu.actor.show();\n        this._orgVolume._primaryIndicator.show();\n    }\n\n    /**\n     * Replaces the current indicator and menu.\n     * @private\n     */\n    _replaceOriginal() {\n        this._indicator = new Indicator(this._mixer, {\n            separator: false,\n            showPercentageLabel: this._settings.get_boolean(SETTING.show_percentage_label),\n            menuClass: 'svm-integrated-menu',\n        });\n\n        // get current indicator position\n        this._indicatorPos = this._getCurrentIndicatorPosition();\n        this._hideOriginal();\n\n        // add our own indicator and menu\n        this._menu._volume = this._indicator;\n        this._menu._indicators.insert_child_at_index(this._indicator, this._indicatorPos);\n        this._menu.menu.addMenuItem(this._indicator.menu, 0);\n\n        this._menu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(), 1);\n\n        // on disable/enable we won't get a stream-changed event, so trigger it here to be safe\n        this._indicator.updateOutputIcon();\n    }\n\n    /**\n     * Find the current volume icon's position.\n     * @private\n     */\n    _getCurrentIndicatorPosition() {\n        let indicators = this._menu._indicators.get_children();\n        let indicatorPos = DEFAULT_INDICATOR_POS;\n\n        for (let i = 0; i < indicators.length; i++) {\n            if (this._orgVolume === indicators[i]) {\n                indicatorPos = i;\n                break;\n            }\n        }\n\n        return indicatorPos;\n    }\n\n    /**\n     * Inits this extension as stand-alone button.\n     * @private\n     */\n    _addPanelButton(position) {\n        if (this._settings.get_boolean(SETTING.remove_original)) {\n            this._hideOriginal();\n        }\n\n        this._panelButton = new PanelButton(this._mixer, {\n            showPercentageLabel: this._settings.get_boolean(SETTING.show_percentage_label),\n        });\n\n        if (position === SETTING.position_at.left) {\n            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton, 999, 'left');\n        } else if (position === SETTING.position_at.center) {\n            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton, 999, 'center');\n        } else {\n            Main.panel.addToStatusArea('ShellVolumeMenu', this._panelButton);\n        }\n    }\n\n    /**\n     * Reloads the extension be disabling / enabling it.\n     * @private\n     */\n    _reloadExtension() {\n        this.disable();\n        this.enable();\n    }\n\n    /**\n     * Enables the debugging and messages.\n     *\n     * @private\n     */\n    _enableDebugging() {\n        Log.verbose = true;\n\n        this._dbus = new Dbus({\n            debugCards: () => {\n                return this._emitDebugEvent('debug-cards');\n            },\n\n            debugStreams: () => {\n                return this._emitDebugEvent('debug-streams');\n            },\n\n            debugEvents: () => {\n                return this._emitDebugEvent('debug-events');\n            },\n\n            reload: () => {\n                this._reloadExtension();\n            },\n        });\n\n        this._dbus.init();\n    }\n\n    _emitDebugEvent(eventName) {\n        this._events.emit(eventName, result => {\n            Log.info(result);\n        });\n\n        return 'OK';\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/menu/indicator.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Customized indicator using Volume Menu.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Indicator */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst { GObject, Clutter } = imports.gi;\nconst PanelMenu = imports.ui.panelMenu;\nconst Volume = imports.ui.status.volume;\n\nconst { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;\nconst { Menu } = Lib.menu.menu;\nconst { PercentageLabel } = Lib.widget.percentageLabel;\nconst Utils = Lib.utils.utils;\n\n\nconst VolumeType = {\n    OUTPUT: 0,\n    INPUT: 1,\n};\n\n\n/**\n * Custom indicator with dropdown menu.\n * Copied from status/volume.js\n *\n * @copypaste from Volume.Indicator: Needs code to not initialize volume menu and all its events.\n */\nvar Indicator = GObject.registerClass(class Indicator extends PanelMenu.SystemIndicator\n{\n    /**\n     * @param {Mixer} mixer\n     * @param {Object} options\n     * @private\n     */\n    _init(mixer, options = {}) {\n        super._init();\n\n        this._primaryIndicator = this._addIndicator();\n\n        if (options.showPercentageLabel) {\n            this._percentageLabel = new PercentageLabel(mixer);\n            this.add_actor(this._percentageLabel);\n\n            this._percentageLabel.reactive = true;\n            this.connect(this._percentageLabel, 'scroll-event',\n                (actor, event) => this._handleScrollEvent(VolumeType.OUTPUT, event));\n            this.connect(this._percentageLabel, 'button-press-event',\n                (actor, event) => this._handleButtonPress(VolumeType.OUTPUT, event));\n        }\n\n        this._inputIndicator = this._addIndicator();\n\n        this._primaryIndicator.reactive = true;\n        this._inputIndicator.reactive = true;\n\n        this.connect(this._primaryIndicator, 'scroll-event',\n            (actor, event) => this._handleScrollEvent(VolumeType.OUTPUT, event));\n        this.connect(this._inputIndicator, 'scroll-event',\n            (actor, event) => this._handleScrollEvent(VolumeType.INPUT, event));\n\n        this.connect(this._primaryIndicator, 'button-press-event',\n            (actor, event) => this._handleButtonPress(VolumeType.OUTPUT, event));\n        this.connect(this._inputIndicator, 'button-press-event',\n            (actor, event) => this._handleButtonPress(VolumeType.INPUT, event));\n\n        this._control = mixer.control;\n        this._volumeMenu = new Menu(mixer, options);\n        this._volumeMenu.actor.add_style_class_name(options.menuClass);\n\n        this.connect(this._volumeMenu, 'output-icon-changed', this.updateOutputIcon.bind(this));\n\n        this._inputIndicator.visible = this._volumeMenu.getInputVisible();\n        this.connect(this._volumeMenu, 'input-visible-changed', () => {\n            this._inputIndicator.visible = this._volumeMenu.getInputVisible();\n        });\n        this.connect(this._volumeMenu, 'input-icon-changed', this.updateInputIcon.bind(this));\n        // initial call to get an icon (especially for \"show-always\" setups)\n        this.updateInputIcon();\n\n        this.menu.addMenuItem(this._volumeMenu);\n    }\n\n    updateOutputIcon() {\n        let icon = this._volumeMenu.getIcon(VolumeType.OUTPUT);\n\n        if (icon) {\n            this._primaryIndicator.icon_name = icon;\n            this._primaryIndicator.visible = true;\n        } else {\n            this._primaryIndicator.visible = false;\n        }\n    }\n\n    updateInputIcon() {\n        let icon = this._volumeMenu.getIcon(VolumeType.INPUT);\n\n        if (icon !== null) {\n            this._inputIndicator.icon_name = icon;\n        }\n    }\n\n    _handleScrollEvent(type, event) {\n        return Volume.Indicator.prototype._handleScrollEvent.apply(this, [type, event]);\n    }\n\n    _handleButtonPress(type, event) {\n        if (event.get_button() === 2) {\n            if (type === VolumeType.OUTPUT) {\n                this._volumeMenu._output.toggleMute();\n            } else {\n                this._volumeMenu._input.toggleMute();\n            }\n\n            return Clutter.EVENT_STOP;\n        }\n\n        return Clutter.EVENT_PROPAGATE;\n    }\n\n    destroy() {\n        this.disconnectAll();\n\n        if (this.menu) {\n            this.menu.destroy();\n            this.menu = null;\n        }\n\n        if (this._percentageLabel) {\n            this._percentageLabel.destroy();\n            this._percentageLabel = null;\n        }\n    }\n});\n\nUtils.mixin(Indicator, EventHandlerDelegate);\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/menu/menu.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Volume menu item implementation.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Menu */\n\nconst Gvc = imports.gi.Gvc;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst PopupMenu = imports.ui.popupMenu;\nconst Volume = imports.ui.status.volume;\n\nconst { EventBroker } = Lib.utils.eventBroker;\nconst { Settings, SETTING } = Lib.settings;\nconst Log = Lib.utils.log;\nconst Utils = Lib.utils.utils;\nconst {\n    AggregatedInput,\n    EventsSlider,\n    InputSlider,\n    InputStreamSlider,\n    MasterSlider,\n    OutputSlider,\n} = Lib.widget.volume;\n\n\nconst VolumeType = {\n    OUTPUT: 0,\n    INPUT: 1,\n};\n\n\n/**\n * Extension of Volume.VolumeMenu without its constructor().\n */\nclass VolumeMenuExtension extends PopupMenu.PopupMenuSection {}\nUtils.mixin(VolumeMenuExtension, Volume.VolumeMenu);\n\n\n/** @typedef {{\n *   mixer: Mixer,\n *   detailed: Boolean,\n *   symbolicIcons: Boolean,\n *   stream: Gcv.MixerStream,\n * }} sliderOptions\n */\n\n\n/**\n * Our own ui.status.VolumeMenu implementation.\n */\nvar Menu = class extends VolumeMenuExtension\n{\n    /**\n     * @param {Mixer} mixer\n     * @param {Object} options\n     */\n    constructor(mixer, options = {}) {\n        super();\n\n        this._events = new EventBroker();\n        this._settings = new Settings();\n\n        this.options = {\n            detailed: this._settings.get_boolean(SETTING.show_detailed_sliders),\n            systemSounds: this._settings.get_boolean(SETTING.show_system_sounds),\n            virtualStreams: this._settings.get_boolean(SETTING.show_virtual_streams),\n            symbolicIcons: this._settings.get_boolean(SETTING.use_symbolic_icons),\n            alwaysShowInputStreams: this._settings.get_boolean(SETTING.always_show_input_streams)\n        };\n\n        this.actor.add_style_class_name('svm-menu');\n\n        // master-menu items\n        this._outputs = {};\n        // input-menu items\n        this._inputs = {};\n        // menu items (all other menu items)\n        this._items = {};\n\n        this._mixer = mixer;\n        this._control = mixer.control;\n\n        this._init(mixer, options);\n\n        this._events.connect('debug-streams', (event, callback) => {\n            this._debugStreams(callback);\n        });\n    }\n\n    _init(mixer, options) {\n        const signals = {\n            'state-changed': this._onControlStateChanged,\n            'default-sink-changed': this._readOutput,\n            'default-source-changed': this._readInput,\n            'stream-added': this._streamAdded,\n            'stream-removed': this._streamRemoved,\n            'stream-changed': this._streamChanged\n        };\n\n        for (let name in signals) {\n            try {\n                mixer.connect(name, signals[name].bind(this));\n            } catch (exception) {\n                Log.info(`Could not connect to signal ${name} -`, exception);\n            }\n        }\n\n        this._output = new MasterSlider(this._control, {\n            mixer: mixer,\n            detailed: this.options.detailed,\n            symbolicIcons: this.options.symbolicIcons\n        });\n\n        this._output.connect('stream-updated', () => {\n            this.emit('output-icon-changed');\n        });\n\n\n        this._inputMenu = new AggregatedInput(this._control, {\n            mixer: mixer,\n            detailed: this.options.detailed,\n            symbolicIcons: this.options.symbolicIcons\n        });\n\n\n        this._input = new InputStreamSlider(this._control, {\n            mixer: mixer,\n            showAlways: this.options.alwaysShowInputStreams,\n            detailed: this.options.detailed,\n            symbolicIcons: this.options.symbolicIcons\n        });\n\n        this._input.item.connect('notify::visible', () => {\n            this.emit('input-visible-changed');\n        });\n        this._input.connect('stream-updated', () => {\n            this.emit('input-icon-changed');\n        });\n\n\n        this.addMenuItem(this._output.item, 0);\n        this.addMenuItem(this._inputMenu.item, 2);\n        this._inputMenu.setInputStream(this._input);\n\n        if (options.separator) {\n            this._addSeparator();\n        }\n\n        this._onControlStateChanged();\n    }\n\n    open(animate) {\n        this._output.hideVolumeInfo();\n        super.open(animate);\n    }\n\n    close(animate) {\n        for (let id in this._outputs) {\n            this._outputs[id].hideVolumeInfo();\n        }\n\n        for (let id in this._inputs) {\n            this._inputs[id].hideVolumeInfo();\n        }\n\n        for (let id in this._items) {\n            this._items[id].hideVolumeInfo();\n        }\n\n        this._output.hideVolumeInfo();\n\n        super.close(animate);\n    }\n\n    _addSeparator() {\n        if (this._separator) {\n            this._separator.destroy();\n        }\n\n        this._separator = new PopupMenu.PopupSeparatorMenuItem();\n        this.addMenuItem(this._separator, 3);\n    }\n\n    _addStream(control, stream) {\n        if (stream instanceof Gvc.MixerSource\n            || stream instanceof Gvc.MixerSourceOutput\n        ) {\n            return;\n        }\n\n        const isSystemSound = stream instanceof Gvc.MixerEventRole;\n        const isInputStream = stream instanceof Gvc.MixerSinkInput;\n        const isOutputStream = stream instanceof Gvc.MixerSink;\n\n        if (stream.is_event_stream) {\n            Log.info(`Skipping event stream (${stream.id})`);\n            return;\n        }\n\n        if (stream.is_virtual && !this.options.virtualStreams) {\n            Log.info(`Skipping virtual stream (${stream.id})`);\n            return;\n        }\n\n        if (isSystemSound && !this.options.systemSounds) {\n            Log.info(`Skipping system sound stream (${stream.id})`);\n            return;\n        }\n\n        const options = {\n            mixer: this._mixer,\n            detailed: this.options.detailed,\n            symbolicIcons: this.options.symbolicIcons,\n            stream: stream\n        };\n\n        if (isSystemSound) {\n            this._addSliderStream(stream, control, options);\n\n        } else if (isInputStream) {\n            this._addInputStream(stream, control, options);\n\n        } else if (isOutputStream) {\n            this._addOutputStream(stream, control, options);\n\n        } else {\n            Log.info(`Unhandled stream ${stream.id} (${stream.name} (${stream.constructor.name}))`);\n        }\n    }\n\n    /**\n     * Adds a stream to the multi-input menu.\n     *\n     * @param stream\n     * @param control\n     * @param {sliderOptions} options\n     * @private\n     */\n    _addInputStream(stream, control, options) {\n        if (stream.id in this._inputs) {\n            Log.info(`Not adding already known input stream ${stream.id}:${stream.name}`);\n            return;\n        }\n\n        Log.info(`Adding input stream ${stream.id}:${stream.name}`);\n\n        let slider = new InputSlider(control, options);\n\n        this._inputs[stream.id] = slider;\n        this._inputMenu.addSlider(slider);\n    }\n\n    /**\n     * Adds a stream to the master slider menu.\n     *\n     * @param stream\n     * @param control\n     * @param {sliderOptions} options\n     * @private\n     */\n    _addOutputStream(stream, control, options) {\n        if (stream.id in this._outputs) {\n            Log.info(`Not adding already known output stream ${stream.id}:${stream.name}`);\n            return;\n        }\n\n        Log.info(`Adding output stream ${stream.id}:${stream.name}`);\n\n        let slider = new OutputSlider(control, options);\n\n        this._outputs[stream.id] = slider;\n        this._output.addOutputSlider(slider);\n    }\n\n    /**\n     * Adds an additional slider below master and input slider.\n     *\n     * @param stream\n     * @param control\n     * @param {sliderOptions} options\n     * @private\n     */\n    _addSliderStream(stream, control, options) {\n        if (stream.id in this._items) {\n            Log.info(`Not adding already known stream ${stream.id}:${stream.name}`);\n            return;\n        }\n\n        Log.info(`Adding stream ${stream.id}:${stream.name}`);\n\n        let slider = new EventsSlider(control, options);\n\n        this._items[stream.id] = slider;\n        this.addMenuItem(slider.item, 1);\n    }\n\n    _streamAdded(control, id) {\n        let stream = control.lookup_stream_id(id);\n        this._addStream(control, stream);\n    }\n\n    _streamRemoved(control, id) {\n        if (id in this._items) {\n            this._items[id].item.destroy();\n            delete this._items[id];\n\n        } else if (id in this._outputs) {\n            this._outputs[id].item.destroy();\n            delete this._outputs[id];\n\n        } else if (id in this._inputs) {\n            this._inputs[id].item.destroy();\n            delete this._inputs[id];\n            this._inputMenu.refresh();\n        }\n    }\n\n    _streamChanged(control, id) {\n        if (id in this._items) {\n            this._items[id].refresh();\n\n        } else if (id in this._outputs) {\n            this._outputs[id].refresh();\n\n        } else if (id in this._inputs) {\n            this._inputs[id].refresh();\n        }\n    }\n\n    _onControlStateChanged() {\n        super._onControlStateChanged();\n\n        if (this._control.get_state() !== Gvc.MixerControlState.READY) {\n            return;\n        }\n\n        let streams = this._control.get_streams();\n        for (let stream of streams) {\n            this._addStream(this._control, stream);\n        }\n    }\n\n    _readInput() {\n        if (!this._input)  {\n            return;\n        }\n\n        super._readInput();\n    }\n\n    _debugStreams(callback) {\n        let dump = [];\n\n        if (Object.keys(this._outputs).length) {\n            dump.push('Output Streams:');\n        }\n\n        for (let id in this._outputs) {\n            const stream = this._outputs[id].stream;\n            dump.push(`  ${stream.id} (${stream.name}) (port=${stream.port}) ${stream.description}`);\n\n            const ports = stream.get_ports();\n            for (let p in ports) {\n                const port = ports[p];\n                dump.push(`    ${port.port}`);\n            }\n        }\n\n        if (Object.keys(this._inputs).length) {\n            dump.push('Input Streams:');\n        }\n\n        for (let id in this._inputs) {\n            const stream = this._inputs[id].stream;\n            dump.push(`  ${stream.id} (name=${stream.name}) (port=${stream.port}) ${stream.description}`);\n        }\n\n        callback(dump.join('\\n'));\n    }\n\n    getInputVisible() {\n        return this._input.isVisible();\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/settings.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Convenience class to wrap Gio.Settings.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Settings, cleanup */\n/* exported SOUND_SETTINGS_SCHEMA, ALLOW_AMPLIFIED_VOLUME_KEY, SETTING */\n\nconst ExtensionUtils = imports.misc.extensionUtils;\nconst Extension = ExtensionUtils.getCurrentExtension();\nconst { Gio, GLib } = imports.gi;\nconst Lib = Extension.imports.lib;\n\nconst Log = Lib.utils.log;\nconst Utils = Lib.utils.utils;\n\n\nconst SETTINGS_SCHEMA = 'org.gnome.shell.extensions.shell-volume-mixer';\nvar SOUND_SETTINGS_SCHEMA = 'org.gnome.desktop.sound';\nvar ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent';\n\nvar SETTING = Object.freeze({\n    position:                  'position',\n    remove_original:           'remove-original',\n    show_percentage_label:     'show-percentage-label',\n    show_detailed_sliders:     'show-detailed-sliders',\n    show_system_sounds:        'show-system-sounds',\n    show_virtual_streams:      'show-virtual-streams',\n    always_show_input_streams: 'always-show-input-streams',\n    use_symbolic_icons:        'use-symbolic-icons',\n    profile_switcher_hotkey:   'profile-switcher-hotkey',\n    pinned_profiles:           'pinned-profiles',\n    debug:                     'debug',\n\n    position_at: {\n        menu:   0,\n        left:   1,\n        center: 2,\n        right:  3,\n    },\n});\n\n\nconst SIGNALS = {};\nconst GSETTINGS = {};\n\n\n\nvar Settings = class\n{\n    get settings() {\n        if (!GSETTINGS[this.schema]) {\n            GSETTINGS[this.schema] = ExtensionUtils.getSettings(this.schema);\n        }\n\n        return GSETTINGS[this.schema];\n    }\n\n    constructor(schema) {\n        this.schema = schema || SETTINGS_SCHEMA;\n\n        if (!SIGNALS[this.schema]) {\n            SIGNALS[this.schema] = {};\n        }\n\n        this._signals = SIGNALS[this.schema];\n    }\n\n    /**\n     * Registers a listener for a signal.\n     *\n     * @param {string} signal\n     * @param {function()} callback\n     * @param {boolean} allowMultiple Whether to error out if the setting has already been connected\n     */\n    connect(signal, callback, allowMultiple = false) {\n        if (!allowMultiple && this._signals[signal]) {\n            Log.error('Settings', 'connect', `Signal \"${signal}\" already bound for \"${this.schema}\"`);\n            return false;\n        }\n\n        Log.info(`Connecting to settings change signal \"${signal}\"`);\n        let id = this.settings.connect(signal, callback);\n        this._signals[signal] = id;\n\n        return id;\n    }\n\n    /**\n     * Registers a listener to changed events.\n     *\n     * @param {function()} callback\n     */\n    connectChanged(callback) {\n        this.connect('changed', callback);\n    }\n\n    /**\n     * Disconnects all connected signals.\n     */\n    disconnectAll() {\n        for (let signal in this._signals) {\n            this.disconnect(this._signals[signal]);\n        }\n    }\n\n    /**\n     * Disconnects a signal by name.\n     */\n    disconnect(signal) {\n        if (this._signals[signal]) {\n            this.settings.disconnect(this._signals[signal]);\n            delete this._signals[signal];\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Disconnects a signal by id.\n     */\n    disconnectById(signalId) {\n        for (let name in this._signals) {\n            let id = this._signals[name];\n            if (signalId === id) {\n                this.settings.disconnect(id);\n                delete this._signals[name];\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n\n    /**\n     * Retrieves the value of an 's' type key.\n     */\n    get_string(key) {\n        return this.settings.get_string(key);\n    }\n\n    /**\n     * Sets the value of an 's' type key.\n     */\n    set_string(key, value) {\n        return this.settings.set_string(key, value);\n    }\n\n    /**\n     * Retrieves the value of an 'i' type key.\n     */\n    get_int(key) {\n        return this.settings.get_int(key);\n    }\n\n    /**\n     * Sets the value of an 'i' type key.\n     */\n    set_int(key, value) {\n        return this.settings.set_int(key, value);\n    }\n\n    /**\n     * Retrieves the value of a 'b' type key.\n     */\n    get_boolean(key) {\n        return this.settings.get_boolean(key);\n    }\n\n    /**\n     * Sets the value of a 'b' type key.\n     */\n    set_boolean(key, value) {\n        return this.settings.set_boolean(key, value);\n    }\n\n    /**\n     * Retrieves the value of an enum key.\n     */\n    get_enum(key) {\n        return this.settings.get_enum(key);\n    }\n\n    /**\n     * Sets the value of an enum key.\n     */\n    set_enum(key, value) {\n        return this.settings.set_enum(key, value);\n    }\n\n    /**\n     * Retrieves the value of an array key.\n     */\n    get_array(key) {\n        return this.settings.get_strv(key);\n    }\n\n    /**\n     * Sets the value of an array key.\n     */\n    set_array(key, value) {\n        return this.settings.set_strv(key, value);\n    }\n};\n\n/**\n * Disconnects all signals of all schemas.\n * Used to make sure all there are no connected signals left.\n */\nfunction cleanup() {\n    for (let schema in SIGNALS) {\n        if (!GSETTINGS[schema]) {\n            continue;\n        }\n\n        for (let signal in SIGNALS[schema]) {\n            GSETTINGS[schema].disconnect(SIGNALS[schema][signal]);\n            delete SIGNALS[schema][signal];\n        }\n    }\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/cards.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * PulseAudio card retrieval utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Cards, STREAM_MATCHING */\n\nconst ExtensionUtils = imports.misc.extensionUtils;\nconst Lib = ExtensionUtils.getCurrentExtension().imports.lib;\nconst Main = imports.ui.main;\nconst { Gvc, GLib } = imports.gi;\nconst __ = ExtensionUtils.gettext;\n\nconst { EventBroker } = Lib.utils.eventBroker;\nconst { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;\nconst Log = Lib.utils.log;\nconst Utils = Lib.utils.utils;\nconst PaHelper = Lib.utils.paHelper;\n\n\nvar STREAM_MATCHING = Object.freeze({\n    stream: 1,\n    card:   2,\n});\n\nconst NULL_CARD = 4294967295;\n\n/** @typedef {{\n *   name: String,\n *   description: String,\n *   available: Boolean,\n * }} paProfile\n */\n\n/** @typedef {{\n *   name: String,\n *   description: String,\n *   available: Boolean,\n *   direction: String,\n * }} paPort\n */\n\n/** @typedef {{\n *   index: Number,\n *   alsaCard: Number,\n *   name: String,\n *   description: String,\n *   active_profile: String,\n *   profiles: Object.<string, paProfile>,\n *   ports: Object.<string, paPort>,\n *   fake: Boolean,\n *   card: Object<Gvc.MixerCard>,\n * }} paCard\n */\n\n/**\n * @property {Object.<string, paCard>} _paCards\n * @mixes EventHandlerDelegate\n */\nvar Cards = class {\n    /**\n     * @param {Gvc.MixerControl} control\n     */\n    constructor(control) {\n        this._events = new EventBroker();\n        this._control = control;\n        this.eventHandlerDelegate = control;\n\n        this._initDone = new Promise(resolve => {\n            this._initialized = resolve;\n        });\n\n        this.connect('state-changed', this._onStateChanged.bind(this), () => {\n            return [this._control, this._control.get_state()];\n        });\n\n        this._events.connect('debug-cards', (event, callback) => {\n            callback(Log.dump(this._paCards));\n        });\n    }\n\n    /**\n     * @returns {boolean}\n     * @private\n     */\n    _controlIsReady() {\n        return (this._control && this._control.get_state() === Gvc.MixerControlState.READY);\n    }\n\n    /**\n     * Callback for state changes.\n     */\n    _onStateChanged(/* control, state */) {\n        if (!this._controlIsReady()) {\n            return;\n        }\n\n        // noinspection JSIgnoredPromiseFromCall\n        this._init();\n    }\n\n    /**\n     * @returns {Promise<void>}\n     * @private\n     */\n    async _init() {\n        try {\n            await this._initCards();\n            this._initialized();\n\n        } catch (e) {\n            Log.error('Cards', '_init', e);\n            Main.notifyError('Volume Mixer', __('Querying PulseAudio sound cards failed, disabling extension'));\n            this._events.emit('extension-disable');\n\n            return;\n        }\n\n        this.connect('card-added', this._onCardAdded.bind(this));\n        this.connect('card-removed', this._onCardRemoved.bind(this));\n    }\n\n    /**\n     * Retrieves a list of all cards available, using our Python helper.\n     * Tries to be error-resistant in case the helper cannot deliver.\n     *\n     * @returns {Promise<void>}\n     */\n    async _initCards() {\n        this._paCards = {};\n        this._cardNames = {};\n\n        let cards;\n\n        let retries = 3;\n        do {\n            cards = await this._getCardDetails();\n        } while (!cards\n            && (--retries) > 0\n            && await new Promise(resolve => GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => resolve(true)))\n        );\n\n        if (!cards) {\n            throw Error('Could not retrieve PA card details with Python helper script');\n        }\n\n        this._paCards = cards;\n    }\n\n    /**\n     * @returns {?Object.<string, paCard>}\n     * @private\n     */\n    async _getCardDetails() {\n        const paCards = await PaHelper.getCards();\n\n        if (!paCards || !Object.keys(paCards).length) {\n            return null;\n        }\n\n        if (this._controlIsReady()) {\n            for (let card of this._control.get_cards()) {\n                if (card.index in paCards) {\n                    this._addGvcCard(paCards[card.index], card);\n                }\n            }\n        }\n\n        return paCards;\n    }\n\n    /**\n     * @param {paCard} paCard\n     * @param {Gvc.MixerCard} card\n     * @private\n     */\n    _addGvcCard(paCard, card) {\n        if (!paCard) {\n            Log.error('Cards', '_addGvcCard', 'No paCard passed');\n            return;\n        }\n\n        if (!paCard.name) {\n            Log.error('Cards', '_addGvcCard', 'Invalid paCard data, name missing');\n            return;\n        }\n\n        Log.info('Card added', card.index, paCard.name);\n\n        paCard.card = card;\n        this._cardNames[paCard.name] = card.index;\n    }\n\n    /**\n     * Signal for added cards.\n     */\n    async _onCardAdded(control, index) {\n        // we're actually looking up card.index\n        let card = control.lookup_card_id(index);\n        let paCard = await this.get(index);\n\n        if (!paCard || paCard.fake) {\n            try {\n                paCard = await PaHelper.getCardByIndex(index);\n            } catch (e) {\n                Log.error('Cards', '_onCardAdded', 'Calling Python helper failed');\n            }\n\n            if (!paCard) {\n                Log.error('Cards', '_onCardAdded', 'GVC card not found through Python helper');\n\n                // external script couldn't get card info, fake it\n                paCard = {\n                    // card name (human name) won't be useful, we'll set it anyway\n                    name:     card.name,\n                    index:    index,\n                    profiles: [],\n                    fake:     true\n                };\n            }\n\n            this._paCards[index] = paCard;\n        }\n\n        this._addGvcCard(paCard, card);\n    }\n\n    /**\n     * Signal for removed cards.\n     */\n    _onCardRemoved(control, index) {\n        if (index in this._paCards) {\n            const name = this._paCards[index].name;\n            delete this._cardNames[name];\n            delete this._paCards[index];\n            Log.info('Card removed', index, name);\n\n        } else {\n            Log.info('Untracked card not removed', index);\n        }\n    }\n\n\n    /**\n     * Finds a card by card index.\n     *\n     * @param {number} index\n     * @param {Boolean} forceRefresh\n     * @returns {Promise<?paCard>}\n     */\n    async get(index, forceRefresh = false) {\n        if (index === NULL_CARD) {\n            return null;\n        }\n\n        await this._initDone;\n\n        if (!forceRefresh) {\n            if (index in this._paCards) {\n                return this._paCards[index];\n            }\n\n            Log.info(`Card ${index} not found, querying...`);\n        }\n\n        const paCard = await PaHelper.getCardByIndex(index);\n\n        if (!paCard) {\n            return null;\n        }\n\n        if (this._controlIsReady()) {\n            let card = this._control.lookup_card_id(paCard.index);\n            this._addGvcCard(paCard, card);\n        }\n\n        return paCard;\n    }\n\n    /**\n     * Finds a card by name.\n     *\n     * @param {string} name\n     * @returns {Promise<?paCard>}\n     */\n    async getByName(name) {\n        await this._initDone;\n\n        const index = this._cardNames[name];\n        if (!isNaN(index) && index >= 0) {\n            return this.get(index);\n        }\n\n        return null;\n    }\n\n    /**\n     * Tries to find out whether a certain stream matches profile for a card.\n     *\n     * @param {Gvc.MixerStream} stream\n     * @param {paCard} paCard\n     * @param {string} profileName\n     * @returns {STREAM_MATCHING}\n     */\n    streamMatchesPaCard(stream, paCard, profileName) {\n        const streamName = stream.name;\n        const cardName = paCard.name;\n\n        let [, streamAddr, streamIndex, streamProfile] = streamName.split('.');\n        const [, cardAddr, cardIndex] = cardName.split('.');\n\n        // try to fix stream names without index (cardName will not have an index either)\n        if (streamIndex && !streamProfile && isNaN(streamIndex)) {\n            streamProfile = streamIndex;\n            streamIndex = undefined;\n        }\n\n        const profileParts = profileName.split(':');\n        // remove direction\n        profileParts.shift();\n        const profile = profileParts.join(':');\n\n        if (streamAddr !== cardAddr\n            || streamIndex !== cardIndex\n        ) {\n            // cards don't match, certainly no hit\n            return false;\n        }\n\n        if (streamProfile !== profile) {\n            return STREAM_MATCHING.card;\n        }\n\n        return STREAM_MATCHING.stream;\n    }\n\n    /**\n     * Cleanup.\n     */\n    destroy() {\n        this.disconnectAll();\n    }\n};\n\nUtils.mixin(Cards, EventHandlerDelegate);\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/eventBroker.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Global event broker singleton.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported EventBroker */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Signals = imports.signals;\n\nconst Log = Lib.utils.log;\n\nlet instance;\n\nvar EventBroker = class {\n    constructor() {\n        if (instance) {\n            return instance;\n        }\n\n        instance = this;\n\n        this.connect('debug-events', (event, callback) => {\n            callback(Log.dump(this._signalConnections));\n        });\n    }\n};\n\nSignals.addSignalMethods(EventBroker.prototype);\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/eventHandlerDelegate.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Event handler mixin.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported EventHandlerDelegate */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Log = Lib.utils.log;\n\n\n/**\n * @mixin\n */\nvar EventHandlerDelegate = class {\n    /**\n     * @returns {string[]}\n     * @private\n     */\n    get _signals() {\n        if (!this.__signals) {\n            this.__signals = [];\n        }\n\n        return this.__signals;\n    }\n\n    /**\n     * @param {Object} delegate\n     */\n    set eventHandlerDelegate(delegate) {\n        this.__delegate = delegate;\n    }\n\n    get eventHandlerDelegate() {\n        return ('__delegate' in this) ? this.__delegate : null;\n    }\n\n    /**\n     * Connects to a signal.\n     *\n     * @param {Object} target\n     * @param {string} signal\n     * @param {function()} [callback]\n     * @param {?function()} [initial] Function returning the initial value\n     */\n    connect(target, signal, callback, initial) {\n        if (typeof target === 'string') {\n            initial = callback;\n            callback = signal;\n            signal = target;\n            target = this.eventHandlerDelegate;\n        }\n\n        if (!target) {\n            Log.error('EventHandlerDelegate', 'connect', `Connect called before setting \"eventHandlerDelegate\" and without passing a target (${signal})`);\n            return;\n        }\n\n        let id = target.connect(signal, callback);\n        this._signals.push([target, id, signal]);\n\n        if (typeof initial === 'function') {\n            callback.apply(callback, initial());\n        }\n    }\n\n    /**\n     * Connects to a signal.\n     *\n     * @param {Object} target\n     * @param {string} signal\n     */\n    disconnect(target, signal) {\n        if (typeof target === 'string') {\n            signal = target;\n        }\n\n        for (let index in this._signals) {\n            const [target, id, name] = this._signals[index];\n            if (target === target && signal === name) {\n                target.disconnect(id);\n                this._signals.splice(index, 1);\n                return;\n            }\n        }\n\n        Log.error('EventHandlerDelegate', 'disconnect', `Signal ${signal} not found on target`);\n    }\n\n    /**\n     * Disconnects all locally used signals.\n     */\n    disconnectAll() {\n        Log.info(`Disconnecting ${this._signals.length} signal(s)`);\n\n        while (this._signals.length > 0) {\n            const [target, id] = this._signals.pop();\n            Log.info(`Disconnecting signal ${id} from ${target.constructor.name}`);\n\n            target.disconnect(id);\n        }\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/hotkeys.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Hotkeys.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Hotkeys  */\n\nconst { Meta, Shell } = imports.gi;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Main = imports.ui.main;\n\nconst Log = Lib.utils.log;\n\nconst BINDINGS = {};\n\nvar Hotkeys = class\n{\n    constructor(settings) {\n        this._settings = settings;\n        this._bindings = BINDINGS;\n    }\n\n    /**\n     * Binds a hotkey using the local settings instance.\n     *\n     * @param {string} setting Settings key\n     * @param {function()} callback\n     */\n    bind(setting, callback) {\n        if (this._bindings[setting]) {\n            Log.info(`Not binding hotkey for ${setting}, already bound`);\n            return false;\n        }\n\n        const mode = Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW;\n        const flags = Meta.KeyBindingFlags.IGNORE_AUTOREPEAT;\n        const action = Main.wm.addKeybinding(setting, this._settings.settings, flags, mode, callback);\n\n        if (action === Meta.KeyBindingAction.NONE) {\n            Log.info(`Could not bind hotkey for ${setting}`);\n        } else {\n            Log.info(`Bound hotkey for ${setting}`);\n            this._bindings[setting] = action;\n        }\n    }\n\n    /**\n     * Unbinds a hotkey.\n     *\n     * @param {string} setting Settings key\n     */\n    unbind(setting) {\n        if (this._bindings[setting]) {\n            Main.wm.removeKeybinding(setting);\n            delete this._bindings[setting];\n            Log.info(`Unbound hotkey for ${setting}`);\n        }\n    }\n\n    /**\n     * Unbinds all hotkeys.\n     */\n    unbindAll() {\n        Log.info('Unbinding all hotkeys');\n        for (let setting in this._bindings) {\n            this.unbind(setting);\n        }\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/log.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Logging.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported l, d, info, error, dump, verbose */\n\nconst Extension = imports.misc.extensionUtils.getCurrentExtension();\nconst Lib = Extension.imports.lib;\n\nconst LOG_PREAMBLE = Extension.metadata.uuid || 'Shell Volume Mixer';\n\n\nvar verbose = false;\n\n\n/**\n * Helper to debug any variables(s) as string, using separators.\n *\n * @param {...string}\n */\nfunction l() {\n    log(Array.prototype.slice.call(arguments).join(' '));\n}\n\n/**\n * Logs verbose messages, if enabled.\n *\n * @param {...string}\n */\nfunction info() {\n    if (verbose) {\n        log(`${LOG_PREAMBLE} | ${Array.prototype.slice.call(arguments).join(' ')}`);\n    }\n}\n\n/**\n * Helper to debug any variable with pretty output.\n *\n * @param {*} object\n * @param {number} maxDepth\n */\nfunction d(object, maxDepth = 1) {\n    try {\n        l(_dumpObject(object, maxDepth));\n    } catch (e) {\n        l(object);\n    }\n}\n\n/**\n * Logs an error message.\n *\n * @param {string} module Module the error occurred in.\n * @param {string} context Optional.\n * @param {string|Error} error Error or error message (to construct Error from).\n */\nfunction error(module, context, error) {\n    if (module && !context && !error) {\n        error = module;\n        module = undefined;\n        context = undefined;\n    }\n    if (context && !error) {\n        error = context;\n        context = undefined;\n    }\n\n    let output = LOG_PREAMBLE;\n    if (module) {\n        output += ` | ${module}.js`;\n    }\n    if (context) {\n        output += ` | ${context}()`;\n    }\n\n    if (!(error instanceof Error)) {\n        error = Error(error);\n    }\n\n    logError(error, output);\n}\n\n/**\n * Helper to dump any variable to a string.\n *\n * @param {*} object\n * @param {number} maxDepth\n */\nfunction dump(object, maxDepth) {\n    try {\n        return _dumpObject(object, maxDepth);\n    } catch (e) {\n        return `Error dumping object: ${e}`;\n    }\n}\n\n\n/**\n * Dumps any variable into a string that can be output through log().\n *\n * @param {*} object\n * @param {number} maxDepth\n * @param {number} currDepth\n * @returns {string}\n */\nfunction _dumpObject(object, maxDepth = 8, currDepth = 0) {\n    if (currDepth > maxDepth) {\n        return '';\n    }\n\n    if (object === null) {\n        return 'null';\n    }\n\n    if (object === undefined) {\n        return 'undefined';\n    }\n\n    let dump = '';\n    let indent = '';\n    let stringMode = false;\n\n\n    if (currDepth > 0) {\n        indent = '\\u00A0'.repeat(currDepth * 4);\n\n    } else {\n        let objectString = object.toString();\n        dump += `${objectString}\\n${'-'.repeat(objectString.length)}\\n\\n`;\n\n        if (typeof object == 'string') {\n            stringMode = true;\n        }\n    }\n\n    let isFirst = true;\n\n    for (let key in object) {\n        const item = object[key];\n        const type = typeof item;\n        let typeInfo = type;\n\n        if (stringMode) {\n            let pos = parseInt(key);\n            if (isNaN(pos)) {\n                // don't debug string methods\n                break;\n            }\n            typeInfo = object.charCodeAt(pos);\n        }\n\n        if (!isFirst) {\n            dump += ',\\n';\n        }\n        isFirst = false;\n\n        dump += `${indent + key} => (${typeInfo})`;\n\n        if (item === null) {\n            dump += ' null';\n\n        } else if (type ==='object' || type === 'function') {\n            const isArray = Array.isArray(item);\n\n            if (isArray) {\n                dump += ' [';\n            } else if (type === 'function') {\n                dump += ' (';\n            } else {\n                // we're assuming toString() yields sane values\n                let itemString = item.toString();\n                if (itemString !== '[object Object]') {\n                    dump += ` ${itemString}`;\n                }\n                dump += ' {';\n            }\n\n            let objDump = '';\n            try {\n                objDump = _dumpObject(item, maxDepth, currDepth + 1);\n            } catch (e) {\n                // object cannot be dumped, probably a non-null pointer\n            }\n\n            if (objDump.trim() !== '') {\n                dump += `\\n${objDump}\\n${indent}`;\n            }\n\n            if (isArray) {\n                dump += ']';\n            } else if (type === 'function') {\n                dump += ')';\n            } else {\n                dump += '}';\n            }\n\n        } else {\n            dump += ` ${item}`;\n        }\n    }\n\n    return dump;\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/paHelper.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * PulseAudio helper.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported getCards, getCardByIndex */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Log = Lib.utils.log;\nconst Process = Lib.utils.process;\nconst Utils = Lib.utils.utils;\n\nconst PYTHON_HELPER_PATH = 'pautils/query.py';\nconst TYPE_CARDS = 'cards';\n\nlet PYTHON;\n\nasync function findPython() {\n    if (PYTHON === undefined) {\n        for (let python of ['python3', 'python']) {\n            let ret;\n            let stderr;\n\n            try {\n                [ret, , stderr] = await Process.execAsync(['/usr/bin/env', python]);\n\n                if (ret === 0) {\n                    PYTHON = python;\n                    break;\n                }\n\n                Log.error('paHelper', 'findPython', `${python} not found: ${stderr} (${ret})`);\n\n            } catch (e) {\n                PYTHON = false;\n                Log.error('paHelper', 'findPython', e);\n            }\n        }\n    }\n\n    return PYTHON;\n}\n\n/**\n * @param {string} type Type of data to query\n * @param {?number} [index=undefined]\n * @returns {Promise<?Object.<string, paCard>>} JSON object of the output\n */\nasync function execHelper(type, index = undefined) {\n    const paUtilPath = Utils.getExtensionPath(PYTHON_HELPER_PATH);\n\n    if (!paUtilPath) {\n        Log.error('paHelper', 'execHelper', `Could not find PulseAudio utility in extension path ${PYTHON_HELPER_PATH}`);\n        return null;\n    }\n\n    const python = await findPython();\n\n    if (!python) {\n        return null;\n    }\n\n    const args = ['/usr/bin/env', python, paUtilPath, type];\n\n    if (!isNaN(index)) {\n        args.push(index);\n    }\n\n\n    let ret;\n    let stdout;\n    let stderr;\n    let pythonError;\n\n    try {\n        [ret, stdout, stderr] = await Process.execAsync(args);\n    } catch (e) {\n        pythonError = e;\n    }\n\n    if (pythonError) {\n        Log.error('paHelper', 'execHelper', pythonError);\n        if (stderr) {\n            Log.error('paHelper', 'execHelper', `(${ret}) ${stderr}`);\n        }\n    }\n\n    if (!stdout) {\n        return null;\n    }\n\n    let data = null;\n    try {\n        data = JSON.parse(stdout);\n    } catch (e) {\n        Log.error('paHelper', 'execHelper', e);\n        return null;\n    }\n\n    if (!data || typeof data !== 'object') {\n        Log.error('paHelper', 'execHelper', 'Invalid response');\n        return null;\n    }\n\n    if ('success' in data && data.success === false) {\n        Log.error('paHelper', 'execHelper', `Error: ${data.error}`);\n        return null;\n    }\n\n    return data;\n}\n\n/**\n * Calls the Python helper script to get details about all available cards and their profiles.\n *\n * @returns {Promise<?Object.<string, paCard>>} JSON object of the output\n */\nasync function getCards() {\n    return await execHelper(TYPE_CARDS);\n}\n\n/**\n * Calls the Python helper script to get more details about a card and its profiles.\n *\n * @param {number} index\n * @returns {Promise<?paCard>} JSON object of the output\n */\nasync function getCardByIndex(index) {\n    const data = await execHelper(TYPE_CARDS, index);\n\n    if (data && data[index]) {\n        return data[index];\n    }\n\n    return null;\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/process.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Process utility.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported execAsync */\n\nconst { Gio } = imports.gi;\n\n/**\n * Executes an async command.\n *\n * @param {Array} command\n * @returns {Promise<[int, string, string]>|Promise<Error>}\n */\nasync function execAsync(command) {\n    const process = new Gio.Subprocess({\n        argv:  command.map(arg => arg.toString()),\n        flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE\n    });\n\n    process.init(null);\n\n    return new Promise((resolve, reject) => {\n        process.communicate_utf8_async(null, null, (process, result) => {\n            try {\n                const [success, stdout, stderr] = process.communicate_utf8_finish(result);\n                const ret = process.get_exit_status();\n\n                if (!success) {\n                    reject(Error('Error spawning subprocess'));\n                } else {\n                    resolve([ret, stdout, stderr]);\n                }\n\n            } catch (e) {\n                reject(e);\n            }\n        });\n    });\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/string.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * String utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported repeat, versionGreaterOrEqual */\n\n\n/**\n * Parses a version string and returns an array.\n *\n * @param {string} string\n * @returns {number[]}\n */\nfunction parseVersionString(string) {\n    let version = string.split('.', 3);\n\n    for (let i = 0; i < 3; i++) {\n        if (version[i]) {\n            version[i] = parseInt(version[i]);\n        } else {\n            version[i] = 0;\n        }\n    }\n\n    return version;\n}\n\n/**\n * Returns true if the current shell version is greater than the version string passed.\n *\n * @param {string} string Version string like 3.38.\n */\nfunction versionGreaterOrEqual(string) {\n    let current = parseVersionString(imports.misc.config.PACKAGE_VERSION);\n    let version = parseVersionString(string);\n\n    for (let i = 0; i < 3; i++) {\n        if (current[i] < version[i]) {\n            return false;\n        }\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/utils/utils.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Utilities.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported getCards, getExtensionPath, mixin */\n\nconst Extension = imports.misc.extensionUtils.getCurrentExtension();\n\n\n/**\n * Returns this extension's path, optionally querying a subdirectory or file.\n *\n * @param {string} [subpath] Subdirectory or -path to get.\n */\nfunction getExtensionPath(subpath) {\n    let dir = Extension.dir;\n\n    if (subpath) {\n        dir = dir.get_child(subpath);\n    }\n\n    if (!dir.query_exists(null)) {\n        return null;\n    }\n\n    return dir.get_path();\n}\n\n\n/**\n * Allows a target object to receive all properties from a source.\n */\nfunction mixin(target, source, keepExisting = false) {\n    const sourceProps = Object.getOwnPropertyDescriptors(source.prototype);\n\n    for (let name of Object.keys(sourceProps)) {\n        if (keepExisting === true && Object.prototype.hasOwnProperty.call(target, name)) {\n            continue;\n        }\n\n        Object.defineProperty(target.prototype, name, sourceProps[name]);\n    }\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/volume/mixer.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Mixer class wrapping the mixer control.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Mixer */\n\nconst {Gio, Gvc} = imports.gi;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst Main = imports.ui.main;\nconst Volume = imports.ui.status.volume;\n\nconst { Cards, STREAM_MATCHING } = Lib.utils.cards;\nconst { EventBroker } = Lib.utils.eventBroker;\nconst { EventHandlerDelegate } = Lib.utils.eventHandlerDelegate;\nconst { Hotkeys } = Lib.utils.hotkeys;\nconst { Profiles } = Lib.volume.profiles;\nconst { Settings, SETTING } = Lib.settings;\nconst Log = Lib.utils.log;\nconst Utils = Lib.utils.utils;\n\n\n/**\n * @mixes EventHandlerDelegate\n */\nvar Mixer = class\n{\n    constructor() {\n        this._events = new EventBroker();\n        this._control = Volume.getMixerControl();\n        this.eventHandlerDelegate = this._control;\n\n        this._settings = new Settings();\n        this._hotkeys = new Hotkeys(this._settings);\n        this._profiles = new Profiles(this._settings);\n        this._cards = new Cards(this._control);\n\n        this._state = null;\n        this._defaultSink = null;\n        this._pauseDefaultSinkEvent = false;\n\n        this.connect(this._control, 'state-changed', this._onStateChanged.bind(this), () => {\n            return [this._control, this._control.get_state()];\n        });\n\n        this.connect(this._control, 'default-sink-changed', this._onDefaultSinkChanged.bind(this));\n\n        this._bindProfileHotkey();\n    }\n\n    /**\n     * The current Gvc.MixerControl.\n     *\n     * @returns {Gvc.MixerControl}\n     */\n    get control() {\n        return this._control;\n    }\n\n    /**\n     * @returns {Cards}\n     */\n    get cards() {\n        return this._cards;\n    }\n\n    /**\n     * @returns {?Gvc.MixerSink}\n     */\n    get defaultSink() {\n        return this._defaultSink;\n    }\n\n    /**\n     * Returns the current volume in percent.\n     *\n     * @returns {?number}\n     */\n    getVolume() {\n        if (!this._defaultSink) {\n            return null;\n        }\n\n        if (this._defaultSink.is_muted) {\n            return 0;\n        }\n\n        return Math.round(this._defaultSink.volume / this._control.get_vol_max_norm() * 100);\n    }\n\n    /**\n     * Cleanup.\n     */\n    destroy() {\n        this.disconnectAll();\n        this._hotkeys.unbindAll();\n        this._cards.destroy();\n    }\n\n    /**\n     * Binds the hotkey for profile rotation.\n     * @private\n     */\n    _bindProfileHotkey() {\n        if (!this._profiles.count()) {\n            Log.info('No profiles found, not enabling profile switching hotkey');\n            return;\n        }\n\n        Log.info('Profiles found, enabling profile switching hotkey');\n        this._hotkeys.bind(SETTING.profile_switcher_hotkey, this._switchProfile.bind(this));\n    }\n\n    /**\n     * Disconnects volume update signals from the default sink.\n     * @private\n     */\n    _disconnectSink() {\n        this.disconnect(this._defaultSink, 'notify::is-muted');\n        this.disconnect(this._defaultSink, 'notify::volume');\n    }\n\n    /**\n     * Connects volume update signals from the default sink for notifications.\n     * @private\n     */\n    _connectSink() {\n        this.connect(this._defaultSink, 'notify::is-muted', this._onVolumeUpdate.bind(this));\n        this.connect(this._defaultSink, 'notify::volume', this._onVolumeUpdate.bind(this));\n    }\n\n    /**\n     * Emits a stream update event if volume changes.\n     * @private\n     */\n    _onVolumeUpdate() {\n        const percent = this.getVolume();\n\n        if (percent !== null) {\n            this._events.emit('volume-changed', percent);\n        }\n    }\n\n    /**\n     * Updates the default sink, trying to mark the currently active card.\n     * @private\n     */\n    async _updateDefaultSink(stream) {\n        if (this._defaultSink !== stream) {\n            if (this._defaultSink) {\n                this._disconnectSink();\n            }\n\n            this._defaultSink = stream;\n\n            if (stream) {\n                Log.info(`Updating default sink ${stream.id}:${stream.name}`);\n\n                this._connectSink();\n                this._onVolumeUpdate();\n\n            } else {\n                Log.info('Default sink updated to null, cannot update');\n            }\n        }\n\n        // we might get a sink id without being able to lookup\n        // ...or local code causing the event triggers another update\n        if (stream && !this._pauseDefaultSinkEvent) {\n            try {\n                const paCard = await this._cards.get(stream.card_index);\n\n                if (!paCard || !paCard.card) {\n                    Log.info(`Default sink updated but PA/GVC card not found (${stream.card_index}/${stream.name})`);\n                } else {\n                    this._profiles.setCurrent(paCard);\n                }\n\n            } catch (e) {\n                Log.error('Mixer', '_updateDefaultSink', e);\n            }\n        }\n\n        this._pauseDefaultSinkEvent = false;\n        this._events.emit('default-sink-updated', stream);\n    }\n\n    /**\n     * Callback for state changes.\n     * @private\n     */\n    _onStateChanged(control, state) {\n        this._state = state;\n\n        if (state !== Gvc.MixerControlState.READY) {\n            return;\n        }\n\n        // noinspection JSIgnoredPromiseFromCall\n        this._updateDefaultSink(this._control.get_default_sink());\n    }\n\n    /**\n     * Callback for default sink changes.\n     * @private\n     */\n    _onDefaultSinkChanged(control, id) {\n        // noinspection JSIgnoredPromiseFromCall\n        this._updateDefaultSink(control.lookup_stream_id(id));\n    }\n\n\n    /**\n     * Hotkey handler to switch between profiles.\n     * @private\n     */\n    async _switchProfile() {\n        if (this._state !== Gvc.MixerControlState.READY) {\n            return;\n        }\n\n        const next = this._profiles.next();\n\n        if (!next) {\n            return;\n        }\n\n        let paCard;\n        try {\n            // lookup card indirectly via name (indexes aren't UUIDs)\n            paCard = await this._cards.getByName(next.card);\n        } catch (e) {\n            Log.error('Mixer', '_switchProfile', e);\n        }\n\n        if (!paCard || !paCard.card) {\n            // we don't know this card, we won't be able to set its profile\n            return;\n        }\n\n        // profile's changed, now get that new sink\n        const sinks = this._control.get_sinks();\n        let newSink = null;\n\n        for (let sink of sinks) {\n            let result = this._cards.streamMatchesPaCard(sink, paCard, next.profile);\n\n            if (result === STREAM_MATCHING.stream) {\n                newSink = sink;\n                break;\n            }\n\n            if (result === STREAM_MATCHING.card || !newSink) {\n                // maybe we can use this stream later, but we'll keep searching\n                newSink = sink;\n            }\n        }\n\n        paCard.card.set_profile(next.profile);\n\n        if (newSink) {\n            this._pauseDefaultSinkEvent = true;\n            this._control.set_default_sink(newSink);\n        }\n\n        const paProfile = paCard.profiles[next.profile];\n        this._showNotification(`${paCard.description || paCard.name}\\n${paProfile.description || paProfile.name}`);\n    }\n\n    /**\n     * Shows a notification window through Shell's OSD Window Manager.\n     *\n     * @param {string} text Text to display\n     * @private\n     */\n    _showNotification(text) {\n        let monitor = -1;\n        let icon = Gio.Icon.new_for_string('audio-speakers-symbolic');\n        Main.osdWindowManager.show(monitor, icon, text);\n    }\n};\n\nUtils.mixin(Mixer, EventHandlerDelegate);\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/volume/profiles.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Card / profile settings cycling.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported Profiles */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst { SETTING } = Lib.settings;\nconst Log = Lib.utils.log;\n\n\n/** @typedef {{\n *   card: String,\n *   profile: String,\n *   switcher: Boolean,\n *   show: Boolean,\n *   next: paCycledProfile,\n * }} paCycledProfile\n */\n\nvar Profiles = class {\n    constructor(settings) {\n        this._settings = settings;\n        this._current = null;\n\n        const data = this._settings.get_array(SETTING.pinned_profiles) || [];\n        this._profiles = this._parseProfilesSetting(data);\n    }\n\n    /**\n     * @returns {number}\n     */\n    count() {\n        return this._profiles.length;\n    }\n\n    /**\n     * @param {paCard} paCard\n     */\n    setCurrent(paCard) {\n        if (!this.count()) {\n            return;\n        }\n\n        let profile = paCard.card.profile;\n\n        for (let entry of this._profiles) {\n            if (entry.card === paCard.name && entry.profile === profile) {\n                this._current = entry;\n                break;\n            }\n        }\n    }\n\n    /**\n     * @returns {?paCycledProfile}\n     */\n    next() {\n        if (!this.count()) {\n            return;\n        }\n\n        if (!this._current) {\n            this._current = this._profiles[0];\n        }\n\n        this._current = this._current.next;\n\n        return this._current;\n    }\n\n    /**\n     * Reads all pinned profiles from settings.\n     *\n     * @param {string[]} data JSON formatted data\n     * @private\n     */\n    _parseProfilesSetting(data) {\n        const profiles = [];\n\n        let count = 0;\n        for (let entry of data) {\n            let item = null;\n            try {\n                item = JSON.parse(entry);\n            } catch (e) {\n                Log.error('Profiles', '_parseProfilesSetting', e);\n            }\n            if (!item) {\n                continue;\n            }\n\n            profiles.push(item);\n\n            if (count > 0) {\n                profiles[count - 1].next = item;\n            }\n\n            count++;\n        }\n\n        if (count > 0) {\n            profiles[count - 1].next = profiles[0];\n        }\n\n        return profiles;\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/floatingLabel.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Floating label.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported FloatingLabel */\n\nconst Main = imports.ui.main;\nconst {Clutter, St} = imports.gi;\n\n\n\n/**\n * A tooltip-like label to display the current value of a slider.\n *\n * Shamelessly stolen from gnome-shell/js/ui/dash.js.\n */\nvar FloatingLabel = class\n{\n    constructor() {\n        this._label = new St.Label({ style_class: 'dash-label floating-label' });\n        this.text = '100%';\n        this._label.hide();\n        Main.layoutManager.addChrome(this._label);\n    }\n\n    get text() {\n        return this._label.get_text();\n    }\n\n    set text(text) {\n        this._label.set_text(text);\n    }\n\n    get size() {\n        return this._label.get_size();\n    }\n\n    show(x, y, animate) {\n        this._label.opacity = 0;\n        this._label.show();\n        Main.uiGroup.set_child_above_sibling(this._label, null);\n\n        const labelHeight = this._label.get_height();\n        const labelWidth = this._label.get_width();\n\n        const node = this._label.get_theme_node();\n        const xOffset = node.get_length('-x-offset');\n\n        let xPos;\n        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {\n            xPos = x - labelWidth - xOffset;\n        } else {\n            xPos = x + labelWidth + xOffset;\n        }\n\n        const yPos = y - labelHeight;\n\n        this._label.set_position(xPos, yPos);\n        this._label.ease({\n            opacity: 255,\n            duration: animate !== false ? 150 : 0,\n            mode: Clutter.AnimationMode.EASE_OUT_QUAD,\n        });\n    }\n\n    hide(animate) {\n        this._label.ease({\n            opacity: 0,\n            duration: animate !== false ? 100 : 0,\n            mode: Clutter.AnimationMode.EASE_OUT_QUAD,\n            onComplete: () => this._label.hide(),\n        });\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/menuItem.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Menu items.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported MasterMenuItem, SubMenuItem */\n\nconst { Clutter, GObject, St } = imports.gi;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst PopupMenu = imports.ui.popupMenu;\n\nconst Slider = Lib.widget.slider;\nconst Utils = Lib.utils.utils;\n\n/**\n * @mixin\n */\nclass BaseMenuItem\n{\n    _makeOrnamentLabel() {\n        return new St.Label({ style_class: 'popup-menu-ornament' });\n    }\n\n    /**\n     * @returns {St.BoxLayout}\n     */\n    _makeItemLine() {\n        return new St.BoxLayout({\n            style_class: 'popup-menu-item svm-container-line',\n            reactive:    true,\n        });\n    }\n\n    _prepareMenuItem() {\n        this.get_children().map(child => {\n            this.remove_actor(child);\n        });\n\n        this.container = new St.BoxLayout({\n            vertical: true,\n            x_expand: true,\n            style_class: 'svm-menu-item-container',\n        });\n\n        this.add(this.container);\n\n        if (!this.firstLine) {\n            this.firstLine = this._makeItemLine();\n            if (this._ornamentLabel) {\n                this.firstLine.add_child(this._ornamentLabel);\n            }\n            this.container.add(this.firstLine);\n        }\n\n        if (!this.secondLine) {\n            this.secondLine = this._makeItemLine();\n            if (this._ornamentLabel) {\n                this.secondLine.add_child(this._makeOrnamentLabel());\n            }\n            this.container.add(this.secondLine);\n        }\n\n        this.firstLine.add_style_class_name('line-1');\n        this.secondLine.add_style_class_name('line-2');\n    }\n}\n\n\n/**\n * Submenu item for the sink selection menu.\n *\n * @mixes BaseMenuItem\n */\nvar MasterMenuItem = GObject.registerClass(class MasterMenuItem extends PopupMenu.PopupSubMenuMenuItem\n{\n    _init() {\n        super._init('', true);\n        this._prepareMenuItem();\n\n        this._slider = new Slider.VolumeSlider(0);\n\n        this.firstLine.add_child(this.icon);\n        this.firstLine.add_child(this.label);\n\n        this.firstLine.add_child(new St.Bin({\n            style_class: 'popup-menu-item-expander',\n            x_expand: true,\n        }));\n\n        this.firstLine.add_child(this._triangleBin);\n\n        this.secondLine.add_child(this._slider); // shell uses add_child here but that breaks layout?\n        this.secondLine.add_style_class_name('svm-master-slider-line');\n\n        this.label.add_style_class_name('svm-master-label');\n        this.add_style_class_name('svm-master-slider svm-menu-item');\n    }\n\n    vfunc_button_release_event(event) {\n        if (event.button === 2) {\n            return Clutter.EVENT_STOP;\n        }\n\n        return super.vfunc_button_release_event(event);\n    }\n\n    /**\n     * Change volume on left / right.\n     */\n    vfunc_key_press_event(event) {\n        const symbol = event.keyval;\n\n        if (symbol === Clutter.KEY_Right || symbol === Clutter.KEY_Left) {\n            return this._slider.emit('key-press-event', event);\n        }\n\n        return super.vfunc_key_press_event(event);\n    }\n\n    addMenuItem(item) {\n        const pos = (this.menu._getMenuItems().length || 0) - 1;\n\n        this.menu.addMenuItem(item, pos < 0 ? 0 : pos);\n    }\n});\n\nUtils.mixin(MasterMenuItem, BaseMenuItem, true);\n\n/**\n * Sub menu item implementation for dropdown menus (via master slider menu or input menu).\n *\n * @mixes BaseMenuItem\n */\nvar SubMenuItem = GObject.registerClass(class SubMenuItem extends PopupMenu.PopupBaseMenuItem\n{\n    _init(params = {}) {\n        super._init({\n            ...params,\n            activate: false,\n        });\n\n        this._prepareMenuItem();\n    }\n\n    addDetails(label) {\n        const line = this._makeItemLine();\n\n        line.add_child(label);\n        this.container.insert_child_at_index(line, 1);\n    }\n});\n\nUtils.mixin(SubMenuItem, BaseMenuItem, true);\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/panelButton.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Stand-alone menu panel button.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported PanelButton */\n\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\nconst { GObject, St } = imports.gi;\nconst PanelMenu = imports.ui.panelMenu;\nconst Volume = imports.ui.status.volume;\n\nconst { Indicator } = Lib.menu.indicator;\n\n/**\n * Stand-alone panel menu button.\n */\nvar PanelButton = GObject.registerClass(class PanelButton extends PanelMenu.Button\n{\n    /**\n     * @param {Mixer} mixer\n     * @param {Object} options\n     * @private\n     */\n    _init(mixer, options = {}) {\n        super._init(0.0, 'ShellVolumeMixer', false);\n\n        this._indicators = new St.BoxLayout({ style_class: 'panel-status-indicators-box' });\n        this.add_child(this._indicators);\n\n        this._volume = new Indicator(mixer, {\n            ...options,\n            separator: true,\n            menuClass: 'svm-standalone-menu',\n        });\n\n        this._indicators.add_child(this._volume);\n        this.menu.addMenuItem(this._volume.menu);\n    }\n});\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/percentageLabel.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Percentage label.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported PercentageLabel */\n\nconst { Clutter, GObject, St } = imports.gi;\nconst Lib = imports.misc.extensionUtils.getCurrentExtension().imports.lib;\n\nconst { EventBroker } = Lib.utils.eventBroker;\n\n\nvar PercentageLabel = GObject.registerClass(class PercentageLabel extends St.Label {\n    _init(mixer) {\n        this._events = new EventBroker();\n\n        super._init({\n            y_expand: true,\n            y_align: Clutter.ActorAlign.CENTER,\n        });\n\n        this.add_style_class_name('percentage-label');\n\n        this._events.connect('volume-changed', (event, volume) => {\n            this._setText(volume);\n        });\n\n        // set initial value, if available\n        this._setText(mixer.getVolume());\n    }\n\n    /**\n     * @param {?number} percent\n     * @private\n     */\n    _setText(percent) {\n        if (percent === null) {\n            this.text = '';\n        } else {\n            this.text = _('%d\\u2009%%').format(percent);\n        }\n    }\n});\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/slider.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Sliders.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported VolumeSlider */\n\nconst { Clutter, GObject } = imports.gi;\nconst Slider = imports.ui.slider;\n\n\n/**\n * Custom Slider to allow for mute via middle button.\n */\nvar VolumeSlider = GObject.registerClass(class VolumeSlider extends Slider.Slider\n{\n    /**\n     * Allow middle button event to bubble up for mute / unmute.\n     */\n    startDragging(event) {\n        if (event.get_button() === 2) {\n            return Clutter.EVENT_PROPAGATE;\n        }\n        return super.startDragging(event);\n    }\n});\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/lib/widget/volume.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Volume widgets.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported MasterSlider, AggregatedInput, OutputSlider, EventsSlider, InputSlider, InputStreamSlider, VolumeMenu */\n\nconst { Clutter, GLib, St } = imports.gi;\nconst ExtensionUtils = imports.misc.extensionUtils;\nconst Lib = ExtensionUtils.getCurrentExtension().imports.lib;\nconst Main = imports.ui.main;\nconst PopupMenu = imports.ui.popupMenu;\nconst Volume = imports.ui.status.volume;\nconst __ = ExtensionUtils.gettext;\n\nconst { EventBroker } = Lib.utils.eventBroker;\nconst { FloatingLabel } = Lib.widget.floatingLabel;\nconst Log = Lib.utils.log;\nconst MenuItem = Lib.widget.menuItem;\nconst Settings = Lib.settings;\nconst Slider = Lib.widget.slider;\nconst Utils = Lib.utils.utils;\n\n\n/**\n * Extension of Volume.StreamSlider without its constructor().\n */\nclass StreamSliderExtension {}\nUtils.mixin(StreamSliderExtension, Volume.StreamSlider);\n\n\n/**\n * Extension of Volume.OutputStreamSlider without its constructor().\n */\nclass OutputStreamSliderExtension extends StreamSliderExtension {}\nUtils.mixin(OutputStreamSliderExtension, Volume.OutputStreamSlider);\n\n\n/**\n * Basic StreamSlider implementation for Input- and OutputStreams.\n *\n * We can extend (and monkey patch) Volume.OutputStreamSlider\n * (Volume.InputStreamSlider is meant for microphones only and Volume.StreamSlider is only basic).\n */\nconst StreamSlider = class extends OutputStreamSliderExtension\n{\n    /**\n     * @param {Gvc.MixerControl} control\n     * @param {sliderOptions} options\n     * @private\n     */\n    constructor(control, options = {}) {\n        super();\n\n        this._isDestroyed = false;\n        this._hasHeadphones = false;\n\n        this.options = options;\n        this._control = control;\n        this._mixer = options.mixer;\n        this._events = new EventBroker();\n\n        this._init(options);\n\n        return this;\n    }\n\n    /**\n     * Init basically copied from Volume.StreamSlider (all init) and Volume.OutputStreamSlider (icons).\n     *\n     * @param {sliderOptions} options\n     */\n    _init(options) {\n        if (!this.item) {\n            this.item = new MenuItem.SubMenuItem();\n        }\n\n        if (this.icon) {\n            // different widgets seem to use different naming\n            this._icon = this.icon;\n        }\n\n        if (!this._icon) {\n            this._icon = new St.Icon({ style_class: 'popup-menu-icon' });\n            this.item.firstLine.add_child(this._icon);\n        }\n\n        if (!this._label) {\n            this._label = new St.Label({ text: '' });\n            this.item.firstLine.add_child(this._label);\n        }\n\n        this.item.label_actor = this._label;\n\n        if (!this._slider) {\n            this._slider = new Slider.VolumeSlider(0);\n            this.item.secondLine.add_child(this._slider);\n        }\n\n        this._volumeInfo = new FloatingLabel();\n\n\n        this.item.connect('destroy', this._onDestroy.bind(this));\n\n        if (this._onButtonPress) {\n            this.item.connect('button-press-event', this._onButtonPress.bind(this));\n        }\n\n        if (this._onKeyPress) {\n            this.item.connect('key-press-event', this._onKeyPress.bind(this));\n        }\n\n        if (this._slider.scroll) {\n            this.item.connect('scroll-event', (slider, event) => {\n                return this._slider.emit('scroll-event', event);\n            });\n        }\n\n        this.item.connect('scroll-event', () => {\n            this._showVolumeInfo();\n        });\n\n\n        this._inDrag = false;\n        this._notifyVolumeChangeId = 0;\n\n        this._soundSettings = new Settings.Settings(Settings.SOUND_SETTINGS_SCHEMA);\n        this._soundSettings.connect(`changed::${Settings.ALLOW_AMPLIFIED_VOLUME_KEY}`, this._amplifySettingsChanged.bind(this), true);\n        this._amplifySettingsChanged();\n\n        this._sliderChangedId = this._slider.connect('notify::value', this._sliderChanged.bind(this));\n        this._slider.connect('drag-begin', () => (this._inDrag = true));\n        this._slider.connect('drag-end', () => {\n            this._inDrag = false;\n            this._notifyVolumeChange();\n        });\n\n        this.stream = options.stream || null;\n        this._volumeCancellable = null;\n\n        this._icons = [\n            'audio-volume-muted-symbolic',\n            'audio-volume-low-symbolic',\n            'audio-volume-medium-symbolic',\n            'audio-volume-high-symbolic',\n            'audio-volume-overamplified-symbolic',\n        ];\n    }\n\n    _onKeyPress(actor, event) {\n        return this._slider.emit('key-press-event', event);\n    }\n\n    _onButtonPress(actor, event) {\n        if (event.get_button() === 2) {\n            this.toggleMute();\n\n            return Clutter.EVENT_STOP;\n        }\n\n        return this._slider.startDragging(event);\n    }\n\n    refresh() {\n        this._updateLabel();\n        this._updateSliderIcon();\n    }\n\n    _updateSliderIcon() {\n        if (this._stream && !this.options.symbolicIcons) {\n            this._icon.gicon = this._stream.get_gicon();\n        } else {\n            super._updateSliderIcon();\n        }\n\n        this.emit('stream-updated');\n    }\n\n    _connectStream(stream) {\n        super._connectStream(stream);\n        this.refresh();\n    }\n\n    _updateLabel() {\n        this._label.text = this._stream.name || this._stream.description || '';\n    }\n\n    _showVolumeInfo(position) {\n        if (!this._stream || !this._volumeInfo) {\n            return;\n        }\n\n        this._volumeInfo.text = Math.round(this._slider.value * 100) + '%';\n\n        if (this._labelTimeoutId) {\n            GLib.source_remove(this._labelTimeoutId);\n            this._labelTimeoutId = undefined;\n        }\n\n        if (!this._infoShowing) {\n            this._infoShowing = true;\n\n            let x, y;\n            if (position) {\n                [x, y] = position;\n                x     += 15;\n                y     += 100;\n            } else {\n                [x, y] = this._slider.get_transformed_position();\n                x = x + Math.floor(this._slider.get_width() / 2);\n            }\n\n            this._volumeInfo.show(x, y);\n        }\n\n        this._labelTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {\n            this._infoShowing = false;\n            this._labelTimeoutId = undefined;\n            this._volumeInfo.hide();\n            return GLib.SOURCE_REMOVE;\n        });\n    }\n\n    hideVolumeInfo() {\n        if (this._labelTimeoutId) {\n            GLib.source_remove(this._labelTimeoutId);\n            this._labelTimeoutId = undefined;\n        }\n\n        this._infoShowing = false;\n        this._volumeInfo.hide(false);\n    }\n\n    toggleMute() {\n        if (this._stream) {\n            this._stream.change_is_muted(!this._stream.is_muted);\n        }\n    }\n\n    _onDestroy() {\n        this._isDestroyed = true;\n\n        // make sure we clean up all bindings\n        if (this._stream) {\n            this._disconnectStream(this._stream);\n        }\n    }\n};\n\n\n\n/**\n * Slider replacing the master volume slider.\n */\nvar MasterSlider = class extends StreamSlider\n{\n    _init(options) {\n        this.item = new MenuItem.MasterMenuItem();\n        this.item.menu.actor.add_style_class_name('svm-master-slider-menu');\n\n        this._slider = this.item._slider;\n        this._icon = this.item.icon;\n        this._label = this.item.label;\n\n        this._slider.accessible_name = _('Volume');\n\n        super._init(options);\n        this._addSettingsItem();\n    }\n\n    /**\n     * Add settings shortcuts to the menu.\n     *\n     * @private\n     */\n    _addSettingsItem() {\n        this.item.menu.addAction(_('Settings'), () => ExtensionUtils.openPrefs());\n    }\n\n    /**\n     * @param {OutputSlider} slider\n     */\n    addOutputSlider(slider) {\n        this.item.addMenuItem(slider.item);\n    }\n\n    /**\n     * Override button click to allow for mute / unmute and menu to be opened.\n     */\n    _onButtonPress(actor, event) {\n        if (event.get_button() === 2) {\n            this.toggleMute();\n        }\n\n        return Clutter.EVENT_STOP;\n    }\n\n    _updateLabel() {\n        this._label.text = this._stream.description;\n    }\n\n    scroll(event) {\n        const eventResult = super.scroll(event);\n\n        if (Main.panel.statusArea.aggregateMenu.menu.isOpen) {\n            this._showVolumeInfo();\n        }\n\n        return eventResult;\n    }\n};\n\n\n/**\n * Menu item for aggregated input streams.\n */\nvar AggregatedInput = class\n{\n    constructor() {\n        this.item = new PopupMenu.PopupSubMenuMenuItem(__('Inputs'), true);\n        this.item.icon.icon_name = 'applications-multimedia-symbolic';\n        this.item.accessible_name = __('Inputs');\n\n        this._inputStream = null;\n    }\n\n    setInputStream(inputSlider) {\n        this._inputStream = inputSlider;\n        this.addSlider(inputSlider, 0);\n    }\n\n    addSlider(slider, pos) {\n        this.item.menu.addMenuItem(slider.item, pos || undefined);\n\n        slider.connect('stream-updated', () => {\n            this.refresh();\n        });\n    }\n\n    refresh() {\n        this.item.visible = (this.item.menu.numMenuItems > 1\n            || (this.item.menu.numMenuItems > 0 && this._inputStream.isVisible())\n        );\n    }\n};\n\n\n\n/**\n * Slider for output sinks (e.g. alsa devices, different profiles).\n */\nvar OutputSlider = class extends StreamSlider\n{\n    _init(options) {\n        // make details widget available before parent triggers accessing it\n        if (options.detailed) {\n            this._details = new St.Label({ text: '', style_class: 'svm-slider-details' });\n        }\n\n        super._init(options);\n\n        if (options.detailed) {\n            this.item.addDetails(this._details);\n        }\n\n        this._cards = options.mixer.cards;\n        this._updateVisibility(false);\n\n        this._events.connect('default-sink-updated', this._onDefaultSinkUpdated.bind(this));\n    }\n\n    _onButtonPress(actor, event) {\n        if (event.get_button() === 1) {\n            this._setAsDefault();\n            return Clutter.EVENT_PROPAGATE;\n        }\n\n        return super._onButtonPress(actor, event);\n    }\n\n    _onKeyPress(actor, event) {\n        let symbol = event.get_key_symbol();\n        if (symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return) {\n            this._setAsDefault();\n            return Clutter.EVENT_STOP;\n        }\n\n        return super._onKeyPress(actor, event);\n    }\n\n    _updateLabel() {\n        let text = this._stream.description;\n        let description = this._stream.name;\n\n        this._label.text = text;\n\n        if (this.options.detailed && text !== description && description) {\n            let parts = description.split('.');\n\n            if (parts.length > 1) {\n                if (parts[0] === 'alsa_output') {\n                    // remove the common first (and uninteresting) part\n                    parts.shift();\n                }\n                // the last segment of the path is the most interesting one\n                description = parts.pop();\n                description += ` | ${parts.join('.')}`;\n            }\n\n            this._details.text = description;\n        }\n    }\n\n    _setAsDefault() {\n        this._control.set_default_sink(this._stream);\n    }\n\n    _onDefaultSinkUpdated(/* stream */) {\n        this._updateVisibility(false);\n    }\n\n    _updateVisibility(forceRefresh = true) {\n        if (!this._shouldBeVisible()) {\n            // set invisible immediately\n            this.item.visible = false;\n\n        } else {\n            // check if port is available before setting visible\n            (async () => {\n                try {\n                    const byPort = await this._shouldBeVisibleByPort(forceRefresh);\n\n                    if (this._isDestroyed) {\n                        // async race condition, we're already gone\n                        return;\n                    }\n\n                    // This could be a race condition with the async code finishing after current conditions have changed.\n                    // Therefore we have to check the sync path again.\n                    this.item.visible = byPort && this._shouldBeVisible();\n\n                } catch (e) {\n                    Log.error('OutputSlider', '_updateVisibility', e);\n                }\n            })();\n        }\n    }\n\n    _shouldBeVisible() {\n        if (this.options.mixer.defaultSink === this._stream) {\n            Log.info(`Hiding ${this._stream.id}:${this._stream.name}, it's the default sink`);\n\n            return false;\n        }\n\n        return super._shouldBeVisible();\n    }\n\n    /**\n     * @returns {Promise<boolean>}\n     * @private\n     */\n    async _shouldBeVisibleByPort(forceRefresh = true) {\n        if (!this._stream || ! this._cards) {\n            return true;\n        }\n\n        if (!this._stream.card_index) {\n            Log.error('OutputSlider', '_shouldBeVisibleByPort', 'Stream cannot be identified, no card index available');\n            return true;\n        }\n\n        const card = await this._cards.get(this._stream.card_index, forceRefresh);\n\n        if (!card) {\n            Log.info(`Card ${this._stream.card_index} not found for stream ${this._stream.id}:${this._stream.name}`);\n            return true;\n        }\n\n        const port = this._stream.port in card.ports ? card.ports[this._stream.port] : null;\n\n        if (!port) {\n            Log.error('OutputSlider', '_shouldBeVisibleByPort', `Port ${this._stream.port} not found for stream ${this._stream.id}:${this._stream.name}`);\n            return true;\n        }\n\n        // null == cannot be disabled, true == available, false == not available\n        if (port.available !== false) {\n            return true;\n        }\n\n        Log.info(`Hiding ${this._stream.id}:${this._stream.name}, port \"${this._stream.port}\" not available`);\n\n        return false;\n    }\n};\n\n\n/**\n * Slider for system sounds.\n */\nvar EventsSlider = class extends StreamSlider\n{\n    /**\n     * @param {sliderOptions} options\n     * @private\n     */\n    _init(options) {\n        super._init(options);\n\n        this.item.add_style_class_name('events-stream-slider');\n        this.item.secondLine.add_style_class_name('svm-events-slider-line');\n    }\n\n    _updateLabel() {\n        this._label.text = this._stream.name;\n    }\n};\n\n\n/**\n * Slider for input sinks (e.g. media players).\n */\nvar InputSlider = class extends StreamSlider\n{\n    _updateLabel() {\n        let text = this._stream.name;\n        let description = this._stream.description;\n\n        if (description && text !== description) {\n            if (text) {\n                text = `${description} | ${text}`;\n            } else {\n                text = description;\n            }\n        }\n\n        this._label.text = text || '[%s]'.format(__('unknown'));\n    }\n};\n\n\n/**\n * Input stream slider (microphones, etc ?).\n */\nvar InputStreamSlider = class extends StreamSlider\n{\n    /**\n     * @param {sliderOptions} options\n     */\n    _init(options) {\n        super._init(options);\n\n        this._showInput = false;\n\n        this._slider.accessible_name = _('Microphone');\n        this._streamAddedId = this._control.connect('stream-added', this._maybeShowInput.bind(this));\n        this._streamRemovedId = this._control.connect('stream-removed', this._maybeShowInput.bind(this));\n\n        this._icon.icon_name = 'audio-input-microphone-symbolic';\n        this._icons = [\n            'microphone-sensitivity-muted-symbolic',\n            'microphone-sensitivity-low-symbolic',\n            'microphone-sensitivity-medium-symbolic',\n            'microphone-sensitivity-high-symbolic',\n        ];\n    }\n\n    _connectStream(stream) {\n        Volume.InputStreamSlider.prototype._connectStream.apply(this, [stream]);\n        this.refresh();\n    }\n\n    _maybeShowInput() {\n        if (this.options.showAlways === true) {\n            this._showInput = true;\n            this._updateVisibility();\n        } else {\n            // we extend from output stream slider impl, but this is an input stream slider\n            Volume.InputStreamSlider.prototype._maybeShowInput.call(this);\n        }\n    }\n\n    _shouldBeVisible() {\n        return Volume.InputStreamSlider.prototype._shouldBeVisible.call(this);\n    }\n\n    isVisible() {\n        return this._shouldBeVisible();\n    }\n\n    _updateLabel() {\n        this._label.text = _('Microphone');\n    }\n\n    _updateSliderIcon() {\n        if (this._stream && !this.options.symbolicIcons) {\n            this._icon.gicon = this._stream.get_gicon();\n        } else {\n            this._icon.icon_name = 'audio-input-microphone-symbolic';\n        }\n\n        this.emit('stream-updated');\n    }\n\n    _onDestroy() {\n        if (this._streamAddedId) {\n            this._control.disconnect(this._streamAddedId);\n        }\n\n        if (this._streamRemovedId) {\n            this._control.disconnect(this._streamRemovedId);\n        }\n\n        super._onDestroy();\n    }\n};\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/cs/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# Czech translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2017\n# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Daniel Rusek <mail@asciiwolf.com>\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: GNOME Shell Volume Mixer\\n\"\n\"PO-Creation-Date: 2017-11-07 23:12+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:37+0100\\n\"\n\"Last-Translator: Daniel Rusek <mail@asciiwolf.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: cs\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Poedit-Basepath: ..\\n\"\n\"X-Poedit-KeywordsList: ;__\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\"X-Poedit-SearchPath-0: .\\n\"\n\"X-Poedit-SearchPathExcluded-0: locale\\n\"\n\"X-Poedit-SearchPathExcluded-1: pautils\\n\"\n\"X-Poedit-SearchPathExcluded-2: schemas\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Chyba při získávání detailů o kartě\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Pomocný skript nevrátil platná data o kartě.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Vstupy\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"neznámé\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Umístění směšovače hlasitosti\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Stavové menu\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Vlevo\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Uprostřed\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Vpravo\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Odstranit původní posuvníky\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Zobrazit detailní posuvníky\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Klávesová zkratka pro přepínač profilů\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(např. „<Super>p”)\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Použít symbolické ikony\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Zobrazit systémové zvuky\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Zobrazit virtuální proudy\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Vždy zobrazit vstupní proudy\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"Zobrazí posuvník i pokud žádná aplikace nenahrává zvuk\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Nastavení\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Zařízení\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Karta\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Profil\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Přepínač\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Zobrazit\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/de/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# German translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2014\n# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Jonatan Zedler <jonatan_zedler@gmx.de>, 2014.\n# Alexander Hofbauer <alex@derhofbauer.at>, 2014.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: GNOME Shell Volume Mixer\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Creation-Date: 2014-11-04 21:00+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:35+0100\\n\"\n\"Last-Translator: Alexander Hofbauer <alex@derhofbauer.at>\\n\"\n\"Language-Team: German\\n\"\n\"Language: de\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Fehler beim Abfragen der Karten\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Hilfsprogramm gab keine gültigen Daten zurück.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"Abfrage der PulseAudio-Soundkarten fehlgeschlagen, Erweiterung deaktiviert\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Eingänge\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"unbekannt\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Position des Lautstärkereglers\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Statusmenü\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Links\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Zentriert\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Rechts\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Originalen Lautstärkeregler entfernen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Details an den Schiebereglern anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Hotkey für Profilumschalter\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(z.B. \\\"<Super>p\\\")\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Symbolische Icons verwenden\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Systemklänge anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Virtuelle Streams anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Eingänge immer anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"\"\n\"Zeigt Aufnahmeströme (Mikrofone) immer an, auch wenn keine Applikation \"\n\"gerade aufnimmt\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"Prozent anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"Lautstärke des aktuellen Ausgangs in Prozent neben dem Icon anzeigen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Einstellungen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Geräte\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Karte\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Profil\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Umschalter\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Anzeigen\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/it/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Gianvito Cavasoli <gianvito@gmx.it>, 2017.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: GNOME Shell Volume Mixer\\n\"\n\"PO-Creation-Date: 2017-11-07 23:12+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:37+0100\\n\"\n\"Last-Translator: Gianvito Cavasoli <gianvito@gmx.it>\\n\"\n\"Language-Team: \\n\"\n\"Language: it\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Errore nel recuperare le informazioni sulla scheda\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Lo script di aiuto non ha restituito dati della scheda validi\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Sorgenti\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"sconosciuto\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Posizione mixer del volume\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Menù di stato\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Sinistra\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Centro\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Destra\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Rimuovere cursore di scorrimento originale\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Mostrare cursore di scorrimento dettagliato\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Scorciatoia selettore del profilo\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(esempio «<Super>p»)\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Usare le icone simboliche\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Mostra i suoni di sistema\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Mostrare i flussi virtuali\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Mostrare sempre i flussi di input\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"Mostra il cursore di scorrimento anche se nessuna applicazione sta registrando\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Impostazioni\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Dispositivi\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Scheda\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Profilo\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Attiva\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Visualizza\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/nl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Heimen Stoffels <vistausss@outlook.com>, 2020.\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: \\n\"\n\"PO-Creation-Date: 2020-10-26 13:47+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:37+0100\\n\"\n\"Last-Translator: Heimen Stoffels <vistausss@outlook.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: nl\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Fout tijdens ophalen van kaartinformatie\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Het hulpscript koppelde geen geldige kaartinformatie terug.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\"Het opvragen van PulseAudio-geluidskaarten is mislukt - de uitbreiding wordt uitgeschakeld\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Invoerbronnen\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"onbekend\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Positie van volumemixer\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Statusmenu\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Links\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Midden\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Rechts\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"GNOME's schuifbalk verbergen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Uitgebreide schuifbalken tonen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Sneltoets om te wisselen van profiel\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(bijv. '<Super>p')\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Symbolische pictogrammen gebruiken\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Systeemgeluiden tonen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Virtuele streams tonen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Altijd invoerstreams tonen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"Toon de schuifbalk ook als er geen opnames plaatsvinden\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"Percentage tonen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"Toon het volumepercentage van de huidige uitvoerbron op het pictogram\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Instellingen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Apparaten\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Kaart\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Profiel\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Wisselen\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Weergave\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/pl/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# Polish translation for GNOME Shell Volume Mixer.\n# Copyright (C) 2017\n# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Piotr Komur, pkomur@gmail.com, 2017.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: GNOME Shell Volume Mixer\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Creation-Date: 2017-11-07 23:12+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:36+0100\\n\"\n\"Last-Translator: Piotr Komur <pkomur@gmail.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: pl_PL\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\\n\"\n\"X-Poedit-Basepath: .\\n\"\n\"X-Poedit-KeywordsList: _;title;label\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Błąd podczas pobierania danych karty\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Narzędzie nie zwróciło prawidłowych danych.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Wejścia\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"nieznany\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Położenie miksera\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Menu systemowe\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Z lewej\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Po środku\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Z prawej\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Ukryj systemowy regulator\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Pokaż szczegóły urządzenia\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Skrót klawiszowy przełączania profilów\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(np. \\\"<Super>p\\\")\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Pokaż ikony symboliczne\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Pokaż dźwięki systemowe\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Pokaż strumienie wirtualne\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Pokaż strumienie wejściowe\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"(Pokazuje regulatory, nawet przy braku aktywnych programów nagrywających)\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Ustawienia\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Urządzenia\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Karta\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Profil\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Przełącznik\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Wyświetlacz\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/pt_BR/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# Brazilian Portuguese translation for GNOME Shell Volume Mixer.\n# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Ricardo Silva Veloso <ricvelozo@gmail.com>, 2018.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: GNOME Shell Volume Mixer\\n\"\n\"PO-Creation-Date: 2017-11-07 23:12+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:37+0100\\n\"\n\"Last-Translator: Ricardo Silva Veloso <ricvelozo@gmail.com>\\n\"\n\"Language-Team: \\n\"\n\"Language: pt_BR\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\"X-Poedit-Basepath: ..\\n\"\n\"X-Poedit-KeywordsList: ;__\\n\"\n\"X-Generator: Poedit 3.0\\n\"\n\"X-Poedit-SourceCharset: UTF-8\\n\"\n\"X-Poedit-SearchPath-0: .\\n\"\n\"X-Poedit-SearchPathExcluded-0: locale\\n\"\n\"X-Poedit-SearchPathExcluded-1: pautils\\n\"\n\"X-Poedit-SearchPathExcluded-2: schemas\\n\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Erro ao recuperar detalhes da placa\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"O script ajudante não retornou dados de placa válidos.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Entradas\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"desconhecido\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Posição do mixador de volume\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"Menu de Status\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Esquerda\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"Centro\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Direita\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Remover controle deslizante\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Exibir controles deslizantes detalhados\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Tecla de atalho para seletor de perfil\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(ex. \\\"<Super>p\\\")\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Usar ícones simbólicos\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Exibir sons do sistema\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Exibir fluxos virtuais\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Sempre exibir fluxos de entrada\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"Exibe o controle deslizante mesmo se nenhum aplicativo estiver gravando\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Configurações\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Dispositivos\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Placa\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Perfil\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Ativar\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Mostrar\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/ru/LC_MESSAGES/gnome-shell-extensions-shell-volume-mixer.po",
    "content": "# This file is distributed under the same license as the GNOME Shell Volume Mixer.\n# Alexandr K <Alexandr1322@yandex.ua>, 2021.\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: gnome-shell-volume-mixer\\n\"\n\"PO-Creation-Date: 2021-11-03 18:39+0100\\n\"\n\"PO-Revision-Date: 2021-12-08 11:37+0100\\n\"\n\"Last-Translator: Alexandr K <Alexandr1322@yandex.ua>\\n\"\n\"Language-Team: Alexandr K <Alexandr1322@yandex.ua>\\n\"\n\"Language: ru\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: Poedit 3.0\\n\"\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\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"Ошибка при получении сведений о карте\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"Вспомогательный скрипт не вернул действительные данные карты.\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"Сбой запроса звуковых карт PulseAudio, отключение расширения\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"Входы\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"неизвестно\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"Позиция volume mixer\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"В меню состояния\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"Слева\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"По центру\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"Справо\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"Скрыть оригинальный ползунок\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"Показывать детали под ползунком\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"Горячая клавиша для переключателя профилей\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"(например, \\\"<Super>p\\\")\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"Использовать символьные иконки\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"Показывать системные звуки\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"Показать действующие устройства\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"Всегда показывать устройства ввода\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"Показывает ползунок, даже если приложение не записывает\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"Показать процент громкости\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"Показывать ли процент от текущего выходного сигнала справа от значка\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"Общие настройки\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"Устройства вывода\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"Карты\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"Профиль\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"Переключить\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"Показать\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/locale/translations.pot",
    "content": "#: shell-volume-mixer@derhofbauer.at/prefs.js:219\nmsgid \"Error retrieving card details\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.js:220\nmsgid \"Helper script did not return valid card data.\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/utils/cards.js:116\nmsgid \"Querying PulseAudio sound cards failed, disabling extension\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:323\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:325\nmsgid \"Inputs\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/lib/widget/volume.js:541\nmsgid \"unknown\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:39\nmsgid \"Position of volume mixer\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:53\nmsgid \"Status Menu\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:54\nmsgid \"Left\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:55\nmsgid \"Center\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:56\nmsgid \"Right\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:69\nmsgid \"Remove original slider\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:81\nmsgid \"Show detailed sliders\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:116\nmsgid \"Hotkey for profile switcher\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:137\nmsgid \"(e.g. \\\"<Super>p\\\")\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:149\nmsgid \"Use symbolic icons\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:172\nmsgid \"Show system sounds\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:195\nmsgid \"Show virtual streams\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:218\nmsgid \"Always show input streams\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:240\nmsgid \"Shows the slider even if no application is recording\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:288\nmsgid \"Show percentage\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:310\nmsgid \"Whether to show percentage of current output right of the icon\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:340\nmsgid \"Settings\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:372\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:502\nmsgid \"Devices\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:413\nmsgid \"Card\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:427\nmsgid \"Profile\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:440\nmsgid \"Switch\"\nmsgstr \"\"\n\n#: shell-volume-mixer@derhofbauer.at/prefs.ui:455\nmsgid \"Display\"\nmsgstr \"\"\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/metadata.json",
    "content": "{\n    \"version\": 9999,\n    \"uuid\": \"shell-volume-mixer@derhofbauer.at\",\n    \"name\": \"Volume Mixer\",\n    \"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.\",\n    \"shell-version\": [\n        \"42\"\n    ],\n    \"settings-schema\": \"org.gnome.shell.extensions.shell-volume-mixer\",\n    \"gettext-domain\": \"gnome-shell-extensions-shell-volume-mixer\",\n    \"url\": \"https://github.com/aleho/gnome-shell-volume-mixer\"\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/__init__.py",
    "content": ""
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/cards.py",
    "content": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nfrom .pulseaudio import Pulseaudio\nfrom . import log\nfrom . import libpulse\n\n\nclass Cards(Pulseaudio):\n    def build_callback(self):\n        return libpulse.card_info_cb_t(self.pa_cb)\n\n    def get_by_index(self, index, callback):\n        return libpulse.context_get_card_info_by_index(self._context, index, callback, None)\n\n    def get_by_name(self, name, callback):\n        return libpulse.context_get_card_info_by_name(self._context, name, callback, None)\n\n    def get_all(self, callback):\n        return libpulse.context_get_card_info_list(self._context, callback, None)\n\n    def cb_data(self, pa_card):\n        card_name = pa_card.name.decode('utf8')\n\n        try:\n            alsa_card = libpulse.proplist_gets(pa_card.proplist, b'alsa.card')\n            alsa_card = int(alsa_card.decode('utf8')) if alsa_card else None\n        except Exception:\n            log.debug('No property \"alsa.card\"')\n            alsa_card = None\n\n        try:\n            description = libpulse.proplist_gets(pa_card.proplist, b'device.description')\n            description = description.decode('utf8') if description else None\n        except Exception:\n            log.debug('No property \"device.description\"')\n            description = None\n\n        if not description:\n            try:\n                description = libpulse.proplist_gets(pa_card.proplist, b'alsa.card_name')\n                description = description.decode('utf8') if description else None\n            except Exception:\n                log.debug('No property \"alsa.card_name\"')\n                description = card_name\n\n        card = {\n            'index': pa_card.index,\n            'alsaCard': alsa_card,\n            'name': card_name,\n            'description': description,\n            'active_profile': None,\n            'profiles': {\n            },\n            'ports': {\n            },\n        }\n\n        if pa_card.active_profile and pa_card.active_profile[0]:\n            ap = pa_card.active_profile[0]\n            card['active_profile'] = ap.name.decode('utf8')\n\n        for i in range(0, pa_card.n_profiles):\n            if not pa_card.profiles2[i] or not pa_card.profiles2[i][0]:\n                continue\n\n            profile = pa_card.profiles2[i][0]\n            name = profile.name.decode('utf8')\n\n            card['profiles'][name] = {\n                'name': name,\n                'description': profile.description.decode('utf8'),\n                'available': bool(profile.available),\n            }\n\n        for index in range(0, pa_card.n_ports):\n            if not pa_card.ports[index] or not pa_card.ports[index][0]:\n                continue\n\n            port = pa_card.ports[index][0]\n            name = port.name.decode('utf8')\n\n            card['ports'][name] = {\n                'name': name,\n                'description': port.description.decode('utf8'),\n                'direction': 'out' if port.direction == 1 else 'in',\n                'available': True if port.available == 2 else (False if port.available == 1 else None),\n            }\n\n        return card\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/libpulse.py",
    "content": "# This library was generated through introspection of\n#     /usr/include/pulse/introspect.h and\n#     /usr/include/pulse/mainloop.h.\n#     (E.g.:\n#         python /usr/share/pyshared/ctypeslib/h2xml.py /usr/include/pulse/introspect.h -o pa.xml\n#         python /usr/share/pyshared/ctypeslib/xml2py.py pa.xml -k f -l /usr/lib/libpulse.so -o pa.py\n#     )\n#\n# License according to the header files listed above:\n# GNU Lesser General Public License\n#\n# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2014 Alexander Hofbauer <alex@derhofbauer.at>\n\n\nfrom ctypes import *\n\ntry:\n    lib = CDLL('libpulse.so.0')\nexcept:\n    lib = CDLL('libpulse.so')\n\nSTRING = c_char_p\nWSTRING = c_wchar_p\n\nclass mainloop(Structure):\n    pass\n\nclass mainloop_api(Structure):\n    pass\n\nclass spawn_api(Structure):\n    pass\n\nclass context(Structure):\n    pass\n\nclass operation(Structure):\n    pass\n\nclass proplist(Structure):\n    pass\n\nclass card_profile_info(Structure):\n    _fields_ = [\n        ('name', STRING),\n        ('description', STRING),\n        ('n_sinks', c_uint32),\n        ('n_sources', c_uint32),\n        ('priority', c_uint32),\n    ]\n\nclass card_profile_info2(Structure):\n    _fields_ = [\n        ('name', STRING),\n        ('description', STRING),\n        ('n_sinks', c_uint32),\n        ('n_sources', c_uint32),\n        ('priority', c_uint32),\n        ('available', c_int),\n    ]\n\nclass card_port_info(Structure):\n    _fields_ = [\n        ('name', STRING),\n        ('description', STRING),\n        ('priority', c_uint32),\n        ('available', c_int),\n        ('direction', c_int),\n        ('n_profiles', c_uint32),\n        ('profiles', POINTER(POINTER(card_profile_info))),\n        ('proplist', POINTER(proplist)),\n        ('latency_offset', c_int64),\n        ('profiles2', POINTER(POINTER(card_profile_info2))),\n    ]\n\nclass card_info(Structure):\n    _fields_ = [\n        ('index', c_uint32),\n        ('name', STRING),\n        ('owner_module', c_uint32),\n        ('driver', STRING),\n        ('n_profiles', c_uint32),\n        ('profiles', POINTER(card_profile_info)),\n        ('active_profile', POINTER(card_profile_info)),\n        ('proplist', POINTER(proplist)),\n        ('n_ports', c_uint32),\n        ('ports', POINTER(POINTER(card_port_info))),\n        ('profiles2', POINTER(POINTER(card_profile_info2))),\n        ('active_profile2', POINTER(card_profile_info2)),\n    ]\n\nclass sink_port_info(Structure):\n    _fields_ = [\n        ('name', STRING),\n        ('description', STRING),\n        ('priority', c_uint32),\n        ('available', c_int),\n        ('availability_group', STRING),\n        ('type', c_uint32),\n    ]\n\nclass format_info(Structure):\n    _fields_ = [\n        ('encoding', c_int),\n        ('plist', POINTER(proplist)),\n    ]\n\nclass sample_spec(Structure):\n    _fields_ = [\n        ('format', c_int),\n        ('rate', c_uint32),\n        ('channels', c_uint8),\n    ]\n\nclass pa_channel_map(Structure):\n    _fields_ = [\n        ('channels', c_uint8),\n        ('map', c_int * int(32)),\n    ]\n\nclass pa_cvolume(Structure):\n    _fields_ = [\n        ('channels', c_uint8),\n        ('values', c_uint32 * int(32)),\n    ]\n\nclass sink_info(Structure):\n    _fields_ = [\n        ('name', STRING),\n        ('index', c_uint32),\n        ('description', STRING),\n        ('sample_spec', sample_spec),\n        ('channel_map', pa_channel_map),\n        ('owner_module', c_uint32),\n        ('volume', pa_cvolume),\n        ('mute', c_int),\n        ('monitor_source', c_uint32),\n        ('monitor_source_name', STRING),\n        ('latency', c_uint64),\n        ('driver', STRING),\n        ('flags', c_int),\n        ('proplist', POINTER(proplist)),\n        ('configured_latency', c_uint64),\n        ('base_volume', c_uint32),\n        ('state', c_int),\n        ('n_volume_steps', c_uint32),\n        ('card', c_uint32),\n        ('n_ports', c_uint32),\n        ('ports', POINTER(POINTER(sink_port_info))),\n        ('active_port', POINTER(sink_port_info)),\n        ('n_formats', c_uint8),\n        ('formats', POINTER(POINTER(format_info))),\n    ]\n\nmainloop_new = lib.pa_mainloop_new\nmainloop_new.restype = POINTER(mainloop)\nmainloop_new.argtypes = []\nmainloop_get_api = lib.pa_mainloop_get_api\nmainloop_get_api.restype = POINTER(mainloop_api)\nmainloop_get_api.argtypes = [POINTER(mainloop)]\nmainloop_iterate = lib.pa_mainloop_iterate\nmainloop_iterate.restype = c_int\nmainloop_iterate.argtypes = [POINTER(mainloop), c_int, POINTER(c_int)]\nmainloop_free = lib.pa_mainloop_free\nmainloop_free.restype = None\nmainloop_free.argtypes = [POINTER(mainloop)]\n\ncontext_flags = c_int  # enum\ncontext_flags_t = context_flags\n\ncontext_state = c_int  # enum\ncontext_state_t = context_state\n# values for enumeration 'context_state'\nCONTEXT_UNCONNECTED = 0\nCONTEXT_CONNECTING = 1\nCONTEXT_AUTHORIZING = 2\nCONTEXT_SETTING_NAME = 3\nCONTEXT_READY = 4\nCONTEXT_FAILED = 5\nCONTEXT_TERMINATED = 6\n\ncontext_new = lib.pa_context_new\ncontext_new.restype = POINTER(context)\ncontext_new.argtypes = [POINTER(mainloop_api), STRING]\ncontext_notify_cb_t = CFUNCTYPE(None, POINTER(context), c_void_p)\ncontext_set_state_callback = lib.pa_context_set_state_callback\ncontext_set_state_callback.restype = None\ncontext_set_state_callback.argtypes = [POINTER(context), context_notify_cb_t, c_void_p]\ncontext_connect = lib.pa_context_connect\ncontext_connect.restype = c_int\ncontext_connect.argtypes = [POINTER(context), STRING, context_flags_t, POINTER(spawn_api)]\ncontext_disconnect = lib.pa_context_disconnect\ncontext_disconnect.restype = None\ncontext_disconnect.argtypes = [POINTER(context)]\ncontext_unref = lib.pa_context_unref\ncontext_unref.restype = None\ncontext_unref.argtypes = [POINTER(context)]\ncontext_get_state = lib.pa_context_get_state\ncontext_get_state.restype = context_state_t\ncontext_get_state.argtypes = [POINTER(context)]\n\noperation_unref = lib.pa_operation_unref\noperation_unref.restype = None\noperation_unref.argtypes = [POINTER(operation)]\n\ncard_info_cb_t = CFUNCTYPE(None, POINTER(context), POINTER(card_info), c_int, c_void_p)\ncontext_get_card_info_by_index = lib.pa_context_get_card_info_by_index\ncontext_get_card_info_by_index.restype = POINTER(operation)\ncontext_get_card_info_by_index.argtypes = [POINTER(context), c_uint32, card_info_cb_t, c_void_p]\ncontext_get_card_info_by_name = lib.pa_context_get_card_info_by_name\ncontext_get_card_info_by_name.restype = POINTER(operation)\ncontext_get_card_info_by_name.argtypes = [POINTER(context), STRING, card_info_cb_t, c_void_p]\ncontext_get_card_info_list = lib.pa_context_get_card_info_list\ncontext_get_card_info_list.restype = POINTER(operation)\ncontext_get_card_info_list.argtypes = [POINTER(context), card_info_cb_t, c_void_p]\n\nsink_info_cb_t = CFUNCTYPE(None, POINTER(context), POINTER(sink_info), c_int, c_void_p)\ncontext_get_sink_info_by_index = lib.pa_context_get_sink_info_by_index\ncontext_get_sink_info_by_index.restype = POINTER(operation)\ncontext_get_sink_info_by_index.argtypes = [POINTER(context), c_uint32, sink_info_cb_t, c_void_p]\ncontext_get_sink_info_by_name = lib.pa_context_get_sink_info_by_name\ncontext_get_sink_info_by_name.restype = POINTER(operation)\ncontext_get_sink_info_by_name.argtypes = [POINTER(context), STRING, sink_info_cb_t, c_void_p]\ncontext_get_sink_info_list = lib.pa_context_get_sink_info_list\ncontext_get_sink_info_list.restype = POINTER(operation)\ncontext_get_sink_info_list.argtypes = [POINTER(context), sink_info_cb_t, c_void_p]\n\nproplist_gets = lib.pa_proplist_gets\nproplist_gets.restype = STRING\nproplist_gets.argtypes = [POINTER(proplist), STRING]\n\nproplist_to_string = lib.pa_proplist_to_string\nproplist_to_string.restype = STRING\nproplist_to_string.argtypes = [POINTER(proplist)]\n\n# this is a \"magic\" number (probably -1 at int32, but unsigned?) and tells us there's something fishy\nNULL_ID = 4294967295\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/log.py",
    "content": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport os\nimport sys\n\nDEBUG = True if 'DEBUG' in os.environ else False\n\n\ndef debug(*msg):\n    if DEBUG:\n        print(*msg, file=sys.stderr)\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/pulseaudio.py",
    "content": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport abc\nfrom time import sleep\n\nfrom . import log\nfrom . import libpulse\n\n\nclass Pulseaudio:\n    _pa_state = libpulse.CONTEXT_UNCONNECTED\n    _op_done = False\n\n    _iterations_max = 10000\n    _iterations_intrv = 0.0001\n\n\n    _data = {}\n    _error = {\n        'success': False,\n        'error': None,\n    }\n\n    def __init__(self):\n        self._pa_mainloop = libpulse.mainloop_new()\n        self._pa_mainloop_api = libpulse.mainloop_get_api(self._pa_mainloop)\n\n        self._context = libpulse.context_new(self._pa_mainloop_api, b'ShellVolumeMixer')\n        self._context_notify_cb = libpulse.context_notify_cb_t(self.context_notify_cb)\n\n        libpulse.context_set_state_callback(self._context, self._context_notify_cb, None)\n\n    def __enter__(self):\n        libpulse.context_connect(self._context, None, 0, None)\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        libpulse.context_disconnect(self._context)\n        libpulse.context_unref(self._context)\n        libpulse.mainloop_free(self._pa_mainloop)\n\n    def context_notify_cb(self, context, userdata):\n        try:\n            self._pa_state = libpulse.context_get_state(context)\n        except Exception:\n            self._pa_state = libpulse.CONTEXT_FAILED\n            log.debug('Context failed')\n\n    def get_info(self, index=None, name=None):\n        log.debug('Querying details...')\n        operation = None\n        count = 0\n\n        while self._pa_state != libpulse.CONTEXT_TERMINATED:\n            if self._op_done:\n                break\n\n            if self._pa_state == libpulse.CONTEXT_FAILED:\n                self._data = self._error\n                self._data['error'] = 'context failed'\n                break\n\n            if count >= self._iterations_max:\n                log.debug(f'Stopping iterations after {self._iterations_intrv * self._iterations_max}s (bug?)')\n                break\n\n            if count > 0 and count % (self._iterations_max / 2) == 0:\n                log.debug(f'Delaying after {self._iterations_intrv * count}s (bug?)')\n                sleep(2)\n\n            if self._pa_state == libpulse.CONTEXT_READY and operation is None:\n                callback = self.build_callback()\n\n                if name:\n                    log.debug('Requesting details for', name)\n                    operation = self.get_by_name(name.encode('utf8'), callback)\n\n                elif index and index >= 0:\n                    log.debug('Requesting details for', index)\n                    operation = self.get_by_index(index, callback)\n\n                else:\n                    log.debug('Requesting all available data')\n                    operation = self.get_all(callback)\n\n            libpulse.mainloop_iterate(self._pa_mainloop, 0, None)\n            count += 1\n            sleep(self._iterations_intrv)\n\n        if operation:\n            libpulse.operation_unref(operation)\n\n        log.debug('Query done')\n\n        return self._data\n\n    def pa_cb(self, context, struct, eol, user_data):\n        log.debug('In callback')\n\n        if eol:\n            self._op_done = True\n            log.debug('All done')\n            return\n\n        if not struct or not struct[0]:\n            log.debug('No data received for callback')\n            return\n\n        item = self.cb_data(struct[0])\n\n        if item and 'index' in item:\n            self._data[item['index']] = item\n\n        log.debug('Callback done')\n\n    @abc.abstractmethod\n    def build_callback(self):\n        return\n\n    @abc.abstractmethod\n    def get_by_index(self, index, callback):\n        return\n\n    @abc.abstractmethod\n    def get_by_name(self, name, callback):\n        return\n\n    @abc.abstractmethod\n    def get_all(self, callback):\n        return\n\n    @abc.abstractmethod\n    def cb_data(self, data):\n        return None\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/lib/sinks.py",
    "content": "# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nfrom .pulseaudio import Pulseaudio\nfrom . import log\nfrom . import libpulse\n\n\nclass Sinks(Pulseaudio):\n    def build_callback(self):\n        return libpulse.sink_info_cb_t(self.pa_cb)\n\n    def get_by_index(self, index, callback):\n        return libpulse.context_get_sink_info_by_index(self._context, index, callback, None)\n\n    def get_by_name(self, name, callback):\n        return libpulse.context_get_sink_info_by_name(self._context, name, callback, None)\n\n    def get_all(self, callback):\n        return libpulse.context_get_sink_info_list(self._context, callback, None)\n\n    def cb_data(self, pa_sink):\n        sink_name = pa_sink.name.decode('utf8')\n        description = pa_sink.description.decode('utf8')\n\n        if not description:\n            try:\n                description = libpulse.proplist_gets(pa_sink.proplist, b'device.description')\n                description = description.decode('utf8') if description else None\n            except Exception:\n                log.debug('No property \"device.description\"')\n                description = sink_name\n\n        try:\n            alsa_card = libpulse.proplist_gets(pa_sink.proplist, b'alsa.card')\n            alsa_card = int(alsa_card.decode('utf8')) if alsa_card else None\n        except Exception:\n            log.debug('No property \"alsa.card\"')\n            alsa_card = None\n\n        sink = {\n            'index': pa_sink.index,\n            'alsaCard': alsa_card,\n            'name': sink_name,\n            'description': description,\n            'card': pa_sink.card if pa_sink.card != libpulse.NULL_ID else None,\n            'active_port': None,\n            'ports': {\n            },\n        }\n\n        if pa_sink.active_port and pa_sink.active_port[0]:\n            ap = pa_sink.active_port[0]\n            sink['active_port'] = ap.name.decode('utf8')\n\n        for index in range(0, pa_sink.n_ports):\n            if not pa_sink.ports[index] or not pa_sink.ports[index][0]:\n                continue\n\n            port = pa_sink.ports[index][0]\n            name = port.name.decode('utf8')\n\n            sink['ports'][name] = {\n                'name': name,\n                'description': port.description.decode('utf8'),\n                'type': port.type,\n                'available': True if port.available == 2 else (False if port.available == 1 else None),\n            }\n\n        return sink\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/pautils/query.py",
    "content": "#!/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 object or an array of all data available, depending on\n# whether an index / name was passed or no parameters at all.\n#\n# This file is part of GNOME Shell Volume Mixer\n# Copyright (C) 2021 Alexander Hofbauer <alex@derhofbauer.at>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport json\nimport sys\n\nfrom lib import log\nfrom lib.cards import Cards\nfrom lib.sinks import Sinks\nfrom lib.libpulse import NULL_ID\n\nif len(sys.argv) < 2:\n    print('Need a type to query')\n    sys.exit(1)\n\nop_type = sys.argv[1]\nindex = name = None\n\nif len(sys.argv) > 2:\n    filter_arg = sys.argv[2]\n    if filter_arg.isdigit():\n        index = int(filter_arg)\n    else:\n        name = filter_arg\n\n\nif index == NULL_ID:\n    result = {}\n\nelif op_type == 'cards':\n    with Cards() as cards:\n        result = cards.get_info(index=index, name=name)\n\nelif op_type == 'sinks':\n    with Sinks() as sinks:\n        result = sinks.get_info(index=index, name=name)\n\nelse:\n    print('Invalid type', op_type, 'requested')\n    sys.exit(1)\n\n\nprint(json.dumps(result, indent=4 if log.DEBUG else None))\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/prefs.js",
    "content": "/**\n * Shell Volume Mixer\n *\n * Preferences widget.\n *\n * @author Alexander Hofbauer <alex@derhofbauer.at>\n */\n\n/* exported init, buildPrefsWidget */\n\nconst { Gtk, GObject } = imports.gi;\nconst ExtensionUtils = imports.misc.extensionUtils;\nconst Lib = ExtensionUtils.getCurrentExtension().imports.lib;\nconst __ = ExtensionUtils.gettext;\n\nconst { Settings, SETTING } = Lib.settings;\nconst Log = Lib.utils.log;\nconst PaHelper = Lib.utils.paHelper;\nconst Utils = Lib.utils.utils;\n\nlet preferences;\n\n\nconst WIDGETS = {\n    cmbPosition:              SETTING.position,\n    swRemoveOriginal:         SETTING.remove_original,\n    swShowPercentageLabel:    SETTING.show_percentage_label,\n    swShowDetailedSliders:    SETTING.show_detailed_sliders,\n    swShowSystemSounds:       SETTING.show_system_sounds,\n    swShowVirtualStreams:     SETTING.show_virtual_streams,\n    swAlwaysShowInputStreams: SETTING.always_show_input_streams,\n    swUseSymbolicIcons:       SETTING.use_symbolic_icons,\n    txtProfileSwitch:         null,\n    treeDevices:              null,\n    treePinned:               null,\n    btnAddDevice:             null,\n    btnRemoveDevice:          null,\n};\n\n\nconst Preferences = GObject.registerClass({\n    Implements: [ Gtk.BuilderScope ],\n}, class Preferences extends GObject.Object\n{\n    vfunc_create_closure(builder, handlerName, flags, connectObject) {\n        if (flags & Gtk.BuilderClosureFlags.SWAPPED)\n            throw new Error('Unsupported template signal flag \"swapped\"');\n\n        if (typeof this[handlerName] === 'undefined')\n            throw new Error(`${handlerName} is undefined`);\n\n        return this[handlerName].bind(connectObject || this);\n    }\n\n    _getId(object) {\n        for (const [id, widget] of Object.entries(this._widgets)) {\n            if (widget === object) {\n                return id;\n            }\n        }\n\n        return null;\n    }\n\n    _getSetting(object) {\n        const id = this._getId(object);\n\n        if (id) {\n            return this._widgetSettings[id] || null;\n        }\n\n        return null;\n    }\n\n    toggleBoolean(object) {\n        const setting = this._getSetting(object);\n\n        if (!setting) {\n            Log.error('Preferences', 'toggleBoolean', new Error(`BUG! No widget found for toggle handler (${this._getId(object)})`));\n            return;\n        }\n\n        const active = object.get_active();\n        this._settings.set_boolean(setting, active);\n    }\n\n    /**\n     * Callback for menu position combobox.\n     *\n     * @param cmbPosition\n     */\n    onPositionChanged(cmbPosition) {\n        let value = cmbPosition.get_active();\n\n        this._settings.set_enum(SETTING.position, value);\n\n        let checkbox = this._widgets.swRemoveOriginal;\n        if (!checkbox) {\n            return;\n        }\n\n        if (value === SETTING.position_at.menu) {\n            checkbox.set_sensitive(false);\n        } else {\n            checkbox.set_sensitive(true);\n        }\n    }\n\n    /**\n     * Callback for hotkey button entry.\n     *\n     * @param widget\n     */\n    onProfileSwitchChanged(widget) {\n        let entry = widget.get_text().trim();\n\n        if (!entry) {\n            this._settings.set_array(SETTING.profile_switcher_hotkey, []);\n            return;\n        }\n\n        try {\n            let [ok, key, mods] = Gtk.accelerator_parse(entry);\n\n            if (!ok || key === 0 || mods === 0) {\n                return;\n            }\n\n            let hotkey = Gtk.accelerator_name(key, mods);\n            if (hotkey) {\n                widget.set_text(hotkey);\n\n                // Main.wm.addKeybinding expects an array\n                this._settings.set_array(SETTING.profile_switcher_hotkey, [hotkey]);\n            }\n\n        } catch (e) {\n            Log.error('Preferences', 'onProfileSwitchChanged', e);\n        }\n    }\n\n    /**\n     * Callback for add button.\n     */\n    onAddDevice(widget) {\n        widget.set_sensitive(false);\n\n        let [isSelected, store, iter] = this._deviceSelection.get_selected();\n\n        if (!isSelected) {\n            return;\n        }\n\n\n        let cardid = store.get_value(iter, 1);\n        let profileid = store.get_value(iter, 2);\n\n        let card = this._cards[cardid];\n        let profile = this._cards[cardid].profiles[profileid];\n\n        if (profile.pinned) {\n            // safety check, we should never reach this anyway\n            return;\n        }\n\n        profile.pinned = true;\n\n        this._pinned.set(this._pinned.append(), [0, 1, 2, 3, 4, 5],\n            [card.description, profile.description, true, true, cardid, profileid]);\n\n        this._storePinned();\n    }\n\n    /**\n     * Callback for remove button.\n     */\n    onRemoveDevice() {\n        let [isSelected, store, iter] = this._pinnedSelection.get_selected();\n\n        if (!isSelected) {\n            return;\n        }\n\n        let cardid = store.get_value(iter, 4);\n        let profileid = store.get_value(iter, 5);\n\n        this._cards[cardid].profiles[profileid].pinned = false;\n\n        store.remove(iter);\n        this._storePinned();\n\n        // now check for the currently selected entry in devices list\n        [isSelected, store, iter] = this._deviceSelection.get_selected();\n\n        if (!isSelected) {\n            return;\n        }\n\n        let cardidSel = store.get_value(iter, 1);\n        let profileidSel = store.get_value(iter, 2);\n\n        if (cardid === cardidSel && profileid === profileidSel) {\n            this._widgets.btnAddDevice.set_sensitive(true);\n        }\n    }\n\n    /**\n     * Callback for notebook tab selection.\n     */\n    onSwitchPage(page, pageNum) {\n        if (this._hasCards || this._cardsWarningShown) {\n            return;\n        }\n\n        if (pageNum === 1) {\n            return;\n        }\n\n        this._showMessage(__('Error retrieving card details'),\n            __('Helper script did not return valid card data.'));\n\n        this._cardsWarningShown = true;\n    }\n\n    /**\n     * Toggle event for quickswitch switches.\n     */\n    onQuickswitchToggled(widget, path) {\n        let active = !widget.active;\n        let [success, iter] = this._pinned.get_iter_from_string(path);\n        if (!success) {\n            return;\n        }\n        this._pinned.set_value(iter, 2, active);\n        this._storePinned();\n    }\n\n    /**\n     * Toggle event for display switches.\n     */\n    onDisplayToggled(widget, path) {\n        let active = !widget.active;\n        let [success, iter] = this._pinned.get_iter_from_string(path);\n        if (!success) {\n            return;\n        }\n        this._pinned.set_value(iter, 3, active);\n        this._storePinned();\n    }\n\n    /**\n     * Determines whether selection allows to enable the remove button.\n     */\n    onPinnedSelectionChanged(selection) {\n        if (selection.count_selected_rows() > 0) {\n            this._widgets.btnRemoveDevice.set_sensitive(true);\n        } else {\n            this._widgets.btnRemoveDevice.set_sensitive(false);\n        }\n    }\n\n    /**\n     * Selection event for devices, before selection is set.\n     */\n    onDeviceSelection(selection, model, path) {\n        return path && path.get_depth() >= 2;\n    }\n\n    /**\n     * Determines whether selection allows to enable the add button.\n     */\n    onDeviceSelectionChanged(selection) {\n        this._widgets.btnAddDevice.set_sensitive(false);\n\n        if (selection.count_selected_rows() <= 0) {\n            return;\n        }\n\n        let [success, store, iter] = selection.get_selected();\n\n        if (!success) {\n            return;\n        }\n\n        let cardid = store.get_value(iter, 1);\n        let profileid = store.get_value(iter, 2);\n\n        if (this._cards[cardid].profiles[profileid].pinned) {\n            // don't allow pinning of already pinned profiles\n            return;\n        }\n\n        this._widgets.btnAddDevice.set_sensitive(true);\n    }\n\n\n\n    _init() {\n        super._init();\n\n        ExtensionUtils.initTranslations();\n\n        this._widgets = {};\n        this._widgetSettings = [];\n\n        this._settings = new Settings();\n\n        this._builder = new Gtk.Builder();\n        this._builder.set_scope(this);\n    }\n\n    buildWidget() {\n        this._builder.add_from_file(Utils.getExtensionPath('prefs.ui'));\n\n        this._tabs = this._builder.get_object('tabs');\n\n        for (const [id, setting] of Object.entries(WIDGETS)) {\n            const widget = this._builder.get_object(id);\n\n            this._widgets[id]        = widget;\n            this._widgetSettings[id] = setting;\n\n            if (!setting) {\n                continue;\n            }\n\n            if (widget instanceof Gtk.ComboBox\n                || widget instanceof Gtk.ComboBoxText\n            ) {\n                widget.set_active(this._settings.get_enum(setting));\n            } else if (widget instanceof Gtk.Switch) {\n                widget.set_active(this._settings.get_boolean(setting));\n            }\n        }\n\n        this._widgets.txtProfileSwitch.set_text(this._settings.get_array(SETTING.profile_switcher_hotkey)[0] || '');\n\n        this._deviceSelection = this._widgets.treeDevices.get_selection();\n        this._deviceSelection.set_select_function(this.onDeviceSelection.bind(this));\n        this._deviceSelection.connect('changed', this.onDeviceSelectionChanged.bind(this));\n\n        this._pinnedSelection = this._widgets.treePinned.get_selection();\n        this._pinnedSelection.connect('changed', this.onPinnedSelectionChanged.bind(this));\n\n        this._devices = this._builder.get_object('storeDevices');\n        this._pinned  = this._builder.get_object('storePinned');\n\n        (async () => {\n            await this._initCards();\n            this._populatePinned();\n        })();\n\n        this.onPositionChanged(this._widgets.cmbPosition);\n\n        return this._tabs;\n    }\n\n\n    /**\n     * Initializes the content of the cards / profiles selection tree.\n     */\n    async _initCards() {\n        this._cards = {};\n        let cards = await PaHelper.getCards();\n\n        let details = this._widgets.swShowDetailedSliders.active;\n\n        for (let k in cards) {\n            let card = cards[k];\n            let row = this._devices.append(null);\n            this._devices.set(row, [0, 1, 2], [card.description, '', '']);\n\n            let profiles = {};\n\n            for (let p in card.profiles) {\n                let profile = card.profiles[p];\n\n                if (profile.name === 'off' || profile.available === false) {\n                    continue;\n                }\n\n                let invalid = false;\n\n                let test = profile.name.split('+');\n                for (let parts of test) {\n                    let [part] = parts.split(':', 1);\n                    // profiles containing 'input' won't be accepted by Gvc\n                    if (part === 'input') {\n                        invalid = true;\n                        break;\n                    }\n                }\n\n                if (invalid) {\n                    continue;\n                }\n\n                let profiletext = profile.description;\n                if (details) {\n                    profiletext += `\\n${profile.name}`;\n                }\n\n                this._devices.set(this._devices.append(row), [0, 1, 2],\n                    [profiletext, card.name, profile.name]);\n\n                profiles[profile.name] = {\n                    description: profile.description,\n                    pinned: false\n                };\n            }\n\n            this._hasCards = true;\n\n            this._cards[card.name] = {\n                description: card.description,\n                profiles: profiles\n            };\n        }\n\n        this._widgets.treeDevices.expand_all();\n    }\n\n    /**\n     * Updates the content of the selection list with all values from the\n     * settings key.\n     */\n    _populatePinned() {\n        let pinned = this._settings.get_array(SETTING.pinned_profiles);\n        this._pinned.clear();\n\n        for (let item of pinned) {\n            if (!item) {\n                continue;\n            }\n            let entry = null;\n            try {\n                entry = JSON.parse(item);\n            } catch (e) {\n                Log.error('Preferences', '_populatePinned', e);\n            }\n            if (!entry || !entry.card || !entry.profile) {\n                continue;\n            }\n\n            let card = this._cards[entry.card];\n            if (!card) {\n                continue;\n            }\n\n            let profile = card.profiles[entry.profile];\n            if (!profile) {\n                continue;\n            }\n\n            profile.pinned = true;\n\n            this._pinned.set(this._pinned.append(), [0, 1, 2, 3, 4, 5], [\n                card.description, profile.description,\n                entry.switcher, entry.show,\n                entry.card, entry.profile\n            ]);\n        }\n    }\n\n    /**\n     * Returns the entries in the selection list as array of strings.\n     */\n    _storePinned() {\n        let values = [];\n        let [success, iter] = this._pinned.get_iter_first();\n\n        while (iter && success) {\n            values.push(JSON.stringify({\n                card: this._pinned.get_value(iter, 4),\n                profile: this._pinned.get_value(iter, 5),\n                switcher: this._pinned.get_value(iter, 2),\n                show: this._pinned.get_value(iter, 3)\n            }));\n            success = this._pinned.iter_next(iter);\n        }\n\n        this._settings.set_array(SETTING.pinned_profiles, values);\n    }\n\n\n    /**\n     * Shows a message dialog bound to the parent window.\n     */\n    _showMessage(title, text, type = 'WARNING') {\n        let dialog = new Gtk.MessageDialog({\n            text:           title,\n            secondary_text: text,\n            message_type:   Gtk.MessageType[type],\n            buttons:        Gtk.ButtonsType.OK,\n            transient_for:  this._tabs.get_root(),\n            modal:          true,\n        });\n\n        dialog.connect('response', () => {\n            dialog.destroy();\n        });\n\n        dialog.show();\n    }\n});\n\n\nfunction init() {\n    preferences = new Preferences();\n}\n\nfunction buildPrefsWidget() {\n    return preferences.buildWidget();\n}\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/prefs.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<interface domain=\"gnome-shell-extensions-shell-volume-mixer\">\n  <requires lib=\"gtk\" version=\"4.0\"/>\n  <object class=\"GtkTreeStore\" id=\"storeDevices\">\n    <columns>\n      <column type=\"gchararray\"/>\n      <column type=\"gchararray\"/>\n      <column type=\"gchararray\"/>\n    </columns>\n  </object>\n  <object class=\"GtkListStore\" id=\"storePinned\">\n    <columns>\n      <column type=\"gchararray\"/>\n      <column type=\"gchararray\"/>\n      <column type=\"gboolean\"/>\n      <column type=\"gboolean\"/>\n      <column type=\"gchararray\"/>\n      <column type=\"gchararray\"/>\n    </columns>\n  </object>\n  <object class=\"GtkNotebook\" id=\"tabs\">\n    <signal name=\"switch-page\" handler=\"onSwitchPage\" swapped=\"no\"/>\n    <child>\n      <object class=\"GtkNotebookPage\">\n        <property name=\"child\">\n          <object class=\"GtkGrid\" id=\"gridSettings\">\n            <property name=\"can_focus\">1</property>\n            <property name=\"margin-start\">20</property>\n            <property name=\"margin-end\">20</property>\n            <property name=\"margin_top\">20</property>\n            <property name=\"margin_bottom\">20</property>\n            <property name=\"row_spacing\">10</property>\n            <property name=\"column_spacing\">20</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblPosition\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Position of volume mixer</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">0</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkComboBoxText\" id=\"cmbPosition\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"valign\">center</property>\n                <property name=\"active\">0</property>\n                <signal name=\"changed\" handler=\"onPositionChanged\" swapped=\"no\"/>\n                <items>\n                  <item id=\"0\" translatable=\"yes\">Status Menu</item>\n                  <item id=\"1\" translatable=\"yes\">Left</item>\n                  <item id=\"2\" translatable=\"yes\">Center</item>\n                  <item id=\"3\" translatable=\"yes\">Right</item>\n                </items>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">0</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblRemoveOriginal\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Remove original slider</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">1</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblShowDetailedSliders\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Show detailed sliders</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">4</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swRemoveOriginal\">\n                <property name=\"sensitive\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">1</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swShowDetailedSliders\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">4</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblProfileSwitch\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Hotkey for profile switcher</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">11</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkEntry\" id=\"txtProfileSwitch\">\n                <property name=\"valign\">center</property>\n                <property name=\"input_purpose\">alpha</property>\n                <signal name=\"changed\" handler=\"onProfileSwitchChanged\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">11</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblProfileSwitchHelp\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"label\" translatable=\"yes\">(e.g. &quot;&lt;Super&gt;p&quot;)</property>\n                <layout>\n                  <property name=\"column\">2</property>\n                  <property name=\"row\">11</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblUseSymbolicIcons\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Use symbolic icons</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">5</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swUseSymbolicIcons\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">5</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblShowSystemSounds\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Show system sounds</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">7</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swShowSystemSounds\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">7</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblShowVirtualStreams\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Show virtual streams</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">8</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swShowVirtualStreams\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">8</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblAlwaysShowInputStreams\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Always show input streams</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">9</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swAlwaysShowInputStreams\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">9</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblAlwaysShowInputStreamsHelp\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Shows the slider even if no application is recording</property>\n                <layout>\n                  <property name=\"column\">2</property>\n                  <property name=\"row\">9</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSeparator\" id=\"sep1\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"margin_top\">10</property>\n                <property name=\"margin_bottom\">10</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">2</property>\n                  <property name=\"column-span\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSeparator\" id=\"sep2\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"margin_top\">10</property>\n                <property name=\"margin_bottom\">10</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">6</property>\n                  <property name=\"column-span\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSeparator\" id=\"sep3\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"margin_top\">10</property>\n                <property name=\"margin_bottom\">10</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">10</property>\n                  <property name=\"column-span\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblShowPercentageLabel\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Show percentage</property>\n                <layout>\n                  <property name=\"column\">0</property>\n                  <property name=\"row\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkSwitch\" id=\"swShowPercentageLabel\">\n                <property name=\"halign\">end</property>\n                <property name=\"valign\">center</property>\n                <signal name=\"notify::active\" handler=\"toggleBoolean\" swapped=\"no\"/>\n                <layout>\n                  <property name=\"column\">1</property>\n                  <property name=\"row\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"lblShowPercentageLabelHelp\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"valign\">baseline</property>\n                <property name=\"label\" translatable=\"yes\">Whether to show percentage of current output right of the icon</property>\n                <layout>\n                  <property name=\"column\">2</property>\n                  <property name=\"row\">3</property>\n                </layout>\n              </object>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n            <child>\n              <placeholder/>\n            </child>\n          </object>\n        </property>\n        <property name=\"tab\">\n          <object class=\"GtkLabel\" id=\"lblSettings\">\n            <property name=\"can_focus\">0</property>\n            <property name=\"label\" translatable=\"yes\">Settings</property>\n          </object>\n        </property>\n      </object>\n    </child>\n    <child>\n      <object class=\"GtkNotebookPage\">\n        <property name=\"child\">\n          <object class=\"GtkBox\" id=\"boxDevices\">\n            <property name=\"can_focus\">1</property>\n            <property name=\"margin-start\">20</property>\n            <property name=\"margin-end\">20</property>\n            <property name=\"margin_top\">10</property>\n            <property name=\"margin_bottom\">20</property>\n            <property name=\"spacing\">10</property>\n            <child>\n              <object class=\"GtkScrolledWindow\" id=\"scrDevices\">\n                <property name=\"width_request\">300</property>\n                <property name=\"min_content_width\">200</property>\n                <property name=\"min_content_height\">400</property>\n                <property name=\"child\">\n                  <object class=\"GtkTreeView\" id=\"treeDevices\">\n                    <property name=\"hexpand\">1</property>\n                    <property name=\"vexpand\">1</property>\n                    <property name=\"model\">storeDevices</property>\n                    <property name=\"headers_clickable\">0</property>\n                    <property name=\"enable_search\">0</property>\n                    <child internal-child=\"selection\">\n                      <object class=\"GtkTreeSelection\" id=\"treeDevicesSelection\"/>\n                    </child>\n                    <child>\n                      <object class=\"GtkTreeViewColumn\" id=\"column-name\">\n                        <property name=\"title\" translatable=\"yes\">Devices</property>\n                        <property name=\"expand\">1</property>\n                        <child>\n                          <object class=\"GtkCellRendererText\" id=\"rndName\">\n                            <property name=\"yalign\">0</property>\n                          </object>\n                          <attributes>\n                            <attribute name=\"text\">0</attribute>\n                          </attributes>\n                        </child>\n                      </object>\n                    </child>\n                  </object>\n                </property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkBox\" id=\"boxSelectedDevices\">\n                <property name=\"can_focus\">0</property>\n                <property name=\"orientation\">vertical</property>\n                <child>\n                  <object class=\"GtkScrolledWindow\" id=\"scrSelection\">\n                    <property name=\"vexpand\">1</property>\n                    <property name=\"width_request\">600</property>\n                    <property name=\"height_request\">400</property>\n                    <property name=\"child\">\n                      <object class=\"GtkTreeView\" id=\"treePinned\">\n                        <property name=\"hexpand\">1</property>\n                        <property name=\"vexpand\">1</property>\n                        <property name=\"model\">storePinned</property>\n                        <property name=\"headers_clickable\">0</property>\n                        <property name=\"enable_search\">0</property>\n                        <property name=\"search_column\">0</property>\n                        <property name=\"show_expanders\">0</property>\n                        <child internal-child=\"selection\">\n                          <object class=\"GtkTreeSelection\" id=\"treeview-selection\"/>\n                        </child>\n                        <child>\n                          <object class=\"GtkTreeViewColumn\" id=\"column-card\">\n                            <property name=\"resizable\">1</property>\n                            <property name=\"sizing\">autosize</property>\n                            <property name=\"title\" translatable=\"yes\">Card</property>\n                            <property name=\"expand\">1</property>\n                            <child>\n                              <object class=\"GtkCellRendererText\" id=\"rndCard\"/>\n                              <attributes>\n                                <attribute name=\"text\">0</attribute>\n                              </attributes>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkTreeViewColumn\" id=\"column-profile\">\n                            <property name=\"resizable\">1</property>\n                            <property name=\"sizing\">autosize</property>\n                            <property name=\"title\" translatable=\"yes\">Profile</property>\n                            <property name=\"expand\">1</property>\n                            <child>\n                              <object class=\"GtkCellRendererText\" id=\"rndProfile\"/>\n                              <attributes>\n                                <attribute name=\"text\">1</attribute>\n                              </attributes>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkTreeViewColumn\" id=\"column-quickswitch\">\n                            <property name=\"sizing\">fixed</property>\n                            <property name=\"title\" translatable=\"yes\">Switch</property>\n                            <child>\n                              <object class=\"GtkCellRendererToggle\" id=\"rndQuickswitch\">\n                                <signal name=\"toggled\" handler=\"onQuickswitchToggled\" swapped=\"no\"/>\n                              </object>\n                              <attributes>\n                                <attribute name=\"active\">2</attribute>\n                              </attributes>\n                            </child>\n                          </object>\n                        </child>\n                        <child>\n                          <object class=\"GtkTreeViewColumn\" id=\"column-display\">\n                            <property name=\"visible\">0</property>\n                            <property name=\"sizing\">fixed</property>\n                            <property name=\"title\" translatable=\"yes\">Display</property>\n                            <child>\n                              <object class=\"GtkCellRendererToggle\" id=\"rndDisplay\">\n                                <signal name=\"toggled\" handler=\"onDisplayToggled\" swapped=\"no\"/>\n                              </object>\n                              <attributes>\n                                <attribute name=\"active\">3</attribute>\n                              </attributes>\n                            </child>\n                          </object>\n                        </child>\n                      </object>\n                    </property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkBox\" id=\"deviceSelectionToolbar\">\n                    <property name=\"can_focus\">0</property>\n                    <child>\n                      <object class=\"GtkButton\" id=\"btnAddDevice\">\n                        <property name=\"sensitive\">0</property>\n                        <property name=\"can_focus\">0</property>\n                        <property name=\"icon_name\">list-add-symbolic</property>\n                        <signal name=\"clicked\" handler=\"onAddDevice\" swapped=\"no\"/>\n                      </object>\n                    </child>\n                    <child>\n                      <object class=\"GtkButton\" id=\"btnRemoveDevice\">\n                        <property name=\"sensitive\">0</property>\n                        <property name=\"can_focus\">0</property>\n                        <property name=\"icon_name\">list-remove-symbolic</property>\n                        <signal name=\"clicked\" handler=\"onRemoveDevice\" swapped=\"no\"/>\n                      </object>\n                    </child>\n                    <style>\n                      <class name=\"toolbar\"/>\n                      <class name=\"inline-toolbar\"/>\n                    </style>\n                  </object>\n                </child>\n              </object>\n            </child>\n          </object>\n        </property>\n        <property name=\"tab\">\n          <object class=\"GtkLabel\" id=\"lblDevices\">\n            <property name=\"can_focus\">0</property>\n            <property name=\"label\" translatable=\"yes\">Devices</property>\n          </object>\n        </property>\n      </object>\n    </child>\n  </object>\n</interface>\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/schemas/org.gnome.shell.extensions.shell-volume-mixer.gschema.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE schemalist SYSTEM \"/usr/share/glib-2.0/schemas/gschema.dtd\">\n<schemalist gettext-domain=\"gnome-shell-extensions-shell-volume-mixer\">\n    <enum id=\"org.gnome.shell.extensions.shell-volume-mixer.position\">\n        <value value=\"0\" nick=\"aggregateMenu\" />\n        <value value=\"1\" nick=\"left\" />\n        <value value=\"2\" nick=\"center\" />\n        <value value=\"3\" nick=\"right\" />\n    </enum>\n    <schema path=\"/org/gnome/shell/extensions/shell-volume-mixer/\"\n            id=\"org.gnome.shell.extensions.shell-volume-mixer\">\n        <key name=\"position\" enum=\"org.gnome.shell.extensions.shell-volume-mixer.position\">\n            <default>'aggregateMenu'</default>\n            <summary>Position of volume mixer</summary>\n            <description>Whether to put the applet in a separate menu or replace the master volume mixer already present</description>\n        </key>\n        <key name=\"remove-original\" type=\"b\">\n            <default>false</default>\n            <summary>Remove original slider</summary>\n            <description>Whether to remove the master volume slider from status menu</description>\n        </key>\n        <key name=\"show-percentage-label\" type=\"b\">\n            <default>false</default>\n            <summary>Show percentage of volume</summary>\n            <description>Whether to show percentage of current output right of the icon</description>\n        </key>\n        <key name=\"show-detailed-sliders\" type=\"b\">\n            <default>false</default>\n            <summary>Show detailed sliders</summary>\n            <description>Whether to show detailed sliders with different icons and the stream's description</description>\n        </key>\n        <key name=\"show-system-sounds\" type=\"b\">\n            <default>false</default>\n            <summary>Show system sounds</summary>\n            <description>Whether to show a sliders for system sounds</description>\n        </key>\n        <key name=\"show-virtual-streams\" type=\"b\">\n            <default>false</default>\n            <summary>Show virtual streams</summary>\n            <description>Whether to show virtual streams in the applications menu</description>\n        </key>\n        <key name=\"always-show-input-streams\" type=\"b\">\n            <default>false</default>\n            <summary>Always show input streams</summary>\n\t    <description>Show input streams even if no application is recording</description>\n        </key>\n        <key name=\"use-symbolic-icons\" type=\"b\">\n            <default>true</default>\n            <summary>Use symbolic icons</summary>\n            <description>Whether to use the default symbolic icons or those provided by PulseAudio</description>\n        </key>\n        <key name=\"profile-switcher-hotkey\" type=\"as\">\n            <default>[]</default>\n            <summary>Profile switcher hotkey</summary>\n            <description>Global hotkey to switch / circle pinned profiles</description>\n        </key>\n        <key name=\"pinned-profiles\" type=\"as\">\n            <default>[]</default>\n            <summary>Pinned profiles</summary>\n            <description>Profiles to always show in the list of devices, even if they're not retrieved through Gvc.MixerControl</description>\n        </key>\n        <key name=\"debug\" type=\"b\">\n            <default>false</default>\n            <summary>Enable debug mode</summary>\n            <description>Enables debugging output and the D-Bus interface</description>\n        </key>\n    </schema>\n</schemalist>\n"
  },
  {
    "path": "shell-volume-mixer@derhofbauer.at/stylesheet.css",
    "content": "/* This stylesheet is generated, DO NOT EDIT */\n/* Copyright 2009, 2015 Red Hat, Inc.\n *\n * Portions adapted from Mx's data/style/default.css\n *   Copyright 2009 Intel Corporation\n *\n * This program is free software; you can redistribute it and/or modify it\n * under the terms and conditions of the GNU Lesser General Public License,\n * version 2.1, as published by the Free Software Foundation.\n *\n * This program is distributed in the hope it will be useful, but WITHOUT ANY\n * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for\n * more details.\n *\n * You should have received a copy of the GNU Lesser General Public License\n * along with this program; if not, write to the Free Software Foundation,\n * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.\n */\n.svm-menu {\n  /**\n   * Common\n   */\n  /**\n   * SVM integrated in status menu dropdown\n   */\n  /**\n   * Stand-alone menu via extra-button\n   */\n}\n.svm-menu .svm-slider-details {\n  font-size: 0.9em;\n  font-weight: 400;\n}\n.svm-menu .popup-menu-item:ltr {\n  margin-left: 0;\n  padding-left: 0;\n}\n.svm-menu .popup-menu-item:rtl {\n  margin-right: 0;\n  padding-right: 0;\n}\n.svm-menu .popup-sub-menu .popup-menu-item:ltr {\n  padding-right: 14px;\n}\n.svm-menu .popup-sub-menu .popup-menu-item:rtl {\n  padding-left: 14px;\n}\n.svm-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container {\n  padding-left: 0;\n  padding-right: 0;\n  margin-left: 0;\n  margin-right: 0;\n}\n.svm-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container > .svm-container-line {\n  /* somewhere along all these paddings there's too much added\n   * here, offsetting sliders in submenus by a few pixels\n   */\n}\n.svm-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container > .svm-container-line:ltr {\n  padding-right: 12px;\n}\n.svm-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container > .svm-container-line:rtl {\n  padding-left: 12px;\n}\n.svm-menu > .popup-menu-item > .svm-menu-item-container {\n  padding-left: 0;\n  padding-right: 0;\n  margin-left: 0;\n  margin-right: 0;\n}\n.svm-menu > .popup-menu-item > .svm-menu-item-container > .svm-container-line {\n  padding-left: 0;\n  padding-right: 0;\n  margin-left: 0;\n  margin-right: 0;\n}\n.svm-menu.svm-integrated-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container > .svm-container-line.line-1 .popup-menu-ornament:ltr {\n  margin-left: 0;\n  padding-left: 0;\n}\n.svm-menu.svm-integrated-menu .popup-sub-menu .popup-menu-item > .svm-menu-item-container > .svm-container-line.line-1 .popup-menu-ornament:rtl {\n  margin-right: 0;\n  padding-right: 0;\n}\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-master-slider-line > .popup-menu-ornament,\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-events-slider-line > .popup-menu-ornament {\n  /** @copypaste from gnome-shell .aggregate-menu .popup-sub-menu .popup-menu-item > :first-child **/\n}\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-master-slider-line > .popup-menu-ornament:ltr,\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-events-slider-line > .popup-menu-ornament:ltr {\n  padding-left: 14px;\n  margin-left: 1.09em;\n}\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-master-slider-line > .popup-menu-ornament:rtl,\n.svm-menu.svm-integrated-menu > .popup-menu-item > .svm-menu-item-container .svm-events-slider-line > .popup-menu-ornament:rtl {\n  padding-right: 14px;\n  margin-right: 1.09em;\n}\n.svm-menu.svm-standalone-menu {\n  width: 30em;\n}\n"
  },
  {
    "path": "styles.scss",
    "content": "$variant: 'light';\n@import \"gnome-shell-sass/colors\";\n@import \"gnome-shell-sass/drawing\";\n@import \"gnome-shell-sass/common\";\n\n.svm-menu {\n    $shell_margin: $base_padding + $base_margin * 2;\n\n    @mixin no-spacing {\n        padding-left: 0;\n        padding-right: 0;\n        margin-left: 0;\n        margin-right: 0;\n    }\n\n    @mixin default-padding {\n        /** @copypaste from gnome-shell .aggregate-menu .popup-sub-menu .popup-menu-item > :first-child **/\n        &:ltr {\n            padding-left: $shell_margin;\n            margin-left: $base_icon_size;\n        }\n\n        &:rtl {\n            padding-right: $shell_margin;\n            margin-right: $base_icon_size;\n        }\n    }\n\n    @mixin submenu-padding {\n        &:ltr {\n            padding-right: $shell_margin;\n        }\n\n        &:rtl {\n            padding-left: $shell_margin;\n        }\n    }\n\n    @mixin no-ornament-margin {\n        // fix padding of ornament (selection) in submenu\n        &:ltr {\n            margin-left: 0;\n            padding-left: 0;\n        }\n\n        &:rtl {\n            margin-right: 0;\n            padding-right: 0;\n        }\n    }\n\n\n    /**\n     * Common\n     */\n    .svm-slider-details {\n        font-size: 0.9em;\n        font-weight: 400;\n    }\n\n    .popup-menu-item {\n        @include no-ornament-margin;\n    }\n\n    // sub menu items (of top level menu items)\n    .popup-sub-menu {\n        .popup-menu-item {\n            @include submenu-padding;\n\n            > .svm-menu-item-container {\n                @include no-spacing;\n\n                > .svm-container-line {\n                    /* somewhere along all these paddings there's too much added\n                     * here, offsetting sliders in submenus by a few pixels\n                     */\n                    &:ltr {\n                        padding-right: $shell_margin - 2px;\n                    }\n\n                    &:rtl {\n                        padding-left: $shell_margin - 2px;\n                    }\n                }\n            }\n        }\n    }\n\n    // top-level menu items\n    > .popup-menu-item {\n        > .svm-menu-item-container {\n            @include no-spacing;\n\n            > .svm-container-line {\n                @include no-spacing;\n            }\n        }\n    }\n\n\n    /**\n     * SVM integrated in status menu dropdown\n     */\n    &.svm-integrated-menu {\n        // only sub menu items (of top level items)\n        .popup-sub-menu {\n            .popup-menu-item {\n                > .svm-menu-item-container {\n                    > .svm-container-line {\n                        &.line-1 {\n                            // align ornament to parent items\n                            .popup-menu-ornament {\n                                @include no-ornament-margin;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // top-level menu items\n        > .popup-menu-item {\n            > .svm-menu-item-container {\n                // offset top level items to align with label instead of ornament icon\n                .svm-master-slider-line,\n                .svm-events-slider-line {\n                    > .popup-menu-ornament {\n                        @include default-padding;\n                    }\n                }\n            }\n        }\n    }\n\n\n    /**\n     * Stand-alone menu via extra-button\n     */\n    &.svm-standalone-menu {\n        width: 30em;\n    }\n}\n"
  }
]