Repository: theRealCarneiro/pulsemeeter (language: python)

Issue:
[Bug report] Hardware output device volumes are not sync'd with the UI
**Describe the bug**
In the UI bottom-left corner are volume controls for the three Hardware Output Channels.
These volume sliders control the volume of the actual selected output devices.
Problem is, these sliders are working in a one-way, write only way, means
that adjusting the volume of a device trough pulsemeeters UI will change the device volume, but if the device volume is adjusted outside of pulsemeeter, pulsemeeter will not notice that.
Also, on the initial start of the application, pulsemeeter does not read out nor update the current devices volumes, thus after the application is started, it will use the last state of the UI  (default value is 100% volume) while the actual device volume never gets read.

**To Reproduce**
Video: https://user-images.githubusercontent.com/39499729/159794646-74729603-0a88-4e88-8a34-793075a0d2a5.mp4
1. Open pulsemeeter and your sound device and volume manager (Sound Settings simply said)
(2.) If it is a fresh install, the volume displayed in pulsemeeters UI is 100% even when the actual device volume is not 100%.
3. Select your hardware output device in pulsemeeter and then change the volume of that device in pulsemeeter. You should see that the volume changes also in your Sound Settings.
4. Change the volume of said device in your Sound Settings. Now you should see that the displayed volume in the pulsemeeter UI stays unchanged. 

Thus I conclude that pulsemeeters hardware output volume is write-only and not synchronized with the actual hardware volume.

**Expected behavior**
That the in the UI displayed volume would match the current hardware device volume.

**Possible Solution**
Upon opening (if a hardware device was previously selected) and after adding another device to one of the hardware output channels,
pulsemeeters should get the current hardware volume of given device and subscribe to any further changes of the hardware devices volume to keep the pulsemeeter UI in sync with the real hardware volume.

**Desktop (please complete the following information):**
 - Distro: Linux Mint 20.3 Cinnamon
 - Python Version 3.8.10

**Additional context:**
I found this bug the hard way. Had a relatively powerful modded sound system (that i only run at less than 25% typically as it is too loud) and headphones connected, to playback audio over both simultaneously. Then when playing around with pulsemeeter i wondered what these hardware volume settings are for (as they still maintained their default 100% volume) and that's how I noticed those volume sliders are for the hardware output devices and they're not synchronized with the actual hardware device volumes.



Discussion / hints:

Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..90b8264
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - Distro: [e.g. Ubuntu]
+ - Python Version [e.g. Python 3.10.2]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..40a9b2c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,32 @@
+# Description
+
+Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
+
+Fixes # (issue)
+
+## Type of change
+
+Please delete options that are not relevant.
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] This change requires a documentation update
+
+# How Has This Been Tested?
+
+Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
+
+- [ ] Test A
+- [ ] Test B
+
+
+# Checklist : 
+You don't need to do all of this, but at least check what you did
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my own code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] Any dependent changes have been merged and published in downstream modules
diff --git a/.github/workflows/aur-publish-git.yml b/.github/workflows/aur-publish-git.yml
new file mode 100644
index 0000000..5d4206c
--- /dev/null
+++ b/.github/workflows/aur-publish-git.yml
@@ -0,0 +1,32 @@
+name: Publish git version on AUR
+on:
+  push:
+    tags:
+     - '*'
+  workflow_dispatch:
+
+jobs:
+        
+  aur-publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+      
+      - name: Create PKGBUILD
+        run:  |
+          curl "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=pulsemeeter-git" --silent > PKGBUILD
+          version=$(printf "%s" "$(git describe --long | sed 's/\([^-]*-\)g/r\1/;s/-/./g; s/.//')")
+          sed -i "s/pkgver=.*/pkgver=$version/g" PKGBUILD
+          
+      - name: Publish AUR package
+        uses: KSXGitHub/github-actions-deploy-aur@v2.2.5
+        with:
+          pkgname: pulsemeeter-git
+          pkgbuild: ./PKGBUILD
+          commit_username: ${{ secrets.AUR_USERNAME }}
+          commit_email: ${{ secrets.AUR_EMAIL }}
+          ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
+          commit_message: Update AUR package
+          ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
diff --git a/.github/workflows/aur-publish-stable.yml b/.github/workflows/aur-publish-stable.yml
new file mode 100644
index 0000000..fbf92bb
--- /dev/null
+++ b/.github/workflows/aur-publish-stable.yml
@@ -0,0 +1,34 @@
+name: Publish stable release on AUR
+
+on:
+  release:
+    types: [published]
+  workflow_dispatch:
+
+jobs:
+        
+  aur-publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      
+      # Runs a single command using the runners shell
+      - name: Create PKGBUILD
+        run:  |
+          VERSION="$(python setup.py --version)"
+          wget https://github.com/theRealCarneiro/pulsemeeter/archive/refs/tags/v$VERSION.tar.gz
+          curl "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=pulsemeeter" --silent > PKGBUILD
+          sed -i "s/pkgver=.*/pkgver=$VERSION/g" PKGBUILD
+          sed -i "s/md5sums=.*/md5sums=($(md5sum --zero v$VERSION.tar.gz | cut -f1 -d ' '))/g" PKGBUILD
+          cat PKGBUILD
+
+      - name: Publish AUR package
+        uses: KSXGitHub/github-actions-deploy-aur@v2.2.5
+        with:
+          pkgname: pulsemeeter
+          pkgbuild: ./PKGBUILD
+          commit_username: ${{ secrets.AUR_USERNAME }}
+          commit_email: ${{ secrets.AUR_EMAIL }}
+          ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
+          commit_message: Update AUR package
+          ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 0000000..68896e2
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,37 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Publish on Pypi
+
+on:
+  release:
+    types: [published]
+  workflow_dispatch:
+
+jobs:
+  deploy:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install build
+    - name: Build package
+      run: python -m build
+    - name: Publish package
+      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+      with:
+        user: __token__
+        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index dddb7c2..237f046 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 teste/*
 teste.py
 *.session
+*-env
 
 # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all
 # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all
diff --git a/MANIFEST.in b/MANIFEST.in
index 6edce19..591189e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,6 @@
 recursive-include share/*
-include pulsemeeter/layouts/*
-include pulsemeeter/Interface.glade
+include pulsemeeter/interface/layouts/*
 include pulsemeeter/config.json
+include requirements.txt
 include LICENSE
 include MANIFEST.in
diff --git a/README.md b/README.md
index 1ba7549..dee3069 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # Pulsemeeter
 A frontend to ease the use of pulseaudio's routing capabilities, mimicking voicemeeter's workflow
 
-[![pypi](https://img.shields.io/badge/pypi-v1.2.6-blue)](https://pypi.org/project/pulsemeeter/)
-[![AUR](https://img.shields.io/badge/AUR-V1.2.6-cyan)](https://aur.archlinux.org/packages/pulsemeeter-git/)
+[![pypi](https://img.shields.io/badge/pypi-v1.2.13-blue)](https://pypi.org/project/pulsemeeter/)
+[![AUR](https://img.shields.io/badge/AUR-V1.2.12-cyan)](https://aur.archlinux.org/packages/pulsemeeter/)
 [![AUR](https://img.shields.io/badge/AUR-pulsemeeter--git-red)](https://aur.archlinux.org/packages/pulsemeeter-git/)
 [![Discord](https://img.shields.io/badge/chat-Discord-lightgrey)](https://discord.gg/ekWt9NuEWv)
 [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
@@ -11,16 +11,16 @@ A frontend to ease the use of pulseaudio's routing capabilities, mimicking voice
 
 ### Wiki: \[[Installation](https://github.com/theRealCarneiro/pulsemeeter/wiki/Installation)\] \[[How to use](https://github.com/theRealCarneiro/pulsemeeter/wiki/Installation)\]
 
-![](https://i.imgur.com/hYDE8dh.png)
-(This screenshot was taken while using ant dracula gtk theme, its a gtk application so it will use your theme)
+![](https://i.imgur.com/L4KZEqV.png)
+(This screenshot was taken while using ant dracula gtk theme, it will use your theme)
 
 # Table of Contents
 - **[Features](#features)**
 - **[Installation](#installation)**
     - [Dependencies](#dependencies)
     - [Arch](#arch-aur)
-    - [Pypi install](#install-using-pip)
-    - [Manual/Git install](#any-distro)
+    - [Pypi install](#any-distro)
+    - [Manual/Git install](#build-from-source)
 - **[Auto Start](#start-devices-on-startup)**
 - **[Discord Server](#discord-server)**
 
@@ -33,31 +33,28 @@ A frontend to ease the use of pulseaudio's routing capabilities, mimicking voice
 
 # Installation
 
-
 ## Dependencies
-
-### Python Dependencies
-
+Visit the [dependencies](https://github.com/theRealCarneiro/pulsemeeter/wiki/Installation#installing-dependencies) section in the wiki to get in depth information on how to install these for your specific system.
  - pip
+ - libappindicator3
+ - gobject-introspection-1.0
+ - libpulse
 
-Pip will automaticly install these dependencies if you're not building from source
- - [appdirs](https://pypi.org/project/appdirs)
- - [setuptools](https://pypi.org/project/setuptools)
+
+### Python Dependencies
+Pip will automaticly install these dependencies
  - [pygobject](https://pypi.org/project/PyGObject)
  - [pulsectl](https://pypi.org/project/pulsectl)
+ - [setuptools](https://pypi.org/project/setuptools/)
  
  ### Optional Dependencies
- These dependencies are only for 
- - [noise-suppression-for-voice](https://github.com/werman/noise-suppression-for-voice)
- - [swh-plugins](https://github.com/swh/ladspa) (apt/dnf/pacman packages available)
+ These dependencies are optional and will enable new features in the application
+ - [noise-suppression-for-voice](https://github.com/werman/noise-suppression-for-voice) for noise reduction
+ - [swh-plugins](https://github.com/swh/ladspa) for equalizers (apt/dnf/pacman packages available)
  - [pulse-vumeter](https://github.com/theRealCarneiro/pulse-vumeter) for volume level information
 
-Visit the [installation](https://github.com/theRealCarneiro/pulsemeeter/wiki/Installation) section in the wiki to get in depth information on how to install these for your specific system.
-
 ## Any distro
 
-Only optional dependencies are not installed using this method, all essential dependencies are automatically installed 
-
 ### Single user
 When installing for a single user (without sudo) you need to add ~/.local/bin to your path, [this section](#add-local-bin-to-path) will show you how to do it
 ```sh
@@ -78,7 +75,6 @@ When installing for a single user (without sudo) you need to add ~/.local/bin to
 ```sh
 git clone https://github.com/theRealCarneiro/pulsemeeter.git
 cd pulsemeeter
-pip install -r requirements.txt
 pip install .
 ```
 
@@ -86,7 +82,6 @@ pip install .
 ```sh
 git clone https://github.com/theRealCarneiro/pulsemeeter.git
 cd pulsemeeter
-sudo pip install -r requirements.txt
 sudo pip install .
 ```
 
@@ -104,7 +99,21 @@ export PATH="$HOME/.local/bin:$PATH"
 ```
 
 ## Start devices on startup
-All connections and devices will be restored with the command `pulsemeeter init`
+
+### Starting devices only
+If you only want to restore all connections and devices but not start the entire application, you can run:
+```sh 
+pulsemeeter init
+```
+
+### Start pulsemeeter as a daemon
+You can run pulsemeeter as a daemon, might be useful if you want the app the start in the tray on startup 
+```sh 
+pulsemeeter daemon
+```
+
+## Extensions
+[@Fl1tzi](https://github.com/Fl1tzi) Elgato Streamdeck intergration [pulsemeeter-streamdeck](https://github.com/Fl1tzi/pulsemeeter-streamdeck)
 
 ## Discord Server
 If you want to get updates about new features, patches or leave some sugestions, join our [discord server](https://discord.gg/ekWt9NuEWv)
diff --git a/pulsemeeter/__main__.py b/pulsemeeter/__main__.py
index 8e7181f..2e87028 100755
--- a/pulsemeeter/__main__.py
+++ b/pulsemeeter/__main__.py
@@ -1,22 +1,17 @@
 import os
 import time
-import signal
-import json
-import sys, threading
+import sys
 import subprocess
 import argparse
 import re
 import platform
-
-from .settings import PIDFILE, CONFIG_FILE, __version__
+import traceback
+from .settings import CONFIG_FILE, __version__
 from . import MainWindow
-from . import Pulse
 from . import Client, Server
-
 from gi import require_version as gi_require_version
-
 gi_require_version('Gtk', '3.0')
-from gi.repository import Gtk
+from gi.repository import Gtk # type: ignore
 
 # change these to change all occurences of these values (also for checking)
 true_values = ('true', '1', 'on')
@@ -27,21 +22,24 @@
 
 all_get = ('volume', 'mute', 'primary', 'name', 'eq', 'rnnoise', 'connect')
 
+
 # PRETTY PRINT
 def pprint_bool_options(style='simple'):
     # pretty print boolean values
     if style == 'pretty':
-        return ' | '.join(map(str, true_values))+'\n'+' | '.join(map(str, false_values))
+        return ' | '.join(map(str, true_values)) + '\n' + ' | '.join(map(str, false_values))
     elif style == 'simple':
-        return '['+'|'.join(map(str, true_values))+'] | ['+'|'.join(map(str, false_values))+']'
+        return f'[{"|".join(map(str, true_values))}] | [{"|".join(map(str, false_values))}]'
+
 
-def pprint_device_options(allowed_devices = all_devices):
+def pprint_device_options(allowed_devices=all_devices):
     # pretty print devices
     if type(allowed_devices) == str:
         return f'{allowed_devices}[number]'
     else:
         return f'{"[number] | ".join(map(str, allowed_devices))}[number]'
 
+
 # HELP MESSAGES
 class help:
     class description:
@@ -53,10 +51,13 @@ class description:
         MUTE = 'mute/unmute a device'
         CHANGE_HARDWARE_DEVICE = 'Change the hardware device.'
         VOLUME = 'change volume'
-        RNNOISE = 'Turn on/off noise reduction and change values. To toggle only include the device.'
+        RNNOISE = 'Turn on/off noise reduction and change values. To toggle ommit the status.'
         EQ = 'Turn on/off eq and change values. To toggle only include the device'
         RENAME = 'rename device'
         GET = 'eg. pulsemeeter get volume a1'
+        DAEMON = 'Start the server and open the tray.'
+        INIT = 'Just start devices and connections.'
+        EXIT = 'Close the server and with that all clients (including the UI).'
 
     class usage:
         RNNOISE = f'%(prog)s [device] ({pprint_bool_options()} | [set]) [value]'
@@ -72,6 +73,7 @@ class value:
         GET = 'value to get'
         GET_DEVICE2 = 'only used for connect'
 
+
 # COLORED TEXT
 class format:
     # format text using ANSI escape codes
@@ -81,7 +83,7 @@ def __init__(self, no_color=False):
         self.GREEN = '\033[92m'
         self.RED = '\033[91m'
         self.YELLOW = '\033[33m'
-        self.BOLD_RED = self.BOLD+self.RED
+        self.BOLD_RED = self.BOLD + self.RED
         self.GREY = '\033[30m'
         self.CLEAR = '\033[2J'
         self.END = '\033[0m'
@@ -90,25 +92,32 @@ def __init__(self, no_color=False):
     def bold(self, text):
         if self.no_color: return text
         else: return self.BOLD + text + self.END
+
     def red_bold(self, text):
         if self.no_color: return text
         else: return self.BOLD_RED + text + self.END
+
     def grey(self, text):
         if self.no_color: return text
         else: return self.GREY + text + self.END
+
     def green(self, text):
         if self.no_color: return text
         else: return self.GREEN + text + self.END
+
     def yellow(self, text):
         if self.no_color: return text
         else: return self.YELLOW + text + self.END
+
     def red(self, text):
         if self.no_color: return text
         else: return self.RED + text + self.END
+
     # clear terminal
     def clear(self, text):
         if self.no_color: return text
         else: return self.CLEAR + self.END
+
     # print a color end
     def end(self, text):
         if self.no_color: return text
@@ -123,6 +132,7 @@ def debug_start(client, start_option):
     elif start_option == 'listen':
         server_listen_mode(client)
 
+
 def server_input_mode(client):
     print(f'\n{color.green("INPUT:")}')
     try:
@@ -142,6 +152,7 @@ def server_input_mode(client):
         print('closing debugger')
         sys.exit(0)
 
+
 def server_listen_mode(client):
     print(f'\n{color.yellow("LISTEN:")}')
     try:
@@ -150,6 +161,7 @@ def server_listen_mode(client):
         print()
         server_input_mode(client)
 
+
 # CONVERTERS
 def str_to_bool(string, parser):
     if type(string) == bool:
@@ -161,6 +173,7 @@ def str_to_bool(string, parser):
     else:
         parser.error(f'value has to be one of these options: {pprint_bool_options()}')
 
+
 # check if values are valid and check devices for different type
 def convert_eq_rnnoise(args, parser, type):
     # generate device_args by using sinks or sources
@@ -175,7 +188,7 @@ def convert_eq_rnnoise(args, parser, type):
             if len(values) == 15:
                 return (*device_args, args.state, args.value)
             else:
-                parser.error('Wrong values supplied. You need to assign 15 values separated by a comma. (eg. 1,2,3,...)')
+                parser.error('Wrong values supplied. You need to assign 15 comma separetaed values')
         elif type == 'rnnoise':
             return (device_args[1], args.state, args.value)
     else:
@@ -190,8 +203,10 @@ def convert_eq_rnnoise(args, parser, type):
             else:
                 return (*device_args, str_to_bool(args.state, parser))
 
+
 # convert [device][number] -> [device] [number] and check if device is valid
-def convert_device(args, parser, device_type='general', allowed_devices=all_devices, con_in=None, con_out=None):
+def convert_device(args, parser, device_type='general',
+        allowed_devices=all_devices, con_in=None, con_out=None):
     if device_type == 'general':
         # convert all devices
         try:
@@ -201,7 +216,7 @@ def convert_device(args, parser, device_type='general', allowed_devices=all_devi
             else:
                 device = re.match(f'^({"|".join(allowed_devices)})', args.device).group()
             num = re.search(r'\d+$', args.device).group()
-        except:
+        except Exception:
             if type(allowed_devices) == str:
                 parser.error(f'device has to be assigned like this: [{allowed_devices}][number].')
             else:
@@ -214,56 +229,67 @@ def convert_device(args, parser, device_type='general', allowed_devices=all_devi
             if con_in is None and con_out is None:
                 inputd = args.input
                 outputd = args.output
-            else: 
+            else:
                 inputd = con_in
                 outputd = con_out
             in_device = re.match(r'^(hi|vi)', inputd).group()
             in_num = re.search(r'\d+$', inputd).group()
             out_device = re.match(r'^(a|b)', outputd).group()
             out_num = re.search(r'\d+$', outputd).group()
-        except:
+        except Exception:
             parser.error('device has to be assigned like this: [hi|vi][number] [a|b][number]')
         else:
             return (in_device, in_num, out_device, out_num)
     else:
         parser.error(f'internal error: unknown device convert "{device_type}".')
 
+
 # PARSER GENERATORS
 device_arg = {'type': str}
 
+
 # generic device = device + [value]
 def parser_generic(parser, value_type, help='', device_help=all_devices):
     parser.add_argument('device', **device_arg, help=pprint_device_options(device_help))
     if value_type is not None:
         parser.add_argument('value', type=value_type, default=None, nargs='?', help=help)
 
+
 # source -> sink (only used for connect)
-def parser_source_to_sink(parser, value_type, help='', help_input=pprint_device_options(('hi', 'vi')), help_output=pprint_device_options(('a', 'b'))):
+def parser_source_to_sink(parser, value_type, help='',
+        help_input=pprint_device_options(('hi', 'vi')),
+        help_output=pprint_device_options(('a', 'b'))):
     parser.add_argument('input', **device_arg, help=help_input)
     parser.add_argument('output', **device_arg, help=help_output)
     parser.add_argument('value', type=value_type, default=None, nargs='?', help=help)
 
+
 # only eq and rnnoise
 def parser_eq_rnnoise(parser, type):
     if type == 'eq':
         parser.add_argument('device', **device_arg, help=pprint_device_options(('a', 'b')))
-        value_help = 'Only needed when using set as state. Needs 15 values seperated by with a comma.'
+        value_help = 'Only needed when using set as state. Needs 15 comma seperated values'
     elif type == 'rnnoise':
         parser.add_argument('device', **device_arg, help=pprint_device_options('hi'))
         value_help = 'Only needed when using set as state.'
-    parser.add_argument('state', type=str, choices=(*true_values, *false_values, 'set', None), default=None, nargs='?', help='')
+    parser.add_argument('state', type=str, choices=(*true_values, *false_values, 'set', None),
+            default=None, nargs='?', help='')
     parser.add_argument('value', type=str, default=None, nargs='?', help=value_help)
 
+
 def parser_get(parser, type):
     parser.add_argument('value', type=type, help=help.value.GET, choices=all_get)
     parser.add_argument('device', **device_arg, help=pprint_device_options())
-    parser.add_argument('device2', **device_arg, help=f'{pprint_device_options()} ({help.value.GET_DEVICE2})', default=None, nargs='?') 
+    parser.add_argument('device2', **device_arg, help=f'{pprint_device_options()}({help.value.GET_DEVICE2})', default=None, nargs='?')
+
 
 # ARGS INTERPRETER AND PARSER
 def create_parser_args():
     color = format()
 
-    parser = argparse.ArgumentParser(prog='pulsemeeter', usage='%(prog)s', description=(f'Use "{color.green("%(prog)s [command] -h")}" to get usage information. Replicating voicemeeter routing functionalities in linux with pulseaudio.'))
+    parser = argparse.ArgumentParser(prog='pulsemeeter', usage='%(prog)s',
+            description=(f'Use "{color.green("%(prog)s [command] -h")}" to get usage information.',
+                'Replicating voicemeeter routing functionalities in linux with pulseaudio.'))
 
     parser.add_argument('-nc', '--no-color', action='store_true', help=help.description.NO_COLOR)
     parser.add_argument('-d', '--debug', action='store_true', help=help.description.DEBUG)
@@ -272,9 +298,9 @@ def create_parser_args():
     subparsers = parser.add_subparsers(dest='command')
 
     # commands to only show in help menu
-    subparsers.add_parser('daemon')
-    subparsers.add_parser('init')
-    subparsers.add_parser('exit')
+    subparsers.add_parser('daemon', description=help.description.DAEMON)
+    subparsers.add_parser('init', description=help.description.INIT)
+    subparsers.add_parser('exit', description=help.description.EXIT)
 
     # get (retrieve values)
     parser_get(subparsers.add_parser('get', description=help.description.GET), str)
@@ -283,19 +309,20 @@ def create_parser_args():
     parser_source_to_sink(subparsers.add_parser('connect', description=help.description.CONNECT), str, help.value.CONNECT)
 
     # generic (device and with value if not None)
-    parser_generic(subparsers.add_parser('primary',description=help.description.PRIMARY), None, help.value.PRIMARY, ('vi', 'b'))
+    parser_generic(subparsers.add_parser('primary', description=help.description.PRIMARY), None, help.value.PRIMARY, ('vi', 'b'))
     parser_generic(subparsers.add_parser('mute', description=help.description.MUTE), str, help.value.MUTE)
-    parser_generic(subparsers.add_parser('change-hardware-device', description=help.description.CHANGE_HARDWARE_DEVICE),str, help.value.CHANGE_HARDWARE_DEVICE, ('vi', 'b'))
+    parser_generic(subparsers.add_parser('change-hardware-device', description=help.description.CHANGE_HARDWARE_DEVICE), str, help.value.CHANGE_HARDWARE_DEVICE, ('vi', 'b'))
     parser_generic(subparsers.add_parser('rename', description=help.description.RENAME), str, help.value.RENAME, ('vi', 'b'))
     parser_generic(subparsers.add_parser('volume', description=help.description.VOLUME), str, help.value.VOLUME)
 
     # eq or rnnoise (allows state)
-    parser_eq_rnnoise(subparsers.add_parser('rnnoise', usage = help.usage.RNNOISE, description=help.description.RNNOISE), 'rnnoise')
+    parser_eq_rnnoise(subparsers.add_parser('rnnoise', usage=help.usage.RNNOISE, description=help.description.RNNOISE), 'rnnoise')
     parser_eq_rnnoise(subparsers.add_parser('eq', usage=help.usage.EQ, description=help.description.EQ), 'eq')
 
     args = parser.parse_args()
     arg_interpreter(args, parser)
 
+
 def arg_interpreter(args, parser):
     global color
     color = format(no_color=args.no_color)
@@ -307,19 +334,22 @@ def arg_interpreter(args, parser):
         else:
             print(f'Server: {color.red("not running")}')
 
+
         try:
             subprocess.check_call('pmctl')
             print(f'Pulseaudio: {color.green("running")}')
-        except:
+        except Exception:
             print(f'Pulseaudio: {color.red("not running")}')
-        
+
+
         try:
             audio_server = os.popen('pactl info | grep "Server Name"').read()
             audio_server = audio_server.split(': ')[1]
             audio_server = audio_server.replace('\n', '')
-        except:
+        except Exception:
             audio_server = color.red('could not be determined')
-      
+
+
         print(f'audio server: {color.bold(audio_server)}')
         print(f'Pulsemeeter version: {color.bold(__version__)}')
         print(f'Config File: {color.bold(CONFIG_FILE)}')
@@ -330,15 +360,15 @@ def arg_interpreter(args, parser):
         # commands which need a client
         try:
             client = Client()
-        except:
+        except Exception:
             print(color.red('error: daemon is not started. Use "pulsemeeter daemon" to start it.'))
         else:
             # debug page
             if args.debug:
-                    print(f'You are entering the {color.red("debug mode")}.')
-                    print(f'While in INPUT mode type "{color.bold("listen")}" to switch to the LISTEN mode.')
-                    print(f'While in LISTEN mode use {color.bold("ctrl+c")} to switch back to INPUT mode.')
-                    debug_start(client, 'input')
+                print(f'You are entering the {color.red("debug mode")}.')
+                print(f'While in INPUT mode type "{color.bold("listen")}" to switch to the LISTEN mode.')
+                print(f'While in LISTEN mode use {color.bold("ctrl+c")} to switch back to INPUT mode.')
+                debug_start(client, 'input')
 
             # COMMAND INTERPRETER
 
@@ -365,9 +395,9 @@ def arg_interpreter(args, parser):
                         print(client.config[device_args[0]][device_args[1]]['use_rnnoise'])
                     elif args.value == 'connect':
                         device_args = convert_device(args, parser, 'source-to-sink', con_in=args.device, con_out=args.device2)
-                        out = device_args[2]+device_args[3]
+                        out = device_args[2] + device_args[3]
                         print(client.config[device_args[0]][device_args[1]][out])
-                except:
+                except Exception:
                     print(color.red('error: Could not get value for this.'))
 
             elif args.command == 'connect':
@@ -411,76 +441,92 @@ def arg_interpreter(args, parser):
             elif args.command == 'rename':
                 client.rename(*convert_device(args, parser), args.value)
 
-    sys.exit(0)
 
 def start_server(server):
     try:
         server.start_server(daemon=True)
         time.sleep(0.1)
-    except:
-        print('Could not start server')
+    except Exception:
+        print(f'Could not start server because of:\n')
+        traceback.print_exc()
         sys.exit(1)
 
+
 def start_app(isserver, trayonly):
-    app = MainWindow(isserver=isserver, trayonly=trayonly)
+    MainWindow(isserver=isserver, trayonly=trayonly)
     Gtk.main()
 
+
 def main():
     global another_sv_running
 
+    server = None
     try:
-        server = Server()
+        # this supresses warnings coming from pmctl, which are not needed when just checking if the server is running
+        server = Server(init_audio_server=False)
         another_sv_running = False
 
     except ConnectionAbortedError:
         another_sv_running = True
 
-    except Exception as ex:
-        print(ex)
+    except Exception:
+        traceback.print_exc()
         return 1
 
     isserver = not another_sv_running
 
-    #none: Start Server (if not already started) and open window 
+    # none: Start Server (if not already started) and open window
     if len(sys.argv) == 1:
         trayonly = False
 
-    # daemon: disable application window creation for instance
-    elif sys.argv[1].lower() == 'daemon':
-        if not isserver:
-            print('The server is already running.')
-            return 1
-        
-        trayonly = True
+    elif len(sys.argv) == 2:
+        # daemon: disable application window creation for instance
+        if sys.argv[1].lower() == 'daemon':
+            if not isserver:
+                print('The server is already running.')
+                return 1
 
-    # init: Just start devices and connections
-    elif sys.argv[1] == 'init':
-        return 0
-    
-    # exit: close server, all clients should close after they recive an exit signal from
-    # the server
-    elif sys.argv[1].lower() == 'exit':
-        try:
-            if another_sv_running:
-                print('closing server...')
-                print('It may take a few seconds...')
-                client = Client()
-                client.close_server()
-                return 0
-            else:
-                print('no instance is running')
+            trayonly = True
+
+        # init: Just start devices and connections
+        elif sys.argv[1] == 'init':
+            Server()
+            return 0
+
+        # exit: close server, all clients should close after they recive an exit signal from
+        # the server
+        elif sys.argv[1].lower() == 'exit':
+            try:
+                if another_sv_running:
+                    print('closing server...')
+                    print('It may take a few seconds...')
+                    client = Client()
+                    client.close_server()
+                    return 0
+                else:
+                    print('no instance is running')
+                    return 1
+            except Exception:
+                print('unable to close server')
+                traceback.print_exc()
                 return 1
-        except Exception as ex:
-            print('unable to close server', ex)
-            return 1
-    
+        else:
+            create_parser_args()
+            return 0
     else:
         create_parser_args()
         return 0
-    
+
     # only no args and daemon arg reach this part of the code
-    if isserver: start_server(server)
+
+    # start server if there's no server running
+    if server is not None:
+        # new instance of server where init starts
+        server = Server()
+        start_server(server)
     start_app(isserver, trayonly)
-    if isserver: server.handle_exit_signal()
+
+    # close server if there was a server started
+    if server is not None: server.handle_exit_signal()
 
     return 0
diff --git a/pulsemeeter/backends/pulse.py b/pulsemeeter/backends/pulse.py
index a4f7cc8..1a96ece 100755
--- a/pulsemeeter/backends/pulse.py
+++ b/pulsemeeter/backends/pulse.py
@@ -1,47 +1,52 @@
 import os
-import shutil
 import json
 import re
 import sys
 import subprocess
- 
+import traceback
+
 import pulsectl
+import pulsectl_asyncio
+
 
 class Pulse:
-    def __init__(self, config, loglevel=0):
+    def __init__(self, config, loglevel=0, init=True):
         self.config = config
         self.loglevel = loglevel
 
         # check if pulseaudio is running
         try:
             subprocess.check_call('pmctl')
-        except:
+        except Exception:
             sys.exit(1)
 
-        self.channels = ['front-left','front-right','rear-left',
-                'rear-right', 'front-center', 'lfe', 'side-left',
-                'side-right', 'aux0', 'aux1', 'aux2', 'aux3'
-                ]
+        self.channels = ['front-left', 'front-right', 'rear-left',
+                         'rear-right', 'front-center', 'lfe', 'side-left',
+                         'side-right', 'aux0', 'aux1', 'aux2', 'aux3'
+                         ]
         # self.read_config()
-        self.pulsectl = pulsectl.Pulse('pulsemeeter')
         # if init == 'cmd': return
+
+
+        self.pulsectl_async = pulsectl_asyncio.PulseAsync('pulsemeeter-listener')
+        self.pulsectl = pulsectl.Pulse('pulsemeeeter')
+
         command = ''
-        command += self.start_sinks()
-        command += self.start_sources()
-        command += self.start_eqs()
-        command += self.start_rnnoise()
-        command += self.start_connections()
-        command += self.start_primarys()
-        # command += self.start_mute()
-        
+        if init is True:
+            command += self.start_sinks()
+            command += self.start_sources()
+            command += self.start_eqs()
+            command += self.start_rnnoise()
+            command += self.start_connections()
+            command += self.start_primarys()
+
         # print(command)
 
         os.popen(command)
 
         # self.restart_window = False
 
-
-    # get the correct device to connect to, e.g. include .monitor in a 
+    # get the correct device to connect to, e.g. include .monitor in a
     # virtual sink name, or use a ladspa sink for a hardware source
     def get_correct_device(self, index, conn_type):
 
@@ -58,7 +63,7 @@ def get_correct_device(self, index, conn_type):
         if index[0] == 'hi':
 
             # return ladspa sink with rnnoise plugin
-            if self.config[index[0]][index[1]]['use_rnnoise'] == True:
+            if self.config[index[0]][index[1]]['use_rnnoise'] is True:
                 name = f'{index[0]}{index[1]}_rnnoise'
                 return name + ".monitor"
 
@@ -70,7 +75,7 @@ def get_correct_device(self, index, conn_type):
         if index[0] == 'a':
 
             # return ladspa sink with eq plugin
-            if self.config[index[0]][index[1]]['use_eq'] == True:
+            if self.config[index[0]][index[1]]['use_eq'] is True:
                 name = f'{index[0]}{index[1]}_eq'
                 return name
 
@@ -82,7 +87,7 @@ def get_correct_device(self, index, conn_type):
         if index[0] == 'b':
 
             # return ladspa sink with eq plugin
-            if self.config[index[0]][index[1]]['use_eq'] == True:
+            if self.config[index[0]][index[1]]['use_eq'] is True:
                 name = f'{index[0]}{index[1]}_eq'
                 if conn_type == 'source':
                     name = name + ".monitor"
@@ -95,7 +100,6 @@ def get_correct_device(self, index, conn_type):
                     name = name + ".monitor"
                 return name
 
-
     # init virtual inputs
     def start_sinks(self):
         command = ''
@@ -107,23 +111,26 @@ def start_sinks(self):
             # if device does not have a name
             if self.config['vi'][device_id]['name'] != '':
 
-                # if device is available on pulse
-                if not re.search(self.config['vi'][device_id]['name'], sink_list):
+                # external key means that the user is responsible for managing that sink
+                if self.config['vi'][device_id]['external'] is False:
 
-                    # set sink properties
-                    sink = self.config['vi'][device_id]['name']
-                    channels = ''
-                    channel_map = ''
-                    output_type = 'sink'
+                    # if device is available on pulse
+                    if not re.search(self.config['vi'][device_id]['name'], sink_list):
 
-                    # for jack sinks
-                    if self.config['jack']['enable'] == True:
-                        channels = self.config['vi'][device_id]['channels']
-                        channel_map = self.config['vi'][device_id]['channel_map']
-                        channel_map = ','.join(channel_map) if len(channel_map) > 0 else ','.join(self.channels[:channels])
-                        output_type = 'jack-sink'
+                        # set sink properties
+                        sink = self.config['vi'][device_id]['name']
+                        channels = ''
+                        channel_map = ''
+                        output_type = 'sink'
 
-                    command += f"pmctl init {output_type} {sink} {channels} {channel_map}\n"
+                        # for jack sinks
+                        if self.config['jack']['enable'] is True:
+                            channels = self.config['vi'][device_id]['channels']
+                            channel_map = self.config['vi'][device_id]['channel_map']
+                            channel_map = ','.join(channel_map) if len(channel_map) > 0 else ','.join(self.channels[:channels])
+                            output_type = 'jack-sink'
+
+                        command += f"pmctl init {output_type} {sink} {channels} {channel_map}\n"
 
         if self.loglevel > 1: print(command)
         return command
@@ -148,7 +155,7 @@ def start_sources(self):
                     input_type = 'source'
 
                     # for jack sources
-                    if self.config['jack']['enable'] == True:
+                    if self.config['jack']['enable'] is True:
                         channels = self.config['hi'][device_id]['channels']
                         channel_map = self.config['hi'][device_id]['channel_map']
                         channel_map = ','.join(channel_map) if len(channel_map) > 0 else ','.join(self.channels[:channels])
@@ -156,11 +163,9 @@ def start_sources(self):
 
                     command += f"pmctl init {input_type} {source} {channels} {channel_map}\n"
 
-
         if self.loglevel > 1: print(command)
         return command
 
-
     # creates all ladspa sinks for eq plugin
     def start_eqs(self):
         command = ''
@@ -168,10 +173,10 @@ def start_eqs(self):
             for output_id in self.config[output_type]:
 
                 # check if device uses an eq
-                if self.config[output_type][output_id]['use_eq'] == True:
+                if self.config[output_type][output_id]['use_eq'] is True:
 
                     # create eq sink
-                    command += self.eq(output_type, output_id, status=True, 
+                    command += self.eq(output_type, output_id, status=True,
                             reconnect=False, change_config=False, run_command=False)
 
         return command
@@ -183,10 +188,10 @@ def start_rnnoise(self):
         for input_id in self.config[input_type]:
 
             # check if device uses rnnoise
-            if self.config[input_type][input_id]['use_rnnoise'] == True:
+            if self.config[input_type][input_id]['use_rnnoise'] is True:
 
                 # create rnnoise sink
-                command += self.rnnoise(input_id, status=True, 
+                command += self.rnnoise(input_id, status=True,
                         reconnect=False, change_config=False, run_command=False)
         return command
 
@@ -204,9 +209,9 @@ def start_primarys(self):
         command = ''
         for device_type in ['vi', 'b']:
             for device_id in self.config[device_type]:
-                if self.config[device_type][device_id]['primary'] == True:
+                if self.config[device_type][device_id]['primary'] is True:
                     command += self.set_primary(device_type, device_id, run_command=False)
-                
+
         if self.loglevel > 1: print(command)
         return command
 
@@ -218,6 +223,7 @@ def stop_connections(self, run_command=True):
         if run_command: os.popen(command)
         return command
 
+
     def cleanup(self):
         # remove connections
         # print('removing connections')
@@ -228,7 +234,7 @@ def cleanup(self):
         for hi_id in self.config['hi']:
             hi_config = self.config['hi'][hi_id]
             if hi_config['use_rnnoise']:
-                command += self.rnnoise(hi_id, status=False, 
+                command += self.rnnoise(hi_id, status=False,
                         reconnect=False, change_config=False, run_command=False)
 
         # remove eqs
@@ -237,7 +243,7 @@ def cleanup(self):
             for output_id in self.config[output_type]:
                 output_config = self.config[output_type][output_id]
                 if output_config['use_eq']:
-                    command += self.eq(output_type, output_id, status=False, 
+                    command += self.eq(output_type, output_id, status=False,
                             reconnect=False, change_config=False, run_command=False)
 
         # remove virtual devices
@@ -249,8 +255,19 @@ def cleanup(self):
 
         os.popen(command)
 
+    # It searches in the facility for the specific device with the index and returns this device.
+    async def device_from_event(self, event):
+        if event.facility == 'sink':
+            dev_list = await self.pulsectl_async.sink_list()
+        elif event.facility == 'source':
+            dev_list = await self.pulsectl_async.source_list()
+        else:
+            return
+
+        for device in dev_list:
+            if device.index == event.index:
+                return device
 
-    
     # creates a ladspa sink with rnnoise for a given hardware input
     def rnnoise(self, input_id, status=None, control=None, latency=None, reconnect=True,
             change_config=True, run_command=True, ladspa_sink=None):
@@ -260,19 +277,20 @@ def rnnoise(self, input_id, status=None, control=None, latency=None, reconnect=T
         source_config = self.config[input_type][input_id]
         source = source_config['name']
 
-        if latency == None:
+        if latency is None:
             latency = source_config['rnnoise_latency']
 
-        if control == None:
-            control = source_config['rnnoise_control'] 
+        if control is None:
+            control = source_config['rnnoise_control']
             control == 95 if control == '' else int(control)
 
         # set name for the plugin sink
-        if ladspa_sink == None: ladspa_sink = f'{input_type}{input_id}_rnnoise'
+        if ladspa_sink is None: ladspa_sink = f'{input_type}{input_id}_rnnoise'
 
         # status = None -> toggle state
-        if status == None:
+        if status is None:
             status = not source_config['use_rnnoise']
+            conn_status = 'connect' if status else 'disconnect'
 
         # if only changing control
         elif status == 'set':
@@ -285,7 +303,7 @@ def rnnoise(self, input_id, status=None, control=None, latency=None, reconnect=T
         # create ladspa sink
         command = f'pmctl rnnoise {ladspa_sink} {source} {control} {conn_status} {latency}\n'
 
-        if change_config: 
+        if change_config:
             if status != 'set': source_config['use_rnnoise'] = status
             source_config['rnnoise_latency'] = int(latency)
             source_config['rnnoise_control'] = int(control)
@@ -297,9 +315,9 @@ def rnnoise(self, input_id, status=None, control=None, latency=None, reconnect=T
             for output_type in ['a', 'b']:
                 for output_id in self.config[output_type]:
                     sink = output_type + output_id
-                    
-                    #if the source is connected to that device
-                    if source_config[sink] == True:
+
+                    # if the source is connected to that device
+                    if source_config[sink] is True:
                         sink_name = self.get_correct_device([output_type, output_id], 'sink')
                         latency = source_config[sink + '_latency']
 
@@ -307,18 +325,18 @@ def rnnoise(self, input_id, status=None, control=None, latency=None, reconnect=T
                         if status == 'set' and conn_status:
                             command += f'pmctl disconnect {ladspa_sink}.monitor {sink_name}\n'
                             command += f'pmctl connect {ladspa_sink}.monitor {sink_name} {latency}\n'
-                        
+
                         # disconnect source from sinks, then connect ladspa sink to sinks
-                        elif status == True:
+                        elif status is True:
                             command += f'pmctl disconnect {source} {sink_name}\n'
                             command += f'pmctl connect {ladspa_sink}.monitor {sink_name} {latency}\n'
 
                         # disconnect ladspa sink from sinks, then connect source to sinks
-                        elif status == False:
+                        elif status is False:
                             command += f'pmctl disconnect {ladspa_sink}.monitor {sink_name} {latency}\n'
                             command += f'pmctl connect {source} {sink_name} {latency}\n'
 
-        if run_command == True:
+        if run_command is True:
             if self.loglevel > 1: print(command)
             os.popen(command)
             return f'rnnoise {input_id} {status} {control}'
@@ -336,15 +354,15 @@ def eq(self, output_type, output_id, status=None, control=None, ladspa_sink=None
 
 
         # set name for the plugin sink
-        if ladspa_sink == None: ladspa_sink = f'{output_type}{output_id}_eq'
+        if ladspa_sink is None: ladspa_sink = f'{output_type}{output_id}_eq'
 
         # control values for the eq
-        if control == None:
+        if control is None:
             control = sink_config['eq_control']
             control = '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0' if control == '' else control
 
         # toggle on/off
-        if status == None:
+        if status is None:
             status = not sink_config['use_eq']
             conn_status = status
 
@@ -355,14 +373,13 @@ def eq(self, output_type, output_id, status=None, control=None, ladspa_sink=None
         else:
             conn_status = str2bool(status)
 
-
-        if change_config: 
+        if change_config:
             if status != 'set': sink_config['use_eq'] = conn_status
             sink_config['eq_control'] = control
 
-        if status == sink_config['use_eq'] == False:
+        if status == sink_config['use_eq'] is False:
             return f'eq {output_type} {output_id} {conn_status} {control}'
-        
+
         # create ladspa sink
         if conn_status:
             command = f'pmctl eq init {ladspa_sink} {master} {control}\n'
@@ -378,8 +395,8 @@ def eq(self, output_type, output_id, status=None, control=None, ladspa_sink=None
             for input_type in ['hi', 'vi']:
                 for input_id in self.config[input_type]:
 
-                    #if the source is connected to that device
-                    if self.config[input_type][input_id][sink] == True:
+                    # if the source is connected to that device
+                    if self.config[input_type][input_id][sink] is True:
                         vi = self.get_correct_device([input_type, input_id], 'source')
 
 
@@ -405,7 +422,7 @@ def eq(self, output_type, output_id, status=None, control=None, ladspa_sink=None
             return f'eq {output_type} {output_id} {status} {control}'
         else:
             return command
-        
+
     # recreates connections fom a device, does not affect config
     def reconnect(self, device_type, device_id, status=True, run_command=True):
         command = ''
@@ -417,18 +434,20 @@ def reconnect(self, device_type, device_id, status=True, run_command=True):
         output_type = ''
         output_id = ''
 
-        # if trying to reset a sink 
+        # if trying to reset a sink
         if device_type in sinks:
             device_types = sources
             output_type = device_type
             output_id = device_id
 
-        # if trying to reset a source 
+        # if trying to reset a source
         elif device_type in sources:
             device_types = sinks
             input_type = device_type
             input_id = device_id
 
+        else: return 'invalid device'
+
         # reseting the values here just to be clear that they're different now
         device_type = ''
         device_id = ''
@@ -437,30 +456,30 @@ def reconnect(self, device_type, device_id, status=True, run_command=True):
         for device_type in device_types:
             for device_id in self.config[device_type]:
 
-                # if trying to reset a sink 
+                # if trying to reset a sink
                 if device_type in sources:
                     input_type = device_type
                     input_id = device_id
 
-                # if trying to reset a source 
+                # if trying to reset a source
                 else:
                     output_type = device_type
                     output_id = device_id
 
                 sink = output_type + output_id
                 # connection status check
-                if self.config[input_type][input_id][sink] == True:
+                if self.config[input_type][input_id][sink] is True:
                     command += self.connect(input_type, input_id, output_type, output_id,
                             status=status, change_state=False, run_command=False, init=True)
 
-        if run_command: 
+        if run_command:
             if self.loglevel > 1: print(command)
             os.popen(command)
         # print(command)
         return command
 
     # connects an input to an output
-    def connect(self, input_type, input_id, output_type, output_id, 
+    def connect(self, input_type, input_id, output_type, output_id,
             status=None, latency=None, run_command=True, change_state=True, init=False):
 
         source_config = self.config[input_type][input_id]
@@ -469,7 +488,7 @@ def connect(self, input_type, input_id, output_type, output_id,
         cur_conn_status = source_config[output_type + output_id]
 
         # toggle connection status
-        if status == None:
+        if status is None:
             status = not cur_conn_status
         else:
             status = str2bool(status)
@@ -477,26 +496,26 @@ def connect(self, input_type, input_id, output_type, output_id,
         conn_status = 'connect' if status else 'disconnect'
 
         # if trying to set the same state
-        if (init == False and latency == None) and ((cur_conn_status and status)
-                or (not cur_conn_status and not status)):
+        if (init is False and latency is None) and ((cur_conn_status and status) or
+                (not cur_conn_status and not status)):
             return ''
 
         # if true, will change the config status
-        if change_state == True:
-            source_config[output_type + output_id] = True if status else False
+        if change_state is True:
+            source_config[output_type + output_id] = status
 
         # if using jack
-        if (jack_config['enable'] == True and 
-                ((sink_config['jack'] == True and output_type == 'a') or 
-                (source_config['jack'] == True and input_type == 'hi') or 
+        if (jack_config['enable'] is True and
+                ((sink_config['jack'] is True and output_type == 'a') or
+                (source_config['jack'] is True and input_type == 'hi') or
                 (input_type == 'vi' and output_type == 'b'))):
-                    return self.connect_jack(conn_status, [input_type, input_id], 
-                            [output_type, output_id])
+            return self.connect_jack(conn_status, [input_type, input_id],
+                                     [output_type, output_id])
 
         # get name and latency of devices
         source = self.get_correct_device([input_type, input_id], 'source')
         sink = self.get_correct_device([output_type, output_id], 'sink')
-        if latency == None:
+        if latency is None:
             latency = source_config[f'{output_type}{output_id}_latency']
         else:
             source_config[f'{output_type}{output_id}_latency'] = int(latency)
@@ -505,18 +524,21 @@ def connect(self, input_type, input_id, output_type, output_id,
         # if (source == '' or sink == ''):
         #    return ''
 
-        command = f"pmctl {conn_status} {source} {sink} {latency}\n"
-        device_exists = self.config[output_type][output_id]['name'] != ''
+        device_exists = source != '' and sink != ''
+        # device_exists = self.config[input_type][output_id]['name'] != ''
 
-        if device_exists:
-            if run_command == True: 
+        if device_exists is True:
+            command = f'pmctl {conn_status} {source} {sink} {latency}\n'
+        else:
+            command = ''
+
+        if run_command is True:
+            if device_exists:
                 if self.loglevel > 1: print(command)
                 os.popen(command)
-                return f'connect {input_type} {input_id} {output_type} {output_id} {status} {latency}'
-            else:
-                return command
-        else: 
             return f'connect {input_type} {input_id} {output_type} {output_id} {status} {latency}'
+        else:
+            return command
 
     # needs commenting
     def connect_jack(self, state, source_index, sink_index, init=None):
@@ -526,14 +548,15 @@ def connect_jack(self, state, source_index, sink_index, init=None):
 
         dev_name = sink_config['name']
         if dev_name == '': return ''
-        
+
         jack_map = f'{sink_index[0]}{sink_index[1]}_jack_map'
         port_group = f'{sink_index[0]}{sink_index[1]}_port_group'
         command = ''
         source = source_config['name']
-        if source_config[port_group] == False:
+        if source_config[port_group] is False:
             for channel in source_config[jack_map]:
                 for system_chan in source_config[jack_map][channel]:
+                    sink = ''
                     if sink_index[0] == 'a':
                         system_chan = f'playback_{system_chan}'
                         sink = 'system'
@@ -545,7 +568,7 @@ def connect_jack(self, state, source_index, sink_index, init=None):
                     command += f"pmctl jack-{state} {source} {channel} {sink} {system_chan}\n"
                     # command += f"jack-{state} {source} {channel} {system_chan}\n"
         else:
-            if source_config['jack'] == False or source_index[0] == 'vi':
+            if source_config['jack'] is False or source_index[0] == 'vi':
                 source_channels = source_config['channel_map']
                 len_source_channels = source_config['channels']
             else:
@@ -562,6 +585,7 @@ def connect_jack(self, state, source_index, sink_index, init=None):
             for channel_num in range(min_len):
                 channel = source_channels[channel_num]
                 sink_channel = channel_group[channel_num]
+                sink = ''
                 if source_index[0] == 'hi':
                     channel = f'capture_{channel}'
                     source = 'system'
@@ -593,9 +617,9 @@ def change_hardware_device(self, output_type, output_id, name):
         # if chosen device is not an empty name
         if name != '':
             self.toggle_hardware_device(output_type, output_id, True)
-            # self.config[output_type][output_id]['jack'] = re.search('JACK:', 
+            # self.config[output_type][output_id]['jack'] = re.search('JACK:',
                     # new_device['description'])
-            # self.config[output_type][output_id]['jack'] = name in  
+            # self.config[output_type][output_id]['jack'] = name in
         else:
             name = None
 
@@ -609,49 +633,51 @@ def toggle_hardware_device(self, device_type, device_id, status, run_command=Tru
         status = str2bool(status)
 
         # check what type of device, then disable plugins
-        if device_type == 'a': 
+        if device_type == 'a':
             device_list = ['hi', 'vi']
-            output_type = device_type
-            output_id = device_id
-            sink = f'{device_type}{device_id}'
-        else:
-            device_list =['a', 'b']
-            input_type = device_type
-            input_id = device_id
 
-        if status:
-            if device_type == 'a' and device_config['use_eq']:
-                command += self.eq(output_type, output_id, status=True, 
-                        reconnect=False, change_config=False, run_command=False)
+            if status:
+                if device_config['use_eq']:
+                    command += self.eq(device_type, device_id, status=True,
+                            reconnect=False, change_config=False, run_command=False)
 
-            elif device_type == 'hi' and device_config['use_rnnoise']:
-                command += self.rnnoise(input_id, status=True, 
-                        reconnect=False, change_config=False, run_command=False)
+        else:
+            device_list = ['a', 'b']
 
+            if status:
+                if device_type == 'hi' and device_config['use_rnnoise']:
+                    command += self.rnnoise(device_id, status=True,
+                            reconnect=False, change_config=False, run_command=False)
 
         # set connections
         for target_type in device_list:
             for target_num in self.config[target_type]:
 
+                sink = ''
                 if device_type == 'a':
                     input_type = target_type
                     input_id = target_num
+                    output_type = device_type
+                    output_id = device_id
+                    sink = f'{device_type}{device_id}'
                 else:
+                    input_type = device_type
+                    input_id = device_id
                     output_type = target_type
                     output_id = target_num
                     sink = f'{output_type}{output_id}'
 
-                if self.config[input_type][input_id][sink] == True:
+                if self.config[input_type][input_id][sink] is True:
                     command += self.connect(input_type, input_id, output_type, output_id,
                             status=status, run_command=False, change_state=False, init=True)
 
         if not status:
             if device_type == 'a' and device_config['use_eq']:
-                command += self.eq(output_type, output_id, status=False, 
+                command += self.eq(device_type, device_id, status=False,
                         reconnect=False, change_config=False, run_command=False)
 
             elif device_type == 'hi' and device_config['use_rnnoise']:
-                command += self.rnnoise(input_id, status=False, 
+                command += self.rnnoise(device_id, status=False,
                         reconnect=False, change_config=False, run_command=False)
 
         if self.loglevel > 1: print(command)
@@ -660,6 +686,10 @@ def toggle_hardware_device(self, device_type, device_id, status, run_command=Tru
     def toggle_virtual_device(self, device_type, device_id, status=False, disconnect=True, run_command=True):
         command = ''
         device_config = self.config[device_type][device_id]
+
+        if device_config['external'] is True:
+            return ''
+
         name = device_config["name"]
         if name == '': return ''
         status = str2bool(status)
@@ -676,7 +706,6 @@ def toggle_virtual_device(self, device_type, device_id, status=False, disconnect
         if not status:
             command += f'pmctl remove {name}\n'
 
-
         if self.loglevel > 1: print(command)
         if run_command: os.popen(command)
         return command
@@ -691,7 +720,7 @@ def rename(self, device_type, device_id, new_name):
             # return False
 
         if old_name != '':
-            command += self.toggle_virtual_device(device_type, device_id, status=False, 
+            command += self.toggle_virtual_device(device_type, device_id, status=False,
                     run_command=False)
 
         if new_name != '':
@@ -733,26 +762,30 @@ def get_virtual_devices(self, kind):
 
     # get list of cards
     def get_hardware_devices(self, kind):
-        command = f"pmctl list {kind}"
-        devices = cmd(command)
         try:
-            jason = json.loads(devices)
+            command = f"pmctl list {kind}"
+            devices = cmd(command)
+            try:
+                jason = json.loads(devices)
+            except:
+                print(f'ERROR: invalid json {devices}')
+                return 'empty'
+
+            # if using jack, also list groups
+            if self.config['jack']['enable'] == True:
+                group_type = 'input_groups' if kind == 'sources' else 'output_groups'
+                for group in self.config['jack'][group_type]:
+                    channels = self.config['jack'][group_type][group]
+                    jason.append({'name': group, 'description': f'JACK:{group}{channels}'})
+                    # devices_concat.append()
+
+            if kind == 'sources':
+                self.source_list = jason
+            else:
+                self.sink_list = jason
+            return json.dumps(jason, ensure_ascii=False)
         except:
-            print(f'ERROR: invalid json {devices}')
-
-        # if using jack, also list groups
-        if self.config['jack']['enable'] == True:
-            group_type = 'input_groups' if kind == 'sources' else 'output_groups'
-            for group in self.config['jack'][group_type]:
-                channels = self.config['jack'][group_type][group]
-                jason.append({'name': group, 'description': f'JACK:{group}{channels}'})
-                # devices_concat.append()
-
-        if kind == 'sources':
-            self.source_list = jason
-        else:
-            self.sink_list = jason
-        return json.dumps(jason, ensure_ascii=False)
+            traceback.print_exc()
 
     def mute(self, device_type, device_id, state=None, run_command=True):
         device_config = self.config[device_type][device_id]
@@ -763,7 +796,7 @@ def mute(self, device_type, device_id, state=None, run_command=True):
         device = 'sink' if device_type == 'a' or device_type == 'vi' else 'source'
 
         # if a state is None, toggle it
-        if state == None:
+        if state is None:
             state = not device_config['mute']
         else:
             state = str2bool(state)
@@ -774,13 +807,13 @@ def mute(self, device_type, device_id, state=None, run_command=True):
         conn_status = 1 if state else 0
         command = f"pmctl mute {device} {name} {conn_status}\n"
 
-        if run_command: 
+        if run_command:
             if self.loglevel > 1: print(command)
             os.popen(command)
             return f'mute {device_type} {device_id} {state}'
         else:
             return command
-    
+
     # set a device as primary (only vi and b)
     def set_primary(self, device_type, device_id, run_command=True):
         name = self.config[device_type][device_id]['name']
@@ -789,7 +822,7 @@ def set_primary(self, device_type, device_id, run_command=True):
 
         device = 'sink' if device_type == 'vi' else 'source'
         command = f'pmctl set-primary {device} {name}\n'
-        if run_command: 
+        if run_command:
             os.popen(command)
             return f'primary {device_type} {device_id}'
         return command
@@ -808,20 +841,23 @@ def volume(self, device_type, device_id, val):
             # check if is an absolute number or + and -
             if val.isdigit():
                 val = int(val)
-            #if re.match('[1-9]', val):
+            # if re.match('[1-9]', val):
             #    val = int(val)
 
-            # if not an absolute number, add it to current volume 
+            # if not an absolute number, add it to current volume
             else:
                 val = device_config['vol'] + int(val)
 
         # limit volume at 153
         if val > 153:
             val = 153
+        # limit volume at 0
+        elif val < 0:
+            val = 0
 
         device_config['vol'] = val
         name = device_config['name']
-        
+
         # get device info from pulsectl
         if device_type in ['a', 'vi']:
             device = self.pulsectl.get_sink_by_name(name)
@@ -830,10 +866,22 @@ def volume(self, device_type, device_id, val):
 
         # set the volume
         volume = device.volume
-        volume.value_flat = val / 100 
+        volume.value_flat = val / 100
         self.pulsectl.volume_set(device, volume)
         return f'volume {device_type} {device_id} {val}'
 
+    def device_new(self, index, facility):
+        if index != 'None' and facility != 'None':
+            return f'device-new {index} {facility}'
+        else:
+            return ''
+
+    def device_remove(self, index, facility):
+        if index != 'None' and facility != 'None':
+            return f'device-remove {index} {facility}'
+        else:
+            return ''
+
     # sink input and source output volumes
     def app_volume(self, id, val, stream_type):
 
@@ -842,10 +890,10 @@ def app_volume(self, id, val, stream_type):
             # check if is an absolute number
             if val.isdigit():
                 val = int(val)
-            else: 
+            else:
                 return False
 
-            # if not an absolute number, add it to current volume 
+            # if not an absolute number, add it to current volume
             # else:
                 # cur_vol = cmd('pmctl get-{stream_type}-volume {id}')
                 # val = device_config['vol'] + int(val)
@@ -874,9 +922,9 @@ def get_app_streams(self, dev_type):
         devices = cmd(command)
         if devices != '':
             try:
-                jason = json.loads(devices)
+                json.loads(devices)
             except Exception:
-                print(f'ERROR: invalid json {devices}')
+                print(f'[ERROR] invalid json {devices}')
                 return 'ERROR: invalid json'
             return devices
         else: return '[]'
@@ -892,14 +940,14 @@ def subscribe(self):
         sys.stdout.flush()
         env = os.environ
         env['LC_ALL'] = 'C'
-        self.MyOut = subprocess.Popen(command, env=env, 
-            stdout=subprocess.PIPE, 
+        self.MyOut = subprocess.Popen(command, env=env,
+            stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             universal_newlines=True)
 
         for stdout_line in iter(self.MyOut.stdout.readline, ""):
-            yield stdout_line 
-            
+            yield stdout_line
+
         self.MyOut.stdout.close()
         return_code = self.MyOut.wait()
         if return_code:
@@ -913,22 +961,16 @@ def jack_get_ports(self):
 
 def cmd(command):
     sys.stdout.flush()
-    p = subprocess.Popen(command.split(' '), 
-        stdout=subprocess.PIPE, 
+    p = subprocess.Popen(command.split(' '),
+        stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT)
-    stdout,stderr = p.communicate()
+    stdout, stderr = p.communicate()
     if p.returncode:
         raise
     return stdout.decode()
 
-def run_command(command):
-    try:
-        subprocess.check_call(command)
-    except:
-        sys.exit(1)
-
 def str2bool(v):
-    if type(v) == bool: 
+    if type(v) == bool:
         return v
     else:
         return v.lower() in ['connect', 'true', 'on', '1']
diff --git a/pulsemeeter/config.json b/pulsemeeter/config.json
index 2b99b7c..2bb258a 100644
--- a/pulsemeeter/config.json
+++ b/pulsemeeter/config.json
@@ -64,6 +64,7 @@
 		"1": {
 			"name": "Virtual_Input_1",
 			"primary": false,
+			"external": false,
 			"a1": false,
 			"a2": false,
 			"a3": false,
@@ -91,6 +92,7 @@
 		"2": {
 			"name": "Virtual_Input_2",
 			"primary": false,
+			"external": false,
 			"a1": false,
 			"a2": false,
 			"a3": false,
@@ -118,6 +120,7 @@
 		"3": {
 			"name": "Virtual_Input_3",
 			"primary": false,
+			"external": true,
 			"a1": false,
 			"a2": false,
 			"a3": false,
diff --git a/pulsemeeter/interface/app_list_widget.py b/pulsemeeter/interface/app_list_widget.py
index 14fc653..e7ba360 100644
--- a/pulsemeeter/interface/app_list_widget.py
+++ b/pulsemeeter/interface/app_list_widget.py
@@ -1,9 +1,10 @@
-import sys
-from ..settings import GLADEFILE
+# import sys
+# from ..settings import GLADEFILE
 
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
-from gi.repository import Gtk,Gio
+from gi.repository import Gtk, Gio
+
 
 class AppList(Gtk.VBox):
 
@@ -17,40 +18,18 @@ def __init__(self, dev_type, client):
         self.load_application_list()
 
     def load_application_list(self, id=None):
-        if id == None and len(self.box_list) > 0:
+        if id is None and len(self.box_list) > 0:
             self.remove_app_dev()
 
         app_list = self.client.list_apps(self.dev_type)
 
-        if len(app_list) == 0: 
+        if len(app_list) == 0:
             return
 
-        name_vi = []
-        name_b = []
-        for i in ['vi', 'b']:
-            for j in self.config[i]:
-                device_config = self.config[i][j]
-                if device_config['name'] != '':
-
-                    # if virtual input
-                    if i == 'vi':
-                        name = device_config['name']
-                        if self.dev_type == 'source-output':
-                            name += '.monitor'
-                        name_vi.append(name)
-
-                    # if virtual output
-                    elif self.dev_type != 'sink-input':
-                        name_b.append(device_config['name'])
-
-        if self.dev_type == 'source-output':
-            name_b.extend(name_vi)
-            dev_list = name_b
-        else:
-            dev_list = name_vi
+        dev_list = self.get_device_list()
 
         for i in app_list:
-            if id != None:
+            if id is not None:
                 if str(id) != str(i['id']):
                     continue
 
@@ -69,7 +48,7 @@ def load_application_list(self, id=None):
                 combobox.append_text(dev)
             try:
                 index = dev_list.index(i['device'])
-            except:
+            except Exception:
                 index = 0
             combobox.set_active(index)
             combobox.connect('changed', self.app_combo_change, i['id'])
@@ -94,23 +73,50 @@ def load_application_list(self, id=None):
             separator_bottom.set_margin_top(5)
 
             box = Gtk.Box(spacing=5)
-            box.pack_start(image, expand = False, fill = False, padding = 0)
-            box.pack_start(label, expand = False, fill = False, padding = 0)
-            box.pack_start(combobox, expand = False, fill = True, padding = 0)
+            box.pack_start(image, expand=False, fill=False, padding=0)
+            box.pack_start(label, expand=False, fill=False, padding=0)
+            box.pack_start(combobox, expand=False, fill=True, padding=0)
 
             vbox = Gtk.VBox(spacing=0)
-            vbox.pack_start(separator_top, expand = False, fill = False, padding = 0)
-            vbox.pack_start(box, expand = False, fill = False, padding = 0)
-            vbox.pack_start(scale, expand = False, fill = False, padding = 0)
-            vbox.pack_start(separator_bottom, expand = False, fill = False, padding = 0)
+            vbox.pack_start(separator_top, expand=False, fill=False, padding=0)
+            vbox.pack_start(box, expand=False, fill=False, padding=0)
+            vbox.pack_start(scale, expand=False, fill=False, padding=0)
+            vbox.pack_start(separator_bottom, expand=False, fill=False, padding=0)
 
             self.box_list.append([vbox, i['id']])
-            self.pack_start(vbox, expand = False, fill = True, padding = 0)
+            self.pack_start(vbox, expand=False, fill=True, padding=0)
 
             self.show_all()
 
+    def get_device_list(self):
+        name_vi = []
+        name_b = []
+        for i in ['vi', 'b']:
+            for j in self.config[i]:
+                device_config = self.config[i][j]
+                if device_config['name'] != '':
+
+                    # if virtual input
+                    if i == 'vi':
+                        name = device_config['name']
+                        if self.dev_type == 'source-output':
+                            name += '.monitor'
+                        name_vi.append(name)
+
+                    # if virtual output
+                    elif self.dev_type != 'sink-input':
+                        name_b.append(device_config['name'])
+
+        if self.dev_type == 'source-output':
+            name_b.extend(name_vi)
+            dev_list = name_b
+        else:
+            dev_list = name_vi
+
+        return dev_list
+
     def remove_app_dev(self, id=None):
-        if id == None:
+        if id is None:
             for i in self.box_list:
                 self.remove(i[0])
             self.box_list.clear()
diff --git a/pulsemeeter/interface/eq_popover.py b/pulsemeeter/interface/eq_popover.py
index 65e6c07..00f89d2 100644
--- a/pulsemeeter/interface/eq_popover.py
+++ b/pulsemeeter/interface/eq_popover.py
@@ -5,7 +5,8 @@
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk
+from gi.repository import Gtk
+
 
 class EqPopover():
 
@@ -67,7 +68,7 @@ def __init__(self, button, sock, output_type, output_id):
         self.apply_eq_button = self.builder.get_object('apply_eq_button')
         self.reset_eq_button = self.builder.get_object('reset_eq_button')
 
-        control = self.device_config['eq_control'] 
+        control = self.device_config['eq_control']
         j = 0
         if control != '':
             for i in control.split(','):
@@ -85,8 +86,6 @@ def __init__(self, button, sock, output_type, output_id):
         self.builder.connect_signals(self)
 
     def apply_eq(self, widget, output_type, output_id):
-        # if self.device_config['use_eq'] == False:
-            # return
         control=''
         for i in self.eq:
             control = control + ',' + str(i.get_value())
diff --git a/pulsemeeter/interface/groups_popover.py b/pulsemeeter/interface/groups_popover.py
index 1cb98a0..567869d 100644
--- a/pulsemeeter/interface/groups_popover.py
+++ b/pulsemeeter/interface/groups_popover.py
@@ -5,7 +5,8 @@
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk,Gio
+from gi.repository import Gtk, Gio
+
 
 class JackGroupsPopover():
     def __init__(self, button, pulse):
diff --git a/pulsemeeter/interface/latency_popover.py b/pulsemeeter/interface/latency_popover.py
index 29d5138..4001195 100644
--- a/pulsemeeter/interface/latency_popover.py
+++ b/pulsemeeter/interface/latency_popover.py
@@ -4,7 +4,8 @@
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk
+from gi.repository import Gtk
+
 
 class LatencyPopover():
     def __init__(self, button, sock, input_index, output_index):
@@ -42,10 +43,9 @@ def __init__(self, button, sock, input_index, output_index):
         self.apply_latency_button = self.builder.get_object('apply_latency_button')
         self.apply_latency_button.connect('pressed', self.apply_latency, input_type, input_id, output_type, output_id)
 
-
         self.latency_popover.popup()
 
     def apply_latency(self, widget, input_type, input_id, output_type, output_id):
-        sink = output_type + output_id
+        # sink = output_type + output_id
         latency = int(self.latency_adjust.get_value())
         self.sock.connect(input_type, input_id, output_type, output_id, True, latency)
diff --git a/pulsemeeter/interface/main_window.py b/pulsemeeter/interface/main_window.py
old mode 100755
new mode 100644
index 5a00713..52f4fc1
--- a/pulsemeeter/interface/main_window.py
+++ b/pulsemeeter/interface/main_window.py
@@ -1,12 +1,9 @@
 import os
-import time
-import pulsectl
 import re
 import signal
 import shutil
 import threading
 import sys
-import json
 
 from .app_list_widget import AppList
 from .eq_popover import EqPopover
@@ -16,7 +13,7 @@
 from .port_select_popover import PortSelectPopover
 from .vumeter_widget import Vumeter
 
-from ..settings import GLADEFILE, LAYOUT_DIR
+from ..settings import LAYOUT_DIR
 from ..socket import Client
 
 from gi import require_version as gi_require_version
@@ -24,7 +21,7 @@
 # from pulsectl import Pulse
 gi_require_version('Gtk', '3.0')
 gi_require_version('AppIndicator3', '0.1')
-from gi.repository import Gtk,Gdk,Gio,GLib,AppIndicator3
+from gi.repository import Gtk, GLib, AppIndicator3
 
 class MainWindow(Gtk.Window):
 
@@ -41,16 +38,13 @@ def __init__(self, isserver=False, trayonly=False):
             self.tray = self.create_indicator()
             self.client.set_callback_function('tray', self.update_tray_status)
 
-        if trayonly: 
+        if trayonly:
             self.client.set_callback_function('exit', self.close_on_server_exit)
             return
 
         self.windowinstance = self.start_window(isserver)
-        # if self.config['tray'] and isserver:
-            # self.create_indicator()
 
     def start_window(self, isserver):
-
         self.trayonly = False
         self.exit_flag = False
         GLib.threads_init()
@@ -60,26 +54,26 @@ def start_window(self, isserver):
         self.layout = self.config['layout']
 
         component_list = [
-                    'window',
-                    'menu_popover',
-                    'rename_popover',
-                    'popover_entry',
-                    'latency_popover',
-                    'latency_adjust',
-                    'rnnoise_popover',
-                    'rnnoise_latency_adjust',
-                    'rnnoise_threshold_adjust',
-                    'jack_group_popover',
-                    'sink_input_list',
-                    'source_output_list',
-                    'sink_input_scroll',
-                    'source_output_scroll',
-                    'source_output_viewport',
-                    'sink_input_viewport',
-                    'vumeter_toggle',
-                    'vi_1_peak',
-                    'channel_groups',
-                ]
+            'window',
+            'menu_popover',
+            'rename_popover',
+            'popover_entry',
+            'latency_popover',
+            'latency_adjust',
+            'rnnoise_popover',
+            'rnnoise_latency_adjust',
+            'rnnoise_threshold_adjust',
+            'jack_group_popover',
+            'sink_input_list',
+            'source_output_list',
+            'sink_input_scroll',
+            'source_output_scroll',
+            'source_output_viewport',
+            'sink_input_viewport',
+            'vumeter_toggle',
+            'vi_1_peak',
+            'channel_groups',
+        ]
 
         for i in range(1, 4):
             component_list.append(f'hi_{i}_adjust')
@@ -87,7 +81,6 @@ def start_window(self, isserver):
             component_list.append(f'a_{i}_adjust')
             component_list.append(f'b_{i}_adjust')
 
-                # os.path.join(LAYOUT_DIR, f'{self.layout}.glade'),
         try:
             self.builder.add_objects_from_file(
                 os.path.join(LAYOUT_DIR, f'{self.layout}.glade'),
@@ -97,7 +90,6 @@ def start_window(self, isserver):
             print('Error building main window!\n{}'.format(ex))
             sys.exit(1)
 
-
         self.devices = {}
         self.devices['a'] = self.client.list_hardware_devices('sinks')
         self.devices['hi'] = self.client.list_hardware_devices('sources')
@@ -114,7 +106,8 @@ def start_window(self, isserver):
         self.eq_buttons = {}
 
         self.enable_vumeters = True
-        if not shutil.which('pulse-vumeter') or self.config['enable_vumeters'] == False:
+        if shutil.which('pulse-vumeter') is False or \
+                self.config['enable_vumeters'] is False:
             self.enable_vumeters = False
 
         self.start_hardware_comboboxes()
@@ -139,8 +132,7 @@ def start_window(self, isserver):
 
         self.window.show_all()
 
-
-        if not isserver:
+        if isserver is not False:
             signal.signal(signal.SIGTERM, self.delete_event)
             signal.signal(signal.SIGINT, self.delete_event)
 
@@ -176,25 +168,24 @@ def start_menu_items(self):
             i += 1
         self.layout_combobox.connect('changed', self.change_layout)
 
-        # self.jack_toggle_check_button = self.builder.get_object('jack_toggle')
-        # self.jack_toggle_check_button.set_active(self.pulse.config['jack']['enable'])
-        # self.jack_toggle_check_button.connect('toggled', self.toggle_jack)
-        # self.jack_toggle_check_button.set_sensitive(False)
-
+        # self.jack_toggle_button = self.builder.get_object('jack_toggle')
+        # self.jack_toggle_button.set_active(self.pulse.config['jack']['enable'])
+        # self.jack_toggle_button.connect('toggled', self.toggle_jack)
+        # self.jack_toggle_button.set_sensitive(False)
 
         # self.test = self.builder.get_object('test')
         # self.test.connect('pressed', self.open_group_popover)
-        # self.jack_group_popover = self.builder.get_object('jack_group_popover')
-        # self.jack_group_popover.set_relative_to(self.test)
-        # self.jack_toggle_check_button.connect('toggled', self.toggle_jack)
-
+        # self.jack_gp_popover = self.builder.get_object('jack_group_popover')
+        # self.jack_gp_popover.set_relative_to(self.test)
+        # self.jack_toggle_button.connect('toggled', self.toggle_jack)
 
     def toggle_tray(self, widget):
         state = widget.get_active()
         self.client.set_tray(state)
         if self.isserver:
             if state:
-                if self.tray == None: self.tray = self.create_indicator()
+                if self.tray is None:
+                    self.tray = self.create_indicator()
                 self.tray.set_status(1)
             else:
                 self.tray.set_status(0)
@@ -206,9 +197,9 @@ def toggle_cleanup(self, widget):
     def change_layout(self, combobox):
         self.client.set_layout(combobox.get_active_text())
         self.windowinstance.destroy()
+        self.delete_event()
         self.windowinstance = self.start_window(self.isserver)
         self.trayonly = False
-        # self.delete_event()
 
     def open_settings(self, widget):
         self.menu_popover.popup()
@@ -227,7 +218,7 @@ def toggle_vumeters(self, widget):
         for device_type in ['hi', 'vi', 'a', 'b']:
             for device_id in self.config[device_type]:
                 # if self.config[device_type][device_id]['name'] != '':
-                if widget.get_active() == False:
+                if widget.get_active() is False:
                     self.vu_list[device_type][device_id].close()
                 else:
                     self.vu_list[device_type][device_id].reload_device()
@@ -241,15 +232,17 @@ def start_vumeters(self):
                 device_config = self.config[device_type][device_id]
                 grid = self.builder.get_object(f'{device_type}_{device_id}_vumeter')
                 vert = True if self.layout == 'default' else False
-                vumeter = Vumeter(device_type, device_id, self.config, vertical=vert)
+                vumeter = Vumeter(device_type, device_id,
+                                  self.config, vertical=vert)
                 grid.add(vumeter)
                 if device_config['name'] != '':
-                    if self.enable_vumeters == True:
+                    if self.enable_vumeters is True:
                         try:
-                            vumeter.start() 
-                        except:
-                            print(f'Could not start vumeter for {device_type}{device_id}')
-                           
+                            vumeter.start()
+                        except Exception:
+                            print('Could not start vumeter for',
+                                  '{device_type}{device_id}')
+
                 self.vu_list[device_type][device_id] = vumeter
 
     def start_app_list(self):
@@ -262,23 +255,14 @@ def start_app_list(self):
 
             sink_input_viewport.add(self.sink_input_box)
             source_output_viewport.add(self.source_output_box)
-
-            self.subscribe_thread = threading.Thread(target=self.listen_subscribe, args=())
-            self.subscribe_thread.start()
         except Exception as ex:
-            print('App sinks returned an error, audio backend probably crashed. Server will be closed.')
+            print('App sinks returned an error, audio backend returned error')
+            print(ex)
             if self.windowinstance is not None:
                 self.windowinstance.destroy()
             self.delete_event()
-            try:
-                # I need to create a new client so it stops listening
-                client = Client()
-                client.close_server()
-            except:
-                print('Could not close server.')
             sys.exit(1)
 
-
     def start_hardware_comboboxes(self):
         for device_type in ['hi', 'a']:
             self.hardware_comboboxes[device_type] = {}
@@ -303,10 +287,11 @@ def start_hardware_comboboxes(self):
                         found = True
                         combobox.set_active(i + 1)
 
-                if found == False and device_config['jack'] == False:
+                if found is False and device_config['jack'] is False:
                     device_config['name'] = ''
 
-                combobox.connect('changed', self.on_combo_changed, device_type, device_id, devices)
+                combobox.connect('changed', self.on_combo_changed, device_type,
+                                 device_id, devices)
                 self.hardware_comboboxes[device_type][device_id] = combobox
 
     def start_inputs(self):
@@ -329,15 +314,20 @@ def start_inputs(self):
                 if input_type == 'vi':
                     name = self.config['vi'][input_id]['name']
                     label = self.builder.get_object(f'vi_{input_id}_label')
-                    label.set_text(name if name != '' else f'Virtual Input {input_id}')
-                    label_evt_box = self.builder.get_object(
-                            f'vi_{input_id}_label_event_box')
-                    label_evt_box.connect('button_press_event', self.label_click, label,
-                            'vi', input_id)
+                    label.set_text(name if name != '' else
+                                   f'Virtual Input {input_id}')
+
+                    label_evt_box = self.builder.get_object(f'vi_{input_id}_label_event_box')
+
+                    label_evt_box.connect('button_press_event', self.label_click,
+                                          label, 'vi', input_id)
+
                     primary = self.builder.get_object(f'vi_{input_id}_primary')
                     primary.set_active(self.config['vi'][input_id]['primary'])
-                    if self.config['vi'][input_id]['primary'] == True:
+
+                    if self.config['vi'][input_id]['primary'] is True:
                         primary.set_sensitive(False)
+
                     primary.connect('clicked', self.toggle_primary, 'vi', input_id)
                     self.primary_buttons['vi'][input_id] = primary
 
@@ -346,40 +336,36 @@ def start_inputs(self):
                     # noise reduction button
                     rnnoise = self.builder.get_object(f'hi_{input_id}_rnnoise')
                     rnnoise.set_active(self.config['hi'][input_id]['use_rnnoise'])
-                    rnnoise.connect('clicked', self.toggle_rnnoise, 'hi', input_id)
+                    rnnoise.connect('clicked', self.toggle_rnnoise, input_id)
+
+                    rnnoise.connect('button_press_event', self.open_popover,
+                                    RnnoisePopover, input_type, input_id)
 
-                    rnnoise.connect('button_press_event', self.open_popover, 
-                            RnnoisePopover, input_type, input_id)
                     self.rnnoise_buttons[input_id] = rnnoise
                     # check for rnnoise plugin
                     found = 0
                     for lib in ['lib', 'lib64']:
-                        for path in [f'/usr/{lib}/ladspa', f'/usr/local/{lib}/ladspa']:
-                            if os.path.isfile(os.path.join(path, 
-                                'librnnoise_ladspa.so')): 
-                                found = 1
-                                break
-                            elif os.path.isfile(os.path.join(path, 
-                                'rnnoise_ladspa.so')):
-                                found = 1
-                                break
+                        for path in [f'/usr/{lib}/ladspa',
+                                     f'/usr/local/{lib}/ladspa']:
+                            for plugin in ['librnnoise_ladspa.so',
+                                           'rnnoise_ladspa.so']:
+                                if os.path.isfile(os.path.join(path, plugin)):
+                                    found = 1
+                                    break
                     if found == 0:
                         rnnoise.set_visible(False)
                         rnnoise.set_no_show_all(True)
 
-
-                # recover volume if possible
                 source_config = self.config[input_type][input_id]
-                # for source in self.devices[input_type]:
-                    # if source['name'] == source_config['name'] and 'volume' in source:
-                        # source_config['vol'] = source['volume']
 
                 # connect volume sliders
                 adjust = self.builder.get_object(f'{input_type}_{input_id}_adjust')
                 adjust.set_value(source_config['vol'])
                 vol_slider = self.builder.get_object(f'{input_type}_{input_id}_vol')
-                vol_slider.connect('value-changed', self.volume_change, 
-                        input_type, input_id)
+
+                vol_slider.connect('value-changed', self.volume_change,
+                                   input_type, input_id)
+
                 vol_slider.add_mark(100, Gtk.PositionType.TOP, '')
                 self.volume_adjusts[input_type][input_id] = adjust
                 self.volume_sliders[input_type][input_id] = vol_slider
@@ -399,14 +385,15 @@ def start_inputs(self):
                         button.set_active(source_config[sink])
                         self.loopback_buttons[input_type][input_id][sink] = button
                         button.connect('clicked', self.toggle_loopback, input_type,
-                                input_id, output_type, output_id)
+                                       input_id, output_type, output_id)
 
-                        if self.config['jack']['enable'] == False:
-                            button.connect('button_press_event', self.latency_popover, LatencyPopover,
-                                    input_type, input_id, output_type, output_id)
+                        if self.config['jack']['enable'] is False:
+                            button.connect('button_press_event', self.latency_popover,
+                                       LatencyPopover, input_type, input_id, output_type, output_id)
                         else:
-                            button.connect('button_press_event', self.open_popover, PortSelectPopover, 
-                                    [input_type, input_id, sink])
+                            button.connect('button_press_event',
+                                       self.open_popover, PortSelectPopover,
+                                       [input_type, input_id, sink])
 
     # start output devices
     def start_outputs(self):
@@ -427,26 +414,24 @@ def start_outputs(self):
                 if output_type == 'b':
                     primary = self.builder.get_object(f'b_{output_id}_primary')
                     primary.set_active(sink_config['primary'])
-                    if sink_config['primary'] == True:
+                    if sink_config['primary'] is True:
                         primary.set_sensitive(False)
 
                     primary.connect('clicked', self.toggle_primary, 'b', output_id)
                     self.primary_buttons['b'][output_id] = primary
 
                     label = self.builder.get_object(f'b{output_id}_label')
-                    if label != None:
+                    if label is not None:
                         label.set_text(f'B{output_id} - {sink_config["name"]}')
 
-                # for sink in self.devices[output_type]:
-                    # if sink['name'] == sink_config['name'] and 'volume' in sink:
-                        # sink_config['vol'] = sink['volume']
-                
-                # volume slider and adjustment 
+                # volume slider and adjustment
                 adjust = self.builder.get_object(f'{output_type}_{output_id}_adjust')
                 adjust.set_value(sink_config['vol'])
                 vol_slider = self.builder.get_object(f'{output_type}_{output_id}_vol')
-                vol_slider.connect('value-changed', self.volume_change, output_type, 
-                        output_id)
+
+                vol_slider.connect('value-changed', self.volume_change,
+                                   output_type, output_id)
+
                 vol_slider.add_mark(100, Gtk.PositionType.TOP, '')
                 self.volume_adjusts[output_type][output_id] = adjust
                 self.volume_sliders[output_type][output_id] = vol_slider
@@ -461,15 +446,15 @@ def start_outputs(self):
                 eq = self.builder.get_object(f'{output_type}_{output_id}_eq')
                 eq.set_active(sink_config['use_eq'])
                 eq.connect('clicked', self.toggle_eq, output_type, output_id)
-                eq.connect('button_press_event', self.open_popover, EqPopover, 
-                        output_type, output_id)
+                eq.connect('button_press_event', self.open_popover, EqPopover,
+                           output_type, output_id)
                 self.eq_buttons[output_type][output_id] = eq
 
                 # to hide eq button if plugin not found
                 found = 0
                 for arc in ['', '64']:
-                    for path in [f'/usr/lib{arc}/ladspa', 
-                            f'/usr/local/lib{arc}/ladspa']:
+                    for path in [f'/usr/lib{arc}/ladspa',
+                                 f'/usr/local/lib{arc}/ladspa']:
                         if os.path.isfile(os.path.join(path, 'mbeq_1197.so')):
                             found = 1
                     if found == 0:
@@ -480,7 +465,7 @@ def toggle_eq(self, button, output_type, output_id):
         state = button.get_active()
         self.client.eq(output_type, output_id, state)
 
-    def toggle_rnnoise(self, widget, input_type, input_id):
+    def toggle_rnnoise(self, widget, input_id):
         state = widget.get_active()
         self.client.rnnoise(input_id, state)
 
@@ -505,11 +490,11 @@ def open_popover(self, button, event, popover, device_type, device_id):
             if self.config[device_type][device_id]['name'] != '':
                 popover(button, self.client, device_type, device_id)
 
-    def latency_popover(self, button, event, popover, input_type, input_id, 
+    def latency_popover(self, button, event, popover, input_type, input_id,
             output_type, output_id):
         if event.button == 3:
             if self.config[input_type][input_id]['name'] != '':
-                popover(button, self.client, [input_type, input_id], 
+                popover(button, self.client, [input_type, input_id],
                         [output_type, output_id])
 
     def label_rename_entry(self, widget):
@@ -558,7 +543,7 @@ def on_combo_changed(self, widget, output_type, output_id, device):
             # self.pulse.config[device_type][device_id]['jack'] = False
 
     def toggle_primary(self, widget, device_type, device_id):
-        if widget.get_active() == False:
+        if widget.get_active() is False:
             return
         else:
             widget.set_sensitive(False)
@@ -574,51 +559,47 @@ def toggle_primary(self, widget, device_type, device_id):
         # else:
             # self.source_output_box.load_application_list()
 
-    def listen_subscribe(self):
-        for i in self.client.subscribe():
-
-            if 'remove' in i:
-                id = i.split('#')[1].strip('\n')
-                if 'sink-input' in i:
-                    GLib.idle_add(self.sink_input_box.remove_app_dev, id)
-
-                elif 'source-output' in i:
-                    GLib.idle_add(self.source_output_box.remove_app_dev, id)
-
-            elif 'new' in i:
-                id = i.split('#')[1].strip('\n')
-                if 'sink-input' in i:
-                    GLib.idle_add(self.sink_input_box.load_application_list, id)
-
-                elif 'source-output' in i:
-                    GLib.idle_add(self.source_output_box.load_application_list, id)
-
+    # TODO: refresh while the UI is open
+    def device_new(self, index, facility):
+        # add event
+        if facility == 'sink-input':
+            GLib.idle_add(self.sink_input_box.load_application_list, index)
+        elif facility == 'source-output':
+            GLib.idle_add(self.source_output_box.load_application_list, index)
+    
+    def device_remove(self, index, facility):
+        # remove event
+        if facility == 'sink-input':
+            GLib.idle_add(self.sink_input_box.remove_app_dev, index)
+        elif facility == 'source-output':
+            GLib.idle_add(self.source_output_box.remove_app_dev, index)
 
     def listen_socket(self):
-        self.client.set_callback_function('connect', 
+        self.client.set_callback_function('connect',
                 self.update_loopback_buttons)
-        self.client.set_callback_function('mute', 
+        self.client.set_callback_function('mute',
                 self.update_mute_buttons)
-        self.client.set_callback_function('primary', 
+        self.client.set_callback_function('primary',
                 self.update_primary_buttons)
-        self.client.set_callback_function('rnnoise', 
+        self.client.set_callback_function('rnnoise',
                 self.update_rnnoise_buttons)
-        self.client.set_callback_function('eq', 
+        self.client.set_callback_function('eq',
                 self.update_eq_buttons)
-        self.client.set_callback_function('volume', 
+        self.client.set_callback_function('volume',
                 self.update_volume_slider)
-        self.client.set_callback_function('change-hd', 
+        self.client.set_callback_function('change-hd',
                 self.update_comboboxes)
-
-        self.client.set_callback_function('exit', 
+        self.client.set_callback_function('device-new',
+                self.device_new)
+        self.client.set_callback_function('device-remove',
+                self.device_remove)
+        self.client.set_callback_function('exit',
                 self.close_on_server_exit)
 
-
     def close_on_server_exit(self):
         if not self.trayonly:
             self.client.end_subscribe()
-            self.subscribe_thread.join()
-            if self.enable_vumeters == True:
+            if self.enable_vumeters is True:
                 for i in ['hi', 'vi', 'a', 'b']:
                     for j in self.vu_list[i]:
                         self.vu_list[i][j].close()
@@ -626,29 +607,29 @@ def close_on_server_exit(self):
         Gtk.main_quit()
 
     def update_loopback_buttons(self, input_type, input_id, output_type,
-            output_id, state, latency):
+            output_id, state, latency=None):
 
         sink = output_type + output_id
         state = state == 'True'
-        
+
         button = self.loopback_buttons[input_type][input_id][sink]
-        
+
         GLib.idle_add(button.set_active, state)
 
     def update_mute_buttons(self, input_type, input_id, state):
         state = state == 'True'
-        
+
         button = self.mute_buttons[input_type][input_id]
         GLib.idle_add(button.set_active, state)
 
     def update_volume_slider(self, device_type, device_id, val):
         val = int(val)
-        
+
         adjust = self.volume_adjusts[device_type][device_id]
         GLib.idle_add(adjust.set_value, val)
 
     def update_primary_buttons(self, device_type, device_id):
-        
+
         button_list = self.primary_buttons[device_type]
         for dev_id in button_list:
             if dev_id == device_id:
@@ -657,21 +638,22 @@ def update_primary_buttons(self, device_type, device_id):
             else:
                 GLib.idle_add(button_list[dev_id].set_active, False)
                 GLib.idle_add(button_list[dev_id].set_sensitive, True)
-        
+
     def update_rnnoise_buttons(self, input_id, state, control):
         state = state == 'True'
-        
+
         button = self.rnnoise_buttons[input_id]
         GLib.idle_add(button.set_active, state)
 
     def update_eq_buttons(self, output_type, output_id, state, control):
         state = state == 'True'
-        
+
         button = self.eq_buttons[output_type][output_id]
         GLib.idle_add(button.set_active, state)
 
     def update_comboboxes(self, device_type, device_id, device):
-        if device == 'None': device = ''
+        if device == 'None':
+            device = ''
         devices = self.devices[device_type]
 
         # for each combobox
@@ -684,7 +666,7 @@ def update_comboboxes(self, device_type, device_id, device):
                 found = True
                 combobox.set_active(i + 1)
 
-        if found == False and device_config['jack'] == False:
+        if found is False and device_config['jack'] is False:
             combobox.set_active(0)
 
     def update_tray_status(self, state):
@@ -697,7 +679,6 @@ def update_tray_status(self, state):
         if self.isserver:
             GLib.idle_add(self.tray.set_status, int(state))
 
-
     def tray_menu(self):
         menu = Gtk.Menu()
 
@@ -724,16 +705,14 @@ def create_indicator(self):
         # Gtk.main()
 
     def open_ui(self, widget):
-        #os.popen('pulsemeeter')
         try:
             self.windowinstance.present()
-        except:
+        except Exception:
             self.windowinstance = self.start_window(self.isserver)
             self.trayonly = False
 
-
     def tray_exit(self, widget):
-        if self.windowinstance != None:
+        if self.windowinstance is not None:
             self.windowinstance.close()
         self.delete_event()
         # maybe TODO: the self.client does not stop listening even with stop listen
@@ -746,12 +725,7 @@ def tray_exit(self, widget):
     def delete_event(self, widget=None, event=None):
         if not self.trayonly:
             self.client.end_subscribe()
-            try:
-                self.subscribe_thread.join()
-            except:
-                # when the application didnt manage to start the app list there is no thread.
-                print('Could not join subscribe_thread (maybe there is none)')
-            if self.enable_vumeters == True:
+            if self.enable_vumeters is True:
                 for i in ['hi', 'vi', 'a', 'b']:
                     for j in self.vu_list[i]:
                         self.vu_list[i][j].close()
diff --git a/pulsemeeter/interface/port_select_popover.py b/pulsemeeter/interface/port_select_popover.py
index 2bb0403..310733a 100644
--- a/pulsemeeter/interface/port_select_popover.py
+++ b/pulsemeeter/interface/port_select_popover.py
@@ -4,7 +4,8 @@
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk
+from gi.repository import Gtk
+
 
 class PortSelectPopover():
     def __init__(self, button, pulse, index):
@@ -32,7 +33,7 @@ def __init__(self, button, pulse, index):
             sys.exit(1)
 
         output = f'{self.index[2][0]}{self.index[2][1]}'
-        jack_ports = f'{output}_jack_map'
+        # jack_ports = f'{output}_jack_map'
         port_group = f'{output}_port_group'
         if port_group not in self.pulse.config[self.index[0]][self.index[1]]:
             self.pulse.config[self.index[0]][self.index[1]][port_group] = True
@@ -56,7 +57,7 @@ def create_port_list(self, default=False):
             ports = self.pulse.config['jack']['output_groups'][channel_group]
         else:
             ports = self.pulse.config['b'][self.index[2][1:]]['channel_map']
-        port_num = len(ports)
+        # port_num = len(ports)
         sink_channel_num = self.pulse.config[self.index[0]][self.index[1]]['channels']
         if self.index[0] != 'hi':
             sink_channel_map = self.pulse.config[self.index[0]][self.index[1]]['channel_map']
@@ -75,13 +76,13 @@ def create_port_list(self, default=False):
         for channel in sink_channel_map:
             hbox = Gtk.HBox(spacing=5)
             label = Gtk.Label(label=channel)
-            label.set_size_request(100,0)
+            label.set_size_request(100, 0)
             hbox.pack_start(label, True, True, 0)
             count = 0
             self.button_list[channel] = {}
             for i in ports:
                 self.button_list[channel][i] = Gtk.CheckButton(label=i)
-                if self.pulse.config[self.index[0]][self.index[1]][port_group] == True or default == True:
+                if self.pulse.config[self.index[0]][self.index[1]][port_group] is True or default is True:
                     if count == sink_channel_map.index(channel):
                         self.button_list[channel][i].set_active(True)
                 else:
@@ -99,10 +100,10 @@ def create_port_list(self, default=False):
 
     def apply(self, widget):
         jack_ports = f'{self.index[2][0]}{self.index[2][1]}_jack_map'
-        if self.toggle_grouping_setting.get_active() == True:
+        if self.toggle_grouping_setting.get_active() is True:
             try:
                 del self.pulse.config[self.index[0]][self.index[1]][jack_ports]
-            except:
+            except Exception:
                 pass
             return
 
@@ -110,14 +111,13 @@ def apply(self, widget):
         for source_port in self.button_list:
             self.pulse.config[self.index[0]][self.index[1]][jack_ports][source_port] = []
             for system_port in self.button_list[source_port]:
-                if self.button_list[source_port][system_port].get_active() == True:
+                if self.button_list[source_port][system_port].get_active() is True:
                     self.pulse.config[self.index[0]][self.index[1]][jack_ports][source_port].append(system_port)
         # self.button_list.clear()
 
-
     def toggle_grouping(self, widget, index, pulse):
         self.pulse.config[index[0]][index[1]][f'{index[2][0]}{index[2][1]}_port_group'] = widget.get_active()
-        if widget.get_active() == False:
+        if widget.get_active() is False:
             self.channel_box.set_sensitive(True)
             self.create_port_list()
         else:
diff --git a/pulsemeeter/interface/rnnoise_popover.py b/pulsemeeter/interface/rnnoise_popover.py
index e4bc0f0..68fd70f 100644
--- a/pulsemeeter/interface/rnnoise_popover.py
+++ b/pulsemeeter/interface/rnnoise_popover.py
@@ -4,7 +4,8 @@
 from gi import require_version as gi_require_version
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk
+from gi.repository import Gtk
+
 
 class RnnoisePopover():
     def __init__(self, button, sock, deivce_type, device_id):
@@ -49,10 +50,9 @@ def apply_button(self, widget, device_id):
         control = int(self.rnnoise_threshold_adjust.get_value())
         # self.device_config['rnnoise_control'] = val
 
-        if self.device_config['use_rnnoise'] == False:
+        if self.device_config['use_rnnoise'] is False:
             return
 
         self.sock.rnnoise(device_id, 'set', control, latency)
         # sink_name = self.device_config['rnnoise_name']
         # print(command)
-
diff --git a/pulsemeeter/interface/vumeter_widget.py b/pulsemeeter/interface/vumeter_widget.py
index c4b205b..4493606 100644
--- a/pulsemeeter/interface/vumeter_widget.py
+++ b/pulsemeeter/interface/vumeter_widget.py
@@ -6,7 +6,8 @@
 # from ..backends import Pulse
 gi_require_version('Gtk', '3.0')
 
-from gi.repository import Gtk,Gdk,Gio,GLib
+from gi.repository import Gtk, GLib
+
 
 class Vumeter(Gtk.ProgressBar):
 
@@ -37,7 +38,7 @@ def listen_peak(self):
         command = ['pulse-vumeter', self.name, dev_type]
         sys.stdout.flush()
         self.process = subprocess.Popen(command,
-            stdout=subprocess.PIPE, 
+            stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             shell=False,
             encoding='utf-8',
@@ -50,20 +51,21 @@ def listen_peak(self):
 
             # if pulse crashes, the vumeter will throw an error
             # but we don't want the ui to crash
-            except:
+            except Exception:
                 print(f'Could not start vumeter for {self.name}')
                 break
 
-            if peak <= 0.00 and self.get_sensitive == True:
+            if peak <= 0.00 and self.get_sensitive is True:
                 GLib.idle_add(self.set_fraction, 0)
                 GLib.idle_add(self.set_sensitive, False)
             else:
                 GLib.idle_add(self.set_sensitive, True)
                 GLib.idle_add(self.set_fraction, peak)
-            
+
         # close connection
         self.process.stdout.close()
-        return_code = self.process.wait()
+        self.process.wait()
+        # return_code = self.process.wait()
 
         # if return_code:
             # raise subprocess.CalledProcessError(return_code, command)
@@ -84,7 +86,7 @@ def start(self):
     def close(self):
         GLib.idle_add(self.set_fraction, 0)
         GLib.idle_add(self.set_sensitive, False)
-        if self.process != None:
+        if self.process is not None:
             self.process.terminate()
             self.thread.join()
         self.process = None
diff --git a/pulsemeeter/settings.py b/pulsemeeter/settings.py
index e36a9f9..5381746 100644
--- a/pulsemeeter/settings.py
+++ b/pulsemeeter/settings.py
@@ -1,16 +1,15 @@
 import os
-import platform
-import appdirs
- 
 
-__version__ = '1.2.9'
+
+__version__ = '1.2.15'
 
 
 HOME = os.getenv('HOME', os.getenv('USERPROFILE'))
 
 USER = os.getenv('USER')
 APP_DIR = os.path.dirname(os.path.realpath(__file__))
-CONFIG_DIR = appdirs.user_config_dir('pulsemeeter')
+XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', f'/home/{USER}/.config')
+CONFIG_DIR = os.path.join(XDG_CONFIG_HOME, 'pulsemeeter')
 CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json')
 ORIG_CONFIG_FILE = os.path.join(APP_DIR, 'config.json')
 LAYOUT_DIR = os.path.join(APP_DIR, 'interface/layouts')
diff --git a/pulsemeeter/socket/client.py b/pulsemeeter/socket/client.py
index 19562eb..0465bad 100755
--- a/pulsemeeter/socket/client.py
+++ b/pulsemeeter/socket/client.py
@@ -1,4 +1,4 @@
-from ..settings import SOCK_FILE
+from ..settings import SOCK_FILE, __version__
 import subprocess
 import socket
 import threading
@@ -8,6 +8,7 @@
 import re
 from queue import SimpleQueue
 
+
 class Client:
 
     def __init__(self, listen=False, noconfig=False):
@@ -20,6 +21,7 @@ def __init__(self, listen=False, noconfig=False):
         self.return_queue = SimpleQueue()
         self.can_listen = listen
         self.noconfig = noconfig
+        self.VERSION = __version__
 
         # connect to server
         try:
@@ -36,17 +38,28 @@ def __init__(self, listen=False, noconfig=False):
 
     # start listen thread
     def start_listen(self, print_event=False):
+        '''
+        starts the listening thread.
+        (starts if listen=True in Client class)
+        '''
         self.stop_listen()
         self.listen_thread = threading.Thread(target=self.listen, args=(print_event,))
         self.listen_thread.start()
 
     # stop listen thread
     def stop_listen(self):
-        if self.listen_thread != None: 
+        '''
+        stops the listening thread if there is one.
+        '''
+        if self.listen_thread is not None:
             self.exit_flag = True
             self.listen_thread.join()
 
     def send_command(self, command, nowait=False):
+        '''
+        Send command manually to server.
+        (only for advanced users)
+        '''
         try:
 
             # encode message ang get it's length
@@ -64,32 +77,36 @@ def send_command(self, command, nowait=False):
             # wait for answer
             ret_msg = ''
             if nowait or not self.can_listen:
-                ret_msg = self.get_message() 
-            else: 
+                ret_msg = self.get_message()
+            else:
                 ret_msg = self.return_queue.get()
 
             return ret_msg
 
-                
-        except:
+        except Exception:
             print('closing socket')
             self.sock.close()
             raise
 
-
     def listen(self, print_event=True):
+        '''
+        Starts to listen to server events. (gets called by start_listen)
+        '''
         while True:
             try:
-                if self.exit_flag == True: break
+                if self.exit_flag is True: break
                 sender_id = self.sock.recv(4)
                 if not sender_id: raise ConnectionError
-                sender_id = int(sender_id)
+                try:
+                    sender_id = int(sender_id)
+                except ValueError:
+                    sender_id = None
 
                 # length of the message
                 msg_len = self.sock.recv(4)
                 if not msg_len: raise ConnectionError
                 msg_len = int(msg_len.decode())
-                
+
                 # get event
                 event = self.sock.recv(msg_len)
                 if not event: raise ConnectionError
@@ -114,21 +131,22 @@ def listen(self, print_event=True):
                 # print('closing socket')
                 # raise
 
-
-
     def get_message(self):
         while True:
             try:
                 # get the id of the client that sent the message
                 sender_id = self.sock.recv(4)
                 if not sender_id: raise
-                sender_id = int(sender_id)
+                try:
+                    sender_id = int(sender_id)
+                except ValueError:
+                    sender_id = None
 
                 # length of the message
                 msg_len = self.sock.recv(4)
                 if not msg_len: raise
                 msg_len = int(msg_len.decode())
-                
+
                 # get event
                 event = self.sock.recv(msg_len)
                 if not event: raise
@@ -138,25 +156,44 @@ def get_message(self):
                 if self.id == sender_id:
                     return event
 
-            except:
+            except Exception:
                 raise
 
-
     # set a callback function to a command
     def set_callback_function(self, command, function):
+        '''
+        Set a callback so Pulsemeeter can call the function you specified if values change.
+
+        available commands:
+        - "connect":        ARGS(input_type, input_id, output_type, output_id, status, latency)
+        - "mute":           ARGS(device_type, device_id, state)
+        - "primary":        ARGS(device_type, device_id)
+        - "rnnoise":        ARGS(input_id, status, control)
+        - "eq":             ARGS(output_type, output_id, status, control)
+        - "volume":         ARGS(device_type, device_id, val)
+        - "primary":        ARGS(device_type, device_id, run_command)
+        - "change-hd":      ARGS(output_type, output_id, name)
+        - "device-new":     ARGS(index, facility)
+        - "device-remove":  ARGS(index, facility)
+        - "exit":           ARGS()
+        '''
         self.callback_dict[command] = function
 
     def handle_callback(self, event):
+        '''
+        Handles calling callback functions.
+        Only for internal use.
+        '''
         command = event.split(' ')
         if command[0] not in self.callback_dict:
             return
-        
+
         function = self.callback_dict[command[0]]
         args = tuple(command[1:])
         function(*args)
 
-    # update the config
     def assert_config(self, event):
+        '''update the config'''
         event = event.split(' ')
         command = event[0]
         args = event[1:]
@@ -191,7 +228,7 @@ def assert_config(self, event):
             device_id = args[1]
             vol = int(args[2])
             self.config[device_type][device_id]['vol'] = vol
-        
+
         elif command == 'rename' or command == 'change-hd':
             device_type = args[0]
             device_id = args[1]
@@ -231,7 +268,6 @@ def assert_config(self, event):
                 control = args[2]
                 self.config['hi'][device_id]['rnnoise_control'] = control
 
-
     def verify_device(self, device_type, device_id, dev):
 
         if dev == 'virtual':
@@ -266,86 +302,113 @@ def verify_device(self, device_type, device_id, dev):
         return True
 
     def connect(self, input_type, input_id, output_type, output_id, state=None, latency=None):
-
-        if (not self.verify_device(input_type, input_id, 'input') 
-                or not self.verify_device(output_type, output_id, 'output')):
+        '''
+        Connect two devices.
+        if state is empty, it gets toggled.
+        latency can be empty.
+
+        input → output
+        '''
+        if (not self.verify_device(input_type, input_id, 'input') or
+                not self.verify_device(output_type, output_id, 'output')):
             return
-           
+
         command = f'connect {input_type} {input_id} {output_type} {output_id}'
-        if state != None: command += f' {state}'
-        if latency != None: command += f' {latency}'
-        
+        if state is not None: command += f' {state}'
+        if latency is not None: command += f' {latency}'
+
         if self.config[input_type][input_id][f'{output_type}{output_id}'] == state:
             return
 
+        print(command)
         return self.send_command(command)
 
-
     def mute(self, device_type, device_id, state=None):
-
+        '''
+        Mute/unmute a device.
+        When the state is empty, it gets toggled.
+        '''
         if not self.verify_device(device_type, device_id, 'any'):
             return
 
         command = f'mute {device_type} {device_id}'
-        if state != None: command += f' {state}'
+        if state is not None: command += f' {state}'
 
         # print('config: ', self.config[device_type][device_id]['mute'], 'new: ', state)
         if self.config[device_type][device_id]['mute'] == state:
             return
         return self.send_command(command)
 
-
     def primary(self, device_type, device_id):
-
+        '''
+        Select a primary device.
+        device_type: virtual devices ("vi", "b")
+        '''
         if not self.verify_device(device_type, device_id, 'any'):
             return
 
         command = f'primary {device_type} {device_id}'
 
-        if self.config[device_type][device_id]['primary'] == True:
+        if self.config[device_type][device_id]['primary'] is True:
             return
         return self.send_command(command)
 
-
     def rnnoise(self, input_id, state=None, control=None, latency=None):
-
+        '''
+        Turn rnnoise on or off and change values.
+        state: boolean or "set"
+        device_type: hardware_input ("hi")
+
+        if state is "set":
+            control: <value>
+            latency (can be empty): <value>
+        '''
         if not input_id.isdigit():
             return 'invalid device index'
 
         command = f'rnnoise {input_id}'
-        if state != None: command += f' {state}'
+        if state is not None: command += f' {state}'
         if control and latency: command += f' {control} {latency}'
 
-        if (self.config['hi'][input_id]['use_rnnoise'] == state
-                or self.config['hi'][input_id]['rnnoise_control'] == control):
+        if (self.config['hi'][input_id]['use_rnnoise'] == state or
+                self.config['hi'][input_id]['rnnoise_control'] == control):
             return
         return self.send_command(command)
 
-
     def eq(self, output_type, output_id, state=None, control=None):
+        '''
+        Turn eq on or off and change values.
+        device_type: output ("a", "b")
+        state: boolean or "set"
+
+        if state is "set":
+            control: "<value1>,<value2>,<value3>,<...>"
+        '''
 
         if not self.verify_device(output_type, output_id, 'output'):
             return
 
         command = f'eq {output_type} {output_id}'
 
-        if control != None and state == 'set':
+        if control is not None and state == 'set':
             command += f' set {control}'
 
-        elif control == None and state != None:
+        elif control is None and state is not None:
             command += f' {state}'
 
-        elif control != None and state != 'set':
+        elif control is not None and state != 'set':
             return
 
-        if (self.config[output_type][output_id]['use_eq'] == state
-                or self.config[output_type][output_id]['eq_control'] == control):
+        if (self.config[output_type][output_id]['use_eq'] == state or
+                self.config[output_type][output_id]['eq_control'] == control):
             return
         return self.send_command(command)
 
-
     def volume(self, device_type, device_id, vol):
-
+        '''
+        change volume of a device
+        vol: +<value> | -<value> | <value>
+        '''
         if not self.verify_device(device_type, device_id, 'any'):
             return
         if type(vol) == str:
@@ -356,12 +419,12 @@ def volume(self, device_type, device_id, vol):
                     return
 
         command = f'volume {device_type} {device_id} {vol}'
+        # print(command)
 
         return self.send_command(command)
 
-
     def rename(self, device_type, device_id, name):
-
+        '''rename a virtual input'''
         if not self.verify_device(device_type, device_id, 'virtual'):
             return
 
@@ -370,9 +433,13 @@ def rename(self, device_type, device_id, name):
             return
         return self.send_command(command)
 
-
     def change_hardware_device(self, device_type, device_id, device):
+        '''
+        Change the device of an input/output.
 
+        Disclaimer:
+        For this you need to specify the pulseaudio device name not the short name shown in the UI.
+        '''
         if device == self.config[device_type][device_id]['name']:
             return
 
@@ -387,28 +454,32 @@ def change_hardware_device(self, device_type, device_id, device):
             return
         return self.send_command(command)
 
-
     def list_hardware_devices(self, device_type):
+        '''
+        returns json of hardware devices.
+        '''
         if device_type not in ['sinks', 'sources']:
             return
 
-        return json.loads(self.send_command(f'get-hd {device_type}'))
-
+        ret = self.send_command(f'get-hd {device_type}')
+        # return is 'empty' if there are no devices
+        if ret != 'empty':
+            return json.loads(ret)
 
     def list_virtual_devices(self, device_type):
         if device_type not in ['sinks', 'sources']:
             return
 
-        ret =self.send_command(f'get-vd {device_type}') 
+        ret = self.send_command(f'get-vd {device_type}')
         return json.loads(ret)
 
     # get sink-input and source-output list
     def list_apps(self, device_type):
         command = f'get-app-list {device_type}'
         try:
-            ret_message = json.loads(self.send_command(command))
-            return ret_message
-        except:
+            ret_message = self.send_command(command)
+            return json.loads(ret_message)
+        except Exception:
             print('invalid json from server')
             return False
             raise
@@ -427,6 +498,9 @@ def set_app_volume(self, app_id, vol, stream_type):
         return self.send_command(command)
 
     def set_layout(self, layout):
+        '''
+        Changes the layout of the UI.
+        '''
         if layout == self.config['layout']:
             return
 
@@ -434,6 +508,9 @@ def set_layout(self, layout):
         return self.send_command(command)
 
     def set_tray(self, state):
+        '''
+        Specifies if the tray should be used.
+        '''
         if type(state) == str:
             state = state.lower() == 'true'
 
@@ -457,27 +534,35 @@ def close_connection(self):
         self.sock.shutdown(socket.SHUT_RDWR)
 
     def close_server(self):
+        '''
+        Closes the Pulsemeeter server (also closes UI).
+        '''
         self.send_command('exit')
 
-
     def subscribe(self):
+        '''
+        Listen to Pulseaudio event.
+
+        Disclaimer:
+        Pulsemeeter already includes a Pulseaudio event listener and you can use the callback functions for that.
+        '''
         command = ['pactl', 'subscribe']
         sys.stdout.flush()
         env = os.environ
         env['LC_ALL'] = 'C'
-        self.sub_proc = subprocess.Popen(command, env=env, 
-            stdout=subprocess.PIPE, 
+        self.sub_proc = subprocess.Popen(command, env=env,
+            stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
             universal_newlines=True)
 
         for stdout_line in iter(self.sub_proc.stdout.readline, ""):
-            yield stdout_line 
-            
+            yield stdout_line
+
         self.sub_proc.stdout.close()
         return_code = self.sub_proc.wait()
         if return_code:
             raise subprocess.CalledProcessError(return_code, command)
 
     def end_subscribe(self):
-        if self.sub_proc != None:
+        if self.sub_proc is not None:
             self.sub_proc.terminate()
diff --git a/pulsemeeter/socket/server.py b/pulsemeeter/socket/server.py
index 0ac3fa4..77a9495 100755
--- a/pulsemeeter/socket/server.py
+++ b/pulsemeeter/socket/server.py
@@ -5,8 +5,12 @@
 import os
 import signal
 import threading
+import codecs
+import traceback
 from queue import SimpleQueue
 
+import asyncio
+
 from ..backends import Pulse
 from ..settings import CONFIG_DIR, CONFIG_FILE, ORIG_CONFIG_FILE, SOCK_FILE, __version__, PIDFILE
 
@@ -15,7 +19,7 @@
 
 
 class Server:
-    def __init__(self):
+    def __init__(self, init_audio_server=True):
 
         # delete socket file if exists
         try:
@@ -32,7 +36,7 @@ def __init__(self):
         audio_server = Pulse
 
         self.config = self.read_config()
-        self.audio_server = audio_server(self.config, loglevel=0)
+        self.audio_server = audio_server(self.config, loglevel=0, init=init_audio_server)
         self.create_command_dict()
 
 
@@ -55,6 +59,9 @@ def __init__(self):
 
     def start_server(self, daemon=False):
 
+        # the asyncio loop
+        self.async_loop = asyncio.get_event_loop()
+
         # Start listener thread
         self.listener_thread = threading.Thread(target=self.query_clients)
         self.listener_thread.start()
@@ -62,6 +69,13 @@ def start_server(self, daemon=False):
         self.main_loop_thread = threading.Thread(target=self.main_loop)
         self.main_loop_thread.start()
 
+        # add the async task to listen to pulseaudio events
+        self.pulse_listener_task = asyncio.run_coroutine_threadsafe(self.pulse_listener(), self.async_loop)
+
+        # run the async code in thread
+        self.pulse_listener_thread = threading.Thread(target=self.async_loop.run_forever, daemon=True)
+        self.pulse_listener_thread.start()
+
         # Register signal handlers
         if daemon:
             signal.signal(signal.SIGINT, self.handle_exit_signal)
@@ -86,50 +100,27 @@ def main_loop(self):
                         del self.event_queues[message[1]]
                     del self.client_handler_connections[message[1]]
                     self.client_handler_threads.pop(message[1]).join()
-                elif message[0] == 'command' or message[0] == 'exit':
 
-                    if message[0] != 'exit':
+                elif message[0] in ['command', 'exit', 'audio_server']:
+
+                    if message[0] == 'command':
                         ret_message, notify_all = self.handle_command(message[2])
                         sender_id = message[1]
                     else:
-                        ret_message = 'exit'
+                        ret_message = message[0] if message[0] == 'exit' else message[2]
                         notify_all = True
-                        sender_id = 9999
+                        sender_id = None
 
+                    # print(ret_message)
                     if ret_message:
-                        encoded_msg = ret_message.encode()
-
-                        # notify observers
-                        if notify_all:
-                            client_list = self.client_handler_connections.values()
-                        elif sender_id in self.client_handler_connections:
-                            client_list = [self.client_handler_connections[message[1]]]
-                        # else:
-                            # continue
-
-                        for conn in client_list:
-
-                            # add 0 until 4 characters long
-                            sender_id = str(sender_id).rjust(4, '0')
-                            msg_len = str(len(encoded_msg)).rjust(4, '0')
-
-                            # send to clients
-                            try:
-                                # print(id, ret_message)
-                                conn.sendall(sender_id.encode()) # sender id
-                                conn.sendall(msg_len.encode()) # message len
-                                conn.sendall(encoded_msg) # command
-                            except OSError:
-                                print(f'client {sender_id} already disconnected, message not sent')
-
-                    if ret_message == 'exit': 
+                        self.send_message(ret_message, message, sender_id, notify_all)
+
+                    if ret_message == 'exit':
                         break
-                                
 
             # TODO: maybe here would be the spot to sent an event signifying that the daemon is shutting down
             # TODO: if we do that, have those client handlers close by themselves instead of shutting down their connections
             # And maybe wait a bit for that to happen
-
             # Close connections and join threads
             print('closing client handler threads...')
             for conn in self.client_handler_connections.values():
@@ -144,13 +135,134 @@ def main_loop(self):
 
         finally:
             # Set the exit flag and wait for the listener thread to timeout
-            print(f'sending exit signal to listener thread, it should exit within {LISTENER_TIMEOUT} seconds...')
+            print(f'sending exit signal to listener threads, they should exit within {LISTENER_TIMEOUT} seconds...')
             self.exit_flag = True
-            self.listener_thread.join()
+            try:
+                self.listener_thread.join()
+            except:
+                print('Could not exit client listener')
+
+            try:
+                self.pulse_listener_task.cancel()
+                self.async_loop.stop()
+                self.pulse_listener_thread.join()
+            except:
+                print('could not exit pulse listener')
+
 
             # Call any code to clean up virtual devices or similar
             self.close_server()
 
+    def to_bytes(self, s):
+        if type(s) is bytes:
+            return s
+        elif type(s) is str or (sys.version_info[0] < 3 and type(s) is unicode):
+            return codecs.encode(s, 'utf-8')
+        else:
+            raise TypeError("Expected bytes or string, but got %s." % type(s))
+
+    def send_message(self, ret_message, message, sender_id, notify_all):
+        encoded_msg = self.to_bytes(ret_message)
+        # encoded_msg = ret_message.encode()
+
+        # notify observers
+        client_list = []
+        if notify_all:
+            client_list = self.client_handler_connections.values()
+        elif sender_id in self.client_handler_connections:
+            client_list = [self.client_handler_connections[message[1]]]
+        # else:
+            # continue
+
+        for conn in client_list:
+
+            # add 0 until 4 characters long
+            sender_id = str(sender_id).rjust(4, '0')
+            msg_len = str(len(encoded_msg)).rjust(4, '0')
+
+            # send to clients
+            try:
+                conn.sendall(sender_id.encode())  # sender id
+                conn.sendall(msg_len.encode())  # message len
+                conn.sendall(encoded_msg)  # command
+            except OSError:
+                print(f'client {sender_id} already disconnected, message not sent')
+
+    # searches for device with name(pulseaudio device) and returns tuple:
+    # - device_type
+    # - device_id
+    def config_device_from_name(self, name):
+        for device_type in ['a', 'b', 'hi', 'vi']:
+            # iterate through all devices (can scale with device count)
+            device_id_range = range(1, len(self.config[device_type])+1)
+            for device_id in device_id_range:
+                device_id = str(device_id)
+                device_config = self.config[device_type][device_id]
+                if device_config["name"] == name:
+                    return {
+                            "device_type": device_type,
+                            "device_id": device_id
+                            }
+        return
+
+    # thanks EnumValue
+    # facility EnumValue to native string
+    def fa_enum_to_string(self, argument):
+        case = {
+            'client',
+            'sink_input',
+            'source_output',
+            'module',
+            'sink',
+            'source'
+        }
+        for case in case:
+            if argument == case: return case
+
+    # handles incoming events from the pulseaudio listener
+    # (updates config if needed and alerts clients)
+    async def pulse_listener_handler(self, event):
+        if event.t == 'change':
+            device = await self.audio_server.device_from_event(event)
+            if device is not None:
+                pulsem_device = self.config_device_from_name(device.name)
+                if pulsem_device is not None:
+                    device_config = self.config[pulsem_device["device_type"]][pulsem_device["device_id"]]
+                    # read the volume data from config and from pulseaudio
+                    config_volume = device_config["vol"]
+                    device_volume = int(round(device.volume.value_flat * 100))
+
+                    # compare config value with pulseaudio value
+                    if config_volume != device_volume:
+                        command = f'volume {pulsem_device["device_type"]} {pulsem_device["device_id"]} {device_volume}'
+                        self.command_queue.put(('audio_server', None, command))
+                        device_config["vol"] = device_volume
+                        return
+
+                    config_mute = device_config["mute"]
+                    device_mute = bool(device.mute)
+
+                    if config_mute != device_mute:
+                        command = f'mute {pulsem_device["device_type"]} {pulsem_device["device_id"]} {device_mute}'
+                        self.command_queue.put(('audio_server', None, command))
+                        device_config["mute"] = device_mute
+                        return
+                    #TODO: Maybe add detection for connection changes
+
+        elif event.t in ['new', 'remove']:
+            index = event.index
+            facility = self.fa_enum_to_string(event.facility)
+            if event.t == 'new':
+                command = f'device-new {index} {facility}'
+            elif event.t == 'remove':
+                command = f'device-remove {index} {facility}'
+            self.command_queue.put(('audio_server', None, command))
+
+    # listener for pulseaudio events
+    async def pulse_listener(self):
+        async with self.audio_server.pulsectl_async as pulse:
+            async for event in pulse.subscribe_events('all'):
+                await self.pulse_listener_handler(event)
 
     def is_running(self):
         try:
@@ -158,9 +270,9 @@ def is_running(self):
                 pid = int(next(f))
 
             # if pid of running instance
-            if os.kill(pid, 0) != False:
+            if os.kill(pid, 0) is not False:
                 return True
-            
+
             # if pid is of a closed instance
             else:
                 # write pid to file
@@ -174,7 +286,6 @@ def is_running(self):
                 f.write(f'{os.getpid()}\n')
             return False
 
-
     # function to register as a signal handler to gracefully exit
     def handle_exit_signal(self, signum=None, frame=None):
         self.command_queue.put(('exit',))
@@ -187,7 +298,7 @@ def close_server(self):
             self.audio_server.cleanup()
         self.closed = True
 
-    # this function handles the connection requests
+    # handles connection requests
     def query_clients(self):
         # loop to get new connections
         with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
@@ -208,7 +319,7 @@ def query_clients(self):
                     event_queue = SimpleQueue()
                     self.client_handler_connections[id] = conn
                     self.event_queues[id] = event_queue
-                    thread = threading.Thread(target=self.listen_to_client, 
+                    thread = threading.Thread(target=self.listen_to_client,
                             args=(conn, event_queue, id), daemon=True)
                     thread.start()
                     self.client_handler_threads[id] = thread
@@ -247,6 +358,7 @@ def listen_to_client(self, conn, event_queue, id):
                     self.client_exit_queue.put(id)
                     break
 
+    # handles incoming commands
     def handle_command(self, data):
         decoded_data = data.decode()
         msg = tuple(decoded_data.split(' '))
@@ -265,59 +377,48 @@ def handle_command(self, data):
         try:
             # verify that command existes
             if command not in self.commands:
-                raise Exception('ERROR command not found')
+                raise Exception(f'[ERROR] command \'{command}\' not found')
 
             if not re.match(self.commands[command]['regex'], str_args):
-                raise Exception('ERROR invalid arguments')
+                raise Exception('[ERROR] invalid arguments')
 
             function = self.commands[command]['function']
             notify = self.commands[command]['notify']
             ret_msg = function(*args)
 
             if not ret_msg:
-                raise Exception('ERROR internal error')
+                raise Exception('[ERROR] internal error')
 
             return (ret_msg, notify,)
 
         except TypeError:
-            return ('ERROR invalid number of arguments', False)
+            return ('[ERROR] invalid number of arguments', False)
 
         except Exception as ex:
             return (str(ex), False)
 
     def read_config(self):
-        # if config exists XDG_CONFIG_HOME 
+        # if config exists XDG_CONFIG_HOME
         if os.path.isfile(CONFIG_FILE):
             try:
                 config = json.load(open(CONFIG_FILE))
-            except:
-                print('ERROR loading config file')
+            except Exception:
+                print('[ERROR] loading config file')
                 sys.exit(1)
 
             # if config is outdated it will try to add missing keys
-            if not 'version' in config or config['version'] != __version__:
+            if 'version' not in config or config['version'] != __version__:
                 config_orig = json.load(open(ORIG_CONFIG_FILE))
                 config['version'] = __version__
 
-                if 'layout' not in config: config['layout'] = 'default'
-
-                if 'cleanup' not in config: config['cleanup'] = False
-
-                if 'tray' not in config: config['tray'] = True
-
-                if 'enable_vumeters' not in config:
-                    config['enable_vumeters'] = True
-
-                if 'jack' not in config:
-                    config['jack'] = {}
-                for i in config_orig['jack']:
-                    if i not in config['jack']:
-                        config['jack'][i] = config_orig['jack'][i]
+                for key in config_orig:
+                    if key not in config:
+                        config[key] = config_orig[key]
 
                 for i in ['a', 'b', 'vi', 'hi']:
                     for j in config_orig[i]:
                         for k in config_orig[i][j]:
-                            if not k in config[i][j]:
+                            if k not in config[i][j]:
                                 config[i][j][k] = config_orig[i][j][k]
                 self.save_config(config)
         else:
@@ -328,7 +429,7 @@ def read_config(self):
         return config
 
     def get_config(self, args=None):
-        if args == None:
+        if args is None:
             return json.dumps(self.config, ensure_ascii=False)
         else:
             args = args.split(':')
@@ -342,9 +443,8 @@ def get_config(self, args=None):
             else:
                 return json.dumps(config, ensure_ascii=False)
 
-
     def save_config(self, config=None):
-        if config == None: config = self.config
+        if config is None: config = self.config
         if not os.path.isdir(CONFIG_DIR):
             os.mkdir(CONFIG_DIR)
         with open(CONFIG_FILE, 'w') as outfile:
@@ -361,17 +461,17 @@ def create_command_dict(self):
 
         # some useful regex
         state = '(True|False|1|0|on|off|true|false)'
-        eq_control = '(\d(\.\d)?)(,\d(\.\d)?){14}'
+        # eq_control = r'([0-9](\.[0-9])?)(,[0-9](\.[0-9])?){14}'
 
-        ## None means that is an optional argument
-        ## STATE == [ [connect|true|on|1] | [disconnect|false|off|0] ]
+        # None means that is an optional argument
+        # STATE == [ [connect|true|on|1] | [disconnect|false|off|0] ]
         self.commands = {
             # ARGS: [hi|vi] id [a|b] id [None|STATE]
             # None = toggle
             'connect': {
-                'function': self.audio_server.connect, 
+                'function': self.audio_server.connect,
                 'notify': True,
-                'regex': f'(vi|hi) [1-9]+( (a|b) [1-9]+)?( ({state}))?( \d+)?$'
+                'regex': f'(vi|hi) [0-9]+( (a|b) [0-9]+)?( ({state}))?( [0-9]+)?$'
             },
 
             # ARGS: [hi|vi|a|b] [None|STATE]
@@ -379,14 +479,14 @@ def create_command_dict(self):
             'mute': {
                 'function': self.audio_server.mute,
                 'notify': True,
-                'regex': f'(vi|hi|a|b) [1-9]+( ({state}))?$'
+                'regex': f'(vi|hi|a|b) [0-9]+( ({state}))?$'
             },
 
             # ARGS: [hi|vi|a|b]
             'primary': {
                 'function': self.audio_server.set_primary,
                 'notify': True,
-                'regex': f'(vi|hi|a|b) [1-9]+$'
+                'regex': r'(vi|hi|a|b) [0-9]+$'
             },
 
             # ARGS: id
@@ -394,16 +494,16 @@ def create_command_dict(self):
             'rnnoise': {
                 'function': self.audio_server.rnnoise,
                 'notify': True,
-                'regex': f'\d+( ({state}|(set \d+ \d+)))?$'
+                'regex': f'[0-9]+( ({state}|(set [0-9]+ [0-9]+)))?$'
             },
 
             # ARGS: [a|b] id [None|STATE|set] [None|control]
             # 'set' is for saving a new control value, if used you HAVE to pass
-            # control, you can ommit the second and third args to toggle 
+            # control, you can ommit the second and third args to toggle
             'eq': {
                 'function': self.audio_server.eq,
                 'notify': True,
-                'regex': f'(a|b) \d+( ({state}|(set (\d+(\.\d+)?)(,\d+(\.\d+)?){{14}})))?$'
+                'regex': r'(a|b) [0-9]+( ((True|False|1|0|on|off|true|false)|(set ([0-9]+(\.[0-9]+)?)(,[0-9]+(\.[0-9]+)?){{14}})))?$'
             },
 
             ''
@@ -413,7 +513,7 @@ def create_command_dict(self):
             'toggle-hd': {
                 'function': self.audio_server.toggle_hardware_device,
                 'notify': False,
-                'regex': f'(hi|a) \d+( {state})?$'
+                'regex': f'(hi|a) [0-9]+( {state})?$'
             },
 
             # ARGS: [vi|b] id STATE
@@ -422,24 +522,24 @@ def create_command_dict(self):
             'toggle-vd': {
                 'function': self.audio_server.toggle_virtual_device,
                 'notify': False,
-                'regex': f'(vi|b) \d+( {state})?$'
+                'regex': f'(vi|b) [0-9]+( {state})?$'
             },
 
             # ARGS: [hi|vi] id
             # wont affect config
             # 'reconnect': {
-                # 'function': self.audio_server.reconnect,
-                # 'notify': False,
-                # 'regex': ''
+            # 'function': self.audio_server.reconnect,
+            # 'notify': False,
+            # 'regex': ''
             # },
-            
+
 
             # ARGS: [a|hi] id NEW_DEVICE
             # NEW_DEVICE is the name of the device
             'change_hd': {
                 'function': self.audio_server.change_hardware_device,
                 'notify': True,
-                'regex': '(a|hi) \d+ \w([\w\.-]+)?$'
+                'regex': r'(a|hi) [0-9]+ \w([\w\.-]+)?$'
             },
 
             # ARGS: [hi|vi|a|b] id vol
@@ -448,7 +548,7 @@ def create_command_dict(self):
             'volume': {
                 'function': self.audio_server.volume,
                 'notify': True,
-                'regex': '(a|b|hi|vi) \d+ [+-]?\d+$'
+                'regex': '(a|b|hi|vi) [0-9]+ [+-]?[0-9]+$'
             },
 
             # ARGS: id vol [sink-input|source-output]
@@ -456,21 +556,21 @@ def create_command_dict(self):
             'app-volume': {
                 'function': self.audio_server.app_volume,
                 'notify': True,
-                'regex': '\d+ \d+ (sink-input|source-output)$'
+                'regex': '[0-9]+ [0-9]+ (sink-input|source-output)$'
             },
 
             # ARGS: id device [sink-input|source-output]
             'move-app-device': {
                 'function': self.audio_server.move_app_device,
                 'notify': True,
-                'regex': '\d+ \w([\w\.-]+)? (sink-input|source-output)$'
+                'regex': r'[0-9]+ \w([\w\.-]+)? (sink-input|source-output)$'
             },
 
             # ARGS: id [sink-input|source-output]
             'get-stream-volume': {
                 'function': self.audio_server.get_app_stream_volume,
                 'notify': False,
-                'regex': '\d+ (sink-input|source-output)$'
+                'regex': '[0-9]+ (sink-input|source-output)$'
             },
 
             # ARGS: [sink-input|source-output]
@@ -509,28 +609,40 @@ def create_command_dict(self):
                 'notify': True,
                 'regex': f'{state}$'
             },
-            
+
+            'device-new': {
+                'function': self.audio_server.device_new,
+                'notify': True,
+                'regex': ''
+            },
+
+            'device-remove': {
+                'function': self.audio_server.device_remove,
+                'notify': True,
+                'regex': ''
+            },
+
             # not ready
             'get-vd': {
-                    'function': self.audio_server.get_virtual_devices, 
-                    'notify': False, 
-                    'regex': ''
+                'function': self.audio_server.get_virtual_devices,
+                'notify': False,
+                'regex': ''
             },
             'get-hd': {
-                    'function': 
-                    self.audio_server.get_hardware_devices, 
-                    'notify': False, 
-                    'regex': ''
+                'function':
+                self.audio_server.get_hardware_devices,
+                'notify': False,
+                'regex': ''
             },
             'list-apps': {
-                    'function': self.audio_server.get_virtual_devices, 
-                    'notify': False, 
-                    'regex': ''
+                'function': self.audio_server.get_virtual_devices,
+                'notify': False,
+                'regex': ''
             },
             'rename': {
-                    'function': self.audio_server.rename, 
-                    'notify': True, 
-                    'regex': ''
-            }, 
+                'function': self.audio_server.rename,
+                'notify': True,
+                'regex': ''
+            },
 
         }
diff --git a/requirements.txt b/requirements.txt
index e800c34..6d4193f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,3 @@
 setuptools
 pulsectl
 pygobject
-appdirs
diff --git a/setup.py b/setup.py
index 833db97..dda2c8d 100644
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,17 @@
 import os
-import sys
 import pathlib
 from setuptools import setup
 
-try:
-    import pulsemeeter
-except ImportError as ex:
-    print(f'ERROR: {ex.name} not installed, please run \'pip install requirements.txt\' inside source directory')
+with open('pulsemeeter/settings.py') as f:
+    for line in f:
+        if line.startswith('__version__'):
+            __version__ = line.replace("'", '').split()[2]
+            break
 
 
-VERSION = pulsemeeter.__version__
+VERSION = __version__
 README = (pathlib.Path(__file__).parent / "README.md").read_text()
-DATA_FILES = [('share/licenses/pulsemeeter/', ['LICENSE']),]
+DATA_FILES = [('share/licenses/pulsemeeter/', ['LICENSE']), ]
 REQUIREMENTS = []
 
 with open('requirements.txt') as file:
@@ -39,7 +39,7 @@
         "License :: OSI Approved :: MIT License",
     ],
     url='https://github.com/theRealCarneiro/pulsemeeter',
-    packages=['pulsemeeter'],
+    packages=['pulsemeeter', 'pulsemeeter.interface', 'pulsemeeter.socket', 'pulsemeeter.backends'],
     install_requires=REQUIREMENTS,
     data_files=DATA_FILES,
     entry_points={


Test patch:
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
new file mode 100644
index 0000000..c2fa9a9
--- /dev/null
+++ b/.github/workflows/build-test.yml
@@ -0,0 +1,29 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Build Test
+
+on: workflow_dispatch
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      run: python -m pip install --upgrade pip build
+    - name: Build package
+      run: |
+        python -m build
+        ls dist/*